DEV Community

Cover image for Cowchain Farm: a Dapp built with Soroban and Flutter
Hasto
Hasto

Posted on • Updated on

Cowchain Farm: a Dapp built with Soroban and Flutter

In this article, I will show you the steps in creating Cowchain Farm, a smart contract app built using Soroban on Stellar blockchain and Flutter as the front-end framework.

You can find the source code for the Dapp on the GitHub link below:

You watch the Web Demo & Code Walkthrough video on the Youtube link below:

You can also try the live application on Cowchain Farm.

Ensure that you already have a Stellar FUTURENET account and have the Freighter extension installed and enabled in your browser.

Also, make sure the Experimental Mode is enabled in the Freighter extension settings.


Install Rust and Soroban CLI

First, we have to install Rust. You can follow the steps to install Rust in the following article:

Next we install Soroban CLI:

cargo install --locked --version 20.0.0-rc1 soroban-cli
Enter fullscreen mode Exit fullscreen mode

OPTIONAL
If you want to write the smart contract using Rust nightly channel, you can install it using:

rustup toolchain install nightly
Enter fullscreen mode Exit fullscreen mode

Then use the nightly channel by default with:

rustup default nightly
Enter fullscreen mode Exit fullscreen mode

Setup New Rust Project

Create a new rust project, then open it with your favorite IDE:

cargo new cowchain-farm-soroban --lib
Enter fullscreen mode Exit fullscreen mode

Before we start writing a smart contract, there are 2 things we must do first on the new rust project:

  • Create a new file named rust-toolchain.toml, which contains:
[toolchain]
channel = "stable" # this value depends on your Rust channel 
targets = ["wasm32-unknown-unknown"]
components = ["rustc", "cargo", "rustfmt", "clippy", "rust-src"]
Enter fullscreen mode Exit fullscreen mode
  • Setup your Cargo.toml file with:
[package]
name = "cowchain_farm"
version = "0.1.0"
edition = "2021"
authors = <Your Info Here>

[lib]
crate-type = ["cdylib"]

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true

[dependencies]
soroban-sdk = "20.0.0-rc1"

[dev_dependencies]
soroban-sdk = { version = "20.0.0-rc1", features = ["testutils"] }
Enter fullscreen mode Exit fullscreen mode

And now we are ready to start writing our smart contracts using Soroban.


Contract Enums

There are 3 enums that we will use in our smart contract:

  • Status
  • DataKey
  • CowBreed

These enums will be used for administrative purposes, result status of function invocation, and identification of the types of cow in the contract.

For that, let's create enums.rs file in the src directory and use the code contained in this GitHub repository file as its contents.


Contract Custom Types

In our smart contract, instead of using the rust Option type, we will use Soroban custom types as the result of a function invocation.

All function invocations, whether successful or unsuccessful, return a custom types.

Custom types are also used for cow data and cow feeding statistics.

There are 7 custom types that we will use in our smart contract:

  • CowData
  • CowStatus
  • BuyCowResult
  • SellCowResult
  • CowFeedingStats
  • GetAllCowResult
  • CowAppraisalResult

Let's create types.rs file in the src directory and use the code contained in this GitHub repository file as its contents.


Contract Constants

There are several types of constants that we use in this contract, including:

  • Amount of ledgers for a certain period
  • Cow prices
  • Reward or fine's multiplier
  • Feeding time limits

Constant of Ledger's amount for a certain period

We can use the ledger as a time reference because ledgers have an average closing time of between 5 and 6 seconds.

Therefore, we use the ledger sequence to measure time in this smart contract.

For example, assuming the closing time of the ledger is 5 seconds, then within 24 hours, there will be an accumulation of 17280 ledgers.

Thus, we can use 17280 as the time limit, which is equal to 24 hours.

Following are the time constants we use in this smart contract:

pub const LEDGER_AMOUNT_IN_24_HOURS: u32 = 17280;
pub const LEDGER_AMOUNT_IN_3_DAYS: u32 = 51840;
pub const LEDGER_AMOUNT_IN_1_WEEK: u32 = 120960;
pub const LEDGER_AMOUNT_IN_1_MONTH: u32 = 483840;
Enter fullscreen mode Exit fullscreen mode

Constant of Cow Prices

This is the price constant for each Cow breed in the XLM unit.
The constants used are:

pub const JERSEY_PRICE: i128 = 1000;
pub const LIMOUSIN_PRICE: i128 = 1000;
pub const HALLIKAR_PRICE: i128 = 1000;
pub const HEREFORD_PRICE: i128 = 5000;
pub const HOLSTEIN_PRICE: i128 = 15000;
pub const SIMMENTAL_PRICE: i128 = 15000;
Enter fullscreen mode Exit fullscreen mode

Constant for Reward or Fine's Multiplier

In this smart contract, we will get rewards or fines every time we feed the cows.

The criteria for these rewards or fines are:

  1. On-time feeding, a reward of 0.5% of the cow's purchase price
  2. Late feeding, a reward of 0.25% of the cow's purchase price
  3. Forget feeding, a fine of 1% of the cow's purchase price

However, since Soroban doesn't enable floating-point arithmetic, we must convert the reward or fine from the floating-point number to a fixed-point number format.

Our smallest number is 0.25. Therefore, we have to multiply all numbers by 100 so there are no more floating-point numbers.

Thus, we will get the rewards or fines constants as follows:

pub const ON_TIME_REWARD: i128 = 50; // 0.5%
pub const LATE_REWARD: i128 = 25; // 0.25%
pub const FORGET_FINE: i128 = 100; // 1%
pub const PRECISION_100_PERCENT: i128 = 10_000; // 100%
Enter fullscreen mode Exit fullscreen mode

Constant of Feeding Time Limits

In the paragraph above, we have mentioned several criteria, such as On-time, Late, and Forget feeding.

Here, we will discuss the time limit for each of these criteria. The time limit measurement starts from when the cow was first purchased or since the last time the cow was fed.

Full stomach, ledger 0 - 4320
On-time feeding, ledger 4320- 8640
Late feeding, ledger 8640 - 12960
Forget feeding, ledger 12960 - 12960

note: 4320 ledger is equal to 6 hours

Based on the time limit range above, the constants used are:

pub const WELL_FED: u32 = 4320;
pub const ON_TIME_FEED: u32 = 8640;
pub const LATE_FEED: u32 = 12960;
Enter fullscreen mode Exit fullscreen mode

Create constants.rs file in the src directory and use all the constants codes above as its contents.

You also can use the code contained in this GitHub repository file as the content for constants.rs file.


Contract Trait (Interface)

In writing Soroban smart contracts, you don't have to use traits, this is optional.

However, using traits will make it easier for us to read what functions are in our smart contract.

The functions that we will implement are the following:

  1. init
  2. upgrade
  3. bump_instance
  4. health_check
  5. buy_cow
  6. sell_cow
  7. feed_the_cow
  8. get_all_cow
  9. cow_appraisal

Let's create interface.rs file in the src directory and use the code contained in this GitHub repository file as its contents.


Contract Implementation

In the lib.rs file, let's use the following code as the initial code before implementing the functions in traits.

#![no_std]

use soroban_sdk::{contract, contractimpl, token, Address, BytesN, Env, String, Symbol, Vec};

use crate::constants::*;
use crate::enums::*;
use crate::interface::*;
use crate::types::*;

mod constants;
mod enums;
mod interface;
mod types;

#[contract]
pub struct CowContract;

#[contractimpl]
impl CowContractTrait for CowContract {
  // implement CowContractTrait items in interface.rs file here
}
Enter fullscreen mode Exit fullscreen mode

You can find the complete implementation of the smart contract function in the GitHub repository lib.rs file.

I will explain in depth what happens in each smart contract function.


init

fn init(env: Env, admin: Address, native_token: Address, message: String) -> Status;
Enter fullscreen mode Exit fullscreen mode

This is the first function that we should call after deploying a contract.

The purpose of this function is to store Admin and Native Token addresses to be used in other functions and extend the contract's storage instance lifetime to the next 50 weeks.

This function requires a password to ensure the identity of the contract initiator, and this is so that no one other than us can initiate it.

The use of this password can be replaced by using a deployer contract. But for this time, I chose to use a password.

Let's see what happened here:

Checking initialization password
The function will check the message/password given by the invoker.

If the message does not match the one in the contract, the function will return the TryAgain status enum.

let internal_password = String::from_slice(&env, "password");
if message.ne(&internal_password) {
    return Status::TryAgain;
}
Enter fullscreen mode Exit fullscreen mode

Checking contract initialization
Checks whether the contract is initialized.

If the contract is initialized, then the function returns the AlreadyInitialized status enum.

let is_admin_exist = env.storage().instance().has(&DataKey::Admin);
if is_admin_exist {
    return Status::AlreadyInitialized;
}
Enter fullscreen mode Exit fullscreen mode

Checking admin authorization
Check whether the admin address provided has authorization.
It will throw an error if the admin address has no authorization.

admin.require_auth();
Enter fullscreen mode Exit fullscreen mode

Saving the admin address, native token address, and initialization ledger sequence to instance storage

env.storage().instance().set(&DataKey::Admin, &admin);
env.storage()
    .instance()
    .set(&DataKey::NativeToken, &native_token);
env.storage()
    .instance()
    .set(&DataKey::InitializedLedger, &env.ledger().sequence());
Enter fullscreen mode Exit fullscreen mode

Bump instance storage lifetime to 50 weeks and return Ok

env.storage().instance().bump(LEDGER_AMOUNT_IN_50_WEEKS);
Status::Ok
Enter fullscreen mode Exit fullscreen mode

upgrade

fn upgrade(env: Env, new_wasm_hash: BytesN<32>) -> Status;
Enter fullscreen mode Exit fullscreen mode

So, each time we deploy a contract, we will get a different contract address.

In some cases, this has the potential to cause problems. Because every time we deploy a contract for a new version, we have to migrate user data from the old contract address to the new one.

This is where the upgrade function comes in handy.
We can deploy the latest contract version using the old contract address.

Let's see what happened here:

Checking contract initialization
Check the contract initialization by checking the Admin address key in the instance storage.

Returns the NotInitialized status enum if the Admin address key does not exist.

let is_admin_exist = env.storage().instance().has(&DataKey::Admin);
if !is_admin_exist {
    return Status::NotInitialized;
}
Enter fullscreen mode Exit fullscreen mode

Load the Admin address and check for its authorization

let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
Enter fullscreen mode Exit fullscreen mode

Updating the current contract using the supplied WASH Hash and return Upgraded

env.deployer().update_current_contract_wasm(new_wasm_hash);
Status::Upgraded
Enter fullscreen mode Exit fullscreen mode

bump_instance

fn bump_instance(env: Env, ledger_amount: u32) -> Status;
Enter fullscreen mode Exit fullscreen mode

Bump instance helps extend the storage instance lifetime on our contract.

Let's see what happened here:

Checking contract initialization
Check the contract initialization by checking the Admin address key in the instance storage.

Returns the NotInitialized status enum if the Admin address key does not exist.

let is_admin_exist = env.storage().instance().has(&DataKey::Admin);
if !is_admin_exist {
    return Status::NotInitialized;
}
Enter fullscreen mode Exit fullscreen mode

Load the Admin address and check for its authorization

let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
Enter fullscreen mode Exit fullscreen mode

Bump the instance storage using the supplied Ledger amount and return Bumped

env.storage().instance().bump(ledger_amount);
Status::Bumped
Enter fullscreen mode Exit fullscreen mode

health_check

fn health_check(env: Env) -> CowStatus;
Enter fullscreen mode Exit fullscreen mode

As the name implies, this function determines if our contract is still alive or has expired.

This function will return CowStatus custom types if the smart contract is alive.

CowStatus {
    status: Status::Ok,
    ledger: env.ledger().sequence(),
}
Enter fullscreen mode Exit fullscreen mode

buy_cow

fn buy_cow(
    env: Env,
    user: Address,
    cow_name: Symbol,
    cow_id: String,
    cow_breed: CowBreed,
) -> BuyCowResult;
Enter fullscreen mode Exit fullscreen mode

The buy cow function requires authorization from the user account.

This is because the user will transfer XLM tokens from the user account to the contract account to purchase the cow.

Before making any transaction, this function will do all necessary checks.

Starting from checking whether the contract has been initiated,
Or does the user have enough balance to make a purchase?

If all checks are passed, the contract will start carrying out activities such as token transfer, storing user data in persistent storage, and storing cow data in temporary storage.

Finally, the function will return the BuyCowResult struct to the function invoker.

Let's see what happened here:

Check for user authorization

user.require_auth();
Enter fullscreen mode Exit fullscreen mode

Checking contract initialization
Check the contract initialization by checking the Native token address key in the instance storage.

let is_native_token_exist = env.storage().instance().has(&DataKey::NativeToken);
if !is_native_token_exist {
    return BuyCowResult::default(env, Status::NotInitialized);
}
Enter fullscreen mode Exit fullscreen mode

Create native token Client

let native_token: Address = env.storage().instance().get(&DataKey::NativeToken).unwrap();
let native_token_client = token::Client::new(&env, &native_token);
Enter fullscreen mode Exit fullscreen mode

Check user balance
Check whether the user has enough balance to purchase the cow.

let user_native_token_balance: i128 = native_token_client.balance(&user);
let minimum_user_balance: i128 = 15_000_000;
let cow_price_in_stroops: i128 = get_cow_base_price_in_stroops(&cow_breed);
let user_balance_after_tx: i128 =
    user_native_token_balance - minimum_user_balance - cow_price_in_stroops.clone();
if user_balance_after_tx <= 0 {
    return BuyCowResult::default(env, Status::InsufficientFund);
}
Enter fullscreen mode Exit fullscreen mode

Transfer funds from the user to the supplier

native_token_client.transfer(
    &user,
    &env.current_contract_address(),
    &cow_price_in_stroops,
);
Enter fullscreen mode Exit fullscreen mode

Creating a new cow data

let new_cow_data = CowData {
    id: cow_id.clone(),
    name: cow_name,
    breed: cow_breed,
    born_ledger: env.ledger().sequence(),
    last_fed_ledger: env.ledger().sequence(),
    feeding_stats: CowFeedingStats::default(),
};
Enter fullscreen mode Exit fullscreen mode

Check ownership data
Check whether the user already has cow ownership data.

The new cow data will be added to the existing ownership list if the owner already has a cow.

let mut cow_ownership_list: Vec<String> = Vec::new(&env);
let is_owner_exist = env.storage().persistent().has(&user);
if is_owner_exist {
    let ownership_data: Vec<String> = env.storage().persistent().get(&user).unwrap();
    cow_ownership_list.append(&ownership_data);
}
cow_ownership_list.push_back(cow_id.clone());
Enter fullscreen mode Exit fullscreen mode

Saving the ownership to persistent storage and the cow data to temporary storage

env.storage().persistent().set(&user, &cow_ownership_list);
env.storage()
    .persistent()
    .bump(&user, LEDGER_AMOUNT_IN_1_WEEK);

env.storage().temporary().set(&cow_id, &new_cow_data);
env.storage()
    .temporary()
    .bump(&cow_id, LEDGER_AMOUNT_IN_24_HOURS);
Enter fullscreen mode Exit fullscreen mode

Return BuyCowResult custom types

BuyCowResult {
    status: Status::Ok,
    cow_data: new_cow_data,
    ownership: cow_ownership_list,
}
Enter fullscreen mode Exit fullscreen mode

sell_cow

fn sell_cow(env: Env, user: Address, cow_id: String) -> SellCowResult;
Enter fullscreen mode Exit fullscreen mode

Like the previous function, the sell cow function requires user account authorization.

This function also performs all necessary checks.

Especially checking whether the cow to be sold is still alive? and if the cow is old enough to be sold.

After all checks have been passed, this function will delete the cow ID from the list of user ownership in persistent storage. And also delete the cow data in the temporary storage.

Finally, the function will return a SellCowResult struct to the invoker.

Let's see what happened here:

Check for user authorization

user.require_auth();
Enter fullscreen mode Exit fullscreen mode

Checking contract initialization
Check the contract initialization by checking the Native token address key in the instance storage.

let is_native_token_exist = env.storage().instance().has(&DataKey::NativeToken);
if !is_native_token_exist {
    return SellCowResult::default(env, Status::NotInitialized);
}
Enter fullscreen mode Exit fullscreen mode

Checking if cow still alive

let is_cow_alive = env.storage().temporary().has(&cow_id);
if !is_cow_alive {
    return SellCowResult::default(env, Status::NotFound);
}
Enter fullscreen mode Exit fullscreen mode

Checking if user data still exist

let is_ownership_exist = env.storage().persistent().has(&user);
if !is_ownership_exist {
    return SellCowResult::default(env, Status::MissingOwnership);
}
Enter fullscreen mode Exit fullscreen mode

Checking for Cow's age
The cow to be sold must be at least 3 days old.

let cow_data: CowData = env.storage().temporary().get(&cow_id).unwrap();
let current_ledger: u32 = env.ledger().sequence();
let cow_age: u32 = current_ledger - cow_data.born_ledger;
if cow_age < LEDGER_AMOUNT_IN_3_DAYS {
    return SellCowResult::default(env, Status::Underage);
}
Enter fullscreen mode Exit fullscreen mode

Get cow selling price

let cow_base_price: i128 = get_cow_base_price_in_stroops(&cow_data.breed);
let cow_selling_price = get_cow_appraisal_price(&cow_data, cow_base_price);
Enter fullscreen mode Exit fullscreen mode

Create native token Client

let native_token: Address = env.storage().instance().get(&DataKey::NativeToken).unwrap();
let native_token_client = token::Client::new(&env, &native_token);
Enter fullscreen mode Exit fullscreen mode

Check current contract balance

let contract_native_token_balance: i128 = native_token_client.balance(&env.current_contract_address());
if contract_native_token_balance < cow_selling_price {
    return SellCowResult::default(env, Status::InsufficientFund);
}
Enter fullscreen mode Exit fullscreen mode

Transfer funds from the supplier to the user

native_token_client.transfer(&env.current_contract_address(), &user, &cow_selling_price);
Enter fullscreen mode Exit fullscreen mode

Update ownership to delete data on cows that have been sold

let mut cow_ownership_list: Vec<String> = env.storage().persistent().get(&user).unwrap();
let index = cow_ownership_list.first_index_of(&cow_id).unwrap();
cow_ownership_list.remove_unchecked(index);
Enter fullscreen mode Exit fullscreen mode

Saving the updated ownership to persistent storage and remove the cow data from temporary storage

env.storage().persistent().set(&user, &cow_ownership_list);
env.storage()
    .persistent()
    .bump(&user, LEDGER_AMOUNT_IN_1_WEEK);

env.storage().temporary().remove(&cow_id);
Enter fullscreen mode Exit fullscreen mode

Return SellCowResult custom types

SellCowResult{
    status: Status::Ok,
    ownership: cow_ownership_list,
}
Enter fullscreen mode Exit fullscreen mode

feed_the_cow

fn feed_the_cow(env: Env, user: Address, cow_id: String) -> CowStatus;
Enter fullscreen mode Exit fullscreen mode

Feed the Cow is a function to update statistical data on cow’s feeding activity and also to extend the life of cow data in temporary storage.

This statistical data will then be used to estimate the cow's selling price.

Let's see what happened here:

Checking if cow still alive

let is_cow_alive = env.storage().temporary().has(&cow_id);
if !is_cow_alive {
    return CowStatus::new(env, Status::NotFound);
}
Enter fullscreen mode Exit fullscreen mode

Checking if user data still exist

let is_ownership_exist = env.storage().persistent().has(&user);
if !is_ownership_exist {
    return CowStatus::new(env, Status::MissingOwnership);
}
Enter fullscreen mode Exit fullscreen mode

Get current cow data from temporary storage

let mut cow_data: CowData = env.storage().temporary().get(&cow_id).unwrap();
Enter fullscreen mode Exit fullscreen mode

Check feeding distance
Check whether the distance between the last feeding time and the current one is larger than 4320 ledgers or 6 hours.

let current_ledger: u32 = env.ledger().sequence();
let last_fed_ledger: u32 = cow_data.last_fed_ledger;
let feed_distance: u32 = current_ledger - last_fed_ledger;
if feed_distance <= WELL_FED {
    return CowStatus::new(env, Status::FullStomach);
}
Enter fullscreen mode Exit fullscreen mode

Calculate feeding performance
Find out whether the feeding time is on time, late, or forgotten.

let mut on_time = cow_data.feeding_stats.on_time;
let mut late = cow_data.feeding_stats.late;
let mut forget = cow_data.feeding_stats.forget;

if feed_distance > WELL_FED && feed_distance <= ON_TIME_FEED {
    on_time = on_time + 1;
}
if feed_distance > ON_TIME_FEED && feed_distance <= LATE_FEED {
    late = late + 1;
}
if feed_distance > LATE_FEED {
    forget = forget + 1;
}
Enter fullscreen mode Exit fullscreen mode

Update cow data with the new stats

cow_data.last_fed_ledger = env.ledger().sequence();
cow_data.feeding_stats = CowFeedingStats {
    on_time,
    late,
    forget,
};
Enter fullscreen mode Exit fullscreen mode

Save the updated cow data to temporary storage and bump user persistent storage

env.storage().temporary().set(&cow_id, &cow_data);
env.storage()
    .temporary()
    .bump(&cow_id, LEDGER_AMOUNT_IN_24_HOURS);

env.storage()
    .persistent()
    .bump(&user, LEDGER_AMOUNT_IN_1_WEEK);
Enter fullscreen mode Exit fullscreen mode

Return CowStatus custom types

CowStatus {
    status: Status::Ok,
    ledger: cow_data.last_fed_ledger,
}
Enter fullscreen mode Exit fullscreen mode

get_all_cow

fn get_all_cow(env: Env, user: Address) -> GetAllCowResult;
Enter fullscreen mode Exit fullscreen mode

Get all cow is a function to get all cow data owned by a user. This function is usually called when the user login to the website.

Let's see what happened here:

Check for user authorization

user.require_auth();
Enter fullscreen mode Exit fullscreen mode

Checking if user data still exist

let is_ownership_exist = env.storage().persistent().has(&user);
if !is_ownership_exist {
    return GetAllCowResult {
        status: Status::Fail,
        data: Vec::new(&env),
    };
}
Enter fullscreen mode Exit fullscreen mode

Get user ownership data

let ownership_data: Vec<String> = env.storage().persistent().get(&user).unwrap();
Enter fullscreen mode Exit fullscreen mode

Iterate the ownership data to get all user cow data

let mut cow_data_list: Vec<CowData> = Vec::new(&env);
for cow_id in ownership_data {
    let is_cow_alive = env.storage().temporary().has(&cow_id);
    if !is_cow_alive {
        continue;
    }
    let cow_data: CowData = env.storage().temporary().get(&cow_id).unwrap();
    cow_data_list.push_back(cow_data);
}
Enter fullscreen mode Exit fullscreen mode

Return GetAllCowResult custom types

GetAllCowResult {
    status: Status::Ok,
    data: cow_data_list,
}
Enter fullscreen mode Exit fullscreen mode

cow_appraisal

fn cow_appraisal(env: Env, cow_id: String) -> CowAppraisalResult;
Enter fullscreen mode Exit fullscreen mode

When we do sales activities, before calling the sell cow function, the function that is called first is cow appraisal.

This is because we need the user's approval regarding the cow's selling price.

This function will estimate the price based on the feeding performance of the cows.

A cow that is constantly fed on time will increase its price.

While cows that are late or even forget to be fed, the price is more difficult to increase and even tends to decrease.

Let's see what happened here:

Checking if cow still alive

let is_cow_alive = env.storage().temporary().has(&cow_id);
if !is_cow_alive {
    return CowAppraisalResult::default(Status::NotFound);
}
Enter fullscreen mode Exit fullscreen mode

Get cow appraisal price

let cow_data: CowData = env.storage().temporary().get(&cow_id).unwrap();
let cow_base_price: i128 = get_cow_base_price_in_stroops(&cow_data.breed);
let cow_price_appraisal = get_cow_appraisal_price(&cow_data, cow_base_price);
Enter fullscreen mode Exit fullscreen mode

Return CowAppraisalResult custom types

CowAppraisalResult {
    status: Status::Ok,
    price: cow_price_appraisal,
}
Enter fullscreen mode Exit fullscreen mode

Contract Internal Function

There are two functions that are part of this smart contract but not exposed to the public.

The two functions are:

  1. get_cow_base_price_in_stroops
  2. get_cow_appraisal_price

get_cow_base_price_in_stroops

fn get_cow_base_price_in_stroops(breed: &CowBreed) -> i128 {
  let cow_price_in_native_token = match breed {
      CowBreed::Jersey => JERSEY_PRICE,
      CowBreed::Limousin => LIMOUSIN_PRICE,
      CowBreed::Hallikar => HALLIKAR_PRICE,
      CowBreed::Hereford => HEREFORD_PRICE,
      CowBreed::Holstein => HOLSTEIN_PRICE,
      CowBreed::Simmental => SIMMENTAL_PRICE,
  };
  cow_price_in_native_token * 10_000_000
}
Enter fullscreen mode Exit fullscreen mode

This function retrieves cow price data from a constant based on the cow's breed.

The price retrieved from the constant is in XLM units.
This function will then convert the price into units of stroops before returning the results to the invoker.

This conversion is necessary because tokens in Soroban use the smallest unit, namely stroops.

get_cow_appraisal_price

fn get_cow_appraisal_price(cow_data: &CowData, cow_base_price: i128) -> i128 {
  let on_time_rewards: i128 = (cow_data.feeding_stats.on_time as i128) * ON_TIME_REWARD;
  let late_rewards: i128 = (cow_data.feeding_stats.late as i128) * LATE_REWARD;
  let forget_fines: i128 = (cow_data.feeding_stats.forget as i128) * FORGET_FINE;
  let mut rewards_fines_multiplier: i128 = on_time_rewards + late_rewards - forget_fines;

  if rewards_fines_multiplier < -PRECISION_100_PERCENT {
      rewards_fines_multiplier = -PRECISION_100_PERCENT;
  }

  let rewards_or_fines: i128 =
      (cow_base_price * rewards_fines_multiplier) / PRECISION_100_PERCENT;

  cow_base_price + rewards_or_fines
}
Enter fullscreen mode Exit fullscreen mode

This function calculates the cow appraisal price using a fixed point number.

First, the function will calculate the multiplier factor for rewards or fines based on cow feeding stats.

After that, the function will ensure that the multiplier cannot be negative.

Finally, the multiplier factor will be multiplied by the base price of the cow, then the unit will be changed to stroops and added to the cow's base price so that the cow's appraisal price is obtained.


Build and Deploy Smart Contract to Stellar FUTURENET

Build contract:

cargo build --target wasm32-unknown-unknown --release
Enter fullscreen mode Exit fullscreen mode

Deploy contract to FUTURENET:

soroban contract deploy \
--wasm target/wasm32-unknown-unknown/release/cowchain-farm-soroban.wasm \
--rpc-url https://rpc-futurenet.stellar.org:443 \
--network-passphrase 'Test SDF Future Network ; October 2022'
Enter fullscreen mode Exit fullscreen mode

After the deployment is complete, you will receive a Contract Address. Save that address to be used in calling the contract functions.

The form of Contract Address will be similar to CB7UCV29SYKUFRZNEIMKVW5XKSJCGTMBCSJFN5OJ2SSXBTPRXO42XGT8.


Calling Smart Contracts using Soroban CLI

Example for calling health_check, a function without argument:

soroban contract invoke \
--id CB7UCV29SYKUFRZNEIMKVW5XKSJCGTMBCSJFN5OJ2SSXBTPRXO42XGT8 \
--rpc-url https://rpc-futurenet.stellar.org:443 \
--network-passphrase 'Test SDF Future Network ; October 2022' \
--fee 12345678 \
-- \
health_check
Enter fullscreen mode Exit fullscreen mode

Example for calling bump_instance, a function with argument:

soroban contract invoke \
--id CB7UCV29SYKUFRZNEIMKVW5XKSJCGTMBCSJFN5OJ2SSXBTPRXO42XGT8 \
--rpc-url https://rpc-futurenet.stellar.org:443 \
--network-passphrase 'Test SDF Future Network ; October 2022' \
--fee 12345678 \
-- \
bump_instance \
--ledger_amount 1234
Enter fullscreen mode Exit fullscreen mode

For more details, you can read the README.md file in the Cowchain Farm Soroban GitHub repository to discover the prerequisites for calling smart contract functions, and how to call them using Soroban CLI.


What's Next? 🚀

Now that we've unraveled the intricacies of this extensive Soroban smart contract code, you might be wondering, 'What's the next step?'

Well, armed with this newfound knowledge, you're poised to take your blockchain journey to the next level.

Keep coding, and remember: each line of code you write is a step toward transforming ideas into reality.

Happy coding and Happy building with Soroban!

Top comments (0)