Skip to main content

๐Ÿ“ Unit 2 โ€” CosmWasm Starter

Goal Unit Ini

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
Prasyarat
  • โœ… Unit 1 selesai โ€” Rust terpasang, target wasm32-unknown-unknown sudah 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 pointKapan dipanggilMengubah state?Bayar gas?
instantiateSekali, saat contract dibuatโœ… Yaโœ… Ya
executeSetiap kali ada aksiโœ… Yaโœ… Ya
querySaat membaca dataโŒ TidakโŒ Tidak
Bandingkan dengan Solidity

Kalau kamu sudah melewati Jalur A, ini padanannya:

SolidityCosmWasm
constructorinstantiate
function biasaexecute
function viewquery

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:

  1. Store โ€” unggah kode Wasm ke chain. Kamu mendapat code_id
  2. Instantiate โ€” buat sebuah instance dari code_id itu. 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,
}
Enum menjadi JSON

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");
TipeUntuk apaPadanan Solidity
Item<T>Satu nilaivariabel state biasa
Map<K, V>Kunci โ†’ nilaimapping
Addr bukan String

CosmWasm 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:

ParameterIsinya
depsAkses ke storage, API, dan querier
envInfo blok: tinggi, waktu, alamat contract
infoinfo.sender = pemanggil (padanan msg.sender), info.funds = token yang dikirim
msgPesan yang dikirim pemanggil
DepsMut vs Deps

instantiate 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.

Kontrol akses tetap tanggung jawabmu

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
Test ketiga adalah yang paling berharga

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 build sukses
  • cargo test โ€” semua test lolos
  • Kamu paham peran msg.rs, state.rs, contract.rs, error.rs
  • Kamu bisa menjelaskan beda instantiate, execute, dan query

๐ŸŽฏ Rangkumanโ€‹

Yang Harus Kamu Ingat
  • Tiga entry point: instantiate (sekali), execute (mengubah state), query (baca saja, gratis)
  • Store dan instantiate terpisah โ€” satu code_id bisa jadi banyak contract
  • Pesan didefinisikan sebagai enum, diterjemahkan otomatis ke JSON (Increment โ†’ increment)
  • State: Item untuk nilai tunggal, Map untuk kunci-nilai
  • info.sender adalah padanan msg.sender
  • DepsMut bisa menulis, Deps hanya baca โ€” dipaksakan compiler
  • Pakai Addr, bukan String, 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โ€‹

  1. Sebutkan tiga entry point CosmWasm dan padanannya di Solidity.
  2. Kenapa store dan instantiate dipisah?
  3. ExecuteMsg::Reset { count: 5 } menjadi JSON seperti apa?
  4. Kenapa function query mustahil mengubah state?
  5. Kenapa memakai Addr lebih baik daripada String untuk alamat?

Lanjut: Unit 3 โ€” Build & Deploy CosmWasm ๐Ÿ‘‰