diff --git a/examples/voting/src/lib.rs b/examples/voting/src/lib.rs index b30e2495..9d8662ef 100644 --- a/examples/voting/src/lib.rs +++ b/examples/voting/src/lib.rs @@ -2,104 +2,63 @@ //! //! # Description //! A contract that allows for conducting an election with several voting -//! options. An `end_time` is set when the election is initialized. Only +//! options. A deadline is set when the election is initialized. Only //! accounts are eligible to vote. Each account can change its -//! selected voting option as often as it desires until the `end_time` is -//! reached. No voting will be possible after the `end_time`. +//! selected voting option as often as it desires until the deadline is +//! reached. No voting will be possible after the deadline. //! //! # Operations //! The contract allows for -//! - `initializing` the election; -//! - `vote` for one of the voting options; -//! - `getNumberOfVotes` for a requested voting option; -//! - `view` general information about the election. -//! -//! Note: Vec (among other variables) is an input parameter to the -//! `init` function. Since there is a limit to the parameter size (65535 Bytes), -//! the size of the Vec is limited. -//! https://developer.concordium.software/en/mainnet/smart-contracts/general/contract-instances.html#limits -#![cfg_attr(not(feature = "std"), no_std)] -use concordium_std::*; - -/// The human-readable description of a voting option. -pub type VotingOption = String; -/// The voting options are stored in a vector. The vector index is used to refer -/// to a specific voting option. -pub type VoteIndex = u32; -/// Number of votes. -pub type VoteCount = u32; +//! - Initializing the election (`init` function). +//! - Viewing general information about the election (`view` function). +//! - Voting for one of the voting options (`vote` function). +//! - Tallying votes for a requested voting option (`getNumberOfVotes` +//! function). -/// The parameter type for the contract function `vote`. -/// Takes a `vote_index` that the account wants to vote for. -#[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] -pub struct VoteParameter { - /// Voting option index to vote for. - pub vote_index: VoteIndex, -} +#![cfg_attr(not(feature = "std"), no_std)] -/// The parameter type for the contract function `init`. -/// Takes a description, the voting options, and the `end_time` to start the -/// election. -#[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] -pub struct InitParameter { - /// The description of the election. - pub description: String, - /// A vector of all voting options. - pub options: Vec, - /// The last timestamp that an account can vote. - /// The election is open from the point in time that this smart contract is - /// initialized until the `end_time`. - pub end_time: Timestamp, -} +use concordium_std::*; -/// The `return_value` type of the contract function `view`. -/// Returns a description, the `end_time`, the voting options as a vector, and -/// the number of voting options of the current election. +/// Configuration for a single election. #[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] -pub struct VotingView { +pub struct ElectionConfig { /// The description of the election. pub description: String, - /// The last timestamp that an account can vote. - /// The election is open from the point in time that this smart contract is - /// initialized until the `end_time`. - pub end_time: Timestamp, - /// A vector of all voting options. - pub options: Vec, - /// The number of voting options. - pub num_options: u32, + /// All the voting options. + pub options: Vec, + /// The last timestamp at which an account can vote. + /// An election is open from the point in time that the smart contract is + /// initialized until the deadline. + pub deadline: Timestamp, } -/// The contract state -#[derive(Serial, DeserialWithState)] -#[concordium(state_parameter = "S")] -struct State { - /// The description of the election. - /// `StateBox` allows for lazy loading data. This is helpful - /// in the situations when one wants to do a partial update not touching - /// this field, which can be large. - description: StateBox, - /// The map connects a voter to the index of the voted-for voting option. - ballots: StateMap, - /// The map connects the index of a voting option to the number of votes - /// it received so far. - tally: StateMap, - /// The last timestamp that an account can vote. - /// The election is open from the point in time that this smart contract is - /// initialized until the `end_time`. - end_time: Timestamp, - /// A vector of all voting options. - /// `StateBox` allows for lazy loading data. This is helpful - /// in the situations when one wants to do a partial update not touching - /// this field, which can be large. - options: StateBox, S>, - /// The number of voting options. - num_options: u32, +/// The smart contract state. +#[derive(Serialize, SchemaType)] +struct State { + /// The configuration of the election. + config: ElectionConfig, + /// A map from voters to options, specifying who has voted for what. + ballots: HashMap, + /// A map from vote options to a vote count, specifying how many votes each + /// option has. + /// + /// Note the data duplication here! The tally can of course always be + /// determined by examining the ballots field. This requires looping + /// over all ballots, counting how many votes exist for a specific option. + /// However, such a loop would be practically unbounded, as it is only + /// limited by the number of votes. Such a loop could exhaust the energy + /// budget of the smart contract functions, potentially making the smart + /// contract unusable and vulnerable to a kind of DDoS attack. + /// + /// Thus, we favor duplicating the data in another hashmap, where the vote + /// count can be retrieved and updated in constant time. + tally: HashMap, } /// The different errors that the `vote` function can produce. #[derive(Reject, Serialize, PartialEq, Eq, Debug, SchemaType)] pub enum VotingError { - /// Raised when parsing the parameter failed. + /// Raised when parsing the input parameter failed. #[from(ParseError)] ParsingFailed, /// Raised when the log is full. @@ -108,8 +67,8 @@ pub enum VotingError { LogMalformed, /// Raised when the vote is placed after the election has ended. VotingFinished, - /// Raised when voting for a voting index that does not exist. - InvalidVoteIndex, + /// Raised when voting for an option that does not exist. + InvalidVote, /// Raised when a smart contract tries to participate in the election. Only /// accounts are allowed to vote. ContractVoter, @@ -125,55 +84,43 @@ impl From for VotingError { } } -/// A custom alias type for the `Result` type with the error type fixed to -/// `VotingError`. -pub type VotingResult = Result; - -/// The event is logged when a new (or replacement) vote is cast by an account. +/// A vote event. The event is logged when a new (or replacement) vote is cast +/// by an account. #[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] pub struct VoteEvent { /// The account that casts the vote. - pub voter: AccountAddress, - /// The index of the voting option that the account is voting for. - pub vote_index: VoteIndex, -} - -/// The event logged by this smart contract. -#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] -pub enum Event { - /// The event is logged when a new (or replacement) vote is cast by an - /// account. - Vote(VoteEvent), + pub voter: AccountAddress, + /// The voting option that the account is voting for. + pub option: String, } // Contract functions /// Initialize the contract instance and start the election. -/// A description, the vector of all voting options, and an `end_time` +/// A description, the vector of all voting options, and a `deadline` /// have to be provided. -#[init(contract = "voting", parameter = "InitParameter", event = "Event")] -fn init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { +/// +/// Note: There is a limit to the size of the function input parameter (65535 +/// Bytes), thus the number of voting options and the length of voting options +/// has a limit. Read more here: +#[init(contract = "voting", parameter = "ElectionConfig", event = "VoteEvent")] +fn init(ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { // Parse the parameter. - let param: InitParameter = ctx.parameter_cursor().get()?; - // Calculate the number of voting options. - let num_options = param.options.len() as u32; + let config: ElectionConfig = ctx.parameter_cursor().get()?; // Set the state. Ok(State { - description: state_builder.new_box(param.description), - ballots: state_builder.new_map(), - tally: state_builder.new_map(), - end_time: param.end_time, - options: state_builder.new_box(param.options), - num_options, + config, + ballots: HashMap::default(), + tally: HashMap::default(), }) } /// Enables accounts to vote for a specific voting option. Each account can /// change its selected voting option with this function as often as it desires -/// until the `end_time` is reached. +/// until the `deadline` is reached. /// -/// A valid vote produces an `Event::Vote` event. +/// A valid vote produces an `VoteEvent` event. /// This is also the case if the account recasts its vote for another, or even /// the same, option. By tracking the events produced, one can reconstruct the /// current state of the election. @@ -181,22 +128,25 @@ fn init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult, logger: &mut impl HasLogger, -) -> VotingResult<()> { +) -> Result<(), VotingError> { // Check that the election hasn't finished yet. - ensure!(ctx.metadata().slot_time() <= host.state().end_time, VotingError::VotingFinished); + ensure!( + ctx.metadata().slot_time() <= host.state().config.deadline, + VotingError::VotingFinished + ); // Ensure that the sender is an account. let acc = match ctx.sender() { @@ -204,35 +154,38 @@ fn vote( Address::Contract(_) => return Err(VotingError::ContractVoter), }; - // Parse the parameter. - let param: VoteParameter = ctx.parameter_cursor().get()?; - - let new_vote_index = param.vote_index; + // Parse the option. + let vote_option: String = ctx.parameter_cursor().get()?; - // Check that vote is in range - ensure!(new_vote_index < host.state().num_options, VotingError::InvalidVoteIndex); + // Check that the vote option is valid (exists). + ensure!(host.state().config.options.contains(&vote_option), VotingError::InvalidVote); - if let Some(old_vote_index) = host.state().ballots.get(&acc) { - let old_vote_index = *old_vote_index; - // Update the tally for the `old_vote_index` by reducing one vote. - *host.state_mut().tally.entry(old_vote_index).or_insert(1) -= 1; - }; + let state_mut = host.state_mut(); // Insert or replace the vote for the account. - host.state_mut() + state_mut .ballots .entry(acc) - .and_modify(|old_vote_index| *old_vote_index = new_vote_index) - .or_insert(new_vote_index); + .and_modify(|old_vote_option| { + // The account has already voted previously - we should subtract one vote from + // the tally for that option. See doc comment on the tally field for + // why it is done this way. + *state_mut.tally.get_mut(old_vote_option).unwrap() -= 1; + + old_vote_option.clone_from(&vote_option); + }) + .or_insert(vote_option.clone()); - // Update the tally for the `new_vote_index` with one additional vote. - *host.state_mut().tally.entry(new_vote_index).or_insert(0) += 1; + // Now that the account has voted, we should increment the vote count for their + // new vote choice. See doc comment on the tally field for why it is done + // this way. + *state_mut.tally.entry(vote_option.clone()).or_default() += 1; // Log event for the vote. - logger.log(&Event::Vote(VoteEvent { - voter: acc, - vote_index: new_vote_index, - }))?; + logger.log(&VoteEvent { + voter: acc, + option: vote_option, + })?; Ok(()) } @@ -244,36 +197,30 @@ fn vote( #[receive( contract = "voting", name = "getNumberOfVotes", - parameter = "VoteParameter", - return_value = "VoteCount" + parameter = "String", + return_value = "u32" )] -fn get_votes(ctx: &ReceiveContext, host: &Host) -> ReceiveResult { - // Parse the parameter. - let param: VoteIndex = ctx.parameter_cursor().get()?; - - // Get the number of votes from the tally. - let result = match host.state().tally.get(¶m) { - Some(votes) => *votes, - None => 0, - }; +fn get_votes(ctx: &ReceiveContext, host: &Host) -> ReceiveResult { + // Parse the vote option. + let vote_option: String = ctx.parameter_cursor().get()?; - Ok(result) + // Get the number of votes for this option from the tally map. + // See doc comment on the tally field for why it is done this way. + Ok(host.state().tally.get(&vote_option).copied().unwrap_or_default()) } /// Get the election information. -#[receive(contract = "voting", name = "view", return_value = "VotingView")] -fn view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { +#[receive(contract = "voting", name = "view", return_value = "ElectionConfig")] +fn view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { // Get information from the state. - let description = host.state().description.clone(); - let end_time = host.state().end_time; - let num_options = host.state().num_options; - let options = host.state().options.clone(); + let description = host.state().config.description.clone(); + let options = host.state().config.options.clone(); + let deadline = host.state().config.deadline; // Return the election information. - Ok(VotingView { + Ok(ElectionConfig { description, - end_time, + deadline, options, - num_options, }) } diff --git a/examples/voting/tests/tests.rs b/examples/voting/tests/tests.rs index 9dae3cda..1f25c499 100644 --- a/examples/voting/tests/tests.rs +++ b/examples/voting/tests/tests.rs @@ -16,9 +16,7 @@ const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(1000); fn test_vote_after_end_time() { let (mut chain, contract_address) = init(); - let params = VoteParameter { - vote_index: 0, - }; + let params = "A".to_string(); // Advance time to after the end time. chain.tick_block_time(Duration::from_millis(1001)).expect("Won't overflow"); @@ -40,12 +38,10 @@ fn test_vote_after_end_time() { /// Test that voting with an voting index that is out of range fails. #[test] -fn test_vote_out_of_range() { +fn test_invalid_vote() { let (mut chain, contract_address) = init(); - let params = VoteParameter { - vote_index: 3, // Valid indexes are: 0, 1, 2. - }; + let params = "invalid vote".to_string(); // Try to vote. let update = chain @@ -59,67 +55,61 @@ fn test_vote_out_of_range() { // Check that the error is correct. let rv: VotingError = update.parse_return_value().expect("Deserialize VotingError"); - assert_eq!(rv, VotingError::InvalidVoteIndex); + assert_eq!(rv, VotingError::InvalidVote); } /// Test that voting and changing your vote works. /// /// In particular: -/// - Alice votes for option 0. -/// - Alice changes her vote to option 1. -/// - Bob votes for option 0. -/// - Bob again votes for option 0. +/// - Alice votes for A. +/// - Alice changes her vote to B. +/// - Bob votes for A. +/// - Bob again votes for A. #[test] fn test_voting_and_changing_vote() { let (mut chain, contract_address) = init(); - // Alice votes for option 0. + // Alice votes for A. let update_1 = chain .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { amount: Amount::zero(), address: contract_address, receive_name: OwnedReceiveName::new_unchecked("voting.vote".to_string()), - message: OwnedParameter::from_serial(&VoteParameter { - vote_index: 0, - }) - .expect("Valid vote parameter"), + message: OwnedParameter::from_serial(&"A".to_string()) + .expect("Valid vote parameter"), }) .expect("Contract updated"); // Check the events and votes. - check_event(&update_1, ALICE, 0); + check_event(&update_1, ALICE, "A"); assert_eq!(get_votes(&chain, contract_address), [1, 0, 0]); - // Alice changes her vote to option 1. + // Alice changes her vote to B. let update_2 = chain .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { amount: Amount::zero(), address: contract_address, receive_name: OwnedReceiveName::new_unchecked("voting.vote".to_string()), - message: OwnedParameter::from_serial(&VoteParameter { - vote_index: 1, - }) - .expect("Valid vote parameter"), + message: OwnedParameter::from_serial(&"B".to_string()) + .expect("Valid vote parameter"), }) .expect("Contract updated"); // Check the events and votes. - check_event(&update_2, ALICE, 1); + check_event(&update_2, ALICE, "B"); assert_eq!(get_votes(&chain, contract_address), [0, 1, 0]); - // Bob votes for option 0. + // Bob votes for A. let update_3 = chain .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10_000), UpdateContractPayload { amount: Amount::zero(), address: contract_address, receive_name: OwnedReceiveName::new_unchecked("voting.vote".to_string()), - message: OwnedParameter::from_serial(&VoteParameter { - vote_index: 0, - }) - .expect("Valid vote parameter"), + message: OwnedParameter::from_serial(&"A".to_string()) + .expect("Valid vote parameter"), }) .expect("Contract updated"); // Check the events and votes. - check_event(&update_3, BOB, 0); + check_event(&update_3, BOB, "A"); assert_eq!(get_votes(&chain, contract_address), [1, 1, 0]); // Bob again votes for option 0. @@ -128,14 +118,12 @@ fn test_voting_and_changing_vote() { amount: Amount::zero(), address: contract_address, receive_name: OwnedReceiveName::new_unchecked("voting.vote".to_string()), - message: OwnedParameter::from_serial(&VoteParameter { - vote_index: 0, - }) - .expect("Valid vote parameter"), + message: OwnedParameter::from_serial(&"A".to_string()) + .expect("Valid vote parameter"), }) .expect("Contract updated"); // Check the events and votes. - check_event(&update_4, BOB, 0); + check_event(&update_4, BOB, "A"); assert_eq!(get_votes(&chain, contract_address), [1, 1, 0]); } @@ -145,9 +133,7 @@ fn test_contract_voter() { let (mut chain, contract_address) = init(); // Try to vote. - let params = VoteParameter { - vote_index: 0, - }; + let params = "A".to_string(); let update = chain .contract_update( SIGNER, @@ -192,10 +178,10 @@ fn init() -> (Chain, ContractAddress) { let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); // Contract init parameters. - let params = InitParameter { + let params = ElectionConfig { description: "Election description".to_string(), options: vec!["A".to_string(), "B".to_string(), "C".to_string()], - end_time: Timestamp::from_timestamp_millis(1000), + deadline: Timestamp::from_timestamp_millis(1000), }; // Initialize contract. @@ -211,58 +197,56 @@ fn init() -> (Chain, ContractAddress) { (chain, init.contract_address) } -/// Get the number of votes for each voting option. +/// Get the number of votes for each voting option, A, B and C. fn get_votes(chain: &Chain, contract_address: ContractAddress) -> [u32; 3] { - let view_0 = chain + let view_a = chain .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { amount: Amount::zero(), address: contract_address, receive_name: OwnedReceiveName::new_unchecked("voting.getNumberOfVotes".to_string()), - message: OwnedParameter::from_serial(&VoteParameter { - vote_index: 0, - }) - .expect("Valid vote parameter"), + message: OwnedParameter::from_serial(&"A".to_string()) + .expect("Valid vote parameter"), }) .expect("View invoked"); - let vote_0: VoteCount = view_0.parse_return_value().expect("Deserialize VoteCount"); + let vote_a: u32 = view_a.parse_return_value().expect("Deserialize VoteCount"); - let view_1 = chain + let view_b = chain .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { amount: Amount::zero(), address: contract_address, receive_name: OwnedReceiveName::new_unchecked("voting.getNumberOfVotes".to_string()), - message: OwnedParameter::from_serial(&VoteParameter { - vote_index: 1, - }) - .expect("Valid vote parameter"), + message: OwnedParameter::from_serial(&"B".to_string()) + .expect("Valid vote parameter"), }) .expect("View invoked"); - let vote_1: VoteCount = view_1.parse_return_value().expect("Deserialize VoteCount"); + let vote_b: u32 = view_b.parse_return_value().expect("Deserialize VoteCount"); - let view_2 = chain + let view_c = chain .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { amount: Amount::zero(), address: contract_address, receive_name: OwnedReceiveName::new_unchecked("voting.getNumberOfVotes".to_string()), - message: OwnedParameter::from_serial(&VoteParameter { - vote_index: 2, - }) - .expect("Valid vote parameter"), + message: OwnedParameter::from_serial(&"C".to_string()) + .expect("Valid vote parameter"), }) .expect("View invoked"); - let vote_2: VoteCount = view_2.parse_return_value().expect("Deserialize VoteCount"); + let vote_c: u32 = view_c.parse_return_value().expect("Deserialize VoteCount"); - [vote_0, vote_1, vote_2] + [vote_a, vote_b, vote_c] } /// Check that the voting event is produced and that it is correct. -fn check_event(update: &ContractInvokeSuccess, voter: AccountAddress, vote_index: VoteIndex) { - let events: Vec = update +fn check_event( + update: &ContractInvokeSuccess, + voter: AccountAddress, + vote_option: impl Into, +) { + let events: Vec = update .events() .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) .collect(); - assert_eq!(events, [Event::Vote(VoteEvent { + assert_eq!(events, [VoteEvent { voter, - vote_index, - })]); + option: vote_option.into(), + }]); } diff --git a/templates/README.md b/templates/README.md index cce35e57..d5b343ab 100644 --- a/templates/README.md +++ b/templates/README.md @@ -51,20 +51,20 @@ cargo concordium test ## The `default` Template -This template generates some smart contract boilerplate code. The boilerplate code has an empty ``State`` struct and three functions (`init`, `receive`, and `view`). -- The `init` function is used to deploy and initialize your module on-chain. -- The `view` function returns the state of the smart contract which is currently empty (fill the state struct with some custom state variables). -- The `receive` function can be invoked with the boolean parameter ``throw_error``. If the function is invoked with `throw_error == true`, the receive function will throw a custom error. If the function is invoked with `throw_error == false`, the receive function executes successfully. Add some custom logic to the ``receive`` function. +This template generates some smart contract boilerplate code. The boilerplate code has an example ``State`` struct and three functions (`init`, `receive`, and `view`). +- The `init` function is used to deploy and initialize your module on-chain. It has an example custom input parameter. +- The `view` function returns the state of the smart contract. +- The `receive` function can be invoked with the boolean parameter ``return_error``. If the function is invoked with `return_error == true`, the receive function will return a custom error. If the function is invoked with `return_error == false`, the receive function executes successfully. You can customize the logic of the ``receive`` function as you see fit. ### Simulating the deployment of the module ``` -cargo concordium run init --module ./my_concordium_project.wasm.v1 --contract my_concordium_project --context contextInit.json --out-bin state.bin +cargo concordium run init --module ./my_concordium_project.wasm.v1 --contract my_concordium_project --context contextInit.json --out-bin state.bin --parameter-json input.json ``` -This command simulates the deployment of your smart contract module. You require a ``contextInit.json`` file similar to the following one: +This command simulates the deployment of your smart contract module. You need a ``contextInit.json`` file and a ``input.json`` file. The ``contextInit.json`` file should look like this: ``` { @@ -80,6 +80,14 @@ This command simulates the deployment of your smart contract module. You require } ``` +The ``input.json`` file should look like this: + +``` +{ + "num": 42 +} +``` + ### Simulating the view function @@ -111,25 +119,29 @@ This command simulates an invoke to the ``view`` function of your smart contract } ``` -### Simulating the receive function with the input parameter `throw_error == false` +### Simulating the receive function with the input parameter `return_error == false` ``` -cargo concordium run update --entrypoint receive --module ./my_concordium_project.wasm.v1 --parameter-json throwErrorFalse.json --state-bin state.bin --contract my_concordium_project --context contextUpdate.json +cargo concordium run update --entrypoint receive --module ./my_concordium_project.wasm.v1 --parameter-json returnErrorFalse.json --state-bin state.bin --contract my_concordium_project --context contextUpdate.json ``` -This command simulates an invoke to the ``receive`` function of your smart contract module with the input parameter `throw_error == false`. +Where the contents of ``returnErrorFalse.json`` is simply `false`. -### Simulating the receive function with the input parameter `throw_error == true` +This command simulates an invoke to the ``receive`` function of your smart contract module with the input parameter `return_error == false`. + +### Simulating the receive function with the input parameter `return_error == true` ``` -cargo concordium run update --entrypoint receive --module ./my_concordium_project.wasm.v1 --parameter-json throwErrorTrue.json --state-bin state.bin --contract my_concordium_project --context contextUpdate.json +cargo concordium run update --entrypoint receive --module ./my_concordium_project.wasm.v1 --parameter-json returnErrorTrue.json --state-bin state.bin --contract my_concordium_project --context contextUpdate.json ``` -This command simulates an invoke to the ``receive`` function of your smart contract module with the input parameter `throw_error == true`. +Where the contents of ``returnErrorTrue.json`` is simply ``true``. + +This command simulates an invoke to the ``receive`` function of your smart contract module with the input parameter `return_error == true`. ## The `cis2-nft` Template diff --git a/templates/default/Cargo.toml b/templates/default/Cargo.toml index 3ab7f95c..34ceaefc 100644 --- a/templates/default/Cargo.toml +++ b/templates/default/Cargo.toml @@ -1,12 +1,11 @@ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [package] -name = "{{crate_name}}" +name = "{{ crate_name }}" version = "0.1.0" edition = "2021" license = "MPL-2.0" -authors = [ "{{authors}}" ] -description = "{{description}}" +authors = ["{{ authors }}"] [features] default = ["std"] @@ -14,13 +13,13 @@ std = ["concordium-std/std"] bump_alloc = ["concordium-std/bump_alloc"] [dependencies] -concordium-std = {version = "10.0", default-features = false} +concordium-std = { version = "10.0", default-features = false } [dev-dependencies] concordium-smart-contract-testing = "4.2" [lib] -crate-type=["cdylib", "rlib"] +crate-type = ["cdylib", "rlib"] [profile.release] opt-level = "s" diff --git a/templates/default/cargo-generate.toml b/templates/default/cargo-generate.toml index 89674011..09a10acc 100644 --- a/templates/default/cargo-generate.toml +++ b/templates/default/cargo-generate.toml @@ -1,5 +1,2 @@ [template] cargo_generate_version = ">= 0.17.0, < 0.22.0" - -[placeholders] -description = { type="string", prompt="Description for the project?" } diff --git a/templates/default/deploy-scripts/Cargo.toml b/templates/default/deploy-scripts/Cargo.toml index 5c593d80..55cc0d3a 100644 --- a/templates/default/deploy-scripts/Cargo.toml +++ b/templates/default/deploy-scripts/Cargo.toml @@ -12,4 +12,4 @@ clap = { version = "4", features = ["derive", "env"]} concordium-rust-sdk="4.2" tokio = {version = "1.18", features = ["rt", "macros", "rt-multi-thread"] } tonic = {version = "0.10", features = ["tls", "tls-roots"]} # Use system trust roots. -{{crate_name}} = {path = "../"} +{{ crate_name }} = { path = "../" } diff --git a/templates/default/deploy-scripts/src/main.rs b/templates/default/deploy-scripts/src/main.rs index 094b3006..7034bd65 100644 --- a/templates/default/deploy-scripts/src/main.rs +++ b/templates/default/deploy-scripts/src/main.rs @@ -102,7 +102,10 @@ async fn main() -> Result<(), Error> { // Write your own deployment/initialization script below. An example is given // here. - let param: OwnedParameter = OwnedParameter::empty(); // Example + // You can easily import a type from the smart contract like so: + use default::CustomInputParameter; // Example + + let param = OwnedParameter::from_serial(&CustomInputParameter { num: 42 })?; // Example let init_method_name: &str = "init_{{crate_name}}"; // Example @@ -118,14 +121,9 @@ async fn main() -> Result<(), Error> { .await .context("Failed to initialize the contract.")?; // Example - // This is how you can use a type from your smart contract. - use {{crate_name}}::MyInputType; // Example - - let input_parameter: MyInputType = false; // Example - // Create a successful transaction. - - let bytes = contracts_common::to_bytes(&input_parameter); // Example + // The input parameter to the receive function is in this example a bool. + let bytes = contracts_common::to_bytes(&false); // Example let update_payload = transactions::UpdateContractPayload { amount: Amount::from_ccd(0), diff --git a/templates/default/src/lib.rs b/templates/default/src/lib.rs index 5e89e282..cef24dc6 100644 --- a/templates/default/src/lib.rs +++ b/templates/default/src/lib.rs @@ -1,58 +1,74 @@ +//! # Concordium V1 Smart Contract Template + #![cfg_attr(not(feature = "std"), no_std)] -//! # A Concordium V1 smart contract use concordium_std::*; use core::fmt::Debug; -/// Your smart contract state. +/// The state of the smart contract. #[derive(Serialize, SchemaType)] pub struct State { - // Your state + // Add fields to this type to hold state in the smart contract. + // This field is just an example. + custom_state_field: i8, } -/// Your smart contract errors. +/// Errors that may be emitted by this smart contract. #[derive(Debug, PartialEq, Eq, Reject, Serialize, SchemaType)] pub enum Error { - /// Failed parsing the parameter. + /// Failed parsing the input parameter. #[from(ParseError)] ParseParams, - /// Your error - YourError, + /// Add variants to this enum to be able to return custom errors from the smart contract. + CustomError, } -/// Init function that creates a new smart contract. -#[init(contract = "{{crate_name}}")] -fn init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { - // Your code - - Ok(State {}) +/// Any type implementing Serialize and SchemaType can be +/// used as an input parameter to a smart contract function. +#[derive(Serialize, SchemaType)] +pub struct CustomInputParameter { + /// Just an example, you could have any fields here. + pub num: i8, } -pub type MyInputType = bool; +/// Creates a new instance of the smart contract. +#[init(contract = "{{ crate_name }}", parameter = "CustomInputParameter")] +fn init(ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { + let param: CustomInputParameter = ctx.parameter_cursor().get()?; + + // Create the initial state of the smart contract here. + // This state can then be used in the other functions. + Ok(State { + custom_state_field: param.num, + }) +} -/// Receive function. The input parameter is the boolean variable `throw_error`. -/// If `throw_error == true`, the receive function will throw a custom error. -/// If `throw_error == false`, the receive function executes successfully. +/// Receive function. The input parameter in this function is the boolean variable `return_error`. +/// If `return_error == true`, the receive function will return a custom error. +/// If `return_error == false`, the receive function executes successfully. #[receive( - contract = "{{crate_name}}", + contract = "{{ crate_name }}", name = "receive", - parameter = "MyInputType", + // You can use any other type than bool here, bool is used here only as an example. + parameter = "bool", error = "Error", mutable )] fn receive(ctx: &ReceiveContext, _host: &mut Host) -> Result<(), Error> { - // Your code + // Parse input and apply any other logic relevant for this function of the smart contract. + // You can mutate the smart contract state here via host.state_mut(), since the receive attribute has the mutable flag. + // You can return any of your custom error variants from above. - let throw_error = ctx.parameter_cursor().get()?; // Returns Error::ParseError on failure - if throw_error { - Err(Error::YourError) + let return_error = ctx.parameter_cursor().get()?; // Returns Error::ParseError on failure. + if return_error { + Err(Error::CustomError) } else { Ok(()) } } -/// View function that returns the content of the state. -#[receive(contract = "{{crate_name}}", name = "view", return_value = "State")] -fn view<'b>(_ctx: &ReceiveContext, host: &'b Host) -> ReceiveResult<&'b State> { +/// Returns the state of the smart contract. +#[receive(contract = "{{ crate_name }}", name = "view", return_value = "State")] +fn view<'a>(_ctx: &ReceiveContext, host: &'a Host) -> ReceiveResult<&'a State> { Ok(host.state()) } diff --git a/templates/default/tests/tests.rs b/templates/default/tests/tests.rs index f8764000..017edbd7 100644 --- a/templates/default/tests/tests.rs +++ b/templates/default/tests/tests.rs @@ -30,7 +30,7 @@ fn test_throw_no_error() { } /// Test that invoking the `receive` endpoint with the `true` parameter -/// results in the `YourError` being thrown. +/// results in the `CustomError` being thrown. #[test] fn test_throw_error() { let (mut chain, init) = initialize(); @@ -45,9 +45,9 @@ fn test_throw_error() { }) .expect_err("Update fails with `true` as input."); - // Check that the contract returned `YourError`. + // Check that the contract returned `CustomError`. let error: Error = update.parse_return_value().expect("Deserialize `Error`"); - assert_eq!(error, Error::YourError); + assert_eq!(error, Error::CustomError); } /// Helper method for initializing the contract.