top of page
Writer's pictureEko Lance

Deep Dive Technical Guide: Building a Supply Chain Tracker on Concordium



 An image that contains all the information for the developer workshop on how to build a supply chain tracker on Concordium.

This comprehensive guide will walk you through understanding, developing, and deploying a supply chain tracking smart contract on the Concordium blockchain. 

We'll break down each contract component, explain the code in detail, and provide step-by-step instructions for setting up your development environment, compiling the contract, and interacting with it.


Setting Up the Development Environment

To start building on Concordium, follow these steps:


1. Install Rust and Cargo:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

This instals the Rust programming language and its package manager, Cargo.


2. Install the wasm32-unknown-unknown target:

rustup target add wasm32-unknown-unknown

This adds support for compiling Rust to WebAssembly, which is required for Concordium smart contracts.


3. Install Concordium's Rust-specific tools:

cargo install concordium-contracts-common
cargo install cargo-concordium

These tools provide Concordium-specific functionality for contract development and compilation.


4. Clone the Concordium smart contracts template:

cd concordium-rust-smart-contracts-template

This gives you a starting point for your Concordium smart contract project.


5. Set up your project structure:

cargo new --lib supply_chain_tracker
cd supply_chain_tracker

Understanding the Smart Contract

Contract Overview

The smart contract implements a simple supply chain tracking system with the following features:

  • Adding products to the supply chain

  • Viewing all products

  • Ordering products


Contract Structure

Let's break down the main components of the contract:


Imports and Type Aliases

use concordium_std::*;
use core::fmt::Debug;

type ProductId = u64;
type OrderId = u64;
  • We import the necessary modules from concordium_std, which provides the core functionality for Concordium smart contracts.

  • We also import Debug from the core library, which we'll use for our custom error types.

  • We define type aliases for ProductId and OrderId. This improves code readability and makes it clear that these u64 values represent specific concepts in our contract.


State Structure

#[derive(Serialize, SchemaType, Clone)]
pub struct State {
    products: Vec<Product>,
    orders: Vec<Order>,
    product_count: u64,
    order_count: u64,
}

This structure holds the contract's state:

  • products: A vector (dynamic array) of Product structures.

  • orders: A vector of Order structures.

  • product_count: A counter for generating unique product IDs.

  • order_count: A counter for generating unique order IDs.


The #[derive(...)] attribute automatically implements certain traits:

  • Serialize: Allows the structure to be serialized (converted to a byte representation).

  • SchemaType: Generates a schema for the structure, which is used by Concordium's tooling.

  • Clone: Allows the structure to be cloned (duplicated).

#[derive(Serialize, SchemaType, Clone, Copy, Debug, PartialEq, Reject)]
pub enum Status {
    Available,
    Ordered,
    Shipped,
    Delivered,
    Cancelled,
}

This enum represents the possible states of a product or order in the supply chain. The derived traits provide various functionalities:

  • Copy: Allows the enum to be copied by value instead of being moved.

  • Debug: Enables debug formatting.

  • PartialEq: Allows comparison between Status values.

  • Reject: Allows the enum to be used in error results.


Product and Order Structures

#[derive(Serialize, SchemaType, Clone, Debug)]
pub struct Product {
    id: ProductId,
    name: String,
    location: String,
    price: Amount,
    status: Status,
}

#[derive(Serialize, SchemaType, Clone, Debug)]
pub struct Order {
    id: OrderId,
    product_id: ProductId,
    ordered_by: AccountAddress,
    approved_by: Option<AccountAddress>,
    delivered_to: Option<AccountAddress>,
    price: Amount,
    status: Status,
}

These structures define the attributes of products and orders in the system:

  • Product contains information about each item in the supply chain.

  • Order represents a purchase order, including who ordered it, approval status, and delivery information.


Custom Errors

#[derive(Debug, PartialEq, Eq, Reject, Serialize, SchemaType)]
pub enum CustomContractError {
    ProductNotFound,
    OrderNotFound,
    Unauthorized,
    InvalidOrderState,
    InsufficientPayment,
    InvalidParameter,
    InvalidSenderAddress,
}

This enum defines custom error types for various failure scenarios in the contract. The Reject trait allows these errors to be returned from contract functions.


Contract Functions

Init Function

#[init(contract = "supply_chain_tracker")]
fn init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult<State> {
    Ok(State {
        products: Vec::new(),
        orders: Vec::new(),
        product_count: 0,
        order_count: 0,
    })
}

This function initializes the contract:

  • The #[init] attribute marks this as the initialization function.

  • It takes an InitContext (which we don't use here, hence the _ prefix) and a StateBuilder.

  • It returns an InitResult<State>, which is a Result type specific to init functions.

  • We create an empty State with no products or orders, and zero counters.


Add Product Function

#[receive(
    contract = "supply_chain_tracker",
    name = "add_product",
    parameter = "AddProductParams",
    mutable
)]
fn add_product(ctx: &ReceiveContext, host: &mut Host<State>) -> Result<(), CustomContractError> {
    ensure!(
        ctx.sender().matches_account(&ctx.owner()),
        CustomContractError::Unauthorized
    );

    let params: AddProductParams = ctx
        .parameter_cursor()
        .get()
        .map_err(|_| CustomContractError::InvalidParameter)?;

    let state = host.state_mut();

    state.product_count += 1;
    let new_product_id = state.product_count;

    let product = Product {
        id: new_product_id,
        name: params.name,
        location: params.location,
        price: Amount::from_micro_ccd(params.price),
        status: Status::Available,
    };

    state.products.push(product);

    Ok(())
}

This function allows the contract owner to add new products:

  • The #[receive] attribute marks this as a receive function (can be called after initialization).

  • ensure! macro checks that the sender is the contract owner, otherwise returns an Unauthorized error.

  • We parse the input parameters using ctx.parameter_cursor().get().

  • We get a mutable reference to the state using host.state_mut().

  • We increment the product_count and use it as the new product ID.

  • We create a new Product with the given parameters and Available status.

  • We add the new product to the products vector in the state.


Get All Products Function

#[receive(
    contract = "supply_chain_tracker",
    name = "get_all_products",
    return_value = "Vec<Product>"
)]
fn get_all_products(_ctx: &ReceiveContext, host: &Host<State>) -> ReceiveResult<Vec<Product>> {
    let state = host.state();
    Ok(state.products.clone())
}

This function returns a list of all products:

  • It's a read-only operation, so we use host.state() instead of host.state_mut().

  • We return a clone of the products vector.


Order Product Function

#[receive(
    contract = "supply_chain_tracker",
    name = "order_product",
    payable,
    parameter = "ProductId",
    mutable
)]
fn order_product(
    ctx: &ReceiveContext,
    host: &mut Host<State>,
    amount: Amount,
) -> Result<(), CustomContractError> {
    let product_id: ProductId = ctx
        .parameter_cursor()
        .get()
        .map_err(|_| CustomContractError::InvalidParameter)?;

    let state = host.state_mut();

    let ordered_by = match ctx.sender() {
        Address::Account(account_address) => account_address,
        _ => return Err(CustomContractError::InvalidSenderAddress),
    };

    let product = state
        .products
        .iter()
        .find(|p| p.id == product_id)
        .ok_or(CustomContractError::ProductNotFound)?;

    ensure!(
        product.status == Status::Available,
        CustomContractError::InvalidOrderState
    );

    ensure!(
        amount >= product.price,
        CustomContractError::InsufficientPayment
    );

    state.order_count += 1;
    let new_order_id = state.order_count;

    let order = Order {
        id: new_order_id,
        product_id,
        ordered_by,
        approved_by: None,
        delivered_to: None,
        price: product.price,
        status: Status::Ordered,
    };

    state.orders.push(order);

    let product = state
        .products
        .iter_mut()
        .find(|p| p.id == product_id)
        .ok_or(CustomContractError::ProductNotFound)?;
    product.status = Status::Ordered;

    Ok(())
}

This function allows users to order products:

  • The payable attribute indicates that this function can receive CCD (Concordium's cryptocurrency).

  • We parse the product_id from the input parameters.

  • We ensure the sender is a valid account address.

  • We find the product with the given ID and ensure it's available.

  • We check that the payment amount is sufficient.

  • We create a new order and add it to the orders vector.

  • We update the product's status to Ordered.


Compiling the Contract

To compile the contract, use the following command:

cargo concordium build --out supply_chain_tracker.wasm.v1 --schema-out supply_chain_tracker.schema.json

This command does several things:

  • It compiles your Rust code to WebAssembly (WASM).

  • It generates a schema file that describes the contract's interface.

  • The --out flag specifies the output file for the compiled WASM module.

  • The --schema-out flag specifies the output file for the schema.


Interacting with the Contract

To interact with the contract, you can use the Concordium Smart Contract Testing Tool (SCTool) available at sctool.concordium.com. Here's a step-by-step guide:


1. Open the SCTool website in your browser.


2. Upload your compiled files:

  • Click on "Upload Module" and select your supply_chain_tracker.wasm.v1 file.

  • Click on "Upload Schema" and select your supply_chain_tracker.schema.json file.


3. Initialize the contract:

  • In the "Init" section, click "Init" to initialize your contract. This calls the init function.


4. Interact with the contract functions:

  • To add a product:

    • In the "Update" section, select the "add_product" function.

    • Enter the product details in JSON format, e.g., {"name": "Widget", "location": "Warehouse A", "price": 1000000} (price is in micro CCD).

    • Click "Update" to call the function.

  • To view all products:

    • In the "Query" section, select the "get_all_products" function.

    • Click "Query" to call the function and view the results.

  • To order a product:

    • In the "Update" section, select the "order_product" function.

    • Enter the product ID as a parameter, e.g., 1.

    • Set the "Amount" field to the price of the product (in micro CCD).

    • Click "Update" to call the function.


Remember to set the appropriate sender address when testing different scenarios, especially for functions that require specific permissions (like add_product which should only be called by the contract owner).


Conclusion

This deep-dive technical guide has walked you through the process of understanding, developing, and interacting with a supply chain tracking smart contract on the Concordium blockchain. We've covered:


  1. The structure and components of the smart contract

  2. Detailed explanations of each function and its purpose

  3. Step-by-step instructions for setting up your development environment

  4. How to compile the contract

  5. How to interact with the contract using the Concordium Smart Contract Testing Tool


By following this guide, you should now have a solid understanding of how to create, deploy, and test smart contracts on Concordium. Remember to always thoroughly test your contracts in a safe environment before deploying them to the main network.  If you want to watch the video tutorial, click here.


Happy coding!


About EkoLance

EkoLance revolutionizes the future of work by empowering Web2 and blockchain professionals through its dual offerings. The first is an educational platform that provides quality and comprehensive training programs for upskilling in the blockchain space, ensuring that professionals are equipped with the latest industry knowledge and practical experience. We currently have a diverse community of over 10,000 blockchain professionals, including over 5000 Web3 developers proficient in Solidity and Rust. 


The second offering is our talent platform, techFiesta. It enables companies to launch hackathons, jobs, bounties, and onboard top-tier talent into their ecosystems, fostering innovation and growth. techFiesta has successfully organized over 50 online hackathons and developer challenges for major blockchain networks such as Gnosis chain, Celo, Solana, Concordium, etc.


Follow us on our social channels:

LinkedIn: EkoLance

Twitter: EkoLance

Instagram: EkoLance

Facebook: EkoLance

Discord: EkoLance


Comments


bottom of page