+5

Phát triển smart contract với Cosmwasm

image.png

1. Cài đặt môi trường và khởi tạo dự án

1. Trước tiên, chúng ta cần cài đặt Rust:

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

2. Khởi tạo thư mục với cargo:

cargo là công cụ dùng để quản lý và cài đặt các package, tương tự như npm hay yarn của Node.js

cargo new --lib ./empty-contract

image.png

File Cargo.toml tương tự như package.json của Node.js, nơi lưu trữ thông tin các package được cài đặt.

3. Cấu hình và cài đặt package

Chúng ta thêm vào file Cargo.toml một số thông tin như sau:

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

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

[dependencies]
cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] }
  • cosmwasm-std: Thư viện chuẩn của Cosmwasm chứa những tính năng để phát triển smart contract.
  • crate-type: Mỗi package trong Rust được gọi với cái tên crate. crate-type định nghĩa cách mà crate được biên dịch và tương tác với các crate khác.

2. Entry points

Về cơ bản, khi gọi tới smart contract sẽ có 3 loại entry points:

  • instantiate: Gọi 1 lần duy nhất khi deploy contract. Tương tự như constructor của Solidity.
  • execute: Các hàm khi gọi sẽ thay đổi trạng thái. VD như gửi token, ...
  • query: Các hàm khi gọi chỉ trả về dữ liệu mà không làm thay đổi trạng thái (các hàm view trong Solidity).
  • migrate: nâng cấp hợp đồng thông minh.

Bây giờ chúng ta bắt đầu những dòng code đầu tiên, mở file src/lib.rs và triển khai đoạn code sau:

use cosmwasm_std::{
    entry_point, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

#[entry_point]
pub fn instantiate(
    _deps: DepsMut
    _env: Env,
    _info: MessageInfo,
    _msg: Empty,
) -> StdResult<Response> {
    Ok(Response::new())
}
  • _deps: DepsMut: Đối tượng giúp truy vấn và thay đổi các biến trạng thái.
  • _env: Env: Đối tượng chứa các thông tin cơ bản về block, contract và transaction:
pub struct Env {
    pub block: BlockInfo,
    pub transaction: Option<TransactionInfo>,
    pub contract: ContractInfo,
}
  • _info: MessageInfo: Đối tượng chứa thông tin về đối tượng giao dịch (tương tự msg.sendermsg.value trong Solidity).
pub struct MessageInfo {
    pub sender: Addr,
    pub funds: Vec<Coin>,
}
  • _msg: Empty: Bao gồm các tham số, ở đây Empty nghĩa là tham số truyền vào rỗng.

Như chúng ta thấy, lượng tham số và ý nghĩa của chúng có vẻ phức tạp và cồng kềnh hơn nhiều so với 1 constructor thông thường trong Solidity. Bởi vì Rust không được thiết kế để dành riêng cho việc phát triển smart contract như Solidity.

Ở các mục tiếp theo chúng ta sẽ cùng phát triển 1 smart contract nho nhỏ với các tính năng như sau:

  • admins: Danh sách các admin, có thể thêm, bỏ và truy vấn.
  • donation_demon: Loại coin mà smart contract có thể nhận donate.

3. Hàm truy vấn (query)

Thêm thư viện serde, một thư viện giúp xử lý các kiểu dữ liệu trong Rust (serializing and deserializing)

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

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

[dependencies]
cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }

[dev-dependencies]
cw-multi-test = "0.13.4"

Ta triển khai thêm file src/lib.rs như sau, hàm query đơn giản nhất sẽ trả về thông điệp "Hello world".

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo,
    Response, StdResult,
};
// import 2 hàm Deserialize và Serialize từ thư viện serde 
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
// Tạo struct với trường message kiểu String
struct QueryResp {
    message: String,
}

#[entry_point]
pub fn instantiate(
    _deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    _msg: Empty,
) -> StdResult<Response> {
    Ok(Response::new())
}

#[entry_point]
pub fn query(_deps: Deps, _env: Env, _msg: Empty) -> StdResult<Binary> {
    let resp = QueryResp {
        message: "Hello World".to_owned(), // chuyển từ kiểu &str sang String
    };
		
		// trả về kiểu nhị phân
    to_binary(&resp)
}
  • _deps: Deps khác với DepsMut của hàm instantiate, các hàm truy vấn chỉ lấy và trả về các biến trạng thái mà không được sửa đổi chúng. Cho nên sẽ là Deps thay vì DepsMut.
  • Các hàm truy vấn cũng không cần sử dụng msg.sender hay msg.value nên chúng ta bỏ _info: MessageInfo.

Nâng cấp hàm query

Như chúng ta thấy, các tham số msg đều là rỗng, chúng ta sẽ định nghĩa lại một kiểu dữ liệu của tham số msg với enum QueryMsg.

#[derive(Serialize, Deserialize)]
pub enum QueryMsg {
    Greet {},
}

#[entry_point]
pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => {
            let resp = QueryResp {
                message: "Hello World".to_owned(),
            };

            to_binary(&resp)
        }
    }
}

Cấu trúc lại cách gọi hàm query. Phụ thuộc vào msg mà hàm query sẽ trả về các kết quả khác nhau. Trong trường hợp đơn giản nhất hiện nay, chúng ta chỉ đang xử lý với QueryMsg::Greet.

#[derive(Serialize, Deserialize)]
pub struct GreetResp {
    message: String,
}

#[entry_point]
pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
		// Gọi hàm greet khi msg là QueryMsg::Greet
        Greet {} => to_binary(&query::greet()?),
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}

Cấu trúc lại source code

Hiện tại, tất cả mã nguồn đều nằm ở file src/lib.rs, chúng ta sẽ cần cấu trúc lại một chút để trông sáng sủa hơn 😄

Cấu trúc thư mục bây giờ sẽ trở thành

image.png

/* src/msg.rs
- Chứa định nghĩa các struct, enum liên quan tới msg
*/
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
// sử dụng từ khóa `pub` để các module khác có thể truy cập
pub struct GreetResp {
    pub message: String,
}

#[derive(Serialize, Deserialize)]
pub enum QueryMsg {
    Greet {},
}
/*	src/contract.rs
- chứa logic các hàm instantiate, query
*/
use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    _deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    _msg: Empty,
) -> StdResult<Response> {
    Ok(Response::new())
}

pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}
/* src/lib.rs
- Chứa các entry points, file contract.rs chỉ chứa logic và loại bỏ các macro #[entry_point].
*/

use cosmwasm_std::{
    entry_point, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

mod contract;
mod msg;

#[entry_point]
pub fn instantiate(deps: DepsMut, env: Env, info: MessageInfo, msg: Empty)
  -> StdResult<Response>
{
    contract::instantiate(deps, env, info, msg)
}

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: msg::QueryMsg)
  -> StdResult<Binary>
{
    contract::query(deps, env, msg)
}

4. Contract state

Ví dụ về hàm query ở trên mang tính minh họa mà chưa mô tả chính xác công việc của hàm truy vấn, chúng cần phải trả về các biến trạng thái của smart contract như số dư, địa chỉ ... cho người dùng.

Cập nhật file Cargo.toml

Chúng thêm thư viện storage-plus giúp xử lý, làm việc dễ dàng hơn với các biến trạng thái.

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

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

[dependencies]
cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw-storage-plus = "0.13.4"

[dev-dependencies]
cw-multi-test = "0.13.4"

Định nghĩa kiểu dữ liệu của biến

// src/state.rs
use cosmwasm_std::Addr;
use cw_storage_plus::Item;

// định nghĩa admins là 1 mảng các địa chỉ
pub const ADMINS: Item<Vec<Addr>> = Item::new("admins");

Lưu ý: Biến ADMINS có nhiệm vụ biểu diễn cấu trúc của biến trạng thái, chứ không phải là biến trạng thái. Do đó, nó ở dạng hằng số (const). Đối tượng trực tiếp làm nhiệm vụ thay đổi, tương tác với biến của smart contract là DepsMut.

Định nghĩa kiểu tham số truyền vào hàm instantiate.

// src/msg.rs
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct InstantiateMsg {
    pub admins: Vec<String>,
}

// thêm AdminsList
[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum QueryMsg {
    Greet {},
    AdminsList {},
}

Chỉnh sửa logic hàm instantiate

use crate::state::ADMINS;

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;

    Ok(Response::new())
}

Cập nhật lại file src/lib.rs

mod state;
use msg::InstantiateMsg;
// --snip--
#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    contract::instantiate(deps, env, info, msg)
}

Cập nhật lại file src/contract.rs

use crate::msg::{AdminsListResp, GreetResp, InstantiateMsg, QueryMsg};

pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
        AdminsList {} => to_binary(&query::admins_list(deps)?),
    }
}
 
mod query {
	// Lấy danh sách admins và trả về
    pub fn admins_list(deps: Deps) -> StdResult<AdminsListResp> {
        let admins = ADMINS.load(deps.storage)?;
        let resp = AdminsListResp { admins };
        Ok(resp)
    }
}

5. Hàm thực thi (Execute)

Chúng ta sẽ triển khai thêm 2 tính năng sau:

  • AddMembers: Thêm admin
  • Leave: Xóa 1 địa chỉ admin khỏi danh sách

Định nghĩa ExecuteMsg

// src/msg.rs

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum ExecuteMsg {
    AddMembers { admins: Vec<String> },
    Leave {},
}

Triển khai logic

// src/contract.rs

use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
 
#[allow(dead_code)]
pub fn execute(
    deps: DepsMut, // sử dụng DepMuts vì sẽ thay đổi trạng thái
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> StdResult<Response> {
    use ExecuteMsg::*;
		
	// phụ thuộc tham số msg mà gọi các phương thức khác nhau
    match msg {
        AddMembers { admins } => exec::add_members(deps, info, admins),
        Leave {} => exec::leave(deps, info),
    }
}

mod exec {
    use cosmwasm_std::StdError;

    use super::*;

    pub fn add_members(
        deps: DepsMut,
        info: MessageInfo,
        admins: Vec<String>,
    ) -> StdResult<Response> {
		    // load storage
        let mut curr_admins = ADMINS.load(deps.storage)?;
				
		// Chỉ admin mới có thể thêm địa chỉ khác vào danh sách admin
        if !curr_admins.contains(&info.sender) {
            return Err(StdError::generic_err("Unauthorised access"));
        }
				
        let admins: StdResult<Vec<_>> = admins
            .into_iter()
            .map(|addr| deps.api.addr_validate(&addr))
            .collect();
		
        // thêm và lưu địa chỉ mới vào
        curr_admins.append(&mut admins?);
        ADMINS.save(deps.storage, &curr_admins)?;

        Ok(Response::new())
    }

    pub fn leave(deps: DepsMut, info: MessageInfo) -> StdResult<Response> {
        
        // filter và loại bỏ địa chỉ khỏi danh sách
        ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
            let admins = admins
                .into_iter()
                .filter(|admin| *admin != info.sender)
                .collect();
            Ok(admins)
        })?;

        Ok(Response::new())
    }
}

Cập nhật thêm execute entrypoint

// src/lib.rs

use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use msg::{ExecuteMsg, InstantiateMsg, QueryMsg};

mod contract;
mod msg;
mod state;

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    contract::instantiate(deps, env, info, msg)
}

#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
    contract::execute(deps, env, info, msg)
}

#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    contract::query(deps, env, msg)
}

Xử lý lỗi

Với tính năng add_members, logic đang trả về StdError::generic_err khi địa chỉ gọi tới không phải là admin. Đây chưa phải là xử lý tối ưu nhất vì căn bản generic_err không mô tả chi tiết và cụ thể về lỗi gặp phải.

Thêm thư viện thiserror

Chúng ta cài đặt thêm thư viên thiserror để định nghĩa rõ hơn các có thể gặp phải trong smart contract.

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

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

[dependencies]
cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw-storage-plus = "0.13.4"
thiserror = "1"

[dev-dependencies]
cw-multi-test = "0.13.4"

Định nghĩa các lỗi

// src/error.rs

use cosmwasm_std::{Addr, StdError};
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    StdError(#[from] StdError),
    // message khi gặp lỗi, tương tự như require message trong Solidity
    #[error("{sender} is not contract admin")]
    Unauthorized { sender: Addr },
}

Xử lý lỗi

// src/contract.rs

use crate::error::ContractError;
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
 
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    use ExecuteMsg::*;

    match msg {
        AddMembers { admins } => exec::add_members(deps, info, admins),
        Leave {} => exec::leave(deps, info).map_err(Into::into),
    }
}

mod exec {
    use super::*;

    pub fn add_members(
        deps: DepsMut,
        info: MessageInfo,
        admins: Vec<String>,
    ) -> Result<Response, ContractError> {
        let mut curr_admins = ADMINS.load(deps.storage)?;
        if !curr_admins.contains(&info.sender) {
		    // "info.sender is not contract admin"
            return Err(ContractError::Unauthorized {
                sender: info.sender,
            });
        }

        let admins: StdResult<Vec<_>> = admins
            .into_iter()
            .map(|addr| deps.api.addr_validate(&addr))
            .collect();

        curr_admins.append(&mut admins?);
        ADMINS.save(deps.storage, &curr_admins)?;

        Ok(Response::new())
    }
}

6. Sự kiện (Event)

Event là một tính năng rất hữu ích trong Solidity, giúp theo dõi và nắm bắt được thông tin về giao dịch. Không hề thua kém, Cosmwasm cũng cung cấp 1 tính năng tương tự, thậm chí có phần mạnh hơn 😄

Event được định nghĩa trong thư viện cosmwasm-std như sau:

pub struct Attribute {
    pub key: String,
    pub value: String,
}

pub struct Event {
    pub ty: String,
    pub attributes: Vec<Attribute>,
}

Mỗi Event sẽ có tên riêng và kèm theo đó là 1 danh sách cách thuộc tính được lưu theo kiểu key-value.

Chúng ta sẽ cùng thêm sự kiện hàm add_members ngay sau đây:

// import cosmwasm_std::Event
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, StdResult,
};
 
mod exec {
    pub fn add_members(
        deps: DepsMut,
        info: MessageInfo,
        admins: Vec<String>,
    ) -> Result<Response, ContractError> {
        let mut curr_admins = ADMINS.load(deps.storage)?;
        if !curr_admins.contains(&info.sender) {
            return Err(ContractError::Unauthorized {
                sender: info.sender,
            });
        }
				
		// Tạo mới event với hàm Event::new
		// Thêm thuộc tính có event với phương thức add_attribute
        let events = admins
            .iter()
            .map(|admin| Event::new("admin_added").add_attribute("addr", admin));
          
		// thêm event vào response
        let resp = Response::new()
            .add_events(events)
            .add_attribute("action", "add_members")
            .add_attribute("added_count", admins.len().to_string());

        let admins: StdResult<Vec<_>> = admins
            .into_iter()
            .map(|addr| deps.api.addr_validate(&addr))
            .collect();

        curr_admins.append(&mut admins?);
        ADMINS.save(deps.storage, &curr_admins)?;

        Ok(resp)
    }
}

7. Nhận/gửi crypto

Khi đã nhắc đến blockchain, chúng ta khó mà bỏ qua tiền điện tử (crypto). Việc smart contract nhận/gửi tiền cũng là 1 tính năng rất quan trọng. Ở phần này, chúng ta cùng triển khai tính năng Donate cho đội ngũ admin, số tiền được gửi vào smart contract sau đó sẽ được chia đều ra cho cho admin =)

Cài thêm thư viện cw-utils

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

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

[features]
library = []

[dependencies]
cosmwasm-std = { version = "1.0.0-beta8", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw-storage-plus = "0.13.4"
thiserror = "1"
schemars = "0.8.1"
cw-utils = "0.13"

Thiết lập loại coin mà smart contract sẽ nhận

Cosmos là hệ sinh thái multichain nên có thể gửi được nhiều coin khác nhau. Nhưng để đơn giản, ta định nghĩa donation_denom (atom, sei, ...) để chỉ có thể nhận donate bằng 1 loại coin.

// src/msg.rs

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct InstantiateMsg {
    pub admins: Vec<String>,
    pub donation_denom: String,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum ExecuteMsg {
    AddMembers { admins: Vec<String> },
    Leave {},
    Donate {},
}
// src/state.rs
use cosmwasm_std::Addr;
use cw_storage_plus::Item;

pub const ADMINS: Item<Vec<Addr>> = Item::new("admins");
pub const DONATION_DENOM: Item<String> = Item::new("donation_denom");
// src/contract.rs
use crate::state::{ADMINS, DONATION_DENOM};

pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admins: StdResult<Vec<_>> = msg
        .admins
        .into_iter()
        .map(|addr| deps.api.addr_validate(&addr))
        .collect();
    ADMINS.save(deps.storage, &admins?)?;
    
    // khởi tạo symbol coin mà sẽ nhận donate
    DONATION_DENOM.save(deps.storage, &msg.donation_denom)?;

    Ok(Response::new())
}

Hàm donate

use cosmwasm_std::{
    coins, to_binary, BankMsg, Binary, Deps, DepsMut, Env, Event, MessageInfo,
    Response, StdResult,
};
 
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    use ExecuteMsg::*;

    match msg {
        AddMembers { admins } => exec::add_members(deps, info, admins),
        Leave {} => exec::leave(deps, info).map_err(Into::into),
        Donate {} => exec::donate(deps, info),
    }
}

mod exec {
    pub fn donate(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
        let denom = DONATION_DENOM.load(deps.storage)?;
        let admins = ADMINS.load(deps.storage)?;
				
		// Kiểm tra xem người gửi có gửi đúng loại tiền không, và số lượng bao nhiêu ?
        let donation = cw_utils::must_pay(&info, &denom)?.u128();
				
		// Chia đều ra lượng tiền được donate cho mỗi admin
        let donation_per_admin = donation / (admins.len() as u128);
				
		// Tạo BankMsg (chuyển tiền) tới các tài khoản admin. Sau đó thêm vào response để có thể gửi tiền
        let messages = admins.into_iter().map(|admin| BankMsg::Send {
            to_address: admin.to_string(),
            amount: coins(donation_per_admin, &denom),
        });

        let resp = Response::new()
            .add_messages(messages)
            .add_attribute("action", "donate")
            .add_attribute("amount", donation.to_string())
            .add_attribute("per_admin", donation_per_admin.to_string());

        Ok(resp)
    }
}

Để rõ hơn logic donate chúng ta cùng xem qua struct BankMsg và hàm must_pay nhé

pub enum BankMsg {
    Send {
        to_address: String, // địa chỉ nhận
        amount: Vec<Coin>,	// dạng mảng, nghĩa là có thể gửi 1 lúc nhiều loại tiền
    },
    Burn {
        amount: Vec<Coin>,
    },
}

// Chỉ cho phép gửi 1 loại tiền vào, nhiều hơn hoặc ko gửi sẽ báo lỗi
pub fn one_coin(info: &MessageInfo) -> Result<Coin, PaymentError> {
    match info.funds.len() {
        0 => Err(PaymentError::NoFunds {}),
        1 => {
            let coin = &info.funds[0];
            if coin.amount.is_zero() {
                Err(PaymentError::NoFunds {})
            } else {
                Ok(coin.clone())
            }
        }
        _ => Err(PaymentError::MultipleDenoms {}),
    }
}

// Kiểm tra xem tiền gửi vào có đúng loại yêu cầu không ?
pub fn must_pay(info: &MessageInfo, denom: &str) -> Result<Uint128, PaymentError> {
    let coin = one_coin(info)?;
    if coin.denom != denom {
        Err(PaymentError::MissingDenom(denom.to_string()))
    } else {
        Ok(coin.amount)
    }
}

8. Kiểm thử (testing)

Kiểm thử là một bước không thể thiếu trong quá trình phát triển smart contract. Với Cosmwasm, chúng ta sẽ dùng trực tiếp luôn Rust để viết test cho smart contract.

cw-multi-test là thư viện hỗ trợ với test cho smart contract. Chúng ta thêm mô tả vào phần [dev-dependencies] của file Cargo.toml.

[package]
name = "contract"
version = "0.1.0"
edition = "2021"

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

[features]
library = []

[dependencies]
cosmwasm-std = { version = "1.1.4", features = ["staking"] }
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw-storage-plus = "0.15.1"
thiserror = "1"
schemars = "0.8.1"
cw-utils = "0.15.1"
cosmwasm-schema = "1.1.4"

[dev-dependencies]
cw-multi-test = "0.15.1"

Test contract state và hàm instantiation

// src/contract.rs

// thêm marco để thông báo với trình biên dịch rằng đây là logic test
[cfg(test)]
mod tests {
    use cosmwasm_std::Addr;
    use cw_multi_test::{App, ContractWrapper, Executor};

    use crate::msg::AdminsListResp;

    use super::*;

    #[test]
    fn instantiation() {
		// Tạo môi trường blockchain ảo để test
        let mut app = App::default();
		
	    // 	Nạp mã nguồn của smart contract
        let code = ContractWrapper::new(execute, instantiate, query);
        
        // wasm bytecode của contract 
        let code_id = app.store_code(Box::new(code));
				
		// deploy smart contract, trả về địa chỉ contract
        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec![],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();
		
		// Gọi query để xem danh sách admin
        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();
				
		// Kiểm tra danh sách admin coi có đúng là rỗng như là khởi tạo ban đầu không ?
        assert_eq!(resp, AdminsListResp { admins: vec![] });
		
		// Khởi tạo với admin1, admin2
        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["admin1".to_owned(), "admin2".to_owned()],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract 2",
                None,
            )
            .unwrap();

        let resp: AdminsListResp = app
            .wrap()
            .query_wasm_smart(addr, &QueryMsg::AdminsList {})
            .unwrap();
			
        assert_eq!(
            resp,
            AdminsListResp {
                admins: vec![Addr::unchecked("admin1"), Addr::unchecked("admin2")],
            }
        );
    }
}

Nhiều hàm về đoạn code mới đọc qua sẽ khá khó hiểu, chúng ta cùng lục lọi tài liệu của cw-multi-test để phân tích kỹ hơn.

ContractWrapper::new

// tham số cần truyền là các hàm exucete, instantiate, query
pub fn new(
        execute_fn: ContractFn<T1, C, E1, Q>,
        instantiate_fn: ContractFn<T2, C, E2, Q>,
        query_fn: QueryFn<T3, E3, Q>,
    ) -> Self {
        Self {
            execute_fn: Box::new(execute_fn),
            instantiate_fn: Box::new(instantiate_fn),
            query_fn: Box::new(query_fn),
            sudo_fn: None,
            reply_fn: None,
            migrate_fn: None,
        }
    }

App::store_cde

// code_id trả về ở dạng uint64
pub fn store_code(&mut self, code: Box<dyn Contract<CustomT::ExecT, CustomT::QueryT>>) -> u64 {
        self.init_modules(|router, _, _| router.wasm.store_code(code) as u64)
}

instantiate_contract

// Truyền các thông tin về smart contract để deploy, khá tương tự hàm `deployContract` trong hardhat
fn instantiate_contract<T: Serialize, U: Into<String>>(
        &mut self,
        code_id: u64,
        sender: Addr,
        init_msg: &T,
        send_funds: &[Coin],
        label: U,
        admin: Option<String>,
    ) -> AnyResult<Addr> {
        // instantiate contract
        let init_msg = to_binary(init_msg)?;
        let msg = WasmMsg::Instantiate {
            admin,
            code_id,
            msg: init_msg,
            funds: send_funds.to_vec(),
            label: label.into(),
        };
        let res = self.execute(sender, msg.into())?;
        let data = parse_instantiate_response_data(res.data.unwrap_or_default().as_slice())?;
        Ok(Addr::unchecked(data.contract_address))
    }

Test hàm query

#[test]
	fn greet_query() {
		let mut app = App::default();

		let code = ContractWrapper::new(execute, instantiate, query);
		let code_id = app.store_code(Box::new(code));

		let addr = app
			.instantiate_contract(
				code_id,
				Addr::unchecked("owner"),
				&InstantiateMsg {
					admins: vec![],
					donation_denom: "eth".to_owned(),
				},
				&[],
				"Contract",
				None,
			)
			.unwrap();

		let resp: GreetResp = app
			.wrap()
			.query_wasm_smart(addr, &QueryMsg::Greet {})
			.unwrap();

		assert_eq!(
			resp,
			GreetResp {
				message: "Hello World".to_owned()
			}
		);
	}

Test hàm exucute

	#[test]
	// test TH địa chỉ gọi ko có quyền admin=> trả về lỗi
    fn unauthorized() {
        let mut app = App::default();

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec![],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();
				
				// 
        let err = app
            .execute_contract(
                Addr::unchecked("user"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[], // send_funds: &[Coin], chúng ta ko gửi kèm theo tiền nên truyền vào rỗng
            )
            .unwrap_err();
		
		// check lỗi xem có như kỳ vọng không
        assert_eq!(
            ContractError::Unauthorized {
                sender: Addr::unchecked("user")
            },
            err.downcast().unwrap()
        );
    }

    #[test]
    // test TH thêm thành công
    fn add_members() {
        let mut app = App::default();

        let code = ContractWrapper::new(execute, instantiate, query);
        let code_id = app.store_code(Box::new(code));

        let addr = app
            .instantiate_contract(
                code_id,
                Addr::unchecked("owner"),
                &InstantiateMsg {
                    admins: vec!["owner".to_owned()],
                    donation_denom: "eth".to_owned(),
                },
                &[],
                "Contract",
                None,
            )
            .unwrap();

        let resp = app
            .execute_contract(
                Addr::unchecked("owner"),
                addr,
                &ExecuteMsg::AddMembers {
                    admins: vec!["user".to_owned()],
                },
                &[],
            )
            .unwrap();

        let wasm = resp.events.iter().find(|ev| ev.ty == "wasm").unwrap();
        assert_eq!(
            wasm.attributes
                .iter()
                .find(|attr| attr.key == "action")
                .unwrap()
                .value,
            "add_members"
        );
        assert_eq!(
            wasm.attributes
                .iter()
                .find(|attr| attr.key == "added_count")
                .unwrap()
                .value,
            "1"
        );

        let admin_added: Vec<_> = resp
            .events
            .iter()
            .filter(|ev| ev.ty == "wasm-admin_added")
            .collect();
        assert_eq!(admin_added.len(), 1);

        assert_eq!(
            admin_added[0]
                .attributes
                .iter()
                .find(|attr| attr.key == "addr")
                .unwrap()
                .value,
            "user"
        );
    }

Chạy test

chúng ta dùng cargo để chạy và kiểm tra các testcase

cargo test

image.png

Toàn bộ mã nguồn và testcase các bạn có thể xem tại đây nhé

9. Build và deploy smart contract

Chúng ta sẽ thực hành với mạng thử nghiệm của Osmosis nha 😄

Cài đặt môi trường biên dịch

# Thiết lập phiên bản rust là stable
rustup default stable

cargo version

# Nếu phiên bản cargo < 1.50.0+ thì nâng cấp lên
rustup update stable

# Cài đặt để Rust có thể biên dịch sang mã WASM được
rustup target list --installed
rustup target add wasm32-unknown-unknown

# Cài thêm 1 số thư viện cần thiết
cargo install cargo-generate --features vendored-openssl
cargo install cargo-run-script

Cài đặt môi trường Osmosis Testnet

curl -sL https://get.osmosis.zone/install > i.py && python3 i.py

image.png

Chọn 2) client.

image.png

Chọn tiếp 2) Testnet

Còn một số bước xác nhận sau, chúng ta chọn theo tùy chọn (recommend) nhé.

Kiểm tra phiên bản osmosisd

osmosisd version

Tạo ví

// passphrase tùy chọn, mn có thể nhập gì tùy thích (8 ký tự)
osmosisd keys add wallet

image.png

Vậy là chúng ta đã có tài khoản ví riêng cho mình trên testnet. Ngay sau đó, cần faucet tiền để có thể giao dịch tại https://faucet.testnet.osmosis.zone/

// Kiểm tra số dư
osmosisd query bank balances $(osmosisd keys show -a wallet)

Deploy smart contract

1. Biên dịch

# Kéo source code về
cargo generate --git https://github.com/osmosis-labs/cw-tpl-osmosis.git --name my-first-contract
cd my-first-contract
cargo wasm

Sau khi biên dịch xong, sẽ tạo ra file ls -lh target/wasm32-unknown-unknown/release/cw_tpl_osmosis.wasm. Kiểm tra bằng lệnh ls -lh thì file này tận 1.8MB MB, chúng ta sẽ cần thêm 1 bước Optimized để giảm dụng lượng file wasm thì mới có thể deploy được.

2. Optimized

sudo docker run --rm -v "$(pwd)":/code \
    --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
    --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
    cosmwasm/rust-optimizer:0.12.6

File nhị phân artifacts/cw_tpl_osmosis.wasm sẽ xuất hiện (chỉ khoảng 130Kb).

3. Đẩy code lên Osmosis Testnet chain

# store the code on chain
RES=$(osmosisd tx wasm store artifacts/cw_tpl_osmosis.wasm --from wallet --gas-prices 0.1uosmo --gas auto --gas-adjustment 1.3 -y --output json -b block)
  • osmosisd tx wasm store : upload tệp nhị phân wasm.
  • --from : Tên hoặc địa chỉ hoặc private của ví deploy.
  • --gas-prices : phí giao dịch.
  • --gas : cài đặt là "auto" để tự tính.
  • --gas-adjustment : adjustment factor to be multiplied against the estimate returned by the tx simulation.
  • -y : chọn yes với mọi form câu hỏi liên quan.
  • --output : output format, ở đây chọn là JSON.
  • -b : transaction broadcasting mode
# Linux
sudo apt-get install jq

# Macbrew install jq

# get CODE_ID
CODE_ID=$(echo $RES | jq -r '.logs[0].events[-1].attributes[0].value')echo $CODE_ID

4.Deploy

# Tham số đầu vào cho hàm instantiate 
INIT='{"count":100}'

# Trả về txhash, có thể kiểm tra tại https://testnet.ping.pub/osmosis
osmosisd tx wasm instantiate $CODE_ID "$INIT" \
    --from wallet --label "my first contract" --gas-prices 0.025uosmo --gas auto --gas-adjustment 1.3 -b block -y --no-admin
# Lấy địa chỉ contract
CONTRACT_ADDR=$(osmosisd query wasm list-contract-by-code $CODE_ID --output json | jq -r '.contracts[0]')

Tài liệu tham khảo

https://book.cosmwasm.com/

https://docs.osmosis.zone


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí