Skip to content

Chapter 3

We have introduced macros including contract, contract methods, and call in Chapter 1 and accessing values of fields from storage in Chapter 2. In this chapter, we will put together all the knowledge and implement the bank smart contract that simulates banking operations with data stored in ParallelChain Mainnet.

Before diving into the writing of a smart contract, let's build the data struct BankAccount using the sdk_method_bindgen macro provided by Parallelchain Mainnet Smart Contract SDK.

bank_account.rs defined the BankAccount data struct which consists of four fields, first_name, last_name, account_id, and amount. All these fields will be initialized to 0 or empty upon deployment.

bank_account.rs: define data struct

use borsh::{BorshDeserialize, BorshSerialize};

use pchain_sdk::{
    storage,
};

// Note that both the serializer and deserializer macros such as Borsh need to 
// be applied to this struct for it to work.

#[derive(BorshSerialize, BorshDeserialize)]
pub struct BankAccount {
    pub first_name: String,
    pub last_name: String,
    pub account_id: String,
    pub amount: u64,
}

After defining the data struct, add the following two functions which are responsible for loading and storing the value with a given key accordingly.

Note The key to be stored is an u8 integer ordered by the index of the fields, e.g. first_name has key [0].

get_bank_account() retrieve the value of the given key using pchain_sdk::storage::get(), deserialize the result and return an Option.

set_bank_account() stores the given key-value pair in the storage using pchain_sdk::storage::set().

We are using BorshDeserialize and BorshSerialize in the above code, so remember to update the Cargo.toml by adding the borsh crate. We need the base64 crate for encoding the account_id too. Therefore, add the following two lines under the dependencies section in Cargo.toml:

  • base64 = "0.13"
  • borsh = "=0.10.2"

bank_account.rs: accessing storage

pub fn get_bank_account(key: &[u8]) -> Option<BankAccount> {
    match storage::get(key) {
        Some(raw_result) => {
            let p: Option<BankAccount> =
                match BorshDeserialize::deserialize(&mut raw_result.as_ref()) {
                    Ok(d) => Some(d),
                    Err(_) => None,
                };
            p
        }
        None => None,
    }
}

pub fn set_bank_account(key: &[u8], value: &BankAccount) {
    let mut buffer: Vec<u8> = Vec::new();
    value.serialize(&mut buffer).unwrap();
    storage::set(key, buffer.as_ref());
}

Lastly, add the impl of BankAccount which includes two methods that perform the actions of deposit and withdrawal.

bank_account.rs: impl methods

impl BankAccount {
    pub fn deposit_to_balance(&mut self, amount_to_add: u64) {
        self.amount += amount_to_add;
    }
    pub fn withdraw_from_balance(&mut self, amount_to_withdraw: u64) -> Option<u64> {
        if amount_to_withdraw <= self.amount {
            self.amount -= amount_to_withdraw;
            Some(self.amount)
        } else {
            None
        }
    }
}

After having the BankAccount struct ready, it is time to start writing the bank smart contract. use bank_account::BankAccount; allows us to use the methods defined in bank_account.rs.

The macro contract on the data struct allows loading/storing fields from/into the world state. The contract struct, MyBank, has only one field, num_of_account, indicating the number of accounts associated with this bank. As mentioned in the previous section, the key of num_of_account in the storage will be [0] according to its index.

lib.rs: define contract struct

use pchain_sdk::{
    contract, contract_methods, call, crypto
};

mod bank_account;

use bank_account::BankAccount;

#[contract]
struct MyBank {
    num_of_account: u64
}

As mentioned in Chapter 1, the macro #[contract_methods] generates entrypoint methods that can be called in transaction. We will create the first entrypoint method in MyBank impl. Firstly, we need the entrypoint method open_account() to create a brand-new account, with the specified first_name, last_name, account_id, and initial_deposit.

In open_account(), we initialize an instance of BankAccount, and store it in the storage directly by invoking bank_account::set_bank_account.

After storing the newly generated account into storage, we have to update the num_of_account. Therefore, we obtain the value of the field by doing MyBank::get_num_of_account(), like how we get the fields of our pony in Chapter 2. Similarly, store the updated value by calling MyBank::set_num_of_account().

lib.rs: open a new account

#[contract_methods]
impl MyBank {

    /// entrypoint method "open_account"
    #[call]
    fn open_account(
        first_name: String,
        last_name: String,
        account_id: String,
        initial_deposit: u64,
    ) {
        let parsed_account_id= 
        if account_id != "" {
            account_id.to_owned().as_bytes().to_vec()
        } else {

            // Generate a new account id using the base64 encoded sha256 hash
            // of the first and last name concatenated together

            let input = format!("{}{}", &first_name, &last_name).to_string().as_bytes().to_vec();
            crypto::sha256(input)
        };

        // Create a new instance of BankAccount
        let opened_bank_account = BankAccount {
            first_name: first_name.to_owned(),
            last_name: last_name.to_owned(),
            account_id:  base64::encode(parsed_account_id),
            amount: initial_deposit,
        };

        // Calling the functions from bank_account.rs
        bank_account::set_bank_account(
            &opened_bank_account.account_id.as_bytes(),
            &opened_bank_account
        );

        let initial_num_of_account = MyBank::get_num_of_account();
        MyBank::set_num_of_account(initial_num_of_account + 1);

        pchain_sdk::log(
            "bank_account: Open".as_bytes(),
            format!("Successfully opened 
            account for {}, {} 
            with account_id: {}",
            &opened_bank_account.first_name,
            &opened_bank_account.last_name,
            &opened_bank_account.account_id).as_bytes()
        );
    }
}

Now, we have successfully created a function that creates a new bank account, we should support other basic banking functionalities.

Let's start with checking account balance, users need to know how much money is left in their accounts.

By calling bank_account::get_bank_account() with a given account_id, Option<BankAccount> will be returned. If None is returned, the account does not exist; otherwise, we will be able to obtain the balance of the account by accessing the value of the field amount.

lib.rs: query account balance

#[call]
fn query_account_balance(account_id: String) {
    match bank_account::get_bank_account(account_id.as_bytes()) {
        Some(balance) => {
            pchain_sdk::log(
                format!("bank: query_account_balance").as_bytes(),
                format!(
                    "The current balance is: {}", 
                    &balance.amount
                ).as_bytes()
            );
        },
        None => {
            pchain_sdk::log(
                format!("bank: query_account_balance").as_bytes(),
                format!("No such account found").as_bytes()
            );
        }
    }
}

Lastly, finish up the functionalities of the bank by completing the implementation of withdraw_money() and deposit_money using the methods mentioned in all previous sections.

lib.rs: withdrawal and deposit

#[call]
fn withdraw_money(account_id: String, amount_to_withdraw: u64) {
    match bank_account::get_bank_account(account_id.as_bytes()) {
        Some(mut query_result) => {
            match query_result.withdraw_from_balance(amount_to_withdraw) {
                Some(balance) => {

                    // update the world state
                    bank_account::set_bank_account(account_id.as_bytes(), &query_result);

                    pchain_sdk::log(
                        format!("bank: withdraw_money").as_bytes(),
                        format!("The updated balance is: \n
                        Name: {} {}\n
                        Account Number: {}\n
                        Balance: {}", 
                        &query_result.first_name,
                        &query_result.last_name,
                        &query_result.account_id,
                        &balance).as_bytes()
                    );
                }
                None => pchain_sdk::log(
                    format!("bank: withdraw_money").as_bytes(),
                    format!("You do not have enough funds to withdraw from this account.").as_bytes()
                ),
            }
        },
        None => pchain_sdk::log(
            format!("bank: withdraw_money").as_bytes(),
            format!("No such account found").as_bytes()
        ),
    };
}
#[call]
fn deposit_money(account_id: String, amount_to_deposit: u64) {
    match bank_account::get_bank_account(account_id.as_bytes()) {
        Some(mut query_result) => {
            query_result.deposit_to_balance(amount_to_deposit);

            // update the world state
            bank_account::set_bank_account(account_id.as_bytes(), &query_result);

            pchain_sdk::log(
                format!("bank: deposit_money").as_bytes(),
                format!("The updated balance is: \nName: {} {}\nAccount Number: {}\nBalance: {}", 
                &query_result.first_name,
                &query_result.last_name,
                &query_result.account_id,
                &query_result.amount).as_bytes()
            );
        },
        None => pchain_sdk::log(
            format!("bank: query_account_balance").as_bytes(),
            format!("No such account found").as_bytes()
        ),
    };
}