diff --git a/src/pages/tutorial/cw-contract/_meta.json b/src/pages/tutorial/cw-contract/_meta.json index 853fd7af..7cf0306b 100644 --- a/src/pages/tutorial/cw-contract/_meta.json +++ b/src/pages/tutorial/cw-contract/_meta.json @@ -6,5 +6,6 @@ "testing-query": "Testing the query", "multitest-introduction": "Multitest introduction", "state": "Storing state", - "execution": "Execution messages" + "execution": "Execution messages", + "event": "Passing events" } diff --git a/src/pages/tutorial/cw-contract/event.mdx b/src/pages/tutorial/cw-contract/event.mdx new file mode 100644 index 00000000..da5d963d --- /dev/null +++ b/src/pages/tutorial/cw-contract/event.mdx @@ -0,0 +1,275 @@ +import { Tabs } from "nextra/components"; + +# Events attributes and data + +The only way our contract can communicate with the world, for now, is through queries. Smart +contracts are passive - they cannot invoke any action by themselves. They can do it only as a +reaction to a call. But if you've ever tried playing with +[`wasmd`](https://github.com/CosmWasm/wasmd), you know that execution on the blockchain can return +some metadata. + +There are two things the contract can return to the caller: events and data. Events are something +produced by almost every real-life smart contract. In contrast, data is rarely used, designed for +contract-to-contract communication. + +## Returning events + +As an example, we would add an event `admin_added` emitted by our contract on the execution of +`AddMembers`: + + + + +```rust {4, 29-35, 45} copy filename="src/contract.rs" +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg}; +use crate::state::ADMINS; +use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cw_storey::CwStorage; + +// ... + +mod exec { + use cosmwasm_std::Event; + + use super::*; + + pub fn add_members( + deps: DepsMut, + info: MessageInfo, + admins: Vec, + ) -> Result { + let mut cw_storage = CwStorage(deps.storage); + + // Consider proper error handling instead of `unwrap`. + let mut curr_admins = ADMINS.access(&cw_storage).get()?.unwrap(); + if !curr_admins.contains(&info.sender) { + return Err(ContractError::Unauthorized { + sender: info.sender, + }); + } + + let events = admins + .iter() + .map(|admin| Event::new("admin_added").add_attribute("addr", admin)); + let resp = Response::new() + .add_events(events) + .add_attribute("action", "add_members") + .add_attribute("added_count", admins.len().to_string()); + + let admins: StdResult> = admins + .into_iter() + .map(|addr| deps.api.addr_validate(&addr)) + .collect(); + + curr_admins.append(&mut admins?); + ADMINS.access(&mut cw_storage).set(&curr_admins)?; + + Ok(resp) + } + + // ... +} + +// ... +``` + + + + +```rust {4, 37-43, 53} filename="src/contract.rs" +use crate::error::ContractError; +use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg}; +use crate::state::ADMINS; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + +// ... + +pub fn execute( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + use ExecuteMsg::*; + + match msg { + AddMembers { admins } => exec::add_members(deps, info, admins), + Leave {} => exec::leave(deps, info), + } +} + +mod exec { + use super::*; + + pub fn add_members( + deps: DepsMut, + info: MessageInfo, + admins: Vec, + ) -> Result { + let mut curr_admins = ADMINS.load(deps.storage)?; + if !curr_admins.contains(&info.sender) { + return Err(ContractError::Unauthorized { + sender: info.sender, + }); + } + + let events = admins + .iter() + .map(|admin| Event::new("admin_added").add_attribute("addr", admin)); + let resp = Response::new() + .add_events(events) + .add_attribute("action", "add_members") + .add_attribute("added_count", admins.len().to_string()); + + let admins: StdResult> = admins + .into_iter() + .map(|addr| deps.api.addr_validate(&addr)) + .collect(); + + curr_admins.append(&mut admins?); + ADMINS.save(deps.storage, &curr_admins)?; + + Ok(resp) + } + + pub fn leave(deps: DepsMut, info: MessageInfo) -> Result { + ADMINS.update(deps.storage, move |admins| -> StdResult<_> { + let admins = admins + .into_iter() + .filter(|admin| *admin != info.sender) + .collect(); + Ok(admins) + })?; + + Ok(Response::new()) + } +} + +// ... +``` + + + + +An event is built from two things: an event type provided in the +[`new`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Event.html#method.new) function and +attributes. Attributes are added to an event with the +[`add_attributes`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Event.html#method.add_attributes) +or the +[`add_attribute`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Event.html#method.add_attribute) +call. Attributes are key-value pairs. Because an event cannot contain any list, to achieve reporting +multiple similar actions taking place, we need to emit multiple small events instead of a collective +one. + +Events are emitted by adding them to the response with +[`add_event`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Response.html#method.add_event) +or +[`add_events`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Response.html#method.add_events) +call. Additionally, there is a possibility to add attributes directly to the response. It is just +sugar. By default, every execution emits a standard "wasm" event. Adding attributes to the result +adds them to the default event. + +We can check if events are properly emitted by contract. It is not always done, as it is much of +boilerplate in test, but events are, generally, more like logs - not necessarily considered main +contract logic. Let's now write single test checking if execution emits events: + +```rust {43-76} filename="src/contract.rs" +#[cfg(test)] +mod tests { + use cw_multi_test::{App, ContractWrapper, Executor, IntoAddr}; + + use crate::msg::AdminsListResp; + + use super::*; + + // ... + + #[test] + 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 owner = "owner".into_addr(); + + let addr = app + .instantiate_contract( + code_id, + owner.clone(), + &InstantiateMsg { + admins: vec![owner.to_string()], + }, + &[], + "Contract", + None, + ) + .unwrap(); + + let resp = app + .execute_contract( + owner.clone(), + addr, + &ExecuteMsg::AddMembers { + admins: vec![owner.to_string()], + }, + &[], + ) + .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, + owner.to_string() + ); + } +} +``` + +As you can see, testing events on a simple test made it clunky. First of all, every event is heavily +string-based - a lack of type control makes writing such tests difficult. Also, event types are +prefixed with "wasm-" - it may not be a huge problem, but it doesn't clarify verification. But the +problem is how layered the structure of events is, which makes verifying them tricky. Also, the +"wasm" event is particularly tricky, as it contains an implied attribute - `_contract_addr` +containing the address that called the contract. My general rule is - do not test emitted events +unless some logic depends on them. + +## Data + +Besides events, any smart contract execution may produce a `data` object. In contrast to events, +`data` can be structured. It makes it a way better choice to perform any communication the logic +relies on. On the other hand, it turns out it is very rarely helpful outside of contract-to-contract +communication. Data is always a singular object within the response, and it's set with the +[`set_data`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Response.html#method.set_data) +function. Because of its low usefulness in a single contract environment, we will not spend time on +it right now - an example of it will be covered later when contract-to-contract communication will +be discussed. Until then, it is just helpful to know such an entity exists.