From 6dd21551dfdb87a2736cf614c8da5e1254f63b05 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Wed, 22 Nov 2023 15:43:51 +0300 Subject: [PATCH 01/15] Initial contract --- .../sponsored-tx-enabled-auction/Cargo.toml | 21 ++ .../sponsored-tx-enabled-auction/src/lib.rs | 214 +++++++++++++ .../tests/tests.rs | 283 ++++++++++++++++++ 3 files changed, 518 insertions(+) create mode 100644 examples/sponsored-tx-enabled-auction/Cargo.toml create mode 100644 examples/sponsored-tx-enabled-auction/src/lib.rs create mode 100644 examples/sponsored-tx-enabled-auction/tests/tests.rs diff --git a/examples/sponsored-tx-enabled-auction/Cargo.toml b/examples/sponsored-tx-enabled-auction/Cargo.toml new file mode 100644 index 00000000..00925ba8 --- /dev/null +++ b/examples/sponsored-tx-enabled-auction/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "sponsored-tx-enabled-auction" +version = "0.1.0" +authors = ["Concordium "] +edition = "2021" +license = "MPL-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["std", "wee_alloc"] +std = ["concordium-std/std"] +wee_alloc = ["concordium-std/wee_alloc"] + +[dependencies] +concordium-std = {path = "../../concordium-std", default-features = false} + +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + +[lib] +crate-type=["cdylib", "rlib"] diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs new file mode 100644 index 00000000..bc5e1640 --- /dev/null +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -0,0 +1,214 @@ +//! # Implementation of an auction smart contract +//! +//! Accounts can invoke the bid function to participate in the auction. +//! An account has to send some CCD when invoking the bid function. +//! This CCD amount has to exceed the current highest bid by a minimum raise +//! to be accepted by the smart contract. +//! +//! The minimum raise is set when initializing and is defined in Euro cent. +//! The contract uses the current exchange rate used by the chain by the time of +//! the bid, to convert the bid into EUR. +//! +//! The smart contract keeps track of the current highest bidder as well as +//! the CCD amount of the highest bid. The CCD balance of the smart contract +//! represents the highest bid. When a new highest bid is accepted by the smart +//! contract, the smart contract refunds the old highest bidder. +//! +//! Bids have to be placed before the auction ends. The participant with the +//! highest bid (the last bidder) wins the auction. +//! +//! After the auction ends, any account can finalize the auction. The owner of +//! the smart contract instance receives the highest bid (the balance of this +//! contract) when the auction is finalized. This can be done only once. +//! +//! Terminology: `Accounts` are derived from a public/private key pair. +//! `Contract` instances are created by deploying a smart contract +//! module and initializing it. + +#![cfg_attr(not(feature = "std"), no_std)] + +use concordium_std::*; + +/// The state of the auction. +#[derive(Debug, Serialize, SchemaType, Eq, PartialEq, PartialOrd, Clone)] +pub enum AuctionState { + /// The auction is either + /// - still accepting bids or + /// - not accepting bids because it's past the auction end, but nobody has + /// finalized the auction yet. + NotSoldYet, + /// The auction has been finalized and the item has been sold to the + /// winning `AccountAddress`. + Sold(AccountAddress), +} + +/// The state of the smart contract. +/// This state can be viewed by querying the node with the command +/// `concordium-client contract invoke` using the `view` function as entrypoint. +#[derive(Debug, Serialize, SchemaType, Clone)] +pub struct State { + /// State of the auction + auction_state: AuctionState, + /// The highest bidder so far; The variant `None` represents + /// that no bidder has taken part in the auction yet. + highest_bidder: Option, + /// The minimum accepted raise to over bid the current bidder in Euro cent. + minimum_raise: u64, + /// The item to be sold (to be displayed by the front-end) + item: String, + /// Time when auction ends (to be displayed by the front-end) + end: Timestamp, +} + +/// Type of the parameter to the `init` function +#[derive(Serialize, SchemaType)] +pub struct InitParameter { + /// The item to be sold + pub item: String, + /// Time when auction ends using the RFC 3339 format (https://tools.ietf.org/html/rfc3339) + pub end: Timestamp, + /// The minimum accepted raise to over bid the current bidder in Euro cent. + pub minimum_raise: u64, +} + +/// `bid` function errors +#[derive(Debug, PartialEq, Eq, Clone, Reject, Serialize, SchemaType)] +pub enum BidError { + /// Raised when a contract tries to bid; Only accounts + /// are allowed to bid. + OnlyAccount, + /// Raised when new bid amount is lower than current highest bid. + BidBelowCurrentBid, + /// Raised when a new bid amount is raising the current highest bid + /// with less than the minimum raise. + BidBelowMinimumRaise, + /// Raised when bid is placed after auction end time passed. + BidTooLate, + /// Raised when bid is placed after auction has been finalized. + AuctionAlreadyFinalized, +} + +/// `finalize` function errors +#[derive(Debug, PartialEq, Eq, Clone, Reject, Serialize, SchemaType)] +pub enum FinalizeError { + /// Raised when finalizing an auction before auction end time passed + AuctionStillActive, + /// Raised when finalizing an auction that is already finalized + AuctionAlreadyFinalized, +} + +/// Init function that creates a new auction +#[init(contract = "sponsored_tx_enabled_auction", parameter = "InitParameter")] +fn auction_init(ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { + // Getting input parameters + let parameter: InitParameter = ctx.parameter_cursor().get()?; + // Creating `State` + let state = State { + auction_state: AuctionState::NotSoldYet, + highest_bidder: None, + minimum_raise: parameter.minimum_raise, + item: parameter.item, + end: parameter.end, + }; + Ok(state) +} + +/// Receive function for accounts to place a bid in the auction +#[receive(contract = "sponsored_tx_enabled_auction", name = "bid", payable, mutable, error = "BidError")] +fn auction_bid( + ctx: &ReceiveContext, + host: &mut Host, + amount: Amount, +) -> Result<(), BidError> { + let state = host.state(); + // Ensure the auction has not been finalized yet + ensure_eq!(state.auction_state, AuctionState::NotSoldYet, BidError::AuctionAlreadyFinalized); + + let slot_time = ctx.metadata().slot_time(); + // Ensure the auction has not ended yet + ensure!(slot_time <= state.end, BidError::BidTooLate); + + // Ensure that only accounts can place a bid + let sender_address = match ctx.sender() { + Address::Contract(_) => bail!(BidError::OnlyAccount), + Address::Account(account_address) => account_address, + }; + + // Balance of the contract + let balance = host.self_balance(); + + // Balance of the contract before the call + let previous_balance = balance - amount; + + // Ensure that the new bid exceeds the highest bid so far + ensure!(amount > previous_balance, BidError::BidBelowCurrentBid); + + // Calculate the difference between the previous bid and the new bid in CCD. + let amount_difference = amount - previous_balance; + // Get the current exchange rate used by the chain + let exchange_rates = host.exchange_rates(); + // Convert the CCD difference to EUR + let euro_cent_difference = exchange_rates.convert_amount_to_euro_cent(amount_difference); + // Ensure that the bid is at least the `minimum_raise` more than the previous + // bid + ensure!(euro_cent_difference >= state.minimum_raise, BidError::BidBelowMinimumRaise); + + if let Some(account_address) = host.state_mut().highest_bidder.replace(sender_address) { + // Refunding old highest bidder; + // This transfer (given enough NRG of course) always succeeds because the + // `account_address` exists since it was recorded when it placed a bid. + // If an `account_address` exists, and the contract has the funds then the + // transfer will always succeed. + // Please consider using a pull-over-push pattern when expanding this smart + // contract to allow smart contract instances to participate in the auction as + // well. https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/ + host.invoke_transfer(&account_address, previous_balance).unwrap_abort(); + } + Ok(()) +} + +/// View function that returns the content of the state +#[receive(contract = "sponsored_tx_enabled_auction", name = "view", return_value = "State")] +fn view<'b>(_ctx: &ReceiveContext, host: &'b Host) -> ReceiveResult<&'b State> { + Ok(host.state()) +} + +/// ViewHighestBid function that returns the highest bid which is the balance of +/// the contract +#[receive(contract = "sponsored_tx_enabled_auction", name = "viewHighestBid", return_value = "Amount")] +fn view_highest_bid(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { + Ok(host.self_balance()) +} + +/// Receive function used to finalize the auction. It sends the highest bid (the +/// current balance of this smart contract) to the owner of the smart contract +/// instance. +#[receive(contract = "sponsored_tx_enabled_auction", name = "finalize", mutable, error = "FinalizeError")] +fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> Result<(), FinalizeError> { + let state = host.state(); + // Ensure the auction has not been finalized yet + ensure_eq!( + state.auction_state, + AuctionState::NotSoldYet, + FinalizeError::AuctionAlreadyFinalized + ); + + let slot_time = ctx.metadata().slot_time(); + // Ensure the auction has ended already + ensure!(slot_time > state.end, FinalizeError::AuctionStillActive); + + if let Some(account_address) = state.highest_bidder { + // Marking the highest bid (the last bidder) as winner of the auction + host.state_mut().auction_state = AuctionState::Sold(account_address); + let owner = ctx.owner(); + let balance = host.self_balance(); + // Sending the highest bid (the balance of this contract) to the owner of the + // smart contract instance; + // This transfer (given enough NRG of course) always succeeds because the + // `owner` exists since it deployed the smart contract instance. + // If an account exists, and the contract has the funds then the + // transfer will always succeed. + host.invoke_transfer(&owner, balance).unwrap_abort(); + } + Ok(()) +} diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs new file mode 100644 index 00000000..d7033f35 --- /dev/null +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -0,0 +1,283 @@ +//! Tests for the auction smart contract. +use sponsored_tx_enabled_auction::*; +use concordium_smart_contract_testing::*; + +/// The tests accounts. +const ALICE: AccountAddress = AccountAddress([0; 32]); +const BOB: AccountAddress = AccountAddress([1; 32]); +const CAROL: AccountAddress = AccountAddress([2; 32]); +const DAVE: AccountAddress = AccountAddress([3; 32]); + +const SIGNER: Signer = Signer::with_one_key(); +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); + +/// Test a sequence of bids and finalizations: +/// 0. Auction is initialized. +/// 1. Alice successfully bids 1 CCD. +/// 2. Alice successfully bids 2 CCD, highest +/// bid becomes 2 CCD. Alice gets her 1 CCD refunded. +/// 3. Bob successfully bids 3 CCD, highest +/// bid becomes 3 CCD. Alice gets her 2 CCD refunded. +/// 4. Alice tries to bid 3 CCD, which matches the current highest bid, which +/// fails. +/// 5. Alice tries to bid 3.5 CCD, which is below the minimum raise +/// threshold of 1 CCD. +/// 6. Someone tries to finalize the auction before +/// its end time. Attempt fails. +/// 7. Someone tries to bid after the auction has ended (but before it has been +/// finalized), which fails. +/// 8. Dave successfully finalizes the auction after +/// its end time. Carol (the owner of the contract) collects the highest bid +/// amount. +/// 9. Attempts to subsequently bid or finalize fail. +#[test] +fn test_multiple_scenarios() { + let (mut chain, contract_address) = initialize_chain_and_auction(); + + // 1. Alice successfully bids 1 CCD. + let _update_1 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(1), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Alice successfully bids 1 CCD"); + + // 2. Alice successfully bids 2 CCD, highest + // bid becomes 2 CCD. Alice gets her 1 CCD refunded. + let update_2 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(2), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Alice successfully bids 2 CCD"); + // Check that 1 CCD is transferred back to ALICE. + assert_eq!(update_2.account_transfers().collect::>()[..], [( + contract_address, + Amount::from_ccd(1), + ALICE + )]); + + // 3. Bob successfully bids 3 CCD, highest + // bid becomes 3 CCD. Alice gets her 2 CCD refunded. + let update_3 = chain + .contract_update( + SIGNER, + BOB, + Address::Account(BOB), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(3), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Bob successfully bids 3 CCD"); + // Check that 2 CCD is transferred back to ALICE. + assert_eq!(update_3.account_transfers().collect::>()[..], [( + contract_address, + Amount::from_ccd(2), + ALICE + )]); + + // 4. Alice tries to bid 3 CCD, which matches the current highest bid, which + // fails. + let update_4 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(3), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Alice tries to bid 3 CCD"); + // Check that the correct error is returned. + let rv: BidError = update_4.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, BidError::BidBelowCurrentBid); + + // 5. Alice tries to bid 3.5 CCD, which is below the minimum raise threshold of + // 1 CCD. + let update_5 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_micro_ccd(3_500_000), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Alice tries to bid 3.5 CCD"); + // Check that the correct error is returned. + let rv: BidError = update_5.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, BidError::BidBelowMinimumRaise); + + // 6. Someone tries to finalize the auction before + // its end time. Attempt fails. + let update_6 = chain + .contract_update( + SIGNER, + DAVE, + Address::Account(DAVE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Attempt to finalize auction before end time"); + // Check that the correct error is returned. + let rv: FinalizeError = update_6.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, FinalizeError::AuctionStillActive); + + // Increment the chain time by 1001 milliseconds. + chain.tick_block_time(Duration::from_millis(1001)).expect("Increment chain time"); + + // 7. Someone tries to bid after the auction has ended (but before it has been + // finalized), which fails. + let update_7 = chain + .contract_update( + SIGNER, + DAVE, + Address::Account(DAVE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(10), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Attempt to bid after auction has reached the endtime"); + // Check that the return value is `BidTooLate`. + let rv: BidError = update_7.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, BidError::BidTooLate); + + // 8. Dave successfully finalizes the auction after its end time. + let update_8 = chain + .contract_update( + SIGNER, + DAVE, + Address::Account(DAVE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Dave successfully finalizes the auction after its end time"); + + // Check that the correct amount is transferred to Carol. + assert_eq!(update_8.account_transfers().collect::>()[..], [( + contract_address, + Amount::from_ccd(3), + CAROL + )]); + + // 9. Attempts to subsequently bid or finalize fail. + let update_9 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(1), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Attempt to bid after auction has been finalized"); + // Check that the return value is `AuctionAlreadyFinalized`. + let rv: BidError = update_9.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, BidError::AuctionAlreadyFinalized); + + let update_10 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Attempt to finalize auction after it has been finalized"); + let rv: FinalizeError = update_10.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, FinalizeError::AuctionAlreadyFinalized); +} + +/// Setup auction and chain. +/// +/// Carol is the owner of the auction, which ends at `1000` milliseconds after +/// the unix epoch. The 'microCCD per euro' exchange rate is set to `1_000_000`, +/// so 1 CCD = 1 euro. +fn initialize_chain_and_auction() -> (Chain, ContractAddress) { + let mut chain = Chain::builder() + .micro_ccd_per_euro( + ExchangeRate::new(1_000_000, 1).expect("Exchange rate is in valid range"), + ) + .build() + .expect("Exchange rate is in valid range"); + + // Create some accounts accounts on the chain. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(CAROL, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(DAVE, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, CAROL, module).expect("Deploy valid module"); + + // Create the InitParameter. + let parameter = InitParameter { + item: "Auction item".to_string(), + end: Timestamp::from_timestamp_millis(1000), + minimum_raise: 100, // 100 eurocent = 1 euro + }; + + // Initialize the auction contract. + let init = chain + .contract_init(SIGNER, CAROL, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_sponsored_tx_enabled_auction".to_string()), + param: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }) + .expect("Initialize auction"); + + (chain, init.contract_address) +} From e614d5d34ac7517baae33e8e9ec4f6dfb6628d35 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 23 Nov 2023 16:43:50 +0300 Subject: [PATCH 02/15] Add view functions --- .../sponsored-tx-enabled-auction/src/lib.rs | 314 ++++++----- .../tests/tests.rs | 487 ++++++++++-------- 2 files changed, 478 insertions(+), 323 deletions(-) diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index bc5e1640..e9358d56 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -45,170 +45,244 @@ pub enum AuctionState { /// The state of the smart contract. /// This state can be viewed by querying the node with the command /// `concordium-client contract invoke` using the `view` function as entrypoint. -#[derive(Debug, Serialize, SchemaType, Clone)] -pub struct State { +#[derive(Debug, Serialize, SchemaType, Clone, PartialEq, Eq)] +pub struct ItemState { /// State of the auction - auction_state: AuctionState, + pub auction_state: AuctionState, /// The highest bidder so far; The variant `None` represents /// that no bidder has taken part in the auction yet. - highest_bidder: Option, - /// The minimum accepted raise to over bid the current bidder in Euro cent. - minimum_raise: u64, + pub highest_bidder: Option, /// The item to be sold (to be displayed by the front-end) - item: String, + pub name: String, /// Time when auction ends (to be displayed by the front-end) - end: Timestamp, + pub end: Timestamp, + pub start: Timestamp, + pub highest_bid: u64, +} + +/// The state of the smart contract. +/// This state can be viewed by querying the node with the command +/// `concordium-client contract invoke` using the `view` function as entrypoint. +// #[derive(Debug, Serialize, SchemaType, Clone)] +#[derive(Serial, DeserialWithState, Debug)] +#[concordium(state_parameter = "S")] +pub struct State { + items: StateMap, + cis2_contract: ContractAddress, + counter: u16, +} + +#[derive(Serialize, SchemaType, Debug, Eq, PartialEq)] + +pub struct ReturnParamView { + pub item_states: Vec<(u16, ItemState)>, + pub cis2_contract: ContractAddress, + pub counter: u16, } /// Type of the parameter to the `init` function #[derive(Serialize, SchemaType)] -pub struct InitParameter { - /// The item to be sold - pub item: String, - /// Time when auction ends using the RFC 3339 format (https://tools.ietf.org/html/rfc3339) - pub end: Timestamp, - /// The minimum accepted raise to over bid the current bidder in Euro cent. - pub minimum_raise: u64, +pub struct AddItemParameter { + /// The item to be sold (to be displayed by the front-end) + pub name: String, + /// Time when auction ends (to be displayed by the front-end) + pub end: Timestamp, + pub start: Timestamp, + pub minimum_bid: u64, } /// `bid` function errors #[derive(Debug, PartialEq, Eq, Clone, Reject, Serialize, SchemaType)] -pub enum BidError { +pub enum Error { /// Raised when a contract tries to bid; Only accounts /// are allowed to bid. OnlyAccount, /// Raised when new bid amount is lower than current highest bid. BidBelowCurrentBid, - /// Raised when a new bid amount is raising the current highest bid - /// with less than the minimum raise. - BidBelowMinimumRaise, /// Raised when bid is placed after auction end time passed. BidTooLate, /// Raised when bid is placed after auction has been finalized. AuctionAlreadyFinalized, -} - -/// `finalize` function errors -#[derive(Debug, PartialEq, Eq, Clone, Reject, Serialize, SchemaType)] -pub enum FinalizeError { + /// + NoItem, /// Raised when finalizing an auction before auction end time passed AuctionStillActive, - /// Raised when finalizing an auction that is already finalized - AuctionAlreadyFinalized, } /// Init function that creates a new auction -#[init(contract = "sponsored_tx_enabled_auction", parameter = "InitParameter")] -fn auction_init(ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { +#[init(contract = "sponsored_tx_enabled_auction", parameter = "ContractAddress")] +fn auction_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { // Getting input parameters - let parameter: InitParameter = ctx.parameter_cursor().get()?; + let contract: ContractAddress = ctx.parameter_cursor().get()?; // Creating `State` let state = State { - auction_state: AuctionState::NotSoldYet, - highest_bidder: None, - minimum_raise: parameter.minimum_raise, - item: parameter.item, - end: parameter.end, + items: state_builder.new_map(), + cis2_contract: contract, + counter: 0, }; Ok(state) } -/// Receive function for accounts to place a bid in the auction -#[receive(contract = "sponsored_tx_enabled_auction", name = "bid", payable, mutable, error = "BidError")] -fn auction_bid( - ctx: &ReceiveContext, - host: &mut Host, - amount: Amount, -) -> Result<(), BidError> { - let state = host.state(); - // Ensure the auction has not been finalized yet - ensure_eq!(state.auction_state, AuctionState::NotSoldYet, BidError::AuctionAlreadyFinalized); +/// ViewHighestBid function that returns the highest bid which is the balance of +/// the contract +#[receive( + contract = "sponsored_tx_enabled_auction", + name = "addItem", + parameter = "AddItemParameter", + mutable +)] +fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { + // Getting input parameters + let item: AddItemParameter = ctx.parameter_cursor().get()?; - let slot_time = ctx.metadata().slot_time(); - // Ensure the auction has not ended yet - ensure!(slot_time <= state.end, BidError::BidTooLate); + let counter = host.state_mut().counter; + host.state_mut().counter = counter + 1; - // Ensure that only accounts can place a bid - let sender_address = match ctx.sender() { - Address::Contract(_) => bail!(BidError::OnlyAccount), - Address::Account(account_address) => account_address, - }; + host.state_mut().items.insert(counter, ItemState { + auction_state: AuctionState::NotSoldYet, + highest_bidder: None, + + name: item.name, + end: item.end, + start: item.start, + highest_bid: item.minimum_bid, + }); - // Balance of the contract - let balance = host.self_balance(); - - // Balance of the contract before the call - let previous_balance = balance - amount; - - // Ensure that the new bid exceeds the highest bid so far - ensure!(amount > previous_balance, BidError::BidBelowCurrentBid); - - // Calculate the difference between the previous bid and the new bid in CCD. - let amount_difference = amount - previous_balance; - // Get the current exchange rate used by the chain - let exchange_rates = host.exchange_rates(); - // Convert the CCD difference to EUR - let euro_cent_difference = exchange_rates.convert_amount_to_euro_cent(amount_difference); - // Ensure that the bid is at least the `minimum_raise` more than the previous - // bid - ensure!(euro_cent_difference >= state.minimum_raise, BidError::BidBelowMinimumRaise); - - if let Some(account_address) = host.state_mut().highest_bidder.replace(sender_address) { - // Refunding old highest bidder; - // This transfer (given enough NRG of course) always succeeds because the - // `account_address` exists since it was recorded when it placed a bid. - // If an `account_address` exists, and the contract has the funds then the - // transfer will always succeed. - // Please consider using a pull-over-push pattern when expanding this smart - // contract to allow smart contract instances to participate in the auction as - // well. https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/ - host.invoke_transfer(&account_address, previous_balance).unwrap_abort(); - } Ok(()) } /// View function that returns the content of the state -#[receive(contract = "sponsored_tx_enabled_auction", name = "view", return_value = "State")] -fn view<'b>(_ctx: &ReceiveContext, host: &'b Host) -> ReceiveResult<&'b State> { - Ok(host.state()) +#[receive( + contract = "sponsored_tx_enabled_auction", + name = "view", + return_value = "ReturnParamView" +)] +fn view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { + let state = host.state(); + + let mut inner_state = Vec::new(); + for (index, item_state) in state.items.iter() { + inner_state.push((*index, item_state.clone())); + } + + Ok(ReturnParamView { + item_states: inner_state, + cis2_contract: host.state().cis2_contract, + counter: host.state().counter, + }) } /// ViewHighestBid function that returns the highest bid which is the balance of /// the contract -#[receive(contract = "sponsored_tx_enabled_auction", name = "viewHighestBid", return_value = "Amount")] -fn view_highest_bid(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { - Ok(host.self_balance()) +#[receive( + contract = "sponsored_tx_enabled_auction", + name = "viewItemState", + return_value = "ItemState", + parameter = "u16" +)] +fn view_item_state(ctx: &ReceiveContext, host: &Host) -> ReceiveResult { + // Getting input parameters + let item_index: u16 = ctx.parameter_cursor().get()?; + let item = host.state().items.get(&item_index).map(|x| x.clone()).ok_or(Error::NoItem)?; + Ok(item) } -/// Receive function used to finalize the auction. It sends the highest bid (the -/// current balance of this smart contract) to the owner of the smart contract -/// instance. -#[receive(contract = "sponsored_tx_enabled_auction", name = "finalize", mutable, error = "FinalizeError")] -fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> Result<(), FinalizeError> { - let state = host.state(); - // Ensure the auction has not been finalized yet - ensure_eq!( - state.auction_state, - AuctionState::NotSoldYet, - FinalizeError::AuctionAlreadyFinalized - ); - - let slot_time = ctx.metadata().slot_time(); - // Ensure the auction has ended already - ensure!(slot_time > state.end, FinalizeError::AuctionStillActive); - - if let Some(account_address) = state.highest_bidder { - // Marking the highest bid (the last bidder) as winner of the auction - host.state_mut().auction_state = AuctionState::Sold(account_address); - let owner = ctx.owner(); - let balance = host.self_balance(); - // Sending the highest bid (the balance of this contract) to the owner of the - // smart contract instance; - // This transfer (given enough NRG of course) always succeeds because the - // `owner` exists since it deployed the smart contract instance. - // If an account exists, and the contract has the funds then the - // transfer will always succeed. - host.invoke_transfer(&owner, balance).unwrap_abort(); - } - Ok(()) -} +// /// Receive function for accounts to place a bid in the auction +// #[receive( +// contract = "sponsored_tx_enabled_auction", +// name = "bid", +// payable, +// mutable, +// error = "BidError" +// )] +// fn auction_bid( +// ctx: &ReceiveContext,ItemState +// host: &mut Host, +// amount: Amount, +// ) -> Result<(), BidError> { +// let state = host.state(); +// // Ensure the auction has not been finalized yet +// ensure_eq!(state.auction_state, AuctionState::NotSoldYet, +// BidError::AuctionAlreadyFinalized); + +// let slot_time = ctx.metadata().slot_time(); +// // Ensure the auction has not ended yet +// ensure!(slot_time <= state.end, BidError::BidTooLate); + +// // Ensure that only accounts can place a bid +// let sender_address = match ctx.sender() { +// Address::Contract(_) => bail!(BidError::OnlyAccount), +// Address::Account(account_address) => account_address, +// }; + +// // Balance of the contract +// let balance = host.self_balance(); + +// // Balance of the contract before the call +// let previous_balance = balance - amount; + +// // Ensure that the new bid exceeds the highest bid so far +// ensure!(amount > previous_balance, BidError::BidBelowCurrentBid); + +// // Calculate the difference between the previous bid and the new bid in +// CCD. let amount_difference = amount - previous_balance; +// // Get the current exchange rate used by the chain +// let exchange_rates = host.exchange_rates(); +// // Convert the CCD difference to EUR +// let euro_cent_difference = +// exchange_rates.convert_amount_to_euro_cent(amount_difference); // Ensure +// that the bid is at least the `minimum_raise` more than the previous // bid +// ensure!(euro_cent_difference >= state.minimum_raise, +// BidError::BidBelowMinimumRaise); + +// if let Some(account_address) = +// host.state_mut().highest_bidder.replace(sender_address) { // +// Refunding old highest bidder; // This transfer (given enough NRG of +// course) always succeeds because the // `account_address` exists since +// it was recorded when it placed a bid. // If an `account_address` +// exists, and the contract has the funds then the // transfer will +// always succeed. // Please consider using a pull-over-push pattern +// when expanding this smart // contract to allow smart contract +// instances to participate in the auction as // well. https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/ +// host.invoke_transfer(&account_address, +// previous_balance).unwrap_abort(); } +// Ok(()) +// } + +// /// Receive function used to finalize the auction. It sends the highest bid +// (the /// current balance of this smart contract) to the owner of the smart +// contract /// instance. +// #[receive( +// contract = "sponsored_tx_enabled_auction", +// name = "finalize", +// mutable, +// error = "FinalizeError" +// )] +// fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> +// Result<(), FinalizeError> { let state = host.state(); +// // Ensure the auction has not been finalized yet +// ensure_eq!( +// state.auction_state, +// AuctionState::NotSoldYet, +// FinalizeError::AuctionAlreadyFinalized +// ); + +// let slot_time = ctx.metadata().slot_time(); +// // Ensure the auction has ended already +// ensure!(slot_time > state.end, FinalizeError::AuctionStillActive); + +// if let Some(account_address) = state.highest_bidder { +// // Marking the highest bid (the last bidder) as winner of the auction +// host.state_mut().auction_state = AuctionState::Sold(account_address); +// let owner = ctx.owner(); +// let balance = host.self_balance(); +// // Sending the highest bid (the balance of this contract) to the +// owner of the // smart contract instance; +// // This transfer (given enough NRG of course) always succeeds because +// the // `owner` exists since it deployed the smart contract instance. +// // If an account exists, and the contract has the funds then the +// // transfer will always succeed. +// host.invoke_transfer(&owner, balance).unwrap_abort(); +// } +// Ok(()) +// } diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs index d7033f35..30aeeeb1 100644 --- a/examples/sponsored-tx-enabled-auction/tests/tests.rs +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -1,9 +1,10 @@ //! Tests for the auction smart contract. -use sponsored_tx_enabled_auction::*; use concordium_smart_contract_testing::*; +use sponsored_tx_enabled_auction::*; /// The tests accounts. const ALICE: AccountAddress = AccountAddress([0; 32]); +const ALICE_ADDR: Address = Address::Account(AccountAddress([0; 32])); const BOB: AccountAddress = AccountAddress([1; 32]); const CAROL: AccountAddress = AccountAddress([2; 32]); const DAVE: AccountAddress = AccountAddress([3; 32]); @@ -11,6 +12,88 @@ const DAVE: AccountAddress = AccountAddress([3; 32]); const SIGNER: Signer = Signer::with_one_key(); const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); +#[test] +fn test_add_item() { + let (mut chain, contract_address) = initialize_chain_and_auction(); + + // Create the InitParameter. + let parameter = AddItemParameter { + name: "MyItem".to_string(), + end: Timestamp::from_timestamp_millis(1000), + start: Timestamp::from_timestamp_millis(5000), + minimum_bid: 3, + }; + + let update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.addItem".to_string(), + ), + message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }, + ) + .expect("Should be able to add Item"); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.view".to_string(), + ), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + + // Check that the tokens are owned by Alice. + let rv: ReturnParamView = invoke.parse_return_value().expect("View return value"); + + assert_eq!(rv, ReturnParamView { + item_states: vec![(0, ItemState { + auction_state: AuctionState::NotSoldYet, + highest_bidder: None, + name: "MyItem".to_string(), + end: Timestamp::from_timestamp_millis(1000), + start: Timestamp::from_timestamp_millis(5000), + highest_bid: 3, + })], + cis2_contract: ContractAddress::new(0, 0), + counter: 1, + }); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.viewItemState".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), + }) + .expect("Invoke view"); + + // Check that the tokens are owned by Alice. + let rv: ItemState = invoke.parse_return_value().expect("ViewItemState return value"); + + assert_eq!(rv, ItemState { + auction_state: AuctionState::NotSoldYet, + highest_bidder: None, + name: "MyItem".to_string(), + end: Timestamp::from_timestamp_millis(1000), + start: Timestamp::from_timestamp_millis(5000), + highest_bid: 3, + }); +} + /// Test a sequence of bids and finalizations: /// 0. Auction is initialized. /// 1. Alice successfully bids 1 CCD. @@ -30,214 +113,214 @@ const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); /// its end time. Carol (the owner of the contract) collects the highest bid /// amount. /// 9. Attempts to subsequently bid or finalize fail. -#[test] -fn test_multiple_scenarios() { - let (mut chain, contract_address) = initialize_chain_and_auction(); +// #[test] +// fn test_multiple_scenarios() { +// let (mut chain, contract_address) = initialize_chain_and_auction(); - // 1. Alice successfully bids 1 CCD. - let _update_1 = chain - .contract_update( - SIGNER, - ALICE, - Address::Account(ALICE), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::from_ccd(1), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), - message: OwnedParameter::empty(), - }, - ) - .expect("Alice successfully bids 1 CCD"); +// // 1. Alice successfully bids 1 CCD. +// let _update_1 = chain +// .contract_update( +// SIGNER, +// ALICE, +// Address::Account(ALICE), +// Energy::from(10000), +// UpdateContractPayload { +// amount: Amount::from_ccd(1), +// address: contract_address, +// receive_name: +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// message: OwnedParameter::empty(), }, +// ) +// .expect("Alice successfully bids 1 CCD"); - // 2. Alice successfully bids 2 CCD, highest - // bid becomes 2 CCD. Alice gets her 1 CCD refunded. - let update_2 = chain - .contract_update( - SIGNER, - ALICE, - Address::Account(ALICE), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::from_ccd(2), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), - message: OwnedParameter::empty(), - }, - ) - .expect("Alice successfully bids 2 CCD"); - // Check that 1 CCD is transferred back to ALICE. - assert_eq!(update_2.account_transfers().collect::>()[..], [( - contract_address, - Amount::from_ccd(1), - ALICE - )]); +// // 2. Alice successfully bids 2 CCD, highest +// // bid becomes 2 CCD. Alice gets her 1 CCD refunded. +// let update_2 = chain +// .contract_update( +// SIGNER, +// ALICE, +// Address::Account(ALICE), +// Energy::from(10000), +// UpdateContractPayload { +// amount: Amount::from_ccd(2), +// address: contract_address, +// receive_name: +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// message: OwnedParameter::empty(), }, +// ) +// .expect("Alice successfully bids 2 CCD"); +// // Check that 1 CCD is transferred back to ALICE. +// assert_eq!(update_2.account_transfers().collect::>()[..], [( +// contract_address, +// Amount::from_ccd(1), +// ALICE +// )]); - // 3. Bob successfully bids 3 CCD, highest - // bid becomes 3 CCD. Alice gets her 2 CCD refunded. - let update_3 = chain - .contract_update( - SIGNER, - BOB, - Address::Account(BOB), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::from_ccd(3), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), - message: OwnedParameter::empty(), - }, - ) - .expect("Bob successfully bids 3 CCD"); - // Check that 2 CCD is transferred back to ALICE. - assert_eq!(update_3.account_transfers().collect::>()[..], [( - contract_address, - Amount::from_ccd(2), - ALICE - )]); +// // 3. Bob successfully bids 3 CCD, highest +// // bid becomes 3 CCD. Alice gets her 2 CCD refunded. +// let update_3 = chain +// .contract_update( +// SIGNER, +// BOB, +// Address::Account(BOB), +// Energy::from(10000), +// UpdateContractPayload { +// amount: Amount::from_ccd(3), +// address: contract_address, +// receive_name: +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// message: OwnedParameter::empty(), }, +// ) +// .expect("Bob successfully bids 3 CCD"); +// // Check that 2 CCD is transferred back to ALICE. +// assert_eq!(update_3.account_transfers().collect::>()[..], [( +// contract_address, +// Amount::from_ccd(2), +// ALICE +// )]); - // 4. Alice tries to bid 3 CCD, which matches the current highest bid, which - // fails. - let update_4 = chain - .contract_update( - SIGNER, - ALICE, - Address::Account(ALICE), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::from_ccd(3), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), - message: OwnedParameter::empty(), - }, - ) - .expect_err("Alice tries to bid 3 CCD"); - // Check that the correct error is returned. - let rv: BidError = update_4.parse_return_value().expect("Return value is valid"); - assert_eq!(rv, BidError::BidBelowCurrentBid); +// // 4. Alice tries to bid 3 CCD, which matches the current highest bid, which +// // fails. +// let update_4 = chain +// .contract_update( +// SIGNER, +// ALICE, +// Address::Account(ALICE), +// Energy::from(10000), +// UpdateContractPayload { +// amount: Amount::from_ccd(3), +// address: contract_address, +// receive_name: +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// message: OwnedParameter::empty(), }, +// ) +// .expect_err("Alice tries to bid 3 CCD"); +// // Check that the correct error is returned. +// let rv: BidError = update_4.parse_return_value().expect("Return value is valid"); +// assert_eq!(rv, BidError::BidBelowCurrentBid); - // 5. Alice tries to bid 3.5 CCD, which is below the minimum raise threshold of - // 1 CCD. - let update_5 = chain - .contract_update( - SIGNER, - ALICE, - Address::Account(ALICE), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::from_micro_ccd(3_500_000), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), - message: OwnedParameter::empty(), - }, - ) - .expect_err("Alice tries to bid 3.5 CCD"); - // Check that the correct error is returned. - let rv: BidError = update_5.parse_return_value().expect("Return value is valid"); - assert_eq!(rv, BidError::BidBelowMinimumRaise); +// // 5. Alice tries to bid 3.5 CCD, which is below the minimum raise threshold of +// // 1 CCD. +// let update_5 = chain +// .contract_update( +// SIGNER, +// ALICE, +// Address::Account(ALICE), +// Energy::from(10000), +// UpdateContractPayload { +// amount: Amount::from_micro_ccd(3_500_000), +// address: contract_address, +// receive_name: +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// message: OwnedParameter::empty(), }, +// ) +// .expect_err("Alice tries to bid 3.5 CCD"); +// // Check that the correct error is returned. +// let rv: BidError = update_5.parse_return_value().expect("Return value is valid"); +// assert_eq!(rv, BidError::BidBelowMinimumRaise); - // 6. Someone tries to finalize the auction before - // its end time. Attempt fails. - let update_6 = chain - .contract_update( - SIGNER, - DAVE, - Address::Account(DAVE), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), - message: OwnedParameter::empty(), - }, - ) - .expect_err("Attempt to finalize auction before end time"); - // Check that the correct error is returned. - let rv: FinalizeError = update_6.parse_return_value().expect("Return value is valid"); - assert_eq!(rv, FinalizeError::AuctionStillActive); +// // 6. Someone tries to finalize the auction before +// // its end time. Attempt fails. +// let update_6 = chain +// .contract_update( +// SIGNER, +// DAVE, +// Address::Account(DAVE), +// Energy::from(10000), +// UpdateContractPayload { +// amount: Amount::zero(), +// address: contract_address, +// receive_name: +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), +// message: OwnedParameter::empty(), }, +// ) +// .expect_err("Attempt to finalize auction before end time"); +// // Check that the correct error is returned. +// let rv: FinalizeError = update_6.parse_return_value().expect("Return value is valid"); +// assert_eq!(rv, FinalizeError::AuctionStillActive); - // Increment the chain time by 1001 milliseconds. - chain.tick_block_time(Duration::from_millis(1001)).expect("Increment chain time"); +// // Increment the chain time by 1001 milliseconds. +// chain.tick_block_time(Duration::from_millis(1001)).expect("Increment chain time"); - // 7. Someone tries to bid after the auction has ended (but before it has been - // finalized), which fails. - let update_7 = chain - .contract_update( - SIGNER, - DAVE, - Address::Account(DAVE), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::from_ccd(10), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), - message: OwnedParameter::empty(), - }, - ) - .expect_err("Attempt to bid after auction has reached the endtime"); - // Check that the return value is `BidTooLate`. - let rv: BidError = update_7.parse_return_value().expect("Return value is valid"); - assert_eq!(rv, BidError::BidTooLate); +// // 7. Someone tries to bid after the auction has ended (but before it has been +// // finalized), which fails. +// let update_7 = chain +// .contract_update( +// SIGNER, +// DAVE, +// Address::Account(DAVE), +// Energy::from(10000), +// UpdateContractPayload { +// amount: Amount::from_ccd(10), +// address: contract_address, +// receive_name: +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// message: OwnedParameter::empty(), }, +// ) +// .expect_err("Attempt to bid after auction has reached the endtime"); +// // Check that the return value is `BidTooLate`. +// let rv: BidError = update_7.parse_return_value().expect("Return value is valid"); +// assert_eq!(rv, BidError::BidTooLate); - // 8. Dave successfully finalizes the auction after its end time. - let update_8 = chain - .contract_update( - SIGNER, - DAVE, - Address::Account(DAVE), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), - message: OwnedParameter::empty(), - }, - ) - .expect("Dave successfully finalizes the auction after its end time"); +// // 8. Dave successfully finalizes the auction after its end time. +// let update_8 = chain +// .contract_update( +// SIGNER, +// DAVE, +// Address::Account(DAVE), +// Energy::from(10000), +// UpdateContractPayload { +// amount: Amount::zero(), +// address: contract_address, +// receive_name: +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), +// message: OwnedParameter::empty(), }, +// ) +// .expect("Dave successfully finalizes the auction after its end time"); - // Check that the correct amount is transferred to Carol. - assert_eq!(update_8.account_transfers().collect::>()[..], [( - contract_address, - Amount::from_ccd(3), - CAROL - )]); +// // Check that the correct amount is transferred to Carol. +// assert_eq!(update_8.account_transfers().collect::>()[..], [( +// contract_address, +// Amount::from_ccd(3), +// CAROL +// )]); - // 9. Attempts to subsequently bid or finalize fail. - let update_9 = chain - .contract_update( - SIGNER, - ALICE, - Address::Account(ALICE), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::from_ccd(1), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), - message: OwnedParameter::empty(), - }, - ) - .expect_err("Attempt to bid after auction has been finalized"); - // Check that the return value is `AuctionAlreadyFinalized`. - let rv: BidError = update_9.parse_return_value().expect("Return value is valid"); - assert_eq!(rv, BidError::AuctionAlreadyFinalized); +// // 9. Attempts to subsequently bid or finalize fail. +// let update_9 = chain +// .contract_update( +// SIGNER, +// ALICE, +// Address::Account(ALICE), +// Energy::from(10000), +// UpdateContractPayload { +// amount: Amount::from_ccd(1), +// address: contract_address, +// receive_name: +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// message: OwnedParameter::empty(), }, +// ) +// .expect_err("Attempt to bid after auction has been finalized"); +// // Check that the return value is `AuctionAlreadyFinalized`. +// let rv: BidError = update_9.parse_return_value().expect("Return value is valid"); +// assert_eq!(rv, BidError::AuctionAlreadyFinalized); - let update_10 = chain - .contract_update( - SIGNER, - ALICE, - Address::Account(ALICE), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), - message: OwnedParameter::empty(), - }, - ) - .expect_err("Attempt to finalize auction after it has been finalized"); - let rv: FinalizeError = update_10.parse_return_value().expect("Return value is valid"); - assert_eq!(rv, FinalizeError::AuctionAlreadyFinalized); -} +// let update_10 = chain +// .contract_update( +// SIGNER, +// ALICE, +// Address::Account(ALICE), +// Energy::from(10000), +// UpdateContractPayload { +// amount: Amount::zero(), +// address: contract_address, +// receive_name: +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), +// message: OwnedParameter::empty(), }, +// ) +// .expect_err("Attempt to finalize auction after it has been finalized"); +// let rv: FinalizeError = update_10.parse_return_value().expect("Return value is valid"); +// assert_eq!(rv, FinalizeError::AuctionAlreadyFinalized); +// } /// Setup auction and chain. /// @@ -263,18 +346,16 @@ fn initialize_chain_and_auction() -> (Chain, ContractAddress) { let deployment = chain.module_deploy_v1(SIGNER, CAROL, module).expect("Deploy valid module"); // Create the InitParameter. - let parameter = InitParameter { - item: "Auction item".to_string(), - end: Timestamp::from_timestamp_millis(1000), - minimum_raise: 100, // 100 eurocent = 1 euro - }; + let parameter = ContractAddress::new(0, 0); // Initialize the auction contract. let init = chain .contract_init(SIGNER, CAROL, Energy::from(10000), InitContractPayload { amount: Amount::zero(), mod_ref: deployment.module_reference, - init_name: OwnedContractName::new_unchecked("init_sponsored_tx_enabled_auction".to_string()), + init_name: OwnedContractName::new_unchecked( + "init_sponsored_tx_enabled_auction".to_string(), + ), param: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), }) .expect("Initialize auction"); From 5bd99d45e82e4476e409d0ad2b6c978d764d4930 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 23 Nov 2023 19:09:25 +0300 Subject: [PATCH 03/15] Add finalize function --- .../sponsored-tx-enabled-auction/Cargo.toml | 7 +- .../sponsored-tx-enabled-auction/src/lib.rs | 176 +++++++++---- .../tests/tests.rs | 239 ++++++++++++++++-- 3 files changed, 353 insertions(+), 69 deletions(-) diff --git a/examples/sponsored-tx-enabled-auction/Cargo.toml b/examples/sponsored-tx-enabled-auction/Cargo.toml index 00925ba8..3358927f 100644 --- a/examples/sponsored-tx-enabled-auction/Cargo.toml +++ b/examples/sponsored-tx-enabled-auction/Cargo.toml @@ -5,17 +5,20 @@ authors = ["Concordium "] edition = "2021" license = "MPL-2.0" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] default = ["std", "wee_alloc"] -std = ["concordium-std/std"] +std = ["concordium-std/std", "concordium-cis2/std"] wee_alloc = ["concordium-std/wee_alloc"] [dependencies] concordium-std = {path = "../../concordium-std", default-features = false} +concordium-cis2 = {path = "../../concordium-cis2", default-features = false} [dev-dependencies] concordium-smart-contract-testing = { path = "../../contract-testing" } +[dev-dependencies.cis3_nft_sponsored_txs] +path = "../cis3-nft-sponsored-txs/" + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index e9358d56..96427976 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -27,6 +27,7 @@ #![cfg_attr(not(feature = "std"), no_std)] +use concordium_cis2::*; use concordium_std::*; /// The state of the auction. @@ -57,7 +58,9 @@ pub struct ItemState { /// Time when auction ends (to be displayed by the front-end) pub end: Timestamp, pub start: Timestamp, - pub highest_bid: u64, + pub highest_bid: TokenAmountU64, + pub token_id: TokenIdU32, + pub creator: AccountAddress, } /// The state of the smart contract. @@ -88,7 +91,8 @@ pub struct AddItemParameter { /// Time when auction ends (to be displayed by the front-end) pub end: Timestamp, pub start: Timestamp, - pub minimum_bid: u64, + pub minimum_bid: TokenAmountU64, + pub token_id: TokenIdU32, } /// `bid` function errors @@ -138,14 +142,21 @@ fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { let counter = host.state_mut().counter; host.state_mut().counter = counter + 1; + // Ensure that only accounts can add an item. + let sender_address = match ctx.sender() { + Address::Contract(_) => bail!(Error::OnlyAccount.into()), + Address::Account(account_address) => account_address, + }; + host.state_mut().items.insert(counter, ItemState { auction_state: AuctionState::NotSoldYet, highest_bidder: None, - - name: item.name, - end: item.end, - start: item.start, - highest_bid: item.minimum_bid, + name: item.name, + end: item.end, + start: item.start, + highest_bid: item.minimum_bid, + creator: sender_address, + token_id: item.token_id, }); Ok(()) @@ -187,6 +198,48 @@ fn view_item_state(ctx: &ReceiveContext, host: &Host) -> ReceiveResult) -> ReceiveResult<()> { + // Getting input parameters + let item_index: u16 = ctx.parameter_cursor().get()?; + let mut item = host.state_mut().items.entry(item_index).occupied_or(Error::NoItem)?; + + // TODO: change the logic + // Ensure that only accounts can add an item. + let sender_address = match ctx.sender() { + Address::Contract(_) => bail!(Error::OnlyAccount.into()), + Address::Account(account_address) => account_address, + }; + + // TODO: change the logic + item.highest_bidder = Some(sender_address); + item.highest_bid = TokenAmountU64(1); + + if let Some(_account_address) = item.highest_bidder { + // TODO: Refund account_address + + // Refunding old highest bidder; + // This transfer (given enough NRG of course) always succeeds because + // the `account_address` exists since it was recorded when it + // placed a bid. If an `account_address` exists, and the + // contract has the funds then the transfer will always succeed. + // Please consider using a pull-over-push pattern when expanding this + // smart contract to allow smart contract instances to + // participate in the auction as well. https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/ + // host.invoke_transfer(&account_address, + // previous_balance).unwrap_abort(); + } + + Ok(()) +} + // /// Receive function for accounts to place a bid in the auction // #[receive( // contract = "sponsored_tx_enabled_auction", @@ -231,8 +284,8 @@ fn view_item_state(ctx: &ReceiveContext, host: &Host) -> ReceiveResult= state.minimum_raise, +// that the bid is at least the `minimum_raise` more than the previous // +// bid ensure!(euro_cent_difference >= state.minimum_raise, // BidError::BidBelowMinimumRaise); // if let Some(account_address) = @@ -249,40 +302,73 @@ fn view_item_state(ctx: &ReceiveContext, host: &Host) -> ReceiveResult) -> -// Result<(), FinalizeError> { let state = host.state(); -// // Ensure the auction has not been finalized yet -// ensure_eq!( -// state.auction_state, -// AuctionState::NotSoldYet, -// FinalizeError::AuctionAlreadyFinalized -// ); +/// Receive function used to finalize the auction. It sends the highest bid (the +/// current balance of this smart contract) to the owner of the smart contract +/// instance. +#[receive( + contract = "sponsored_tx_enabled_auction", + name = "finalize", + parameter = "u16", + mutable, + error = "Error" +)] +fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { + // Getting input parameters + let item_index: u16 = ctx.parameter_cursor().get()?; + let cis2_contract = host.state().cis2_contract; -// let slot_time = ctx.metadata().slot_time(); -// // Ensure the auction has ended already -// ensure!(slot_time > state.end, FinalizeError::AuctionStillActive); - -// if let Some(account_address) = state.highest_bidder { -// // Marking the highest bid (the last bidder) as winner of the auction -// host.state_mut().auction_state = AuctionState::Sold(account_address); -// let owner = ctx.owner(); -// let balance = host.self_balance(); -// // Sending the highest bid (the balance of this contract) to the -// owner of the // smart contract instance; -// // This transfer (given enough NRG of course) always succeeds because -// the // `owner` exists since it deployed the smart contract instance. -// // If an account exists, and the contract has the funds then the -// // transfer will always succeed. -// host.invoke_transfer(&owner, balance).unwrap_abort(); -// } -// Ok(()) -// } + let item = host.state_mut().items.get(&item_index).ok_or(Error::NoItem)?; + + // Ensure the auction has not been finalized yet + ensure_eq!(item.auction_state, AuctionState::NotSoldYet, Error::AuctionAlreadyFinalized.into()); + + let slot_time = ctx.metadata().slot_time(); + // Ensure the auction has ended already + ensure!(slot_time > item.end, Error::AuctionStillActive.into()); + + if let Some(account_address) = item.highest_bidder { + // Marking the highest bid (the last bidder) as winner of the auction + // item.auction_state = AuctionState::Sold(account_address); + // let owner = ctx.owner(); + // let balance = host.self_balance(); + // // Sending the highest bid (the balance of this contract) to the owner of the + // // smart contract instance; + // // This transfer (given enough NRG of course) always succeeds because the + // // If an account exists, and the contract has the funds then the + // // transfer will always succeed. + // host.invoke_transfer(&owner, balance).unwrap_abort(); + + let parameter = TransferParameter { + 0: vec![Transfer { + token_id: item.token_id, + amount: item.highest_bid, + from: concordium_std::Address::Contract(ctx.self_address()), + to: concordium_cis2::Receiver::Account(item.creator), + data: AdditionalData::empty(), + }], + }; + + host.invoke_contract( + &cis2_contract, + ¶meter, + EntrypointName::new_unchecked("transfer"), + Amount::zero(), + )?; + + let mut item = host.state_mut().items.entry(item_index).occupied_or(Error::NoItem)?; + item.auction_state = AuctionState::Sold(account_address); + } + + Ok(()) +} + +/// Contract token ID type. +/// To save bytes we use a token ID type limited to a `u32`. +pub type ContractTokenId = TokenIdU32; + +/// Contract token amount. +/// Since the tokens are non-fungible the total supply of any token will be at +/// most 1 and it is fine to use a small type for representing token amounts. +pub type ContractTokenAmount = TokenAmountU64; + +pub type TransferParameter = TransferParams; diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs index 30aeeeb1..07d5f0d1 100644 --- a/examples/sponsored-tx-enabled-auction/tests/tests.rs +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -1,4 +1,7 @@ //! Tests for the auction smart contract. +use concordium_cis2::{ + BalanceOfQuery, BalanceOfQueryParams, BalanceOfQueryResponse, TokenAmountU64, TokenIdU32, +}; use concordium_smart_contract_testing::*; use sponsored_tx_enabled_auction::*; @@ -12,19 +15,189 @@ const DAVE: AccountAddress = AccountAddress([3; 32]); const SIGNER: Signer = Signer::with_one_key(); const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); +#[test] +fn test_finalizing_auction() { + let (mut chain, auction_contract_address, token_contract_address) = + initialize_chain_and_auction(); + + // Create the InitParameter. + let parameter = AddItemParameter { + name: "MyItem".to_string(), + end: Timestamp::from_timestamp_millis(1000), + start: Timestamp::from_timestamp_millis(5000), + token_id: TokenIdU32(1), + minimum_bid: TokenAmountU64(3), + }; + + let _update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: auction_contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.addItem".to_string(), + ), + message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }, + ) + .expect("Should be able to add Item"); + + let _update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: auction_contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.bid".to_string(), + ), + message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), + }, + ) + .expect("Should be able to finalize"); + + let parameter = cis3_nft_sponsored_txs::MintParams { + owner: concordium_smart_contract_testing::Address::Contract(auction_contract_address), + }; + + let _update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: token_contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.mint".to_string()), + message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }, + ) + .expect("Should be able to finalize"); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.viewItemState".to_string(), + ), + address: auction_contract_address, + message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), + }) + .expect("Invoke view"); + + // Check that the tokens are owned by Alice. + let rv: ItemState = invoke.parse_return_value().expect("ViewItemState return value"); + println!("{:?}", rv); + + let balance_of_params = ContractBalanceOfQueryParams { + queries: vec![BalanceOfQuery { + token_id: TokenIdU32(1), + address: ALICE_ADDR, + }], + }; + + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.balanceOf".to_string()), + address: token_contract_address, + message: OwnedParameter::from_serial(&balance_of_params) + .expect("BalanceOf params"), + }) + .expect("Invoke balanceOf"); + let rv: ContractBalanceOfQueryResponse = + invoke.parse_return_value().expect("BalanceOf return value"); + println!("{rv:?}"); + + // Increment the chain time by 100000 milliseconds. + chain.tick_block_time(Duration::from_millis(100000)).expect("Increment chain time"); + + let _update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: auction_contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.finalize".to_string(), + ), + message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), + }, + ) + .expect("Should be able to finalize"); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.viewItemState".to_string(), + ), + address: auction_contract_address, + message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), + }) + .expect("Invoke view"); + + // Check that the tokens are owned by Alice. + let rv: ItemState = invoke.parse_return_value().expect("ViewItemState return value"); + println!("{rv:?}"); + assert_eq!(rv.auction_state, AuctionState::Sold(ALICE)); + + /// Parameter type for the CIS-2 function `balanceOf` specialized to the + /// subset of TokenIDs used by this contract. + pub type ContractBalanceOfQueryParams = BalanceOfQueryParams; + /// Response type for the CIS-2 function `balanceOf` specialized to the + /// subset of TokenAmounts used by this contract. + pub type ContractBalanceOfQueryResponse = BalanceOfQueryResponse; + + let balance_of_params = ContractBalanceOfQueryParams { + queries: vec![BalanceOfQuery { + token_id: TokenIdU32(1), + address: ALICE_ADDR, + }], + }; + + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.balanceOf".to_string()), + address: token_contract_address, + message: OwnedParameter::from_serial(&balance_of_params) + .expect("BalanceOf params"), + }) + .expect("Invoke balanceOf"); + let rv: ContractBalanceOfQueryResponse = + invoke.parse_return_value().expect("BalanceOf return value"); + println!("{rv:?}"); +} + #[test] fn test_add_item() { - let (mut chain, contract_address) = initialize_chain_and_auction(); + let (mut chain, auction_contract_address, _token_contract_address) = + initialize_chain_and_auction(); // Create the InitParameter. let parameter = AddItemParameter { name: "MyItem".to_string(), end: Timestamp::from_timestamp_millis(1000), start: Timestamp::from_timestamp_millis(5000), - minimum_bid: 3, + token_id: TokenIdU32(1), + minimum_bid: TokenAmountU64(3), }; - let update = chain + let _update = chain .contract_update( SIGNER, ALICE, @@ -32,7 +205,7 @@ fn test_add_item() { Energy::from(10000), UpdateContractPayload { amount: Amount::from_ccd(0), - address: contract_address, + address: auction_contract_address, receive_name: OwnedReceiveName::new_unchecked( "sponsored_tx_enabled_auction.addItem".to_string(), ), @@ -48,7 +221,7 @@ fn test_add_item() { receive_name: OwnedReceiveName::new_unchecked( "sponsored_tx_enabled_auction.view".to_string(), ), - address: contract_address, + address: auction_contract_address, message: OwnedParameter::empty(), }) .expect("Invoke view"); @@ -63,7 +236,9 @@ fn test_add_item() { name: "MyItem".to_string(), end: Timestamp::from_timestamp_millis(1000), start: Timestamp::from_timestamp_millis(5000), - highest_bid: 3, + token_id: TokenIdU32(1), + creator: ALICE, + highest_bid: TokenAmountU64(3), })], cis2_contract: ContractAddress::new(0, 0), counter: 1, @@ -76,7 +251,7 @@ fn test_add_item() { receive_name: OwnedReceiveName::new_unchecked( "sponsored_tx_enabled_auction.viewItemState".to_string(), ), - address: contract_address, + address: auction_contract_address, message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), }) .expect("Invoke view"); @@ -90,7 +265,9 @@ fn test_add_item() { name: "MyItem".to_string(), end: Timestamp::from_timestamp_millis(1000), start: Timestamp::from_timestamp_millis(5000), - highest_bid: 3, + token_id: TokenIdU32(1), + creator: ALICE, + highest_bid: TokenAmountU64(3), }); } @@ -128,7 +305,7 @@ fn test_add_item() { // amount: Amount::from_ccd(1), // address: contract_address, // receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), // message: OwnedParameter::empty(), }, // ) // .expect("Alice successfully bids 1 CCD"); @@ -145,7 +322,7 @@ fn test_add_item() { // amount: Amount::from_ccd(2), // address: contract_address, // receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), // message: OwnedParameter::empty(), }, // ) // .expect("Alice successfully bids 2 CCD"); @@ -168,7 +345,7 @@ fn test_add_item() { // amount: Amount::from_ccd(3), // address: contract_address, // receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), // message: OwnedParameter::empty(), }, // ) // .expect("Bob successfully bids 3 CCD"); @@ -191,7 +368,7 @@ fn test_add_item() { // amount: Amount::from_ccd(3), // address: contract_address, // receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), // message: OwnedParameter::empty(), }, // ) // .expect_err("Alice tries to bid 3 CCD"); @@ -211,7 +388,7 @@ fn test_add_item() { // amount: Amount::from_micro_ccd(3_500_000), // address: contract_address, // receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), // message: OwnedParameter::empty(), }, // ) // .expect_err("Alice tries to bid 3.5 CCD"); @@ -231,7 +408,7 @@ fn test_add_item() { // amount: Amount::zero(), // address: contract_address, // receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), // message: OwnedParameter::empty(), }, // ) // .expect_err("Attempt to finalize auction before end time"); @@ -254,7 +431,7 @@ fn test_add_item() { // amount: Amount::from_ccd(10), // address: contract_address, // receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), // message: OwnedParameter::empty(), }, // ) // .expect_err("Attempt to bid after auction has reached the endtime"); @@ -273,7 +450,7 @@ fn test_add_item() { // amount: Amount::zero(), // address: contract_address, // receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), // message: OwnedParameter::empty(), }, // ) // .expect("Dave successfully finalizes the auction after its end time"); @@ -296,7 +473,7 @@ fn test_add_item() { // amount: Amount::from_ccd(1), // address: contract_address, // receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), // message: OwnedParameter::empty(), }, // ) // .expect_err("Attempt to bid after auction has been finalized"); @@ -314,7 +491,7 @@ fn test_add_item() { // amount: Amount::zero(), // address: contract_address, // receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), +// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), // message: OwnedParameter::empty(), }, // ) // .expect_err("Attempt to finalize auction after it has been finalized"); @@ -327,7 +504,7 @@ fn test_add_item() { /// Carol is the owner of the auction, which ends at `1000` milliseconds after /// the unix epoch. The 'microCCD per euro' exchange rate is set to `1_000_000`, /// so 1 CCD = 1 euro. -fn initialize_chain_and_auction() -> (Chain, ContractAddress) { +fn initialize_chain_and_auction() -> (Chain, ContractAddress, ContractAddress) { let mut chain = Chain::builder() .micro_ccd_per_euro( ExchangeRate::new(1_000_000, 1).expect("Exchange rate is in valid range"), @@ -342,14 +519,32 @@ fn initialize_chain_and_auction() -> (Chain, ContractAddress) { chain.create_account(Account::new(DAVE, ACC_INITIAL_BALANCE)); // Load and deploy the module. - let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let module = module_load_v1("../cis3-nft-sponsored-txs/concordium-out/module.wasm.v1") + .expect("Module exists"); let deployment = chain.module_deploy_v1(SIGNER, CAROL, module).expect("Deploy valid module"); // Create the InitParameter. let parameter = ContractAddress::new(0, 0); // Initialize the auction contract. - let init = chain + let token = chain + .contract_init(SIGNER, CAROL, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_cis3_nft".to_string()), + param: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }) + .expect("Initialize auction"); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, CAROL, module).expect("Deploy valid module"); + + // Create the InitParameter. + let parameter = token.contract_address; + + // Initialize the auction contract. + let init_auction = chain .contract_init(SIGNER, CAROL, Energy::from(10000), InitContractPayload { amount: Amount::zero(), mod_ref: deployment.module_reference, @@ -360,5 +555,5 @@ fn initialize_chain_and_auction() -> (Chain, ContractAddress) { }) .expect("Initialize auction"); - (chain, init.contract_address) + (chain, init_auction.contract_address, token.contract_address) } From 7efdf9640f091c39f28fbd62d116b1bce2bf4aa3 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Fri, 24 Nov 2023 14:24:12 +0300 Subject: [PATCH 04/15] Add bid function --- .../sponsored-tx-enabled-auction/src/lib.rs | 190 +++++++++--------- .../tests/tests.rs | 69 +++++-- 2 files changed, 146 insertions(+), 113 deletions(-) diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index 96427976..7bb06fbf 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -30,6 +30,17 @@ use concordium_cis2::*; use concordium_std::*; +/// Contract token ID type. +/// To save bytes we use a token ID type limited to a `u32`. +pub type ContractTokenId = TokenIdU32; + +/// Contract token amount. +/// Since the tokens are non-fungible the total supply of any token will be at +/// most 1 and it is fine to use a small type for representing token amounts. +pub type ContractTokenAmount = TokenAmountU64; + +pub type TransferParameter = TransferParams; + /// The state of the auction. #[derive(Debug, Serialize, SchemaType, Eq, PartialEq, PartialOrd, Clone)] pub enum AuctionState { @@ -100,17 +111,20 @@ pub struct AddItemParameter { pub enum Error { /// Raised when a contract tries to bid; Only accounts /// are allowed to bid. - OnlyAccount, + OnlyAccount, //-1 /// Raised when new bid amount is lower than current highest bid. - BidBelowCurrentBid, + BidBelowCurrentBid, //-2 /// Raised when bid is placed after auction end time passed. - BidTooLate, + BidTooLate, //-3 /// Raised when bid is placed after auction has been finalized. - AuctionAlreadyFinalized, + AuctionAlreadyFinalized, //-4 /// - NoItem, + NoItem, //-5 /// Raised when finalizing an auction before auction end time passed - AuctionStillActive, + AuctionStillActive, //-6 + /// + NotTokenContract, //-7 + WrongTokenID, //-8 } /// Init function that creates a new auction @@ -198,33 +212,74 @@ fn view_item_state(ctx: &ReceiveContext, host: &Host) -> ReceiveResult { + /// The ID of the token received. + pub token_id: T, + /// The amount of tokens received. + pub amount: A, + /// The previous owner of the tokens. + pub from: Address, + /// Some extra information which where sent as part of the transfer. + pub data: AdditionalDataItem, +} + +/// Additional information to include with a transfer. +#[derive(Debug, Serialize, Clone, SchemaType)] +#[concordium(transparent)] +pub struct AdditionalDataItem(#[concordium(size_length = 2)] Vec); + +#[derive(Debug, Deserial, Serial, Clone, SchemaType)] +pub struct AdditionalDataIndex { + pub item_index: u16, +} + /// Receive function for accounts to place a bid in the auction #[receive( contract = "sponsored_tx_enabled_auction", name = "bid", mutable, - parameter = "u16", + parameter = "TestOnReceivingCis2Params", error = "Error" )] fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { - // Getting input parameters - let item_index: u16 = ctx.parameter_cursor().get()?; - let mut item = host.state_mut().items.entry(item_index).occupied_or(Error::NoItem)?; + // Parse the parameter. + let params: TestOnReceivingCis2Params = + ctx.parameter_cursor().get()?; + + // Ensure the sender is the cis2_token_contract. + if let Address::Contract(contract) = ctx.sender() { + ensure_eq!(contract, host.state().cis2_contract, Error::NotTokenContract.into()); + } else { + bail!(Error::NotTokenContract.into()) + }; - // TODO: change the logic - // Ensure that only accounts can add an item. - let sender_address = match ctx.sender() { + let additional_data_index: AdditionalDataIndex = from_bytes(¶ms.data.0)?; + + let cis2_contract = host.state().cis2_contract; + + let item = + host.state_mut().items.get(&additional_data_index.item_index).ok_or(Error::NoItem)?; + + ensure_eq!(item.token_id, params.token_id, Error::WrongTokenID.into()); + + // Ensure that only accounts can bid for an item. + let bidder_address = match params.from { Address::Contract(_) => bail!(Error::OnlyAccount.into()), Address::Account(account_address) => account_address, }; - // TODO: change the logic - item.highest_bidder = Some(sender_address); - item.highest_bid = TokenAmountU64(1); + // Ensure the auction has not been finalized yet + ensure_eq!(item.auction_state, AuctionState::NotSoldYet, Error::AuctionAlreadyFinalized.into()); - if let Some(_account_address) = item.highest_bidder { - // TODO: Refund account_address + let slot_time = ctx.metadata().slot_time(); + // Ensure the auction has not ended yet + ensure!(slot_time <= item.end, Error::BidTooLate.into()); + // Ensure that the new bid exceeds the highest bid so far + ensure!(params.amount > item.highest_bid, Error::BidBelowCurrentBid.into()); + + if let Some(account_address) = item.highest_bidder { // Refunding old highest bidder; // This transfer (given enough NRG of course) always succeeds because // the `account_address` exists since it was recorded when it @@ -233,75 +288,35 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<() // Please consider using a pull-over-push pattern when expanding this // smart contract to allow smart contract instances to // participate in the auction as well. https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/ - // host.invoke_transfer(&account_address, - // previous_balance).unwrap_abort(); + let parameter = TransferParameter { + 0: vec![Transfer { + token_id: item.token_id, + amount: item.highest_bid, + from: concordium_std::Address::Contract(ctx.self_address()), + to: concordium_cis2::Receiver::Account(account_address), + data: AdditionalData::empty(), + }], + }; + + host.invoke_contract( + &cis2_contract, + ¶meter, + EntrypointName::new_unchecked("transfer"), + Amount::zero(), + )?; } + let mut item = host + .state_mut() + .items + .entry(additional_data_index.item_index) + .occupied_or(Error::NoItem)?; + item.highest_bidder = Some(bidder_address); + item.highest_bid = params.amount; + Ok(()) } -// /// Receive function for accounts to place a bid in the auction -// #[receive( -// contract = "sponsored_tx_enabled_auction", -// name = "bid", -// payable, -// mutable, -// error = "BidError" -// )] -// fn auction_bid( -// ctx: &ReceiveContext,ItemState -// host: &mut Host, -// amount: Amount, -// ) -> Result<(), BidError> { -// let state = host.state(); -// // Ensure the auction has not been finalized yet -// ensure_eq!(state.auction_state, AuctionState::NotSoldYet, -// BidError::AuctionAlreadyFinalized); - -// let slot_time = ctx.metadata().slot_time(); -// // Ensure the auction has not ended yet -// ensure!(slot_time <= state.end, BidError::BidTooLate); - -// // Ensure that only accounts can place a bid -// let sender_address = match ctx.sender() { -// Address::Contract(_) => bail!(BidError::OnlyAccount), -// Address::Account(account_address) => account_address, -// }; - -// // Balance of the contract -// let balance = host.self_balance(); - -// // Balance of the contract before the call -// let previous_balance = balance - amount; - -// // Ensure that the new bid exceeds the highest bid so far -// ensure!(amount > previous_balance, BidError::BidBelowCurrentBid); - -// // Calculate the difference between the previous bid and the new bid in -// CCD. let amount_difference = amount - previous_balance; -// // Get the current exchange rate used by the chain -// let exchange_rates = host.exchange_rates(); -// // Convert the CCD difference to EUR -// let euro_cent_difference = -// exchange_rates.convert_amount_to_euro_cent(amount_difference); // Ensure -// that the bid is at least the `minimum_raise` more than the previous // -// bid ensure!(euro_cent_difference >= state.minimum_raise, -// BidError::BidBelowMinimumRaise); - -// if let Some(account_address) = -// host.state_mut().highest_bidder.replace(sender_address) { // -// Refunding old highest bidder; // This transfer (given enough NRG of -// course) always succeeds because the // `account_address` exists since -// it was recorded when it placed a bid. // If an `account_address` -// exists, and the contract has the funds then the // transfer will -// always succeed. // Please consider using a pull-over-push pattern -// when expanding this smart // contract to allow smart contract -// instances to participate in the auction as // well. https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/ -// host.invoke_transfer(&account_address, -// previous_balance).unwrap_abort(); } -// Ok(()) -// } - /// Receive function used to finalize the auction. It sends the highest bid (the /// current balance of this smart contract) to the owner of the smart contract /// instance. @@ -361,14 +376,3 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResu Ok(()) } - -/// Contract token ID type. -/// To save bytes we use a token ID type limited to a `u32`. -pub type ContractTokenId = TokenIdU32; - -/// Contract token amount. -/// Since the tokens are non-fungible the total supply of any token will be at -/// most 1 and it is fine to use a small type for representing token amounts. -pub type ContractTokenAmount = TokenAmountU64; - -pub type TransferParameter = TransferParams; diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs index 07d5f0d1..8f355280 100644 --- a/examples/sponsored-tx-enabled-auction/tests/tests.rs +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -1,6 +1,7 @@ //! Tests for the auction smart contract. use concordium_cis2::{ - BalanceOfQuery, BalanceOfQueryParams, BalanceOfQueryResponse, TokenAmountU64, TokenIdU32, + AdditionalData, BalanceOfQuery, BalanceOfQueryParams, BalanceOfQueryResponse, Receiver, + TokenAmountU64, TokenIdU32, TransferParams, }; use concordium_smart_contract_testing::*; use sponsored_tx_enabled_auction::*; @@ -26,7 +27,7 @@ fn test_finalizing_auction() { end: Timestamp::from_timestamp_millis(1000), start: Timestamp::from_timestamp_millis(5000), token_id: TokenIdU32(1), - minimum_bid: TokenAmountU64(3), + minimum_bid: TokenAmountU64(0), }; let _update = chain @@ -46,25 +47,8 @@ fn test_finalizing_auction() { ) .expect("Should be able to add Item"); - let _update = chain - .contract_update( - SIGNER, - ALICE, - Address::Account(ALICE), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::from_ccd(0), - address: auction_contract_address, - receive_name: OwnedReceiveName::new_unchecked( - "sponsored_tx_enabled_auction.bid".to_string(), - ), - message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), - }, - ) - .expect("Should be able to finalize"); - let parameter = cis3_nft_sponsored_txs::MintParams { - owner: concordium_smart_contract_testing::Address::Contract(auction_contract_address), + owner: concordium_smart_contract_testing::Address::Account(ALICE), }; let _update = chain @@ -82,6 +66,51 @@ fn test_finalizing_auction() { ) .expect("Should be able to finalize"); + let test = AdditionalDataIndex { + item_index: 0u16, + }; + + // Transfer one token from Alice to bid function in auction. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Contract( + auction_contract_address, + OwnedEntrypointName::new_unchecked("bid".to_string()), + ), + token_id: TokenIdU32(1), + amount: TokenAmountU64(1), + data: AdditionalData::from(to_bytes(&test)), + }]); + + let _update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.transfer".to_string()), + address: token_contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + let balance_of_params = ContractBalanceOfQueryParams { + queries: vec![BalanceOfQuery { + token_id: TokenIdU32(1), + address: concordium_std::Address::Contract(auction_contract_address), + }], + }; + + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.balanceOf".to_string()), + address: token_contract_address, + message: OwnedParameter::from_serial(&balance_of_params) + .expect("BalanceOf params"), + }) + .expect("Invoke balanceOf"); + let rv: ContractBalanceOfQueryResponse = + invoke.parse_return_value().expect("BalanceOf return value"); + println!("sssssssssssss{rv:?}"); + // Invoke the view entrypoint and check that the tokens are owned by Alice. let invoke = chain .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { From e7be4f97d2fc8050bb8d91adca386383ea347b20 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Mon, 27 Nov 2023 11:42:06 +0300 Subject: [PATCH 05/15] Add signture generation in test cases --- .../sponsored-tx-enabled-auction/Cargo.toml | 1 + .../sponsored-tx-enabled-auction/src/lib.rs | 6 +- .../tests/tests.rs | 484 ++++++++++++------ 3 files changed, 327 insertions(+), 164 deletions(-) diff --git a/examples/sponsored-tx-enabled-auction/Cargo.toml b/examples/sponsored-tx-enabled-auction/Cargo.toml index 3358927f..d7ad6aad 100644 --- a/examples/sponsored-tx-enabled-auction/Cargo.toml +++ b/examples/sponsored-tx-enabled-auction/Cargo.toml @@ -16,6 +16,7 @@ concordium-cis2 = {path = "../../concordium-cis2", default-features = false} [dev-dependencies] concordium-smart-contract-testing = { path = "../../contract-testing" } +rand = "0.7.0" [dev-dependencies.cis3_nft_sponsored_txs] path = "../cis3-nft-sponsored-txs/" diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index 7bb06fbf..a1be92e2 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -37,7 +37,7 @@ pub type ContractTokenId = TokenIdU32; /// Contract token amount. /// Since the tokens are non-fungible the total supply of any token will be at /// most 1 and it is fine to use a small type for representing token amounts. -pub type ContractTokenAmount = TokenAmountU64; +pub type ContractTokenAmount = TokenAmountU8; pub type TransferParameter = TransferParams; @@ -69,7 +69,7 @@ pub struct ItemState { /// Time when auction ends (to be displayed by the front-end) pub end: Timestamp, pub start: Timestamp, - pub highest_bid: TokenAmountU64, + pub highest_bid: TokenAmountU8, pub token_id: TokenIdU32, pub creator: AccountAddress, } @@ -102,7 +102,7 @@ pub struct AddItemParameter { /// Time when auction ends (to be displayed by the front-end) pub end: Timestamp, pub start: Timestamp, - pub minimum_bid: TokenAmountU64, + pub minimum_bid: TokenAmountU8, pub token_id: TokenIdU32, } diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs index 8f355280..81337737 100644 --- a/examples/sponsored-tx-enabled-auction/tests/tests.rs +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -1,24 +1,36 @@ //! Tests for the auction smart contract. +use std::collections::BTreeMap; + +use cis3_nft_sponsored_txs::{ + ContractBalanceOfQueryParams, ContractBalanceOfQueryResponse, PermitMessage, PermitParam, +}; use concordium_cis2::{ - AdditionalData, BalanceOfQuery, BalanceOfQueryParams, BalanceOfQueryResponse, Receiver, - TokenAmountU64, TokenIdU32, TransferParams, + AdditionalData, BalanceOfQuery, BalanceOfQueryParams, Receiver, TokenAmountU8, TokenIdU32, + TransferParams, }; use concordium_smart_contract_testing::*; +use concordium_std::{AccountSignatures, CredentialSignatures, HashSha2256, SignatureEd25519}; use sponsored_tx_enabled_auction::*; /// The tests accounts. const ALICE: AccountAddress = AccountAddress([0; 32]); const ALICE_ADDR: Address = Address::Account(AccountAddress([0; 32])); const BOB: AccountAddress = AccountAddress([1; 32]); +const BOB_ADDR: Address = Address::Account(AccountAddress([1; 32])); const CAROL: AccountAddress = AccountAddress([2; 32]); const DAVE: AccountAddress = AccountAddress([3; 32]); const SIGNER: Signer = Signer::with_one_key(); const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); +const DUMMY_SIGNATURE: SignatureEd25519 = SignatureEd25519([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]); + #[test] -fn test_finalizing_auction() { - let (mut chain, auction_contract_address, token_contract_address) = +fn test_add_item() { + let (mut chain, _keypairs, auction_contract_address, _token_contract_address) = initialize_chain_and_auction(); // Create the InitParameter. @@ -27,7 +39,95 @@ fn test_finalizing_auction() { end: Timestamp::from_timestamp_millis(1000), start: Timestamp::from_timestamp_millis(5000), token_id: TokenIdU32(1), - minimum_bid: TokenAmountU64(0), + minimum_bid: TokenAmountU8(3), + }; + + let _update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: auction_contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.addItem".to_string(), + ), + message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }, + ) + .expect("Should be able to add Item"); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.view".to_string(), + ), + address: auction_contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + + // Check that the tokens are owned by Alice. + let rv: ReturnParamView = invoke.parse_return_value().expect("View return value"); + + assert_eq!(rv, ReturnParamView { + item_states: vec![(0, ItemState { + auction_state: AuctionState::NotSoldYet, + highest_bidder: None, + name: "MyItem".to_string(), + end: Timestamp::from_timestamp_millis(1000), + start: Timestamp::from_timestamp_millis(5000), + token_id: TokenIdU32(1), + creator: ALICE, + highest_bid: TokenAmountU8(3), + })], + cis2_contract: ContractAddress::new(0, 0), + counter: 1, + }); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.viewItemState".to_string(), + ), + address: auction_contract_address, + message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), + }) + .expect("Invoke view"); + + // Check that the tokens are owned by Alice. + let rv: ItemState = invoke.parse_return_value().expect("ViewItemState return value"); + + assert_eq!(rv, ItemState { + auction_state: AuctionState::NotSoldYet, + highest_bidder: None, + name: "MyItem".to_string(), + end: Timestamp::from_timestamp_millis(1000), + start: Timestamp::from_timestamp_millis(5000), + token_id: TokenIdU32(1), + creator: ALICE, + highest_bid: TokenAmountU8(3), + }); +} + +#[test] +fn full_auction_flow_with_cis3_permit_function() { + let (mut chain, keypairs, auction_contract_address, token_contract_address) = + initialize_chain_and_auction(); + + // Create the InitParameter. + let parameter = AddItemParameter { + name: "MyItem".to_string(), + end: Timestamp::from_timestamp_millis(1000), + start: Timestamp::from_timestamp_millis(5000), + token_id: TokenIdU32(1), + minimum_bid: TokenAmountU8(0), }; let _update = chain @@ -66,86 +166,96 @@ fn test_finalizing_auction() { ) .expect("Should be able to finalize"); - let test = AdditionalDataIndex { + let additional_data = AdditionalDataIndex { item_index: 0u16, }; - // Transfer one token from Alice to bid function in auction. - let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + // Check balances in state. + let balance_of_alice_and_auction_contract = + get_balances(&chain, auction_contract_address, token_contract_address); + + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU8(1), TokenAmountU8(0)]); + + // Create input parameters for the `permit` transfer function. + let transfer = concordium_cis2::Transfer { from: ALICE_ADDR, to: Receiver::Contract( auction_contract_address, OwnedEntrypointName::new_unchecked("bid".to_string()), ), token_id: TokenIdU32(1), - amount: TokenAmountU64(1), - data: AdditionalData::from(to_bytes(&test)), - }]); - - let _update = chain - .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis3_nft.transfer".to_string()), - address: token_contract_address, - message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), - }) - .expect("Transfer tokens"); - - let balance_of_params = ContractBalanceOfQueryParams { - queries: vec![BalanceOfQuery { - token_id: TokenIdU32(1), - address: concordium_std::Address::Contract(auction_contract_address), - }], + amount: ContractTokenAmount::from(1), + data: AdditionalData::from(to_bytes(&additional_data)), + }; + let payload = TransferParams::from(vec![transfer]); + + // The `viewMessageHash` function uses the same input parameter `PermitParam` as + // the `permit` function. The `PermitParam` type includes a `signature` and + // a `signer`. Because these two values (`signature` and `signer`) are not + // read in the `viewMessageHash` function, any value can be used and we choose + // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. + let signature_map = BTreeMap::from([(0u8, CredentialSignatures { + sigs: BTreeMap::from([(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE))]), + })]); + + let mut permit_transfer_param = PermitParam { + signature: AccountSignatures { + sigs: signature_map, + }, + signer: ALICE, + message: PermitMessage { + timestamp: Timestamp::from_timestamp_millis(10_000_000_000), + contract_address: ContractAddress::new(0, 0), + entry_point: OwnedEntrypointName::new_unchecked("transfer".into()), + nonce: 0, + payload: to_bytes(&payload), + }, }; + // Get the message hash to be signed. let invoke = chain - .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis3_nft.balanceOf".to_string()), address: token_contract_address, - message: OwnedParameter::from_serial(&balance_of_params) - .expect("BalanceOf params"), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.viewMessageHash".to_string()), + message: OwnedParameter::from_serial(&permit_transfer_param) + .expect("Should be a valid inut parameter"), }) - .expect("Invoke balanceOf"); - let rv: ContractBalanceOfQueryResponse = - invoke.parse_return_value().expect("BalanceOf return value"); - println!("sssssssssssss{rv:?}"); + .expect("Should be able to query viewMessageHash"); - // Invoke the view entrypoint and check that the tokens are owned by Alice. - let invoke = chain - .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked( - "sponsored_tx_enabled_auction.viewItemState".to_string(), - ), - address: auction_contract_address, - message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), - }) - .expect("Invoke view"); + let message_hash: HashSha2256 = + from_bytes(&invoke.return_value).expect("Should return a valid result"); - // Check that the tokens are owned by Alice. - let rv: ItemState = invoke.parse_return_value().expect("ViewItemState return value"); - println!("{:?}", rv); + permit_transfer_param.signature = keypairs.sign_message(&to_bytes(&message_hash)); - let balance_of_params = ContractBalanceOfQueryParams { - queries: vec![BalanceOfQuery { - token_id: TokenIdU32(1), - address: ALICE_ADDR, - }], - }; + // Transfer token with the permit function. + let _update = chain + .contract_update( + Signer::with_one_key(), + BOB, + BOB_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: token_contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.permit".to_string()), + message: OwnedParameter::from_serial(&permit_transfer_param) + .expect("Should be a valid inut parameter"), + }, + ) + .expect("Should be able to transfer token with permit"); - let invoke = chain - .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis3_nft.balanceOf".to_string()), - address: token_contract_address, - message: OwnedParameter::from_serial(&balance_of_params) - .expect("BalanceOf params"), - }) - .expect("Invoke balanceOf"); - let rv: ContractBalanceOfQueryResponse = - invoke.parse_return_value().expect("BalanceOf return value"); - println!("{rv:?}"); + // Check balances in state. + let balance_of_alice_and_auction_contract = + get_balances(&chain, auction_contract_address, token_contract_address); + + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU8(0), TokenAmountU8(1)]); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let item_state = view_item_state(&chain, auction_contract_address); + + // Check that item is not sold yet. + assert_eq!(item_state.auction_state, AuctionState::NotSoldYet); // Increment the chain time by 100000 milliseconds. chain.tick_block_time(Duration::from_millis(100000)).expect("Increment chain time"); @@ -168,53 +278,14 @@ fn test_finalizing_auction() { .expect("Should be able to finalize"); // Invoke the view entrypoint and check that the tokens are owned by Alice. - let invoke = chain - .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked( - "sponsored_tx_enabled_auction.viewItemState".to_string(), - ), - address: auction_contract_address, - message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), - }) - .expect("Invoke view"); + let item_state = view_item_state(&chain, auction_contract_address); - // Check that the tokens are owned by Alice. - let rv: ItemState = invoke.parse_return_value().expect("ViewItemState return value"); - println!("{rv:?}"); - assert_eq!(rv.auction_state, AuctionState::Sold(ALICE)); - - /// Parameter type for the CIS-2 function `balanceOf` specialized to the - /// subset of TokenIDs used by this contract. - pub type ContractBalanceOfQueryParams = BalanceOfQueryParams; - /// Response type for the CIS-2 function `balanceOf` specialized to the - /// subset of TokenAmounts used by this contract. - pub type ContractBalanceOfQueryResponse = BalanceOfQueryResponse; - - let balance_of_params = ContractBalanceOfQueryParams { - queries: vec![BalanceOfQuery { - token_id: TokenIdU32(1), - address: ALICE_ADDR, - }], - }; - - let invoke = chain - .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis3_nft.balanceOf".to_string()), - address: token_contract_address, - message: OwnedParameter::from_serial(&balance_of_params) - .expect("BalanceOf params"), - }) - .expect("Invoke balanceOf"); - let rv: ContractBalanceOfQueryResponse = - invoke.parse_return_value().expect("BalanceOf return value"); - println!("{rv:?}"); + assert_eq!(item_state.auction_state, AuctionState::Sold(ALICE)); } #[test] -fn test_add_item() { - let (mut chain, auction_contract_address, _token_contract_address) = +fn full_auction_flow_with_cis3_transfer_function() { + let (mut chain, _keypair, auction_contract_address, token_contract_address) = initialize_chain_and_auction(); // Create the InitParameter. @@ -223,7 +294,7 @@ fn test_add_item() { end: Timestamp::from_timestamp_millis(1000), start: Timestamp::from_timestamp_millis(5000), token_id: TokenIdU32(1), - minimum_bid: TokenAmountU64(3), + minimum_bid: TokenAmountU8(0), }; let _update = chain @@ -243,61 +314,93 @@ fn test_add_item() { ) .expect("Should be able to add Item"); - // Invoke the view entrypoint and check that the tokens are owned by Alice. - let invoke = chain - .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked( - "sponsored_tx_enabled_auction.view".to_string(), - ), - address: auction_contract_address, - message: OwnedParameter::empty(), - }) - .expect("Invoke view"); + let parameter = cis3_nft_sponsored_txs::MintParams { + owner: concordium_smart_contract_testing::Address::Account(ALICE), + }; - // Check that the tokens are owned by Alice. - let rv: ReturnParamView = invoke.parse_return_value().expect("View return value"); + let _update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: token_contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.mint".to_string()), + message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }, + ) + .expect("Should be able to finalize"); - assert_eq!(rv, ReturnParamView { - item_states: vec![(0, ItemState { - auction_state: AuctionState::NotSoldYet, - highest_bidder: None, - name: "MyItem".to_string(), - end: Timestamp::from_timestamp_millis(1000), - start: Timestamp::from_timestamp_millis(5000), - token_id: TokenIdU32(1), - creator: ALICE, - highest_bid: TokenAmountU64(3), - })], - cis2_contract: ContractAddress::new(0, 0), - counter: 1, - }); + // Check balances in state. + let balance_of_alice_and_auction_contract = + get_balances(&chain, auction_contract_address, token_contract_address); - // Invoke the view entrypoint and check that the tokens are owned by Alice. - let invoke = chain - .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU8(1), TokenAmountU8(0)]); + + let additional_data = AdditionalDataIndex { + item_index: 0u16, + }; + + // Transfer one token from Alice to bid function in auction. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Contract( + auction_contract_address, + OwnedEntrypointName::new_unchecked("bid".to_string()), + ), + token_id: TokenIdU32(1), + amount: TokenAmountU8(1), + data: AdditionalData::from(to_bytes(&additional_data)), + }]); + + let _update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked( - "sponsored_tx_enabled_auction.viewItemState".to_string(), - ), - address: auction_contract_address, - message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.transfer".to_string()), + address: token_contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), }) - .expect("Invoke view"); + .expect("Transfer tokens"); - // Check that the tokens are owned by Alice. - let rv: ItemState = invoke.parse_return_value().expect("ViewItemState return value"); + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let item_state = view_item_state(&chain, auction_contract_address); - assert_eq!(rv, ItemState { - auction_state: AuctionState::NotSoldYet, - highest_bidder: None, - name: "MyItem".to_string(), - end: Timestamp::from_timestamp_millis(1000), - start: Timestamp::from_timestamp_millis(5000), - token_id: TokenIdU32(1), - creator: ALICE, - highest_bid: TokenAmountU64(3), - }); + // Check that item is not sold yet. + assert_eq!(item_state.auction_state, AuctionState::NotSoldYet); + + // Check balances in state. + let balance_of_alice_and_auction_contract = + get_balances(&chain, auction_contract_address, token_contract_address); + + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU8(0), TokenAmountU8(1)]); + + // Increment the chain time by 100000 milliseconds. + chain.tick_block_time(Duration::from_millis(100000)).expect("Increment chain time"); + + let _update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: auction_contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.finalize".to_string(), + ), + message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), + }, + ) + .expect("Should be able to finalize"); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let item_state = view_item_state(&chain, auction_contract_address); + + // Check that item is not sold yet. + assert_eq!(item_state.auction_state, AuctionState::Sold(ALICE)); } /// Test a sequence of bids and finalizations: @@ -528,12 +631,61 @@ fn test_add_item() { // assert_eq!(rv, FinalizeError::AuctionAlreadyFinalized); // } +/// Get the `ItemState` at index 0. +fn view_item_state(chain: &Chain, auction_contract_address: ContractAddress) -> ItemState { + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.viewItemState".to_string(), + ), + address: auction_contract_address, + message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), + }) + .expect("Invoke view"); + + invoke.parse_return_value().expect("BalanceOf return value") +} + +/// Get the `TOKEN_1` balances for Alice and the auction contract. +fn get_balances( + chain: &Chain, + auction_contract_address: ContractAddress, + token_contract_address: ContractAddress, +) -> ContractBalanceOfQueryResponse { + let balance_of_params: ContractBalanceOfQueryParams = BalanceOfQueryParams { + queries: vec![ + BalanceOfQuery { + token_id: TokenIdU32(1), + address: ALICE_ADDR, + }, + BalanceOfQuery { + token_id: TokenIdU32(1), + address: Address::from(auction_contract_address), + }, + ], + }; + + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.balanceOf".to_string()), + address: token_contract_address, + message: OwnedParameter::from_serial(&balance_of_params) + .expect("BalanceOf params"), + }) + .expect("Invoke balanceOf"); + + invoke.parse_return_value().expect("BalanceOf return value") +} + /// Setup auction and chain. /// /// Carol is the owner of the auction, which ends at `1000` milliseconds after /// the unix epoch. The 'microCCD per euro' exchange rate is set to `1_000_000`, /// so 1 CCD = 1 euro. -fn initialize_chain_and_auction() -> (Chain, ContractAddress, ContractAddress) { +fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, ContractAddress) { let mut chain = Chain::builder() .micro_ccd_per_euro( ExchangeRate::new(1_000_000, 1).expect("Exchange rate is in valid range"), @@ -541,8 +693,18 @@ fn initialize_chain_and_auction() -> (Chain, ContractAddress, ContractAddress) { .build() .expect("Exchange rate is in valid range"); + let rng = &mut rand::thread_rng(); + + let keypairs = AccountKeys::singleton(rng); + + let balance = AccountBalance { + total: ACC_INITIAL_BALANCE, + staked: Amount::zero(), + locked: Amount::zero(), + }; + // Create some accounts accounts on the chain. - chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new_with_keys(ALICE, balance, (&keypairs).into())); chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(CAROL, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(DAVE, ACC_INITIAL_BALANCE)); @@ -584,5 +746,5 @@ fn initialize_chain_and_auction() -> (Chain, ContractAddress, ContractAddress) { }) .expect("Initialize auction"); - (chain, init_auction.contract_address, token.contract_address) + (chain, keypairs, init_auction.contract_address, token.contract_address) } From e39fd36f1f48705ea462aad2bb517478c8395fa3 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 28 Nov 2023 10:46:51 +0300 Subject: [PATCH 06/15] Use cis2_multi --- .../sponsored-tx-enabled-auction/Cargo.toml | 4 +- .../sponsored-tx-enabled-auction/src/lib.rs | 12 +- .../tests/tests.rs | 117 +++++++++++------- 3 files changed, 79 insertions(+), 54 deletions(-) diff --git a/examples/sponsored-tx-enabled-auction/Cargo.toml b/examples/sponsored-tx-enabled-auction/Cargo.toml index d7ad6aad..a68e0a97 100644 --- a/examples/sponsored-tx-enabled-auction/Cargo.toml +++ b/examples/sponsored-tx-enabled-auction/Cargo.toml @@ -18,8 +18,8 @@ concordium-cis2 = {path = "../../concordium-cis2", default-features = false} concordium-smart-contract-testing = { path = "../../contract-testing" } rand = "0.7.0" -[dev-dependencies.cis3_nft_sponsored_txs] -path = "../cis3-nft-sponsored-txs/" +[dev-dependencies.cis2-multi] +path = "../cis2-multi/" [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index a1be92e2..96ad1cf0 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -32,12 +32,12 @@ use concordium_std::*; /// Contract token ID type. /// To save bytes we use a token ID type limited to a `u32`. -pub type ContractTokenId = TokenIdU32; +pub type ContractTokenId = TokenIdU8; /// Contract token amount. /// Since the tokens are non-fungible the total supply of any token will be at /// most 1 and it is fine to use a small type for representing token amounts. -pub type ContractTokenAmount = TokenAmountU8; +pub type ContractTokenAmount = TokenAmountU64; pub type TransferParameter = TransferParams; @@ -69,8 +69,8 @@ pub struct ItemState { /// Time when auction ends (to be displayed by the front-end) pub end: Timestamp, pub start: Timestamp, - pub highest_bid: TokenAmountU8, - pub token_id: TokenIdU32, + pub highest_bid: TokenAmountU64, + pub token_id: TokenIdU8, pub creator: AccountAddress, } @@ -102,8 +102,8 @@ pub struct AddItemParameter { /// Time when auction ends (to be displayed by the front-end) pub end: Timestamp, pub start: Timestamp, - pub minimum_bid: TokenAmountU8, - pub token_id: TokenIdU32, + pub minimum_bid: TokenAmountU64, + pub token_id: TokenIdU8, } /// `bid` function errors diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs index 81337737..b6f7352b 100644 --- a/examples/sponsored-tx-enabled-auction/tests/tests.rs +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -1,15 +1,17 @@ //! Tests for the auction smart contract. use std::collections::BTreeMap; -use cis3_nft_sponsored_txs::{ +use cis2_multi::{ ContractBalanceOfQueryParams, ContractBalanceOfQueryResponse, PermitMessage, PermitParam, }; use concordium_cis2::{ - AdditionalData, BalanceOfQuery, BalanceOfQueryParams, Receiver, TokenAmountU8, TokenIdU32, + AdditionalData, BalanceOfQuery, BalanceOfQueryParams, Receiver, TokenAmountU64, TokenIdU8, TransferParams, }; use concordium_smart_contract_testing::*; -use concordium_std::{AccountSignatures, CredentialSignatures, HashSha2256, SignatureEd25519}; +use concordium_std::{ + AccountSignatures, CredentialSignatures, HashSha2256, MetadataUrl, SignatureEd25519, +}; use sponsored_tx_enabled_auction::*; /// The tests accounts. @@ -30,16 +32,21 @@ const DUMMY_SIGNATURE: SignatureEd25519 = SignatureEd25519([ #[test] fn test_add_item() { - let (mut chain, _keypairs, auction_contract_address, _token_contract_address) = - initialize_chain_and_auction(); + let ( + mut chain, + _keypairs_alice, + _keypairs_bob, + auction_contract_address, + _token_contract_address, + ) = initialize_chain_and_auction(); // Create the InitParameter. let parameter = AddItemParameter { name: "MyItem".to_string(), end: Timestamp::from_timestamp_millis(1000), start: Timestamp::from_timestamp_millis(5000), - token_id: TokenIdU32(1), - minimum_bid: TokenAmountU8(3), + token_id: TokenIdU8(1), + minimum_bid: TokenAmountU64(3), }; let _update = chain @@ -81,9 +88,9 @@ fn test_add_item() { name: "MyItem".to_string(), end: Timestamp::from_timestamp_millis(1000), start: Timestamp::from_timestamp_millis(5000), - token_id: TokenIdU32(1), + token_id: TokenIdU8(1), creator: ALICE, - highest_bid: TokenAmountU8(3), + highest_bid: TokenAmountU64(3), })], cis2_contract: ContractAddress::new(0, 0), counter: 1, @@ -110,24 +117,29 @@ fn test_add_item() { name: "MyItem".to_string(), end: Timestamp::from_timestamp_millis(1000), start: Timestamp::from_timestamp_millis(5000), - token_id: TokenIdU32(1), + token_id: TokenIdU8(1), creator: ALICE, - highest_bid: TokenAmountU8(3), + highest_bid: TokenAmountU64(3), }); } #[test] fn full_auction_flow_with_cis3_permit_function() { - let (mut chain, keypairs, auction_contract_address, token_contract_address) = - initialize_chain_and_auction(); + let ( + mut chain, + keypairs_alice, + _keypairs_bob, + auction_contract_address, + token_contract_address, + ) = initialize_chain_and_auction(); // Create the InitParameter. let parameter = AddItemParameter { name: "MyItem".to_string(), end: Timestamp::from_timestamp_millis(1000), start: Timestamp::from_timestamp_millis(5000), - token_id: TokenIdU32(1), - minimum_bid: TokenAmountU8(0), + token_id: TokenIdU8(1), + minimum_bid: TokenAmountU64(0), }; let _update = chain @@ -147,8 +159,13 @@ fn full_auction_flow_with_cis3_permit_function() { ) .expect("Should be able to add Item"); - let parameter = cis3_nft_sponsored_txs::MintParams { - owner: concordium_smart_contract_testing::Address::Account(ALICE), + let parameter = cis2_multi::MintParams { + owner: concordium_smart_contract_testing::Address::Account(ALICE), + metadata_url: MetadataUrl { + url: "https://some.example/token/0".to_string(), + hash: None, + }, + token_id: concordium_cis2::TokenIdU8(1u8), }; let _update = chain @@ -160,7 +177,7 @@ fn full_auction_flow_with_cis3_permit_function() { UpdateContractPayload { amount: Amount::from_ccd(0), address: token_contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis3_nft.mint".to_string()), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.mint".to_string()), message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), }, ) @@ -174,7 +191,7 @@ fn full_auction_flow_with_cis3_permit_function() { let balance_of_alice_and_auction_contract = get_balances(&chain, auction_contract_address, token_contract_address); - assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU8(1), TokenAmountU8(0)]); + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(100), TokenAmountU64(0)]); // Create input parameters for the `permit` transfer function. let transfer = concordium_cis2::Transfer { @@ -183,8 +200,8 @@ fn full_auction_flow_with_cis3_permit_function() { auction_contract_address, OwnedEntrypointName::new_unchecked("bid".to_string()), ), - token_id: TokenIdU32(1), - amount: ContractTokenAmount::from(1), + token_id: TokenIdU8(1), + amount: TokenAmountU64(1), data: AdditionalData::from(to_bytes(&additional_data)), }; let payload = TransferParams::from(vec![transfer]); @@ -217,7 +234,7 @@ fn full_auction_flow_with_cis3_permit_function() { .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), address: token_contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis3_nft.viewMessageHash".to_string()), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.viewMessageHash".to_string()), message: OwnedParameter::from_serial(&permit_transfer_param) .expect("Should be a valid inut parameter"), }) @@ -226,7 +243,7 @@ fn full_auction_flow_with_cis3_permit_function() { let message_hash: HashSha2256 = from_bytes(&invoke.return_value).expect("Should return a valid result"); - permit_transfer_param.signature = keypairs.sign_message(&to_bytes(&message_hash)); + permit_transfer_param.signature = keypairs_alice.sign_message(&to_bytes(&message_hash)); // Transfer token with the permit function. let _update = chain @@ -238,7 +255,7 @@ fn full_auction_flow_with_cis3_permit_function() { UpdateContractPayload { amount: Amount::zero(), address: token_contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis3_nft.permit".to_string()), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.permit".to_string()), message: OwnedParameter::from_serial(&permit_transfer_param) .expect("Should be a valid inut parameter"), }, @@ -249,7 +266,7 @@ fn full_auction_flow_with_cis3_permit_function() { let balance_of_alice_and_auction_contract = get_balances(&chain, auction_contract_address, token_contract_address); - assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU8(0), TokenAmountU8(1)]); + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(99), TokenAmountU64(1)]); // Invoke the view entrypoint and check that the tokens are owned by Alice. let item_state = view_item_state(&chain, auction_contract_address); @@ -285,7 +302,7 @@ fn full_auction_flow_with_cis3_permit_function() { #[test] fn full_auction_flow_with_cis3_transfer_function() { - let (mut chain, _keypair, auction_contract_address, token_contract_address) = + let (mut chain, _keypair_alice, _keypair_bob, auction_contract_address, token_contract_address) = initialize_chain_and_auction(); // Create the InitParameter. @@ -293,8 +310,8 @@ fn full_auction_flow_with_cis3_transfer_function() { name: "MyItem".to_string(), end: Timestamp::from_timestamp_millis(1000), start: Timestamp::from_timestamp_millis(5000), - token_id: TokenIdU32(1), - minimum_bid: TokenAmountU8(0), + token_id: TokenIdU8(1), + minimum_bid: TokenAmountU64(0), }; let _update = chain @@ -314,8 +331,13 @@ fn full_auction_flow_with_cis3_transfer_function() { ) .expect("Should be able to add Item"); - let parameter = cis3_nft_sponsored_txs::MintParams { - owner: concordium_smart_contract_testing::Address::Account(ALICE), + let parameter = cis2_multi::MintParams { + owner: concordium_smart_contract_testing::Address::Account(ALICE), + metadata_url: MetadataUrl { + url: "https://some.example/token/0".to_string(), + hash: None, + }, + token_id: concordium_cis2::TokenIdU8(1u8), }; let _update = chain @@ -327,7 +349,7 @@ fn full_auction_flow_with_cis3_transfer_function() { UpdateContractPayload { amount: Amount::from_ccd(0), address: token_contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis3_nft.mint".to_string()), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.mint".to_string()), message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), }, ) @@ -337,7 +359,7 @@ fn full_auction_flow_with_cis3_transfer_function() { let balance_of_alice_and_auction_contract = get_balances(&chain, auction_contract_address, token_contract_address); - assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU8(1), TokenAmountU8(0)]); + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(100), TokenAmountU64(0)]); let additional_data = AdditionalDataIndex { item_index: 0u16, @@ -350,15 +372,15 @@ fn full_auction_flow_with_cis3_transfer_function() { auction_contract_address, OwnedEntrypointName::new_unchecked("bid".to_string()), ), - token_id: TokenIdU32(1), - amount: TokenAmountU8(1), + token_id: TokenIdU8(1), + amount: TokenAmountU64(1), data: AdditionalData::from(to_bytes(&additional_data)), }]); let _update = chain .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis3_nft.transfer".to_string()), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), address: token_contract_address, message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), }) @@ -374,7 +396,7 @@ fn full_auction_flow_with_cis3_transfer_function() { let balance_of_alice_and_auction_contract = get_balances(&chain, auction_contract_address, token_contract_address); - assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU8(0), TokenAmountU8(1)]); + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(99), TokenAmountU64(1)]); // Increment the chain time by 100000 milliseconds. chain.tick_block_time(Duration::from_millis(100000)).expect("Increment chain time"); @@ -657,11 +679,11 @@ fn get_balances( let balance_of_params: ContractBalanceOfQueryParams = BalanceOfQueryParams { queries: vec![ BalanceOfQuery { - token_id: TokenIdU32(1), + token_id: TokenIdU8(1), address: ALICE_ADDR, }, BalanceOfQuery { - token_id: TokenIdU32(1), + token_id: TokenIdU8(1), address: Address::from(auction_contract_address), }, ], @@ -670,7 +692,7 @@ fn get_balances( let invoke = chain .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis3_nft.balanceOf".to_string()), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.balanceOf".to_string()), address: token_contract_address, message: OwnedParameter::from_serial(&balance_of_params) .expect("BalanceOf params"), @@ -685,7 +707,8 @@ fn get_balances( /// Carol is the owner of the auction, which ends at `1000` milliseconds after /// the unix epoch. The 'microCCD per euro' exchange rate is set to `1_000_000`, /// so 1 CCD = 1 euro. -fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, ContractAddress) { +fn initialize_chain_and_auction( +) -> (Chain, AccountKeys, AccountKeys, ContractAddress, ContractAddress) { let mut chain = Chain::builder() .micro_ccd_per_euro( ExchangeRate::new(1_000_000, 1).expect("Exchange rate is in valid range"), @@ -695,7 +718,8 @@ fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, Contr let rng = &mut rand::thread_rng(); - let keypairs = AccountKeys::singleton(rng); + let keypairs_alice = AccountKeys::singleton(rng); + let keypairs_bob = AccountKeys::singleton(rng); let balance = AccountBalance { total: ACC_INITIAL_BALANCE, @@ -704,14 +728,15 @@ fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, Contr }; // Create some accounts accounts on the chain. - chain.create_account(Account::new_with_keys(ALICE, balance, (&keypairs).into())); + chain.create_account(Account::new_with_keys(ALICE, balance, (&keypairs_alice).into())); + chain.create_account(Account::new_with_keys(BOB, balance, (&keypairs_bob).into())); chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(CAROL, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(DAVE, ACC_INITIAL_BALANCE)); // Load and deploy the module. - let module = module_load_v1("../cis3-nft-sponsored-txs/concordium-out/module.wasm.v1") - .expect("Module exists"); + let module = + module_load_v1("../cis2-multi/concordium-out/module.wasm.v1").expect("Module exists"); let deployment = chain.module_deploy_v1(SIGNER, CAROL, module).expect("Deploy valid module"); // Create the InitParameter. @@ -722,7 +747,7 @@ fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, Contr .contract_init(SIGNER, CAROL, Energy::from(10000), InitContractPayload { amount: Amount::zero(), mod_ref: deployment.module_reference, - init_name: OwnedContractName::new_unchecked("init_cis3_nft".to_string()), + init_name: OwnedContractName::new_unchecked("init_cis2_multi".to_string()), param: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), }) .expect("Initialize auction"); @@ -746,5 +771,5 @@ fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, Contr }) .expect("Initialize auction"); - (chain, keypairs, init_auction.contract_address, token.contract_address) + (chain, keypairs_alice, keypairs_bob, init_auction.contract_address, token.contract_address) } From 09748fdd85492ec1cfdd269fad442a7cafb679d5 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 28 Nov 2023 11:42:41 +0300 Subject: [PATCH 07/15] Add several bids test case --- .../sponsored-tx-enabled-auction/src/lib.rs | 24 +- .../tests/tests.rs | 252 ++++++++++++++++-- 2 files changed, 236 insertions(+), 40 deletions(-) diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index 96ad1cf0..808c52c5 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -212,24 +212,8 @@ fn view_item_state(ctx: &ReceiveContext, host: &Host) -> ReceiveResult { - /// The ID of the token received. - pub token_id: T, - /// The amount of tokens received. - pub amount: A, - /// The previous owner of the tokens. - pub from: Address, - /// Some extra information which where sent as part of the transfer. - pub data: AdditionalDataItem, -} - -/// Additional information to include with a transfer. -#[derive(Debug, Serialize, Clone, SchemaType)] -#[concordium(transparent)] -pub struct AdditionalDataItem(#[concordium(size_length = 2)] Vec); - #[derive(Debug, Deserial, Serial, Clone, SchemaType)] +#[concordium(transparent)] pub struct AdditionalDataIndex { pub item_index: u16, } @@ -239,12 +223,12 @@ pub struct AdditionalDataIndex { contract = "sponsored_tx_enabled_auction", name = "bid", mutable, - parameter = "TestOnReceivingCis2Params", + parameter = "OnReceivingCis2Params", error = "Error" )] fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { // Parse the parameter. - let params: TestOnReceivingCis2Params = + let params: OnReceivingCis2Params = ctx.parameter_cursor().get()?; // Ensure the sender is the cis2_token_contract. @@ -254,7 +238,7 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<() bail!(Error::NotTokenContract.into()) }; - let additional_data_index: AdditionalDataIndex = from_bytes(¶ms.data.0)?; + let additional_data_index: AdditionalDataIndex = from_bytes(params.data.as_ref())?; let cis2_contract = host.state().cis2_contract; diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs index b6f7352b..04ad24ac 100644 --- a/examples/sponsored-tx-enabled-auction/tests/tests.rs +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -32,13 +32,8 @@ const DUMMY_SIGNATURE: SignatureEd25519 = SignatureEd25519([ #[test] fn test_add_item() { - let ( - mut chain, - _keypairs_alice, - _keypairs_bob, - auction_contract_address, - _token_contract_address, - ) = initialize_chain_and_auction(); + let (mut chain, _keypairs_alice, auction_contract_address, _token_contract_address) = + initialize_chain_and_auction(); // Create the InitParameter. let parameter = AddItemParameter { @@ -125,13 +120,8 @@ fn test_add_item() { #[test] fn full_auction_flow_with_cis3_permit_function() { - let ( - mut chain, - keypairs_alice, - _keypairs_bob, - auction_contract_address, - token_contract_address, - ) = initialize_chain_and_auction(); + let (mut chain, keypairs_alice, auction_contract_address, token_contract_address) = + initialize_chain_and_auction(); // Create the InitParameter. let parameter = AddItemParameter { @@ -300,9 +290,233 @@ fn full_auction_flow_with_cis3_permit_function() { assert_eq!(item_state.auction_state, AuctionState::Sold(ALICE)); } +#[test] +fn full_auction_flow_with_several_bids() { + let (mut chain, keypairs_alice, auction_contract_address, token_contract_address) = + initialize_chain_and_auction(); + + // Create the InitParameter. + let parameter = AddItemParameter { + name: "MyItem".to_string(), + end: Timestamp::from_timestamp_millis(1000), + start: Timestamp::from_timestamp_millis(5000), + token_id: TokenIdU8(1), + minimum_bid: TokenAmountU64(0), + }; + + let _update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: auction_contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.addItem".to_string(), + ), + message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }, + ) + .expect("Should be able to add Item"); + + let parameter = cis2_multi::MintParams { + owner: concordium_smart_contract_testing::Address::Account(ALICE), + metadata_url: MetadataUrl { + url: "https://some.example/token/0".to_string(), + hash: None, + }, + token_id: concordium_cis2::TokenIdU8(1u8), + }; + + let _update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: token_contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.mint".to_string()), + message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }, + ) + .expect("Should be able to finalize"); + + let additional_data = AdditionalDataIndex { + item_index: 0u16, + }; + + // Check balances in state. + let balance_of_alice_and_auction_contract = + get_balances(&chain, auction_contract_address, token_contract_address); + + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(100), TokenAmountU64(0)]); + + // Create input parameters for the `permit` transfer function. + let transfer = concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Contract( + auction_contract_address, + OwnedEntrypointName::new_unchecked("bid".to_string()), + ), + token_id: TokenIdU8(1), + amount: TokenAmountU64(1), + data: AdditionalData::from(to_bytes(&additional_data)), + }; + let payload = TransferParams::from(vec![transfer]); + + // The `viewMessageHash` function uses the same input parameter `PermitParam` as + // the `permit` function. The `PermitParam` type includes a `signature` and + // a `signer`. Because these two values (`signature` and `signer`) are not + // read in the `viewMessageHash` function, any value can be used and we choose + // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. + let signature_map = BTreeMap::from([(0u8, CredentialSignatures { + sigs: BTreeMap::from([(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE))]), + })]); + + let mut permit_transfer_param = PermitParam { + signature: AccountSignatures { + sigs: signature_map, + }, + signer: ALICE, + message: PermitMessage { + timestamp: Timestamp::from_timestamp_millis(10_000_000_000), + contract_address: ContractAddress::new(0, 0), + entry_point: OwnedEntrypointName::new_unchecked("transfer".into()), + nonce: 0, + payload: to_bytes(&payload), + }, + }; + + // Get the message hash to be signed. + let invoke = chain + .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: token_contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.viewMessageHash".to_string()), + message: OwnedParameter::from_serial(&permit_transfer_param) + .expect("Should be a valid inut parameter"), + }) + .expect("Should be able to query viewMessageHash"); + + let message_hash: HashSha2256 = + from_bytes(&invoke.return_value).expect("Should return a valid result"); + + permit_transfer_param.signature = keypairs_alice.sign_message(&to_bytes(&message_hash)); + + // Transfer token with the permit function. + let _update = chain + .contract_update( + Signer::with_one_key(), + BOB, + BOB_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: token_contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.permit".to_string()), + message: OwnedParameter::from_serial(&permit_transfer_param) + .expect("Should be a valid inut parameter"), + }, + ) + .expect("Should be able to transfer token with permit"); + + // Check balances in state. + let balance_of_alice_and_auction_contract = + get_balances(&chain, auction_contract_address, token_contract_address); + + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(99), TokenAmountU64(1)]); + + let parameter = cis2_multi::MintParams { + owner: concordium_smart_contract_testing::Address::Account(BOB), + metadata_url: MetadataUrl { + url: "https://some.example/token/0".to_string(), + hash: None, + }, + token_id: concordium_cis2::TokenIdU8(1u8), + }; + + let _update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(BOB), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: token_contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.mint".to_string()), + message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }, + ) + .expect("Should be able to finalize"); + + // Transfer one token from Alice to bid function in auction. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: BOB_ADDR, + to: Receiver::Contract( + auction_contract_address, + OwnedEntrypointName::new_unchecked("bid".to_string()), + ), + token_id: TokenIdU8(1), + amount: TokenAmountU64(2), + data: AdditionalData::from(to_bytes(&additional_data)), + }]); + + let _update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), + address: token_contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that ALICE has been refunded her initial bid and the auction contract + // recieved the 2 tokens from BOB. + let balance_of_alice_and_auction_contract = + get_balances(&chain, auction_contract_address, token_contract_address); + + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(100), TokenAmountU64(2)]); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let item_state = view_item_state(&chain, auction_contract_address); + + // Check that item is not sold yet. + assert_eq!(item_state.auction_state, AuctionState::NotSoldYet); + + // Increment the chain time by 100000 milliseconds. + chain.tick_block_time(Duration::from_millis(100000)).expect("Increment chain time"); + + let _update = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(0), + address: auction_contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "sponsored_tx_enabled_auction.finalize".to_string(), + ), + message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), + }, + ) + .expect("Should be able to finalize"); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let item_state = view_item_state(&chain, auction_contract_address); + + assert_eq!(item_state.auction_state, AuctionState::Sold(BOB)); +} + #[test] fn full_auction_flow_with_cis3_transfer_function() { - let (mut chain, _keypair_alice, _keypair_bob, auction_contract_address, token_contract_address) = + let (mut chain, _keypair_alice, auction_contract_address, token_contract_address) = initialize_chain_and_auction(); // Create the InitParameter. @@ -707,8 +921,7 @@ fn get_balances( /// Carol is the owner of the auction, which ends at `1000` milliseconds after /// the unix epoch. The 'microCCD per euro' exchange rate is set to `1_000_000`, /// so 1 CCD = 1 euro. -fn initialize_chain_and_auction( -) -> (Chain, AccountKeys, AccountKeys, ContractAddress, ContractAddress) { +fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, ContractAddress) { let mut chain = Chain::builder() .micro_ccd_per_euro( ExchangeRate::new(1_000_000, 1).expect("Exchange rate is in valid range"), @@ -719,7 +932,6 @@ fn initialize_chain_and_auction( let rng = &mut rand::thread_rng(); let keypairs_alice = AccountKeys::singleton(rng); - let keypairs_bob = AccountKeys::singleton(rng); let balance = AccountBalance { total: ACC_INITIAL_BALANCE, @@ -729,7 +941,7 @@ fn initialize_chain_and_auction( // Create some accounts accounts on the chain. chain.create_account(Account::new_with_keys(ALICE, balance, (&keypairs_alice).into())); - chain.create_account(Account::new_with_keys(BOB, balance, (&keypairs_bob).into())); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(CAROL, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(DAVE, ACC_INITIAL_BALANCE)); @@ -771,5 +983,5 @@ fn initialize_chain_and_auction( }) .expect("Initialize auction"); - (chain, keypairs_alice, keypairs_bob, init_auction.contract_address, token.contract_address) + (chain, keypairs_alice, init_auction.contract_address, token.contract_address) } From 888344aa1b2f2d01d65bd88004043c69073f2e1a Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 28 Nov 2023 21:13:49 +0300 Subject: [PATCH 08/15] Add comments --- examples/cis2-multi/tests/tests.rs | 2 +- .../sponsored-tx-enabled-auction/src/lib.rs | 345 +++++++++++------- .../tests/tests.rs | 24 +- 3 files changed, 221 insertions(+), 150 deletions(-) diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index 694d574b..d65e1837 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -772,7 +772,7 @@ fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress) { mod_ref: deployment.module_reference, init_name: OwnedContractName::new_unchecked("init_cis2_multi".to_string()), param: OwnedParameter::from_serial(&TokenAmountU64(100)) - .expect("UpdateOperator params"), + .expect("Init params"), }) .expect("Initialize contract"); diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index 808c52c5..a70c14bd 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -1,25 +1,41 @@ //! # Implementation of an auction smart contract //! -//! Accounts can invoke the bid function to participate in the auction. -//! An account has to send some CCD when invoking the bid function. -//! This CCD amount has to exceed the current highest bid by a minimum raise -//! to be accepted by the smart contract. +//! The contract is initialized with a cis2 token contract. +//! Any `token_id` from this cis2 token contract can be used as a payment +//! token when auctioning an item within this contract. //! -//! The minimum raise is set when initializing and is defined in Euro cent. -//! The contract uses the current exchange rate used by the chain by the time of -//! the bid, to convert the bid into EUR. +//! To initiate a new auction, any account can call the `addItem` entry point. +//! The account initiating the auction (referred to as the creator) is required +//! to specify the start time, end time, minimum bid, and the `token_id` +//! associated with the item. At this stage, the item/auction is assigned the +//! next consecutive index for future reference. +//! +//! Any account can bid for an item. +//! The `bid` entry point in this contract is not meant to be invoked directly +//! but rather through the `onCIS2Receive` hook mechanism in the cis2 token +//! contract. The `bid` entry point can be invoked via a sponsored transaction +//! mechanism (`permit` entry point) in case it is implemented in the cis2 token +//! contract. The bidding flow starts with an account invoking either the +//! `transfer` or the `permit` entry point in the cis2 token contract and +//! including the `item_index` in the `additionalData` section of the input +//! parameter. The `transfer` or the `permit` entry point will send some token +//! amounts to this contract from the bidder. If the token amount exceeds the +//! current highest bid, the bid is accepted and the previous highest bidder is +//! refunded its token investment. //! //! The smart contract keeps track of the current highest bidder as well as -//! the CCD amount of the highest bid. The CCD balance of the smart contract -//! represents the highest bid. When a new highest bid is accepted by the smart +//! the token amount of the highest bid. The token balances of the smart +//! contract represent the sums of all highest bids from the items (that haven't +//! been finalized). When a new highest bid is accepted by the smart //! contract, the smart contract refunds the old highest bidder. //! //! Bids have to be placed before the auction ends. The participant with the //! highest bid (the last bidder) wins the auction. //! -//! After the auction ends, any account can finalize the auction. The owner of -//! the smart contract instance receives the highest bid (the balance of this -//! contract) when the auction is finalized. This can be done only once. +//! After the auction ends for a specific item, any account can finalize the +//! auction. The creator of that auction receives the highest bid when the +//! auction is finalized and the item is marked as sold to the highest bidder. +//! This can be done only once. //! //! Terminology: `Accounts` are derived from a public/private key pair. //! `Contract` instances are created by deploying a smart contract @@ -30,15 +46,16 @@ use concordium_cis2::*; use concordium_std::*; -/// Contract token ID type. -/// To save bytes we use a token ID type limited to a `u32`. +/// Contract token ID type. It has to be the `ContractTokenId` from the cis2 +/// token contract. pub type ContractTokenId = TokenIdU8; -/// Contract token amount. -/// Since the tokens are non-fungible the total supply of any token will be at -/// most 1 and it is fine to use a small type for representing token amounts. +/// Contract token amount. It has to be the `ContractTokenAmount` from the cis2 +/// token contract. pub type ContractTokenAmount = TokenAmountU64; +/// TransferParameter type that has a specific `ContractTokenId` and +/// `ContractTokenAmount` set. pub type TransferParameter = TransferParams; /// The state of the auction. @@ -56,83 +73,122 @@ pub enum AuctionState { /// The state of the smart contract. /// This state can be viewed by querying the node with the command -/// `concordium-client contract invoke` using the `view` function as entrypoint. +/// `concordium-client contract invoke` using the `view` function as entry +/// point. #[derive(Debug, Serialize, SchemaType, Clone, PartialEq, Eq)] pub struct ItemState { - /// State of the auction + /// State of the auction. pub auction_state: AuctionState, - /// The highest bidder so far; The variant `None` represents + /// The highest bidder so far. The variant `None` represents /// that no bidder has taken part in the auction yet. pub highest_bidder: Option, - /// The item to be sold (to be displayed by the front-end) + /// The item name to be sold. pub name: String, - /// Time when auction ends (to be displayed by the front-end) + /// The time when the auction ends. pub end: Timestamp, + /// The time when the auction starts. pub start: Timestamp, + /// In case `highest_bidder==None`, the minimum bid as set by the creator. + /// In case `highest_bidder==Some(AccountAddress)`, the highest bid that a + /// bidder has bid so far. pub highest_bid: TokenAmountU64, + /// The `token_id` from the cis2 token contract used as payment token. pub token_id: TokenIdU8, + /// The account address that created the auction for this item. pub creator: AccountAddress, } /// The state of the smart contract. /// This state can be viewed by querying the node with the command -/// `concordium-client contract invoke` using the `view` function as entrypoint. -// #[derive(Debug, Serialize, SchemaType, Clone)] +/// `concordium-client contract invoke` using the `view` function as entry +/// point. #[derive(Serial, DeserialWithState, Debug)] #[concordium(state_parameter = "S")] pub struct State { + /// A mapping including all items that have been added to this contract. items: StateMap, + /// The cis2 token contract. Its tokens can be used to bid for items in this + /// contract. cis2_contract: ContractAddress, + /// A counter that is sequentially increased whenever a new item is added to + /// the contract. counter: u16, } +/// The return_value for the entry point `view` which returns the contract +/// state. #[derive(Serialize, SchemaType, Debug, Eq, PartialEq)] pub struct ReturnParamView { + /// A vector including all items that have been added to this contract. pub item_states: Vec<(u16, ItemState)>, + /// The cis2 token contract. Its tokens can be used to bid for items in this + /// contract. pub cis2_contract: ContractAddress, + /// A counter that is sequentially increased whenever a new item is added to + /// the contract. pub counter: u16, } -/// Type of the parameter to the `init` function +/// The parameter for the entry point `addItem` that adds a new item to this +/// contract. #[derive(Serialize, SchemaType)] pub struct AddItemParameter { - /// The item to be sold (to be displayed by the front-end) + /// The item name to be sold. pub name: String, - /// Time when auction ends (to be displayed by the front-end) + /// The time when the auction ends. pub end: Timestamp, + /// The time when the auction starts. pub start: Timestamp, + // The minimum bid that the first bidder has to overbid. pub minimum_bid: TokenAmountU64, + // The `token_id` from the cis2 token contract that the item should be sold for. pub token_id: TokenIdU8, } -/// `bid` function errors +/// The `additionData` that has to be passed to the `bid` entry point. +#[derive(Debug, Deserial, Serial, Clone, SchemaType)] +#[concordium(transparent)] +pub struct AdditionalDataIndex { + /// The index of the item. + pub item_index: u16, +} + +/// Errors of this contract. #[derive(Debug, PartialEq, Eq, Clone, Reject, Serialize, SchemaType)] pub enum Error { + // Raised when adding an item; The start time needs to be strictly smaller than the end time. + StartEndTimeError, //-1 + // Raised when adding an item; The end time needs to be in the future. + EndTimeError, //-2 /// Raised when a contract tries to bid; Only accounts /// are allowed to bid. - OnlyAccount, //-1 - /// Raised when new bid amount is lower than current highest bid. - BidBelowCurrentBid, //-2 - /// Raised when bid is placed after auction end time passed. - BidTooLate, //-3 - /// Raised when bid is placed after auction has been finalized. - AuctionAlreadyFinalized, //-4 - /// - NoItem, //-5 - /// Raised when finalizing an auction before auction end time passed - AuctionStillActive, //-6 - /// - NotTokenContract, //-7 - WrongTokenID, //-8 + OnlyAccount, //-3 + /// Raised when the new bid amount is not greater than the current highest + /// bid. + BidNotGreaterCurrentBid, //-4 + /// Raised when the bid is placed after the auction end time passed. + BidTooLate, //-5 + /// Raised when the bid is placed after the auction has been finalized. + AuctionAlreadyFinalized, //-6 + /// Raised when the item index cannot be found in the contract. + NoItem, //-7 + /// Raised when finalizing an auction before the auction end time passed. + AuctionStillActive, //-8 + /// Raised when someone else than the cis2 token contract invokes the `bid` + /// entry point. + NotTokenContract, //-9 + /// Raised when payment is attempted with a different `token_id` than + /// specified for an item. + WrongTokenID, //-10 } -/// Init function that creates a new auction +/// Init entry point that creates a new auction contract. #[init(contract = "sponsored_tx_enabled_auction", parameter = "ContractAddress")] fn auction_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { - // Getting input parameters + // Getting input parameters. let contract: ContractAddress = ctx.parameter_cursor().get()?; - // Creating `State` + // Creating `State`. let state = State { items: state_builder.new_map(), cis2_contract: contract, @@ -141,8 +197,12 @@ fn auction_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResu Ok(state) } -/// ViewHighestBid function that returns the highest bid which is the balance of -/// the contract +/// AddItem entry point to add an item to this contract. To initiate a new +/// auction, any account can call this entry point. The account initiating the +/// auction (referred to as the creator) is required to specify the start time, +/// end time, minimum bid, and the `token_id` associated with the item. At this +/// stage, the item/auction is assigned the next consecutive index for future +/// reference. #[receive( contract = "sponsored_tx_enabled_auction", name = "addItem", @@ -150,11 +210,15 @@ fn auction_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResu mutable )] fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { - // Getting input parameters + // Getting input parameters. let item: AddItemParameter = ctx.parameter_cursor().get()?; - let counter = host.state_mut().counter; - host.state_mut().counter = counter + 1; + // Ensure start < end. + ensure!(item.start < item.end, Error::StartEndTimeError.into()); + + let slot_time = ctx.metadata().slot_time(); + // Ensure the auction can run. + ensure!(slot_time <= item.end, Error::EndTimeError.into()); // Ensure that only accounts can add an item. let sender_address = match ctx.sender() { @@ -162,6 +226,11 @@ fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { Address::Account(account_address) => account_address, }; + // Assign an index to the item/auction. + let counter = host.state_mut().counter; + host.state_mut().counter = counter + 1; + + // Insert the item into the state. host.state_mut().items.insert(counter, ItemState { auction_state: AuctionState::NotSoldYet, highest_bidder: None, @@ -176,49 +245,17 @@ fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { Ok(()) } -/// View function that returns the content of the state -#[receive( - contract = "sponsored_tx_enabled_auction", - name = "view", - return_value = "ReturnParamView" -)] -fn view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { - let state = host.state(); - - let mut inner_state = Vec::new(); - for (index, item_state) in state.items.iter() { - inner_state.push((*index, item_state.clone())); - } - - Ok(ReturnParamView { - item_states: inner_state, - cis2_contract: host.state().cis2_contract, - counter: host.state().counter, - }) -} - -/// ViewHighestBid function that returns the highest bid which is the balance of -/// the contract -#[receive( - contract = "sponsored_tx_enabled_auction", - name = "viewItemState", - return_value = "ItemState", - parameter = "u16" -)] -fn view_item_state(ctx: &ReceiveContext, host: &Host) -> ReceiveResult { - // Getting input parameters - let item_index: u16 = ctx.parameter_cursor().get()?; - let item = host.state().items.get(&item_index).map(|x| x.clone()).ok_or(Error::NoItem)?; - Ok(item) -} - -#[derive(Debug, Deserial, Serial, Clone, SchemaType)] -#[concordium(transparent)] -pub struct AdditionalDataIndex { - pub item_index: u16, -} - -/// Receive function for accounts to place a bid in the auction +/// The `bid` entry point in this contract is not meant to be invoked directly +/// but rather through the `onCIS2Receive` hook mechanism in the cis2 token +/// contract. Any account can bid for an item. The `bid` entry point can be +/// invoked via a sponsored transaction mechanism (`permit` entry point) in case +/// it is implemented in the cis2 token contract. The bidding flow starts with +/// an account invoking either the `transfer` or the `permit` entry point in the +/// cis2 token contract and including the `item_index` in the `additionalData` +/// section of the input parameter. The `transfer` or the `permit` entry point +/// will send some token amounts to this contract from the bidder. If the token +/// amount exceeds the current highest bid, the bid is accepted and the previous +/// highest bidder is refunded its token investment. #[receive( contract = "sponsored_tx_enabled_auction", name = "bid", @@ -227,23 +264,25 @@ pub struct AdditionalDataIndex { error = "Error" )] fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { - // Parse the parameter. + // Getting input parameters. let params: OnReceivingCis2Params = ctx.parameter_cursor().get()?; - // Ensure the sender is the cis2_token_contract. + // Ensure the sender is the cis2 token contract. if let Address::Contract(contract) = ctx.sender() { ensure_eq!(contract, host.state().cis2_contract, Error::NotTokenContract.into()); } else { bail!(Error::NotTokenContract.into()) }; + // Get the item_index from the additionalData. let additional_data_index: AdditionalDataIndex = from_bytes(params.data.as_ref())?; - let cis2_contract = host.state().cis2_contract; - - let item = - host.state_mut().items.get(&additional_data_index.item_index).ok_or(Error::NoItem)?; + let mut item = host + .state_mut() + .items + .entry(additional_data_index.item_index) + .occupied_or(Error::NoItem)?; ensure_eq!(item.token_id, params.token_id, Error::WrongTokenID.into()); @@ -253,18 +292,22 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<() Address::Account(account_address) => account_address, }; - // Ensure the auction has not been finalized yet + // Ensure the auction has not been finalized yet. ensure_eq!(item.auction_state, AuctionState::NotSoldYet, Error::AuctionAlreadyFinalized.into()); let slot_time = ctx.metadata().slot_time(); - // Ensure the auction has not ended yet + // Ensure the auction has not ended yet. ensure!(slot_time <= item.end, Error::BidTooLate.into()); - // Ensure that the new bid exceeds the highest bid so far - ensure!(params.amount > item.highest_bid, Error::BidBelowCurrentBid.into()); + // Ensure that the new bid exceeds the highest bid so far. + let old_highest_bid = item.highest_bid; + ensure!(params.amount > old_highest_bid, Error::BidNotGreaterCurrentBid.into()); - if let Some(account_address) = item.highest_bidder { - // Refunding old highest bidder; + // Set the new highest_bid. + item.highest_bid = params.amount; + + if let Some(account_address) = item.highest_bidder.replace(bidder_address) { + // Refunding old highest bidder. // This transfer (given enough NRG of course) always succeeds because // the `account_address` exists since it was recorded when it // placed a bid. If an `account_address` exists, and the @@ -275,35 +318,29 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<() let parameter = TransferParameter { 0: vec![Transfer { token_id: item.token_id, - amount: item.highest_bid, + amount: old_highest_bid, from: concordium_std::Address::Contract(ctx.self_address()), to: concordium_cis2::Receiver::Account(account_address), data: AdditionalData::empty(), }], }; + drop(item); + host.invoke_contract( - &cis2_contract, + &host.state().cis2_contract.clone(), ¶meter, EntrypointName::new_unchecked("transfer"), Amount::zero(), )?; } - let mut item = host - .state_mut() - .items - .entry(additional_data_index.item_index) - .occupied_or(Error::NoItem)?; - item.highest_bidder = Some(bidder_address); - item.highest_bid = params.amount; - Ok(()) } -/// Receive function used to finalize the auction. It sends the highest bid (the -/// current balance of this smart contract) to the owner of the smart contract -/// instance. +/// The `finalize` entry point can be called by anyone. It sends the highest bid +/// in tokens to the creator of the auction if the item is past its auction end +/// time. #[receive( contract = "sponsored_tx_enabled_auction", name = "finalize", @@ -312,31 +349,31 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<() error = "Error" )] fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { - // Getting input parameters + // Getting input parameter. let item_index: u16 = ctx.parameter_cursor().get()?; - let cis2_contract = host.state().cis2_contract; - let item = host.state_mut().items.get(&item_index).ok_or(Error::NoItem)?; + // Get the item from state. + let mut item = host.state_mut().items.entry(item_index).occupied_or(Error::NoItem)?; - // Ensure the auction has not been finalized yet + // Ensure the auction has not been finalized yet. ensure_eq!(item.auction_state, AuctionState::NotSoldYet, Error::AuctionAlreadyFinalized.into()); let slot_time = ctx.metadata().slot_time(); - // Ensure the auction has ended already + // Ensure the auction has ended already. ensure!(slot_time > item.end, Error::AuctionStillActive.into()); if let Some(account_address) = item.highest_bidder { - // Marking the highest bid (the last bidder) as winner of the auction - // item.auction_state = AuctionState::Sold(account_address); - // let owner = ctx.owner(); - // let balance = host.self_balance(); - // // Sending the highest bid (the balance of this contract) to the owner of the - // // smart contract instance; - // // This transfer (given enough NRG of course) always succeeds because the - // // If an account exists, and the contract has the funds then the - // // transfer will always succeed. - // host.invoke_transfer(&owner, balance).unwrap_abort(); + // Marking the highest bidder (the last bidder) as winner of the auction. + item.auction_state = AuctionState::Sold(account_address); + // Sending the highest bid in tokens to the creator of the auction. + // This transfer (given enough NRG of course) always succeeds because + // the `creator` exists since it was recorded when it + // added the item. If an `account_address` exists, and the + // contract has the funds then the transfer will always succeed. + // Please consider using a pull-over-push pattern when expanding this + // smart contract to allow smart contract instances to + // participate in the auction as well. https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/ let parameter = TransferParameter { 0: vec![Transfer { token_id: item.token_id, @@ -347,16 +384,50 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResu }], }; + drop(item); + host.invoke_contract( - &cis2_contract, + &host.state().cis2_contract.clone(), ¶meter, EntrypointName::new_unchecked("transfer"), Amount::zero(), )?; - - let mut item = host.state_mut().items.entry(item_index).occupied_or(Error::NoItem)?; - item.auction_state = AuctionState::Sold(account_address); } Ok(()) } + +/// View function that returns the content of the state. +#[receive( + contract = "sponsored_tx_enabled_auction", + name = "view", + return_value = "ReturnParamView" +)] +fn view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { + let state = host.state(); + + let mut inner_state = Vec::new(); + for (index, item_state) in state.items.iter() { + inner_state.push((*index, item_state.to_owned())); + } + + Ok(ReturnParamView { + item_states: inner_state, + cis2_contract: host.state().cis2_contract, + counter: host.state().counter, + }) +} + +/// ViewItemState function that returns the state of a specific item. +#[receive( + contract = "sponsored_tx_enabled_auction", + name = "viewItemState", + return_value = "ItemState", + parameter = "u16" +)] +fn view_item_state(ctx: &ReceiveContext, host: &Host) -> ReceiveResult { + // Getting input parameter. + let item_index: u16 = ctx.parameter_cursor().get()?; + let item = host.state().items.get(&item_index).map(|x| x.to_owned()).ok_or(Error::NoItem)?; + Ok(item) +} diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs index 04ad24ac..3c956aec 100644 --- a/examples/sponsored-tx-enabled-auction/tests/tests.rs +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -38,8 +38,8 @@ fn test_add_item() { // Create the InitParameter. let parameter = AddItemParameter { name: "MyItem".to_string(), - end: Timestamp::from_timestamp_millis(1000), - start: Timestamp::from_timestamp_millis(5000), + start: Timestamp::from_timestamp_millis(1_000), + end: Timestamp::from_timestamp_millis(5000), token_id: TokenIdU8(1), minimum_bid: TokenAmountU64(3), }; @@ -81,8 +81,8 @@ fn test_add_item() { auction_state: AuctionState::NotSoldYet, highest_bidder: None, name: "MyItem".to_string(), - end: Timestamp::from_timestamp_millis(1000), - start: Timestamp::from_timestamp_millis(5000), + start: Timestamp::from_timestamp_millis(1000), + end: Timestamp::from_timestamp_millis(5000), token_id: TokenIdU8(1), creator: ALICE, highest_bid: TokenAmountU64(3), @@ -110,8 +110,8 @@ fn test_add_item() { auction_state: AuctionState::NotSoldYet, highest_bidder: None, name: "MyItem".to_string(), - end: Timestamp::from_timestamp_millis(1000), - start: Timestamp::from_timestamp_millis(5000), + start: Timestamp::from_timestamp_millis(1000), + end: Timestamp::from_timestamp_millis(5000), token_id: TokenIdU8(1), creator: ALICE, highest_bid: TokenAmountU64(3), @@ -126,8 +126,8 @@ fn full_auction_flow_with_cis3_permit_function() { // Create the InitParameter. let parameter = AddItemParameter { name: "MyItem".to_string(), - end: Timestamp::from_timestamp_millis(1000), - start: Timestamp::from_timestamp_millis(5000), + start: Timestamp::from_timestamp_millis(1000), + end: Timestamp::from_timestamp_millis(5000), token_id: TokenIdU8(1), minimum_bid: TokenAmountU64(0), }; @@ -298,8 +298,8 @@ fn full_auction_flow_with_several_bids() { // Create the InitParameter. let parameter = AddItemParameter { name: "MyItem".to_string(), - end: Timestamp::from_timestamp_millis(1000), - start: Timestamp::from_timestamp_millis(5000), + start: Timestamp::from_timestamp_millis(1000), + end: Timestamp::from_timestamp_millis(5000), token_id: TokenIdU8(1), minimum_bid: TokenAmountU64(0), }; @@ -522,8 +522,8 @@ fn full_auction_flow_with_cis3_transfer_function() { // Create the InitParameter. let parameter = AddItemParameter { name: "MyItem".to_string(), - end: Timestamp::from_timestamp_millis(1000), - start: Timestamp::from_timestamp_millis(5000), + start: Timestamp::from_timestamp_millis(1000), + end: Timestamp::from_timestamp_millis(5000), token_id: TokenIdU8(1), minimum_bid: TokenAmountU64(0), }; From 6b9dd25002e36265fc339583e109c649b151bccf Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 28 Nov 2023 21:49:08 +0300 Subject: [PATCH 09/15] Add comments in test cases --- examples/cis2-multi/tests/tests.rs | 3 +- .../tests/tests.rs | 358 +++--------------- 2 files changed, 62 insertions(+), 299 deletions(-) diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index d65e1837..6ea49459 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -771,8 +771,7 @@ fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress) { amount: Amount::zero(), mod_ref: deployment.module_reference, init_name: OwnedContractName::new_unchecked("init_cis2_multi".to_string()), - param: OwnedParameter::from_serial(&TokenAmountU64(100)) - .expect("Init params"), + param: OwnedParameter::from_serial(&TokenAmountU64(100)).expect("Init params"), }) .expect("Initialize contract"); diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs index 3c956aec..b611dc83 100644 --- a/examples/sponsored-tx-enabled-auction/tests/tests.rs +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -1,6 +1,4 @@ -//! Tests for the auction smart contract. -use std::collections::BTreeMap; - +//! Tests for the sponsored-tx-enabled-auction smart contract. use cis2_multi::{ ContractBalanceOfQueryParams, ContractBalanceOfQueryResponse, PermitMessage, PermitParam, }; @@ -13,14 +11,14 @@ use concordium_std::{ AccountSignatures, CredentialSignatures, HashSha2256, MetadataUrl, SignatureEd25519, }; use sponsored_tx_enabled_auction::*; +use std::collections::BTreeMap; -/// The tests accounts. +/// The test accounts. const ALICE: AccountAddress = AccountAddress([0; 32]); const ALICE_ADDR: Address = Address::Account(AccountAddress([0; 32])); const BOB: AccountAddress = AccountAddress([1; 32]); const BOB_ADDR: Address = Address::Account(AccountAddress([1; 32])); const CAROL: AccountAddress = AccountAddress([2; 32]); -const DAVE: AccountAddress = AccountAddress([3; 32]); const SIGNER: Signer = Signer::with_one_key(); const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); @@ -38,12 +36,13 @@ fn test_add_item() { // Create the InitParameter. let parameter = AddItemParameter { name: "MyItem".to_string(), - start: Timestamp::from_timestamp_millis(1_000), + start: Timestamp::from_timestamp_millis(1000), end: Timestamp::from_timestamp_millis(5000), token_id: TokenIdU8(1), minimum_bid: TokenAmountU64(3), }; + // Add item. let _update = chain .contract_update( SIGNER, @@ -61,7 +60,7 @@ fn test_add_item() { ) .expect("Should be able to add Item"); - // Invoke the view entrypoint and check that the tokens are owned by Alice. + // Invoke the view entry point and check that the item was added. let invoke = chain .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), @@ -73,7 +72,6 @@ fn test_add_item() { }) .expect("Invoke view"); - // Check that the tokens are owned by Alice. let rv: ReturnParamView = invoke.parse_return_value().expect("View return value"); assert_eq!(rv, ReturnParamView { @@ -91,7 +89,7 @@ fn test_add_item() { counter: 1, }); - // Invoke the view entrypoint and check that the tokens are owned by Alice. + // Invoke the viewItemState entry point and check that the item was added. let invoke = chain .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), @@ -101,9 +99,8 @@ fn test_add_item() { address: auction_contract_address, message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), }) - .expect("Invoke view"); + .expect("Invoke viewItemState"); - // Check that the tokens are owned by Alice. let rv: ItemState = invoke.parse_return_value().expect("ViewItemState return value"); assert_eq!(rv, ItemState { @@ -132,6 +129,7 @@ fn full_auction_flow_with_cis3_permit_function() { minimum_bid: TokenAmountU64(0), }; + // Add item. let _update = chain .contract_update( SIGNER, @@ -149,6 +147,7 @@ fn full_auction_flow_with_cis3_permit_function() { ) .expect("Should be able to add Item"); + // Airdrop tokens to ALICE. let parameter = cis2_multi::MintParams { owner: concordium_smart_contract_testing::Address::Account(ALICE), metadata_url: MetadataUrl { @@ -171,19 +170,18 @@ fn full_auction_flow_with_cis3_permit_function() { message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), }, ) - .expect("Should be able to finalize"); - - let additional_data = AdditionalDataIndex { - item_index: 0u16, - }; + .expect("Should be able to mint"); // Check balances in state. let balance_of_alice_and_auction_contract = get_balances(&chain, auction_contract_address, token_contract_address); - assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(100), TokenAmountU64(0)]); // Create input parameters for the `permit` transfer function. + let additional_data = AdditionalDataIndex { + item_index: 0u16, + }; + let transfer = concordium_cis2::Transfer { from: ALICE_ADDR, to: Receiver::Contract( @@ -233,6 +231,7 @@ fn full_auction_flow_with_cis3_permit_function() { let message_hash: HashSha2256 = from_bytes(&invoke.return_value).expect("Should return a valid result"); + // Sign message hash. permit_transfer_param.signature = keypairs_alice.sign_message(&to_bytes(&message_hash)); // Transfer token with the permit function. @@ -255,18 +254,16 @@ fn full_auction_flow_with_cis3_permit_function() { // Check balances in state. let balance_of_alice_and_auction_contract = get_balances(&chain, auction_contract_address, token_contract_address); - assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(99), TokenAmountU64(1)]); - // Invoke the view entrypoint and check that the tokens are owned by Alice. + // Check that the item is not sold yet. let item_state = view_item_state(&chain, auction_contract_address); - - // Check that item is not sold yet. assert_eq!(item_state.auction_state, AuctionState::NotSoldYet); // Increment the chain time by 100000 milliseconds. chain.tick_block_time(Duration::from_millis(100000)).expect("Increment chain time"); + // Finalize the auction. let _update = chain .contract_update( SIGNER, @@ -284,9 +281,8 @@ fn full_auction_flow_with_cis3_permit_function() { ) .expect("Should be able to finalize"); - // Invoke the view entrypoint and check that the tokens are owned by Alice. + // Check that the item is sold to ALICE. let item_state = view_item_state(&chain, auction_contract_address); - assert_eq!(item_state.auction_state, AuctionState::Sold(ALICE)); } @@ -304,6 +300,7 @@ fn full_auction_flow_with_several_bids() { minimum_bid: TokenAmountU64(0), }; + // Add item. let _update = chain .contract_update( SIGNER, @@ -321,6 +318,7 @@ fn full_auction_flow_with_several_bids() { ) .expect("Should be able to add Item"); + // Airdrop tokens to ALICE. let parameter = cis2_multi::MintParams { owner: concordium_smart_contract_testing::Address::Account(ALICE), metadata_url: MetadataUrl { @@ -343,19 +341,18 @@ fn full_auction_flow_with_several_bids() { message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), }, ) - .expect("Should be able to finalize"); - - let additional_data = AdditionalDataIndex { - item_index: 0u16, - }; + .expect("Should be able to mint"); // Check balances in state. let balance_of_alice_and_auction_contract = get_balances(&chain, auction_contract_address, token_contract_address); - assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(100), TokenAmountU64(0)]); // Create input parameters for the `permit` transfer function. + let additional_data = AdditionalDataIndex { + item_index: 0u16, + }; + let transfer = concordium_cis2::Transfer { from: ALICE_ADDR, to: Receiver::Contract( @@ -405,6 +402,7 @@ fn full_auction_flow_with_several_bids() { let message_hash: HashSha2256 = from_bytes(&invoke.return_value).expect("Should return a valid result"); + // Sign message hash. permit_transfer_param.signature = keypairs_alice.sign_message(&to_bytes(&message_hash)); // Transfer token with the permit function. @@ -427,9 +425,9 @@ fn full_auction_flow_with_several_bids() { // Check balances in state. let balance_of_alice_and_auction_contract = get_balances(&chain, auction_contract_address, token_contract_address); - assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(99), TokenAmountU64(1)]); + // Airdrop tokens to BOB. let parameter = cis2_multi::MintParams { owner: concordium_smart_contract_testing::Address::Account(BOB), metadata_url: MetadataUrl { @@ -452,9 +450,9 @@ fn full_auction_flow_with_several_bids() { message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), }, ) - .expect("Should be able to finalize"); + .expect("Should be able to mint"); - // Transfer one token from Alice to bid function in auction. + // Transfer two tokens from BOB to the auction contract. let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { from: BOB_ADDR, to: Receiver::Contract( @@ -476,21 +474,19 @@ fn full_auction_flow_with_several_bids() { .expect("Transfer tokens"); // Check that ALICE has been refunded her initial bid and the auction contract - // recieved the 2 tokens from BOB. + // received the 2 tokens from BOB. let balance_of_alice_and_auction_contract = get_balances(&chain, auction_contract_address, token_contract_address); - assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(100), TokenAmountU64(2)]); - // Invoke the view entrypoint and check that the tokens are owned by Alice. + // Check that the item is not sold yet. let item_state = view_item_state(&chain, auction_contract_address); - - // Check that item is not sold yet. assert_eq!(item_state.auction_state, AuctionState::NotSoldYet); // Increment the chain time by 100000 milliseconds. chain.tick_block_time(Duration::from_millis(100000)).expect("Increment chain time"); + // Finalize the auction. let _update = chain .contract_update( SIGNER, @@ -508,10 +504,14 @@ fn full_auction_flow_with_several_bids() { ) .expect("Should be able to finalize"); - // Invoke the view entrypoint and check that the tokens are owned by Alice. + // Check that the item is sold to BOB. let item_state = view_item_state(&chain, auction_contract_address); - assert_eq!(item_state.auction_state, AuctionState::Sold(BOB)); + + // Check that ALICE as creator got the payout when the auction ended. + let balance_of_alice_and_auction_contract = + get_balances(&chain, auction_contract_address, token_contract_address); + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(102), TokenAmountU64(0)]); } #[test] @@ -528,6 +528,7 @@ fn full_auction_flow_with_cis3_transfer_function() { minimum_bid: TokenAmountU64(0), }; + // Add item. let _update = chain .contract_update( SIGNER, @@ -545,6 +546,7 @@ fn full_auction_flow_with_cis3_transfer_function() { ) .expect("Should be able to add Item"); + // Airdrop tokens to ALICE. let parameter = cis2_multi::MintParams { owner: concordium_smart_contract_testing::Address::Account(ALICE), metadata_url: MetadataUrl { @@ -567,19 +569,18 @@ fn full_auction_flow_with_cis3_transfer_function() { message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), }, ) - .expect("Should be able to finalize"); + .expect("Should be able to mint"); // Check balances in state. let balance_of_alice_and_auction_contract = get_balances(&chain, auction_contract_address, token_contract_address); - assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(100), TokenAmountU64(0)]); let additional_data = AdditionalDataIndex { item_index: 0u16, }; - // Transfer one token from Alice to bid function in auction. + // Transfer one token from ALICE to the auction contract. let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { from: ALICE_ADDR, to: Receiver::Contract( @@ -600,21 +601,19 @@ fn full_auction_flow_with_cis3_transfer_function() { }) .expect("Transfer tokens"); - // Invoke the view entrypoint and check that the tokens are owned by Alice. + // Check that the item is not sold yet. let item_state = view_item_state(&chain, auction_contract_address); - - // Check that item is not sold yet. assert_eq!(item_state.auction_state, AuctionState::NotSoldYet); // Check balances in state. let balance_of_alice_and_auction_contract = get_balances(&chain, auction_contract_address, token_contract_address); - assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(99), TokenAmountU64(1)]); // Increment the chain time by 100000 milliseconds. chain.tick_block_time(Duration::from_millis(100000)).expect("Increment chain time"); + // Finalize the auction. let _update = chain .contract_update( SIGNER, @@ -632,244 +631,19 @@ fn full_auction_flow_with_cis3_transfer_function() { ) .expect("Should be able to finalize"); - // Invoke the view entrypoint and check that the tokens are owned by Alice. + // Check that the item is sold to ALICE. let item_state = view_item_state(&chain, auction_contract_address); - - // Check that item is not sold yet. assert_eq!(item_state.auction_state, AuctionState::Sold(ALICE)); -} -/// Test a sequence of bids and finalizations: -/// 0. Auction is initialized. -/// 1. Alice successfully bids 1 CCD. -/// 2. Alice successfully bids 2 CCD, highest -/// bid becomes 2 CCD. Alice gets her 1 CCD refunded. -/// 3. Bob successfully bids 3 CCD, highest -/// bid becomes 3 CCD. Alice gets her 2 CCD refunded. -/// 4. Alice tries to bid 3 CCD, which matches the current highest bid, which -/// fails. -/// 5. Alice tries to bid 3.5 CCD, which is below the minimum raise -/// threshold of 1 CCD. -/// 6. Someone tries to finalize the auction before -/// its end time. Attempt fails. -/// 7. Someone tries to bid after the auction has ended (but before it has been -/// finalized), which fails. -/// 8. Dave successfully finalizes the auction after -/// its end time. Carol (the owner of the contract) collects the highest bid -/// amount. -/// 9. Attempts to subsequently bid or finalize fail. -// #[test] -// fn test_multiple_scenarios() { -// let (mut chain, contract_address) = initialize_chain_and_auction(); - -// // 1. Alice successfully bids 1 CCD. -// let _update_1 = chain -// .contract_update( -// SIGNER, -// ALICE, -// Address::Account(ALICE), -// Energy::from(10000), -// UpdateContractPayload { -// amount: Amount::from_ccd(1), -// address: contract_address, -// receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), -// message: OwnedParameter::empty(), }, -// ) -// .expect("Alice successfully bids 1 CCD"); - -// // 2. Alice successfully bids 2 CCD, highest -// // bid becomes 2 CCD. Alice gets her 1 CCD refunded. -// let update_2 = chain -// .contract_update( -// SIGNER, -// ALICE, -// Address::Account(ALICE), -// Energy::from(10000), -// UpdateContractPayload { -// amount: Amount::from_ccd(2), -// address: contract_address, -// receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), -// message: OwnedParameter::empty(), }, -// ) -// .expect("Alice successfully bids 2 CCD"); -// // Check that 1 CCD is transferred back to ALICE. -// assert_eq!(update_2.account_transfers().collect::>()[..], [( -// contract_address, -// Amount::from_ccd(1), -// ALICE -// )]); - -// // 3. Bob successfully bids 3 CCD, highest -// // bid becomes 3 CCD. Alice gets her 2 CCD refunded. -// let update_3 = chain -// .contract_update( -// SIGNER, -// BOB, -// Address::Account(BOB), -// Energy::from(10000), -// UpdateContractPayload { -// amount: Amount::from_ccd(3), -// address: contract_address, -// receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), -// message: OwnedParameter::empty(), }, -// ) -// .expect("Bob successfully bids 3 CCD"); -// // Check that 2 CCD is transferred back to ALICE. -// assert_eq!(update_3.account_transfers().collect::>()[..], [( -// contract_address, -// Amount::from_ccd(2), -// ALICE -// )]); - -// // 4. Alice tries to bid 3 CCD, which matches the current highest bid, which -// // fails. -// let update_4 = chain -// .contract_update( -// SIGNER, -// ALICE, -// Address::Account(ALICE), -// Energy::from(10000), -// UpdateContractPayload { -// amount: Amount::from_ccd(3), -// address: contract_address, -// receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), -// message: OwnedParameter::empty(), }, -// ) -// .expect_err("Alice tries to bid 3 CCD"); -// // Check that the correct error is returned. -// let rv: BidError = update_4.parse_return_value().expect("Return value is valid"); -// assert_eq!(rv, BidError::BidBelowCurrentBid); - -// // 5. Alice tries to bid 3.5 CCD, which is below the minimum raise threshold of -// // 1 CCD. -// let update_5 = chain -// .contract_update( -// SIGNER, -// ALICE, -// Address::Account(ALICE), -// Energy::from(10000), -// UpdateContractPayload { -// amount: Amount::from_micro_ccd(3_500_000), -// address: contract_address, -// receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), -// message: OwnedParameter::empty(), }, -// ) -// .expect_err("Alice tries to bid 3.5 CCD"); -// // Check that the correct error is returned. -// let rv: BidError = update_5.parse_return_value().expect("Return value is valid"); -// assert_eq!(rv, BidError::BidBelowMinimumRaise); - -// // 6. Someone tries to finalize the auction before -// // its end time. Attempt fails. -// let update_6 = chain -// .contract_update( -// SIGNER, -// DAVE, -// Address::Account(DAVE), -// Energy::from(10000), -// UpdateContractPayload { -// amount: Amount::zero(), -// address: contract_address, -// receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), -// message: OwnedParameter::empty(), }, -// ) -// .expect_err("Attempt to finalize auction before end time"); -// // Check that the correct error is returned. -// let rv: FinalizeError = update_6.parse_return_value().expect("Return value is valid"); -// assert_eq!(rv, FinalizeError::AuctionStillActive); - -// // Increment the chain time by 1001 milliseconds. -// chain.tick_block_time(Duration::from_millis(1001)).expect("Increment chain time"); - -// // 7. Someone tries to bid after the auction has ended (but before it has been -// // finalized), which fails. -// let update_7 = chain -// .contract_update( -// SIGNER, -// DAVE, -// Address::Account(DAVE), -// Energy::from(10000), -// UpdateContractPayload { -// amount: Amount::from_ccd(10), -// address: contract_address, -// receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), -// message: OwnedParameter::empty(), }, -// ) -// .expect_err("Attempt to bid after auction has reached the endtime"); -// // Check that the return value is `BidTooLate`. -// let rv: BidError = update_7.parse_return_value().expect("Return value is valid"); -// assert_eq!(rv, BidError::BidTooLate); - -// // 8. Dave successfully finalizes the auction after its end time. -// let update_8 = chain -// .contract_update( -// SIGNER, -// DAVE, -// Address::Account(DAVE), -// Energy::from(10000), -// UpdateContractPayload { -// amount: Amount::zero(), -// address: contract_address, -// receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), -// message: OwnedParameter::empty(), }, -// ) -// .expect("Dave successfully finalizes the auction after its end time"); - -// // Check that the correct amount is transferred to Carol. -// assert_eq!(update_8.account_transfers().collect::>()[..], [( -// contract_address, -// Amount::from_ccd(3), -// CAROL -// )]); - -// // 9. Attempts to subsequently bid or finalize fail. -// let update_9 = chain -// .contract_update( -// SIGNER, -// ALICE, -// Address::Account(ALICE), -// Energy::from(10000), -// UpdateContractPayload { -// amount: Amount::from_ccd(1), -// address: contract_address, -// receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.bid".to_string()), -// message: OwnedParameter::empty(), }, -// ) -// .expect_err("Attempt to bid after auction has been finalized"); -// // Check that the return value is `AuctionAlreadyFinalized`. -// let rv: BidError = update_9.parse_return_value().expect("Return value is valid"); -// assert_eq!(rv, BidError::AuctionAlreadyFinalized); - -// let update_10 = chain -// .contract_update( -// SIGNER, -// ALICE, -// Address::Account(ALICE), -// Energy::from(10000), -// UpdateContractPayload { -// amount: Amount::zero(), -// address: contract_address, -// receive_name: -// OwnedReceiveName::new_unchecked("sponsored_tx_enabled_auction.finalize".to_string()), -// message: OwnedParameter::empty(), }, -// ) -// .expect_err("Attempt to finalize auction after it has been finalized"); -// let rv: FinalizeError = update_10.parse_return_value().expect("Return value is valid"); -// assert_eq!(rv, FinalizeError::AuctionAlreadyFinalized); -// } + // Check that ALICE as creator got the payout when the auction ended. + let balance_of_alice_and_auction_contract = + get_balances(&chain, auction_contract_address, token_contract_address); + assert_eq!(balance_of_alice_and_auction_contract.0, [TokenAmountU64(100), TokenAmountU64(0)]); +} /// Get the `ItemState` at index 0. fn view_item_state(chain: &Chain, auction_contract_address: ContractAddress) -> ItemState { - // Invoke the view entrypoint and check that the tokens are owned by Alice. + // Invoke the view entry point. let invoke = chain .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), @@ -879,9 +653,9 @@ fn view_item_state(chain: &Chain, auction_contract_address: ContractAddress) -> address: auction_contract_address, message: OwnedParameter::from_serial(&0u16).expect("Serialize parameter"), }) - .expect("Invoke view"); + .expect("Invoke viewItemState"); - invoke.parse_return_value().expect("BalanceOf return value") + invoke.parse_return_value().expect("ViewItemState return value") } /// Get the `TOKEN_1` balances for Alice and the auction contract. @@ -917,18 +691,10 @@ fn get_balances( } /// Setup auction and chain. -/// -/// Carol is the owner of the auction, which ends at `1000` milliseconds after -/// the unix epoch. The 'microCCD per euro' exchange rate is set to `1_000_000`, -/// so 1 CCD = 1 euro. fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, ContractAddress) { - let mut chain = Chain::builder() - .micro_ccd_per_euro( - ExchangeRate::new(1_000_000, 1).expect("Exchange rate is in valid range"), - ) - .build() - .expect("Exchange rate is in valid range"); + let mut chain = Chain::builder().build().expect("Should be able to build chain"); + // Create keys for ALICE. let rng = &mut rand::thread_rng(); let keypairs_alice = AccountKeys::singleton(rng); @@ -939,14 +705,12 @@ fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, Contr locked: Amount::zero(), }; - // Create some accounts accounts on the chain. + // Create some accounts on the chain. chain.create_account(Account::new_with_keys(ALICE, balance, (&keypairs_alice).into())); chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); - chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(CAROL, ACC_INITIAL_BALANCE)); - chain.create_account(Account::new(DAVE, ACC_INITIAL_BALANCE)); - // Load and deploy the module. + // Load and deploy the cis2 token module. let module = module_load_v1("../cis2-multi/concordium-out/module.wasm.v1").expect("Module exists"); let deployment = chain.module_deploy_v1(SIGNER, CAROL, module).expect("Deploy valid module"); @@ -954,7 +718,7 @@ fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, Contr // Create the InitParameter. let parameter = ContractAddress::new(0, 0); - // Initialize the auction contract. + // Initialize the cis2 token contract. let token = chain .contract_init(SIGNER, CAROL, Energy::from(10000), InitContractPayload { amount: Amount::zero(), @@ -962,9 +726,9 @@ fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, Contr init_name: OwnedContractName::new_unchecked("init_cis2_multi".to_string()), param: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), }) - .expect("Initialize auction"); + .expect("Initialize cis2 token contract"); - // Load and deploy the module. + // Load and deploy the auction module. let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); let deployment = chain.module_deploy_v1(SIGNER, CAROL, module).expect("Deploy valid module"); From 375bcb9c99e167a2527528da15bdc5836912643a Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Wed, 29 Nov 2023 14:34:32 +0300 Subject: [PATCH 10/15] Address comments --- .../sponsored-tx-enabled-auction/Cargo.toml | 4 +- .../sponsored-tx-enabled-auction/src/lib.rs | 45 +++++++++---------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/examples/sponsored-tx-enabled-auction/Cargo.toml b/examples/sponsored-tx-enabled-auction/Cargo.toml index a68e0a97..527b1bcf 100644 --- a/examples/sponsored-tx-enabled-auction/Cargo.toml +++ b/examples/sponsored-tx-enabled-auction/Cargo.toml @@ -16,10 +16,8 @@ concordium-cis2 = {path = "../../concordium-cis2", default-features = false} [dev-dependencies] concordium-smart-contract-testing = { path = "../../contract-testing" } +cis2-multi = { path = "../cis2-multi/" } rand = "0.7.0" -[dev-dependencies.cis2-multi] -path = "../cis2-multi/" - [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index a70c14bd..edb187cb 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -30,7 +30,7 @@ //! contract, the smart contract refunds the old highest bidder. //! //! Bids have to be placed before the auction ends. The participant with the -//! highest bid (the last bidder) wins the auction. +//! highest bid (the last accepted bidder) wins the auction. //! //! After the auction ends for a specific item, any account can finalize the //! auction. The creator of that auction receives the highest bid when the @@ -71,10 +71,10 @@ pub enum AuctionState { Sold(AccountAddress), } -/// The state of the smart contract. +/// The state of an item up for auction. /// This state can be viewed by querying the node with the command -/// `concordium-client contract invoke` using the `view` function as entry -/// point. +/// `concordium-client contract invoke` using the `view_item_state` function as +/// entry point. #[derive(Debug, Serialize, SchemaType, Clone, PartialEq, Eq)] pub struct ItemState { /// State of the auction. @@ -118,7 +118,6 @@ pub struct State { /// The return_value for the entry point `view` which returns the contract /// state. #[derive(Serialize, SchemaType, Debug, Eq, PartialEq)] - pub struct ReturnParamView { /// A vector including all items that have been added to this contract. pub item_states: Vec<(u16, ItemState)>, @@ -210,6 +209,12 @@ fn auction_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResu mutable )] fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { + // Ensure that only accounts can add an item. + let sender_address = match ctx.sender() { + Address::Contract(_) => bail!(Error::OnlyAccount.into()), + Address::Account(account_address) => account_address, + }; + // Getting input parameters. let item: AddItemParameter = ctx.parameter_cursor().get()?; @@ -220,12 +225,6 @@ fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { // Ensure the auction can run. ensure!(slot_time <= item.end, Error::EndTimeError.into()); - // Ensure that only accounts can add an item. - let sender_address = match ctx.sender() { - Address::Contract(_) => bail!(Error::OnlyAccount.into()), - Address::Account(account_address) => account_address, - }; - // Assign an index to the item/auction. let counter = host.state_mut().counter; host.state_mut().counter = counter + 1; @@ -264,15 +263,19 @@ fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { error = "Error" )] fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { + // Ensure the sender is the cis2 token contract. + if !ctx.sender().matches_contract(&host.state().cis2_contract) { + bail!(Error::NotTokenContract.into()); + } + // Getting input parameters. let params: OnReceivingCis2Params = ctx.parameter_cursor().get()?; - // Ensure the sender is the cis2 token contract. - if let Address::Contract(contract) = ctx.sender() { - ensure_eq!(contract, host.state().cis2_contract, Error::NotTokenContract.into()); - } else { - bail!(Error::NotTokenContract.into()) + // Ensure that only accounts can bid for an item. + let bidder_address = match params.from { + Address::Contract(_) => bail!(Error::OnlyAccount.into()), + Address::Account(account_address) => account_address, }; // Get the item_index from the additionalData. @@ -284,14 +287,9 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<() .entry(additional_data_index.item_index) .occupied_or(Error::NoItem)?; + // Ensure the token_id matches. ensure_eq!(item.token_id, params.token_id, Error::WrongTokenID.into()); - // Ensure that only accounts can bid for an item. - let bidder_address = match params.from { - Address::Contract(_) => bail!(Error::OnlyAccount.into()), - Address::Account(account_address) => account_address, - }; - // Ensure the auction has not been finalized yet. ensure_eq!(item.auction_state, AuctionState::NotSoldYet, Error::AuctionAlreadyFinalized.into()); @@ -363,7 +361,8 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResu ensure!(slot_time > item.end, Error::AuctionStillActive.into()); if let Some(account_address) = item.highest_bidder { - // Marking the highest bidder (the last bidder) as winner of the auction. + // Marking the highest bidder (the last accepted bidder) as winner of the + // auction. item.auction_state = AuctionState::Sold(account_address); // Sending the highest bid in tokens to the creator of the auction. From 2e0d84a3756529e86225c68787ba8d0e34b8bb9d Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 30 Nov 2023 12:07:40 +0300 Subject: [PATCH 11/15] Add parameter type OnReceivingCis2DataParams to cis2 token --- concordium-cis2/CHANGELOG.md | 3 ++ concordium-cis2/src/lib.rs | 54 ++++++++++++++++++- examples/cis2-multi/src/lib.rs | 26 ++++----- .../sponsored-tx-enabled-auction/src/lib.rs | 20 ++++--- .../tests/tests.rs | 6 +-- 5 files changed, 80 insertions(+), 29 deletions(-) diff --git a/concordium-cis2/CHANGELOG.md b/concordium-cis2/CHANGELOG.md index a3866a8f..7a873d30 100644 --- a/concordium-cis2/CHANGELOG.md +++ b/concordium-cis2/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## Unreleased changes +- Add specific parameter type `OnReceivingCis2DataParams` for a contract function which receives CIS2 tokens with a specific type D for the AdditionalData. +- Add `Deserial` trait for `OnReceivingCis2DataParams`. +- Add `Serial` trait for `OnReceivingCis2DataParams`. ## concordium-cis2 5.1.0 (2023-10-18) diff --git a/concordium-cis2/src/lib.rs b/concordium-cis2/src/lib.rs index 7816a25e..b30123dc 100644 --- a/concordium-cis2/src/lib.rs +++ b/concordium-cis2/src/lib.rs @@ -1235,7 +1235,7 @@ impl AsRef<[MetadataUrl]> for TokenMetadataQueryResponse { fn as_ref(&self) -> &[MetadataUrl] { &self.0 } } -/// The parameter type for a contract function which receives CIS2 tokens. +/// Generic parameter type for a contract function which receives CIS2 tokens. // Note: For the serialization to be derived according to the CIS2 // specification, the order of the fields cannot be changed. #[derive(Debug, Serialize, SchemaType)] @@ -1250,6 +1250,58 @@ pub struct OnReceivingCis2Params { pub data: AdditionalData, } +/// Specific parameter type for a contract function which receives CIS2 tokens +/// with a specific type D for the AdditionalData. +// Note: For the serialization to be derived according to the CIS2 +// specification, the order of the fields cannot be changed. +#[derive(Debug, SchemaType)] +pub struct OnReceivingCis2DataParams { + /// The ID of the token received. + pub token_id: T, + /// The amount of tokens received. + pub amount: A, + /// The previous owner of the tokens. + pub from: Address, + /// Some extra information which where sent as part of the transfer. + pub data: D, +} + +/// Deserial trait for OnReceivingCis2DataParams. +/// The specific type D is constructed from the general `AdditionalData` type +/// first before the `OnReceivingCis2DataParams` object is deserialized. +impl Deserial for OnReceivingCis2DataParams { + fn deserial(source: &mut R) -> ParseResult { + let params: OnReceivingCis2Params = source.get()?; + let additional_data_type: D = from_bytes(params.data.as_ref())?; + Ok(OnReceivingCis2DataParams { + token_id: params.token_id, + amount: params.amount, + from: params.from, + data: additional_data_type, + }) + } +} + +/// Serial trait for OnReceivingCis2DataParams. +/// The specific type D is converted into the general `AdditionalData` type +/// first before the `OnReceivingCis2Params` object is serialized. +impl Serial + for OnReceivingCis2DataParams +{ + fn serial(&self, out: &mut W) -> Result<(), W::Err> { + let additional_data = AdditionalData::from(to_bytes(&self.data)); + let object: OnReceivingCis2Params = OnReceivingCis2Params { + token_id: self.token_id.clone(), + amount: self.amount.clone(), + from: self.from, + data: additional_data, + }; + + object.serial(out)?; + Ok(()) + } +} + /// Identifier for a smart contract standard. /// Consists of a string of ASCII characters up to a length of 255. /// diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 0d0f3307..7628c8ce 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -305,34 +305,34 @@ pub struct PermitParamPartial { pub enum CustomContractError { /// Failed parsing the parameter. #[from(ParseError)] - ParseParams, + ParseParams, // -1 /// Failed logging: Log is full. - LogFull, + LogFull, // -2 /// Failed logging: Log is malformed. - LogMalformed, + LogMalformed, // -3 /// Invalid contract name. - InvalidContractName, + InvalidContractName, // -4 /// Only a smart contract can call this function. - ContractOnly, + ContractOnly, // -5 /// Failed to invoke a contract. - InvokeContractError, + InvokeContractError, // -6 /// Failed to verify signature because signer account does not exist on /// chain. - MissingAccount, + MissingAccount, // -7 /// Failed to verify signature because data was malformed. - MalformedData, + MalformedData, // -8 /// Failed signature verification: Invalid signature. - WrongSignature, + WrongSignature, // -9 /// Failed signature verification: A different nonce is expected. - NonceMismatch, + NonceMismatch, // -10 /// Failed signature verification: Signature was intended for a different /// contract. - WrongContract, + WrongContract, // -11 /// Failed signature verification: Signature was intended for a different /// entry_point. - WrongEntryPoint, + WrongEntryPoint, // -12 /// Failed signature verification: Signature is expired. - Expired, + Expired, // -13 } pub type ContractError = Cis2Error; diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index edb187cb..dea94bd7 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -259,7 +259,8 @@ fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { contract = "sponsored_tx_enabled_auction", name = "bid", mutable, - parameter = "OnReceivingCis2Params", + parameter = "OnReceivingCis2DataParams", error = "Error" )] fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { @@ -269,8 +270,11 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<() } // Getting input parameters. - let params: OnReceivingCis2Params = - ctx.parameter_cursor().get()?; + let params: OnReceivingCis2DataParams< + ContractTokenId, + ContractTokenAmount, + AdditionalDataIndex, + > = ctx.parameter_cursor().get()?; // Ensure that only accounts can bid for an item. let bidder_address = match params.from { @@ -278,14 +282,8 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<() Address::Account(account_address) => account_address, }; - // Get the item_index from the additionalData. - let additional_data_index: AdditionalDataIndex = from_bytes(params.data.as_ref())?; - - let mut item = host - .state_mut() - .items - .entry(additional_data_index.item_index) - .occupied_or(Error::NoItem)?; + let mut item = + host.state_mut().items.entry(params.data.item_index).occupied_or(Error::NoItem)?; // Ensure the token_id matches. ensure_eq!(item.token_id, params.token_id, Error::WrongTokenID.into()); diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs index b611dc83..20a5475d 100644 --- a/examples/sponsored-tx-enabled-auction/tests/tests.rs +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -715,16 +715,14 @@ fn initialize_chain_and_auction() -> (Chain, AccountKeys, ContractAddress, Contr module_load_v1("../cis2-multi/concordium-out/module.wasm.v1").expect("Module exists"); let deployment = chain.module_deploy_v1(SIGNER, CAROL, module).expect("Deploy valid module"); - // Create the InitParameter. - let parameter = ContractAddress::new(0, 0); - // Initialize the cis2 token contract. let token = chain .contract_init(SIGNER, CAROL, Energy::from(10000), InitContractPayload { amount: Amount::zero(), mod_ref: deployment.module_reference, init_name: OwnedContractName::new_unchecked("init_cis2_multi".to_string()), - param: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + param: OwnedParameter::from_serial(&TokenAmountU64(100u64)) + .expect("Serialize parameter"), }) .expect("Initialize cis2 token contract"); From 3e5b5d070ba35fdefb60077e0c09e3b0fc8c358d Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 30 Nov 2023 14:57:46 +0300 Subject: [PATCH 12/15] Try using Cis2Client --- .../sponsored-tx-enabled-auction/src/lib.rs | 98 +++++++++++++++---- 1 file changed, 79 insertions(+), 19 deletions(-) diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index dea94bd7..96b5d6a1 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -43,7 +43,7 @@ #![cfg_attr(not(feature = "std"), no_std)] -use concordium_cis2::*; +use concordium_cis2::{Cis2Client, *}; use concordium_std::*; /// Contract token ID type. It has to be the `ContractTokenId` from the cis2 @@ -180,8 +180,52 @@ pub enum Error { /// Raised when payment is attempted with a different `token_id` than /// specified for an item. WrongTokenID, //-10 + InvokeContractError, //-11 + ParseResult, //-12 + InvalidResponse, //-13 } +/// Mapping Cis2ClientError to Error +impl From> for Error { + fn from(e: Cis2ClientError) -> Self { + match e { + Cis2ClientError::InvokeContractError(_) => Self::InvokeContractError, + Cis2ClientError::ParseResult => Self::ParseResult, + Cis2ClientError::InvalidResponse => Self::InvalidResponse, + } + } +} + +// impl From for Reject { +// fn from(error:Error) -> Self { +// match error { +// Error::StartEndTimeError => Self::default(), +// Error::EndTimeError =>Self::default(), +// Error::OnlyAccount =>Self::default(), +// Error::BidNotGreaterCurrentBid => Self::default(), +// Error::BidTooLate =>Self::default(), +// Error::AuctionAlreadyFinalized => Self::default(), +// Error::NoItem => Self::default(), +// Error::AuctionStillActive => Self::default(), +// Error::NotTokenContract =>Self::default(), +// Error::WrongTokenID => Self::default(), +// Error::InvokeContractError => Self::default(), +// Error::ParseResult =>Self::default(), +// Error::InvalidResponse =>Self::default(), +// } +// } +// } + +// impl From> for Reject { +// fn from(error: Cis2ClientError) -> Self { +// match error { +// Cis2ClientError::InvokeContractError(_) =>Self::default(), +// Cis2ClientError::ParseResult => Self::default(), +// Cis2ClientError::InvalidResponse => Self::default(), +// } +// } +// } + /// Init entry point that creates a new auction contract. #[init(contract = "sponsored_tx_enabled_auction", parameter = "ContractAddress")] fn auction_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { @@ -348,6 +392,8 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResu // Getting input parameter. let item_index: u16 = ctx.parameter_cursor().get()?; + let contract = host.state().cis2_contract; + // Get the item from state. let mut item = host.state_mut().items.entry(item_index).occupied_or(Error::NoItem)?; @@ -371,24 +417,41 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResu // Please consider using a pull-over-push pattern when expanding this // smart contract to allow smart contract instances to // participate in the auction as well. https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/ - let parameter = TransferParameter { - 0: vec![Transfer { - token_id: item.token_id, - amount: item.highest_bid, - from: concordium_std::Address::Contract(ctx.self_address()), - to: concordium_cis2::Receiver::Account(item.creator), - data: AdditionalData::empty(), - }], - }; + // let parameter = TransferParameter { + // 0: vec![Transfer { + // token_id: item.token_id, + // amount: item.highest_bid, + // from: concordium_std::Address::Contract(ctx.self_address()), + // to: concordium_cis2::Receiver::Account(item.creator), + // data: AdditionalData::empty(), + // }], + // }; + + // drop(item); + + let client = Cis2Client::new(contract); + + let read_item = item.clone(); drop(item); - host.invoke_contract( - &host.state().cis2_contract.clone(), - ¶meter, - EntrypointName::new_unchecked("transfer"), - Amount::zero(), + let test = client.transfer::( + host, + Transfer { + amount: read_item.highest_bid, + from: concordium_std::Address::Contract(ctx.self_address()), + to: concordium_cis2::Receiver::Account(read_item.creator), + token_id: read_item.token_id, + data: AdditionalData::empty(), + }, )?; + + // host.invoke_contract( + // &host.state().cis2_contract.clone(), + // ¶meter, + // EntrypointName::new_unchecked("transfer"), + // Amount::zero(), + // )?; } Ok(()) @@ -403,10 +466,7 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResu fn view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { let state = host.state(); - let mut inner_state = Vec::new(); - for (index, item_state) in state.items.iter() { - inner_state.push((*index, item_state.to_owned())); - } + let inner_state = state.items.iter().map(|x| (*x.0, x.1.clone())).collect(); Ok(ReturnParamView { item_states: inner_state, From d35bece5bd00527001a9e1a1d82bbdc78907dc7a Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 30 Nov 2023 15:35:53 +0300 Subject: [PATCH 13/15] Fix error types --- .../sponsored-tx-enabled-auction/src/lib.rs | 124 +++++++++--------- 1 file changed, 64 insertions(+), 60 deletions(-) diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index 96b5d6a1..81c7afc0 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -156,33 +156,64 @@ pub struct AdditionalDataIndex { /// Errors of this contract. #[derive(Debug, PartialEq, Eq, Clone, Reject, Serialize, SchemaType)] pub enum Error { + /// Failed parsing the parameter. + #[from(ParseError)] + ParseParams, //-1 // Raised when adding an item; The start time needs to be strictly smaller than the end time. - StartEndTimeError, //-1 + StartEndTimeError, //-2 // Raised when adding an item; The end time needs to be in the future. - EndTimeError, //-2 + EndTimeError, //-3 /// Raised when a contract tries to bid; Only accounts /// are allowed to bid. - OnlyAccount, //-3 + OnlyAccount, //-4 /// Raised when the new bid amount is not greater than the current highest /// bid. - BidNotGreaterCurrentBid, //-4 + BidNotGreaterCurrentBid, //-5 /// Raised when the bid is placed after the auction end time passed. - BidTooLate, //-5 + BidTooLate, //-6 /// Raised when the bid is placed after the auction has been finalized. - AuctionAlreadyFinalized, //-6 + AuctionAlreadyFinalized, //-7 /// Raised when the item index cannot be found in the contract. - NoItem, //-7 + NoItem, //-8 /// Raised when finalizing an auction before the auction end time passed. - AuctionStillActive, //-8 + AuctionStillActive, //-9 /// Raised when someone else than the cis2 token contract invokes the `bid` /// entry point. - NotTokenContract, //-9 + NotTokenContract, //-10 /// Raised when payment is attempted with a different `token_id` than /// specified for an item. - WrongTokenID, //-10 - InvokeContractError, //-11 - ParseResult, //-12 - InvalidResponse, //-13 + WrongTokenID, //-11 + InvokeContractError, //-12 + ParseResult, //-13 + InvalidResponse, //-14 + AmountTooLarge, //-15 + MissingAccount, //-16 + MissingContract, //-17 + MissingEntrypoint, //-18 + MessageFailed, //-19 + LogicReject, //-20 + Trap, //-21 + TransferFailed, //-22 +} + +pub type ContractResult = Result; + +/// Mapping Cis2ClientError to Error +impl From> for Error { + fn from(e: CallContractError) -> Self { + match e { + CallContractError::AmountTooLarge => Self::AmountTooLarge, + CallContractError::MissingAccount => Self::MissingAccount, + CallContractError::MissingContract => Self::MissingContract, + CallContractError::MissingEntrypoint => Self::MissingEntrypoint, + CallContractError::MessageFailed => Self::MessageFailed, + CallContractError::LogicReject { + reason: _, + return_value: _, + } => Self::LogicReject, + CallContractError::Trap => Self::Trap, + } + } } /// Mapping Cis2ClientError to Error @@ -196,36 +227,6 @@ impl From> for Error { } } -// impl From for Reject { -// fn from(error:Error) -> Self { -// match error { -// Error::StartEndTimeError => Self::default(), -// Error::EndTimeError =>Self::default(), -// Error::OnlyAccount =>Self::default(), -// Error::BidNotGreaterCurrentBid => Self::default(), -// Error::BidTooLate =>Self::default(), -// Error::AuctionAlreadyFinalized => Self::default(), -// Error::NoItem => Self::default(), -// Error::AuctionStillActive => Self::default(), -// Error::NotTokenContract =>Self::default(), -// Error::WrongTokenID => Self::default(), -// Error::InvokeContractError => Self::default(), -// Error::ParseResult =>Self::default(), -// Error::InvalidResponse =>Self::default(), -// } -// } -// } - -// impl From> for Reject { -// fn from(error: Cis2ClientError) -> Self { -// match error { -// Cis2ClientError::InvokeContractError(_) =>Self::default(), -// Cis2ClientError::ParseResult => Self::default(), -// Cis2ClientError::InvalidResponse => Self::default(), -// } -// } -// } - /// Init entry point that creates a new auction contract. #[init(contract = "sponsored_tx_enabled_auction", parameter = "ContractAddress")] fn auction_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { @@ -252,10 +253,10 @@ fn auction_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResu parameter = "AddItemParameter", mutable )] -fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { +fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Ensure that only accounts can add an item. let sender_address = match ctx.sender() { - Address::Contract(_) => bail!(Error::OnlyAccount.into()), + Address::Contract(_) => bail!(Error::OnlyAccount), Address::Account(account_address) => account_address, }; @@ -263,11 +264,11 @@ fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { let item: AddItemParameter = ctx.parameter_cursor().get()?; // Ensure start < end. - ensure!(item.start < item.end, Error::StartEndTimeError.into()); + ensure!(item.start < item.end, Error::StartEndTimeError); let slot_time = ctx.metadata().slot_time(); // Ensure the auction can run. - ensure!(slot_time <= item.end, Error::EndTimeError.into()); + ensure!(slot_time <= item.end, Error::EndTimeError); // Assign an index to the item/auction. let counter = host.state_mut().counter; @@ -307,10 +308,10 @@ fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { AdditionalDataIndex>", error = "Error" )] -fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { +fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Ensure the sender is the cis2 token contract. if !ctx.sender().matches_contract(&host.state().cis2_contract) { - bail!(Error::NotTokenContract.into()); + bail!(Error::NotTokenContract); } // Getting input parameters. @@ -322,7 +323,7 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<() // Ensure that only accounts can bid for an item. let bidder_address = match params.from { - Address::Contract(_) => bail!(Error::OnlyAccount.into()), + Address::Contract(_) => bail!(Error::OnlyAccount), Address::Account(account_address) => account_address, }; @@ -330,18 +331,18 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<() host.state_mut().items.entry(params.data.item_index).occupied_or(Error::NoItem)?; // Ensure the token_id matches. - ensure_eq!(item.token_id, params.token_id, Error::WrongTokenID.into()); + ensure_eq!(item.token_id, params.token_id, Error::WrongTokenID); // Ensure the auction has not been finalized yet. - ensure_eq!(item.auction_state, AuctionState::NotSoldYet, Error::AuctionAlreadyFinalized.into()); + ensure_eq!(item.auction_state, AuctionState::NotSoldYet, Error::AuctionAlreadyFinalized); let slot_time = ctx.metadata().slot_time(); // Ensure the auction has not ended yet. - ensure!(slot_time <= item.end, Error::BidTooLate.into()); + ensure!(slot_time <= item.end, Error::BidTooLate); // Ensure that the new bid exceeds the highest bid so far. let old_highest_bid = item.highest_bid; - ensure!(params.amount > old_highest_bid, Error::BidNotGreaterCurrentBid.into()); + ensure!(params.amount > old_highest_bid, Error::BidNotGreaterCurrentBid); // Set the new highest_bid. item.highest_bid = params.amount; @@ -388,7 +389,7 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<() mutable, error = "Error" )] -fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { +fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Getting input parameter. let item_index: u16 = ctx.parameter_cursor().get()?; @@ -398,11 +399,11 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResu let mut item = host.state_mut().items.entry(item_index).occupied_or(Error::NoItem)?; // Ensure the auction has not been finalized yet. - ensure_eq!(item.auction_state, AuctionState::NotSoldYet, Error::AuctionAlreadyFinalized.into()); + ensure_eq!(item.auction_state, AuctionState::NotSoldYet, Error::AuctionAlreadyFinalized); let slot_time = ctx.metadata().slot_time(); // Ensure the auction has ended already. - ensure!(slot_time > item.end, Error::AuctionStillActive.into()); + ensure!(slot_time > item.end, Error::AuctionStillActive); if let Some(account_address) = item.highest_bidder { // Marking the highest bidder (the last accepted bidder) as winner of the @@ -435,7 +436,7 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResu drop(item); - let test = client.transfer::( + let success = client.transfer::( host, Transfer { amount: read_item.highest_bid, @@ -446,6 +447,9 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResu }, )?; + // Ensure the transfer was successful ??? Shouldn't this return `true` + ensure!(!success, Error::TransferFailed); + // host.invoke_contract( // &host.state().cis2_contract.clone(), // ¶meter, @@ -463,7 +467,7 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResu name = "view", return_value = "ReturnParamView" )] -fn view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { +fn view(_ctx: &ReceiveContext, host: &Host) -> ContractResult { let state = host.state(); let inner_state = state.items.iter().map(|x| (*x.0, x.1.clone())).collect(); @@ -482,7 +486,7 @@ fn view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult) -> ReceiveResult { +fn view_item_state(ctx: &ReceiveContext, host: &Host) -> ContractResult { // Getting input parameter. let item_index: u16 = ctx.parameter_cursor().get()?; let item = host.state().items.get(&item_index).map(|x| x.to_owned()).ok_or(Error::NoItem)?; From 27a7e04e86f735c0f57881b4e17b6067715d0809 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 30 Nov 2023 17:56:01 +0300 Subject: [PATCH 14/15] Address comments --- concordium-cis2/src/lib.rs | 22 +-- .../sponsored-tx-enabled-auction/Cargo.toml | 2 +- .../sponsored-tx-enabled-auction/src/lib.rs | 126 ++++++++---------- .../tests/tests.rs | 2 +- 4 files changed, 62 insertions(+), 90 deletions(-) diff --git a/concordium-cis2/src/lib.rs b/concordium-cis2/src/lib.rs index b30123dc..cdda68f3 100644 --- a/concordium-cis2/src/lib.rs +++ b/concordium-cis2/src/lib.rs @@ -1267,8 +1267,6 @@ pub struct OnReceivingCis2DataParams { } /// Deserial trait for OnReceivingCis2DataParams. -/// The specific type D is constructed from the general `AdditionalData` type -/// first before the `OnReceivingCis2DataParams` object is deserialized. impl Deserial for OnReceivingCis2DataParams { fn deserial(source: &mut R) -> ParseResult { let params: OnReceivingCis2Params = source.get()?; @@ -1283,21 +1281,13 @@ impl Deserial for OnReceivingCis2DataPara } /// Serial trait for OnReceivingCis2DataParams. -/// The specific type D is converted into the general `AdditionalData` type -/// first before the `OnReceivingCis2Params` object is serialized. -impl Serial - for OnReceivingCis2DataParams -{ +impl Serial for OnReceivingCis2DataParams { fn serial(&self, out: &mut W) -> Result<(), W::Err> { - let additional_data = AdditionalData::from(to_bytes(&self.data)); - let object: OnReceivingCis2Params = OnReceivingCis2Params { - token_id: self.token_id.clone(), - amount: self.amount.clone(), - from: self.from, - data: additional_data, - }; - - object.serial(out)?; + self.token_id.serial(out)?; + self.amount.serial(out)?; + self.from.serial(out)?; + (to_bytes(&self.data).len() as u16).serial(out)?; + out.write_all(&to_bytes(&self.data))?; Ok(()) } } diff --git a/examples/sponsored-tx-enabled-auction/Cargo.toml b/examples/sponsored-tx-enabled-auction/Cargo.toml index 527b1bcf..b5740d7f 100644 --- a/examples/sponsored-tx-enabled-auction/Cargo.toml +++ b/examples/sponsored-tx-enabled-auction/Cargo.toml @@ -17,7 +17,7 @@ concordium-cis2 = {path = "../../concordium-cis2", default-features = false} [dev-dependencies] concordium-smart-contract-testing = { path = "../../contract-testing" } cis2-multi = { path = "../cis2-multi/" } -rand = "0.7.0" +rand = "0.7" [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/sponsored-tx-enabled-auction/src/lib.rs b/examples/sponsored-tx-enabled-auction/src/lib.rs index 81c7afc0..89ecf54d 100644 --- a/examples/sponsored-tx-enabled-auction/src/lib.rs +++ b/examples/sponsored-tx-enabled-auction/src/lib.rs @@ -36,11 +36,6 @@ //! auction. The creator of that auction receives the highest bid when the //! auction is finalized and the item is marked as sold to the highest bidder. //! This can be done only once. -//! -//! Terminology: `Accounts` are derived from a public/private key pair. -//! `Contract` instances are created by deploying a smart contract -//! module and initializing it. - #![cfg_attr(not(feature = "std"), no_std)] use concordium_cis2::{Cis2Client, *}; @@ -183,22 +178,37 @@ pub enum Error { /// Raised when payment is attempted with a different `token_id` than /// specified for an item. WrongTokenID, //-11 + /// Raised when the invocation of the cis2 token contract fails. InvokeContractError, //-12 - ParseResult, //-13 + /// Raised when the parsing of the result from the cis2 token contract + /// fails. + ParseResult, //-13 + /// Raised when the response of the cis2 token contract is invalid. InvalidResponse, //-14 + /// Raised when the amount of cis2 tokens that was to be transferred is not + /// available to the sender. AmountTooLarge, //-15 + /// Raised when the owner account of the cis 2 token contract that is being + /// invoked does not exist. This variant should in principle not happen, + /// but is here for completeness. MissingAccount, //-16 + /// Raised when the cis2 token contract that is to be invoked does not + /// exist. MissingContract, //-17 + /// Raised when the cis2 token contract to be invoked exists, but the entry + /// point that was named does not. MissingEntrypoint, //-18 + // Raised when the sending of a message to the V0 contract failed. MessageFailed, //-19 - LogicReject, //-20 - Trap, //-21 - TransferFailed, //-22 + // Raised when the cis2 token contract called rejected with the given reason. + LogicReject, //-20 + // Raised when the cis2 token contract execution triggered a runtime error. + Trap, //-21 } pub type ContractResult = Result; -/// Mapping Cis2ClientError to Error +/// Mapping CallContractError to Error impl From> for Error { fn from(e: CallContractError) -> Self { match e { @@ -266,9 +276,9 @@ fn add_item(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> // Ensure start < end. ensure!(item.start < item.end, Error::StartEndTimeError); - let slot_time = ctx.metadata().slot_time(); + let block_time = ctx.metadata().block_time(); // Ensure the auction can run. - ensure!(slot_time <= item.end, Error::EndTimeError); + ensure!(block_time <= item.end, Error::EndTimeError); // Assign an index to the item/auction. let counter = host.state_mut().counter; @@ -314,6 +324,8 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<( bail!(Error::NotTokenContract); } + let contract = host.state().cis2_contract; + // Getting input parameters. let params: OnReceivingCis2DataParams< ContractTokenId, @@ -336,9 +348,9 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<( // Ensure the auction has not been finalized yet. ensure_eq!(item.auction_state, AuctionState::NotSoldYet, Error::AuctionAlreadyFinalized); - let slot_time = ctx.metadata().slot_time(); + let block_time = ctx.metadata().block_time(); // Ensure the auction has not ended yet. - ensure!(slot_time <= item.end, Error::BidTooLate); + ensure!(block_time <= item.end, Error::BidTooLate); // Ensure that the new bid exceeds the highest bid so far. let old_highest_bid = item.highest_bid; @@ -348,6 +360,12 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<( item.highest_bid = params.amount; if let Some(account_address) = item.highest_bidder.replace(bidder_address) { + let client = Cis2Client::new(contract); + + let read_item = item.clone(); + + drop(item); + // Refunding old highest bidder. // This transfer (given enough NRG of course) always succeeds because // the `account_address` exists since it was recorded when it @@ -356,24 +374,13 @@ fn auction_bid(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<( // Please consider using a pull-over-push pattern when expanding this // smart contract to allow smart contract instances to // participate in the auction as well. https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/ - let parameter = TransferParameter { - 0: vec![Transfer { - token_id: item.token_id, - amount: old_highest_bid, - from: concordium_std::Address::Contract(ctx.self_address()), - to: concordium_cis2::Receiver::Account(account_address), - data: AdditionalData::empty(), - }], - }; - - drop(item); - - host.invoke_contract( - &host.state().cis2_contract.clone(), - ¶meter, - EntrypointName::new_unchecked("transfer"), - Amount::zero(), - )?; + client.transfer::(host, Transfer { + amount: old_highest_bid, + from: concordium_std::Address::Contract(ctx.self_address()), + to: concordium_cis2::Receiver::Account(account_address), + token_id: read_item.token_id, + data: AdditionalData::empty(), + })?; } Ok(()) @@ -401,15 +408,21 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ContractRes // Ensure the auction has not been finalized yet. ensure_eq!(item.auction_state, AuctionState::NotSoldYet, Error::AuctionAlreadyFinalized); - let slot_time = ctx.metadata().slot_time(); + let block_time = ctx.metadata().block_time(); // Ensure the auction has ended already. - ensure!(slot_time > item.end, Error::AuctionStillActive); + ensure!(block_time > item.end, Error::AuctionStillActive); if let Some(account_address) = item.highest_bidder { // Marking the highest bidder (the last accepted bidder) as winner of the // auction. item.auction_state = AuctionState::Sold(account_address); + let client = Cis2Client::new(contract); + + let read_item = item.clone(); + + drop(item); + // Sending the highest bid in tokens to the creator of the auction. // This transfer (given enough NRG of course) always succeeds because // the `creator` exists since it was recorded when it @@ -418,44 +431,13 @@ fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> ContractRes // Please consider using a pull-over-push pattern when expanding this // smart contract to allow smart contract instances to // participate in the auction as well. https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/ - // let parameter = TransferParameter { - // 0: vec![Transfer { - // token_id: item.token_id, - // amount: item.highest_bid, - // from: concordium_std::Address::Contract(ctx.self_address()), - // to: concordium_cis2::Receiver::Account(item.creator), - // data: AdditionalData::empty(), - // }], - // }; - - // drop(item); - - let client = Cis2Client::new(contract); - - let read_item = item.clone(); - - drop(item); - - let success = client.transfer::( - host, - Transfer { - amount: read_item.highest_bid, - from: concordium_std::Address::Contract(ctx.self_address()), - to: concordium_cis2::Receiver::Account(read_item.creator), - token_id: read_item.token_id, - data: AdditionalData::empty(), - }, - )?; - - // Ensure the transfer was successful ??? Shouldn't this return `true` - ensure!(!success, Error::TransferFailed); - - // host.invoke_contract( - // &host.state().cis2_contract.clone(), - // ¶meter, - // EntrypointName::new_unchecked("transfer"), - // Amount::zero(), - // )?; + client.transfer::(host, Transfer { + amount: read_item.highest_bid, + from: concordium_std::Address::Contract(ctx.self_address()), + to: concordium_cis2::Receiver::Account(read_item.creator), + token_id: read_item.token_id, + data: AdditionalData::empty(), + })?; } Ok(()) diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs index 20a5475d..2c28bbe8 100644 --- a/examples/sponsored-tx-enabled-auction/tests/tests.rs +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -658,7 +658,7 @@ fn view_item_state(chain: &Chain, auction_contract_address: ContractAddress) -> invoke.parse_return_value().expect("ViewItemState return value") } -/// Get the `TOKEN_1` balances for Alice and the auction contract. +/// Get the `TokenIdU8(1)` balances for Alice and the auction contract. fn get_balances( chain: &Chain, auction_contract_address: ContractAddress, From 45e3d086b863465e73c4e9ded8bf4acbf9f93a77 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Mon, 4 Dec 2023 10:54:44 +0300 Subject: [PATCH 15/15] Address comments --- concordium-cis2/src/lib.rs | 8 ++++---- examples/sponsored-tx-enabled-auction/src/lib.rs | 12 ++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/concordium-cis2/src/lib.rs b/concordium-cis2/src/lib.rs index cdda68f3..5aaee813 100644 --- a/concordium-cis2/src/lib.rs +++ b/concordium-cis2/src/lib.rs @@ -1246,7 +1246,7 @@ pub struct OnReceivingCis2Params { pub amount: A, /// The previous owner of the tokens. pub from: Address, - /// Some extra information which where sent as part of the transfer. + /// Some extra information which was sent as part of the transfer. pub data: AdditionalData, } @@ -1262,7 +1262,7 @@ pub struct OnReceivingCis2DataParams { pub amount: A, /// The previous owner of the tokens. pub from: Address, - /// Some extra information which where sent as part of the transfer. + /// Some extra information which was sent as part of the transfer. pub data: D, } @@ -1286,8 +1286,8 @@ impl Serial for OnReceivingCis2DataParams) -> ContractResult) -> ContractResult<()> { + Ok(()) +}