Skip to content

Commit 9e7f55c

Browse files
committed
Added good practices chapter.
1 parent d6c5a8e commit 9e7f55c

File tree

1 file changed

+386
-0
lines changed

1 file changed

+386
-0
lines changed
Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
---
2+
sidebar_position: 10
3+
---
4+
5+
# Good practices
6+
7+
All the relevant basics are covered. Now let's talk about some good practices.
8+
9+
## JSON renaming
10+
11+
Due to Rust style, all our message variants are spelled in a
12+
[camel-case](https://en.wikipedia.org/wiki/CamelCase). It is standard practice, but it has a
13+
drawback - all messages are serialized and deserialized by serde using these variant names. The
14+
problem is that it is more common to use [snake case](https://en.wikipedia.org/wiki/Snake_case) for
15+
field names in the JSON world. Fortunately, there is an effortless way to tell serde to change the
16+
casing for serialization purposes. Let's update our messages with a `#[serde]` attribute:
17+
18+
```rust title="src/msg.rs" {5,12,20,26,32}
19+
use cosmwasm_std::Addr;
20+
use serde::{Deserialize, Serialize};
21+
22+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
23+
#[serde(rename_all = "snake_case")]
24+
pub struct InstantiateMsg {
25+
pub admins: Vec<String>,
26+
pub donation_denom: String,
27+
}
28+
29+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
30+
#[serde(rename_all = "snake_case")]
31+
pub enum ExecuteMsg {
32+
AddMembers { admins: Vec<String> },
33+
Leave {},
34+
Donate {},
35+
}
36+
37+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
38+
#[serde(rename_all = "snake_case")]
39+
pub struct GreetResp {
40+
pub message: String,
41+
}
42+
43+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
44+
#[serde(rename_all = "snake_case")]
45+
pub struct AdminsListResp {
46+
pub admins: Vec<Addr>,
47+
}
48+
49+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
50+
#[serde(rename_all = "snake_case")]
51+
pub enum QueryMsg {
52+
Greet {},
53+
AdminsList {},
54+
}
55+
```
56+
57+
## JSON schema
58+
59+
Talking about JSON API, it is worth mentioning JSON Schema. It is a way of defining the structure of
60+
JSON messages. It is good practice to provide a way to generate schemas for contract API. The good
61+
news is that there is a crate that would help us with that. Go to the `Cargo.toml`:
62+
63+
```toml title="Cargo.toml" {15-16}
64+
[package]
65+
name = "contract"
66+
version = "0.1.0"
67+
edition = "2021"
68+
69+
[lib]
70+
crate-type = ["cdylib", "rlib"]
71+
72+
[dependencies]
73+
cosmwasm-std = { version = "2.1.4", features = ["staking"] }
74+
serde = { version = "1.0.214", default-features = false, features = ["derive"] }
75+
cw-storey = "0.4.0"
76+
thiserror = "2.0.3"
77+
cw-utils = "2.0.0"
78+
schemars = "0.8.21"
79+
cosmwasm-schema = "2.1.4"
80+
81+
[dev-dependencies]
82+
cw-multi-test = "2.2.0"
83+
```
84+
85+
There is one additional change in this file - in
86+
[`crate-type`](https://doc.rust-lang.org/reference/linkage.html) I added "rlib". "cdylib" crates
87+
cannot be used as typical Rust dependencies. As a consequence, it is impossible to create examples
88+
for such crates.
89+
90+
Now go back to `src/msg.rs` and add a new derive for all messages:
91+
92+
```rust title="src/msg.rs" {2,5,12,20,26,32}
93+
use cosmwasm_std::Addr;
94+
use schemars::JsonSchema;
95+
use serde::{Deserialize, Serialize};
96+
97+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
98+
#[serde(rename_all = "snake_case")]
99+
pub struct InstantiateMsg {
100+
pub admins: Vec<String>,
101+
pub donation_denom: String,
102+
}
103+
104+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
105+
#[serde(rename_all = "snake_case")]
106+
pub enum ExecuteMsg {
107+
AddMembers { admins: Vec<String> },
108+
Leave {},
109+
Donate {},
110+
}
111+
112+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
113+
#[serde(rename_all = "snake_case")]
114+
pub struct GreetResp {
115+
pub message: String,
116+
}
117+
118+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
119+
#[serde(rename_all = "snake_case")]
120+
pub struct AdminsListResp {
121+
pub admins: Vec<Addr>,
122+
}
123+
124+
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, JsonSchema)]
125+
#[serde(rename_all = "snake_case")]
126+
pub enum QueryMsg {
127+
Greet {},
128+
AdminsList {},
129+
}
130+
```
131+
132+
You may argue that all those derives look slightly clunky, and I agree. Fortunately, the
133+
[`cosmwasm-schema`](https://docs.rs/cosmwasm-schema/latest/cosmwasm_schema/#) crate delivers a
134+
utility [`cw_serde`](https://docs.rs/cosmwasm-schema/latest/cosmwasm_schema/attr.cw_serde.html)
135+
macro, which we can use to reduce a boilerplate:
136+
137+
```rust title="src/msg.rs" template="empty" {2,4,10,17,22,27}
138+
use cosmwasm_std::Addr;
139+
use cosmwasm_schema::cw_serde;
140+
141+
#[cw_serde]
142+
pub struct InstantiateMsg {
143+
pub admins: Vec<String>,
144+
pub donation_denom: String,
145+
}
146+
147+
#[cw_serde]
148+
pub enum ExecuteMsg {
149+
AddMembers { admins: Vec<String> },
150+
Leave {},
151+
Donate {},
152+
}
153+
154+
#[cw_serde]
155+
pub struct GreetResp {
156+
pub message: String,
157+
}
158+
159+
#[cw_serde]
160+
pub struct AdminsListResp {
161+
pub admins: Vec<Addr>,
162+
}
163+
164+
#[cw_serde]
165+
pub enum QueryMsg {
166+
Greet {},
167+
AdminsList {},
168+
}
169+
```
170+
171+
Additionally, we have to derive the additional
172+
[`QueryResponses`](https://docs.rs/cosmwasm-schema/latest/cosmwasm_schema/derive.QueryResponses.html)
173+
trait for our query message to correlate the message variants with responses we would generate for them:
174+
175+
```rust title="src/msg.rs" {2,28,30,32}
176+
use cosmwasm_std::Addr;
177+
use cosmwasm_schema::{cw_serde, QueryResponses};
178+
179+
#[cw_serde]
180+
pub struct InstantiateMsg {
181+
pub admins: Vec<String>,
182+
pub donation_denom: String,
183+
}
184+
185+
#[cw_serde]
186+
pub enum ExecuteMsg {
187+
AddMembers { admins: Vec<String> },
188+
Leave {},
189+
Donate {},
190+
}
191+
192+
#[cw_serde]
193+
pub struct GreetResp {
194+
pub message: String,
195+
}
196+
197+
#[cw_serde]
198+
pub struct AdminsListResp {
199+
pub admins: Vec<Addr>,
200+
}
201+
202+
#[cw_serde]
203+
#[derive(QueryResponses)]
204+
pub enum QueryMsg {
205+
#[returns(GreetResp)]
206+
Greet {},
207+
#[returns(AdminsListResp)]
208+
AdminsList {},
209+
}
210+
```
211+
212+
The [`QueryResponses`](https://docs.rs/cosmwasm-schema/latest/cosmwasm_schema/derive.QueryResponses.html)
213+
is a trait that requires the `#[returns(...)]` attribute to all your query variants to generate
214+
additional information about the query-response relationship.
215+
216+
Now, we want to make the `msg` module public and accessible by crates depending on our contract (in
217+
this case, for schema example). Update your `src/lib.rs`:
218+
219+
```rust title="src/lib.rs" {5-8}
220+
use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
221+
use error::ContractError;
222+
use msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
223+
224+
pub mod contract;
225+
pub mod error;
226+
pub mod msg;
227+
pub mod state;
228+
229+
#[entry_point]
230+
pub fn instantiate(
231+
deps: DepsMut,
232+
env: Env,
233+
info: MessageInfo,
234+
msg: InstantiateMsg,
235+
) -> StdResult<Response> {
236+
contract::instantiate(deps, env, info, msg)
237+
}
238+
239+
#[entry_point]
240+
pub fn execute(
241+
deps: DepsMut,
242+
env: Env,
243+
info: MessageInfo,
244+
msg: ExecuteMsg,
245+
) -> Result<Response, ContractError> {
246+
contract::execute(deps, env, info, msg)
247+
}
248+
249+
#[entry_point]
250+
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
251+
contract::query(deps, env, msg)
252+
}
253+
```
254+
255+
I changed the visibility of all modules - as our crate can now be used as a dependency. If someone
256+
would like to do so, he may need access to handlers or state.
257+
258+
The next step is to create a tool generating actual schemas. We will do it by creating a binary in
259+
our crate. Create a new `src/bin/schema.rs` file:
260+
261+
```rust title="src/bin/schema.rs"
262+
use contract::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
263+
use cosmwasm_schema::write_api;
264+
265+
fn main() {
266+
write_api! {
267+
instantiate: InstantiateMsg,
268+
execute: ExecuteMsg,
269+
query: QueryMsg
270+
}
271+
}
272+
```
273+
274+
Cargo is smart enough to recognize files in `src/bin` directory as utility binaries for the crate.
275+
Now we can generate our schemas:
276+
277+
```shell copy title="terminal"
278+
cargo run schema
279+
```
280+
281+
We encourage you to go to the generated file to see what the schema looks like.
282+
283+
The problem is that, unfortunately, creating this binary makes our project fail to compile on the
284+
Wasm target - which is, in the end, the most important one. Fortunately, we don't need to build the
285+
schema binary for the Wasm target - let's align the `.cargo/config` file:
286+
287+
```toml
288+
[alias]
289+
wasm = "build --target wasm32-unknown-unknown --release --lib"
290+
wasm-debug = "build --target wasm32-unknown-unknown --lib"
291+
schema = "run schema"
292+
```
293+
294+
The `--lib` flag added to `wasm` cargo aliases tells the toolchain to build only the library
295+
target - it would skip building any binaries. Additionally, I added the convenience `schema` alias
296+
so that one can generate schema calling simply `cargo schema`.
297+
298+
## Disabling entry points for libraries
299+
300+
Since we added the **`rlib`** target to the contract, it is now, as previously mentioned,
301+
usable as a Rust dependency. The problem is that the contract dependent on ours would have Wasm entry points
302+
generated twice - once in the dependency and once in the final contract. We can work this around by
303+
disabling generating Wasm entry points for the contract if the crate is used as a dependency. We
304+
would use [feature flags](https://doc.rust-lang.org/cargo/reference/features.html) for that.
305+
306+
Start with updating `Cargo.toml`:
307+
308+
```toml title="Cargo.toml" {9-10}
309+
[package]
310+
name = "contract"
311+
version = "0.1.0"
312+
edition = "2021"
313+
314+
[lib]
315+
crate-type = ["cdylib", "rlib"]
316+
317+
[features]
318+
library = []
319+
320+
[dependencies]
321+
cosmwasm-std = { version = "2.1.4", features = ["staking"] }
322+
serde = { version = "1.0.214", default-features = false, features = ["derive"] }
323+
cw-storey = "0.4.0"
324+
thiserror = "2.0.3"
325+
cw-utils = "2.0.0"
326+
schemars = "0.8.21"
327+
cosmwasm-schema = "2.1.4"
328+
329+
[dev-dependencies]
330+
cw-multi-test = "2.2.0"
331+
```
332+
333+
This way, we created a new feature for our crate. Now we want to disable the `entry_point` attribute
334+
on entry points - we will do it by a slight update of `src/lib.rs`:
335+
336+
```rust title="src/lib.rs" {1,12,22,32}
337+
#[cfg(not(feature = "library"))]
338+
use cosmwasm_std::entry_point;
339+
use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
340+
use error::ContractError;
341+
use msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
342+
343+
pub mod contract;
344+
pub mod error;
345+
pub mod msg;
346+
pub mod state;
347+
348+
#[cfg_attr(not(feature = "library"), entry_point)]
349+
pub fn instantiate(
350+
deps: DepsMut,
351+
env: Env,
352+
info: MessageInfo,
353+
msg: InstantiateMsg,
354+
) -> StdResult<Response> {
355+
contract::instantiate(deps, env, info, msg)
356+
}
357+
358+
#[cfg_attr(not(feature = "library"), entry_point)]
359+
pub fn execute(
360+
deps: DepsMut,
361+
env: Env,
362+
info: MessageInfo,
363+
msg: ExecuteMsg,
364+
) -> Result<Response, ContractError> {
365+
contract::execute(deps, env, info, msg)
366+
}
367+
368+
#[cfg_attr(not(feature = "library"), entry_point)]
369+
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
370+
contract::query(deps, env, msg)
371+
}
372+
```
373+
374+
The
375+
[`cfg_attr`](https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute)
376+
attribute is a conditional compilation attribute, similar to the `cfg` we used before for the test.
377+
It expands to the given attribute if the condition expands to true. In our case - it would expand to
378+
nothing if the feature "library" is enabled, or it would expand just to `#[entry_point]` in another
379+
case.
380+
381+
Since now to add this contract as a dependency, don't forget to enable the feature like this:
382+
383+
```toml
384+
[dependencies]
385+
my_contract = { version = "0.1", features = ["library"] }
386+
```

0 commit comments

Comments
 (0)