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"
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
Navigate to the project directory:
cd product-crud
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
}
Explanation:
- The
Product
struct is defined with fields such asid
,name
,description
, andprice
. 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");
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
}
}
Explanation:
- The
create_product
function increments thecount_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,
})
}
}
Explanation:
- The
get_product_by_id
function retrieves a product from storage by its ID. If the product is not found, it returns a defaultProduct
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);
}
}
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);
}
}
}
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
}
}
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 namedalice
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
- Alternte command:
cargo build --target wasm32-unknown-unknown --release
- Install Optimizer:
cargo install --locked soroban-cli --features opt
- Build an Opmize the contract:
soroban contract optimize --wasm target/wasm32-unknown-unknown/release/hello_world.wasm
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
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>
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
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
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
4. Delete a Product
soroban contract invoke --id <DEPLOYED_CONTRACT_ADDRESS> --source <YOUR_ACCOUNT_NAME> --network testnet -- delete_product --product_id 1
5. Permanently Delete a Product
soroban contract invoke --id <DEPLOYED_CONTRACT_ADDRESS> --source <YOUR_ACCOUNT_NAME> --network testnet -- delete_product_permanently --product_id 1
6. Delete All Products
soroban contract invoke --id <DEPLOYED_CONTRACT_ADDRESS> --source <YOUR_ACCOUNT_NAME> --network testnet -- delete_all_products
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)