diff --git a/.gitignore b/.gitignore index 8bdcd69..e45ba63 100644 --- a/.gitignore +++ b/.gitignore @@ -2,18 +2,4 @@ dist *.concordiumwallet logs/* concordium-backup.concordiumwallet.json -kubo -kubo_v0.14.0_linux-amd64.tar.gz -cis2-multi/module.wasm -cis2-multi/schema.bin -cis2-market/module.wasm -cis2-market/schema.bin -cis2-nft/module.wasm -cis2-nft/schema.bin -cis2-fractionalizer/module.wasm -cis2-fractionalizer/schema.bin -.vscode -Cargo.lock -target -cis2-auctions/module.wasm -cis2-auctions/schema.bin \ No newline at end of file +.vscode \ No newline at end of file diff --git a/README.md b/README.md index ceea65c..1b59700 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,26 @@ -# Step by step [Token / CIS2](https://proposals.concordium.software/CIS/cis-2.html) tutorial on [Concordium](http://concordium.io) +# Sample [CIS2 Token](https://proposals.concordium.software/CIS/cis-2.html) tutorial Market & Minting on [Concordium](http://concordium.io) Concordium is a science-based proof-of-stake blockchain created for all, with in particular business applications in mind. [Read more about concordium](https://www.concordium.com/about) This repository provides sample implementations of following ways in which a developer can interact with an on chain contract on Concordium. -- Using [Concordium Client](<(https://developer.concordium.software/en/mainnet/smart-contracts/guides/on-chain-index.html)>) -- Using [Node SDK](https://www.npmjs.com/package/@concordium/node-sdk) - Using Frontend React Code (using [Web SDK](https://github.com/Concordium/concordium-node-sdk-js/tree/main/packages/web) and [Concordium Browser Wallet](https://chrome.google.com/webstore/detail/concordium-wallet/mnnkpffndmickbiakofclnpoiajlegmg?hl=en-US)) Please do note that this is **not** an exhaustive list of the languages supported by concordium. There are are SDK's present to interact with the contracts using multiple other languages. A complete list can be found [here](https://developer.concordium.software/en/mainnet/net/guides/sdks-apis.html) ## Contents of Repository -- [CIS2 Multi / Semi Fungible Smart Contract](./cis2-multi/README.md) - Reference CIS2 Multi Smart Contract Implementation. [Read more about CIS2 - Contract Token Standard](https://proposals.concordium.software/CIS/cis-2.html) -- [Marketplace Contract](./cis2-market/README.md) - Reference Marketplace Smart Contract Implementation for CIS2 tokens. -- [market-ui](./market-ui/README.md) - React based frontend DAPP for marketplace contract. This is the typescript code which can be used with Concordium Browser Wallet to interact with CIS2-Multi and Marketplace Contract in Browser based environments. -- [sample-artifacts](./sample-artifacts/README.md) - JSON requests for Smart Contract Methods & Sample Token Metadata Images. This is used while interacting with on chain contracts using Concordium Client & node-cli -- [node-cli](./node-cli/README.md) - nodejs based, reference cli implementation for interacting with CIS2-Multi Smart Contract. -- Sample scripts for interacting with Smart Contract using Concordium Client - - [For CIS2 Multi](./concordium-client/rust-cli-cis2-multi.README.md) - - [For Marketplace Contract](./concordium-client/rust-cli-cis2-market.README.md) +- [CIS2 Token Contract](./cis2-multi/README.md) +- [CIS2 Market Contract](./cis2-multi/README.md) +- [Market React Application](./market-ui/README.md) : + React based frontend DAPP for marketplace contract. This is the typescript code which can be used with Concordium Browser Wallet to interact with CIS2-Multi and Marketplace Contract in Browser based environments. It has following features + - Initialize a new CIS2 Token Contract + - Mint a CIS2 Token + - List a CIS2 Token on the Market + - Buy a CIS2 Token from Market + - Pinata (IPFS) based metadata Upload for the Token Metadata +- [Mint React Application](./mint-ui/README.md) + React based frontend DAPP for [cis2-multi](./cis2-multi/src/lib.rs) contract. It allows to Initialize a new contract & mint a token. ## Get Started @@ -42,14 +38,6 @@ Throughout this repository [Concordium Testnet](https://testnet.ccdscan.io/) is - [Create Testnet Account](https://developer.concordium.software/en/mainnet/net/guides/create-account.html) - [Export wallet](https://developer.concordium.software/en/mainnet/net/guides/export-import.html#export-import) and then copy the file in root named [concordium-backup.concordiumwallet](./concordium-backup.concordiumwallet) -- Build Contracts - - ```bash - cd cis2-multi ## Or cd cis2-market - cargo concordium build --out module.wasm --schema-out schema.bin - ``` - - Interact with Contracts - - Using [Concordium Client](./concordium-client/README.md) - - Using [Node SDK](./node-cli/README.md) - - Using [Frontend React Code](./market-ui/README.md) + - Sell / Buy a CIS2 Token : Using [Frontend React Code](./market-ui/README.md) + - Mint a CIS2 Token : Using [Frontend React Code](./mint-ui/README.md) diff --git a/cis2-auctions/Cargo.toml b/cis2-auctions/Cargo.toml deleted file mode 100644 index 5e01f64..0000000 --- a/cis2-auctions/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "auction-smart-contract" -version = "0.1.0" -authors = ["Concordium "] -edition = "2018" -license = "MPL-2.0" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -default = ["std"] -std = ["concordium-std/std", "concordium-cis2/std"] - -[dependencies] -concordium-std = {version = "*", default-features = false} -concordium-cis2 = {version = "*", default-features = false} - -[lib] -crate-type=["cdylib", "rlib"] diff --git a/cis2-auctions/README.md b/cis2-auctions/README.md deleted file mode 100644 index ebebe85..0000000 --- a/cis2-auctions/README.md +++ /dev/null @@ -1,10 +0,0 @@ -## Build - -- ### [Install tools for development](https://developer.concordium.software/en/mainnet/smart-contracts/guides/setup-tools.html#setup-tools) -- ### [Build the Smart Contract Module](https://developer.concordium.software/en/mainnet/smart-contracts/guides/compile-module.html) - - Make sure your working directory is [cis2-auctions](./) ie `cd cis2-auctions`. - - Execute the following commands - ```bash - cis2-auctions$ cargo concordium build --out ./module.wasm --schema-out ./schema.bin - ``` - - You should have [module file](./module.wasm) & [schema file](./schema.bin) if everything has executed normally diff --git a/cis2-auctions/src/cis2_client.rs b/cis2-auctions/src/cis2_client.rs deleted file mode 100644 index 3e1052c..0000000 --- a/cis2-auctions/src/cis2_client.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! CIS2 client is the intermediatory layer between contract and CIS2 contract. -//! -//! # Description -//! It allows contract to abstract away logic of calling CIS2 contract for the following methods -//! - `transfer` : Calls [`transfer`](https://proposals.concordium.software/CIS/cis-2.html#transfer) - -use std::vec; - -use concordium_cis2::*; -use concordium_std::*; - -use crate::state::State; - -pub const TRANSFER_ENTRYPOINT_NAME: &str = "transfer"; - -#[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -pub enum Cis2ClientError { - InvokeContractError, - ParseParams, -} - -pub struct Cis2Client; - -impl Cis2Client { - pub(crate) fn transfer< - S, - T: IsTokenId + Clone + Copy, - A: IsTokenAmount + Clone + Copy + ops::Sub, - >( - host: &impl HasHost, StateApiType = S>, - token_id: T, - nft_contract_address: ContractAddress, - amount: A, - from: Address, - to: Receiver, - ) -> Result<(), Cis2ClientError> - where - S: HasStateApi, - A: IsTokenAmount, - { - let params = TransferParams(vec![Transfer { - token_id, - amount, - from, - data: AdditionalData::empty(), - to, - }]); - - Cis2Client::invoke_contract_read_only( - host, - &nft_contract_address, - TRANSFER_ENTRYPOINT_NAME, - ¶ms, - )?; - - Ok(()) - } - - fn invoke_contract_read_only( - host: &impl HasHost, StateApiType = S>, - contract_address: &ContractAddress, - entrypoint_name: &str, - params: &P, - ) -> Result { - let invoke_contract_result = host - .invoke_contract_read_only( - contract_address, - params, - EntrypointName::new(entrypoint_name).unwrap_abort(), - Amount::from_ccd(0), - ) - .map_err(|_e| Cis2ClientError::InvokeContractError)?; - let mut invoke_contract_res = match invoke_contract_result { - Some(s) => s, - None => return Result::Err(Cis2ClientError::InvokeContractError), - }; - let parsed_res = - R::deserial(&mut invoke_contract_res).map_err(|_e| Cis2ClientError::ParseParams)?; - - Ok(parsed_res) - } -} diff --git a/cis2-auctions/src/contract.rs b/cis2-auctions/src/contract.rs deleted file mode 100644 index 0a24f70..0000000 --- a/cis2-auctions/src/contract.rs +++ /dev/null @@ -1,579 +0,0 @@ -use concordium_cis2::{OnReceivingCis2Params, Receiver}; -use concordium_std::*; - -use crate::{cis2_client::Cis2Client, error::*, state::*}; - -pub type ContractOnReceivingCis2Params = - OnReceivingCis2Params; - -/// Type of the parameter to the `init` function -#[derive(Serialize, SchemaType)] -pub struct InitParameter { - /// 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, - /// Token needed to participate in the Auction. - pub participation_token: ParticipationTokenIdentifier, -} - -/// Init function that creates a new auction -#[init(contract = "auction", parameter = "InitParameter")] -pub fn auction_init( - ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { - let parameter: InitParameter = ctx.parameter_cursor().get()?; - Ok(State::new(parameter, state_builder)) -} - -#[receive( - contract = "auction", - name = "onReceivingCIS2", - error = "ReceiveError", - mutable -)] -fn auction_on_cis2_received( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> Result<(), ReceiveError> { - // Ensure the sender is a contract. - let sender = if let Address::Contract(contract) = ctx.sender() { - contract - } else { - bail!(ReceiveError::ContractOnly) - }; - - // Parse the parameter. - let params: ContractOnReceivingCis2Params = ctx - .parameter_cursor() - .get() - .map_err(|_| ReceiveError::ParseParams)?; - - let from_account = match params.from { - Address::Account(a) => a, - Address::Contract(_) => bail!(ReceiveError::OnlyAccount), - }; - - let token_identifier = AuctionTokenIdentifier::new(sender, params.token_id, params.amount); - let state = host.state_mut(); - - if state.participation_token.token_eq(&token_identifier) { - state.participants.insert(from_account); - } else { - ensure!(from_account.eq(&ctx.owner()), ReceiveError::UnAuthorized); - ensure_eq!( - state.auction_state, - AuctionState::NotInitialized, - ReceiveError::AuctionAlreadyInitialized - ); - state.auction_state = AuctionState::NotSoldYet(token_identifier) - } - - Ok(()) -} - -/// Receive function for accounts to place a bid in the auction -#[receive( - contract = "auction", - name = "bid", - payable, - mutable, - error = "BidError" -)] -pub fn auction_bid( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, - amount: Amount, -) -> Result<(), BidError> { - let state = host.state(); - // Ensure the auction has not been finalized yet - ensure!(state.auction_state.is_open(), BidError::AuctionNotOpen); - - 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, - }; - - ensure!( - host.state().participants.contains(&sender_address), - BidError::NotAParticipant - ); - - // 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(()) -} - -#[derive(Serial, SchemaType)] -pub struct ViewState { - pub auction_state: AuctionState, - /// The highest bidder so far; The variant `None` represents - /// that no bidder has taken part in the auction yet. - pub highest_bidder: Option, - /// The minimum accepted raise to over bid the current bidder in Euro cent. - pub minimum_raise: u64, - /// Time when auction ends (to be displayed by the front-end) - pub end: Timestamp, - /// Token needed to participate in the Auction - pub participation_token: ParticipationTokenIdentifier, - pub participants: Vec, -} - -/// View function that returns the content of the state -#[receive(contract = "auction", name = "view", return_value = "ViewState")] -pub fn view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { - let state = host.state(); - let participants = state.participants.iter().map(|a| *a).collect(); - - Ok(ViewState { - auction_state: state.auction_state.to_owned(), - highest_bidder: state.highest_bidder, - minimum_raise: state.minimum_raise, - end: state.end, - participation_token: state.participation_token.to_owned(), - participants, - }) -} - -/// ViewHighestBid function that returns the highest bid which is the balance of -/// the contract -#[receive(contract = "auction", name = "viewHighestBid", return_value = "Amount")] -pub fn view_highest_bid( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> 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 = "auction", - name = "finalize", - mutable, - error = "FinalizeError" -)] -pub fn auction_finalize( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> Result<(), FinalizeError> { - let state = host.state(); - // Ensure the auction has not been finalized yet - ensure!(state.auction_state.is_open(), FinalizeError::AuctionNotOpen); - - 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 { - if let AuctionState::NotSoldYet(token_identifier) = &state.auction_state { - Cis2Client::transfer( - host, - token_identifier.token_id, - token_identifier.contract, - token_identifier.amount, - Address::Contract(ctx.self_address()), - Receiver::Account(account_address), - ) - .map_err(|_| FinalizeError::Cis2TransferError)? - } - - // 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(()) -} - -#[concordium_cfg_test] -mod tests { - use super::*; - use concordium_cis2::{AdditionalData, TokenAmountU64, TokenIdU8}; - use core::fmt::Debug; - use std::sync::atomic::{AtomicU8, Ordering}; - use test_infrastructure::*; - - // A counter for generating new accounts - static ADDRESS_COUNTER: AtomicU8 = AtomicU8::new(0); - const AUCTION_END: u64 = 1; - const OWNER_ACCOUNT: AccountAddress = AccountAddress([ - 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, 1, - ]); - const PARTICIPATION_TOKEN: ParticipationTokenIdentifier = ParticipationTokenIdentifier { - contract: ContractAddress { - index: 1, - subindex: 0, - }, - token_id: TokenIdU8(1), - }; - - fn expect_error(expr: Result, err: E, msg: &str) - where - E: Eq + Debug, - T: Debug, - { - let actual = expr.expect_err_report(msg); - unsafe { - claim_eq!(actual, err); - } - } - - fn item_end_parameter() -> InitParameter { - InitParameter { - end: Timestamp::from_timestamp_millis(AUCTION_END), - minimum_raise: 100, - participation_token: PARTICIPATION_TOKEN, - } - } - - fn create_parameter_bytes(parameter: &InitParameter) -> Vec { - to_bytes(parameter) - } - - fn parametrized_init_ctx(parameter_bytes: &[u8]) -> TestInitContext { - let mut ctx = TestInitContext::empty(); - ctx.set_parameter(parameter_bytes); - ctx - } - - fn new_account() -> AccountAddress { - let account = AccountAddress([ADDRESS_COUNTER.load(Ordering::SeqCst); 32]); - ADDRESS_COUNTER.fetch_add(1, Ordering::SeqCst); - account - } - - fn new_account_ctx<'a>() -> (AccountAddress, TestReceiveContext<'a>) { - let account = new_account(); - let ctx = new_ctx(account, Address::Account(account), AUCTION_END); - (account, ctx) - } - - fn new_ctx<'a>( - owner: AccountAddress, - sender: Address, - slot_time: u64, - ) -> TestReceiveContext<'a> { - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(sender); - ctx.set_owner(owner); - ctx.set_metadata_slot_time(Timestamp::from_timestamp_millis(slot_time)); - ctx - } - - fn bid( - host: &mut TestHost>, - ctx: &TestContext, - amount: Amount, - current_smart_contract_balance: Amount, - ) { - // Setting the contract balance. - // This should be the sum of the contract’s initial balance and - // the amount you wish to invoke it with when using the TestHost. - // https://docs.rs/concordium-std/latest/concordium_std/test_infrastructure/struct.TestHost.html#method.set_self_balance - // This is because the `self_balance` function on-chain behaves as follows: - // https://docs.rs/concordium-std/latest/concordium_std/trait.HasHost.html#tymethod.self_balance - host.set_self_balance(amount + current_smart_contract_balance); - - // Invoking the bid function. - auction_bid(ctx, host, amount).expect_report("Bidding should pass."); - } - - fn initialize_auction(host: &mut TestHost>) { - let sender = Address::Contract(PARTICIPATION_TOKEN.contract); - let mut ctx = new_ctx(OWNER_ACCOUNT, sender, 0); - let params = ContractOnReceivingCis2Params { - amount: TokenAmountU64::from(1), - data: AdditionalData::empty(), - from: Address::Account(OWNER_ACCOUNT), - token_id: TokenIdU8(2), - }; - let param_bytes = to_bytes(¶ms); - ctx.set_parameter(¶m_bytes); - - auction_on_cis2_received(&ctx, host).expect("should add a token for Auction"); - } - - fn add_auction_participant(host: &mut TestHost>, participant: Address) { - let sender = Address::Contract(PARTICIPATION_TOKEN.contract); - let mut ctx = new_ctx(OWNER_ACCOUNT, sender, 0); - let params = ContractOnReceivingCis2Params { - amount: TokenAmountU64::from(1), - data: AdditionalData::empty(), - from: participant, - token_id: PARTICIPATION_TOKEN.token_id, - }; - let param_bytes = to_bytes(¶ms); - ctx.set_parameter(¶m_bytes); - - auction_on_cis2_received(&ctx, host).expect("should add a participant"); - } - - #[concordium_test] - /// Test that the smart-contract initialization sets the state correctly - /// (no bids, active state, indicated auction-end time and item name). - fn test_init() { - let parameter_bytes = create_parameter_bytes(&item_end_parameter()); - let ctx = parametrized_init_ctx(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - - let state_result = auction_init(&ctx, &mut state_builder); - state_result.expect_report("Contract initialization results in error"); - } - - #[concordium_test] - /// Test a sequence of bids and finalizations: - /// 0. Auction is initialized. - /// 1. Alice successfully bids 0.1 CCD. - /// 2. Alice successfully bids 0.2 CCD, highest - /// bid becomes 0.2 CCD. Alice gets her 0.1 CCD refunded. - /// 3. Bob successfully bids 0.3 CCD, highest - /// bid becomes 0.3 CCD. Alice gets her 0.2 CCD refunded. - /// 4. Someone tries to finalize the auction before - /// its end time. Attempt fails. - /// 5. Dave successfully finalizes the auction after its end time. - /// Carol (the owner of the contract) collects the highest bid amount. - /// 6. Attempts to subsequently bid or finalize fail. - fn test_auction_bid_and_finalize() { - let parameter_bytes = create_parameter_bytes(&item_end_parameter()); - let ctx0 = parametrized_init_ctx(¶meter_bytes); - - let amount = Amount::from_micro_ccd(100); - let winning_amount = Amount::from_micro_ccd(300); - let big_amount = Amount::from_micro_ccd(500); - - let mut state_builder = TestStateBuilder::new(); - - // Initializing auction - let initial_state = - auction_init(&ctx0, &mut state_builder).expect("Initialization should pass"); - - let mut host = TestHost::new(initial_state, state_builder); - host.set_exchange_rates(ExchangeRates { - euro_per_energy: ExchangeRate::new_unchecked(1, 1), - micro_ccd_per_euro: ExchangeRate::new_unchecked(1, 1), - }); - - initialize_auction(&mut host); - - // 1st bid: Alice bids `amount`. - // The current_smart_contract_balance before the invoke is 0. - let (alice, alice_ctx) = new_account_ctx(); - add_auction_participant(&mut host, alice_ctx.sender()); - bid(&mut host, &alice_ctx, amount, Amount::from_micro_ccd(0)); - - // 2nd bid: Alice bids `amount + amount`. - // Alice gets her initial bid refunded. - // The current_smart_contract_balance before the invoke is amount. - bid(&mut host, &alice_ctx, amount + amount, amount); - - // 3rd bid: Bob bids `winning_amount`. - // Alice gets refunded. - // The current_smart_contract_balance before the invoke is amount + amount. - let (bob, bob_ctx) = new_account_ctx(); - add_auction_participant(&mut host, bob_ctx.sender()); - bid(&mut host, &bob_ctx, winning_amount, amount + amount); - - // Trying to finalize auction that is still active - // (specifically, the tx is submitted at the last moment, - // at the AUCTION_END time) - let mut ctx4 = TestReceiveContext::empty(); - ctx4.set_metadata_slot_time(Timestamp::from_timestamp_millis(AUCTION_END)); - ctx4.set_self_address(ContractAddress { - index: 1, - subindex: 0, - }); - let fin_res = auction_finalize(&ctx4, &mut host); - expect_error( - fin_res, - FinalizeError::AuctionStillActive, - "Finalizing the auction should fail when it's before auction end time", - ); - - // Finalizing auction - let carol = new_account(); - let dave = new_account(); - let mut ctx5 = new_ctx(carol, Address::Account(dave), AUCTION_END + 1); - ctx5.set_self_address(ContractAddress { - index: 1, - subindex: 0, - }); - host.setup_mock_entrypoint( - ContractAddress { - index: 1, - subindex: 0, - }, - OwnedEntrypointName::new_unchecked("transfer".into()), - MockFn::returning_ok(()), - ); - let fin_res2 = auction_finalize(&ctx5, &mut host); - fin_res2.expect_report("Finalizing the auction should work"); - let transfers = host.get_transfers(); - // The input arguments of all executed `host.invoke_transfer` - // functions are checked here. - unsafe { - claim_eq!( - &transfers[..], - &[ - (alice, amount), - (alice, amount + amount), - (carol, winning_amount), - ], - "Transferring CCD to Alice/Carol should work" - ); - claim_eq!( - host.state().auction_state, - AuctionState::Sold(bob), - "Finalizing the auction should change the auction state to `Sold(bob)`" - ); - claim_eq!( - host.state().highest_bidder, - Some(bob), - "Finalizing the auction should mark bob as highest bidder" - ); - } - - // Attempting to finalize auction again should fail. - let fin_res3 = auction_finalize(&ctx5, &mut host); - expect_error( - fin_res3, - FinalizeError::AuctionNotOpen, - "Finalizing the auction a second time should fail", - ); - - // Attempting to bid again should fail. - let res4 = auction_bid(&bob_ctx, &mut host, big_amount); - expect_error( - res4, - BidError::AuctionNotOpen, - "Bidding should fail because the auction is finalized", - ); - } - - #[concordium_test] - /// Bids for amounts lower or equal to the highest bid should be rejected. - fn test_auction_bid_repeated_bid() { - let ctx1 = new_account_ctx().1; - let ctx2 = new_account_ctx().1; - - let parameter_bytes = create_parameter_bytes(&item_end_parameter()); - let ctx0 = parametrized_init_ctx(¶meter_bytes); - - let amount = Amount::from_micro_ccd(100); - - let mut state_builder = TestStateBuilder::new(); - - // Initializing auction - let initial_state = - auction_init(&ctx0, &mut state_builder).expect("Initialization should succeed."); - - let mut host = TestHost::new(initial_state, state_builder); - host.set_exchange_rates(ExchangeRates { - euro_per_energy: ExchangeRate::new_unchecked(1, 1), - micro_ccd_per_euro: ExchangeRate::new_unchecked(1, 1), - }); - - initialize_auction(&mut host); - add_auction_participant(&mut host, ctx1.sender()); - add_auction_participant(&mut host, ctx2.sender()); - - // 1st bid: Account1 bids `amount`. - // The current_smart_contract_balance before the invoke is 0. - bid(&mut host, &ctx1, amount, Amount::from_micro_ccd(0)); - - // Setting the contract balance. - // This should be the sum of the contract’s initial balance and - // the amount you wish to invoke it with when using the TestHost. - // The current_smart_contract_balance before the invoke is `amount`. - // The balance we wish to invoke the next function with is `amount` as well. - // https://docs.rs/concordium-std/latest/concordium_std/test_infrastructure/struct.TestHost.html#method.set_self_balance - // This is because the `self_balance` function on-chain behaves as follows: - // https://docs.rs/concordium-std/latest/concordium_std/trait.HasHost.html#tymethod.self_balance - host.set_self_balance(amount + amount); - - // 2nd bid: Account2 bids `amount` (should fail - // because amount is equal to highest bid). - let res2 = auction_bid(&ctx2, &mut host, amount); - expect_error( - res2, - BidError::BidBelowCurrentBid, - "Bidding 2 should fail because bid amount must be higher than highest bid", - ); - } - - #[concordium_test] - /// Bids for 0 CCD should be rejected. - fn test_auction_bid_zero() { - let mut state_builder = TestStateBuilder::new(); - - // initializing auction - let parameter_bytes = create_parameter_bytes(&item_end_parameter()); - let ctx = parametrized_init_ctx(¶meter_bytes); - let initial_state = - auction_init(&ctx, &mut state_builder).expect("Initialization should succeed."); - - let mut host = TestHost::new(initial_state, state_builder); - - initialize_auction(&mut host); - let ctx1 = new_account_ctx().1; - add_auction_participant(&mut host, ctx1.sender()); - - let res = auction_bid(&ctx1, &mut host, Amount::zero()); - expect_error( - res, - BidError::BidBelowCurrentBid, - "Bidding zero should fail", - ); - } -} diff --git a/cis2-auctions/src/error.rs b/cis2-auctions/src/error.rs deleted file mode 100644 index 243a502..0000000 --- a/cis2-auctions/src/error.rs +++ /dev/null @@ -1,39 +0,0 @@ -use concordium_std::*; - -/// `bid` function errors -#[derive(Debug, PartialEq, Eq, Clone, Reject, Serial, 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. - AuctionNotOpen, - NotAParticipant -} - -/// `finalize` function errors -#[derive(Debug, PartialEq, Eq, Clone, Reject, Serial, SchemaType)] -pub enum FinalizeError { - /// Raised when finalizing an auction before auction end time passed - AuctionStillActive, - /// Raised when finalizing an auction that is already finalized - AuctionNotOpen, - Cis2TransferError, -} - -#[derive(Debug, PartialEq, Eq, Clone, Reject, Serial, SchemaType)] -pub enum ReceiveError { - ParseParams, - ContractOnly, - OnlyAccount, - UnAuthorized, - AuctionAlreadyInitialized - -} diff --git a/cis2-auctions/src/lib.rs b/cis2-auctions/src/lib.rs deleted file mode 100644 index 3935a23..0000000 --- a/cis2-auctions/src/lib.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! # 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)] - -mod cis2_client; -pub mod contract; -mod error; -mod state; diff --git a/cis2-auctions/src/state.rs b/cis2-auctions/src/state.rs deleted file mode 100644 index 8e0e6c8..0000000 --- a/cis2-auctions/src/state.rs +++ /dev/null @@ -1,99 +0,0 @@ -use concordium_cis2::{TokenAmountU64, TokenIdU8}; -use concordium_std::*; - -use crate::contract::InitParameter; - -pub type ContractTokenId = TokenIdU8; - -/// Contract token amount type. -pub type ContractTokenAmount = TokenAmountU64; - -#[derive(Debug, Serialize, SchemaType, Eq, PartialEq, PartialOrd, Clone)] -pub struct AuctionTokenIdentifier { - pub(crate) contract: ContractAddress, - pub(crate) token_id: ContractTokenId, - pub(crate) amount: ContractTokenAmount, -} - -#[derive(Serial, Deserial, SchemaType, Clone)] -pub struct ParticipationTokenIdentifier { - pub(crate) contract: ContractAddress, - pub(crate) token_id: ContractTokenId, -} - -impl ParticipationTokenIdentifier { - pub(crate) fn token_eq(&self, token_identifier: &AuctionTokenIdentifier) -> bool { - self.contract.eq(&token_identifier.contract) && self.token_id.eq(&token_identifier.token_id) - } -} - -impl AuctionTokenIdentifier { - pub(crate) fn new( - contract: ContractAddress, - token_id: ContractTokenId, - amount: ContractTokenAmount, - ) -> Self { - AuctionTokenIdentifier { - contract, - token_id, - amount, - } - } -} - -/// The state of the auction. -#[derive(Debug, Serialize, SchemaType, Eq, PartialEq, PartialOrd, Clone)] -pub enum AuctionState { - NotInitialized, - /// 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(AuctionTokenIdentifier), - /// The auction has been finalized and the item has been sold to the - /// winning `AccountAddress`. - Sold(AccountAddress), -} - -impl AuctionState { - pub fn is_open(&self) -> bool { - match self { - AuctionState::NotInitialized => false, - AuctionState::NotSoldYet(_) => true, - AuctionState::Sold(_) => false, - } - } -} - -/// 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(StateClone, Serial, DeserialWithState)] -#[concordium(state_parameter = "S")] -pub struct State { - /// State of the auction - pub auction_state: AuctionState, - /// The highest bidder so far; The variant `None` represents - /// that no bidder has taken part in the auction yet. - pub highest_bidder: Option, - /// The minimum accepted raise to over bid the current bidder in Euro cent. - pub minimum_raise: u64, - /// Time when auction ends (to be displayed by the front-end) - pub end: Timestamp, - /// Token needed to participate in the Auction - pub participation_token: ParticipationTokenIdentifier, - pub participants: StateSet, -} - -impl State { - pub fn new(parameter: InitParameter, state_builder: &mut StateBuilder) -> Self { - State { - auction_state: AuctionState::NotInitialized, - highest_bidder: None, - minimum_raise: parameter.minimum_raise, - end: parameter.end, - participation_token: parameter.participation_token, - participants: state_builder.new_set(), - } - } -} diff --git a/cis2-fractionalizer/.gitignore b/cis2-fractionalizer/.gitignore deleted file mode 100644 index f2f9e58..0000000 --- a/cis2-fractionalizer/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target -Cargo.lock \ No newline at end of file diff --git a/cis2-fractionalizer/Cargo.toml b/cis2-fractionalizer/Cargo.toml deleted file mode 100644 index 4de1163..0000000 --- a/cis2-fractionalizer/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "cis2-multi" -version = "0.1.0" -authors = ["Concordium "] -edition = "2018" -license = "MPL-2.0" - -[features] -default = ["std"] -std = ["concordium-std/std", "concordium-cis2/std"] - -[dependencies] -concordium-std = { version = "*", default-features = false } -concordium-cis2 = { version = "*", default-features = false } -hex = "*" - -[lib] -crate-type=["cdylib", "rlib"] - -[profile.release] -codegen-units = 1 -opt-level = "s" diff --git a/cis2-fractionalizer/README.md b/cis2-fractionalizer/README.md deleted file mode 100644 index d111a6a..0000000 --- a/cis2-fractionalizer/README.md +++ /dev/null @@ -1,10 +0,0 @@ -## Build - -- ### [Install tools for development](https://developer.concordium.software/en/mainnet/smart-contracts/guides/setup-tools.html#setup-tools) -- ### [Build the Smart Contract Module](https://developer.concordium.software/en/mainnet/smart-contracts/guides/compile-module.html) - - Make sure your working directory is [cis2-fractionalizer](./) ie `cd cis2-fractionalizer`. - - Execute the following commands - ```bash - cis2-fractionalizer$ cargo concordium build --out ./module.wasm --schema-out ./schema.bin - ``` - - You should have [module file](./module.wasm) & [schema file](./schema.bin) if everything has executed normally diff --git a/cis2-fractionalizer/src/cis2_client.rs b/cis2-fractionalizer/src/cis2_client.rs deleted file mode 100644 index 137e14b..0000000 --- a/cis2-fractionalizer/src/cis2_client.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! CIS2 client is the intermediatory layer between contract and CIS2 contract. -//! -//! # Description -//! It allows contract to abstract away logic of calling CIS2 contract for the following methods -//! - `transfer` : Calls [`transfer`](https://proposals.concordium.software/CIS/cis-2.html#transfer) - -use std::vec; - -use concordium_cis2::*; -use concordium_std::*; - -use crate::state::State; - -pub const TRANSFER_ENTRYPOINT_NAME: &str = "transfer"; - -#[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -pub enum Cis2ClientError { - InvokeContractError, - ParseParams, -} - -pub struct Cis2Client; - -impl Cis2Client { - pub(crate) fn transfer< - S, - T: IsTokenId + Clone + Copy, - A: IsTokenAmount + Clone + Copy + ops::Sub, - >( - host: &mut impl HasHost, StateApiType = S>, - token_id: T, - nft_contract_address: ContractAddress, - amount: A, - from: Address, - to: Receiver, - ) -> Result<(), Cis2ClientError> - where - S: HasStateApi, - A: IsTokenAmount, - { - let params = TransferParams(vec![Transfer { - token_id, - amount, - from, - data: AdditionalData::empty(), - to, - }]); - - Cis2Client::invoke_contract_read_only( - host, - &nft_contract_address, - TRANSFER_ENTRYPOINT_NAME, - ¶ms, - )?; - - Ok(()) - } - - fn invoke_contract_read_only( - host: &mut impl HasHost, StateApiType = S>, - contract_address: &ContractAddress, - entrypoint_name: &str, - params: &P, - ) -> Result { - let invoke_contract_result = host - .invoke_contract_read_only( - contract_address, - params, - EntrypointName::new(entrypoint_name).unwrap_abort(), - Amount::from_ccd(0), - ) - .map_err(|_e| Cis2ClientError::InvokeContractError)?; - let mut invoke_contract_res = match invoke_contract_result { - Some(s) => s, - None => return Result::Err(Cis2ClientError::InvokeContractError), - }; - let parsed_res = - R::deserial(&mut invoke_contract_res).map_err(|_e| Cis2ClientError::ParseParams)?; - - Ok(parsed_res) - } -} diff --git a/cis2-fractionalizer/src/contract.rs b/cis2-fractionalizer/src/contract.rs deleted file mode 100644 index 6fbcd43..0000000 --- a/cis2-fractionalizer/src/contract.rs +++ /dev/null @@ -1,954 +0,0 @@ -use concordium_cis2::*; -use concordium_std::*; - -use crate::{ - cis2_client::Cis2Client, - error::{ContractError, CustomContractError}, - params::{ - ContractBalanceOfQueryParams, ContractBalanceOfQueryResponse, MintParams, - SetImplementorsParams, TransferParameter, ViewAddressState, ViewState, - }, - state::State, - ContractResult, ContractTokenAmount, ContractTokenId, SUPPORTS_STANDARDS, -}; - -// Contract functions -/// Initialize contract instance with a no token types. -#[init(contract = "CIS2-Fractionalizer")] -fn contract_init( - _ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { - // Construct the initial contract state. - Ok(State::empty(state_builder)) -} - -/// View function for testing. This reports on the entire state of the contract -/// for testing purposes. In a realistic example there `balance_of` and similar -/// functions with a smaller response. -#[receive( - contract = "CIS2-Fractionalizer", - name = "view", - return_value = "ViewState" -)] -fn contract_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { - let state = host.state(); - - let mut inner_state = Vec::new(); - for (k, a_state) in state.state.iter() { - let mut balances = Vec::new(); - let mut operators = Vec::new(); - for (token_id, amount) in a_state.balances.iter() { - balances.push((*token_id, *amount)); - } - for o in a_state.operators.iter() { - operators.push(*o); - } - - inner_state.push(( - *k, - ViewAddressState { - balances, - operators, - }, - )); - } - let mut tokens = Vec::new(); - for v in state.tokens.iter() { - tokens.push(v.0.to_owned()); - } - - Ok(ViewState { - state: inner_state, - tokens, - collaterals: state.collaterals.iter().map(|(a, b)| (*a, *b)).collect(), - }) -} - -/// Mint new tokens with a given address as the owner of these tokens. -/// Can only be called by the contract owner. -/// Logs a `Mint` and a `TokenMetadata` event for each token. -/// The url for the token metadata is the token ID encoded in hex, appended on -/// the `TOKEN_METADATA_BASE_URL`. -/// -/// It rejects if: -/// - The sender is not the contract instance owner. -/// - Fails to parse parameter. -/// - Any of the tokens fails to be minted, which could be if: -/// - Fails to log Mint event. -/// - Fails to log TokenMetadata event. -/// -/// Note: Can at most mint 32 token types in one call due to the limit on the -/// number of logs a smart contract can produce on each function call. -#[receive( - contract = "CIS2-Fractionalizer", - name = "mint", - parameter = "MintParams", - error = "ContractError", - enable_logger, - mutable -)] -fn contract_mint( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, - logger: &mut impl HasLogger, -) -> ContractResult<()> { - let sender = match ctx.sender() { - Address::Account(a) => a, - Address::Contract(_) => bail!(CustomContractError::AccountsOnly.into()), - }; - - // Parse the parameter. - let params: MintParams = ctx.parameter_cursor().get()?; - - let (state, builder) = host.state_and_builder(); - for (token_id, token_info) in params.tokens { - ensure!( - state.has_collateral(&token_info.contract, &token_info.token_id, &sender), - concordium_cis2::Cis2Error::Custom(CustomContractError::InvalidCollateral) - ); - - // Mint the token in the state. - state.mint( - &token_id, - &token_info.metadata, - token_info.amount, - ¶ms.owner, - builder, - ); - - state.update_collateral_token( - token_info.contract, - token_info.token_id, - sender, - token_id, - )?; - - // Event for minted token. - logger.log(&Cis2Event::Mint(MintEvent { - token_id, - amount: token_info.amount, - owner: params.owner, - }))?; - - // Metadata URL for the token. - logger.log(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id, - metadata_url: token_info.metadata.to_metadata_url(), - }, - ))?; - } - Ok(()) -} - -/// Execute a list of token transfers, in the order of the list. -/// If the transfer is to the self address the tokens are burned instead. -/// If the balance after burning is zero then the collateral is returned back to the original sender. -/// -/// Logs a `Transfer` event and invokes a receive hook function for every -/// transfer in the list. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Any of the transfers fail to be executed, which could be if: -/// - The `token_id` does not exist. -/// - The sender is not the owner of the token, or an operator for this -/// specific `token_id` and `from` address. -/// - The token is not owned by the `from`. -/// - Fails to log event. -/// - Any of the receive hook function calls rejects. -#[receive( - contract = "CIS2-Fractionalizer", - name = "transfer", - parameter = "TransferParameter", - error = "ContractError", - enable_logger, - mutable -)] -fn contract_transfer( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, - logger: &mut impl HasLogger, -) -> ContractResult<()> { - // Parse the parameter. - let TransferParams(transfers): TransferParameter = ctx.parameter_cursor().get()?; - // Get the sender who invoked this contract function. - let sender = ctx.sender(); - - for Transfer { - token_id, - amount, - from, - to, - data, - } in transfers - { - let (state, builder) = host.state_and_builder(); - // Authenticate the sender for this transfer - ensure!( - from == sender || state.is_operator(&sender, &from), - ContractError::Unauthorized - ); - - if to.address().matches_contract(&ctx.self_address()) { - // tokens are being transferred to self - // burn the tokens - let remaining_amount: ContractTokenAmount = state.burn(&token_id, amount, &from)?; - - // log burn event - logger.log(&Cis2Event::Burn(BurnEvent { - token_id, - amount, - owner: from, - }))?; - - // Check of there is any remaining amount - if remaining_amount.eq(&ContractTokenAmount::from(0)) { - // Everything has been burned - // Transfer collateral back to the original owner - let (collateral_key, collateral_amount) = state - .find_collateral(&token_id) - .ok_or(Cis2Error::Custom(CustomContractError::InvalidCollateral))?; - - // Return back the collateral - Cis2Client::transfer( - host, - collateral_key.token_id, - collateral_key.contract, - collateral_amount, - concordium_std::Address::Contract(ctx.self_address()), - concordium_cis2::Receiver::Account(collateral_key.owner), - ) - .map_err(CustomContractError::Cis2ClientError)?; - } - } else { - let to_address = to.address(); - - // Tokens are being transferred to another address - // Update the contract state - state.transfer(&token_id, amount, &from, &to_address, builder)?; - - // Log transfer event - logger.log(&Cis2Event::Transfer(TransferEvent { - token_id, - amount, - from, - to: to_address, - }))?; - - // If the receiver is a contract we invoke it. - if let Receiver::Contract(address, entrypoint_name) = to { - let parameter = OnReceivingCis2Params { - token_id, - amount, - from, - data, - }; - host.invoke_contract( - &address, - ¶meter, - entrypoint_name.as_entrypoint_name(), - Amount::zero(), - )?; - } - } - } - - Ok(()) -} - -/// Enable or disable addresses as operators of the sender address. -/// Logs an `UpdateOperator` event. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Fails to log event. -#[receive( - contract = "CIS2-Fractionalizer", - name = "updateOperator", - parameter = "UpdateOperatorParams", - error = "ContractError", - enable_logger, - mutable -)] -fn contract_update_operator( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, - logger: &mut impl HasLogger, -) -> ContractResult<()> { - // Parse the parameter. - let UpdateOperatorParams(params) = ctx.parameter_cursor().get()?; - // Get the sender who invoked this contract function. - let sender = ctx.sender(); - - let (state, builder) = host.state_and_builder(); - for param in params { - // Update the operator in the state. - match param.update { - OperatorUpdate::Add => state.add_operator(&sender, ¶m.operator, builder), - OperatorUpdate::Remove => state.remove_operator(&sender, ¶m.operator), - } - - // Log the appropriate event - logger.log( - &Cis2Event::::UpdateOperator( - UpdateOperatorEvent { - owner: sender, - operator: param.operator, - update: param.update, - }, - ), - )?; - } - Ok(()) -} - -/// Get the balance of given token IDs and addresses. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Any of the queried `token_id` does not exist. -#[receive( - contract = "CIS2-Fractionalizer", - name = "balanceOf", - parameter = "ContractBalanceOfQueryParams", - return_value = "ContractBalanceOfQueryResponse", - error = "ContractError" -)] -fn contract_balance_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ContractResult { - // Parse the parameter. - let params: ContractBalanceOfQueryParams = ctx.parameter_cursor().get()?; - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for query in params.queries { - // Query the state for balance. - let amount = host.state().balance(&query.token_id, &query.address)?; - response.push(amount); - } - let result = ContractBalanceOfQueryResponse::from(response); - Ok(result) -} - -/// Takes a list of queries. Each query is an owner address and some address to -/// check as an operator of the owner address. -/// -/// It rejects if: -/// - It fails to parse the parameter. -#[receive( - contract = "CIS2-Fractionalizer", - name = "operatorOf", - parameter = "OperatorOfQueryParams", - return_value = "OperatorOfQueryResponse", - error = "ContractError" -)] -fn contract_operator_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ContractResult { - // Parse the parameter. - let params: OperatorOfQueryParams = ctx.parameter_cursor().get()?; - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for query in params.queries { - // Query the state for address being an operator of owner. - let is_operator = host.state().is_operator(&query.address, &query.owner); - response.push(is_operator); - } - let result = OperatorOfQueryResponse::from(response); - Ok(result) -} - -/// Parameter type for the CIS-2 function `tokenMetadata` specialized to the -/// subset of TokenIDs used by this contract. -type ContractTokenMetadataQueryParams = TokenMetadataQueryParams; - -/// Get the token metadata URLs and checksums given a list of token IDs. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Any of the queried `token_id` does not exist. -#[receive( - contract = "CIS2-Fractionalizer", - name = "tokenMetadata", - parameter = "ContractTokenMetadataQueryParams", - return_value = "TokenMetadataQueryResponse", - error = "ContractError" -)] -fn contract_token_metadata( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ContractResult { - // Parse the parameter. - let params: ContractTokenMetadataQueryParams = ctx.parameter_cursor().get()?; - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for token_id in params.queries { - let metadata_url: MetadataUrl = match host - .state() - .tokens - .get(&token_id) - .map(|metadata| metadata.to_owned()) - { - Option::Some(m) => Result::Ok(m), - Option::None => Result::Err(ContractError::InvalidTokenId), - }?; - - response.push(metadata_url); - } - let result = TokenMetadataQueryResponse::from(response); - Ok(result) -} - -/// This functions should be invoked by any CIS2 Contract whose token is being transferred. -/// TO this contract -/// -/// Upon receiving any token its added to the collateral state of the contract. -/// Mint function can be called in a separate transaction to mint a token against the collateral. -/// -/// It rejects if: -/// - Sender is not a contract. -/// - It fails to parse the parameter. -/// - Contract name part of the parameter is invalid. -/// - Calling back `transfer` to sender contract rejects. -#[receive( - contract = "CIS2-Fractionalizer", - name = "onReceivingCIS2", - error = "ContractError", - mutable -)] -fn contract_on_cis2_received( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { - // Ensure the sender is a contract. - let sender = if let Address::Contract(contract) = ctx.sender() { - contract - } else { - bail!(CustomContractError::ContractOnly.into()) - }; - - // Parse the parameter. - let params: OnReceivingCis2Params = - ctx.parameter_cursor().get()?; - - let from_account = match params.from { - Address::Account(a) => a, - Address::Contract(_) => bail!(CustomContractError::ContractOnly.into()), - }; - - host.state_mut() - .add_collateral(sender, params.token_id, from_account, params.amount); - Ok(()) -} - -/// Get the supported standards or addresses for a implementation given list of -/// standard identifiers. -/// -/// It rejects if: -/// - It fails to parse the parameter. -#[receive( - contract = "CIS2-Fractionalizer", - name = "supports", - parameter = "SupportsQueryParams", - return_value = "SupportsQueryResponse", - error = "ContractError" -)] -fn contract_supports( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ContractResult { - // Parse the parameter. - let params: SupportsQueryParams = ctx.parameter_cursor().get()?; - - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for std_id in params.queries { - if SUPPORTS_STANDARDS.contains(&std_id.as_standard_identifier()) { - response.push(SupportResult::Support); - } else { - response.push(host.state().have_implementors(&std_id)); - } - } - let result = SupportsQueryResponse::from(response); - Ok(result) -} - -/// Set the addresses for an implementation given a standard identifier and a -/// list of contract addresses. -/// -/// It rejects if: -/// - Sender is not the owner of the contract instance. -/// - It fails to parse the parameter. -#[receive( - contract = "CIS2-Fractionalizer", - name = "setImplementors", - parameter = "SetImplementorsParams", - error = "ContractError", - mutable -)] -fn contract_set_implementor( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { - // Authorize the sender. - ensure!( - ctx.sender().matches_account(&ctx.owner()), - ContractError::Unauthorized - ); - // Parse the parameter. - let params: SetImplementorsParams = ctx.parameter_cursor().get()?; - // Update the implementors in the state - host.state_mut() - .set_implementors(params.id, params.implementors); - Ok(()) -} - -// Tests -#[concordium_cfg_test] -mod tests { - use crate::params::{TokenMetadata, TokenMintParams}; - - use super::*; - use test_infrastructure::*; - - const ACCOUNT_0: AccountAddress = AccountAddress([0u8; 32]); - const ADDRESS_0: Address = Address::Account(ACCOUNT_0); - const ACCOUNT_1: AccountAddress = AccountAddress([1u8; 32]); - const ADDRESS_1: Address = Address::Account(ACCOUNT_1); - const TOKEN_0: ContractTokenId = TokenIdU8(2); - const TOKEN_1: ContractTokenId = TokenIdU8(42); - const TOKEN_COLLATERAL_0: ContractTokenId = TokenIdU8(100); - const TOKEN_COLLATERAL_1: ContractTokenId = TokenIdU8(200); - const CONTRACT_COLLATERAL_0: ContractAddress = ContractAddress { - index: 1, - subindex: 0, - }; - - /// Test helper function which creates a contract state with two tokens with - /// id `TOKEN_0` and id `TOKEN_1` owned by `ADDRESS_0` - fn initial_state(state_builder: &mut StateBuilder) -> State { - let mut state = State::empty(state_builder); - state.mint( - &TOKEN_0, - &{ - let url = "url".to_owned(); - let hash = - "db2ca420a0090593ac6559ff2a98ce30abfe665d7a18ff3c63883e8b98622a73".to_owned(); - TokenMetadata { url, hash } - }, - 400.into(), - &ADDRESS_0, - state_builder, - ); - state.mint( - &TOKEN_1, - &{ - let url = "url".to_owned(); - let hash = - "db2ca420a0090593ac6559ff2a98ce30abfe665d7a18ff3c63883e8b98622a73".to_owned(); - TokenMetadata { url, hash } - }, - 1.into(), - &ADDRESS_0, - state_builder, - ); - state - } - - /// Test initialization succeeds with a state with no tokens. - #[concordium_test] - fn test_init() { - // Setup the context - let ctx = TestInitContext::empty(); - let mut builder = TestStateBuilder::new(); - - // Call the contract function. - let result = contract_init(&ctx, &mut builder); - - // Check the result - let state = result.expect_report("Contract initialization failed"); - - // Check the state - claim_eq!( - state.tokens.iter().count(), - 0, - "Only one token is initialized" - ); - } - - /// Test minting succeeds and the tokens are owned by the given address and - /// the appropriate events are logged. - #[concordium_test] - fn test_mint() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - - let url = "url".to_owned(); - let hash = "db2ca420a0090593ac6559ff2a98ce30abfe665d7a18ff3c63883e8b98622a73".to_owned(); - // and parameter. - let mut tokens = collections::BTreeMap::new(); - tokens.insert( - TOKEN_0, - TokenMintParams { - amount: 400.into(), - token_id: TOKEN_COLLATERAL_0, - contract: CONTRACT_COLLATERAL_0, - metadata: TokenMetadata { - url: url.to_string(), - hash: hash.to_string(), - }, - }, - ); - tokens.insert( - TOKEN_1, - TokenMintParams { - amount: 400.into(), - token_id: TOKEN_COLLATERAL_1, - contract: CONTRACT_COLLATERAL_0, - metadata: TokenMetadata { url, hash }, - }, - ); - let parameter = MintParams { - owner: ADDRESS_0, - tokens, - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = State::empty(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_mint(&ctx, &mut host, &mut logger); - - // Check the result - claim!(result.is_ok(), "Results in rejection"); - - // Check the state - claim_eq!( - host.state().tokens.iter().count(), - 2, - "Only one token is initialized" - ); - let balance0 = host - .state() - .balance(&TOKEN_0, &ADDRESS_0) - .expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 400.into(), - "Initial tokens are owned by the contract instantiater" - ); - - let balance1 = host - .state() - .balance(&TOKEN_1, &ADDRESS_0) - .expect_report("Token is expected to exist"); - unsafe { - claim_eq!( - balance1, - 1.into(), - "Initial tokens are owned by the contract instantiater" - ); - - // Check the logs - claim_eq!(logger.logs.len(), 4, "Exactly four events should be logged"); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(400), - }))), - "Expected an event for minting TOKEN_0" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_1, - amount: ContractTokenAmount::from(1), - }))), - "Expected an event for minting TOKEN_1" - ); - - claim!( - logger.logs.contains(&to_bytes( - &Cis2Event::TokenMetadata::<_, ContractTokenAmount>(TokenMetadataEvent { - token_id: TOKEN_0, - metadata_url: (TokenMetadata { - url: "url".to_string(), - hash: - "db2ca420a0090593ac6559ff2a98ce30abfe665d7a18ff3c63883e8b98622a73" - .to_string() - }) - .to_metadata_url(), - }) - )), - "Expected an event for token metadata for TOKEN_0" - ); - - claim!( - logger.logs.contains(&to_bytes( - &Cis2Event::TokenMetadata::<_, ContractTokenAmount>(TokenMetadataEvent { - token_id: TOKEN_1, - metadata_url: (TokenMetadata { - url: "url".to_string(), - hash: - "db2ca420a0090593ac6559ff2a98ce30abfe665d7a18ff3c63883e8b98622a73" - .to_string() - }) - .to_metadata_url(), - }) - )), - "Expected an event for token metadata for TOKEN_1" - ); - } - } - - /// Test transfer succeeds, when `from` is the sender. - #[concordium_test] - fn test_transfer_account() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let transfer = Transfer { - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = host - .state() - .balance(&TOKEN_0, &ADDRESS_0) - .expect_report("Token is expected to exist"); - let balance1 = host - .state() - .balance(&TOKEN_0, &ADDRESS_1) - .expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 300.into(), - "Token owner balance should be decreased by the transferred amount." - ); - claim_eq!( - balance1, - 100.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - })), - "Incorrect event emitted" - ) - } - - /// Test transfer token fails, when sender is neither the owner or an - /// operator of the owner. - #[concordium_test] - fn test_transfer_not_authorized() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - let err = result.expect_err_report("Expected to fail"); - claim_eq!( - err, - ContractError::Unauthorized, - "Error is expected to be Unauthorized" - ) - } - - /// Test transfer succeeds when sender is not the owner, but is an operator - /// of the owner. - #[concordium_test] - fn test_operator_transfer() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let mut state = initial_state(&mut state_builder); - state.add_operator(&ADDRESS_0, &ADDRESS_1, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = host - .state() - .balance(&TOKEN_0, &ADDRESS_0) - .expect_report("Token is expected to exist"); - let balance1 = host - .state() - .balance(&TOKEN_0, &ADDRESS_1) - .expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 300.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 100.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - })), - "Incorrect event emitted" - ) - } - - /// Test adding an operator succeeds and the appropriate event is logged. - #[concordium_test] - fn test_add_operator() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let update = UpdateOperator { - operator: ADDRESS_1, - update: OperatorUpdate::Add, - }; - let parameter = UpdateOperatorParams(vec![update]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_update_operator(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let is_operator = host.state().is_operator(&ADDRESS_1, &ADDRESS_0); - claim!(is_operator, "Account should be an operator"); - - // Checking that `ADDRESS_1` is an operator in the query response of the - // `contract_operator_of` function as well. - // Setup parameter. - let operator_of_query = OperatorOfQuery { - address: ADDRESS_1, - owner: ADDRESS_0, - }; - - let operator_of_query_vector = OperatorOfQueryParams { - queries: vec![operator_of_query], - }; - let parameter_bytes = to_bytes(&operator_of_query_vector); - - ctx.set_parameter(¶meter_bytes); - - // Checking the return value of the `contract_operator_of` function - let result: ContractResult = contract_operator_of(&ctx, &host); - - claim_eq!( - result.expect_report("Failed getting result value").0, - [true], - "Account should be an operator in the query response" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes( - &Cis2Event::::UpdateOperator( - UpdateOperatorEvent { - owner: ADDRESS_0, - operator: ADDRESS_1, - update: OperatorUpdate::Add, - } - ) - ), - "Incorrect event emitted" - ) - } -} diff --git a/cis2-fractionalizer/src/error.rs b/cis2-fractionalizer/src/error.rs deleted file mode 100644 index 97157bf..0000000 --- a/cis2-fractionalizer/src/error.rs +++ /dev/null @@ -1,65 +0,0 @@ -use concordium_cis2::Cis2Error; -use concordium_std::*; - -use crate::cis2_client::Cis2ClientError; - -pub type ContractError = Cis2Error; - -/// The different errors the contract can produce. -#[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -pub enum CustomContractError { - /// Failed parsing the parameter. - #[from(ParseError)] - ParseParams, - /// Failed logging: Log is full. - LogFull, - /// Failed logging: Log is malformed. - LogMalformed, - /// Invalid contract name. - InvalidContractName, - /// Only a smart contract can call this function. - ContractOnly, - /// Failed to invoke a contract. - InvokeContractError, - TokenAlreadyMinted, - InvalidCollateral, - NoBalanceToBurn, - AccountsOnly, - Cis2ClientError(Cis2ClientError), -} - -/// Mapping the logging errors to ContractError. -impl From for CustomContractError { - fn from(le: LogError) -> Self { - match le { - LogError::Full => Self::LogFull, - LogError::Malformed => Self::LogMalformed, - } - } -} - -/// Mapping errors related to contract invocations to CustomContractError. -impl From> for CustomContractError { - fn from(_cce: CallContractError) -> Self { - Self::InvokeContractError - } -} - -/// Mapping CustomContractError to ContractError -impl From for ContractError { - fn from(c: CustomContractError) -> Self { - Cis2Error::Custom(c) - } -} - -impl From for CustomContractError { - fn from(_: NewReceiveNameError) -> Self { - Self::InvalidContractName - } -} - -impl From for CustomContractError { - fn from(_: NewContractNameError) -> Self { - Self::InvalidContractName - } -} diff --git a/cis2-fractionalizer/src/lib.rs b/cis2-fractionalizer/src/lib.rs deleted file mode 100644 index 68776ab..0000000 --- a/cis2-fractionalizer/src/lib.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! A multi token example implementation of the Concordium Token Standard CIS2. -//! -//! # Description -//! An instance of this smart contract can contain a number of different token -//! types each identified by a token ID. A token type is then globally -//! identified by the contract address together with the token ID. -//! -//! In this example the contract is initialized with no tokens, and tokens can -//! be minted through a `mint` contract function, which will only succeed for -//! the contract owner. No functionality to burn token is defined in this -//! example. -//! -//! Note: The word 'address' refers to either an account address or a -//! contract address. -//! -//! As follows from the CIS2 specification, the contract has a `transfer` -//! function for transferring an amount of a specific token type from one -//! address to another address. An address can enable and disable one or more -//! addresses as operators. An operator of some address is allowed to transfer -//! any tokens owned by this address. -//! -//! This contract also contains an example of a function to be called when -//! receiving tokens. In which case the contract will forward the tokens to -//! the contract owner. -//! This function is not very useful and is only there to showcase a simple -//! implementation of a token receive hook. - -#![cfg_attr(not(feature = "std"), no_std)] -pub mod contract; -mod error; -mod params; -mod state; -mod cis2_client; - -use concordium_cis2::*; - -use crate::error::ContractError; - -/// List of supported standards by this contract address. -const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = - [CIS0_STANDARD_IDENTIFIER, CIS2_STANDARD_IDENTIFIER]; - -// Types - -/// Contract token ID type. -/// To save bytes we use a small token ID type, but is limited to be represented -/// by a `u8`. -type ContractTokenId = TokenIdU8; - -/// Contract token amount type. -type ContractTokenAmount = TokenAmountU64; -type ContractResult = Result; diff --git a/cis2-fractionalizer/src/params.rs b/cis2-fractionalizer/src/params.rs deleted file mode 100644 index 8ee1086..0000000 --- a/cis2-fractionalizer/src/params.rs +++ /dev/null @@ -1,92 +0,0 @@ -use concordium_cis2::*; -use concordium_std::*; -use core::convert::TryInto; - -use crate::{ - state::{CollateralKey, CollateralState}, - ContractTokenAmount, ContractTokenId, -}; - -#[derive(Serial, Deserial, SchemaType)] -pub struct TokenMintParams { - pub metadata: TokenMetadata, - pub amount: ContractTokenAmount, - pub contract: ContractAddress, - pub token_id: ContractTokenId, -} - -/// The parameter for the contract function `mint` which mints a number of -/// token types and/or amounts of tokens to a given address. -#[derive(Serial, Deserial, SchemaType)] -pub struct MintParams { - /// Owner of the newly minted tokens. - pub owner: Address, - /// A collection of tokens to mint. - pub tokens: collections::BTreeMap, -} - -/// The parameter type for the contract function `setImplementors`. -/// Takes a standard identifier and a list of contract addresses providing -/// implementations of this standard. -#[derive(Debug, Serialize, SchemaType)] -pub struct SetImplementorsParams { - /// The identifier for the standard. - pub id: StandardIdentifierOwned, - /// The addresses of the implementors of the standard. - pub implementors: Vec, -} - -#[derive(Debug, Serialize, Clone, SchemaType)] -pub struct TokenMetadata { - /// The URL following the specification RFC1738. - #[concordium(size_length = 2)] - pub url: String, - /// A optional hash of the content. - #[concordium(size_length = 2)] - pub hash: String, -} - -impl TokenMetadata { - fn get_hash_bytes(&self) -> Option<[u8; 32]> { - match hex::decode(self.hash.to_owned()) { - Ok(v) => { - let slice = v.as_slice(); - match slice.try_into() { - Ok(array) => Option::Some(array), - Err(_) => Option::None, - } - } - Err(_) => Option::None, - } - } - - pub(crate) fn to_metadata_url(&self) -> MetadataUrl { - MetadataUrl { - url: self.url.to_string(), - hash: self.get_hash_bytes(), - } - } -} - -#[derive(Serialize, SchemaType)] -pub struct ViewAddressState { - pub balances: Vec<(ContractTokenId, ContractTokenAmount)>, - pub operators: Vec
, -} - -#[derive(Serialize, SchemaType)] -pub struct ViewState { - pub state: Vec<(Address, ViewAddressState)>, - pub tokens: Vec, - pub collaterals: Vec<(CollateralKey, CollateralState)>, -} - -/// 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; - -pub type TransferParameter = TransferParams; diff --git a/cis2-fractionalizer/src/state.rs b/cis2-fractionalizer/src/state.rs deleted file mode 100644 index 7f669f9..0000000 --- a/cis2-fractionalizer/src/state.rs +++ /dev/null @@ -1,317 +0,0 @@ -use concordium_cis2::*; -use concordium_std::{*}; - -use crate::{ - error::{ContractError, CustomContractError}, - params::TokenMetadata, - ContractResult, ContractTokenAmount, ContractTokenId, -}; - -/// The state for each address. -#[derive(Serial, DeserialWithState, Deletable, StateClone)] -#[concordium(state_parameter = "S")] -pub struct AddressState { - /// The amount of tokens owned by this address. - pub(crate) balances: StateMap, - /// The address which are currently enabled as operators for this address. - pub(crate) operators: StateSet, -} - -impl AddressState { - fn empty(state_builder: &mut StateBuilder) -> Self { - AddressState { - balances: state_builder.new_map(), - operators: state_builder.new_set(), - } - } -} - -#[derive(Serial, Deserial, Clone, SchemaType, Copy)] -pub struct CollateralKey { - pub contract: ContractAddress, - pub token_id: ContractTokenId, - pub owner: AccountAddress, -} - -#[derive(Serial, Deserial, Clone, Copy, SchemaType)] -pub struct CollateralState { - pub received_token_amount: ContractTokenAmount, - pub minted_token_id: Option, -} - -impl CollateralState { - fn new() -> Self { - CollateralState { - received_token_amount: ContractTokenAmount::from(0), - minted_token_id: Option::None, - } - } -} - -/// The contract state, -/// -/// Note: The specification does not specify how to structure the contract state -/// and this could be structured in a more space efficient way. -#[derive(Serial, DeserialWithState, StateClone)] -#[concordium(state_parameter = "S")] -pub struct State { - /// The state of addresses. - pub(crate) state: StateMap, S>, - /// All of the token IDs - pub(crate) tokens: StateMap, - /// Map with contract addresses providing implementations of additional - /// standards. - pub(crate) implementors: StateMap, S>, - pub(crate) collaterals: StateMap, -} - -impl State { - /// Construct a state with no tokens - pub(crate) fn empty(state_builder: &mut StateBuilder) -> Self { - State { - state: state_builder.new_map(), - tokens: state_builder.new_map(), - implementors: state_builder.new_map(), - collaterals: state_builder.new_map(), - } - } - - /// Mints an amount of tokens with a given address as the owner. - pub(crate) fn mint( - &mut self, - token_id: &ContractTokenId, - token_metadata: &TokenMetadata, - amount: ContractTokenAmount, - owner: &Address, - state_builder: &mut StateBuilder, - ) { - self.tokens - .insert(*token_id, token_metadata.to_metadata_url()); - let mut owner_state = self - .state - .entry(*owner) - .or_insert_with(|| AddressState::empty(state_builder)); - let mut owner_balance = owner_state.balances.entry(*token_id).or_insert(0.into()); - *owner_balance += amount; - } - - pub(crate) fn burn( - &mut self, - token_id: &ContractTokenId, - amount: ContractTokenAmount, - owner: &Address, - ) -> ContractResult { - match self.state.get_mut(owner) { - Some(address_state) => match address_state.balances.get_mut(token_id) { - Some(mut b) => { - ensure!( - b.cmp(&amount).is_ge(), - Cis2Error::Custom(CustomContractError::NoBalanceToBurn) - ); - - *b -= amount; - Ok(*b) - } - None => Err(Cis2Error::Custom(CustomContractError::NoBalanceToBurn)), - }, - None => Err(Cis2Error::Custom(CustomContractError::NoBalanceToBurn)), - } - } - - /// Check that the token ID currently exists in this contract. - #[inline(always)] - pub(crate) fn contains_token(&self, token_id: &ContractTokenId) -> bool { - self.tokens.get(token_id).is_some() - } - - /// Get the current balance of a given token id for a given address. - /// Results in an error if the token id does not exist in the state. - pub(crate) fn balance( - &self, - token_id: &ContractTokenId, - address: &Address, - ) -> ContractResult { - ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); - let balance = self.state.get(address).map_or(0.into(), |address_state| { - address_state - .balances - .get(token_id) - .map_or(0.into(), |x| *x) - }); - Ok(balance) - } - - /// Check if an address is an operator of a given owner address. - pub(crate) fn is_operator(&self, address: &Address, owner: &Address) -> bool { - self.state - .get(owner) - .map(|address_state| address_state.operators.contains(address)) - .unwrap_or(false) - } - - /// Update the state with a transfer. - /// Results in an error if the token id does not exist in the state or if - /// the from address have insufficient tokens to do the transfer. - pub(crate) fn transfer( - &mut self, - token_id: &ContractTokenId, - amount: ContractTokenAmount, - from: &Address, - to: &Address, - state_builder: &mut StateBuilder, - ) -> ContractResult<()> { - ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); - // A zero transfer does not modify the state. - if amount == 0.into() { - return Ok(()); - } - - // Get the `from` state and balance, if not present it will fail since the - // balance is interpreted as 0 and the transfer amount must be more than - // 0 as this point.; - { - let mut from_address_state = self - .state - .entry(*from) - .occupied_or(ContractError::InsufficientFunds)?; - let mut from_balance = from_address_state - .balances - .entry(*token_id) - .occupied_or(ContractError::InsufficientFunds)?; - ensure!(*from_balance >= amount, ContractError::InsufficientFunds); - *from_balance -= amount; - } - - let mut to_address_state = self - .state - .entry(*to) - .or_insert_with(|| AddressState::empty(state_builder)); - let mut to_address_balance = to_address_state - .balances - .entry(*token_id) - .or_insert(0.into()); - *to_address_balance += amount; - - Ok(()) - } - - /// Update the state adding a new operator for a given address. - /// Succeeds even if the `operator` is already an operator for the - /// `address`. - pub(crate) fn add_operator( - &mut self, - owner: &Address, - operator: &Address, - state_builder: &mut StateBuilder, - ) { - let mut owner_state = self - .state - .entry(*owner) - .or_insert_with(|| AddressState::empty(state_builder)); - owner_state.operators.insert(*operator); - } - - /// Update the state removing an operator for a given address. - /// Succeeds even if the `operator` is not an operator for the `address`. - pub(crate) fn remove_operator(&mut self, owner: &Address, operator: &Address) { - self.state.entry(*owner).and_modify(|address_state| { - address_state.operators.remove(operator); - }); - } - - /// Check if state contains any implementors for a given standard. - pub(crate) fn have_implementors(&self, std_id: &StandardIdentifierOwned) -> SupportResult { - if let Some(addresses) = self.implementors.get(std_id) { - SupportResult::SupportBy(addresses.to_vec()) - } else { - SupportResult::NoSupport - } - } - - pub(crate) fn add_collateral( - &mut self, - contract: ContractAddress, - token_id: ContractTokenId, - owner: AccountAddress, - received_token_amount: ContractTokenAmount, - ) { - let key = CollateralKey { - contract, - token_id, - owner, - }; - - let mut cs = match self.collaterals.get(&key) { - Some(v) => *v, - None => CollateralState::new(), - }; - - cs.received_token_amount += received_token_amount; - - self.collaterals.insert(key, cs); - } - - pub(crate) fn has_collateral( - &self, - contract: &ContractAddress, - token_id: &ContractTokenId, - owner: &AccountAddress, - ) -> bool { - let key = CollateralKey { - contract: *contract, - token_id: *token_id, - owner: *owner, - }; - - self.collaterals.get(&key).is_some() - } - - pub(crate) fn find_collateral( - &self, - token_id: &ContractTokenId, - ) -> Option<(CollateralKey, ContractTokenAmount)> { - for c in self.collaterals.iter() { - match c.1.minted_token_id { - Some(t) => { - if t.eq(token_id) { - return Some((c.0.clone(), c.1.received_token_amount)); - } - } - None => continue, - }; - } - - None - } - - pub(crate) fn update_collateral_token( - &mut self, - contract: ContractAddress, - token_id: ContractTokenId, - owner: AccountAddress, - minted_token_id: ContractTokenId, - ) -> ContractResult<()> { - let key = CollateralKey { - contract, - token_id, - owner, - }; - - match self.collaterals.entry(key) { - Entry::Vacant(_) => bail!(Cis2Error::Custom(CustomContractError::InvalidCollateral)), - Entry::Occupied(mut e) => { - e.modify(|s| s.minted_token_id = Some(minted_token_id)); - Ok(()) - } - } - } - - /// Set implementors for a given standard. - pub(crate) fn set_implementors( - &mut self, - std_id: StandardIdentifierOwned, - implementors: Vec, - ) { - self.implementors.insert(std_id, implementors); - } -} diff --git a/cis2-market/.gitignore b/cis2-market/.gitignore index 110d010..6f3d794 100644 --- a/cis2-market/.gitignore +++ b/cis2-market/.gitignore @@ -1,3 +1,4 @@ -target Cargo.lock -.vscode \ No newline at end of file +target +module.wasm +schema.bin \ No newline at end of file diff --git a/cis2-multi/.gitignore b/cis2-multi/.gitignore index f2f9e58..6f3d794 100644 --- a/cis2-multi/.gitignore +++ b/cis2-multi/.gitignore @@ -1,2 +1,4 @@ +Cargo.lock target -Cargo.lock \ No newline at end of file +module.wasm +schema.bin \ No newline at end of file diff --git a/cis2-nft/.gitignore b/cis2-nft/.gitignore deleted file mode 100644 index cb117fd..0000000 --- a/cis2-nft/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -Cargo.lock -target \ No newline at end of file diff --git a/cis2-nft/Cargo.toml b/cis2-nft/Cargo.toml deleted file mode 100644 index 93bcdff..0000000 --- a/cis2-nft/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "cis2-nft" -version = "0.1.0" -authors = ["Concordium "] -edition = "2018" -license = "MPL-2.0" - -[features] -default = ["std"] -std = ["concordium-std/std", "concordium-cis2/std"] - -[dependencies] -concordium-std = { version = "4.0.0", default-features = false } -concordium-cis2 = { version = "1.1.0", default-features = false } -hex = "*" - -[lib] -crate-type = ["cdylib", "rlib"] - -[profile.release] -codegen-units = 1 -opt-level = "s" diff --git a/cis2-nft/README.md b/cis2-nft/README.md deleted file mode 100644 index c49bbe9..0000000 --- a/cis2-nft/README.md +++ /dev/null @@ -1,10 +0,0 @@ -## Build - -- ### [Install tools for development](https://developer.concordium.software/en/mainnet/smart-contracts/guides/setup-tools.html#setup-tools) -- ### [Build the Smart Contract Module](https://developer.concordium.software/en/mainnet/smart-contracts/guides/compile-module.html) - - Make sure your working directory is [cis2-nft](./) ie `cd cis2-nft`. - - Execute the following commands - ```bash - cis2-nft$ cargo concordium build --out ./module.wasm --schema-out ./schema.bin - ``` - - You should have [module file](./module.wasm) & [schema file](./schema.bin) if everything has executed normally diff --git a/cis2-nft/src/lib.rs b/cis2-nft/src/lib.rs deleted file mode 100644 index 7b49af4..0000000 --- a/cis2-nft/src/lib.rs +++ /dev/null @@ -1,754 +0,0 @@ -//! A NFT smart contract example using the Concordium Token Standard CIS2. -//! -//! # Description -//! An instance of this smart contract can contain a number of different token -//! each identified by a token ID. A token is then globally identified by the -//! contract address together with the token ID. -//! -//! In this example the contract is initialized with no tokens, and tokens can -//! be minted through a `mint` contract function, which will only succeed for -//! the contract owner. No functionality to burn token is defined in this -//! example. -//! -//! Note: The word 'address' refers to either an account address or a -//! contract address. -//! -//! As follows from the CIS2 specification, the contract has a `transfer` -//! function for transferring an amount of a specific token type from one -//! address to another address. An address can enable and disable one or more -//! addresses as operators. An operator of some address is allowed to transfer -//! any tokens owned by this address. - -#![cfg_attr(not(feature = "std"), no_std)] - -use core::convert::TryInto; - -use concordium_cis2::*; -use concordium_std::*; - -/// The baseurl for the token metadata, gets appended with the token ID as hex -/// encoding before emitted in the TokenMetadata event. -// const TOKEN_METADATA_BASE_URL: &str = "https://some.example/token/"; - -/// List of supported standards by this contract address. -const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = - [CIS0_STANDARD_IDENTIFIER, CIS2_STANDARD_IDENTIFIER]; - -// Types - -/// Contract token ID type. -/// To save bytes we use a token ID type limited to a `u32`. -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. -type ContractTokenAmount = TokenAmountU8; - -#[derive(Debug, Serialize, Clone, SchemaType)] -pub struct TokenMetadata { - /// The URL following the specification RFC1738. - #[concordium(size_length = 2)] - pub url: String, - /// A optional hash of the content. - #[concordium(size_length = 2)] - pub hash: String, -} - -/// The parameter for the contract function `mint` which mints a number of -/// tokens to a given address. -#[derive(Serial, Deserial, SchemaType)] -struct MintParams { - /// Owner of the newly minted tokens. - owner: Address, - /// A collection of tokens to mint. - #[concordium(size_length = 1)] - tokens: collections::BTreeMap, -} - -/// The state for each address. -#[derive(Serial, DeserialWithState, Deletable)] -#[concordium(state_parameter = "S")] -struct AddressState { - /// The tokens owned by this address. - owned_tokens: StateSet, - /// The address which are currently enabled as operators for this address. - operators: StateSet, -} - -impl AddressState { - fn empty(state_builder: &mut StateBuilder) -> Self { - AddressState { - owned_tokens: state_builder.new_set(), - operators: state_builder.new_set(), - } - } -} - -/// The contract state. -// Note: The specification does not specify how to structure the contract state -// and this could be structured in a more space efficient way depending on the use case. -#[derive(Serial, DeserialWithState)] -#[concordium(state_parameter = "S")] -struct State { - /// The state for each address. - state: StateMap, S>, - /// All of the token IDs - all_tokens: StateSet, - /// Map with contract addresses providing implementations of additional - /// standards. - implementors: StateMap, S>, - metadata: StateMap, -} - -/// The parameter type for the contract function `setImplementors`. -/// Takes a standard identifier and list of contract addresses providing -/// implementations of this standard. -#[derive(Debug, Serialize, SchemaType)] -struct SetImplementorsParams { - /// The identifier for the standard. - id: StandardIdentifierOwned, - /// The addresses of the implementors of the standard. - implementors: Vec, -} - -/// The custom errors the contract can produce. -#[derive(Serialize, Debug, PartialEq, Eq, Reject)] -enum CustomContractError { - /// Failed parsing the parameter. - #[from(ParseError)] - ParseParams, - /// Failed logging: Log is full. - LogFull, - /// Failed logging: Log is malformed. - LogMalformed, - /// Failing to mint new tokens because one of the token IDs already exists - /// in this contract. - TokenIdAlreadyExists, - /// Failed to invoke a contract. - InvokeContractError, -} - -/// Wrapping the custom errors in a type with CIS1 errors. -type ContractError = Cis2Error; - -type ContractResult = Result; - -/// Mapping the logging errors to CustomContractError. -impl From for CustomContractError { - fn from(le: LogError) -> Self { - match le { - LogError::Full => Self::LogFull, - LogError::Malformed => Self::LogMalformed, - } - } -} - -/// Mapping errors related to contract invocations to CustomContractError. -impl From> for CustomContractError { - fn from(_cce: CallContractError) -> Self { - Self::InvokeContractError - } -} - -/// Mapping CustomContractError to ContractError -impl From for ContractError { - fn from(c: CustomContractError) -> Self { - Cis2Error::Custom(c) - } -} - -// Functions for creating, updating and querying the contract state. -impl State { - /// Creates a new state with no tokens. - fn empty(state_builder: &mut StateBuilder) -> Self { - State { - state: state_builder.new_map(), - all_tokens: state_builder.new_set(), - implementors: state_builder.new_map(), - metadata: state_builder.new_map(), - } - } - - /// Mint a new token with a given address as the owner - fn mint( - &mut self, - token: ContractTokenId, - metadata: &TokenMetadata, - owner: &Address, - state_builder: &mut StateBuilder, - ) -> ContractResult<()> { - ensure!( - self.all_tokens.insert(token), - CustomContractError::TokenIdAlreadyExists.into() - ); - self.metadata.insert(token, metadata.clone()); - - let mut owner_state = self - .state - .entry(*owner) - .or_insert_with(|| AddressState::empty(state_builder)); - owner_state.owned_tokens.insert(token); - Ok(()) - } - - /// Check that the token ID currently exists in this contract. - #[inline(always)] - fn contains_token(&self, token_id: &ContractTokenId) -> bool { - self.all_tokens.contains(token_id) - } - - /// Get the current balance of a given token ID for a given address. - /// Results in an error if the token ID does not exist in the state. - /// Since this contract only contains NFTs, the balance will always be - /// either 1 or 0. - fn balance( - &self, - token_id: &ContractTokenId, - address: &Address, - ) -> ContractResult { - ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); - let balance = self - .state - .get(address) - .map(|address_state| { - if address_state.owned_tokens.contains(token_id) { - 1 - } else { - 0 - } - }) - .unwrap_or(0); - Ok(balance.into()) - } - - /// Check if a given address is an operator of a given owner address. - fn is_operator(&self, address: &Address, owner: &Address) -> bool { - self.state - .get(owner) - .map(|address_state| address_state.operators.contains(address)) - .unwrap_or(false) - } - - /// Update the state with a transfer of some token. - /// Results in an error if the token ID does not exist in the state or if - /// the from address have insufficient tokens to do the transfer. - fn transfer( - &mut self, - token_id: &ContractTokenId, - amount: ContractTokenAmount, - from: &Address, - to: &Address, - state_builder: &mut StateBuilder, - ) -> ContractResult<()> { - ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); - // A zero transfer does not modify the state. - if amount == 0.into() { - return Ok(()); - } - // Since this contract only contains NFTs, no one will have an amount greater - // than 1. And since the amount cannot be the zero at this point, the - // address must have insufficient funds for any amount other than 1. - ensure_eq!(amount, 1.into(), ContractError::InsufficientFunds); - - { - let mut from_address_state = self - .state - .get_mut(from) - .ok_or(ContractError::InsufficientFunds)?; - // Find and remove the token from the owner, if nothing is removed, we know the - // address did not own the token.. - let from_had_the_token = from_address_state.owned_tokens.remove(token_id); - ensure!(from_had_the_token, ContractError::InsufficientFunds); - } - - // Add the token to the new owner. - let mut to_address_state = self - .state - .entry(*to) - .or_insert_with(|| AddressState::empty(state_builder)); - to_address_state.owned_tokens.insert(*token_id); - Ok(()) - } - - /// Update the state adding a new operator for a given address. - /// Succeeds even if the `operator` is already an operator for the - /// `address`. - fn add_operator( - &mut self, - owner: &Address, - operator: &Address, - state_builder: &mut StateBuilder, - ) { - let mut owner_state = self - .state - .entry(*owner) - .or_insert_with(|| AddressState::empty(state_builder)); - owner_state.operators.insert(*operator); - } - - /// Update the state removing an operator for a given address. - /// Succeeds even if the `operator` is _not_ an operator for the `address`. - fn remove_operator(&mut self, owner: &Address, operator: &Address) { - self.state.entry(*owner).and_modify(|address_state| { - address_state.operators.remove(operator); - }); - } - - /// Check if state contains any implementors for a given standard. - fn have_implementors(&self, std_id: &StandardIdentifierOwned) -> SupportResult { - if let Some(addresses) = self.implementors.get(std_id) { - SupportResult::SupportBy(addresses.to_vec()) - } else { - SupportResult::NoSupport - } - } - - /// Set implementors for a given standard. - fn set_implementors( - &mut self, - std_id: StandardIdentifierOwned, - implementors: Vec, - ) { - self.implementors.insert(std_id, implementors); - } -} - -// Contract functions - -/// Initialize contract instance with no token types initially. -#[init(contract = "CIS2-NFT")] -fn contract_init( - _ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { - // Construct the initial contract state. - Ok(State::empty(state_builder)) -} - -#[derive(Serialize, SchemaType)] -struct ViewAddressState { - owned_tokens: Vec, - operators: Vec
, -} - -#[derive(Serialize, SchemaType)] -struct ViewState { - state: Vec<(Address, ViewAddressState)>, - all_tokens: Vec, - metadata: Vec<(ContractTokenId, TokenMetadata)>, -} - -/// View function that returns the entire contents of the state. Meant for -/// testing. -#[receive(contract = "CIS2-NFT", name = "view", return_value = "ViewState")] -fn contract_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { - let state = host.state(); - - let mut inner_state = Vec::new(); - for (k, a_state) in state.state.iter() { - let owned_tokens = a_state.owned_tokens.iter().map(|x| *x).collect(); - let operators = a_state.operators.iter().map(|x| *x).collect(); - inner_state.push(( - *k, - ViewAddressState { - owned_tokens, - operators, - }, - )); - } - let all_tokens = state.all_tokens.iter().map(|x| *x).collect(); - let all_tokens_metadata = state - .metadata - .iter() - .map(|(k, v)| (*k, v.clone())) - .collect(); - - Ok(ViewState { - state: inner_state, - all_tokens, - metadata: all_tokens_metadata, - }) -} - -/// Mint new tokens with a given address as the owner of these tokens. -/// Can only be called by the contract owner. -/// Logs a `Mint` and a `TokenMetadata` event for each token. -/// -/// It rejects if: -/// - The sender is not the contract instance owner. -/// - Fails to parse parameter. -/// - Any of the tokens fails to be minted, which could be if: -/// - The minted token ID already exists. -/// - Fails to log Mint event -/// - Fails to log TokenMetadata event -/// -/// Note: Can at most mint 32 token types in one call due to the limit on the -/// number of logs a smart contract can produce on each function call. -#[receive( - contract = "CIS2-NFT", - name = "mint", - parameter = "MintParams", - enable_logger, - mutable -)] -fn contract_mint( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, - logger: &mut impl HasLogger, -) -> ContractResult<()> { - // Get the contract owner - let owner = ctx.owner(); - // Get the sender of the transaction - let sender = ctx.sender(); - - ensure!(sender.matches_account(&owner), ContractError::Unauthorized); - - // Parse the parameter. - let params: MintParams = ctx.parameter_cursor().get()?; - - let (state, builder) = host.state_and_builder(); - - for (&token_id, metadata) in params.tokens.iter() { - // Mint the token in the state. - state.mint(token_id, metadata, ¶ms.owner, builder)?; - - // Event for minted NFT. - logger.log(&Cis2Event::Mint(MintEvent { - token_id, - amount: ContractTokenAmount::from(1), - owner: params.owner, - }))?; - - let hash: Option<[u8; 32]> = match hex::decode(metadata.hash.to_owned()) { - Ok(v) => { - let slice = v.as_slice(); - match slice.try_into() { - Ok(array) => Option::Some(array), - Err(_) => Option::None, - } - } - Err(_) => Option::None, - }; - - // Metadata URL for the NFT. - logger.log(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id, - metadata_url: MetadataUrl { - url: metadata.url.clone(), - hash, - }, - }, - ))?; - } - Ok(()) -} - -type TransferParameter = TransferParams; - -/// Execute a list of token transfers, in the order of the list. -/// -/// Logs a `Transfer` event and invokes a receive hook function for every -/// transfer in the list. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Any of the transfers fail to be executed, which could be if: -/// - The `token_id` does not exist. -/// - The sender is not the owner of the token, or an operator for this -/// specific `token_id` and `from` address. -/// - The token is not owned by the `from`. -/// - Fails to log event. -/// - Any of the receive hook function calls rejects. -#[receive( - contract = "CIS2-NFT", - name = "transfer", - parameter = "TransferParameter", - enable_logger, - mutable -)] -fn contract_transfer( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, - logger: &mut impl HasLogger, -) -> ContractResult<()> { - // Parse the parameter. - let TransferParams(transfers): TransferParameter = ctx.parameter_cursor().get()?; - // Get the sender who invoked this contract function. - let sender = ctx.sender(); - - for Transfer { - token_id, - amount, - from, - to, - data, - } in transfers - { - let (state, builder) = host.state_and_builder(); - // Authenticate the sender for this transfer - ensure!( - from == sender || state.is_operator(&sender, &from), - ContractError::Unauthorized - ); - let to_address = to.address(); - // Update the contract state - state.transfer(&token_id, amount, &from, &to_address, builder)?; - - // Log transfer event - logger.log(&Cis2Event::Transfer(TransferEvent { - token_id, - amount, - from, - to: to_address, - }))?; - - // If the receiver is a contract: invoke the receive hook function. - if let Receiver::Contract(address, function) = to { - let parameter = OnReceivingCis2Params { - token_id, - amount, - from, - data, - }; - host.invoke_contract( - &address, - ¶meter, - function.as_entrypoint_name(), - Amount::zero(), - )?; - } - } - Ok(()) -} - -/// Enable or disable addresses as operators of the sender address. -/// Logs an `UpdateOperator` event. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Fails to log event. -#[receive( - contract = "CIS2-NFT", - name = "updateOperator", - parameter = "UpdateOperatorParams", - enable_logger, - mutable -)] -fn contract_update_operator( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, - logger: &mut impl HasLogger, -) -> ContractResult<()> { - // Parse the parameter. - let UpdateOperatorParams(params) = ctx.parameter_cursor().get()?; - // Get the sender who invoked this contract function. - let sender = ctx.sender(); - let (state, builder) = host.state_and_builder(); - for param in params { - // Update the operator in the state. - match param.update { - OperatorUpdate::Add => state.add_operator(&sender, ¶m.operator, builder), - OperatorUpdate::Remove => state.remove_operator(&sender, ¶m.operator), - } - - // Log the appropriate event - logger.log( - &Cis2Event::::UpdateOperator( - UpdateOperatorEvent { - owner: sender, - operator: param.operator, - update: param.update, - }, - ), - )?; - } - - Ok(()) -} - -/// Takes a list of queries. Each query is an owner address and some address to -/// check as an operator of the owner address. -/// -/// It rejects if: -/// - It fails to parse the parameter. -#[receive( - contract = "CIS2-NFT", - name = "operatorOf", - parameter = "OperatorOfQueryParams", - return_value = "OperatorOfQueryResponse" -)] -fn contract_operator_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ContractResult { - // Parse the parameter. - let params: OperatorOfQueryParams = ctx.parameter_cursor().get()?; - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for query in params.queries { - // Query the state for address being an operator of owner. - let is_operator = host.state().is_operator(&query.address, &query.owner); - response.push(is_operator); - } - let result = OperatorOfQueryResponse::from(response); - Ok(result) -} - -/// Parameter type for the CIS-2 function `balanceOf` specialized to the subset -/// of TokenIDs used by this contract. -type ContractBalanceOfQueryParams = BalanceOfQueryParams; -/// Response type for the CIS-2 function `balanceOf` specialized to the subset -/// of TokenAmounts used by this contract. -type ContractBalanceOfQueryResponse = BalanceOfQueryResponse; - -/// Get the balance of given token IDs and addresses. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Any of the queried `token_id` does not exist. -#[receive( - contract = "CIS2-NFT", - name = "balanceOf", - parameter = "ContractBalanceOfQueryParams", - return_value = "ContractBalanceOfQueryResponse" -)] -fn contract_balance_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ContractResult { - // Parse the parameter. - let params: ContractBalanceOfQueryParams = ctx.parameter_cursor().get()?; - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for query in params.queries { - // Query the state for balance. - let amount = host.state().balance(&query.token_id, &query.address)?; - response.push(amount); - } - let result = ContractBalanceOfQueryResponse::from(response); - Ok(result) -} - -/// Parameter type for the CIS-2 function `tokenMetadata` specialized to the -/// subset of TokenIDs used by this contract. -type ContractTokenMetadataQueryParams = TokenMetadataQueryParams; - -/// Get the token metadata URLs and checksums given a list of token IDs. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Any of the queried `token_id` does not exist. -#[receive( - contract = "CIS2-NFT", - name = "tokenMetadata", - parameter = "ContractTokenMetadataQueryParams", - return_value = "TokenMetadataQueryResponse" -)] -fn contract_token_metadata( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ContractResult { - // Parse the parameter. - let params: ContractTokenMetadataQueryParams = ctx.parameter_cursor().get()?; - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for token_id in params.queries { - // Check the token exists. - ensure!( - host.state().contains_token(&token_id), - ContractError::InvalidTokenId - ); - - let metadata_url: MetadataUrl = host - .state() - .metadata - .get(&token_id) - .map(|metadata| { - let hash: Option<[u8; 32]> = match hex::decode(metadata.hash.to_owned()) { - Ok(v) => { - let slice = v.as_slice(); - match slice.try_into() { - Ok(array) => Option::Some(array), - Err(_) => Option::None, - } - } - Err(_) => Option::None, - }; - - MetadataUrl { - hash, - url: metadata.url.to_owned(), - } - }) - .ok_or(ContractError::InvalidTokenId)?; - - response.push(metadata_url); - } - - Ok(TokenMetadataQueryResponse::from(response)) -} - -/// Get the supported standards or addresses for a implementation given list of -/// standard identifiers. -/// -/// It rejects if: -/// - It fails to parse the parameter. -#[receive( - contract = "CIS2-NFT", - name = "supports", - parameter = "SupportsQueryParams", - return_value = "SupportsQueryResponse" -)] -fn contract_supports( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ContractResult { - // Parse the parameter. - let params: SupportsQueryParams = ctx.parameter_cursor().get()?; - - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for std_id in params.queries { - if SUPPORTS_STANDARDS.contains(&std_id.as_standard_identifier()) { - response.push(SupportResult::Support); - } else { - response.push(host.state().have_implementors(&std_id)); - } - } - let result = SupportsQueryResponse::from(response); - Ok(result) -} - -/// Set the addresses for an implementation given a standard identifier and a -/// list of contract addresses. -/// -/// It rejects if: -/// - Sender is not the owner of the contract instance. -/// - It fails to parse the parameter. -#[receive( - contract = "CIS2-NFT", - name = "setImplementors", - parameter = "SetImplementorsParams", - mutable -)] -fn contract_set_implementor( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { - // Authorize the sender. - ensure!( - ctx.sender().matches_account(&ctx.owner()), - ContractError::Unauthorized - ); - // Parse the parameter. - let params: SetImplementorsParams = ctx.parameter_cursor().get()?; - // Update the implementors in the state - host.state_mut() - .set_implementors(params.id, params.implementors); - Ok(()) -} diff --git a/concordium-client/README.md b/concordium-client/README.md deleted file mode 100644 index b0a89f4..0000000 --- a/concordium-client/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Interaction with Concordium Smart Contracts - -## Setup - -- Start Docker Testnet Node - ``` - docker-compose up node - ``` - -- Initialize Concordium Client - ```bash - concordium-client config init - ``` -- Import Wallet - ```bash - concordium-client config account import concordium-backup.concordiumwallet - ``` -- Setup Env Variables (Optional) - ```bash - export ACCOUNT= ## This your account name in the exported wallet file - export GRPC_IP=127.0.0.1 ## this is the default value if you use docker compose node - export GRPC_PORT=10001 ## this is the default value if you use docker compose node - ``` - -## Deploy Contracts - -```bash -concordium-client module deploy --sender $ACCOUNT --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT -``` -here the `WASM-FILE-PATH` can be -- [cis2-nft wasm](../cis2-nft/module.wasm) -- [cis2-multi wasm](../cis2-multi/module.wasm) -- [marketplace wasm](../cis2-market/module.wasm) -- [Fractionalizer wasm](../cis2-fractionalizer/module.wasm) -- [Auctions Wasm](../cis2-auctions/module.wasm) - -You can [read more](https://developer.concordium.software/en/mainnet/net/references/concordium-client.html#concordium-client) about `concordium-client` and its cli params like `--grpc-ip` & `--grpc-port` - -**The last line contains module reference. We will be using this in subsequent steps.** - -Name the module - -```bash -concordium-client module name --name -``` -Here lets for the context of this repository name our contracts in the following format. The same format will be used for interaction with contracts -- for CIS2-NFT : `cis2nft` -- for CIS2-Multi : `cis2multi` -- for Marketplace Contract : `market` -- for Auctions Contract : `auction` - -## Interact with Contract - -- [CIS2 NFT](./cis2-nft.README.md) -- [CIS2 Multi](./cis2-multi.README.md) -- [Marketplace](./cis2-market.README.md) -- [CIS2 Fractionalizer](./cis2-fractionalizer.README.md) -- [CIS2 Auctions](./cis2-auctions.README.md.README.md) diff --git a/concordium-client/cis2-auctions.README.md b/concordium-client/cis2-auctions.README.md deleted file mode 100644 index 21b96e3..0000000 --- a/concordium-client/cis2-auctions.README.md +++ /dev/null @@ -1,103 +0,0 @@ -## Process of Auctions - -Auctions contract allows the Instantiator to create an Auction for any possible CIS2 Tokens. The participation of the auction is controlled by a Participation Tokens which needs to be specified while Instantiating the Auction Contract and represented by the field `participation_token` in the [init.json file](../sample-artifacts/cis2-auctions/init.json) - -## CLI Interactions - -- Deploy - - ```bash - export ACCOUNT=new - export GRPC_IP=127.0.0.1 - export GRPC_PORT=10001 - - concordium-client module deploy ./module.wasm --sender $ACCOUNT --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT - ``` - -- Initialize Smart Contract - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract init ed8cee6895f1ded9559e8027da76080788f00364be5f7a7831852d77fa62e8b9 --contract auction --sender $ACCOUNT --energy 3000 --schema ../cis2-auctions/schema.bin --parameter-json ../sample-artifacts/cis2-auctions/init.json - ``` - -- CIS2 Requirements - To read more about the CIS2 tokens and its interactions please go through the steps in the [file](../concordium-client/cis2-multi.README.md) - - ```bash - ## This is the Contract Subindex of the CIS2 Multi Contract. - export CIS2_CONTRACT=2371 - ``` - - - Mint Participation Token - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update $CIS2_CONTRACT --entrypoint mint --parameter-json ../sample-artifacts/cis2-auctions/mint-participation.json --schema ../cis2-multi/schema.bin --sender $ACCOUNT --energy 6000 - ``` - - - Add Participant (Transfer Participation Token) - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update $CIS2_CONTRACT --entrypoint transfer --parameter-json ../sample-artifacts/cis2-auctions/transfer-participation.json --schema ../cis2-multi/schema.bin --sender $ACCOUNT --energy 6000 - ``` - - - Mint Auction Token - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update $CIS2_CONTRACT --entrypoint mint --parameter-json ../sample-artifacts/cis2-auctions/mint-auction.json --schema ../cis2-multi/schema.bin --sender $ACCOUNT --energy 6000 - ``` - - - Start Auction (Transfer Auction Token) - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update $CIS2_CONTRACT --entrypoint transfer --parameter-json ../sample-artifacts/cis2-auctions/transfer-auction.json --schema ../cis2-multi/schema.bin --sender $ACCOUNT --energy 6000 - ``` - -- Bid - - ```bash - ## Contract Index of the `auction` contract. - export CONTRACT=2372 - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update $CONTRACT --entrypoint bid --sender $ACCOUNT --energy 6000 - ``` - -- View State - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract invoke $CONTRACT --entrypoint view --schema ../cis2-auctions/schema.bin --energy 6000 - ``` - - **Sample output** - - ```json - { - "auction_state": { - "NotSoldYet": [ - { - "amount": "1", - "contract": { - "index": 2371, - "subindex": 0 - }, - "token_id": "02" - } - ] - }, - "end": "2023-01-10T00:00:00Z", - "highest_bidder": { - "Some": ["48x2Uo8xCMMxwGuSQnwbqjzKtVqK5MaUud4vG7QEUgDmYkV85e"] - }, - "minimum_raise": 10, - "participants": ["48x2Uo8xCMMxwGuSQnwbqjzKtVqK5MaUud4vG7QEUgDmYkV85e"], - "participation_token": { - "contract": { - "index": 2371, - "subindex": 0 - }, - "token_id": "01" - } - } - ``` - -- Finalize Auction - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update $CONTRACT --entrypoint finalize --sender $ACCOUNT --energy 6000 - ``` diff --git a/concordium-client/cis2-fractionalizer.README.md b/concordium-client/cis2-fractionalizer.README.md deleted file mode 100644 index ae91bec..0000000 --- a/concordium-client/cis2-fractionalizer.README.md +++ /dev/null @@ -1,34 +0,0 @@ -- Perquisites - - - A CIS2 Compatible Contract Instance with atleast 1 minted token (Please see [cis2-multi instructions](./cis2-multi.README.md) to do the same) - -- Initialize Smart Contract - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract init --contract CIS2-Fractionalizer --sender $ACCOUNT --energy 3000 - ``` - -- Transfer CIS2 Token to the Fractionalizer Contract - - ``` - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update 2164 --entrypoint transfer --parameter-json ../sample-artifacts/cis2-fractionalizer/cis2-multi-transfer.json --schema ../cis2-multi/schema.bin --sender $ACCOUNT --energy 6000 - ``` - -- Mint - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update 2163 --entrypoint mint --parameter-json ../sample-artifacts/cis2-fractionalizer/mint.json --schema ../cis2-fractionalizer/schema.bin --sender $ACCOUNT --energy 6000 - ``` - -- Burn - - We transfer some part of the amount of the minted tokens back to the contract address to burn the tokens - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update 2163 --entrypoint transfer --parameter-json ../sample-artifacts/cis2-fractionalizer/burn-20.json --schema ../cis2-fractionalizer/schema.bin --sender $ACCOUNT --energy 6000 - ``` - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update 2163 --entrypoint transfer --parameter-json ../sample-artifacts/cis2-fractionalizer/burn-80.json --schema ../cis2-fractionalizer/schema.bin --sender $ACCOUNT --energy 6000 - ``` - -- [View CIS2-Multi](./cis2-multi.README.md) diff --git a/concordium-client/cis2-market.README.md b/concordium-client/cis2-market.README.md deleted file mode 100644 index 0827e55..0000000 --- a/concordium-client/cis2-market.README.md +++ /dev/null @@ -1,35 +0,0 @@ -- Initialize Smart Contract - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract init market --contract Market-NFT --parameter-json ../sample-artifacts/marketplace/init.json --sender $ACCOUNT --energy 3000 --schema ../cis2-market/schema.bin - ``` - - - Name the contract instance. So thats it's easier to work with - ``` - export MARKETPLACE_CONTRACT=2098 - ``` - -- Update Token Operator - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update 2097 --entrypoint updateOperator --parameter-json ../sample-artifacts/marketplace/update-operator.json --schema ../cis2-multi/schema.bin --sender $ACCOUNT --energy 6000 - ``` - - Please note that this command is being issued to the CIS2 TOken contract. So that Marketplace contract gets the authority to transfer tokens on behalf of the seller. - -- Add Token - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update 2099 --entrypoint add --parameter-json ../sample-artifacts/marketplace/add.json --schema ../cis2-market/schema.bin --sender $ACCOUNT --energy 10000 - ``` - -- List Token - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract invoke 2099 --entrypoint list --schema ../cis2-market/schema.bin - ``` - -- Transfer Token - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update 2099 --entrypoint transfer --parameter-json ../sample-artifacts/marketplace/transfer.json --schema ../cis2-market/schema.bin --sender $ACCOUNT --energy 6000 --amount 1 - ``` diff --git a/concordium-client/cis2-multi.README.md b/concordium-client/cis2-multi.README.md deleted file mode 100644 index 4314973..0000000 --- a/concordium-client/cis2-multi.README.md +++ /dev/null @@ -1,23 +0,0 @@ -- Initialize Smart Contract - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract init cis2multi --contract CIS2-Multi --sender $ACCOUNT --energy 3000 - ``` - -- Mint - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update 2097 --entrypoint mint --parameter-json ../sample-artifacts/cis2-multi/mint.json --schema ../cis2-multi/schema.bin --sender $ACCOUNT --energy 6000 - ``` - -- View - - ``` - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract invoke 2097 --entrypoint view --schema ../cis2-multi/schema.bin - ``` - -- Transfer - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update 2097 --entrypoint transfer --parameter-json ../sample-artifacts/cis2-multi/transfer.json --schema ../cis2-multi/schema.bin --sender $ACCOUNT --energy 6000 - ``` diff --git a/concordium-client/cis2-nft.README.md b/concordium-client/cis2-nft.README.md deleted file mode 100644 index cc72add..0000000 --- a/concordium-client/cis2-nft.README.md +++ /dev/null @@ -1,23 +0,0 @@ -- Initialize Smart Contract - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract init cis2nft --contract CIS2-NFT --sender $ACCOUNT --energy 3000 - ``` -- Mint - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update 2095 --entrypoint mint --parameter-json ../sample-artifacts/cis2-nft/mint.json --schema ../cis2-nft/schema.bin --sender $ACCOUNT --energy 6000 - ``` - - **2095 here is the index of the contract that was initialized.** - -- View Contract State - - ``` - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract invoke 2095 --entrypoint view --schema ../cis2-nft/schema.bin - ``` - -- Transfer - - ```bash - concordium-client --grpc-ip $GRPC_IP --grpc-port $GRPC_PORT contract update 2095 --entrypoint transfer --parameter-json ../sample-artifacts/cis2-nft/transfer.json --schema ../cis2-nft/schema.bin --sender $ACCOUNT --energy 6000 - ``` diff --git a/market-ui/README.md b/market-ui/README.md index ce62b5e..f2cd658 100644 --- a/market-ui/README.md +++ b/market-ui/README.md @@ -28,7 +28,7 @@ ## Deploy -Currently the browser wallet does not allow to deploy modules to Concordium chain. However node-cli and concordium client can be used to deploy contracts +Currently the browser wallet does not allow to deploy modules to Concordium chain. However [node-cli](https://github.com/chainorders/concordium-contracts-node-cli) and [concordium client](https://github.com/chainorders/concordium-contracts/tree/main/concordium-client) can be used to deploy contracts ## Interact diff --git a/market-ui/package.json b/market-ui/package.json index 3ddc4d7..816d376 100644 --- a/market-ui/package.json +++ b/market-ui/package.json @@ -1,5 +1,6 @@ { "name": "market-ui", + "homepage": "https://chainorders.github.io/concordium-nft-tutorials", "version": "0.1.0", "private": true, "dependencies": { @@ -19,6 +20,7 @@ "@types/react-dom": "^18.0.6", "axios": "^1.1.3", "crypto-js": "^4.1.1", + "gh-pages": "^4.0.0", "leb128": "^0.0.5", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -32,7 +34,9 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "predeploy": "yarn build", + "deploy": "gh-pages -d build" }, "eslintConfig": { "extends": [ @@ -51,5 +55,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "dotenv": "^16.0.3" } } diff --git a/market-ui/src/App.tsx b/market-ui/src/App.tsx index e0221f5..b8a3ec5 100644 --- a/market-ui/src/App.tsx +++ b/market-ui/src/App.tsx @@ -5,7 +5,7 @@ import { detectConcordiumProvider, WalletApi, } from "@concordium/browser-wallet-api-helpers"; -import { Box, Link, Typography } from "@mui/material"; +import { AlertColor, Box, Link, Typography } from "@mui/material"; import { Route, Routes, @@ -28,6 +28,7 @@ import { import ConnectWallet from "./components/ConnectWallet"; import Header from "./components/ui/Header"; import { MINTING_UI_ONLY } from "./Constants"; +import Alert from "./components/ui/Alert"; function App() { const params = useParams(); @@ -53,6 +54,15 @@ function App() { marketplaceContractAddress, }); + const [alertState, setAlertState] = useState<{ + open: boolean; + message: string; + severity?: AlertColor; + }>({ + open: false, + message: "", + }); + function connect() { detectConcordiumProvider() .then((provider) => { @@ -65,7 +75,11 @@ function App() { setState({ ...state, provider, account }); }) .catch((_) => { - alert("Please allow wallet connection"); + setAlertState({ + open: true, + message: "Please allow wallet connection", + severity: "error" + }); }); provider.on("accountDisconnected", () => { setState({ ...state, account: undefined }); @@ -206,6 +220,13 @@ function App() { ) : ( )} + setAlertState({ open: false, message: "" })} + severity={alertState.severity} + anchorOrigin={{ vertical: "top", horizontal: "center" }} + />