Phát triển smart contract với Cosmwasm
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
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àmview
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.sender
vàmsg.value
trong Solidity).
pub struct MessageInfo {
pub sender: Addr,
pub funds: Vec<Coin>,
}
_msg: Empty
: Bao gồm các tham số, ở đâyEmpty
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ớiDepsMut
của hàminstantiate
, 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
haymsg.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
/* 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 adminLeave
: 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
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
Chọn 2) client.
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
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
All rights reserved