Skip to content

Commit

Permalink
Describe events in cw-tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
jawoznia committed Dec 5, 2024
1 parent 541e5cf commit c33cfd0
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/pages/tutorial/cw-contract/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
274 changes: 274 additions & 0 deletions src/pages/tutorial/cw-contract/event.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import { Tabs } from "nextra/components";

# Events attributes and data

The only way our contract can communicate to the world, for now, is by 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 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`:

<Tabs items={['Storey', 'StoragePlus']}>
<Tabs.Tab>

```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<String>,
) -> Result<Response, ContractError> {
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<Vec<_>> = 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)
}

// ...
}

// ...
```

</Tabs.Tab>
<Tabs.Tab>

```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<Response, ContractError> {
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<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,
});
}

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<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)
}

pub fn leave(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
let admins = admins
.into_iter()
.filter(|admin| *admin != info.sender)
.collect();
Ok(admins)
})?;

Ok(Response::new())
}
}

// ...
```

</Tabs.Tab>
</Tabs>

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 events structure are, which makes verifying them tricky. Also, the "wasm"
event is particularly tricky, as it contains an implied attribute - `_contract_addr` containing an
address called a 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 logic relies
on. On the other hand, it turns out it is very rarely helpful outside of contract-to-contract
communication. Data is always only one single object on the response, which is set using 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.

0 comments on commit c33cfd0

Please sign in to comment.