Skip to content

Commit 1929600

Browse files
authored
Describe events in cw-tutorial (#200)
2 parents ecfdc62 + 0aaee52 commit 1929600

File tree

2 files changed

+277
-1
lines changed

2 files changed

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

0 commit comments

Comments
 (0)