使用 Sway 构建去中心化英式拍卖

本文详细介绍了如何使用 Sway 编程语言构建一个去中心化的英语拍卖合约,包含了拍卖的基本原理、安装工具的步骤、合约的结构与实现细节。通过代码示例,读者能够学习如何创建拍卖、出价、取消拍卖及提取资产,并且文中还强调了对数据结构、错误处理和事件的管理。最后,文章还提到了如何为拍卖应用程序实施测试,确保代码的正常运作。

"使用 Sway 构建去中心化的英式拍卖-Three Sigma" 横幅

在 Three Sigma,我们认识到 Web3 领域的复杂机遇。我们团队的专家驻扎在里斯本,提供开发、安全和经济建模方面的顶级服务,以推动你的项目成功。无论你需要精确的代码审计、先进的经济建模还是全面的区块链工程,Three Sigma 都是你值得信赖的合作伙伴。

介绍

在这个示例应用中,我将向你展示如何使用 Sway 创建一个英式拍卖合约,并在 Harness 上实现一些测试。

大多数人可能已经知道什么是英式拍卖,但对于那些不知道的,这里有一个基本的解释:

英式拍卖是一种拍卖形式,卖方以初始价格和保留价格提供资产。用户随后在拍卖期间对资产进行出价,直到竞标期结束或达到保留价格。完成后,用户将根据结果提取其原始押金或新购买的资产。

英式拍卖应用以去中心化的方式实现了这一理念,消除了第三方的需求,并提供了强有力的结算保证。

对于懒惰的人,这里是 GitHub 仓库 的完整代码。但我知道你渴望学习且自律,因此让我们逐步深入这个示例应用。

安装

运行 fuelup-init

要安装 Fuel 工具链,你可以使用 fuelup-init 脚本。这将安装 forcforc-clientforc-fmtforc-lspforc-wallet 以及 fuel-core~/.fuelup/bin

在终端中运行以下脚本。

curl https://install.fuel.network | sh

请注意,目前 Windows 不支持原生使用。如果你希望在 Windows 上使用 fuelup,请使用 Windows Subsystem for Linux。

安装 Rust

如果你的机器上没有安装 Rust,请在你的 shell 运行以下命令;这将下载并运行 rustup-init.sh,它将下载并运行适合你平台的 rustup-init 可执行文件的正确版本。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

编写合约

在开始编写合约之前,看看它的结构以便更好地理解它。

英式拍卖合约的结构

让我们开始创建一个名为 english-auction 的新空文件夹,导航至该文件夹,并使用 forc 创建合约项目:

mkdir english-auction
cd english-auction
forc new auction-contract

在代码编辑器中打开你的项目,并删除 src/main.sw 中的所有内容,除了第一行。

每个 Sway 文件必须以声明文件包含的程序类型开始;在这里,我们声明该文件是一个合约。有关 Sway 程序类型的更多信息,请参见 Sway Book

contract;

模块和导入

接下来,我们将声明我们将使用的模块和导入项。

mod errors;
mod data_structures;
mod events;
mod interface;

errors:包含合约将使用的自定义错误类型。

data_structures:包含合约中使用的数据结构,如 AuctionState

events:定义合约将发射的事件,如 BidEventCancelAuctionEventCreateAuctionEventWithdrawEvent

interface:定义合约的接口(或 ABI),如 EnglishAuctionInfo

// use ::data_structures::{auction::Auction, state::State};
use ::data_structures::auction::Auction;
use ::data_structures::state::State;
use ::errors::{AccessError, InitError, InputError, UserError};
use ::events::{BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent};
use ::interface::{EnglishAuction, Info};
use std::{
    asset::transfer,
    block::height,
    call_frames::msg_asset_id,
    context::msg_amount,
    hash::Hash,
};

这些行从模块和标准库 (std) 导入所需的项:

AuctionState 来自 data_structures 模块。

各种错误类型(AccessErrorInitErrorInputErrorUserError)来自 errors 模块。

事件类型(BidEventCancelAuctionEventCreateAuctionEventWithdrawEvent)来自 events 模块。

接口特征(EnglishAuctionInfo)来自 interface 模块。

标准库工具,如 transfer(用于转移资产)、height(获取当前区块高度)、msg_asset_id(获取正在转移的资产的 ID)、msg_amount(获取正在转移的资产的数量)和 Hash

存储声明

storage {
    auctions: StorageMap<u64, Auction> = StorageMap {},
    total_auctions: u64 = 0,
}

该块定义了合约的存储布局:

auctions:一个 StorageMap,将拍卖 ID (u64) 映射到 Auction 对象。所有拍卖数据存储在这里。

total_auctions:一个 u64,用于跟踪创建的总拍卖数量。每次创建新的拍卖时都会递增。

扩展机制的常量

const EXTENSION_THRESHOLD: u32 = 5;
const EXTENSION_DURATION: u32 = 5;

这些常量定义了拍卖扩展机制的行为:

EXTENSION_THRESHOLD:拍卖结束前的区块数,其中新的出价将触发扩展。例如,如果设置为 5,则在拍卖最后 5 个区块内进行的任何出价都会延长拍卖。

EXTENSION_DURATION:如果在扩展阈值内提出出价,拍卖将延长的区块数。例如,如果设置为 5,则每次进行符合条件的出价时,拍卖将延长 5 个区块。

在设置合约的结构和导入之后,定义模块并导入所需的类型和函数,同时设置拍卖机制的存储和常量后,我们的合约应该如下所示:

contract;

mod errors;
mod data_structures;
mod events;
mod interface;

// use ::data_structures::{auction::Auction, state::State};
use ::data_structures::auction::Auction;
use ::data_structures::state::State;
use ::errors::{AccessError, InitError, InputError, UserError};
use ::events::{BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent};
use ::interface::{EnglishAuction, Info};
use std::{
    asset::transfer,
    block::height,
    call_frames::msg_asset_id,
    context::msg_amount,
    hash::Hash,
};

storage {
    auctions: StorageMap<u64, Auction> = StorageMap {},
    total_auctions: u64 = 0,
}

const EXTENSION_THRESHOLD: u32 = 5;
const EXTENSION_DURATION: u32 = 5;

ABI

ABI 意味着应用程序二进制接口。ABI 定义合约的接口。合约必须定义或导入 ABI 声明。

最佳实践是将你的 ABI 定义在单独的库中并将其导入到合约中。这使得合约调用者可以更轻松地导入和使用 ABI。

为了遵循最佳实践,我们将创建一个 interface.sw 文件,在相同的 src 文件夹中定义 ABI:

将此内容粘贴到新创建的 interface.sw 文件中:

library;

use ::data_structures::auction::Auction;

abi EnglishAuction {
    /// 在指定拍卖上出价。
    ///
    /// # 参数
    ///
    /// * `auction_id`: [u64] - 拍卖的 ID。
    ///
    /// # 回退
    ///
    /// * 当 `auction_id` 不映射到现有拍卖时。
    /// * 当拍卖已经结束时。
    /// * 当拍卖的竞标期已经过去时。
    /// * 当提供的资产不匹配拍卖接受的资产时。
    /// * 当竞标者是拍卖的卖方时。
    /// * 当将 NFT 资产转移到拍卖合约失败时。
    /// * 当发送的本土资产数量和 `bid_asset` 枚举不匹配时。
    /// * 当发送的本土资产类型和 `bid_asset` 枚举不匹配时。
    /// * 当出价数量低于初始价格时。
    /// * 当竞标者的总押金不大于当前出价时。
    /// * 当竞标者的总押金大于保留价格时。
    #[payable]
    #[storage(read, write)]
    fn bid(auction_id: u64);

    /// 取消指定拍卖。
    ///
    /// # 参数
    ///
    /// * `auction_id`: [u64] - 拍卖的 `u64` ID。
    ///
    /// # 回退
    ///
    /// * 当 `auction_id` 不映射到现有拍卖时。
    /// * 当拍卖不再开放时。
    /// * 当发送者不是拍卖的卖方时。
    #[storage(read, write)]
    fn cancel(auction_id: u64);

    /// 启动一个拍卖,卖方、待售资产、接受的出价资产、初始价格、
    /// 可能的保留价格,以及拍卖的持续时间。
    ///
    /// 此函数将返回新创建的拍卖的 ID,用于与拍卖进行交互。
    ///
    /// # 参数
    ///
    /// `bid_asset`: [AssetId] - 卖方愿意接受的作为出售资产的资产。
    /// `duration`: [u32] - 拍卖应该开放的持续时间。
    /// `initial_price`: [u64] - 拍卖的起始价格。
    /// `reserve_price`: [Option<u64>] - 买家可以直接购买 `sell_asset` 的价格。
    /// `seller`: [Identity] - 此拍卖的卖方。
    ///
    /// # 返回
    ///
    /// * [u64] - 新创建拍卖的 ID。
    ///
    /// # 回退
    ///
    /// * 当 `reserve_price` 小于 `initial_price` 且设置了保留价格时。
    /// * 当拍卖的 `duration` 设置为零时。
    /// * 当 `bid_asset` 数量不为零时。
    /// * 当币的 `initial_price` 设置为零时。
    /// * 当发送的本土资产数量和 `sell_asset` 枚举不匹配时。
    /// * 当发送的本土资产类型和 `sell_asset` 枚举不匹配时。
    /// * 当 NFT 的 `initial_price` 不为一时。
    /// * 当将 NFT 资产转移到合约失败时。
    #[payable]
    #[storage(read, write)]
    fn create(
        bid_asset: AssetId,
        duration: u32,
        initial_price: u64,
        reserve_price: Option<u64>,
        seller: Identity,
    ) -> u64;

    /// 允许用户提取他们应得的资产,如果拍卖的出价期结束,
    /// 保留价格已达到,或拍卖被取消。
    ///
    /// # 附加信息
    ///
    /// 1. 如果发送者是赢家,他们将提取出售资产。
    /// 2. 如果发送者的出价未赢得拍卖,他们的总押金将被提取。
    /// 3. 如果发送者是卖方,且没有出价,或拍卖被取消
    ///   ,他们将提取出售资产。
    /// 4. 如果发送者是卖方,且已有出价,他们将提取赢家的总押金。
    ///
    /// # 参数
    ///
    /// * `auction_id`: [u64] - 拍卖的 ID。
    ///
    /// # 回退
    ///
    /// * 当提供的 `auction_id` 不映射到现有拍卖时。
    /// * 当拍卖的出价期尚未结束时。
    /// * 当拍卖的 `state` 仍处于开放竞标状态时。
    /// * 当发送者已提取其存款时。
    #[storage(read, write)]
    fn withdraw(auction_id: u64);
}

abi Info {
    /// 返回对应拍卖 ID 的拍卖结构。
    ///
    /// # 参数
    ///
    /// * `auction_id`: [u64] - 拍卖的 ID。
    ///
    /// # 返回
    ///
    /// * [Option<Auction>] - 对应拍卖 ID 的拍卖结构。
    #[storage(read)]
    fn auction_info(auction_id: u64) -> Option<Auction>;

    /// 返回用户在指定拍卖中存入的资产余额。
    ///
    /// # 附加信息
    ///
    /// 此金额将代表竞标者的出价资产数量,以及
    /// 卖方的出售资产。
    ///
    /// # 参数
    ///
    /// * `auction_id`: [u64] - 拍卖的 ID。
    /// * `identity`: [Identity] - 存入资产的用户。
    ///
    /// # 返回
    ///
    /// * [Option<u64>] - 用户为该拍卖存入的资产数量。
    #[storage(read)]
    fn deposit_balance(auction_id: u64, identity: Identity) -> Option<u64>;

    /// 返回使用此拍卖合约启动的总拍卖数量。
    ///
    /// # 返回
    ///
    /// * [u64] - 拍卖的总数量。
    #[storage(read)]
    fn total_auctions() -> u64;
}

在跳入实现 ABI 的主合约之前,让我们首先创建数据结构、错误和事件:

数据结构

src 文件夹中,创建一个 data_structures 文件夹。

mkdir data_structures

创建两个文件:一个 auction.sw 文件,用于实现拍卖自定义库逻辑,一个 state.sw 文件,用于实现状态自定义库逻辑。

拍卖库

首先,我们声明这个文件是一个库,意味着它包含可在程序其他部分包含的可重用代码。接下来,我们从 data_structures 模块中导入 State 数据结构,这将用于表示拍卖的状态(开放或关闭)。

library;

use ::data_structures::state::State;

现在我们将声明拍卖结构体,定义拍卖的属性。

pub struct Auction {
    pub bid_asset: AssetId,
    pub end_block: u32,
    pub highest_bid: u64,
    pub highest_bidder: Option<Identity>,
    pub initial_price: u64,
    pub reserve_price: Option<u64>,
    pub sell_asset: AssetId,
    pub sell_asset_amount: u64,
    pub seller: Identity,
    pub state: State,
    pub deposits: StorageMap<Identity, u64> = StorageMap {},
}

bid_asset:拍卖中将被接受的支付资产。

end_block:拍卖结束时的区块号。

highest_bid:当前最高出价金额。

highest_bidder:当前最高出价者的身份。这是可选的,因为最初可能没有最高出价者。

initial_price:拍卖的起始价格。

reserve_price:保留价格,即资产可以被直接购买的价格。这是可选的。

sell_asset:正在拍卖中出售的资产。

sell_asset_amount:待拍卖资产的数量。

seller:卖方的身份。

state:拍卖的状态,指示它是开放还是关闭。

deposits:存储用户存入的押金的映射。它将用户身份映射到他们存入的金额。

还有 new 函数,用于创建一个具有指定细节的新拍卖:

impl Auction {
    pub fn new(
        bid_asset: AssetId,
        end_block: u32,
        initial_price: u64,
        reserve_price: Option<u64>,
        sell_asset: AssetId,
        sell_asset_amount: u64,
        seller: Identity,
    ) -> Self {
        Auction {
            bid_asset,
            end_block,
            highest_bid: 0,
            highest_bidder: Option::None,
            initial_price,
            reserve_price,
            sell_asset,
            sell_asset_amount,
            seller,
            state: State::Open,
            deposits: StorageMap::new(),
        }
    }
}

auction.sw 库应该如下所示:

library;

use ::data_structures::state::State;

pub struct Auction {
    pub bid_asset: AssetId,
    pub end_block: u32,
    pub highest_bid: u64,
    pub highest_bidder: Option<Identity>,
    pub initial_price: u64,
    pub reserve_price: Option<u64>,
    pub sell_asset: AssetId,
    pub sell_asset_amount: u64,
    pub seller: Identity,
    pub state: State,
    pub deposits: StorageMap<Identity, u64> = StorageMap {},
}

impl Auction {
    pub fn new(
        bid_asset: AssetId,
        end_block: u32,
        initial_price: u64,
        reserve_price: Option<u64>,
        sell_asset: AssetId,
        sell_asset_amount: u64,
        seller: Identity,
    ) -> Self {
        Auction {
            bid_asset,
            end_block,
            highest_bid: 0,
            highest_bidder: Option::None,
            initial_price,
            reserve_price,
            sell_asset,
            sell_asset_amount,
            seller,
            state: State::Open,
            deposits: StorageMap::new(),
        }
    }
}

从我们停下来的地方继续,让我们创建 State 库,该库将定义拍卖的可能状态,并提供比较这些状态的功能。

同样声明这个文件是一个库。接下来,我们定义 State 枚举,表示拍卖的状态。枚举是一种可以表示多个命名值的类型,称为变体。

library;

pub enum State {
    Closed: (),
    Open: (),
}

Closed:表示拍卖不再接受出价的状态。

Open:表示在拍卖中可以进行出价的状态。

为使不同状态之间的比较成为可能,我们为 State 枚举实现 Eq trait。Eq trait 允许我们检查两个状态是否相等。

impl core::ops::Eq for State {
    fn eq(self, other: Self) -> bool {
        match (self, other) {
            (State::Open, State::Open) => true,
            (State::Closed, State::Closed) => true,
            _ => false,
        }
    }
}

Eq trait 用于定义相等性比较。在这个实现中,我们检查两个状态是否相同。

如果两个状态都是 Open,则认为它们是相等的。

如果两个状态都是 Closed,则认为它们是相等的。

否则,它们是不相等的。

完整状态库:

library;

pub enum State {
    Closed: (),
    Open: (),
}

impl core::ops::Eq for State {
    fn eq(self, other: Self) -> bool {
        match (self, other) {
            (State::Open, State::Open) => true,
            (State::Closed, State::Closed) => true,
            _ => false,
        }
    }
}

最后,在 src 文件夹中创建一个 data_structures.sw 文件。此文件将包含状态和拍卖的模块声明,从而将所有内容聚集在一起。每个模块封装相关的功能,使代码更易于维护、理解和重用。

library;

pub mod state;
pub mod auction;

错误

错误比较简单,我们只需在 src 文件夹中创建一个 errors.sw 文件,并用我们将在合约中使用的错误填充它。

library;

/// 与权限相关的错误。
pub enum AccessError {
    /// 拍卖尚未结束。
    AuctionIsNotClosed: (),
    /// 拍卖尚未开放。
    AuctionIsNotOpen: (),
    /// 发送者不是拍卖卖方。
    SenderIsNotSeller: (),
}

/// 与拍卖初始化相关的错误。
pub enum InitError {
    /// 拍卖持续时间未提供。
    AuctionDurationNotProvided: (),
    /// 初始价格不能为零。
    InitialPriceCannotBeZero: (),
    /// 保留价格不能低于初始价格。
    ReserveLessThanInitialPrice: (),
}

/// 与输入参数相关的错误。
pub enum InputError {
    /// 请求的拍卖不存在。
    AuctionDoesNotExist: (),
    /// 拍卖的初始价格未满足。
    InitialPriceNotMet: (),
    /// 提供的资产数量不正确。
    IncorrectAmountProvided: (),
    /// 提供的资产不正确。
    IncorrectAssetProvided: (),
}

/// 用户错误。
pub enum UserError {
    /// 卖方不能在自己的拍卖上出价。
    BidderIsSeller: (),
    /// 用户已提取其应得的资产。
    UserHasAlreadyWithdrawn: (),
}

事件

与错误一样,事件也很简单,我们需要在 src 文件夹中创建一个 events.sw 文件,并用我们将在合约中使用的事件填充它。

library;

use ::data_structures::auction::Auction;

/// 当拍卖被取消时的事件。
pub struct CancelAuctionEvent {
    /// 被取消的拍卖的拍卖 ID。
    pub auction_id: u64,
}

/// 当拍卖被创建时的事件。
pub struct CreateAuctionEvent {
    /// 创建的拍卖的拍卖 ID。
    pub auction_id: u64,
    /// 将接收出价的资产。
    pub bid_asset: AssetId,
    /// 将要出售的资产。
    pub sell_asset: AssetId,
    /// 正在出售的资产数量。
    pub sell_asset_amount: u64,
}

/// 当出价被提出时的事件。
pub struct BidEvent {
    /// 出价的金额。
    pub amount: u64,
    /// 被出价的拍卖的拍卖 ID。
    pub auction_id: u64,
    /// 竞标者。
    pub user: Identity,
}

/// 当资产被提取时的事件。
pub struct WithdrawEvent {
    /// 被提取的资产。
    pub asset: AssetId,
    /// 提取的资产金额。
    pub asset_amount: u64,
    /// 被提取的拍卖的拍卖 ID。
    pub auction_id: u64,
    /// 提取资产的用户。
    pub user: Identity,
}

实现 ABI

现在我们将转到 main.sw 文件,并开始编写在 ABI 中定义的函数的实现。

但首先请不要忘记将 ABI 实现到你的合约中:

impl EnglishAuction for Contract {}

bid() 函数

现在,让我们从 bid() 函数开始,该函数允许用户在拍卖中出价。

默认情况下,合约可能无法在合约调用中接收原生资产。为了允许向合约转移资产,我们将为函数添加 #[payable] 属性。由于我们想要读取或写入存储,我们还需要添加 #[storage(read, write)] 属性。

#[payable]
#[storage(read, write)]

由于我们希望用户能够指定对哪个拍卖出价,我们应该添加一个参数 auction_id 类型为 u64,这是出价的拍卖的 ID。

fn bid(auction_id: u64) {}

首先,我们需要从存储中检索具有 specified auction_id 的拍卖。这很重要,因为我们需要了解拍卖的详细信息才可以出价。接着检查拍卖是否存在。如果不存在,就抛出错误以确保用户无法对不存在的拍卖出价。如果拍卖存在,我们将其解包以获取实际的拍卖数据。unwrap() 函数从 Option 中提取值,假设它为 Some

let auction = storage.auctions.get(auction_id).try_read();
require(auction.is_some(), InputError::AuctionDoesNotExist);
let mut auction = auction.unwrap();

接下来,我们需要检索发送者的地址和出价信息:出价资产和出价金额。这很重要,因为我们需要知道谁在出价以及他们所用的是什么。

let sender = msg_sender().unwrap();
let bid_asset = msg_asset_id();
let bid_amount = msg_amount();

我们需要确保发送者不是拍卖的卖方,以防止他们在自己的拍卖中出价。这维护了拍卖过程的完整性。我们还检查拍卖是否开放以及拍卖结束区块是否尚未到达。这确保出价只能在活跃的拍卖中进行。接着,我们验证出价资产是否与拍卖所需的资产匹配。这确保用户用正确类型的资产进行出价。

require(sender != auction.seller, UserError::BidderIsSeller);
require(
        auction.state == State::Open && auction.end_block >= height(),
        AccessError::AuctionIsNotOpen,
);
require(bid_asset == auction.bid_asset, InputError::IncorrectAssetProvided);

然后,我们合并用户的先前押金和当前出价,以获取用户对拍卖的总押金。这很重要,以便跟踪每个用户的总出价。

let total_bid = match auction.deposits.get(&sender) {
        Some(sender_deposit) => bid_amount + sender_deposit,
        None => bid_amount,
};

什么是 match?与 if 表达式不同,match 表达式在 编译时 断言所有可能的模式都有匹配。如果你没有处理所有模式,将会收到编译器错误,指示你的 match 表达式是非穷尽的。

&sender 的含义是什么& 符号用于创建对一个值的引用。引用允许你在不拥有该值的情况下借用该值,这有助于在不移动或复制实际数据的情况下查找数据结构中的值。

之后我们确保总出价达到或超过拍卖的初始价格,并且高于当前最高出价。这确保每个新出价实际上是比之前的出价要更高的。

require(total_bid >= auction.initial_price, InputError::InitialPriceNotMet);
require(total_bid > auction.highest_bid, InputError::IncorrectAmountProvided);

如果设置了保留价格,我们检查总出价是否满足或超过了保留价格。保留价格是卖方愿意接受的最低金额。

if auction.reserve_price.is_some() {
        let reserve_price = auction.reserve_price.unwrap();
        require(reserve_price >= total_bid, InputError::IncorrectAmountProvided);

        if reserve_price == total_bid {
            auction.state = State::Closed;
        }
    }

我们随后检查出价是否在延长阈值内。如果是,我们将延长拍卖持续时间,以便其他竞标者有机会作出响应。这防止了最后一刻的“抢标”,即有人在拍卖结束前出价,而没有给其他人机会反击。

if auction.end_block - height() <= EXTENSION_THRESHOLD {
        auction.end_block += EXTENSION_DURATION;
}

接着,我们更新拍卖的信息并存储新的状态。这包括设置新的最高出价者,更新最高出价以及存储用户的总出价。

auction.highest_bidder = Option::Some(sender);
auction.highest_bid = total_bid;
auction.deposits.insert(sender, total_bid);
storage.auctions.insert(auction_id, auction);

最后,我们记录出价事件,包括最高出价的金额、拍卖 ID 和出价用户。记录对于透明度和记录保存来说很重要。

log(BidEvent {
        amount: auction.highest_bid,
        auction_id: auction_id,
        user: sender,
    });
}

总之,bid 函数允许用户在拍卖中出价。它确保拍卖存在,验证出价细节,并相应地更新拍卖状态。该函数还处理保留价格和拍卖延长,以防止最后一刻的抢标。在更新拍卖信息之后,它记录出价事件以保持透明。

如果每一步都按正确的步骤执行,现在你的 main.sw 文件应该如下所示:

contract;

mod errors;
mod data_structures;
mod events;
mod interface;

// use ::data_structures::{auction::Auction, state::State};
use ::data_structures::auction::Auction;
use ::data_structures::state::State;
use ::errors::{AccessError, InitError, InputError, UserError};
use ::events::{BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent};
use ::interface::{EnglishAuction, Info};
use std::{
    asset::transfer,
    block::height,
    call_frames::msg_asset_id,
    context::msg_amount,
    hash::Hash,
};

storage {
    auctions: StorageMap<u64, Auction> = StorageMap {},
    total_auctions: u64 = 0,
}

const EXTENSION_THRESHOLD: u32 = 5;
const EXTENSION_DURATION: u32 = 5;

impl EnglishAuction for Contract {
#[payable]
#[storage(read, write)]
fn bid(auction_id: u64) {

    let auction = storage.auctions.get(auction_id).try_read();
    require(auction.is_some(), InputError::AuctionDoesNotExist);

    let mut auction = auction.unwrap();

    let sender = msg_sender().unwrap();
    let bid_asset = msg_asset_id();
    let bid_amount = msg_amount();

    require(sender != auction.seller, UserError::BidderIsSeller);
    require(
        auction.state == State::Open && auction.end_block >= height(),
        AccessError::AuctionIsNotOpen,
    );
    require(bid_asset == auction.bid_asset, InputError::IncorrectAssetProvided);

    let total_bid = match auction.deposits.get(&sender) {
        Some(sender_deposit) => bid_amount + sender_deposit,
        None => bid_amount,
    };

    require(total_bid >= auction.initial_price, InputError::InitialPriceNotMet);
    require(total_bid > auction.highest_bid, InputError::IncorrectAmountProvided);

    if auction.reserve_price.is_some() {
        let reserve_price = auction.reserve_price.unwrap();
        require(reserve_price >= total_bid, InputError::IncorrectAmountProvided);

        if reserve_price == total_bid {
            auction.state = State::Closed;
        }
    }

    if auction.end_block - height() <= EXTENSION_THRESHOLD {
        auction.end_block += EXTENSION_DURATION;
    }

    auction.highest_bidder = Option::Some(sender);
    auction.highest_bid = total_bid;
    auction.deposits.insert(sender, total_bid);
    storage.auctions.insert(auction_id, auction);

    log(BidEvent {
        amount: auction.highest_bid,
        auction_id: auction_id,
        user: sender,
    });
}
}

create() 函数

接下来是 create 函数,该函数允许用户创建一个新的拍卖。

同样,我们将添加 #[payable]#[storage(read, write)] 属性以允许将资产转移到合约,并启用对存储的读写。

#[payable]
#[storage(read, write)]

由于我们希望用户能够指定拍卖的详细信息,我们应该为出价资产(bid_asset,类型为 AssetId)、拍卖的持续时间(duration,类型为 u32)、初始价格(initial_price,类型为 u64)、保留价格(reserve_price,类型为 Option<u64>)和卖方(seller,类型为 Identity)添加参数。

fn create(
    bid_asset: AssetId,
    duration: u32,
    initial_price: u64,
    reserve_price: Option<u64>,
    seller: Identity,
) -> u64 {}

首先,我们需要确保保留价格(如果提供)大于初始价格。

require(
        reserve_price.is_none() || (reserve_price.is_some() && reserve_price.unwrap() > initial_price),
        InitError::ReserveLessThanInitialPrice,
    );

我们还需要确保拍卖的持续时间和初始价格不为零。

require(duration != 0, InitError::AuctionDurationNotProvided);
require(initial_price != 0, InitError::InitialPriceCannotBeZero);

接下来,我们从消息上下文中检索待售资产及其数量。这很重要,因为我们需要知道正在拍卖的资产是什么及其数量,并确保出售资产的数量不为零。这防止创建零资产的拍卖。

let sell_asset = msg_asset_id();
let sell_asset_amount = msg_amount();

require(sell_asset_amount != 0, InputError::IncorrectAmountProvided);

``在这里,我们从在/data_structures/auction.sw` 中定义的 Auction 结构体创建一个新的拍卖,然后使用提供的详细信息设置拍卖。这包括出价资产、拍卖结束区块、初始价格、保留价格、待售资产、待售资产的数量以及卖家。

1let auction = Auction::new(
2        bid_asset,
3        duration + height(),
4        initial_price,
5        reserve_price,
6        sell_asset,
7        sell_asset_amount,
8        seller,
9    );

接下来,我们将拍卖信息存储在存储中。我们首先读取当前拍卖的总数。然后,我们将新拍卖插入存储映射中,使用当前总数作为键,并将总拍卖数增加 1 并写回存储。这可以跟踪已经创建了多少个拍卖。

1let total_auctions = storage.total_auctions.read();
2storage.auctions.insert(total_auctions, auction);
3storage.total_auctions.write(total_auctions + 1);

最后,我们记录创建拍卖事件,其中包括拍卖 ID、出价资产、出售资产和出售资产的数量。记录日志对于透明性和记录保存很重要。

1log(CreateAuctionEvent {
2        auction_id: total_auctions,
3        bid_asset,
4        sell_asset,
5        sell_asset_amount,
6    });
7
8    total_auctions
9}

总之,create 函数允许用户创建新拍卖。它确保提供的详细信息是有效的,使用给定的参数设置拍卖、存储拍卖信息并记录事件以确保透明性。

现在你的 main.sw 应该看起来像这样:

1contract;
2
3mod errors;
4mod data_structures;
5mod events;
6mod interface;
7
8// use ::data_structures::{auction::Auction, state::State};
9use ::data_structures::auction::Auction;
10use ::data_structures::state::State;
11use ::errors::{AccessError, InitError, InputError, UserError};
12use ::events::{BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent};
13use ::interface::{EnglishAuction, Info};
14use std::{
15    asset::transfer,
16    block::height,
17    call_frames::msg_asset_id,
18    context::msg_amount,
19    hash::Hash,
20};
21
22storage {
23    auctions: StorageMap<u64, Auction> = StorageMap {},
24    total_auctions: u64 = 0,
25}
26
27const EXTENSION_THRESHOLD: u32 = 5;
28const EXTENSION_DURATION: u32 = 5;
29
30impl EnglishAuction for Contract {
31
32#[payable]
33#[storage(read, write)]
34fn bid(auction_id: u64) {
35
36    let auction = storage.auctions.get(auction_id).try_read();
37    require(auction.is_some(), InputError::AuctionDoesNotExist);
38
39    let mut auction = auction.unwrap();
40
41    let sender = msg_sender().unwrap();
42    let bid_asset = msg_asset_id();
43    let bid_amount = msg_amount();
44
45    require(sender != auction.seller, UserError::BidderIsSeller);
46    require(
47        auction.state == State::Open && auction.end_block >= height(),
48        AccessError::AuctionIsNotOpen,
49    );
50    require(bid_asset == auction.bid_asset, InputError::IncorrectAssetProvided);
51
52    let total_bid = match auction.deposits.get(&sender) {
53        Some(sender_deposit) => bid_amount + sender_deposit,
54        None => bid_amount,
55    };
56
57    require(total_bid >= auction.initial_price, InputError::InitialPriceNotMet);
58    require(total_bid > auction.highest_bid, InputError::IncorrectAmountProvided);
59
60    if auction.reserve_price.is_some() {
61        let reserve_price = auction.reserve_price.unwrap();
62        require(reserve_price >= total_bid, InputError::IncorrectAmountProvided);
63
64        if reserve_price == total_bid {
65            auction.state = State::Closed;
66        }
67    }
68
69    if auction.end_block - height() <= EXTENSION_THRESHOLD {
70        auction.end_block += EXTENSION_DURATION;
71    }
72
73    auction.highest_bidder = Option::Some(sender);
74    auction.highest_bid = total_bid;
75    auction.deposits.insert(sender, total_bid);
76    storage.auctions.insert(auction_id, auction);
77
78    log(BidEvent {
79        amount: auction.highest_bid,
80        auction_id: auction_id,
81        user: sender,
82    });
83}
84
85#[payable]
86#[storage(read, write)]
87fn create(
88    bid_asset: AssetId,
89    duration: u32,
90    initial_price: u64,
91    reserve_price: Option<u64>,
92    seller: Identity,
93) -> u64 {
94    require(
95        reserve_price.is_none() || (reserve_price.is_some() && reserve_price.unwrap() > initial_price),
96        InitError::ReserveLessThanInitialPrice,
97    );
98    require(duration != 0, InitError::AuctionDurationNotProvided);
99    require(initial_price != 0, InitError::InitialPriceCannotBeZero);
100
101    let sell_asset = msg_asset_id();
102    let sell_asset_amount = msg_amount();
103    require(sell_asset_amount != 0, InputError::IncorrectAmountProvided);
104
105    let auction = Auction::new(
106        bid_asset,
107        duration + height(),
108        initial_price,
109        reserve_price,
110        sell_asset,
111        sell_asset_amount,
112        seller,
113    );
114
115    let total_auctions = storage.total_auctions.read();
116    storage.auctions.insert(total_auctions, auction);
117    storage.total_auctions.write(total_auctions + 1);
118
119    log(CreateAuctionEvent {
120        auction_id: total_auctions,
121        bid_asset,
122        sell_asset,
123        sell_asset_amount,
124    });
125
126    total_auctions
127}
128
129#[storage(read, write)]
130fn cancel(auction_id: u64) {
131    let auction = storage.auctions.get(auction_id).try_read();
132    require(auction.is_some(), InputError::AuctionDoesNotExist);
133
134    let mut auction = auction.unwrap();
135
136    require(
137        auction.state == State::Open && auction.end_block >= height(),
138        AccessError::AuctionIsNotOpen,
139    );
140
141    require(
142        msg_sender().unwrap() == auction.seller,
143        AccessError::SenderIsNotSeller,
144    );
145
146    auction.highest_bidder = Option::None;
147    auction.state = State::Closed;
148    storage.auctions.insert(auction_id, auction);
149
150    log(CancelAuctionEvent { auction_id });
151}
152
153#[storage(read, write)]
154fn withdraw(auction_id: u64) {
155    let auction = storage.auctions.get(auction_id).try_read();
156    require(auction.is_some(), InputError::AuctionDoesNotExist);
157
158    let mut auction = auction.unwrap();
159    require(
160        auction.state == State::Closed || auction.end_block <= height(),
161        AccessError::AuctionIsNotClosed,
162    );
163
164    if auction.end_block <= height() && auction.state == State::Open {
165        auction.state = State::Closed;
166        storage.auctions.insert(auction_id, auction);
167    }
168
169    let sender = msg_sender().unwrap();
170    let bidder = auction.highest_bidder;
171    let sender_deposit = auction.deposits.get(&sender);
172
173    require(sender_deposit.is_some(), UserError::UserHasAlreadyWithdrawn);
174    auction.deposits.remove(&sender);
175    let mut withdrawn_amount = *sender_deposit.unwrap();
176    let mut withdrawn_asset = auction.bid_asset;
177
178    if (bidder.is_some() && sender == bidder.unwrap()) || (bidder.is_none() && sender == auction.seller) {
179        transfer(sender, auction.sell_asset, auction.sell_asset_amount);
180        withdrawn_asset = auction.sell_asset;
181        withdrawn_amount = auction.sell_asset_amount;
182    } else if sender == auction.seller {
183        transfer(sender, auction.bid_asset, auction.highest_bid);
184        withdrawn_amount = auction.highest_bid;
185    } else {
186        transfer(sender, withdrawn_asset, withdrawn_amount);
187    }
188
189    log(WithdrawEvent {
190        asset: withdrawn_asset,
191        asset_amount: withdrawn_amount,
192        auction_id,
193        user: sender,
194    });
195}
196}

cancel() 函数

create 函数之后,我们有 cancel 函数,允许用户取消拍卖。

我们将为该函数添加 #[storage(read, write)] 属性,以便能够读取和写入存储。

1#[storage(read, write)]

由于我们希望用户指定他们想取消的拍卖,因此添加一个参数 auction_id,类型为 u64,这是正在取消的拍卖的 ID。

1fn cancel(auction_id: u64) {}

首先,我们需要从存储中检索带有指定 auction_id 的拍卖。然后,我们检查拍卖是否存在。如果不存在,则抛出错误。这确保用户无法取消不存在的拍卖,如果拍卖存在,则将其展开以获得实际的拍卖数据。unwrap() 函数从 Option 中提取值,假设它是 Some

1let auction = storage.auctions.get(auction_id).try_read();
2require(auction.is_some(), InputError::AuctionDoesNotExist);
3let mut auction = auction.unwrap();

接下来,我们确保拍卖仍然开放且未结束,并且发送者是拍卖的卖家。这防止未经授权的用户取消拍卖。

1require(
2    auction.state == State::Open && auction.end_block >= height(),
3    AccessError::AuctionIsNotOpen,
4);
5require(
6    msg_sender().unwrap() == auction.seller,
7    AccessError::SenderIsNotSeller,
8);

然后,我们更新拍卖的信息以反映取消。首先,我们将最高出价者重置为 None,并将拍卖状态更改为 Closed,之后我们将更新后的拍卖保存回存储,以确保更改得到持久化。最后,我们记录取消拍卖事件,其中包括拍卖 ID。

1auction.highest_bidder = Option::None;
2auction.state = State::Closed;
3storage.auctions.insert(auction_id, auction);
4
5log(CancelAuctionEvent { auction_id });

现在合约包括 cancel 函数,这使用户能够取消拍卖,确保拍卖存在,验证用户的取消权限,按需更新拍卖状态,并记录事件。

这里是你的 main.sw 在更改后的样子:

1contract;
2
3mod errors;
4mod data_structures;
5mod events;
6mod interface;
7
8// use ::data_structures::{auction::Auction, state::State};
9use ::data_structures::auction::Auction;
10use ::data_structures::state::State;
11use ::errors::{AccessError, InitError, InputError, UserError};
12use ::events::{BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent};
13use ::interface::{EnglishAuction, Info};
14use std::{
15    asset::transfer,
16    block::height,
17    call_frames::msg_asset_id,
18    context::msg_amount,
19    hash::Hash,
20};
21
22storage {
23    auctions: StorageMap<u64, Auction> = StorageMap {},
24    total_auctions: u64 = 0,
25}
26
27const EXTENSION_THRESHOLD: u32 = 5;
28const EXTENSION_DURATION: u32 = 5;
29
30impl EnglishAuction for Contract {
31
32#[payable]
33#[storage(read, write)]
34fn bid(auction_id: u64) {
35
36    let auction = storage.auctions.get(auction_id).try_read();
37    require(auction.is_some(), InputError::AuctionDoesNotExist);
38
39    let mut auction = auction.unwrap();
40
41    let sender = msg_sender().unwrap();
42    let bid_asset = msg_asset_id();
43    let bid_amount = msg_amount();
44
45    require(sender != auction.seller, UserError::BidderIsSeller);
46    require(
47        auction.state == State::Open && auction.end_block >= height(),
48        AccessError::AuctionIsNotOpen,
49    );
50    require(bid_asset == auction.bid_asset, InputError::IncorrectAssetProvided);
51
52    let total_bid = match auction.deposits.get(&sender) {
53        Some(sender_deposit) => bid_amount + sender_deposit,
54        None => bid_amount,
55    };
56
57    require(total_bid >= auction.initial_price, InputError::InitialPriceNotMet);
58    require(total_bid > auction.highest_bid, InputError::IncorrectAmountProvided);
59
60    if auction.reserve_price.is_some() {
61        let reserve_price = auction.reserve_price.unwrap();
62        require(reserve_price >= total_bid, InputError::IncorrectAmountProvided);
63
64        if reserve_price == total_bid {
65            auction.state = State::Closed;
66        }
67    }
68
69    if auction.end_block - height() <= EXTENSION_THRESHOLD {
70        auction.end_block += EXTENSION_DURATION;
71    }
72
73    auction.highest_bidder = Option::Some(sender);
74    auction.highest_bid = total_bid;
75    auction.deposits.insert(sender, total_bid);
76    storage.auctions.insert(auction_id, auction);
77
78    log(BidEvent {
79        amount: auction.highest_bid,
80        auction_id: auction_id,
81        user: sender,
82    });
83}
84
85#[payable]
86#[storage(read, write)]
87fn create(
88    bid_asset: AssetId,
89    duration: u32,
90    initial_price: u64,
91    reserve_price: Option<u64>,
92    seller: Identity,
93) -> u64 {
94    require(
95        reserve_price.is_none() || (reserve_price.is_some() && reserve_price.unwrap() > initial_price),
96        InitError::ReserveLessThanInitialPrice,
97    );
98    require(duration != 0, InitError::AuctionDurationNotProvided);
99    require(initial_price != 0, InitError::InitialPriceCannotBeZero);
100
101    let sell_asset = msg_asset_id();
102    let sell_asset_amount = msg_amount();
103    require(sell_asset_amount != 0, InputError::IncorrectAmountProvided);
104
105    let auction = Auction::new(
106        bid_asset,
107        duration + height(),
108        initial_price,
109        reserve_price,
110        sell_asset,
111        sell_asset_amount,
112        seller,
113    );
114
115    let total_auctions = storage.total_auctions.read();
116    storage.auctions.insert(total_auctions, auction);
117    storage.total_auctions.write(total_auctions + 1);
118
119    log(CreateAuctionEvent {
120        auction_id: total_auctions,
121        bid_asset,
122        sell_asset,
123        sell_asset_amount,
124    });
125
126    total_auctions
127}
128
129#[storage(read, write)]
130fn cancel(auction_id: u64) {
131    let auction = storage.auctions.get(auction_id).try_read();
132    require(auction.is_some(), InputError::AuctionDoesNotExist);
133
134    let mut auction = auction.unwrap();
135
136    require(
137        auction.state == State::Open && auction.end_block >= height(),
138        AccessError::AuctionIsNotOpen,
139    );
140
141    require(
142        msg_sender().unwrap() == auction.seller,
143        AccessError::SenderIsNotSeller,
144    );
145
146    auction.highest_bidder = Option::None;
147    auction.state = State::Closed;
148    storage.auctions.insert(auction_id, auction);
149
150    log(CancelAuctionEvent { auction_id });
151}
152
153#[storage(read, write)]
154fn withdraw(auction_id: u64) {
155    let auction = storage.auctions.get(auction_id).try_read();
156    require(auction.is_some(), InputError::AuctionDoesNotExist);
157
158    let mut auction = auction.unwrap();
159    require(
160        auction.state == State::Closed || auction.end_block <= height(),
161        AccessError::AuctionIsNotClosed,
162    );
163
164    if auction.end_block <= height() && auction.state == State::Open {
165        auction.state = State::Closed;
166        storage.auctions.insert(auction_id, auction);
167    }
168

169    let sender = msg_sender().unwrap();
170    let bidder = auction.highest_bidder;
171    let sender_deposit = auction.deposits.get(&sender);
172

173    require(sender_deposit.is_some(), UserError::UserHasAlreadyWithdrawn);
174    auction.deposits.remove(&sender);
175    let mut withdrawn_amount = *sender_deposit.unwrap();
176    let mut withdrawn_asset = auction.bid_asset;
177

178    if (bidder.is_some() && sender == bidder.unwrap()) || (bidder.is_none() && sender == auction.seller) {
179        transfer(sender, auction.sell_asset, auction.sell_asset_amount);
180        withdrawn_asset = auction.sell_asset;
181        withdrawn_amount = auction.sell_asset_amount;
182    } else if sender == auction.seller {
183        transfer(sender, auction.bid_asset, auction.highest_bid);
184        withdrawn_amount = auction.highest_bid;
185    } else {
186        transfer(sender, withdrawn_asset, withdrawn_amount);
187    }
188

189    log(WithdrawEvent {
190        asset: withdrawn_asset,
191        asset_amount: withdrawn_amount,
192        auction_id,
193        user: sender,
194    });
195}
196}

只读方法

首先,我们将从 interface.sw 文件中实现 Info 接口。它提供只读方法来查询有关拍卖的信息。

如同名称所示,只读方法 是仅从区块链读取数据而不做任何修改的函数。

让我们分解此实现中的每个函数。

auction_info() 函数

auction_info 函数检索特定拍卖的 信息。

开始时我们将添加 #[storage(read)] 属性,指定该函数将从合约的存储中读取。

1#[storage(read)]

该函数接受一个拍卖 ID 作为参数,并返回一个 Option<Auction>,如果拍卖存在,将包含拍卖详细信息。

1fn auction_info(auction_id: u64) -> Option<Auction> {
2        storage.auctions.get(auction_id).try_read()
3    }

deposit_balance() 函数

deposit_balance 函数检索用户在特定拍卖中的存款余额。

同样在开始时添加 #[storage(read)] 属性,指定该函数将从合约的存储中读取。

1#[storage(read)]

该函数接受拍卖 ID 和用户身份作为参数,返回一个 Option<u64>,如果存在,将包含用户的存款余额。

1fn deposit_balance(auction_id: u64, identity: Identity) -> Option<u64> {}

首先,我们尝试从存储中检索带有指定 auction_id 的拍卖。接下来,我们使用 match 表达式来处理检索结果。

1let auction = storage.auctions.get(auction_id).try_read();
2
3match auction {
4    Some(auction) => auction.deposits.get(&identity).copied(),
5    None => None,
6}

可能的结果有:

Some(auction):如果拍卖存在,则尝试使用其身份获取用户的存款。

auction.deposits.get(&identity).copied():如果用户有存款,则返回存款金额的副本。

None:如果拍卖不存在,则返回 None

total_auctions() 函数

total_auctions 函数旨在检索已创建的总拍卖数量。

首先,我们添加 #[storage(read)] 属性,表明该函数将从合约的存储中读取。该函数不接受任何参数,返回一个 u64 值,表示总拍卖数量。

1#[storage(read)]
2fn total_auctions() -> u64 {
3        storage.total_auctions.read()
4    }

这些只读函数提供有关拍卖的重要信息,而不会更改合约的状态。以下是完整的实现:

1impl Info for Contract {
2    #[storage(read)]
3    fn auction_info(auction_id: u64) -> Option<Auction> {
4        storage.auctions.get(auction_id).try_read()
5    }
6
7    #[storage(read)]
8    fn deposit_balance(auction_id: u64, identity: Identity) -> Option<u64> {
9        let auction = storage.auctions.get(auction_id).try_read();
10        match auction {
11            Some(auction) => auction.deposits.get(&identity).copied(),
12            None => None,
13        }
14    }
15
16    #[storage(read)]
17    fn total_auctions() -> u64 {
18        storage.total_auctions.read()
19    }
20}

分步测试

现在,英文拍卖应用程序已经开发完成,确保其没有错误非常重要,这需要通过彻底测试。让我们实现一些测试!

Rust 和 Fuel 工具链已在 安装 部分安装完毕。对于测试,请安装 Cargo generate:

在终端中运行以下命令:

1cargo install cargo-generate --locked

接下来,实现一个测试,以验证可以对拍卖进行多次出价。

首先,导入用于创建和出价拍卖所需的模块和函数,以及设置测试环境和默认值的内容。

1use crate::utils::{
2    interface::core::auction::{bid, create},
3    setup::{defaults, setup},
4};
5use fuels::types::Identity;

定义一个名为 success 的新模块,以对成功测试用例进行分组,在此模块中将包括测试。

1mod success {
2    use super::*;
3    use crate::utils::{
4        interface::info::{auction_info, deposit_balance},
5        setup::{Auction, State},
6    };
7}

声明一个异步测试函数,使用 tokio::test,并命名该函数。

1   #[tokio::test]
2    async fn places_multiple_bids() {}

异步调用 setup 函数以初始化测试环境,并解构返回的元组以提取 sellerbuyer1sell_assetbuy_asset。类似地,异步调用 defaults 函数,并解构返回的元组以提取 sell_amountinitial_pricereserve_priceduration

注意: 下划线 _ 表示将在此测试中忽略的元组中的值。

1let (_, seller, buyer1, _, _, sell_asset, buy_asset) = setup().await;
2let (sell_amount, initial_price, reserve_price, duration, _initial_wallet_amount) = defaults().await;

将卖家和买家1的钱包地址转换为 Identity 类型,以确保与拍卖函数兼容。

1let seller_identity = Identity::Address(seller.wallet.address().into());
2let buyer1_identity = Identity::Address(buyer1.wallet.address().into());

异步创建一个拍卖,使用指定的参数,将拍卖 ID 分配给 auction_id

1     let auction_id = create(
2            buy_asset,
3            &seller.auction,
4            duration,
5            initial_price,
6            Some(reserve_price),
7            seller_identity,
8            sell_asset,
9            sell_amount,
10        )
11        .await;

买家1在拍卖中的初始存款余额应为 None(表示尚未存入任何金额),因此断言 buyer1_deposit 为 None。

1let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await;
2assert!(buyer1_deposit.is_none());

买家1以初始价格异步出价,并检查买家1的存款余额等于初始价格,最高出价者为买家1,拍卖状态为开放。

1bid(auction_id, buy_asset, initial_price, &buyer1.auction).await;
2
3let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await.unwrap();
4
5let auction: Auction = auction_info(auction_id, &seller.auction).await.unwrap();
6
7assert_eq!(buyer1_deposit, initial_price);
8assert_eq!(auction.highest_bidder.unwrap(), buyer1_identity);
9assert_eq!(auction.state, State::Open);

以递增 1 单位的方式再次让买家1出价异步。应用相同检查,只不过买家1的存款余额应该等于初始价格加 1。

1bid(auction_id, buy_asset, 1, &buyer1.auction).await;
2
3let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await.unwrap();
4
5let auction: Auction = auction_info(auction_id, &seller.auction).await.unwrap();
6
7assert_eq!(buyer1_deposit, initial_price + 1);
8assert_eq!(auction.highest_bidder.unwrap(), buyer1_identity);
9assert_eq!(auction.state, State::Open);

如果所有步骤执行正确,测试应该如下所示:

1use crate::utils::{
2    interface::core::auction::{bid, create},
3    setup::{defaults, setup},
4};
5use fuels::types::Identity;
6
7mod success {
8    use super::*;
9    use crate::utils::{
10        interface::info::{auction_info, deposit_balance},
11        setup::{Auction, State},
12    };
13
14    #[tokio::test]
15    async fn places_multiple_bids() {
16
17    let (_, seller, buyer1, _, _, sell_asset, buy_asset) = setup().await;
18      let (sell_amount, initial_price, reserve_price, duration, _initial_wallet_amount) = defaults().await;
19
20    let seller_identity = Identity::Address(seller.wallet.address().into());
21      let buyer1_identity = Identity::Address(buyer1.wallet.address().into());
22
23    let auction_id = create(
24            buy_asset,
25            &seller.auction,
26            duration,
27            initial_price,
28            Some(reserve_price),
29            seller_identity,
30            sell_asset,
31            sell_amount,
32        ).await;
33
34    let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await;
35      assert!(buyer1_deposit.is_none());
36
37      bid(auction_id, buy_asset, initial_price, &buyer1.auction).await;
38
39      let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await.unwrap();
40
41      let auction: Auction = auction_info(auction_id, &seller.auction).await.unwrap();
42
43      assert_eq!(buyer1_deposit, initial_price);
44      assert_eq!(auction.highest_bidder.unwrap(), buyer1_identity);
45      assert_eq!(auction.state, State::Open);
46
47      bid(auction_id, buy_asset, 1, &buyer1.auction).await;
48
49      let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await.unwrap();
50
51      let auction: Auction = auction_info(auction_id, &seller.auction).await.unwrap();
52
53      assert_eq!(buyer1_deposit, initial_price + 1);
54      assert_eq!(auction.highest_bidder.unwrap(), buyer1_identity);
55      assert_eq!(auction.state, State::Open);
56    }
57}

现在轮到你实现更多的测试了!

参考 repo 的测试文件夹

结论

总之,Fuel 和 Sway 编程语言提供了一个功能强大且用户友好的平台,用于在以太坊上构建高性能的去中心化应用程序。借助 Fuel 的高级功能和 Sway 的现代、区块链优化语法,你可以轻松创建安全且高效的智能合约。

英文拍卖示例提供了有关如何开发、部署和测试 Fuel 上的去中心化应用程序的实际视角。这一经历表明,Fuel 的独特能力,如 FuelVM 和模块化架构,使其成为希望突破以太坊可行性局限的开发者们的激动之选。

随着你继续探索和构建 Fuel 和 Sway,你将进入一个快速发展的生态系统,旨在使去中心化经济更加易于访问和高效。无论你是经验丰富的开发人员还是刚入门,Fuel 都提供了实现 Web3 想法所需的工具。参考文献: Fuel, Fuel 文档

联系我们的 Three Sigma,让我们经验丰富的专业团队自信地引导你穿越 Web3 领域。凭借我们在智能合约安全、经济模型和区块链工程方面的专业知识,我们将帮助你保障项目的未来。

今天就 联系我们,将你的 Web3愿景变为现实!

  • 原文链接: threesigma.xyz/blog/buil...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Three Sigma
Three Sigma
Three Sigma is a blockchain engineering and auditing firm focused on improving Web3 by working closely with projects in the space.