๐ Unit 2 โ CosmWasm Starter
Di akhir unit ini kamu akan:
- Paham tiga entry point CosmWasm:
instantiate,execute,query - Bisa membaca struktur file sebuah project contract
- Paham cara menyimpan state dengan
cw-storage-plus - Punya contract counter yang ter-compile dan lolos test
- โ
Unit 1 selesai โ Rust terpasang, target
wasm32-unknown-unknownsudah ditambahkan - โ
Kamu paham enum dengan data dan
Result
๐ง Model Mental CosmWasmโ
Sebelum melihat kode, pahami dulu bentuk besarnya.
Sebuah contract CosmWasm adalah program yang merespons tiga jenis panggilan:
| Entry point | Kapan dipanggil | Mengubah state? | Bayar gas? |
|---|---|---|---|
instantiate | Sekali, saat contract dibuat | โ Ya | โ Ya |
execute | Setiap kali ada aksi | โ Ya | โ Ya |
query | Saat membaca data | โ Tidak | โ Tidak |
Kalau kamu sudah melewati Jalur A, ini padanannya:
| Solidity | CosmWasm |
|---|---|
constructor | instantiate |
| function biasa | execute |
function view | query |
Konsepnya sama. Yang berbeda adalah di CosmWasm ketiganya dipisahkan secara eksplisit, dan aksi dipilih lewat pesan (enum), bukan lewat nama function langsung.
Perbedaan penting: store code dan instantiate itu terpisahโ
Di Solidity, deploy = satu langkah. Di CosmWasm ada dua langkah:
- Store โ unggah kode Wasm ke chain. Kamu mendapat
code_id - Instantiate โ buat sebuah instance dari
code_iditu. Kamu mendapat alamat contract
Satu code_id bisa di-instantiate berkali-kali menjadi banyak contract terpisah dengan state masing-masing โ mirip class dan object.
๐ Membuat Projectโ
cargo install cargo-generate
cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 1.0 --name counter-injective
cd counter-injective
Struktur yang dihasilkan:
counter-injective/
โโโ Cargo.toml # dependensi
โโโ src/
โ โโโ lib.rs # titik masuk modul
โ โโโ contract.rs # instantiate / execute / query
โ โโโ msg.rs # definisi pesan
โ โโโ state.rs # penyimpanan state
โ โโโ error.rs # tipe error khusus
โโโ examples/
โโโ schema.rs # generator JSON schema
Empat file yang akan kamu sentuh: msg.rs, state.rs, contract.rs, error.rs.
๐จ msg.rs โ Mendefinisikan Pesanโ
Ingat enum dengan data dari Unit 1? Inilah tempatnya dipakai.
use cosmwasm_schema::{cw_serde, QueryResponses};
// Pesan saat contract pertama kali dibuat
#[cw_serde]
pub struct InstantiateMsg {
pub count: i32,
}
// Semua aksi yang bisa dilakukan
#[cw_serde]
pub enum ExecuteMsg {
Increment {},
Reset { count: i32 },
}
// Semua pertanyaan yang bisa diajukan
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(GetCountResponse)]
GetCount {},
}
#[cw_serde]
pub struct GetCountResponse {
pub count: i32,
}
Setiap varian enum diterjemahkan menjadi JSON saat dipanggil dari luar:
{"increment": {}}
{"reset": {"count": 5}}
{"get_count": {}}
Perhatikan nama berubah dari Increment menjadi increment dan GetCount menjadi get_count. Ini otomatis dilakukan #[cw_serde].
Kamu akan mengetik JSON persis seperti ini di Unit 3 saat memanggil contract dari terminal โ jadi ingat bentuknya.
๐พ state.rs โ Menyimpan Dataโ
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Addr;
use cw_storage_plus::{Item, Map};
#[cw_serde]
pub struct State {
pub count: i32,
pub owner: Addr,
}
// Item = satu nilai tunggal
pub const STATE: Item<State> = Item::new("state");
// Map = pasangan kunci-nilai, padanan mapping di Solidity
pub const KONTRIBUSI: Map<&Addr, i32> = Map::new("kontribusi");
| Tipe | Untuk apa | Padanan Solidity |
|---|---|---|
Item<T> | Satu nilai | variabel state biasa |
Map<K, V> | Kunci โ nilai | mapping |
Addr bukan StringCosmWasm punya tipe Addr khusus untuk alamat. Selalu pakai itu, bukan String.
Alasannya: Addr hanya dibuat setelah alamat divalidasi. Ini mencegah alamat yang salah format masuk ke state-mu โ kesalahan yang tidak bisa diperbaiki setelah contract di-deploy.
โ๏ธ contract.rs โ Logika Utamaโ
instantiateโ
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Env,
MessageInfo, Response, StdResult,
};
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, GetCountResponse, InstantiateMsg, QueryMsg};
use crate::state::{State, STATE};
#[entry_point]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let state = State {
count: msg.count,
owner: info.sender.clone(),
};
STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("owner", info.sender)
.add_attribute("count", msg.count.to_string()))
}
Empat parameter yang selalu ada:
| Parameter | Isinya |
|---|---|
deps | Akses ke storage, API, dan querier |
env | Info blok: tinggi, waktu, alamat contract |
info | info.sender = pemanggil (padanan msg.sender), info.funds = token yang dikirim |
msg | Pesan yang dikirim pemanggil |
DepsMut vs Depsinstantiate dan execute menerima DepsMut โ bisa menulis ke storage.
query menerima Deps โ hanya bisa membaca.
Ini borrow checker Rust bekerja untukmu: secara struktural mustahil menulis state dari dalam function query. Compiler tidak akan mengizinkannya.
executeโ
#[entry_point]
pub fn execute(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Increment {} => execute_increment(deps),
ExecuteMsg::Reset { count } => execute_reset(deps, info, count),
}
}
fn execute_increment(deps: DepsMut) -> Result<Response, ContractError> {
STATE.update(deps.storage, |mut state| -> Result<_, ContractError> {
state.count += 1;
Ok(state)
})?;
Ok(Response::new().add_attribute("method", "increment"))
}
fn execute_reset(
deps: DepsMut,
info: MessageInfo,
count: i32,
) -> Result<Response, ContractError> {
STATE.update(deps.storage, |mut state| -> Result<_, ContractError> {
// Kontrol akses โ hanya pemilik
if info.sender != state.owner {
return Err(ContractError::Unauthorized {});
}
state.count = count;
Ok(state)
})?;
Ok(Response::new().add_attribute("method", "reset"))
}
Perhatikan pola match msg โ inilah kenapa Unit 1 menekankan enum dengan data.
Sama seperti Solidity: siapa pun bisa mengirim pesan apa pun ke contract-mu. Rust melindungimu dari bug memori, bukan dari logika yang salah.
Perhatikan pemeriksaan info.sender != state.owner di execute_reset. Tanpa itu, siapa pun bisa mereset counter. Setiap execute yang seharusnya terbatas butuh pemeriksaan eksplisit seperti ini.
queryโ
#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetCount {} => to_json_binary(&query_count(deps)?),
}
}
fn query_count(deps: Deps) -> StdResult<GetCountResponse> {
let state = STATE.load(deps.storage)?;
Ok(GetCountResponse { count: state.count })
}
โ error.rsโ
use cosmwasm_std::StdError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized {},
#[error("Jumlah tidak boleh nol")]
ZeroAmount {},
}
Setiap varian jadi pesan error yang bisa dibaca pengguna saat transaksi gagal. Ini padanan custom error di Solidity.
๐งช Testโ
Keunggulan besar CosmWasm: kamu bisa mengetes seluruh contract tanpa menyentuh blockchain.
Tambahkan di akhir contract.rs:
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info,
};
use cosmwasm_std::{coins, from_json};
#[test]
fn proper_initialization() {
let mut deps = mock_dependencies();
let msg = InstantiateMsg { count: 17 };
let info = mock_info("creator", &coins(1000, "inj"));
let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(0, res.messages.len());
let res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap();
let value: GetCountResponse = from_json(&res).unwrap();
assert_eq!(17, value.count);
}
#[test]
fn increment_works() {
let mut deps = mock_dependencies();
let info = mock_info("creator", &coins(2, "inj"));
instantiate(deps.as_mut(), mock_env(), info.clone(), InstantiateMsg { count: 17 }).unwrap();
execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Increment {}).unwrap();
let res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap();
let value: GetCountResponse = from_json(&res).unwrap();
assert_eq!(18, value.count);
}
#[test]
fn reset_hanya_pemilik() {
let mut deps = mock_dependencies();
let creator = mock_info("creator", &coins(2, "inj"));
instantiate(deps.as_mut(), mock_env(), creator, InstantiateMsg { count: 17 }).unwrap();
// Orang lain mencoba reset โ harus gagal
let orang_asing = mock_info("orang_asing", &coins(2, "inj"));
let res = execute(
deps.as_mut(),
mock_env(),
orang_asing,
ExecuteMsg::Reset { count: 5 },
);
assert!(res.is_err());
}
}
Jalankan:
cargo test
reset_hanya_pemilik menguji bahwa contract-mu menolak yang seharusnya ditolak.
Pemula cenderung hanya menguji jalur bahagia โ "apakah fiturnya jalan?". Yang justru menyebabkan kehilangan dana adalah jalur yang seharusnya gagal tapi ternyata tidak. Biasakan menulis test untuk keduanya.
โ Checklistโ
- Project ter-generate dari
cw-template -
cargo buildsukses -
cargo testโ semua test lolos - Kamu paham peran
msg.rs,state.rs,contract.rs,error.rs - Kamu bisa menjelaskan beda
instantiate,execute, danquery
๐ฏ Rangkumanโ
- Tiga entry point:
instantiate(sekali),execute(mengubah state),query(baca saja, gratis) - Store dan instantiate terpisah โ satu
code_idbisa jadi banyak contract - Pesan didefinisikan sebagai enum, diterjemahkan otomatis ke JSON (
Incrementโincrement) - State:
Itemuntuk nilai tunggal,Mapuntuk kunci-nilai info.senderadalah padananmsg.senderDepsMutbisa menulis,Depshanya baca โ dipaksakan compiler- Pakai
Addr, bukanString, untuk alamat - Kontrol akses tetap tanggung jawabmu โ Rust tidak melindungimu dari logika yang salah
- Tulis test untuk yang seharusnya gagal, bukan hanya jalur bahagia
โ Quick Checkโ
- Sebutkan tiga entry point CosmWasm dan padanannya di Solidity.
- Kenapa store dan instantiate dipisah?
ExecuteMsg::Reset { count: 5 }menjadi JSON seperti apa?- Kenapa function
querymustahil mengubah state? - Kenapa memakai
Addrlebih baik daripadaStringuntuk alamat?
Lanjut: Unit 3 โ Build & Deploy CosmWasm ๐