🔄Counter

This tutorial guides you through creating a smart contract with a Counter.

the This tutorial guides you through creating a smart contract using the Concordium default contract template. The contract maintains a counter value in its state.

Prerequisites

To follow along with this tutorial, ensure you have the following:

Follow these steps to create the contract:

Step 1: Initialize Project

  1. Create a working directory called "counter" and navigate to it in your terminal with these commands:

mkdir counter
cd counter
  1. Install "cargo-generate" (a helper tool for Rust project initialization) within your working directory with this command:

cargo install --locked cargo-generate@0.18.0

When you run the command, you will get the following result:

  1. Run the following command to set up the initial project:

cargo concordium init
  1. Select the "Default" option from the menu and provide a name and description for your project when prompted:

Afterward, your project will be created with the default smart contract template:

  1. Run the following commands to see what the project folder contains:

ls
cd counter-contract

You will see the following:

Here's a short description of the project folder's contents:

  • Cargo.toml: Manifest file containing project details like name, dependencies, build instructions, and configurations.

  • deploy-scripts: Directory for scripts deploying smart contracts to the Concordium network, including code building, deployment, and interaction management.

  • src: Core directory for smart contract code, typically containing Rust source files defining logic and functions.

  • tests: Directory for Rust unit tests ensuring smart contract functionality, aiding error detection and code consistency.

Step 2: Define the Contract Structure

  1. Open the project folder in your code editor. Go to the "src > lib.rs" directory:

#![cfg_attr(not(feature = "std"), no_std)]

//! # A Concordium V1 smart contract
use concordium_std::*;
use core::fmt::Debug;

/// Your smart contract state.
#[derive(Serialize, SchemaType)]
pub struct State {
    counter: i8,
}

/// Your smart contract errors.
#[derive(Debug, PartialEq, Eq, Reject, Serialize, SchemaType)]
pub enum Error {
    /// Failed parsing the parameter.
    #[from(ParseError)]
    ParseParams,
    /// Your error
    OwnerError,
    IncrementError,
    DecrementError,
}

/// Init function that creates a new smart contract.
#[init(contract = "counter_contract")]
fn init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult<State> {
    // Your code

    Ok(State { counter: 0 })
}

pub type MyInputType = bool;

/// 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(
    contract = "counter_contract",
    name = "receive",
    parameter = "MyInputType",
    error = "Error",
    mutable
)]
fn receive(ctx: &ReceiveContext, _host: &mut Host<State>) -> Result<(), Error> {
    // Your code

    let throw_error = ctx.parameter_cursor().get()?; // Returns Error::ParseError on failure
    if throw_error {
        Err(Error::YourError)
    } else {
        Ok(())
    }
}

/// View function that returns the content of the state.
#[receive(contract = "counter_contract", name = "view", return_value = "State")]
fn view<'b>(_ctx: &ReceiveContext, host: &'b Host<State>) -> ReceiveResult<&'b State> {
    Ok(host.state())
}

This is a basic skeleton of a smart contract containing the following:

  • State struct: Holds the data your smart contract stores.

  • init function: Creates a new instance of your smart contract.

  • Error enum: Defines custom errors your contract can throw.

  • view function: Allows reading the contract's state without modifying it.

  • receive function: Handles incoming transactions and interacts with the contract's state.

  1. In the State struct, modify the code to include a field named "counter" of type "i8" for the integer. This field will hold the counter value:

/// Your smart contract state.
#[derive(Serialize, SchemaType)]
pub struct State {
    counter: i8, // Stores the counter value
}
  1. In the init function, return a State object and initialize the counter field to "0". This will set the starting value of the counter when the smart contract is created:

/// Init function that creates a new smart contract.
#[init(contract = "counter_contract")]
fn init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult<State> {
    Ok(State { counter: 0 }) // Set initial counter value to 0
}
  1. Add the values OwnerError, IncrementError, and DecrementError to the Error enum to enhance error handling for ownership and counter-manipulation scenarios:

/// Your smart contract errors.
#[derive(Debug, PartialEq, Eq, Reject, Serialize, SchemaType)]
pub enum Error {
    /// Failed parsing the parameter.
    #[from(ParseError)]
    ParseParams,
    /// Your error
    OwnerError,
    IncrementError,
    DecrementError,
}

Here's a short description of the purpose of these error values:

  • OwnerError: Stops unauthorized users from changing the counter value.

  • IncrementError: Catches attempts to decrease the counter disguised as increments (negative increment values).

  • DecrementError (potential): Would prevent attempts to increase the counter disguised as decrements (negative decrement values).

Step 3: Implement Increment Function

Add a function to increment the counter value. Ensure that the function can only be called by the contract owner and that the input parameter is positive:

type IncrementVal = i8;
/// Receive function. The input parameter is the increment value `i8`.
///  If the account owner does not match the contract owner, the receive function will throw [`Error::OwnerError`].
///  If the number to increment by is not positive or is zero, the receive function will throw [`Error::IncrementError`].
#[receive(
    contract = "counter_contract",
    name = "increment",
    parameter = "i8",
    error = "Error",
    mutable
)]
fn increment<S: HasStateApi>(
    ctx: &impl HasReceiveContext,
    host: &mut impl HasHost<State, StateApiType = S>,
) -> Result<(), Error> {
    // Your code

    let param: IncrementVal = ctx.parameter_cursor().get()?;
    let state = host.state_mut();
    ensure!(
        ctx.sender().matches_account(&ctx.owner()),
        Error::OwnerError
    );

    ensure!(param > 0, Error::IncrementError);
    state.counter += param;
    Ok(())
}

Note the following:

  • The value must be positive, otherwise, you will get an Error::IncrementError.

  • The owner of the contract instance must trigger the transaction, or it will throw Error::OwnerError.

  • The function itself has to be mutable because you will change the state of the contract.

Step 4: Implement Decrement Function

Add a function to decrement the counter value. Similar to the increment function, ensure it can only be called by the contract owner and that the input parameter is negative:

#[receive(
    contract = "counter_contract",
    name = "decrement",
    parameter = "i8",
    error = "Error",
    mutable
)]
fn decrement<S: HasStateApi>(
    ctx: &impl HasReceiveContext,
    host: &mut impl HasHost<State, StateApiType = S>,
) -> Result<(), Error> {
    // Your code

    let param: IncrementVal = ctx.parameter_cursor().get()?;
    let state = host.state_mut();
    ensure!(
        ctx.sender().matches_account(&ctx.owner()),
        Error::OwnerError
    );

    ensure!(param < 0, Error::DecrementError);
    state.counter += param;
    Ok(())
}

If the input parameter is not negative, you will get a Error::DecrementError. Like the Increment counter, this function can be triggered by only the owner of the contract, otherwise it will throw an Error::OwnerError.

Step 5: Implement View Function

Create a function to retrieve the current counter value. This function should return the counter value as an i8 type, updating its return value within the host.state():

/// View function that returns the counter value.
#[receive(contract = "counter_contract", name = "view", return_value = "i8")]
fn view<'a, 'b, S: HasStateApi>(
    _ctx: &'a impl HasReceiveContext,
    host: &'b impl HasHost<State, StateApiType = S>,
) -> ReceiveResult<i8> {
    Ok(host.state().counter)
}

Step 6: Build and Deploy the Contract

  1. Create a folder named "dist" in your project directory.

  2. Run the following command. This command will generate the schema output file and the Wasm compiled contract and place them in the "dist" folder for you:

cargo concordium build --out dist/module.wasm.v1 --schema-out dist/schema.bin

You will see the following in your terminal:

3. Deploy it with the command below:

concordium-client module deploy dist/module.wasm.v1 --sender <YOUR-ACCOUNT> --name counter --grpc-port 20000 --grpc-ip node.testnet.concordium.com

When you run the command, you will see the following in your terminal:

  1. Run the following command to initialize it and create your contract instance so you'll be ready to invoke the functions in the next section:

concordium-client contract init <YOUR-MODULE-HASH> --sender <YOUR-ADDRESS> --energy 30000 --contract counter --grpc-port 20000 --grpc-ip node.testnet.concordium.com

When you run the command, you'll see the following:

Step 7: Interact with the Contract

After deploying the contract, you can interact with it using various functions. View Function

Check the initial state of the contract with the following command:

concordium-client contract invoke <YOUR-CONTRACT-INSTANCE> --entrypoint view --schema dist/schema.bin --grpc-port 20000 --grpc-ip node.testnet.concordium.com

When you run the command, you will see the following:

Since you just initialized the contract, the counter value is 0.

Increment Function

  1. Create a JSON file that holds your operator, which will be given as input to the function.

  2. Run the following command to increment the counter:

concordium-client contract update <YOUR-CONTRACT-INSTANCE> --entrypoint increment --parameter-json <PATH-TO-JSON> --schema dist/schema.bin --sender <YOUR-ADDRESS> --energy 6000 --grpc-port 20000 --grpc-ip node.testnet.concordium.com
  1. Start by testing with conditions. Try using another account other than the contract's owner, confirming that only the owner can call this function.

  1. You will receive the error code -2, indicating that you call the second error code from your Error enum, OwnerError.

  2. Update the state with a number, for example, 2:

  1. Check the state once more. It should be 2:

  1. To test the requirement that you cannot increment with a negative number, change the value in the JSON file to a negative number like -2:

  1. You cannot do it because of error code -3, which corresponds to the third element in the enum: IncrementError.

Test the decrement function in the same way as the increment function.

Congratulations! You've successfully created and tested a smart contract with a counter using the Concordium platform.

Last updated