DEV Community

Cover image for Stellar contract CRUD Tutorial A Step-by-Step Guide from Zero to Hero
amionweb
amionweb

Posted on

Stellar contract CRUD Tutorial A Step-by-Step Guide from Zero to Hero

In this tutorial, we'll guide you through creating a Stellar smart contract from scratch. You'll learn how to implement basic CRUD (Create, Read, Update, Delete) operations for managing products in a decentralized application (dApp). By the end of this tutorial, you'll be able to add, update, delete, view, and get all products using a Stellar smart contract.

Prerequisites

Before we dive into the code, make sure you have the following:

Set Up Environment / Project Installation Guide

A) Environment Setup:

  • Install Rust, using command:
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

  • nstall the Soroban CLI using below mentioned command. For more info visit => Soroban docs
    cargo install --locked soroban-cli

  • Install Node.js

  • Get the Freighter Wallet extension for you browser.
    Once enabled, then got to the network section and connect your wallet to the testnet.

  • Install wasm32-unknown-unknown package using command:
    rustup target add wasm32-unknown-unknown

  • To configure your CLI to interact with Testnet, run the following command:

soroban network add \
  --global testnet \
  --rpc-url https://soroban-testnet.stellar.org:443 \
  --network-passphrase "Test SDF Network ; September 2015"
Enter fullscreen mode Exit fullscreen mode

Step 1: Setting Up the Project

First, create a new Rust project by running the following command in your terminal:

stellar contract init product-crud
Enter fullscreen mode Exit fullscreen mode

Navigate to the project directory:

cd product-crud
Enter fullscreen mode Exit fullscreen mode

These dependencies allow us to interact with Stellar's Soroban platform.

Step 3: Defining the Product Struct

In your lib.rs file, start by defining the Product struct. This struct will hold the details of each product.

#![no_std] // We don't use the Rust standard library in smart contracts
use soroban_sdk::{contracttype, Env, String};

// Struct to represent a product in our dApp
#[contracttype]
#[derive(Clone)]
pub struct Product {
    pub id: u64,              // Unique ID for the product
    pub name: String,         // Name of the product
    pub description: String,  // Description of the product
    pub price: u64,           // Price of the product
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The Product struct is defined with fields such as id, name, description, and price. This struct will store information about each product.

Step 4: Setting Up the Contract

Next, define the main contract structure and the CRUD operations. Here's how you can start:

use soroban_sdk::{contract, contractimpl, symbol_short, Symbol};

// Define a contract type called ProductContract
#[contract]
pub struct ProductContract;

// Enum for referencing product storage
#[contracttype]
pub enum Productbook {
    Product(u64),
}

// Symbol to track the total count of products
const COUNT_PRODUCT: Symbol = symbol_short!("C_PROD");
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The ProductContract struct is defined as the main contract.
  • Productbook is an enum used to store and retrieve products by their ID.
  • COUNT_PRODUCT is a symbol used to track the number of products stored.

Step 5: Implementing the Create Operation

Now, let's implement the function to create a new product.

#[contractimpl]
impl ProductContract {
    pub fn create_product(
        env: Env,
        name: String,
        description: String,
        price: u64,
    ) -> u64 {
        let mut count_product: u64 = env.storage().instance().get(&COUNT_PRODUCT).unwrap_or(0);
        count_product += 1;

        let new_product = Product {
            id: count_product,
            name,
            description,
            price,
        };

        env.storage()
            .instance()
            .set(&Productbook::Product(new_product.id.clone()), &new_product);
        env.storage().instance().set(&COUNT_PRODUCT, &count_product);

        log!(&env, "Product Created with ID: {}", new_product.id);

        new_product.id
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The create_product function increments the count_product by 1 and creates a new product with the provided details.
  • The product is then stored in the contract's storage, and the ID of the newly created product is returned.

Step 6: Implementing the Read Operation

Let's add a function to retrieve a product by its ID.

impl ProductContract {
    pub fn get_product_by_id(env: Env, product_id: u64) -> Product {
        let key = Productbook::Product(product_id);

        env.storage().instance().get(&key).unwrap_or(Product {
            id: 0,
            name: String::from_str(&env, "Not Found"),
            description: String::from_str(&env, "Not Found"),
            price: 0,
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The get_product_by_id function retrieves a product from storage by its ID. If the product is not found, it returns a default Product with "Not Found" values.

Step 7: Implementing the Update Operation

Now, add a function to update the details of an existing product.

impl ProductContract {
    pub fn update_product(
        env: Env,
        product_id: u64,
        new_name: Option<String>,
        new_description: Option<String>,
        new_price: Option<u64>,
    ) {
        let key = Productbook::Product(product_id);
        let mut product = Self::get_product_by_id(env.clone(), product_id);

        if let Some(name) = new_name {
            product.name = name;
        }
        if let Some(description) = new_description {
            product.description = description;
        }
        if let Some(price) = new_price {
            product.price = price;
        }

        env.storage().instance().set(&key, &product);

        log!(&env, "Product with ID: {} has been updated.", product_id);
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The update_product function allows updating the product's name, description, and price. If new values are provided, they replace the existing ones.

Step 8: Implementing the Delete Operation

Finally, let's implement a function to delete a product by its ID.

impl ProductContract {
    pub fn delete_product(env: Env, product_id: u64) {
        let key = Productbook::Product(product_id);

        if env.storage().instance().has(&key) {
            env.storage().instance().remove(&key);

            log!(&env, "Product with ID: {} has been deleted.", product_id);
        } else {
            log!(&env, "Product with ID: {} does not exist.", product_id);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The delete_product function removes a product from storage by its ID. If the product doesn't exist, a message is logged.

Step 9: Implementing the Get All Products Operation

Lastly, let's add a function to retrieve all products.

impl ProductContract {
    pub fn get_all_products(env: Env) -> Vec<Product> {
        let count_product: u64 = env.storage().instance().get(&COUNT_PRODUCT).unwrap_or(0);
        let mut products = Vec::new(&env);

        for i in 1..=count_product {
            let product = Self::get_product_by_id(env.clone(), i);
            products.push_back(product);
        }

        products
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The get_all_products function iterates through all the stored products and returns them as a vector.

Step 10: Testing and Deploying the Contract

  • In order to deploy the smartcontract you will need an account. You can either use the an account from the Freighter Wallet or can configure an account named alice in the testnet using the command:
    soroban keys generate --global alice --network testnet

  • You can see the public key of account alice:
    soroban keys address alice

=> Go inside the /product-crud directory and do the below mentioned steps:

  • Build the contract:
soroban contract build
Enter fullscreen mode Exit fullscreen mode
  • Alternte command:
cargo build --target wasm32-unknown-unknown --release
Enter fullscreen mode Exit fullscreen mode
  • Install Optimizer:
cargo install --locked soroban-cli --features opt
Enter fullscreen mode Exit fullscreen mode
  • Build an Opmize the contract:
soroban contract optimize --wasm target/wasm32-unknown-unknown/release/hello_world.wasm 
Enter fullscreen mode Exit fullscreen mode

Steps to the Deploy smart-contract on testnet:

  • deploy the smartcontract on the testnet and get deployed address of the smartcontract using the following command:
stellar contract deploy --wasm target\wasm32-unknown-unknown\release\hello_world.wasm  --network testnet --source alice
Enter fullscreen mode Exit fullscreen mode

Deployed address of this smartcontract: contract_address

*NOTE: If you get the XDR Error error: xdr processing error: xdr value invalid, then follow this article.

Invoke functions from the smart-contract:

  • #### To invoke any of the function from the smartcontract you can use this command fromat.
soroban contract invoke \
  --id <DEPLOYED_CONTRACT_ADDRESS> \
  --source <YOUR_ACCOUNT_NAME> \
  --network testnet \
  -- \
  <FUNCTION_NAME> --<FUNCTION_PARAMETER> <ARGUMENT>
Enter fullscreen mode Exit fullscreen mode

Here are example soroban contract invoke commands for each of the functions in the ProductContract smart contract, using dummy data. Replace <DEPLOYED_CONTRACT_ADDRESS>, <YOUR_ACCOUNT_NAME>, and other placeholders with actual values.

1. Create a Product

soroban contract invoke --id <DEPLOYED_CONTRACT_ADDRESS> --source <YOUR_ACCOUNT_NAME> --network testnet -- create_product --name "Sample Product" --description "A description of the sample product." --price 1000
Enter fullscreen mode Exit fullscreen mode

2. Get a Product by ID

soroban contract invoke --id <DEPLOYED_CONTRACT_ADDRESS> --source <YOUR_ACCOUNT_NAME> --network testnet -- get_product_by_id --product_id 1
Enter fullscreen mode Exit fullscreen mode

3. Update a Product

soroban contract invoke --id <DEPLOYED_CONTRACT_ADDRESS> --source <YOUR_ACCOUNT_NAME> --network testnet -- update_product --product_id 1 --new_name "Updated Product" --new_description "Updated description of the product." --new_price 1200
Enter fullscreen mode Exit fullscreen mode

4. Delete a Product

soroban contract invoke --id <DEPLOYED_CONTRACT_ADDRESS> --source <YOUR_ACCOUNT_NAME> --network testnet -- delete_product --product_id 1
Enter fullscreen mode Exit fullscreen mode

5. Permanently Delete a Product

soroban contract invoke --id <DEPLOYED_CONTRACT_ADDRESS> --source <YOUR_ACCOUNT_NAME> --network testnet -- delete_product_permanently --product_id 1
Enter fullscreen mode Exit fullscreen mode

6. Delete All Products

soroban contract invoke --id <DEPLOYED_CONTRACT_ADDRESS> --source <YOUR_ACCOUNT_NAME> --network testnet -- delete_all_products
Enter fullscreen mode Exit fullscreen mode

Explanation of Parameters

  • --id <DEPLOYED_CONTRACT_ADDRESS>: The address where the smart contract is deployed.
  • --source <YOUR_ACCOUNT_NAME>: The account name used to invoke the contract.
  • --network testnet: The network on which the contract is deployed (e.g., testnet).
  • <FUNCTION_NAME>: The function you want to invoke in the smart contract.
  • <FUNCTION_PARAMETER <ARGUMENT>: The parameters required by the function.

Replace placeholders with actual values to interact with your deployed contract on the Stellar testnet.

Conclusion

Congratulations! You've successfully created a Stellar smart contract that performs CRUD operations for managing products. This guide walked you through setting up a Rust project, defining the contract and product structure, and implementing each CRUD operation step by step. You can now use this knowledge to build more complex dApps on the Stellar network.

Top comments (0)