diff --git a/crates/phactory/src/contracts/ghost_auction.rs b/crates/phactory/src/contracts/ghost_auction.rs index 3aee88e9d9..e6140ff726 100644 --- a/crates/phactory/src/contracts/ghost_auction.rs +++ b/crates/phactory/src/contracts/ghost_auction.rs @@ -5,8 +5,8 @@ use log::info; use parity_scale_codec::{Decode, Encode}; use phala_mq::MessageOrigin; use serde::{Deserialize, Serialize}; -use serde_json; -use lazy_static; +use serde_json::{error, Value}; +use chain::BlockNumber; use surf; @@ -19,14 +19,24 @@ extern crate runtime as chain; use phala_types::messaging::GhostAuctionCommand; type Command = GhostAuctionCommand; - -lazy_static! { - // 10000...000, used to tell if this is a NFT - static ref TYPE_NF_BIT: U256 = U256::from(1) << 255; - // 1111...11000...00, used to store the type in the upper 128 bits - static ref TYPE_MASK: U256 = U256::from(!u128::MIN) << 128; - // 00...00011....11, used to get NFT index in the lower 128 bits - static ref NF_INDEX_MASK: U256 = U256::from(!u128::MIN); +/// Ghost Auctioneer Bot +/// The bot is consists of the following: +/// Owner +/// Bot Token from Telegram +/// Chat Id from Telegram Group +/// RMRK NFT Id for querying the RMRK HTTP endpoint +/// Minimum Reserve Price +/// Auto-Increment per Request +/// Current Bidder +pub struct GhostAuctioneerBot { + owner: AccountId, + bot_token: String, + chat_id: String, + nft_id: String, + reserve_price: u64, + auto_bid_increase: u64, + bidder: AccountId, + settled: bool, } /// The payloads of the Telegram `sendMessage` request @@ -37,37 +47,12 @@ struct TgMessage { text: String, } -#[derive(Deserialize, Serialize, Debug, PartialEq)] -pub struct RmrkNft { - nft_id: String, - block: BlockNumber, - metadata: String, +#[derive(Serialize, Deserialize)] +struct RmrkNft { + id: String, name: String, -} - -#[derive(Encode, Decode, Debug, Clone, PartialEq)] -pub struct Auction { - rmrk_nft: RmrkNft, - amount: u64, - start_time: u64, - end_time: u64, - bidder: AccountId, - settled: bool, -} - -pub struct GhostAuction { - rmrk_nft: RmrkNft, - reserve_price: u64, - min_bid_increase: u8, - duration: u64, - auction: Auction, -} - -pub struct GhostAuctioneerBot { - owner: AccountId, - bot_token: String, - chat_id: String, - ghost_auction: GhostAuction, + metadata: String, + block: BlockNumber, } #[derive(Encode, Decode, Debug, Clone)] @@ -76,8 +61,7 @@ pub enum Request { QueryBotToken, QueryChatId, QueryNft, - QueryNftTopBid, - QueryAuctionTimeLeft, + QueryNextBidPrice, } #[derive(Encode, Decode, Debug, Clone, PartialEq)] @@ -86,8 +70,7 @@ pub enum Response { BotToken(String), ChatId(String), Nft(String), - NftTopBid(u64), - TimeLeft(u64), + NextBidPrice(u64), } #[derive(Encode, Decode, Debug)] @@ -97,47 +80,6 @@ pub enum Error { NoAuctionDetected, NoNftDetected, } -/* -impl RmrkNft { - pub fn new(nft_id: String, block: BlockNumber, metadata: String, name: String) -> Self { - RmrkNft { - nft_id, - block, - metadata, - name, - } - } -} -*/ -impl Auction { - pub fn new(rmrk_nft: RmrkNft, - amount: u64, - start_time: u64, - end_time: u64, - bidder: AccountId, - settled: bool) -> Self { - Auction { - rmrk_nft, - amount, - start_time, - end_time, - bidder, - settled, - } - } -} - -impl GhostAuction { - pub fn new() -> Self { - GhostAuction { - rmrk_nft: Default::default(), - reserve_price: Default::default(), - min_bid_increase: Default::default(), - duration: Default::default(), - auction: Default::default(), - } - } -} impl GhostAuctioneerBot { pub fn new() -> Self { @@ -145,7 +87,11 @@ impl GhostAuctioneerBot { owner: Default::default(), bot_token: Default::default(), chat_id: Default::default(), - ghost_auction: Default::default(), + nft_id: Default::default(), + reserve_price: Default::default(), + auto_bid_increase: Default::default(), + bidder: Default::default(), + settled: false, } } } @@ -153,7 +99,7 @@ impl GhostAuctioneerBot { // Alice is the pre-defined root account in dev mode const ALICE: &str = "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"; // RMRK 1.0 HTTP URI -const RMRK_URI: String = "https://singular.rmrk.app/api/rmrk1/nft/"; +const RMRK_URI: &str = "https://singular.rmrk.app/api/rmrk1/nft/"; impl contracts::NativeContract for GhostAuctioneerBot { type Cmd = Command; @@ -195,18 +141,17 @@ impl contracts::NativeContract for GhostAuctioneerBot { self.chat_id = chat_id; Ok(()) } - Command::SetupGhostAuction { nft_id, reserve_price, min_bid_increase, duration } => { + Command::SetupGhostAuction { nft_id, reserve_price, auto_bid_increase } => { if sender != alice && sender != self.owner { return Err(TransactionError::BadOrigin); } - // Check if valid NFT ID - if nft_id & *TYPE_NF_BIT != *TYPE_NF_BIT { - return Err(Error::NoNftDetected); - } let bot_token = self.bot_token.clone(); let chat_id = self.chat_id.clone(); - let query_nft_uri = format!("{}{}", RMRK_URI, nft_id); + let query_nft_uri = format!("{}{}", RMRK_URI, nft_id.clone()); + self.reserve_price = reserve_price; + self.auto_bid_increase = auto_bid_increase; + self.bidder = self.owner.clone(); // This Command triggers the use of `AsyncSideTask`, it first send a HTTP request to get the current BTC // price from https://min-api.cryptocompare.com/, then sends the price to a Telegram bot through another @@ -231,7 +176,7 @@ impl contracts::NativeContract for GhostAuctioneerBot { query_nft_uri, ) .send() - .await() + .await { Ok(r) => r, Err(err) => { @@ -246,14 +191,10 @@ impl contracts::NativeContract for GhostAuctioneerBot { }; log::info!("Side task got RMRK NFT response: {}", result); - let rmrk_nft_vec: Vec = serde_json::from_str(result.as_str()).expect("broken RMRK NFT result"); - // If results are an empty array then the NFT ID doesn't exists - if rmrk_nft_vec.is_empty() == true { - return Err(Error::NoNftDetected); - } + let mut rmrk_nft_vec: Vec = serde_json::from_str(result.as_str()).expect("broken RMRK NFT result"); - let rmrk_nft: RmrkNft = rmrk_nft_vec[0]; - let rmrk_nft_id = rmrk_nft.nft_id; + let rmrk_nft = rmrk_nft_vec.remove(0); + let rmrk_nft_id = rmrk_nft.id; log::info!("RMRK NFT ID: {}", rmrk_nft_id); let rmrk_nft_name = rmrk_nft.name; log::info!("RMRK NFT name: {}", rmrk_nft_name); @@ -262,34 +203,63 @@ impl contracts::NativeContract for GhostAuctioneerBot { let rmrk_nft_block = rmrk_nft.block; log::info!("RMRK NFT ID: {}", rmrk_nft_block); - let start_time = SystemTime::now() - .checked_add(Duration::from_secs(600)) // 10 minutes - .expect("Failed to generate new start time for Ghost Auction"); - let end_time = SystemTime::now() - .checked_add(Duration::from_secs(24 * 3600)) // 24 hours - .expect("Failed to generate new start time for Ghost Auction"); - let auction: Auction = - Auction::new( - rmrk_nft.clone(), - reserve_price, - start_time, - end_time, - self.owner, - false - ); - - let ghost_auction: GhostAuction = - GhostAuction::new( - rmrk_nft.clone(), - reserve_price, - min_bid_increase, - 86400, - auction.clone() - ); - - self.ghost_auction = ghost_auction.clone(); - - let text = format!("Ghost Auction Alert in 10 minutes for NFT ID: {}", rmrk_nft_id); + let text = format!("New Ghost Auction Alert for NFT ID: {} starting...", rmrk_nft_id); + let uri = format!( + "https://api.telegram.org/bot{}/{}", + bot_token, "sendMessage" + ); + // Report new ghost auction created by owner + let data = &TgMessage { chat_id, text }; + + let mut resp = match surf::post(uri) + .body_json(data) + .expect("should not fail with valid data; qed.") + .await + { + Ok(r) => r, + Err(err) => { + return format!("Network error: {:?}", err); + } + }; + let result = match resp.body_string().await { + Ok(body) => body, + Err(err) => { + format!("Network error: {:?}", err) + } + }; + log::info!("Side task sent new Ghost Auction info: {}", result); + result + }, + |_result, _context| { + // You can send deterministic number of transactions in the result process + // In this case, we don't send the price since it has already been reported to the TG bot above + }, + ); + context.block.side_task_man.add_task(task); + + Ok(()) + } + /* + Command::SubmitBid {} => { + if sender != alice && sender != self.owner { + return Err(TransactionError::BadOrigin); + } + let bot_token = self.bot_token.clone(); + let chat_id = self.chat_id.clone(); + let nft_id = self.nft_id.clone(); + let reserve_price = self.reserve_price + self.auto_bid_increase; + let block_number = context.block.block_number; + let duration = 2; + + let task = AsyncSideTask::spawn( + block_number, + duration, + async move { + // Do network request in this block and return the result. + // Do NOT send mq message in this block. + log::info!("Side task starts to submit new bid on NFT"); + + let text = format!("NFT ID: {} auction has closed. Sold for {} KSM to Account Id", nft_id, reserve_price); let uri = format!( "https://api.telegram.org/bot{}/{}", bot_token, "sendMessage" @@ -313,6 +283,9 @@ impl contracts::NativeContract for GhostAuctioneerBot { format!("Network error: {:?}", err) } }; + // Update the reserve_price with new bid amount + self.reserve_price = reserve_price; + self.bidder = sender; log::info!("Side task sent new Ghost Auction info: {}", result); result }, @@ -325,16 +298,69 @@ impl contracts::NativeContract for GhostAuctioneerBot { Ok(()) } + Command::SettleAuction {} => { + let bot_token = self.bot_token.clone(); + let chat_id = self.chat_id.clone(); + let nft_id = self.nft_id.clone(); + let block_number = context.block.block_number; + let duration = 2; + + let task = AsyncSideTask::spawn( + block_number, + duration, + async move { + // Do network request in this block and return the result. + // Do NOT send mq message in this block. + log::info!("Side task starts to settle auction on NFT"); + + let text = format!("NFT ID: {} auction has closed. Sold for {} KSM to Account Id {}", nft_id, self.reserve_price, self.bidder.to_string()); + let uri = format!( + "https://api.telegram.org/bot{}/{}", + bot_token, "sendMessage" + ); + // Report new ghost auction created by owner + let data = &TgMessage { chat_id, text }; + + let mut resp = match surf::post(uri) + .body_json(data) + .expect("should not fail with valid data; qed.") + .await + { + Ok(r) => r, + Err(err) => { + return format!("Network error: {:?}", err); + } + }; + let result = match resp.body_string().await { + Ok(body) => body, + Err(err) => { + format!("Network error: {:?}", err) + } + }; + self.settled = true; + log::info!("Side task sent ghost auction settled: {}, results: {}", self.settled, result); + result + }, + |_result, _context| { + // You can send deterministic number of transactions in the result process + // In this case, we don't send the price since it has already been reported to the TG bot above + }, + ); + context.block.side_task_man.add_task(task); + + Ok(()) + } + */ } } // Handle a direct Query and respond to it. It shouldn't modify the contract state. fn handle_query( &mut self, - origin:Option<&chain::AcountId>, + origin:Option<&chain::AccountId>, req: Request, ) -> Result { - info!("Query received: {?:}", &req); + info!("Query received: {:?}", &req); let sender = origin.ok_or(Error::OriginUnavailable)?; let alice = contracts::account_id_from_hex(ALICE) @@ -360,24 +386,18 @@ impl contracts::NativeContract for GhostAuctioneerBot { return Err(Error::NotAuthorized); } - let nft_id = self.ghost_auction.rmrk_nft.nft_id.clone().ok_or(Error::NoNftDetected)?; + let nft_id = Some(self.nft_id.clone()).ok_or(Error::NoNftDetected)?; Ok(Response::Nft(nft_id)) } - Request::QueryNftTopBid => { - if sender != &alice && sender != &self.owner { - return Err(Error::NotAuthorized); - } - - let top_bid = self.ghost_auction.auction.amount.clone().ok_or(Error::NoAuctionDetected)?; - Ok(Response::NftTopBid(top_bid)) - } - Request::QueryAuctionTimeLeft => { + Request::QueryNextBidPrice => { if sender != &alice && sender != &self.owner { return Err(Error::NotAuthorized); } - let time_left = self.ghost_auction.duration.clone().ok_or(Error::NoAuctionDetected)?; - Ok(Response::TimeLeft(time_left)) + let mut top_bid = Some(self.reserve_price.clone()).ok_or(Error::NoAuctionDetected)?; + let auto_incr_bid_by = Some(self.auto_bid_increase.clone()).ok_or(Error::NoAuctionDetected)?; + top_bid += auto_incr_bid_by; + Ok(Response::NextBidPrice(top_bid)) } } } diff --git a/crates/phactory/src/prpc_service.rs b/crates/phactory/src/prpc_service.rs index 55b87b0e6e..76e93840a7 100644 --- a/crates/phactory/src/prpc_service.rs +++ b/crates/phactory/src/prpc_service.rs @@ -395,7 +395,7 @@ impl Phactory { install_contract!( contracts::GHOST_AUCTIONEER_BOT, - contracts::ghost_action::GhostAuctioneerBot::new() + contracts::ghost_auction::GhostAuctioneerBot::new() ) } diff --git a/crates/phala-types/src/lib.rs b/crates/phala-types/src/lib.rs index ff46b9a993..af27c9f14b 100644 --- a/crates/phala-types/src/lib.rs +++ b/crates/phala-types/src/lib.rs @@ -14,7 +14,7 @@ use sp_core::H256; pub mod messaging { use alloc::string::String; use alloc::vec::Vec; - use codec::{Decode, Encode}; + use codec::{Decode, Encode, Input, Output, Error}; use core::fmt::Debug; use sp_core::U256; @@ -279,9 +279,11 @@ pub mod messaging { /// the identifier to target chat (https://core.telegram.org/bots/api#sendmessage) SetupBot { token: String, chat_id: String }, /// Query RMRK HTTP endpoint for NFT, create ghost auction & report to Telegram group - SetupGhostAuction { nft_id: String, reserve_price: u64, min_bid_increase: u8, duration: u64}, - // TODO users bid - // TODO validate & close auction + SetupGhostAuction { nft_id: String, reserve_price: u64, auto_bid_increase: u64}, + // Submit new bid at the auto-incremented price + //SubmitBid, + // Settle auction and establish winning bidder + //SettleAuction, } /// A fixed point number with 64 integer bits and 64 fractional bits.