本文详细介绍了如何使用 Sway 编程语言构建一个去中心化的英语拍卖合约,包含了拍卖的基本原理、安装工具的步骤、合约的结构与实现细节。通过代码示例,读者能够学习如何创建拍卖、出价、取消拍卖及提取资产,并且文中还强调了对数据结构、错误处理和事件的管理。最后,文章还提到了如何为拍卖应用程序实施测试,确保代码的正常运作。
在 Three Sigma,我们认识到 Web3 领域的复杂机遇。我们团队的专家驻扎在里斯本,提供开发、安全和经济建模方面的顶级服务,以推动你的项目成功。无论你需要精确的代码审计、先进的经济建模还是全面的区块链工程,Three Sigma 都是你值得信赖的合作伙伴。
在这个示例应用中,我将向你展示如何使用 Sway 创建一个英式拍卖合约,并在 Harness 上实现一些测试。
大多数人可能已经知道什么是英式拍卖,但对于那些不知道的,这里有一个基本的解释:
英式拍卖是一种拍卖形式,卖方以初始价格和保留价格提供资产。用户随后在拍卖期间对资产进行出价,直到竞标期结束或达到保留价格。完成后,用户将根据结果提取其原始押金或新购买的资产。
英式拍卖应用以去中心化的方式实现了这一理念,消除了第三方的需求,并提供了强有力的结算保证。
对于懒惰的人,这里是 GitHub 仓库 的完整代码。但我知道你渴望学习且自律,因此让我们逐步深入这个示例应用。
要安装 Fuel 工具链,你可以使用 fuelup-init
脚本。这将安装 forc
、forc-client
、forc-fmt
、forc-lsp
、forc-wallet
以及 fuel-core
到 ~/.fuelup/bin
。
在终端中运行以下脚本。
curl https://install.fuel.network | sh
请注意,目前 Windows 不支持原生使用。如果你希望在 Windows 上使用 fuelup,请使用 Windows Subsystem for Linux。
如果你的机器上没有安装 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
:包含合约中使用的数据结构,如 Auction
和 State
。
events
:定义合约将发射的事件,如 BidEvent
、CancelAuctionEvent
、CreateAuctionEvent
和 WithdrawEvent
。
interface
:定义合约的接口(或 ABI
),如 EnglishAuction
和 Info
。
// 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
) 导入所需的项:
Auction
和 State
来自 data_structures
模块。
各种错误类型(AccessError
、InitError
、InputError
、UserError
)来自 errors
模块。
事件类型(BidEvent
、CancelAuctionEvent
、CreateAuctionEvent
、WithdrawEvent
)来自 events
模块。
接口特征(EnglishAuction
、Info
)来自 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。
为了遵循最佳实践,我们将创建一个 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,
}
现在我们将转到 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
函数以初始化测试环境,并解构返回的元组以提取 seller
、buyer1
、sell_asset
和 buy_asset
。类似地,异步调用 defaults
函数,并解构返回的元组以提取 sell_amount
、initial_price
、reserve_price
和 duration
。
注意: 下划线
_
表示将在此测试中忽略的元组中的值。
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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!