From b7c577a403a99a0ff15fe6953f9894346a5b495d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=9C=D0=B5=D0=B4=D0=BE?= =?UTF-8?q?=D0=B2?= Date: Mon, 2 Oct 2023 15:55:41 +0300 Subject: [PATCH] Updated contracts and adding links to code sections --- docs/examples/DAO.md | 181 ++++++++--------- docs/examples/concert.md | 92 +++++---- docs/examples/crowdsale.md | 125 ++++++------ docs/examples/dein.md | 2 +- docs/examples/dutch-auction.md | 36 ++-- docs/examples/dynamic-nft.md | 202 +++++++++--------- docs/examples/escrow.md | 33 ++- docs/examples/game-of-chance.md | 54 ++--- docs/examples/gnft-4907.md | 14 +- docs/examples/monopoly.md | 6 +- docs/examples/multisig-wallet.md | 108 +++++----- docs/examples/nft-marketplace/marketplace.md | 75 +++---- docs/examples/nft-pixelboard.md | 23 ++- docs/examples/onchain-nft.md | 203 ++++++++++++++----- docs/examples/ping.md | 123 +++++------ docs/examples/prerequisites.mdx | 2 +- docs/examples/rock-paper-scissors.md | 104 ++++++++-- docs/examples/staking.md | 132 ++++++------ docs/examples/supply-chain.md | 126 ++++++++---- docs/examples/tequila-train.md | 53 +++-- 20 files changed, 953 insertions(+), 741 deletions(-) diff --git a/docs/examples/DAO.md b/docs/examples/DAO.md index de6eb27fb..e5a2fedf9 100644 --- a/docs/examples/DAO.md +++ b/docs/examples/DAO.md @@ -25,18 +25,9 @@ This article explains the programming interface, data structure, basic functions --> ## Logic -To use the hashmap you should add the `hashbrown` package into your Cargo.toml file: -```toml -[dependencies] -# ... -hashbrown = "0.13.1" -``` - The contract has the following structs: -```rust -use hashbrown::HashMap; - +```rust title="dao-light/src/lib.rs" struct Dao { approved_token_program_id: ActorId, period_duration: u64, @@ -45,11 +36,8 @@ struct Dao { total_shares: u128, members: HashMap, proposal_id: u128, - proposals: HashMap, locked_funds: u128, - balance: u128, - transaction_id: u64, - transactions: BTreeMap, + proposals: HashMap, } ``` where: @@ -69,53 +57,62 @@ where: `proposal_id` - the index of the last proposal. -`proposals` - all proposals (the proposal queue). - `locked_funds` - tokens that are locked when a funding proposal is submitted. -`balance` - the amount of tokens in the DAO contract. - -`transaction_id` - the transaction number that is used for tracking transactions in the fungible token contract. - -`transactions` - the transaction history. - +`proposals` - all proposals (the proposal queue). -Parameters `approved_token_program_id`, `period_duration`, `grace_period_length` are set when initializing a contract. The contract is initialized with the following struct: +Parameters `approved_token_program_id`, `voting_period_length`, `period_duration`, `grace_period_length` are set when initializing a contract. The contract is initialized with the following struct: -```rust -struct InitDao { - approved_token_program_id: ActorId, - period_duration: u64, - voting_period_length: u64, - grace_period_length: u64, +```rust title="dao-light/io/src/lib.rs" +pub struct InitDao { + pub approved_token_program_id: ActorId, + pub voting_period_length: u64, + pub period_duration: u64, + pub grace_period_length: u64, } ``` The proposal struct: -```rust +```rust title="dao-light/io/src/lib.rs" pub struct Proposal { - pub proposer: ActorId, /// - the member who submitted the proposal - pub applicant: ActorId, /// - the applicant who wishes to become a member - pub yes_votes: u128, /// - the total number of YES votes for that proposal - pub no_votes: u128, /// - the total number of NO votes for that proposal - pub quorum: u128, /// - a certain threshold of YES votes in order for the proposal to pass - pub processed: bool, /// - true if the proposal has already been processed - pub did_pass: bool, /// - true if the proposal has passed - pub details: String, /// - proposal details - pub starting_period: u64, /// - the start of the voting period - pub ended_at: u64, /// - the end of the voting period - pub votes_by_member: BTreeMap, /// - the votes on that proposal by each member + pub proposer: ActorId, + pub applicant: ActorId, + pub yes_votes: u128, + pub no_votes: u128, + pub quorum: u128, + pub amount: u128, + pub processed: bool, + pub did_pass: bool, + pub details: String, + pub starting_period: u64, + pub ended_at: u64, + pub votes_by_member: Vec<(ActorId, Vote)>, } ``` +- `proposer` - the member who submitted the proposal +- `applicant` - the applicant who wishes to become a member +- `yes_votes` - the total number of YES votes for that proposal +- `no_votes` - the total number of NO votes for that proposal +- `quorum` - a certain threshold of YES votes in order for the proposal to pass +- `amount` - number of fungible tokens for donation +- `processed` - true if the proposal has already been processed +- `did_pass` - true if the proposal has passed +- `details` - proposal details +- `starting_period` - the start of the voting period +- `ended_at` - the end of the voting period +- `votes_by_member` - the votes on that proposal by each member + The member struct: -```rust +```rust title="dao-light/io/src/lib.rs" pub struct Member { - pub shares: u128, /// - the shares of that member - pub highest_index_yes_vote: u128, /// - the index of the highest proposal on which the members voted YES (that value is checked when user is going to leave the DAO) + pub shares: u128, + pub highest_index_yes_vote: Option, } ``` +- `shares` - the shares of that member +- `highest_index_yes_vote` - - the index of the highest proposal on which the members voted YES (that value is checked when user is going to leave the DAO) The actions that the contract receives outside are defined in enum `DaoActions`. The contract's replies are defined in the enum `DaoEvents`. @@ -123,7 +120,7 @@ The actions that the contract receives outside are defined in enum `DaoActions`. - Joining DAO. To join the DAO and become a DAO member, a user needs to send the following message to the DAO contract:" -```rust +```rust title="dao-light/io/src/lib.rs" /// Deposits tokens to DAO /// The account gets a share in DAO that is calculated as: (amount * self.total_shares / self.balance) /// @@ -135,7 +132,7 @@ Deposit { ``` - The funding proposal. The 'applicant' is an actor that will be funded: -```rust +```rust title="dao-light/io/src/lib.rs" /// The proposal of funding. /// /// Requirements: @@ -147,7 +144,7 @@ Deposit { /// On success replies with [`DaoEvent::SubmitFundingProposal`] SubmitFundingProposal { /// an actor that will be funded - receiver: ActorId, + applicant: ActorId, /// the number of fungible tokens that will be sent to the receiver amount: u128, /// a certain threshold of YES votes in order for the proposal to pass @@ -159,7 +156,7 @@ SubmitFundingProposal { - The member or the delegate address of the member submits their vote (YES or NO) on the proposal. -```rust +```rust title="dao-light/io/src/lib.rs" /// The member submits a vote (YES or NO) on the proposal. /// /// Requirements: @@ -178,7 +175,7 @@ SubmitVote { - Members have the option to withdraw their capital during a grace period. This feature is useful when members disagree with the outcome of a proposal, especially if the acceptance of that proposal could impact their shares. A member can initiate a 'ragequit' only if they have voted 'NO' on the proposal. -```rust +```rust title="dao-light/io/src/lib.rs" /// Withdraws the capital of the member /// /// Requirements: @@ -195,7 +192,7 @@ RageQuit { - The proposal processing occurs after the proposal completes its grace period. If the proposal is accepted, the tribute tokens are deposited into the contract, and new shares are minted and issued to the applicant. In the event of rejection, the tribute tokens are returned to the applicant. -```rust +```rust title="dao-light/io/src/lib.rs" /// The proposal processing after the proposal completes during the grace period. /// If the proposal is accepted, the indicated amount of tokens are sent to the receiver. /// @@ -206,31 +203,20 @@ RageQuit { /// /// On success replies with [`DaoEvent::ProcessProposal`] ProcessProposal { + /// the actor who was funded + applicant: ActorId, /// the proposal ID proposal_id: u128, + /// true if funding proposal has passed + did_pass: bool, }, ``` -- The option to resume the transaction is available. If a transaction hasn't been completed due to a network failure, the user can send a `Continue` message specifying the transaction ID that needs to be finalized: - -```rust - /// Continues the transaction if it fails due to lack of gas -/// or due to an error in the token contract. -/// -/// Requirements: -/// * Transaction must exist. -/// -/// On success replies with the DaoEvent of continued transaction. -Continue( - /// the transaction ID - u64, -), -``` + -To use the hashmap you should include `hashbrown` package into your *Cargo.toml* file: -```toml -[dependencies] -# ... -hashbrown = "0.13.1" -``` - ## Logic The contract state: -```rust -use hashbrown::{HashMap, HashSet}; - +```rust title="nft-marketplace/io/src/lib.rs" pub struct Market { pub admin_id: ActorId, pub treasury_id: ActorId, - pub treasury_fee: u128, - pub items: HashMap, - pub approved_nft_contracts: HashSet, - pub approved_ft_contracts: HashSet, + pub treasury_fee: u16, + pub items: BTreeMap<(ContractId, TokenId), Item>, + pub approved_nft_contracts: BTreeSet, + pub approved_ft_contracts: BTreeSet, pub tx_id: TransactionId, } ``` @@ -130,8 +121,9 @@ The marketplace contract is initialized with the following fields; The marketplace item has the following struct: -```rust +```rust title="nft-marketplace/io/src/lib.rs" pub struct Item { + pub token_id: TokenId, pub owner: ActorId, pub ft_contract_id: Option, pub price: Option, @@ -140,6 +132,7 @@ pub struct Item { pub tx: Option<(TransactionId, MarketTx)>, } ``` +- `token_id` is the ID of the NFT within its contract. - `owner` - an item owner; - `ft_contract_id` - a contract of fungible tokens for which that item can be bought. If that field is `None` then the item is on sale for native Gear value; - `price` - the item price. `None` field means that the item is not on the sale; @@ -149,7 +142,7 @@ pub struct Item { `MarketTx` is an enum of possible transactions that can occur with NFT: -```rust +```rust title="nft-marketplace/io/src/lib.rs" #[derive(Debug, Encode, Decode, TypeInfo, Clone, PartialEq, Eq)] pub enum MarketTx { CreateAuction, @@ -179,7 +172,7 @@ pub enum MarketTx { To list NFT on the marketplace or modify the terms of sale send the following message: -```rust +```rust title="nft-marketplace/io/src/lib.rs" /// Adds data on market item. /// If the item of that NFT does not exist on the marketplace then it will be listed. /// If the item exists then that action is used to change the price or suspend the sale. @@ -205,7 +198,7 @@ AddMarketData { To buy NFT send the following message: -```rust +```rust title="nft-marketplace/io/src/lib.rs" /// Sells the NFT. /// /// # Requirements: @@ -228,7 +221,7 @@ BuyItem { The marketplace contract includes the *English auction*. *English auction* is an open auction at an increasing price, where participants openly bid against each other, with each subsequent bid being greater than the previous one. The auction has the following struct: -```rust +```rust title="nft-marketplace/io/src/lib.rs" pub struct Auction { pub bid_period: u64, pub started_at: u64, @@ -245,7 +238,7 @@ pub struct Auction { The auction is started with the following message: -```rust +```rust title="nft-marketplace/io/src/lib.rs" /// Creates an auction for selected item. /// If the NFT item doesn't exist on the marketplace then it will be listed /// @@ -272,7 +265,7 @@ CreateAuction { ``` To add a bid to the current auction send the following message: -```rust +```rust title="nft-marketplace/io/src/lib.rs" /// Adds a bid to an ongoing auction. /// /// # Requirements: @@ -295,7 +288,7 @@ AddBid { If the auction period is over then anyone can send a message `SettleAuction` that will send the NFT to the winner and pay to the owner: -```rust +```rust title="nft-marketplace/io/src/lib.rs" /// Settles the auction. /// /// Requirements: @@ -315,7 +308,7 @@ SettleAuction { To make an offer on the marketplace item send the following message: -```rust +```rust title="nft-marketplace/io/src/lib.rs" /// Adds a price offer to the item. /// /// Requirements: @@ -335,13 +328,13 @@ AddOffer { /// the NFT id token_id: TokenId, /// the offer price - price: u128, + price: Price, }, ``` The item owner can accept the offer: -```rust +```rust title="nft-marketplace/io/src/lib.rs" /// Accepts an offer. /// /// Requirements: @@ -365,7 +358,7 @@ AcceptOffer { The user who made the offer can also withdraw his tokens: -```rust +```rust title="nft-marketplace/io/src/lib.rs" /// Withdraws tokens. /// /// Requirements: @@ -393,7 +386,7 @@ The `market` contract interacts with `fungible` and `non-fungible` token contrac Metadata interface description: -```rust +```rust title="nft-marketplace/io/src/lib.rs" pub struct MarketMetadata; impl Metadata for MarketMetadata { @@ -408,33 +401,27 @@ impl Metadata for MarketMetadata { To display the full contract state information, the `state()` function is used: -```rust +```rust title="nft-marketplace/src/lib.rs" #[no_mangle] -extern "C" fn state() { - msg::reply( - unsafe { - let market = MARKET.as_ref().expect("Uninitialized market state"); - &(*market).clone() - }, - 0, - ) - .expect("Failed to share state"); +extern fn state() { + let market = unsafe { MARKET.as_ref().expect("Uninitialized market state") }; + msg::reply(market, 0).expect("Failed to share state"); } ``` To display only necessary certain values from the state, you need to write a separate crate. In this crate, specify functions that will return the desired values from the `Market` state. For example - [gear-foundation/dapps-nft-marketplace/state](https://github.com/gear-foundation/dapps/tree/master/contracts/nft-marketplace/state): ```rust title="nft-marketplace/state/src/lib.rs" -#[metawasm] -pub trait Metawasm { - type State = Market; +#[gmeta::metawasm] +pub mod metafns { + pub type State = Market; - fn all_items(state: Self::State) -> Vec { - ... + pub fn all_items(state: State) -> Vec { + nft_marketplace_io::all_items(state) } - fn item_info(args: ItemInfoArgs, state: Self::State) -> Item { - ... + pub fn item_info(state: State, args: ItemInfoArgs) -> Option { + nft_marketplace_io::item_info(state, &args) } } ``` diff --git a/docs/examples/nft-pixelboard.md b/docs/examples/nft-pixelboard.md index 972d2e9fe..d52dd011f 100644 --- a/docs/examples/nft-pixelboard.md +++ b/docs/examples/nft-pixelboard.md @@ -24,7 +24,7 @@ The `painting()` metafunction is used to get the entire painting of a pixelboard ### Initialization -```rust +```rust title="nft-pixelboard/io/src/lib.rs" /// Initializes the NFT pixelboard program. /// /// # Requirements @@ -42,6 +42,8 @@ The `painting()` metafunction is used to get the entire painting of a pixelboard /// * `ft_program` address mustn't be [`ActorId::zero()`]. /// * `nft_program` address mustn't be [`ActorId::zero()`]. #[derive(Decode, Encode, TypeInfo, Clone)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub struct InitNFTPixelboard { /// An address of a pixelboard owner to which minting fees and commissions /// on resales will be transferred. @@ -72,9 +74,11 @@ pub struct InitNFTPixelboard { ### Actions -```rust -/// Sends a program info about what it should do. +```rust title="nft-pixelboard/io/src/lib.rs" +// Sends a program info about what it should do. #[derive(Decode, Encode, TypeInfo, Clone, PartialEq, Eq)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub enum NFTPixelboardAction { /// Mints one NFT on a pixelboard with given `token_metadata` & `painting`. /// @@ -87,7 +91,7 @@ pub enum NFTPixelboardAction { /// * `rectangle` coordinates must observe a block layout. In other words, /// each `rectangle` coordinate must be a multiple of a block side length in /// the canvas. The block side length can be obtained by - /// [`NFTPixelboardStateQuery::BlockSideLength`]. + /// [`block_side_length()`](../nft_pixelboard_state/metafns/fn.block_side_length.html). /// * NFT `rectangle` mustn't collide with already minted one. /// * `painting` length must equal a pixel count in an NFT /// (which can be calculated by multiplying a [width](`Rectangle::width`) & @@ -98,7 +102,7 @@ pub enum NFTPixelboardAction { /// pixel. The area can be calculated by multiplying a /// [width](`Rectangle::width`) & [height](`Rectangle::height`) from /// `rectangle`. The price of a free pixel can be obtained by - /// [`NFTPixelboardStateQuery::PixelPrice`]. + /// [`pixel_price()`](../nft_pixelboard_state/metafns/fn.pixel_price.html). /// /// On success, returns [`NFTPixelboardEvent::Minted`]. /// @@ -123,16 +127,17 @@ pub enum NFTPixelboardAction { /// # Requirements /// * An NFT must be minted on a pixelboard. /// * An NFT must be for sale. This can be found out by - /// [`NFTPixelboardStateQuery::TokenInfo`]. See also the documentation of + /// [`token_info()`]. See also the documentation of /// [`TokenInfo#structfield.pixel_price`]. /// * [`msg::source()`] must have enough fungible tokens to buy all pixels /// that an NFT occupies. This can be found out by - /// [`NFTPixelboardStateQuery::TokenInfo`]. See also the documentation of + /// [`token_info()`]. See also the documentation of /// [`TokenInfo#structfield.pixel_price`]. /// /// On success, returns [`NFTPixelboardEvent::Bought`]. /// /// [`msg::source()`]: gstd::msg::source + /// [`token_info()`]: ../nft_pixelboard_state/metafns/fn.token_info.html Buy(TokenId), /// Changes a sale state of an NFT minted on a pixelboard. @@ -151,7 +156,7 @@ pub enum NFTPixelboardAction { /// **Note:** A commission is included in each NFT resale, so a seller /// will receive not all fungible tokens but tokens with a commission /// deduction. A commission percentage can be obtained by - /// [`NFTPixelboardStateQuery::CommissionPercentage`]. + /// [`commission_percentage()`](../nft_pixelboard_state/metafns/fn.commission_percentage.html). /// /// # Requirements /// * An NFT must be minted on a pixelboard. @@ -175,7 +180,7 @@ pub enum NFTPixelboardAction { /// * `painting` length must equal a pixel count in an NFT. The count can be /// calculated by multiplying a [width](`Rectangle::width`) & /// [height](`Rectangle::height`) from a rectangle of the NFT. The NFT - /// rectangle can be obtained by [`NFTPixelboardStateQuery::TokenInfo`]. + /// rectangle can be obtained by [`token_info()`](../nft_pixelboard_state/metafns/fn.token_info.html). /// /// On success, returns [`NFTPixelboardEvent::Painted`]. Paint { diff --git a/docs/examples/onchain-nft.md b/docs/examples/onchain-nft.md index c97e9238d..0289c4620 100644 --- a/docs/examples/onchain-nft.md +++ b/docs/examples/onchain-nft.md @@ -31,14 +31,11 @@ To use the default implementation you should include the packages into your *Car ```toml gear-lib = { git = "https://github.com/gear-foundation/dapps", tag = "0.3.3" } -hashbrown = "0.13.1" ``` First, we start by modifying the state and the init message: -```rust -use hashbrown::{HashMap, HashSet}; - +```rust title="on-chain-nft/src/lib.rs" #[derive(Debug, Default, NFTStateKeeper, NFTCore, NFTMetaState)] pub struct OnChainNFT { #[NFTStateField] @@ -52,55 +49,145 @@ pub struct OnChainNFT { } ``` -```rust +```rust title="on-chain-nft/io/src/lib.rs" +/// Initializes on-chain NFT +/// Requirements: +/// * all fields except `royalties` should be specified #[derive(Debug, Encode, Decode, TypeInfo)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub struct InitOnChainNFT { + /// NFT name pub name: String, + /// NFT symbol pub symbol: String, + /// NFT base_uri (not applicable in on-chain) pub base_uri: String, + /// Base Image is base64encoded svg. + /// Provides a base layer for all future nfts. pub base_image: String, - // Layers MUST be specified in sequence order starting from 0, - // otherwise the contract will return the "No such layer" error. + /// Layers map - mapping of layerid the list of layer items. + /// Each layer item is a base64encoded svg. pub layers: Vec<(LayerId, Vec)>, + /// Royalties for NFT pub royalties: Option, } ``` Next let's rewrite several functions: `mint`, `burn` and `token_uri`. Our `mint` and `burn` functions will behave as one woud expect them to with the addition of slight state modification (e.g. checking against the state, adding/removing). `token_uri` will return an NFT's metadata as well as all the layer content provided for a specified NFT: -```rust +```rust title="on-chain-nft/io/src/lib.rs" #[derive(Debug, Encode, Decode, TypeInfo)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub enum OnChainNFTQuery { + /// Returns an NFT for a specified `token_id`. + /// + /// Requirements: + /// * `token_id` MUST exist + /// + /// Arguments: + /// * `token_id` - is the id of the NFT + /// + /// On success, returns TokenURI struct. TokenURI { token_id: TokenId }, + /// Base NFT query. Derived from gear-lib. Base(NFTQuery), } #[derive(Debug, Encode, Decode, TypeInfo)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub enum OnChainNFTAction { + /// Mints an NFT consisting of layers provided in the `description` parameter. + /// + /// Requirements: + /// * `description` MUST contain layers and layers' items that EXIST + /// + /// Arguments: + /// * `token_metadata` - is a default token metadata from gear-lib. + /// * `description` - is the vector of layer's item id, where + /// the index i is the layer id. + /// + /// On success, returns NFTEvent::Mint from gear-lib. Mint { + /// Metadata token_metadata: TokenMetadata, + /// Layers description of an NFT description: Vec, }, + /// Burns an NFT. + /// + /// Requirements: + /// * `token_id` MUST exist + /// Arguments: + /// + /// * `token_id` - is the id of the burnt token + /// + /// On success, returns NFTEvent::Burn from gear-lib. Burn { + /// Token id to burn. token_id: TokenId, }, + /// Transfers an NFT. + /// + /// Requirements: + /// * `token_id` MUST exist + /// * `to` MUST be a non-zero addresss + /// + /// Arguments: + /// * `token_id` - is the id of the transferred token + /// + /// On success, returns NFTEvent::Transfer from gear-lib. Transfer { + /// A recipient address. to: ActorId, + /// Token id to transfer. token_id: TokenId, }, + /// Approves an account to perform operation upon the specifiefd NFT. + /// + /// Requirements: + /// * `token_id` MUST exist + /// * `to` MUST be a non-zero addresss + /// + /// Arguments: + /// * `token_id` - is the id of the transferred token + /// + /// On success, returns NFTEvent::Approval from gear-lib. Approve { + /// An account being approved. to: ActorId, + /// Token id approved for the account. token_id: TokenId, }, + /// Transfers payouts from an NFT to an account. + /// + /// Requirements: + /// * `token_id` MUST exist + /// * `to` MUST be a non-zero addresss + /// * `amount` MUST be a non-zero number + /// + /// Arguments: + /// * `token_id` - is the id of the transferred token + /// + /// On success, returns NFTEvent::Approval from gear-lib. TransferPayout { + /// Payout recipient to: ActorId, + /// Token id to get the payout from. token_id: TokenId, + /// Payout amount. amount: u128, }, } #[derive(Debug, Encode, Decode, TypeInfo)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub struct TokenURI { + /// Token metadata derived from gear-lib pub metadata: TokenMetadata, + /// List of base64encoded svgs representing different layers of an NFT. pub content: Vec, } ``` @@ -108,7 +195,7 @@ pub struct TokenURI { The `TokenMetadata` is also defined in the gear NFT library: -```rust +```rust title="gear-lib-old/src/non_fungible_token/token.rs" #[derive(Debug, Default, Encode, Decode, Clone, TypeInfo)] pub struct TokenMetadata { // ex. "CryptoKitty #100" @@ -122,28 +209,33 @@ pub struct TokenMetadata { } ``` Define a trait for our new functions that will extend the default `NFTCore` trait: -```rust +```rust title="on-chain-nft/src/lib.rs" pub trait OnChainNFTCore: NFTCore { - fn mint(&mut self, description: Vec, metadata: TokenMetadata); - fn burn(&mut self, token_id: TokenId); + fn mint(&mut self, description: Vec, metadata: TokenMetadata) -> NFTTransfer; + fn burn(&mut self, token_id: TokenId) -> NFTTransfer; fn token_uri(&mut self, token_id: TokenId) -> Option>; } ``` and write the implementation of that trait: -```rust +```rust title="on-chain-nft/src/lib.rs" impl OnChainNFTCore for OnChainNFT { - /// Mint an NFT on chain. /// `description` - is the vector of ids , /// where each index represents a layer id, and element represents a layer item id. /// `metadata` - is the default metadata provided by gear-lib. - fn mint(&mut self, description: Vec, metadata: TokenMetadata) { + fn mint(&mut self, description: Vec, metadata: TokenMetadata) -> NFTTransfer { // precheck if the layers actually exist for (layer_id, layer_item_id) in description.iter().enumerate() { if layer_id > self.layers.len() { panic!("No such layer"); } - if *layer_item_id > self.layers.get(&(layer_id as u128)).expect("No such layer").len() as u128 { + if *layer_item_id + > self + .layers + .get(&(layer_id as u128)) + .expect("No such layer") + .len() as u128 + { panic!("No such item"); } } @@ -163,15 +255,16 @@ impl OnChainNFTCore for OnChainNFT { panic!("Such nft already exists"); } self.nfts_existence.insert(key); - NFTCore::mint(self, &msg::source(), self.token_id, Some(metadata)); + let transfer = NFTCore::mint(self, &msg::source(), self.token_id, Some(metadata)); self.nfts.insert(self.token_id, description); self.token_id = self.token_id.saturating_add(U256::one()); + transfer } /// Burns an NFT. /// `token_id` - is the id of a token. MUST exist. - fn burn(&mut self, token_id: TokenId) { - NFTCore::burn(self, token_id); + fn burn(&mut self, token_id: TokenId) -> NFTTransfer { + let transfer = NFTCore::burn(self, token_id); let key = self .nfts .get(&token_id) @@ -181,6 +274,7 @@ impl OnChainNFTCore for OnChainNFT { .collect::(); self.nfts.remove(&token_id); self.nfts_existence.remove(&key); + transfer } /// Returns token information - metadata and all the content of all the layers for the NFT. @@ -209,50 +303,49 @@ impl OnChainNFTCore for OnChainNFT { } ``` Accordingly, it is necessary to make changes to the `handle` and `meta_state` functions: -```rust +```rust title="on-chain-nft/src/lib.rs" #[no_mangle] -extern "C" fn handle() { +extern fn handle() { let action: OnChainNFTAction = msg::load().expect("Could not load OnChainNFTAction"); let nft = unsafe { CONTRACT.get_or_insert(Default::default()) }; match action { OnChainNFTAction::Mint { description, token_metadata, - } => OnChainNFTCore::mint(nft, description, token_metadata), - OnChainNFTAction::Burn { token_id } => OnChainNFTCore::burn(nft, token_id), - OnChainNFTAction::Transfer { to, token_id } => NFTCore::transfer(nft, &to, token_id), + } => msg::reply( + OnChainNFTEvent::Transfer(OnChainNFTCore::mint(nft, description, token_metadata)), + 0, + ), + OnChainNFTAction::Burn { token_id } => msg::reply( + OnChainNFTEvent::Transfer(OnChainNFTCore::burn(nft, token_id)), + 0, + ), + OnChainNFTAction::Transfer { to, token_id } => msg::reply( + OnChainNFTEvent::Transfer(NFTCore::transfer(nft, &to, token_id)), + 0, + ), OnChainNFTAction::TransferPayout { to, token_id, amount, - } => NFTCore::transfer_payout(nft, &to, token_id, amount), - OnChainNFTAction::Approve { to, token_id } => NFTCore::approve(nft, &to, token_id), + } => msg::reply( + OnChainNFTEvent::TransferPayout(NFTCore::transfer_payout(nft, &to, token_id, amount)), + 0, + ), + OnChainNFTAction::Approve { to, token_id } => msg::reply( + OnChainNFTEvent::Approval(NFTCore::approve(nft, &to, token_id)), + 0, + ), } + .expect("Error during replying with `OnChainNFTEvent`"); } -#[no_mangle] -extern "C" fn meta_state() -> *mut [i32; 2] { - let query: OnChainNFTQuery = msg::load().expect("failed to decode input argument"); - let nft = unsafe { CONTRACT.get_or_insert(Default::default()) }; - match query { - OnChainNFTQuery::TokenURI { token_id } => { - let encoded = OnChainNFTCore::token_uri(nft, token_id) - .expect("Error in reading OnChainNFT contract state"); - gstd::util::to_leak_ptr(encoded) - } - OnChainNFTQuery::Base(query) => { - let encoded = - NFTMetaState::proc_state(nft, query).expect("Error in reading NFT contract state"); - gstd::util::to_leak_ptr(encoded) - } - } -} ``` ### Programm metadata and state Metadata interface description: -```rust +```rust title="on-chain-nft/io/src/lib.rs" pub struct ContractMetadata; impl Metadata for ContractMetadata { @@ -266,22 +359,22 @@ impl Metadata for ContractMetadata { ``` To display the full contract state information, the `state()` function is used: -```rust +```rust title="on-chain-nft/src/lib.rs" #[no_mangle] -extern "C" fn state() { - reply(common_state()) - .expect("Failed to encode or reply with `::State` from `state()`"); +extern fn state() { + let contract = unsafe { CONTRACT.take().expect("Unexpected error in taking state") }; + msg::reply::(contract.into(), 0) + .expect("Failed to encode or reply with `State` from `state()`"); } ``` To display only necessary certain values from the state, you need to write a separate crate. In this crate, specify functions that will return the desired values from the `State` state. For example - [gear-foundation/dapps/on-chain-nft/state](https://github.com/gear-foundation/dapps/tree/master/contracts/on-chain-nft/state): -```rust - -#[metawasm] -pub trait Metawasm { - type State = on_chain_nft_io::State; +```rust title="on-chain-nft/state/src/lib.rs" +#[gmeta::metawasm] +pub mod metafns { + pub type State = on_chain_nft_io::State; - fn token_uri(token_id: TokenId, state: Self::State) -> Option> { + pub fn token_uri(state: State, token_id: TokenId) -> Option> { let metadata = state .token .token_metadata_by_id @@ -309,7 +402,7 @@ pub trait Metawasm { Some(TokenURI { metadata, content }.encode()) } - fn base(query: NFTQuery, state: Self::State) -> Option> { + pub fn base(state: State, query: NFTQuery) -> Option> { let encoded = match query { NFTQuery::NFTInfo => NFTQueryReply::NFTInfo { name: state.token.name.clone(), diff --git a/docs/examples/ping.md b/docs/examples/ping.md index b31d3c1a1..2f3661d78 100644 --- a/docs/examples/ping.md +++ b/docs/examples/ping.md @@ -11,109 +11,94 @@ Let's look at the [minimal program](https://github.com/gear-foundation/dapps/tre The code of the program is in the `src/lib.rs` file. The program replies with `Pong` string if the sender sent `Ping` message to it. It also saves how many times a user sent a ping message to the program. So, the program contains: -- state definition: -```rust -static mut STATE: Option> = None; +- message log definition: +```rust title="ping/src/lib.rs" +static mut MESSAGE_LOG: Vec = vec![]; ``` -- `init` and `handle` entrypoints: -```rust +- entry point `handle`: +```rust title="ping/src/lib.rs" #[no_mangle] -extern "C" fn init() { - unsafe { STATE = Some(HashMap::new()) } -} +extern fn handle() { + let new_msg: String = msg::load().expect("Unable to create string"); -#[no_mangle] -extern "C" fn handle() { - process_handle() - .expect("Failed to load, decode, encode, or reply with `PingPong` from `handle()`") -} - -fn process_handle() -> Result<(), ContractError> { - let payload = msg::load()?; + if new_msg == "PING" { + msg::reply_bytes("PONG", 0).expect("Unable to reply"); + } - if let PingPong::Ping = payload { - let pingers = static_mut_state(); + unsafe { + MESSAGE_LOG.push(new_msg); - pingers - .entry(msg::source()) - .and_modify(|ping_count| *ping_count = ping_count.saturating_add(1)) - .or_insert(1); + debug!("{:?} total message(s) stored: ", MESSAGE_LOG.len()); - reply(PingPong::Pong)?; + for log in &MESSAGE_LOG { + debug!("{log:?}"); + } } - - Ok(()) } ``` - `state` function that allows to read the program state: -```rust +```rust title="ping/src/lib.rs" #[no_mangle] -extern "C" fn state() { - reply(common_state()).expect( - "Failed to encode or reply with `::State` from `state()`", - ); +extern fn state() { + msg::reply(unsafe { MESSAGE_LOG.clone() }, 0) + .expect("Failed to encode or reply with `::State` from `state()`"); } ``` -The `lib.rs` file in the `src` directory contains the following code: -``` +The `io` crate defines the contract metadata. +```rust title="ping/io/src/lib.rs" #![no_std] -#[cfg(not(feature = "binary-vendor"))] -mod contract; +use gmeta::{InOut, Metadata, Out}; +use gstd::prelude::*; -#[cfg(feature = "binary-vendor")] -include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); -``` -The enabled `binary-vendor` feature will include the generated WASM binary as 3 constants: WASM_BINARY, WASM_BINARY_OPT and WASM_BINARY_META in the root crate. These constants can be used in tests with `gclient` instead of paths to wasm files in the target directory. You may not use that approach and simply write the contract code in the `lib.rs` file. - -The `io` crate defines the contract metadata, namely, the state of the program and what messages the program receives and sends. -```rust -#[derive(Encode, Decode, TypeInfo, Hash, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] -pub enum PingPong { - Ping, - Pong, -} +pub struct DemoPingMetadata; -pub struct ContractMetadata; - -impl Metadata for ContractMetadata { +impl Metadata for DemoPingMetadata { type Init = (); - type Handle = InOut; + type Handle = InOut; type Others = (); type Reply = (); type Signal = (); - type State = Out; + type State = Out>; } - -#[derive(Encode, Decode, TypeInfo, Hash, PartialEq, PartialOrd, Eq, Ord, Clone, Debug, Default)] -pub struct State(pub Vec<(ActorId, u128)>); ``` -The `ContractMetadata` struct is used in `build.rs` in order to generate `meta.txt` file: -```rust -use app_io::ContractMetadata; +The `DemoPingMetadata` struct is used in `build.rs` in order to generate `meta.txt` file: +```rust title="ping/build.rs" +use ping_io::DemoPingMetadata; fn main() { - gear_wasm_builder::build_with_metadata::(); + gear_wasm_builder::build_with_metadata::(); } ``` The `state` is the independent crate for reading the program state. It depends on the `ping-io` crate where the type of the contract state is defined: -```rust -use app_io::*; -use gmeta::{metawasm, Metadata}; -use gstd::{prelude::*, ActorId}; +```rust title="ping/state/src/lib.rs" +#![no_std] + +use gstd::prelude::*; -#[metawasm] -pub trait Metawasm { - type State = ping_io::State; +#[gmeta::metawasm] +pub mod metafns { + pub type State = Vec; + + pub fn get_first_message(state: State) -> String { + state.first().expect("Message log is empty!").to_string() + } + + pub fn get_last_message(state: State) -> String { + state.last().expect("Message log is empty!").to_string() + } - fn pingers(state: Self::State) -> Vec { - state.pingers() + pub fn get_messages_len(state: State) -> u64 { + state.len() as u64 } - fn ping_count(actor: ActorId, state: Self::State) -> u128 { - state.ping_count(actor) + pub fn get_message(state: State, index: u64) -> String { + state + .get(index as usize) + .expect("Invalid index!") + .to_string() } } ``` diff --git a/docs/examples/prerequisites.mdx b/docs/examples/prerequisites.mdx index c4780df9c..8dfb01848 100644 --- a/docs/examples/prerequisites.mdx +++ b/docs/examples/prerequisites.mdx @@ -151,7 +151,7 @@ fn main() { ``` `Cargo.toml` is a project manifest in Rust, it contains all metadata necessary for compiling the project. -Configure the `Cargo.toml` similarly to how it is configured [ping/Cargo.toml](https://github.com/gear-foundation/dapps-app/blob/master/Cargo.toml). You can refer to [Getting Started](/docs/getting-started-in-5-minutes.md) for additional details. +Configure the `Cargo.toml` similarly to how it is configured [dapp-template/Cargo.toml](https://github.com/gear-foundation/dapp-template/blob/master/Cargo.toml). You can refer to [Getting Started](/docs/getting-started-in-5-minutes.md) for additional details. ## Building Rust Contract diff --git a/docs/examples/rock-paper-scissors.md b/docs/examples/rock-paper-scissors.md index 6b62c91d4..4605995b4 100644 --- a/docs/examples/rock-paper-scissors.md +++ b/docs/examples/rock-paper-scissors.md @@ -20,7 +20,7 @@ Anyone can easily create their own decentralized game application and run it on First of all someone(admin) should deploy a "Rock Paper Scissors Lizard spock" program with a set of game parameters. -```rust +```rust title="rock-paper-scissors/io/src/lib.rs" pub struct GameConfig { pub bet_size: u128, pub players_count_limit: u8, @@ -86,12 +86,73 @@ When the game ends, a new game starts immediately with the new config that was s ### Actions -```rust +```rust title="rock-paper-scissors/io/src/lib.rs" pub enum Action { + /// Registers a player for the game. + /// Player must send value to be registered + /// + /// # Requirements: + /// * Game is not in progress yet. E.g. the `GameStage` must be `GameStage::Preparation` + /// * `msg::value()` is greater or equal to `bet_size` in the config(refund will return to user). + /// * Player not registred yet. + /// * Lobby is not full. + /// + /// On success replies `Event::PlayerRegistred`. Register, + + /// Submits player's move to the program in encrypted form. + /// Player can't change his move after it. + /// + /// # Arguments: + /// * `Vec`: is the binary 256-bit blake2b hash of move("0" or "1" or "2" or "3" or "4") + "password". + /// + /// # Requirements: + /// * The `GameStage` must be `GameStage::InProgress(StageDesciption)` where `StageDescription::anticipated_players` must contains `msg::source()` + /// + /// On success replies `Event::SuccessfulReveal(RevealResult)` where `RevealResult` will correspond to the situation after this reveal. MakeMove(Vec), + + /// Reveals the move of the player, with which players must confirm their moves. + /// In this step the program validates that the hash submitted during the moves stage is equal + /// to a hashed open string and save this move(first character from string) to determine the winners. + /// + /// # Arguments: + /// * `Vec`: is the binary move("0" or "1" or "2" or "3" or "4") + "password" that should be equal to binary that was sent in `MakeMove(Vec)` without hashing. + /// + /// # Requirements: + /// * The hashed(by program) `Reveal` binary must be equal to this round `MakeMove` binary. + /// * The `GameStage` must be `GameStage::Reveal(StageDesciption)` where `StageDescription::anticipated_players` must contains `msg::source()` + /// + /// On success replies `Event::SuccessfulMove(ActorId)` where `ActorId` is the moved player's address. Reveal(Vec), + + /// Changes the game config of the next game. + /// When the current game ends, this config will be applied. + /// + /// # Arguments: + /// * `GameConfig`: is the config that will be applied to the next game. + /// + /// # Requirements: + /// * The `msg::source()` must be the owner of the program. + /// * `players_count_limit` of the `GameConfig` must be greater than 1 + /// * `entry_timeout` of the `GameConfig` must be greater than 5000(5 sec) + /// * `move_timeout` of the `GameConfig` must be greater than 5000(5 sec) + /// * `reveal_timeout` of the `GameConfig` must be greater than 5000(5 sec) + /// + /// On success replies `Event::GameConfigChanged`. ChangeNextGameConfig(GameConfig), + + /// Stops the game. + /// This action can be used, for example, to change the configuration of the game, + /// or if the players have gone on strike and do not want to continue playing, + /// or if the game has gone on for a long time. + /// When the admin stops the game, all funds are distributed among the players remaining in the game. + /// If the game is in the registration stage, bets will be returned to the entire lobby. + /// + /// # Requirements: + /// * The `msg::source()` must be the owner of the program. + /// + /// On success replies `Event::GameWasStopped(BTreeSet)` where inside are the players who got the money. StopGame, } ``` @@ -104,15 +165,16 @@ pub enum Action { ### Events -```rust +```rust title="rock-paper-scissors/io/src/lib.rs" pub enum Event { - PlayerRegistred, + PlayerRegistered, SuccessfulMove(ActorId), SuccessfulReveal(RevealResult), GameConfigChanged, GameStopped(BTreeSet), } - +``` +```rust title="rock-paper-scissors/io/src/lib.rs" pub enum RevealResult { Continue, NextRoundStarted { players: BTreeSet }, @@ -129,12 +191,12 @@ pub enum RevealResult { ### Programm metadata and state Metadata interface description: -```rust +```rust title="rock-paper-scissors/io/src/lib.rs" pub struct ContractMetadata; impl Metadata for ContractMetadata { - type Init = (); - type Handle = (); + type Init = In; + type Handle = InOut; type Reply = (); type Others = (); type Signal = (); @@ -143,34 +205,34 @@ impl Metadata for ContractMetadata { ``` To display the full contract state information, the `state()` function is used: -```rust +```rust title="rock-paper-scissors/src/lib.rs" #[no_mangle] -extern "C" fn state() { - reply(common_state()) - .expect("Failed to encode or reply with `::State` from `state()`"); +extern fn state() { + let game = unsafe { RPS_GAME.take().expect("Unexpected error in taking state") }; + msg::reply::(game.into(), 0) + .expect("Failed to encode or reply with `ContractState` from `state()`"); } ``` To display only necessary certain values from the state, you need to write a separate crate. In this crate, specify functions that will return the desired values from the `ContractState` state. For example - [rock-paper-scissors/state](https://github.com/gear-foundation/dapps/tree/master/contracts/rock-paper-scissors/state): -```rust - -#[metawasm] -pub trait Metawasm { - type State = ContractState; +```rust title="rock-paper-scissors/state/src/lib.rs" +#[gmeta::metawasm] +pub mod metafns { + pub type State = ContractState; - fn config(state: Self::State) -> GameConfig { + pub fn config(state: State) -> GameConfig { state.game_config } - fn lobby_list(state: Self::State) -> Vec { + pub fn lobby_list(state: State) -> Vec { state.lobby } - fn game_stage(state: Self::State) -> GameStage { + pub fn game_stage(state: State) -> GameStage { state.stage } - fn current_stage_start_timestamp(state: Self::State) -> u64 { + pub fn current_stage_start_timestamp(state: State) -> u64 { state.current_stage_start_timestamp } } diff --git a/docs/examples/staking.md b/docs/examples/staking.md index 9b9efa98a..d63537c5e 100644 --- a/docs/examples/staking.md +++ b/docs/examples/staking.md @@ -83,13 +83,6 @@ The admin initializes the contract by transmitting information about the staking Admin can view the Stakers list (`GetStakers` message). The admin can update the reward that will be distributed (`UpdateStaking` message). The user first makes a bet (`Stake` message), and then he can receive his reward on demand (`GetReward` message). The user can withdraw part of the amount (`Withdraw` message). -## Interface -To use the hashmap you should include `hashbrown` package into your *Cargo.toml* file: -```toml -[dependecies] -# ... -hashbrown = "0.13.1" -``` ### Source files 1. `staking/src/lib.rs` - contains functions of the 'staking' contract. 2. `staking/io/src/lib.rs` - contains Enums and structs that the contract receives and sends in the reply. @@ -98,9 +91,7 @@ hashbrown = "0.13.1" The contract has the following structs: -```rust -use hashbrown::HashMap; - +```rust title="staking/src/lib.rs" struct Staking { owner: ActorId, staking_token_address: ActorId, @@ -113,6 +104,8 @@ struct Staking { all_produced: u128, reward_produced: u128, stakers: HashMap, + transactions: BTreeMap>, + current_tid: TransactionId, } ``` where: @@ -139,8 +132,12 @@ where: `stakers` - 'map' of the 'stakers' +`transactions` - 'map' of the 'transactions' -```rust +`current_tid` - current transaction identifier. + + +```rust title="staking/io/src/lib.rs" pub struct InitStaking { pub staking_token_address: ActorId, pub reward_token_address: ActorId, @@ -159,7 +156,7 @@ where: `reward_total` - the reward to be distributed within distribution time -```rust +```rust title="staking/io/src/lib.rs" pub struct Staker { pub balance: u128, pub reward_allowed: u128, @@ -180,108 +177,122 @@ where: ### Enums -```rust +```rust title="staking/io/src/lib.rs" pub enum StakingAction { Stake(u128), Withdraw(u128), UpdateStaking(InitStaking), GetReward, } - +``` +```rust title="staking/io/src/lib.rs" pub enum StakingEvent { StakeAccepted(u128), - Withdrawn(u128), Updated, Reward(u128), + Withdrawn(u128), } ``` ### Functions -Staking contract interacts with fungible token contract through function `transfer_tokens()`. +Staking contract interacts with fungible token contract through function `transfer_tokens()`. This function sends a message (the action is defined in the enum `FTAction`) and gets a reply (the reply is defined in the enum `FTEvent`). -```rust -pub async fn transfer_tokens( +```rust title="staking/src/lib.rs" +/// Transfers `amount` tokens from `sender` account to `recipient` account. +/// Arguments: +/// * `token_address`: token address +/// * `from`: sender account +/// * `to`: recipient account +/// * `amount_tokens`: amount of tokens +async fn transfer_tokens( &mut self, - token_address: &ActorId, /// - the token address - from: &ActorId, /// - the sender address - to: &ActorId, /// - the recipient address - amount_tokens: u128 /// - the amount of tokens -) -> Result<(), Error> -``` + token_address: &ActorId, + from: &ActorId, + to: &ActorId, + amount_tokens: u128, +) -> Result<(), Error> { + let payload = LogicAction::Transfer { + sender: *from, + recipient: *to, + amount: amount_tokens, + }; -This function sends a message (the action is defined in the enum `FTAction`) and gets a reply (the reply is defined in the enum `FTEvent`). + let transaction_id = self.current_tid; + self.current_tid = self.current_tid.saturating_add(99); -```rust -msg::send_for_reply( - *token_address, /// - the fungible token contract address - FTAction::Transfer { /// - action in the fungible token-contract - from: *from, - to: *to, - amount: amount_tokens, - }, - 0, - 0, -) + let payload = FTokenAction::Message { + transaction_id, + payload, + }; + + let result = msg::send_for_reply_as(*token_address, payload, 0, 0)?.await?; + + if let FTokenEvent::Err = result { + Err(Error::TransferTokens) + } else { + Ok(()) + } +} ``` Calculates the reward produced so far -```rust +```rust title="staking/src/lib.rs" fn produced(&mut self) -> u128 ``` Updates the reward produced so far and calculates tokens per stake -```rust +```rust title="staking/src/lib.rs" fn update_reward(&mut self) ``` Calculates the maximum possible reward. The reward that the depositor would have received if he had initially paid this amount -```rust +```rust title="staking/src/lib.rs" fn get_max_reward(&self, amount: u128) -> u128 ``` Calculates the reward of the staker that is currently available. The return value cannot be less than zero according to the algorithm -```rust -fn calc_reward(&mut self) -> u128 +```rust title="staking/src/lib.rs" +fn calc_reward(&mut self) -> Result ``` Updates the staking contract. Sets the reward to be distributed within distribution time -```rust -fn update_staking(&mut self, config: InitStaking) +```rust title="staking/src/lib.rs" +fn update_staking(&mut self, config: InitStaking) -> Result ``` Stakes the tokens -```rust -async fn stake(&mut self, amount: u128) +```rust title="staking/src/lib.rs" +async fn stake(&mut self, amount: u128) -> Result ``` Sends reward to the staker -```rust -async fn send_reward(&mut self) +```rust title="staking/src/lib.rs" +async fn send_reward(&mut self) -> Result ``` Withdraws the staked the tokens -```rust -async fn withdraw(&mut self, amount: u128) +```rust title="staking/src/lib.rs" +async fn withdraw(&mut self, amount: u128) -> Result ``` These functions are called in `async fn main()` through enum `StakingAction`. This is the entry point to the program, and the program is waiting for a message in `StakingAction` format. -```rust +```rust title="staking/src/lib.rs" #[gstd::async_main] async fn main() { let staking = unsafe { STAKING.get_or_insert(Staking::default()) }; @@ -296,7 +307,8 @@ async fn main() { }) = staking.transactions.get(&msg_source) { if action != *pend_action { - reply(_reply).expect("Failed to encode or reply with `Result`"); + msg::reply(_reply, 0) + .expect("Failed to encode or reply with `Result`"); return; } *id @@ -334,14 +346,14 @@ async fn main() { result } }; - reply(result).expect("Failed to encode or reply with `Result`"); + msg::reply(result, 0).expect("Failed to encode or reply with `Result`"); } ``` ### Programm metadata and state Metadata interface description: -```rust +```rust title="staking/io/src/lib.rs" pub struct StakingMetadata; impl Metadata for StakingMetadata { @@ -355,17 +367,18 @@ impl Metadata for StakingMetadata { ``` To display the full contract state information, the `state()` function is used: -```rust +```rust title="staking/src/lib.rs" #[no_mangle] -extern "C" fn state() { - reply(common_state()) - .expect("Failed to encode or reply with `::State` from `state()`"); +extern fn state() { + let staking = unsafe { STAKING.take().expect("Unexpected error in taking state") }; + msg::reply::(staking.into(), 0) + .expect("Failed to encode or reply with `IoStaking` from `state()`"); } ``` To display only necessary certain values from the state, you need to write a separate crate. In this crate, specify functions that will return the desired values from the `IoStaking` state. For example - [staking/state](https://github.com/gear-foundation/dapps/tree/master/contracts/staking/state): -```rust -#[metawasm] +```rust title="staking/state/src/lib.rs" +#[gmeta::metawasm] pub mod metafns { pub type State = IoStaking; @@ -381,7 +394,6 @@ pub mod metafns { .map(|(_, staker)| staker.clone()) } } - ``` ## Consistency of contract states diff --git a/docs/examples/supply-chain.md b/docs/examples/supply-chain.md index fe624863c..1c0662079 100644 --- a/docs/examples/supply-chain.md +++ b/docs/examples/supply-chain.md @@ -85,7 +85,7 @@ yarn start * Sale, purchase, delivery is made in [Gear fungible tokens (gFT)](gft-20.md). Item info has the following struct: -```rust +```rust title="supply-chain/io/src/lib.rs" pub struct ItemInfo { /// Item’s producer [`ActorId`]. pub producer: ActorId, @@ -108,13 +108,15 @@ pub struct ItemInfo { ``` And `ItemState` has the following struct and inner enums: -```rust +```rust title="supply-chain/io/src/lib.rs" pub struct ItemState { pub state: ItemEventState, pub by: Role, } - +``` +```rust title="supply-chain/io/src/lib.rs" pub enum ItemEventState { + #[default] Produced, Purchased, Received, @@ -124,11 +126,13 @@ pub enum ItemEventState { Approved, Shipped, } - +``` +```rust title="supply-chain/io/src/lib.rs" pub enum Role { Producer, Distributor, Retailer, + #[default] Consumer, } ``` @@ -157,13 +161,15 @@ The end! ### Initialization -```rust +```rust title="supply-chain/io/src/lib.rs" /// Initializes the Supply chain contract. /// /// # Requirements /// - Each [`ActorId`] of `producers`, `distributors`, and `retailers` mustn't /// equal [`ActorId::zero()`]. #[derive(Encode, Decode, Hash, TypeInfo, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub struct Initialize { /// IDs of actors that'll have the right to interact with a supply chain on /// behalf of a producer. @@ -184,14 +190,17 @@ pub struct Initialize { ### Actions -```rust +```rust title="supply-chain/io/src/lib.rs" /// Sends the contract info about what it should do. #[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub struct Action { pub action: InnerAction, pub kind: TransactionKind, } - +``` +```rust title="supply-chain/io/src/lib.rs" /// A part of [`Action`]. /// /// Determines how an action will be processed. @@ -205,34 +214,42 @@ pub struct Action { /// [`msg::source()`](gstd::msg::source) is cached. /// - Non-asynchronous actions are never cached. /// - There's no guarantee every underprocessed asynchronous action will be -/// cached. Use [`StateQuery::IsActionCached`] to check if some action is cached -/// for some [`ActorId`]. +/// cached. Use +/// [`is_action_cached()`](../supply_chain_state/metafns/fn.is_action_cached.html) +/// to check if some action is cached for some [`ActorId`]. /// - It's possible to send a retry action with a different payload, and it'll /// continue with it because, for some action, not all payload is saved in the /// cache (see [`CachedAction`]). /// - The cache memory has a limit, so when it's reached every oldest cached /// action is replaced with a new one. -#[derive( - Default, Encode, Decode, TypeInfo, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, -)] +#[derive(Default, Encode, Decode, TypeInfo, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub enum TransactionKind { #[default] New, Retry, } - +``` +```rust title="supply-chain/io/src/lib.rs" /// A part of [`Action`]. #[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub enum InnerAction { Producer(ProducerAction), Distributor(DistributorAction), Retailer(RetailerAction), Consumer(ConsumerAction), } +``` +```rust title="supply-chain/io/src/lib.rs" /// Actions for a producer. /// /// Should be used inside [`InnerAction::Producer`]. #[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub enum ProducerAction { /// Produces one item and a corresponding NFT with given `token_metadata`. /// @@ -307,11 +324,14 @@ pub enum ProducerAction { /// [`ItemEventState::Shipped`] & [`Role::Producer`]. Ship(ItemId), } - +``` +```rust title="supply-chain/io/src/lib.rs" /// Actions for a distributor. /// /// Should be used inside [`InnerAction::Distributor`]. #[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub enum DistributorAction { /// Purchases an item from a producer on behalf of a distributor. /// @@ -447,11 +467,14 @@ pub enum DistributorAction { /// [`ItemEventState::Shipped`] & [`Role::Distributor`]. Ship(ItemId), } - +``` +```rust title="supply-chain/io/src/lib.rs" /// Actions for a retailer. /// /// Should be used inside [`InnerAction::Retailer`]. #[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub enum RetailerAction { /// Purchases an item from a distributor on behalf of a retailer. /// @@ -516,11 +539,14 @@ pub enum RetailerAction { /// [`ItemEventState::ForSale`] & [`Role::Retailer`]. PutUpForSale { item_id: ItemId, price: u128 }, } - +``` +```rust title="supply-chain/io/src/lib.rs" /// Actions for a consumer. /// /// Should be used inside [`InnerAction::Consumer`]. #[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] pub enum ConsumerAction { /// Purchases an item from a retailer. /// @@ -543,7 +569,7 @@ pub enum ConsumerAction { ### Program metadata and state Metadata interface description: -```rust +```rust title="supply-chain/io/src/lib.rs" pub struct ContractMetadata; impl Metadata for ContractMetadata { @@ -557,52 +583,72 @@ impl Metadata for ContractMetadata { ``` To display the full contract state information, the `state()` function is used: -```rust +```rust title="supply-chain/src/lib.rs" #[no_mangle] -extern "C" fn state() { - reply(common_state()) - .expect("Failed to encode or reply with `::State` from `state()`"); +extern fn state() { + let (contract, _) = unsafe { STATE.take().expect("Unexpected error in taking state") }; + msg::reply::(contract.into(), 0) + .expect("Failed to encode or reply with `State` from `state()`"); } ``` To display only necessary certain values from the state, you need to write a separate crate. In this crate, specify functions that will return the desired values from the `State` struct. For example - [supply-chain/state](https://github.com/gear-foundation/dapps/tree/master/contracts/supply-chain/state): -```rust -#[metawasm] -pub trait Metawasm { - type State = supply_chain_io::State; +```rust title="supply-chain/state/src/lib.rs" +#[gmeta::metawasm] +pub mod metafns { + pub type State = supply_chain_io::State; - fn item_info(item_id: ItemId, state: Self::State) -> Option { - state.item_info(item_id) + pub fn item_info(state: State, item_id: ItemId) -> Option { + state + .items + .into_iter() + .find_map(|(some_item_id, item_info)| (some_item_id == item_id).then_some(item_info)) } - fn participants(state: Self::State) -> Participants { - state.participants() + pub fn participants(state: State) -> Participants { + Participants { + producers: state.producers, + distributors: state.distributors, + retailers: state.retailers, + } } - fn roles(actor: ActorId, state: Self::State) -> Vec { - state.roles(actor) + pub fn roles(state: State, actor: ActorId) -> Vec { + let mut roles = vec![Role::Consumer]; + + if state.producers.contains(&actor) { + roles.push(Role::Producer); + } + if state.distributors.contains(&actor) { + roles.push(Role::Distributor); + } + if state.retailers.contains(&actor) { + roles.push(Role::Retailer); + } + + roles } - fn existing_items(state: Self::State) -> Vec<(ItemId, ItemInfo)> { + pub fn existing_items(state: State) -> Vec<(ItemId, ItemInfo)> { state.items } - fn fungible_token(state: Self::State) -> ActorId { + pub fn fungible_token(state: State) -> ActorId { state.fungible_token } - fn non_fungible_token(state: Self::State) -> ActorId { + pub fn non_fungible_token(state: State) -> ActorId { state.non_fungible_token } - fn is_action_cached(actor_action: ActorIdInnerSupplyChainAction, state: Self::State) -> bool { - let (actor, action) = actor_action; - - state.is_action_cached(actor, action) + pub fn is_action_cached(state: State, actor: ActorId, action: InnerAction) -> bool { + if let Some(action) = action.into() { + state.cached_actions.contains(&(actor, action)) + } else { + false + } } } - -pub type ActorIdInnerSupplyChainAction = (ActorId, InnerAction); ``` ## Source code diff --git a/docs/examples/tequila-train.md b/docs/examples/tequila-train.md index 29008b522..339e73288 100644 --- a/docs/examples/tequila-train.md +++ b/docs/examples/tequila-train.md @@ -154,9 +154,9 @@ cd contracts/tequila-train One user (he may be a player or not) uploads the game program and initializes it. Initialization data is a vector of players’ public addresses: -```rust title="contracts/io/src/lib.rs" +```rust title="tequila-train/io/src/lib.rs" pub struct Players { - players: Vec, + players: Vec<(ActorId, String)>, } ``` @@ -166,13 +166,18 @@ Program makes all preparations during initialization. 2. The program tries to find the maximum double through users. If it doesn’t, it adds one tile to each user and repeats this step until double has been found. 3. Program chooses the double and selects the first user. -```rust title="contracts/src/contract.rs" +```rust title="tequila-train/src/contract.rs" #[no_mangle] -extern "C" fn init() { - let players_init: Players = msg::load().expect("Failed to decode `Players'"); - - // All game initializing logic is inside GameState constructor - unsafe { GAME_STATE = GameState::new(&players_init) } +extern fn init() { + let maybe_limit: Option = msg::load().expect("Unexpected invalid payload."); + + unsafe { + GAME_LAUNCHER = Some(if let Some(limit) = maybe_limit { + GameLauncher::new_with_limit(limit) + } else { + GameLauncher::default() + }) + } } ``` @@ -181,8 +186,7 @@ Every player move is the command message sent to the program: 1. Pass: skip the turn if there is no tile to place. 2. Turn: place selected tile to the selected track. Additionally, in certain circumstances the player may get their train back. -```rust title="contracts/io/src/lib.rs" -/// Command to the program +```rust title="tequila-train/io/src/contract.rs" pub enum Command { Skip, Place { @@ -190,15 +194,24 @@ pub enum Command { track_id: u32, remove_train: bool, }, + Register { + player: ActorId, + name: String, + }, + StartGame, + RestartGame( + /// Optional players limit. + Option, + ), } ``` User interface gets the program state after every action and renders it in the browser. -```rust title="contracts/io/src/lib.rs" +```rust title="tequila-train/io/src/contract.rs" /// The whole game state pub struct GameState { - pub players: Vec, + pub players: Vec<(ActorId, String)>, pub tracks: Vec, pub shots: Vec, pub start_tile: u32, @@ -208,19 +221,22 @@ pub struct GameState { pub remaining_tiles: BTreeSet, pub state: State, } - +``` +```rust title="tequila-train/io/src/contract.rs" /// Information about the player's track pub struct TrackData { pub tiles: Vec, pub has_train: bool, } - +``` +```rust title="tequila-train/io/src/contract.rs" /// Domino tile pub struct Tile { pub left: Face, pub right: Face, } - +``` +```rust title="tequila-train/io/src/contract.rs" /// Tile's face (number of dots) pub enum Face { Zero, @@ -237,12 +253,15 @@ pub enum Face { Eleven, Twelve, } - +``` +```rust title="tequila-train/io/src/contract.rs" /// The state of the game pub enum State { Playing, Stalled, - Winner(ActorId), + Winner((ActorId, String)), + #[default] + Registration, } ```