Skip to content
This repository was archived by the owner on Dec 12, 2025. It is now read-only.

Commit 3a507f7

Browse files
committed
Describe events in cw-tutorial
1 parent ecfdc62 commit 3a507f7

File tree

2 files changed

+276
-1
lines changed

2 files changed

+276
-1
lines changed

src/pages/tutorial/cw-contract/_meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
"testing-query": "Testing the query",
77
"multitest-introduction": "Multitest introduction",
88
"state": "Storing state",
9-
"execution": "Execution messages"
9+
"execution": "Execution messages",
10+
"event": "Passing events"
1011
}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { Tabs } from "nextra/components";
2+
3+
# Events attributes and data
4+
5+
The only way our contract can communicate to the world, for now, is by queries. Smart contracts are
6+
passive - they cannot invoke any action by themselves. They can do it only as a reaction to a call.
7+
But if you tried playing with [`wasmd`](https://github.com/CosmWasm/wasmd), you know that execution
8+
on the blockchain can return some metadata.
9+
10+
There are two things the contract can return to the caller: events and data. Events are something
11+
produced by almost every real-life smart contract. In contrast, data is rarely used, designed for
12+
contract-to-contract communication.
13+
14+
## Returning events
15+
16+
As an example, we would add an event `admin_added` emitted by our contract on the execution of
17+
`AddMembers`:
18+
19+
<Tabs items={['Storey', 'StoragePlus']}>
20+
<Tabs.Tab>
21+
22+
```rust {4, 29-35, 45} copy filename="src/contract.rs"
23+
use crate::error::ContractError;
24+
use crate::msg::{ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
25+
use crate::state::ADMINS;
26+
use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
27+
use cw_storey::CwStorage;
28+
29+
// ...
30+
31+
mod exec {
32+
use cosmwasm_std::Event;
33+
34+
use super::*;
35+
36+
pub fn add_members(
37+
deps: DepsMut,
38+
info: MessageInfo,
39+
admins: Vec<String>,
40+
) -> Result<Response, ContractError> {
41+
let mut cw_storage = CwStorage(deps.storage);
42+
43+
// Consider proper error handling instead of `unwrap`.
44+
let mut curr_admins = ADMINS.access(&cw_storage).get()?.unwrap();
45+
if !curr_admins.contains(&info.sender) {
46+
return Err(ContractError::Unauthorized {
47+
sender: info.sender,
48+
});
49+
}
50+
51+
let events = admins
52+
.iter()
53+
.map(|admin| Event::new("admin_added").add_attribute("addr", admin));
54+
let resp = Response::new()
55+
.add_events(events)
56+
.add_attribute("action", "add_members")
57+
.add_attribute("added_count", admins.len().to_string());
58+
59+
let admins: StdResult<Vec<_>> = admins
60+
.into_iter()
61+
.map(|addr| deps.api.addr_validate(&addr))
62+
.collect();
63+
64+
curr_admins.append(&mut admins?);
65+
ADMINS.access(&mut cw_storage).set(&curr_admins)?;
66+
67+
Ok(resp)
68+
}
69+
70+
// ...
71+
}
72+
73+
// ...
74+
```
75+
76+
</Tabs.Tab>
77+
<Tabs.Tab>
78+
79+
```rust {4, 37-43, 53} filename="src/contract.rs"
80+
use crate::error::ContractError;
81+
use crate::msg::{AdminsListResp, ExecuteMsg, GreetResp, InstantiateMsg, QueryMsg};
82+
use crate::state::ADMINS;
83+
use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
84+
85+
// ...
86+
87+
pub fn execute(
88+
deps: DepsMut,
89+
_env: Env,
90+
info: MessageInfo,
91+
msg: ExecuteMsg,
92+
) -> Result<Response, ContractError> {
93+
use ExecuteMsg::*;
94+
95+
match msg {
96+
AddMembers { admins } => exec::add_members(deps, info, admins),
97+
Leave {} => exec::leave(deps, info),
98+
}
99+
}
100+
101+
mod exec {
102+
use super::*;
103+
104+
pub fn add_members(
105+
deps: DepsMut,
106+
info: MessageInfo,
107+
admins: Vec<String>,
108+
) -> Result<Response, ContractError> {
109+
let mut curr_admins = ADMINS.load(deps.storage)?;
110+
if !curr_admins.contains(&info.sender) {
111+
return Err(ContractError::Unauthorized {
112+
sender: info.sender,
113+
});
114+
}
115+
116+
let events = admins
117+
.iter()
118+
.map(|admin| Event::new("admin_added").add_attribute("addr", admin));
119+
let resp = Response::new()
120+
.add_events(events)
121+
.add_attribute("action", "add_members")
122+
.add_attribute("added_count", admins.len().to_string());
123+
124+
let admins: StdResult<Vec<_>> = admins
125+
.into_iter()
126+
.map(|addr| deps.api.addr_validate(&addr))
127+
.collect();
128+
129+
curr_admins.append(&mut admins?);
130+
ADMINS.save(deps.storage, &curr_admins)?;
131+
132+
Ok(resp)
133+
}
134+
135+
pub fn leave(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
136+
ADMINS.update(deps.storage, move |admins| -> StdResult<_> {
137+
let admins = admins
138+
.into_iter()
139+
.filter(|admin| *admin != info.sender)
140+
.collect();
141+
Ok(admins)
142+
})?;
143+
144+
Ok(Response::new())
145+
}
146+
}
147+
148+
// ...
149+
```
150+
151+
</Tabs.Tab>
152+
</Tabs>
153+
154+
An event is built from two things: an event type provided in the
155+
[`new`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Event.html#method.new) function and
156+
attributes. Attributes are added to an event with the
157+
[`add_attributes`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Event.html#method.add_attributes)
158+
or the
159+
[`add_attribute`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Event.html#method.add_attribute)
160+
call. Attributes are key-value pairs. Because an event cannot contain any list, to achieve reporting
161+
multiple similar actions taking place, we need to emit multiple small events instead of a collective
162+
one.
163+
164+
Events are emitted by adding them to the response with
165+
[`add_event`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Response.html#method.add_event)
166+
or
167+
[`add_events`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Response.html#method.add_events)
168+
call. Additionally, there is a possibility to add attributes directly to the response. It is just
169+
sugar. By default, every execution emits a standard "wasm" event. Adding attributes to the result
170+
adds them to the default event.
171+
172+
We can check if events are properly emitted by contract. It is not always done, as it is much of
173+
boilerplate in test, but events are, generally, more like logs - not necessarily considered main
174+
contract logic. Let's now write single test checking if execution emits events:
175+
176+
```rust {43-76} filename="src/contract.rs"
177+
#[cfg(test)]
178+
mod tests {
179+
use cw_multi_test::{App, ContractWrapper, Executor, IntoAddr};
180+
181+
use crate::msg::AdminsListResp;
182+
183+
use super::*;
184+
185+
// ...
186+
187+
#[test]
188+
fn add_members() {
189+
let mut app = App::default();
190+
191+
let code = ContractWrapper::new(execute, instantiate, query);
192+
let code_id = app.store_code(Box::new(code));
193+
let owner = "owner".into_addr();
194+
195+
let addr = app
196+
.instantiate_contract(
197+
code_id,
198+
owner.clone(),
199+
&InstantiateMsg {
200+
admins: vec![owner.to_string()],
201+
},
202+
&[],
203+
"Contract",
204+
None,
205+
)
206+
.unwrap();
207+
208+
let resp = app
209+
.execute_contract(
210+
owner.clone(),
211+
addr,
212+
&ExecuteMsg::AddMembers {
213+
admins: vec![owner.to_string()],
214+
},
215+
&[],
216+
)
217+
.unwrap();
218+
219+
let wasm = resp.events.iter().find(|ev| ev.ty == "wasm").unwrap();
220+
assert_eq!(
221+
wasm.attributes
222+
.iter()
223+
.find(|attr| attr.key == "action")
224+
.unwrap()
225+
.value,
226+
"add_members"
227+
);
228+
assert_eq!(
229+
wasm.attributes
230+
.iter()
231+
.find(|attr| attr.key == "added_count")
232+
.unwrap()
233+
.value,
234+
"1"
235+
);
236+
237+
let admin_added: Vec<_> = resp
238+
.events
239+
.iter()
240+
.filter(|ev| ev.ty == "wasm-admin_added")
241+
.collect();
242+
assert_eq!(admin_added.len(), 1);
243+
244+
assert_eq!(
245+
admin_added[0]
246+
.attributes
247+
.iter()
248+
.find(|attr| attr.key == "addr")
249+
.unwrap()
250+
.value,
251+
owner.to_string()
252+
);
253+
}
254+
}
255+
```
256+
257+
As you can see, testing events on a simple test made it clunky. First of all, every event is heavily
258+
string-based - a lack of type control makes writing such tests difficult. Also, event types are
259+
prefixed with "wasm-" - it may not be a huge problem, but it doesn't clarify verification. But the
260+
problem is, how layered events structure are, which makes verifying them tricky. Also, the "wasm"
261+
event is particularly tricky, as it contains an implied attribute - `_contract_addr` containing an
262+
address called a contract. My general rule is - do not test emitted events unless some logic depends
263+
on them.
264+
265+
## Data
266+
267+
Besides events, any smart contract execution may produce a `data` object. In contrast to events,
268+
`data` can be structured. It makes it a way better choice to perform any communication logic relies
269+
on. On the other hand, it turns out it is very rarely helpful outside of contract-to-contract
270+
communication. Data is always only one single object on the response, which is set using the
271+
[`set_data`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Response.html#method.set_data)
272+
function. Because of its low usefulness in a single contract environment, we will not spend time on
273+
it right now - an example of it will be covered later when contract-to-contract communication will
274+
be discussed. Until then, it is just helpful to know such an entity exists.

0 commit comments

Comments
 (0)