第 5 课:在智能合约中实现拍卖功能

  • GearFans
  • 发布于 2023-06-06 14:44
  • 阅读 5985

本课提供了在 Tamagotchi 拍卖合约中实施英式拍卖模型的分步指南,包括自动程序执行,拍卖功能所需的合同状态和逻辑。

课程总结

  • 本课提供了在 Tamagotchi 拍卖合约中实施英式拍卖模型的分步指南。

  • 我们将介绍自动程序执行以促进英式拍卖模型

  • 我们将突出显示拍卖功能所需的合同状态和逻辑

  • 期待拍卖功能解决方案的动手开发

课程目标

在本课结束时,你将收获:

  • 通过智能合约自动化理解自动程序执行

  • 理解实现拍卖功能时的智能合约状态和逻辑

  • 了解系统信号、gas 预留以及如何将 gas 预留添加到系统信号

  • 将拍卖模型纳入你的智能合约

让我们开始吧!

在本课中,我们将创建一个合约,用户可以在其中拍卖他们的 Tamagotchi。我们实施英式拍卖模型吧。

这种类型的拍卖以电子宠物所有者设定的最低出价声明开始。在此之后,有兴趣的投标人开始按升序排列他们的投标,即下一个投标应高于前一个。这个过程一直持续到出价高于该价格时其他买家都没有兴趣购买 Tamagotchi。最高出价是 Tamagotchi 的售价。

正如你可能猜到的那样,你的 Tamagotchi 约应该扩展为可以更改所有权的功能(这正是上一课的作业)。

拍卖期间,拍卖合约暂时成为 Tamagotchi 的主人。拍卖结束后,合约会指定 Tamagotchi 的新主人 —— 拍卖的赢家。如果没有人出价,Tamagotchi 将退还给之前的所有者。

通过智能合约自动化自动执行程序

在我们开始编写拍卖智能合约之前,我们将讨论智能合约自动化。

智能合约无法自动执行。在链上交易触发之前,他们的代码不会在区块链上运行和更改状态。外部交易作为“戳”来唤醒智能合约并启动其逻辑。例如,我们可以通过向拍卖合约发送消息来开始拍卖。

当拍卖时间过去后,需要处理拍卖结果。但是,在有人向合约发送适当的消息之前,不会处理结果。

在 Gear 中,我们通过延迟消息解决了这个问题。

msg::send_delayed(program, payload, value, delay)
msg::send_bytes_delayed(program, payload, value, delay)

延迟消息在指定的延迟后执行。在我们的场景中这很方便:我们可以通过向拍卖合约发送消息来开始拍卖。完成所有必要的逻辑后,拍卖合约将向自己发送一条延迟消息,它将在指定时间后结算拍卖。

因此,发送延迟消息的能力允许你自动执行合约。只要有足够的 gas 来执行,合约就可以自动执行无限数量的区块。但如果 gas 用完,执行可能会中断。

Gear Protocol 允许另一个强大的功能 —— gas 预留。开发人员可以保留可用于发送常规或延迟消息的 gas。

要保留 gas 量以供进一步使用,请使用以下功能:

let reservation_id = ReservationId::reserve(RESERVATION_AMOUNT, TIME)
                                   .expect("reservation across executions");

该函数从该程序的可用量中获取一定量的 gas 并将其保留。保留获得一个程序使用的唯一标识符,以获取此保留 gas 并在以后使用它。

你还必须指明必须使用储备的区块数。Gas 预留不是免费的:一个区块的预留需要 100 gas。reserve 函数返回 ReservationId,用于发送带有该 gas 的消息。要使用保留的 gas 发送消息:

msg::send_from_reservation(reservation_id, program, payload, value)
                               .expect("Failed to send message from reservation");

如果在预留期限内不需要 gas,可以取消预留,gas 将退还给预留用户。

id.unreserve().expect("unreservation across executions");

理解合约状态和逻辑

让我们开始编写拍卖合约。首先我们定义合约状态的结构:

  • Tamagotchi_id - 当前拍卖的 Tamagotchi 合约的地址;

  • status - 拍卖状态(可以是 ReadyToStart 或 InProcess);

  • current_bid - 当前最高出价;

  • current_bidder - 当前出价最高的参与者的地址;

  • ended_at - 拍卖结束时间;

  • prev_tmg_owner - Tamagotchi 的前任所有者(拍卖合约必须存储此帐户,以防万一没有人参加拍卖并且 Tamagotchi 必须归还给前任所有者)。

static mut AUCTION: Option<Auction> = None;

#[derive(Default)]
pub struct Auction {
   Tamagotchi_id: TamagotchiId,
   status: Status,
   current_bid: u128,
   current_bidder: ActorId,
   ft_contract_id: ActorId,
   transaction_id: TransactionId,
   ended_at: u64,
   prev_tmg_owner: ActorId,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Status {
   ReadyToStart,
   InProcess,
}

impl Default for Status {
   fn default() -> Self {
       Self::ReadyToStart
   }
}

让我们定义合约将接收的消息:

  • StartAuction {Tamagotchi_id, minimum_bid, duration} - 启动拍卖开始的消息。Tamagotchi 所有者必须注明 Tamagotchi 合约的地址、起拍价和拍卖持续时间;

  • MakeBid { bid } - 来自拍卖参与者的消息,他们必须表明他们准备为 Tamagotchi 支付的价格(出价);

  • SettleAuction - 合约在拍卖结束后收到的消息。如果有出价,那么拍卖合约会将 Tamagotchi 分配给拍卖获胜者。否则,拍卖合约会将 Tamagotchi 转让给之前的所有者。

pub type TamagotchiId = ActorId;
pub type Bid = u128;

#[derive(Encode, Decode)]
pub enum AuctionAction {
   StartAuction {
       Tamagotchi_id: TamagotchiId,
       minimum_bid: Bid,
       duration: u64,
   },
   MakeBid {
       bid: Bid,
   },
   SettleAuction,
}

你可以猜到,拍卖合约将与同质化代币合约以及 Tamagotchi 合约进行交互。也就是说,它会向这些合约发送消息并等待回复。因此,交易不会是原子性的,我们必须考虑这一点以保持 3 个合约的状态一致。

让我们详细看看合约的每个动作。StartAuction 必须将 Tamagotchi 的所有者更改为拍卖合约并设置拍卖参数。

我们考虑所有可能导致状态不一致的情况。拍卖合约向 Tamagotchi 合约发送消息,可能出现以下情况:

  1. Tamagotchi 合约在消息执行期间因缺乏 gas 或逻辑错误而失败。它没有保存状态,因此拍卖和 Tamagotchi 处于一致状态,但是,拍卖合约不知道 Tamagotchi 合约中发生了什么。

  2. Tamagotchi 执行消息并保存状态,但在进一步操作期间 gas 耗尽。然后,Tamagotchi 合约的状态发生了变化,但这并没有反映在拍卖合约中。 image

MakeBid 动作的工作流程如下:

  1. 用户出价,表明他愿意为 Tamagotchi 支付的代币数量。

  2. 合约将他的代币转移到它的余额中,如果转移成功,它会将代币返回给之前的投标人。在代币合约执行期间、对拍卖合约的回复期间或进一步执行期间,gas 可能会用完。 image

收到 SettleAuction 消息后,合约执行以下操作:

  1. 将 Tamagotchi 所有者更改为拍卖获胜者。在 Tamagotchi 合约执行期间、回复期间或进一步拍卖合约执行期间,gas 可能会用完。

  2. 将代币转让给之前的所有者。同样,在可替代合约执行期间、回复期间或进一步拍卖合约执行期间,gas 可能会用完。 image

因此,让我们创建以下枚举来跟踪交易:

#[derive(Clone)]
enum Transaction {
   StartAuction {
       tamagotchi_id: ActorId,
       bid: Bid,
       duration: u64,
   },
   MakeBid {
       transaction_id: TransactionId,
       bidder: ActorId,
       bid: u128,
   },
   SettleAuction {
       transaction_id: TransactionId,
   },
}

并将字段添加到合同状态:

#[derive(Default)]
pub struct Auction {
   . . .
  transaction: Some<Transaction>,
  transaction_id: TransactionId
   . . .
}

其中字段 transaction_id 将用于跟踪同质化代币合约中的交易。

练习合约编码

在本教程中,我们不使用 panic!,而是返回 Result<AuctionEvent,AuctionError>。当合约的执行是原子的并且其中没有异步调用时,panic 使用起来很方便。

在我们的范例中,会有很多异步消息调用的场景,我们必须仔细跟踪程序的状态。在这种情况下,使用 Result 枚举是首选。

我们将创建我们将在编写程序期间扩展的枚举 AuctionEvent 和 AuctionError。

#[derive(Encode, Decode)]
pub enum AuctionEvent {
   ...
}

#[derive(Encode, Decode)]
pub enum AuctionError {
   ...
}

因此,主要功能:

#[gstd::async_main]
async fn main() {
   let action: AuctionAction = msg::load().expect("Unable to decode `AuctionAction`");
   let auction = unsafe { AUCTION.get_or_insert(Default::default()) };
   let reply = match action {
       AuctionAction::StartAuction {
           Tamagotchi_id,
           minimum_bid,
           duration,
       } => {
           auction
               .start_auction(&Tamagotchi_id, minimum_bid, duration)
               .await
       }
       AuctionAction::MakeBid { bid } => {
           auction.make_bid(bid).await
       }
       AuctionAction::SettleAuction => {
           auction.settle_auction().await
       }
   };
   msg::reply(reply, 0).expect("Failed to encode or reply with `Result&lt;MarketEvent, MarketErr>`");
}

让我们开始编写函数 start_auction:

async fn start_auction(
       &mut self,
       Tamagotchi_id: &TamagotchiId,
       minimum_bid: Bid,
       duration: u64,
   ) -> Result&lt;AuctionEvent, AuctionError> {}

我们检查拍卖是否处于 ReadyToStart 状态:

if self.status != Status::ReadyToStart {
           return Err(AuctionError::WrongState);
       }

然后我们检查是否有未决交易。如果有,我们:

  • 检查它是交易 StartAuction;

  • 检查函数的输入参数。如果它们与存储在交易中的不同,合约会回复错误;

  • 找到 Tamagotchi 主人。如果已经是拍卖合约,则不再向 Tamagotchi 合约发送消息,直接保存在拍卖合约中。然后我们停止消息执行。

       // Check if there is already a pending transaction
       if let Some(tx) = self.transaction.clone() {
           match tx {
               Transaction::StartAuction { 
                       tamagotchi: prev_tmg_id,
                       bid, 
                       duration: prev_duration, 
                   } => {
                       if *tamagotchi_id != prevamagotchi_id
                       || bid != minimum_bid
                       || prev_duration != duration
                   {
                       return Err(AuctionError::WrongParams);
                   }

                   // get the Tamagotchi owner
                   let tmg_owner = 
                      if let Ok(tmg_owner) = get_owner(&self.Tamagotchi_id).await {
                       tmg_owner
                   } else {
                       self.transaction = None;
                       return Err(AuctionError::WrongReceivedMessage);
                   };

                   // if Tamagotchi owner is already the current contract
                   // we just change its state and start the auction
                   if tmg_owner == exec::program_id() {                    
                       self.tamagotchi_id = *tamagotchi_id;
                       self.status = Status::InProcess;
                       self.current_bid = bid;
                       self.ended_at = exec::block_timestamp() + duration;
                       self.transaction = None;
                       return Ok(AuctionEvent::AuctionStarted)
                   };
  
                   // check that owner starts the auction
                   if tmg_owner != msg::source() {
                       return Err(AuctionError::NotOwner);
                   }
  
                   if change_owner(&self.Tamagotchi_id, &exec::program_id())
                       .await
                       .is_err()
                   {
                       self.transaction = None;
                       return Err(AuctionError::UnableToChangeOwner);
                   } else {
                       self.tamagotchi_id = *tamagotchi_id;
                       self.status = Status::InProcess;
                       self.current_bid = bid;
                       self.prev_tmg_owner = tmg_owner;
                       self.ended_at = exec::block_timestamp() + duration;
                       self.transaction = None;
                       msg::send_delayed(
                           exec::program_id(),
                           AuctionAction::SettleAuction,
                           0,
                           duration as u32,
                       )
                       .expect("Error in sending a delayed message `AuctionAction::SettleAuction`");
                       return Ok(AuctionEvent::AuctionStarted);
                   }
               }
               _ => {
                   return Err(AuctionError::WrongTx);
               }
           }
       }

获取所有者的功能位置:

async fn get_owner(Tamagotchi_id: &TamagotchiId) -> Result&lt;ActorId, AuctionError> {
   let reply = msg::send_for_reply_as(*Tamagotchi_id, TmgAction::Owner, 0)
       .expect("Error in sending a message `TmgAction::Owner` to Tamagotchi contract")
       .await;
   match reply {
       Ok(TmgEvent::Owner(tmg_owner)) => {
           Ok(tmg_owner)
       },
       _ => Err(AuctionError::WrongReceivedMessage)
   }
}

以及更改所有者的功能:

async fn change_owner(
   Tamagotchi_id: &TamagotchiId,
   new_owner: &ActorId,
) -> Result&lt;TmgEvent, ContractError> {
   msg::send_for_reply_as::&lt;_, TmgEvent>(
       *Tamagotchi_id,
       TmgAction::ChangeOwner {
           new_owner: *new_owner,
       },
       0,
   )
   .expect("Error in sending a message `TmgAction::ChangeOwner` to Tamagotchi contract")
   .await
}

如果没有未决交易,下面的逻辑很简单:

      if duration &lt; MIN_DURATION {
           return Err(AuctionError::WrongDuration);
       }


       self.transaction = Some(Transaction::StartAuction {
           tamagotchi_id: *tamagotchi_id,
           bid: minimum_bid,
           duration,
       });

       let tmg_owner = if let Ok(tmg_owner) = get_owner(&self.Tamagotchi_id).await {
           tmg_owner
       } else {
           self.transaction = None;
           return Err(AuctionError::WrongReceivedMessage);
       };
      
       // check that owner starts the auction
       if tmg_owner != msg::source() {
           self.transaction = None;
           return Err(AuctionError::NotOwner);
       }

       if change_owner(&self.Tamagotchi_id, &exec::program_id())
           .await
           .is_err()
       {
           self.transaction = None;
           Err(AuctionError::UnableToChangeOwner)
       } else {
           self.tamagotchi_id = *tamagotchi_id;
           self.status = Status::InProcess;
           self.current_bid = minimum_bid;
           self.prev_tmg_owner = tmg_owner;
           self.ended_at = exec::block_timestamp() + duration;
           self.transaction = None;
           msg::send_delayed(
               exec::program_id(),
               AuctionAction::SettleAuction,
               0,
               duration as u32,
           )
           .expect("Error in sending a delayed message `AuctionAction::SettleAuction`");
           Ok(AuctionEvent::AuctionStarted) 
}

如你所见,当我们继续上一个交易或执行当前交易时,代码会重复。

让我们编写函数 complete_tx:

async fn complete_tx(&mut self, tx: Transaction) -> Result&lt;AuctionEvent, AuctionError> {
       match tx {
           Transaction::StartAuction { bid, duration } => {
               let tmg_owner = if let Ok(tmg_owner) = get_owner(&self.Tamagotchi_id).await {
                   tmg_owner
               } else {
                   self.transaction = None;
                   return Err(AuctionError::WrongReceivedMessage);
               };
               // if Tamagotchi owner is already the current contract
               // we just change its state and start the auction
               if tmg_owner == exec::program_id() 
                   self.status = Status::InProcess;
                   self.current_bid = bid;
                   self.ended_at = exec::block_timestamp() + duration;
                   self.transaction = None;
                   return Ok(AuctionEvent::AuctionStarted);
               };

               // check that owner starts the auction
               if tmg_owner != msg::source() {
                   return Err(AuctionError::NotOwner);
               }

               if change_owner(&self.Tamagotchi_id, &exec::program_id())
                   .await
                   .is_err()
               {
                   self.transaction = None;
                   Err(AuctionError::UnableToChangeOwner)
               } else {
                   self.status = Status::InProcess;
                   self.current_bid = bid;
                   self.prev_tmg_owner = tmg_owner;
                   self.ended_at = exec::block_timestamp() + duration;
                   self.transaction = None;
                   msg::send_delayed(
                       exec::program_id(),
                       AuctionAction::SettleAuction,
                       0,
                       duration as u32,
                   )
                   .expect("Error in sending a delayed message `AuctionAction::SettleAuction`");
                   Ok(AuctionEvent::AuctionStarted)
               }
           }
           Transaction::MakeBid {
               transaction_id,
               bidder,
               bid,
           } => Ok(AuctionEvent::BidMade { bid }),
           Transaction::SettleAuction { transaction_id } => Ok(AuctionEvent::AuctionSettled),
       }
   }

那么函数 start_auction 将改写如下

async fn start_auction(
       &mut self,
       Tamagotchi_id: &TamagotchiId,
       minimum_bid: Bid,
       duration: u64,
   ) -> Result&lt;AuctionEvent, AuctionError> {
       if self.status != Status::ReadyToStart {
           return Err(AuctionError::WrongState);
       }

       // Check if there is already a pending transaction
       if let Some(tx) = self.transaction.clone() {
           match tx {
              Transaction::StartAuction { 
                       tamagotchi: prev_tmg_id,
                       bid, 
                       duration: prev_duration, 
                   } => {
                       if *tamagotchi_id != prevamagotchi_id
                       || bid != minimum_bid
                       || prev_duration != duration
                   {
                       return Err(AuctionError::WrongParams);
                   }
                       return self
                          .complete_tx(Transaction::StartAuction { bid, duration })
                       .  await;
               }
               _ => {
                   return Err(AuctionError::WrongTx);
               }
           }
       }

       if duration &lt; MIN_DURATION {
           return Err(AuctionError::WrongDuration);
       }
       
       let tx = Transaction::StartAuction {
              tamagotchi_id: *tamagotchi_id,
              bid: minimum_bid,
              duration,
       };
       self.transaction = Some(tx.clone());

       self.complete_tx(tx).await
   }

太好了,我们完成了这个函数,现在我们将开始编写出价函数:

  1. 首先,我们检查是否没有待处理的交易 MakeBid;

  2. 接下来,我们检查输入参数。如果它们与保存在交易中的不同,我们完成前一个交易并执行当前交易。如果它们相同,我们完成未决交易并停止函数执行。

  3. 如果没有未决交易,我们执行当前交易。

async fn make_bid(&mut self, bid: u128) -> Result&lt;AuctionEvent, AuctionError> {
       if self.status != Status::InProcess {
           return Err(AuctionError::WrongState);
       }

       // Check if there is already a pending transaction
       if let Some(tx) = self.transaction.clone() {
           match tx {
               Transaction::MakeBid {
                   transaction_id,
                   bidder,
                   bid: prev_bid,
               } => {
                   let result = self
                       .complete_tx(tx).await;
                   if bidder == msg::source() && bid == prev_bid {
                       return result;
                   }
               }
               _ => {
                   return Err(AuctionError::WrongTx);
               }
           }
       }

       if bid &lt;= self.current_bid {
           return Err(AuctionError::WrongBid);
       }

       let transaction_id = self.transaction_id;
       let bidder = msg::source(); 
       // We reserve 2 transaction ids since there will be 2 messages to the token contract
       self.transaction_id = self.transaction_id.wrapping_add(2);
       let tx = Transaction::MakeBid {
           transaction_id,
           bidder,
           bid,
       };
       self.transaction = Some(tx.clone());
       self.complete_tx(tx).await
   }

让我们扩展函数 complete_tx:

async fn complete_tx(&mut self, tx: Transaction) -> Result&lt;AuctionEvent, AuctionError> {
       match tx {
           ...
           Transaction::MakeBid {
               transaction_id,
               bidder,
               bid,
           } => {
               if transfer_tokens(
                   transaction_id,
                   &self.ft_contract_id,
                   &bidder,
                   &exec::program_id(),
                   bid,
               )
               .await
               .is_err()
               {
                   self.transaction = None;
                   return Err(AuctionError::UnableToTransferTokens);
               }

               // if it's not the first bet
               // we have to return the tokens to the previous bidder
               // since the tokens are on the auction contract
               // the transaction can fail only due to lack of gas
               // it's necessary to rerun the transaction
               if !self.current_bidder.is_zero()
                   && transfer_tokens(
                       transaction_id + 1,
                       &self.ft_contract_id,
                       &exec::program_id(),
                       &self.current_bidder,
                       self.current_bid,
                   )
                   .await
                   .is_err()
               {
                   return Err(AuctionError::RerunTransaction);
               }

               self.current_bid = bid;
               self.current_bidder = bidder;
               Ok(AuctionEvent::BidMade { bid })
           }
        ...
       }
   }

因此,下一步是编写函数 settle_auction。在这里,我们再次检查是否存在未决交易。

但是有可能在用户出价的时候状态留下了一笔交易 MakeBid。

在这种情况下,我们必须先完成这笔交易,然后再执行交易 SettleAuction:

async fn settle_auction(&mut self) -> Result&lt;AuctionEvent, AuctionError> {
       if self.ended_at &lt; exec::block_timestamp() {
           return Err(AuctionError::WrongState);
       }

       // it's possible that there is a pending transaction `MakeBid`
       if let Some(tx) = self.transaction.clone() {
           match tx {
               Transaction::MakeBid { .. } => {
                   self.complete_tx(tx).await;
               }
               Transaction::SettleAuction { .. } => {
                   return self.complete_tx(tx).await;
               }
               _ => {
                   return Err(AuctionError::WrongTx);
               }
           }
       }

       let transaction_id = self.transaction_id;
       self.transaction_id = self.transaction_id.wrapping_add(1);
       let tx = Transaction::SettleAuction { transaction_id };
       self.transaction = Some(tx.clone());
       return self.complete_tx(tx).await;
   }

相应地,complete_tx 函数:

async fn complete_tx(&mut self, tx: Transaction) -> Result&lt;AuctionEvent, AuctionError> {
       match tx {
	     ...
           Transaction::SettleAuction { transaction_id } => {
               let tmg_owner = if let Ok(tmg_owner) = get_owner(&self.Tamagotchi_id).await
               {
                   tmg_owner
               } else {
                   return Err(AuctionError::WrongReceivedMessage);
               };
               if tmg_owner == exec::program_id() {
                   if self.current_bidder.is_zero() {
                       if change_owner(&self.Tamagotchi_id, &self.prev_tmg_owner)
                           .await
                           .is_err()
                       {
                           return Err(AuctionError::RerunTransaction);
                       };
                   } else {
                       if transfer_tokens(
                           transaction_id,
                           &self.ft_contract_id,
                           &exec::program_id(),
                           &self.prev_tmg_owner,
                           self.current_bid,
                       )
                       .await
                       .is_err()
                       {
                           return Err(AuctionError::RerunTransaction);
                       };

                       if change_owner(&self.Tamagotchi_id, &self.current_bidder)
                           .await
                           .is_err()
                       {
                           return Err(AuctionError::RerunTransaction);
                       };
                   }
               }
               self.transaction = None;
               self.prev_tmg_owner = ActorId::zero();
               self.current_bidder = ActorId::zero();
               self.status = Status::ReadyToStart;
               self.ended_at = 0;
               self.tamagotchi_id = ActorId::zero();

               Ok(AuctionEvent::AuctionSettled)
           }

系统信号

在 Gear 程序中,共有三个公共入口点:init、handle 和 handle_reply。Gear Protocol 还引入了 handle_signal 入口点,它使系统能够与程序通信并通知它们(信号)与程序消息相关的事件。只有系统(Gear 节点运行时)可以向程序发送信号消息。

如果在程序执行期间发生某些错误,系统会向程序发送消息。例如,一个程序可能会崩溃或耗尽 gas。

gstd 库提供了一个单独的函数来专门为系统信号消息保留 gas。

exec::system_reserve_gas(1_000_000_000).expect("Error during system gas reservation");

这不能用于发送其他常规的 cross-actor 消息。虽然信号可用于 actor 间通信,但它们不适合发送常规的cross-actor 消息。

信号消息使用专门为它们保留的 gas 。如果没有为系统消息保留 gas,它们将被跳过,程序将不会接收它们。

如果 gas 已被保留,但在当前执行期间没有系统消息出现,则该 gas 将返回至它被获取的地方。

如果你的程序使用异步消息,使用 #[gstd::async_main] 宏扩展 handle_signal 入口点,会释放程序占用的资源。

在 Gear 中,使用自定义异步逻辑涉及将 Future 存储在程序的内存中。这些 Futures 的执行上下文会占用大量内存,尤其是在处理许多 Futures 时。

需要注意的是,如果一个程序发送消息并等待回复,但无法收到回复,则可能是因为缺少 gas。例如 waitlist 中的初始消息用完 gas 或者 gas 量不足,就收不到回复。

要在程序中处理信号,你可以定义自己的 my_handle_signal 入口点并为其编写自定义逻辑。在下一节中,我们将看到如何在拍卖合约中编写此函数的示例。

添加 gas 预留和系统信号

让我们将入口点 my_handle_signal 添加到我们的拍卖合约中。该功能将确认是否有未决交易。如果存在,该函数将获取预留的 gas 并使用该 gas 发送消息 CompleteTx。

首先,我们必须通过扩展枚举 AuctionAction 和 AuctionEvent 添加 gas 预留操作:

#[derive(Encode, Decode)]
pub enum AuctionAction {
   ... 
   MakeReservation,
}

#[derive(Encode, Decode)]
pub enum AuctionEvent {
   ...
   ReservationMade,
}

我们还需要在 Auction 结构中添加一个字段 reservations 来保存 gas reservations 的 ids:

#[derive(Default)]
pub struct Auction {
   ... 
   reservations: Vec&lt;ReservationId>,
}

接下来,我们将为 Auction 结构定义 make_reservation 方法:

impl Auction {
   ...
   fn make_reservation(&mut self) -> Result&lt;AuctionEvent, AuctionError> {
       let reservation_id = ReservationId::reserve(RESERVATION_AMOUNT, RESERVATION_DURATION)
           .expect("reservation across executions");
       self.reservations.push(reservation_id);
       Ok(AuctionEvent::ReservationMade)
   }
}

这里,RESERVATION_AMOUNT 和 RESERVATION_DURATION 是常量,定义如下:

const RESERVATION_AMOUNT: u64 = 50_000_000_000;
const RESERVATION_DURATION: u32 = 86400;

然后我们必须添加完成交易的动作。

我们将使用以下方法将操作添加到枚举 AuctionAction:

#[derive(Encode, Decode)]
pub enum AuctionAction {
   ...
   CompleteTx(Transaction),
}

该交易是我们之前定义过的一个枚举

#[derive(Clone, Encode, Decode, PartialEq, Eq)]
pub enum Transaction {
   StartAuction {
       tamagotchi: ActorId,
       bid: Bid,
       duration: u64,
   },
   MakeBid {
       transaction_id: TransactionId,
       bidder: ActorId,
       bid: u128,
   },
   SettleAuction {
       transaction_id: TransactionId,
   },
}

我们还将扩展入口点 main():

#[gstd::async_main]
async fn main() {
       ...
       AuctionAction::MakeReservation => auction.make_reservation(),
       AuctionAction::CompleteTx(tx) => {
           let result = if let Some(_tx) = &auction.transaction {
               if tx == _tx.clone() {
                  auction.complete_tx(tx).await
               } else {
                   Err(AuctionError::WrongTx)
               }
           } else {
               Err(AuctionError::NoTx)
           };
           result     
       },
   };
}

让我们编写函数 my_handle_signal。此功能负责检查是否有未决交易以及是否有可用的 gas 预留。如果是这样,它将使用该 gas 发送一条消息 CompleteTx。

#[no_mangle]
extern "C" fn my_handle_signal() {
   let auction = unsafe { AUCTION.get_or_insert(Default::default()) };
   if let Some(tx) = &auction.transaction {
       let reservation_id = if !auction.reservations.is_empty() {
           auction.reservations.remove(0)
       } else {
           return;
       };
       msg::send_from_reservation(
           reservation_id,
           exec::program_id(),
           AuctionAction::CompleteTx(tx.clone()),
           0,
       )
       .expect("Failed to send message");
   }
}

在每次交易之前,还需要为系统消息预留 gas。下面是我们将如何实现它:

#[gstd::async_main]
async fn main() {
   ... 
   let reply = match action {
       AuctionAction::StartAuction {
           Tamagotchi_id,
           minimum_bid,
           duration,
       } => {
           system_reserve_gas();
           auction
               .start_auction(&Tamagotchi_id, minimum_bid, duration)
               .await
       }
       AuctionAction::MakeBid { bid } => {
           system_reserve_gas();
           auction.make_bid(bid).await
       }
       AuctionAction::SettleAuction => {
           system_reserve_gas();
           auction.settle_auction().await
       }
       ...
   msg::reply(reply, 0).expect("Failed to encode or reply with `Result&lt;MarketEvent, MarketErr>`");
}

fn system_reserve_gas() {
   exec::system_reserve_gas(SYSTEM_GAS).expect("Error during system gas reservation");
}

课后作业:

在此作业中,你将向你的 Tamagotchi 合约添加一个新功能,这将允许它以指定的时间间隔向自己发送延迟消息以检查其状态。如果 Tamagotchi 饥饿、疲惫或不开心,它应该向用户发送一条消息,要求喂食或玩耍。

要实现此功能,您需要使用 reservations 字段扩展 Tamagotchi 状态,如下所示:

#[derive(Default, Encode, Decode, TypeInfo)]
pub struct Tamagotchi {
   pub name: String,
   pub date_of_birth: u64,
   pub owner: ActorId,
   pub fed: u64,
   pub fed_block: u64,
   pub entertained: u64,
   pub entertained_block: u64,
   pub rested: u64,
   pub rested_block: u64,
   pub allowed_account: Option&lt;ActorId>,
   pub ft_contract_id: ActorId,
   pub ft_transaction_id: TransactionId,
   pub approve_transaction: Option&lt;(TransactionId, ActorId, u128)>,
   pub reservations: Vec&lt;ReservationId>,
}

接下来,你需要向 TmgAction 枚举添加两个新的传入消息:CheckState 和 ReserveGas,如下所示:

#[derive(Encode, Decode, TypeInfo)]
pub enum TmgAction {
   Name,
   Age,
   Feed,
   Play,
   Sleep,
   Transfer(ActorId),
   Approve(ActorId),
   RevokeApproval,
   ApproveTokens {
       account: ActorId,
       amount: u128,
   },
   SetFTokenContract(ActorId),
   BuyAttribute {
       store_id: ActorId,
       attribute_id: AttributeId,
   },
   CheckState,
   ReserveGas {
       reservation_amount: u64,
       duration: u32,
   },
}

你还需要向 TmgEvent 枚举添加三个新的输出消息:FeedMe、PlayWithMe 和 WantToSleep。如果 Tamagotchi 用完了 gas,它应该发送消息 MakeReservation 要求所有者保留 gas 以继续检查状态。

你还应该添加输出消息 GasReserved 以指示成功预留 gas ,如下所示:

#[derive(Encode, Decode, TypeInfo)]
pub enum TmgEvent {
   Name(String),
   Age(u64),
   Fed,
   Entertained,
   Slept,
   Transfer(ActorId),
   Approve(ActorId),
   RevokeApproval,
   ApproveTokens { account: ActorId, amount: u128 },
   FTokenContractSet,
   ApprovalError,
   AttributeBought(AttributeId),
   CompletePrevPurchase(AttributeId),
   ErrorDuringPurchase,
   FeedMe,
   PlayWithMe,
   WantToSleep,
   MakeReservation,
   GasReserved,
}

因此,Tamagotchi 必须在一定的时间间隔内向自己发送一次消息。定义此时间间隔并确定 Tamagotchi 将在吃饱、睡觉或娱乐的水平开始发送消息。

将你的 Tamagotchi 连接到应用程序,看看它如何与你交流!

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论