zilliqa_rs/contract/mod.rs
1/*!
2Interact with scilla contracts.
3
4This module provides everything you need to work with contracts.
5From deployment to call transitions and get fields.
6
7One of the coolest features of zilliqa-rs is
8generating rust code for your scilla contracts during build time.
9It means if your contract has a transition like `transfer`,
10you can call it the same as a normal rust function.
11If it has a parameter of an address, you must pass an address to this function.
12And this means all of the beauties of type-checking of rust come to working with scilla contracts.
13
14# Generating rust code from scilla contracts
15
16We want to deploy a simple contract named [HelloWorld] and call its `setHello` transition.
17
18First, we need to create a folder next to `src`. Let's call it
19`contracts`. Then we move [HelloWorld.scilla] to this folder.
20
21To let zilliqa-rs scilla-to-rust code generation know about the
22contracts path, we need to export the `CONTRACTS_PATH` environment
23variable.
24
25The simplest way is to create a `.cargo/config.toml` file
26and change it like:
27
28```toml
29[env]
30CONTRACTS_PATH = {value = "contracts", relative = true}
31```
32
33setting `relative` to **`true`** is crucial - this tells cargo that
34the `contracts` directory is relative to the crate directory.
35
36If you now build your project with `cargo build`, the `build.rs` in
37this package will parse any contracts in the `CONTRACTS_PATH` and
38generate a corresponding rust struct whose implementation will allow
39you to deploy or call those contracts.
40
41We generate three things for each contract:
42
43 * `<contract>State` - a struct to represent the state of a contract.
44 * `<contract>Init` - a struct to represent the initialisation parameters of a contract.
45 * `<contract>` - an implementation which allows you to deploy, query, or call the contract.
46
47The generated code for [HelloWorld.scilla] is something like this:
48
49```rust,ignore
50impl<T: Middleware> HelloWorld<T> {
51 pub async fn deploy(client: Arc<T> , owner: ZilAddress) -> Result<Self, Error> {
52 }
53
54 pub fn address(&self) -> &ZilAddress {
55 }
56
57 pub fn set_hello(&self , msg: String) -> RefMut<'_, transition_call::TransitionCall<T>> {
58 }
59
60 pub fn get_hello(&self ) -> RefMut<'_, transition_call::TransitionCall<T>> {
61 }
62
63 pub async fn welcome_msg(&self) -> Result<String, Error> {
64 }
65
66 pub async fn owner(&self) -> Result<ZilAddress, Error> {
67 }
68}
69```
70* The `deploy` function deploys the contract to the network. Because [HelloWorld.scilla] accepts an address, `owner`, as a deployment parameter, the `deploy` function needs that too.
71* The `address` function returns the address of the deployed contract.
72* `set_hello` corresponds to `setHello` transition in the contract. Again, because the transition accepts a string parameter, the `set_hello` function does too.
73* `get_hello` corresponds to the `getHello` transition.
74* The contract has a field named, `welcome_msg`, to get the value of this field, the `welcome_msg` function should be called.
75* The contract has an immutable state named, `owner` and we passed the value during deployment. To get the value of the owner, we need to call `owner`
76
77All contracts will have the functions:
78
79* `deploy` - to deploy the contract.
80* `address` - to retrieve the contract's address once deployed.
81* `new` - to create an instance of the contract object for deployment.
82* `get_state` - to retrieve the contract state (modelled as a `..State` struct).
83
84For details, you can run `cargo doc` and then look at the generated documentation.
85
86# Deploying the contract
87
88Here is the code example to deploy [HelloWorld]:
89```
90use std::sync::Arc;
91
92use zilliqa_rs::{
93 contract,
94 providers::{Http, Provider},
95 signers::LocalWallet,
96};
97
98#[tokio::main]
99async fn main() -> anyhow::Result<()> {
100 const END_POINT: &str = "http://localhost:5555";
101
102 let wallet = "d96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba".parse::<LocalWallet>()?;
103
104 let provider = Provider::<Http>::try_from(END_POINT)?
105 .with_chain_id(222)
106 .with_signer(wallet.clone());
107
108 let contract = contract::HelloWorld::deploy(Arc::new(provider), wallet.address.clone()).await?;
109
110 Ok(())
111}
112```
113
114Instead of `deploy`, you can use `deploy_compressed` if you like to deploy a compressed version of the contract.
115
116Alternatively, If the contract is already deployed and you have its address, it's possible to create a new instance of the target contract by calling `attach` function:
117```
118use std::sync::Arc;
119
120use zilliqa_rs::{
121 contract,
122 providers::{Http, Provider},
123 signers::LocalWallet,
124};
125
126#[tokio::main]
127async fn main() -> anyhow::Result<()> {
128 const END_POINT: &str = "http://localhost:5555";
129
130 let wallet = "d96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba".parse::<LocalWallet>()?;
131
132 let provider = Arc::new(Provider::<Http>::try_from(END_POINT)?
133 .with_chain_id(222)
134 .with_signer(wallet.clone()));
135
136 let contract = contract::HelloWorld::deploy(provider.clone(), wallet.address.clone()).await?;
137
138 // Create a new instance by using the address of a deployed contract.
139 let contract2 = contract::HelloWorld::attach(contract.address().clone(), provider.clone());
140
141 Ok(())
142}
143```
144In the above code, we first deploy [HelloWorld] as before, then we use its address to create a new instance of [HelloWorld].
145
146Instead of using rust binding, it's possible to use [ContractFactory::deploy_from_file] or [ContractFactory::deploy_str]
147functions to deploy a contract manually.
148
149# Calling a transition
150
151The [HelloWorld] contract has a `setHello` transition. It can be called in rust like:
152
153 ```
154 use std::sync::Arc;
155
156 use zilliqa_rs::{
157 contract,
158 core::BNum,
159 providers::{Http, Provider},
160 signers::LocalWallet,
161 };
162
163 #[tokio::main]
164 async fn main() -> anyhow::Result<()> {
165 const END_POINT: &str = "http://localhost:5555";
166
167 let wallet = "d96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba".parse::<LocalWallet>()?;
168
169 let provider = Provider::<Http>::try_from(END_POINT)?
170 .with_chain_id(222)
171 .with_signer(wallet.clone());
172
173 let contract = contract::HelloWorld::deploy(Arc::new(provider), wallet.address.clone()).await?;
174 contract.set_hello("Salaam".to_string()).call().await?;
175
176 Ok(())
177 }
178 ```
179 If a transition needs some parameters, like here, You must pass them too, otherwise you won't be able to compile the code.
180
181## Calling a transaction with custom parameters for nonce, amount, etc.
182
183It's possible to override default transaction parameters such as nonce and amount. Here we call `accept_zil` transition of
184`SendZil` contract:
185
186 ```
187 use zilliqa_rs::{contract, middlewares::Middleware, core::parse_zil, signers::LocalWallet, providers::{Http, Provider}};
188 use std::sync::Arc;
189
190 #[tokio::main]
191 async fn main() -> anyhow::Result<()> {
192 const END_POINT: &str = "http://localhost:5555";
193
194 let wallet = "e53d1c3edaffc7a7bab5418eb836cf75819a82872b4a1a0f1c7fcf5c3e020b89".parse::<LocalWallet>()?;
195
196 let provider = Arc::new(Provider::<Http>::try_from(END_POINT)?
197 .with_chain_id(222)
198 .with_signer(wallet.clone()));
199
200 let contract = contract::SendZil::deploy(provider.clone()).await?;
201 // Override the amount before sending the transaction.
202 contract.accept_zil().amount(parse_zil("0.5")?).call().await?;
203 assert_eq!(provider.get_balance(contract.address()).await?.balance, parse_zil("0.5")?);
204 Ok(())
205 }
206 ```
207
208 It's possible to call a transition without using rust binding.
209 You need to call [BaseContract::call] and provide needed parameters for the transition.
210
211## Getting the contract's state
212
213The [HelloWorld] contract has a `welcome_msg` field.
214
215You can get the latest value of this field by calling `welcome_msg` function:
216
217```rust
218use std::sync::Arc;
219
220use zilliqa_rs::{
221 contract,
222 providers::{Http, Provider},
223 signers::LocalWallet,
224};
225#[tokio::main]
226async fn main() -> anyhow::Result<()> {
227 const END_POINT: &str = "http://localhost:5555";
228
229 let wallet = "d96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba".parse::<LocalWallet>()?;
230
231 let provider = Provider::<Http>::try_from(END_POINT)?
232 .with_chain_id(222)
233 .with_signer(wallet.clone());
234
235 let contract = contract::HelloWorld::deploy(Arc::new(provider), wallet.address.clone()).await?;
236
237 let hello = contract.welcome_msg().await?;
238 assert_eq!(hello, "Hello world!".to_string());
239
240 contract.set_hello("Salaam".to_string()).call().await?;
241 let hello = contract.welcome_msg().await?;
242 assert_eq!(hello, "Salaam".to_string());
243 Ok(())
244}
245```
246
247[HelloWorld]: https://github.com/Zilliqa/zilliqa-rs/blob/master/tests/contracts/HelloWorld.scilla
248[HelloWorld.scilla]: https://github.com/Zilliqa/zilliqa-rs/blob/master/tests/contracts/HelloWorld.scilla
249[SendZil]: https://github.com/Zilliqa/zilliqa-rs/blob/master/tests/contracts/SendZil.scilla
250*/
251
252pub mod factory;
253pub mod scilla_value;
254pub mod transition_call;
255use std::{ops::Deref, str::FromStr, sync::Arc};
256
257pub use factory::Factory as ContractFactory;
258use regex::Regex;
259pub use scilla_value::*;
260use serde::{de::DeserializeOwned, Deserialize, Serialize};
261use serde_json::Value as JsonValue;
262pub use transition_call::*;
263
264use crate::core::{GetTransactionResponse, ZilAddress};
265use crate::signers::Signer;
266use crate::{middlewares::Middleware, transaction::TransactionParams, Error};
267
268#[derive(Debug)]
269pub struct BaseContract<T: Middleware> {
270 address: ZilAddress,
271 client: Arc<T>,
272}
273
274#[derive(Debug, Serialize, Deserialize)]
275pub struct Init(pub Vec<ScillaVariable>);
276
277impl Deref for Init {
278 type Target = Vec<ScillaVariable>;
279
280 fn deref(&self) -> &Self::Target {
281 &self.0
282 }
283}
284
285#[derive(Debug, Serialize)]
286struct Transition {
287 #[serde(rename = "_tag")]
288 tag: String,
289 params: Vec<ScillaVariable>,
290}
291
292impl<T: Middleware> BaseContract<T> {
293 pub fn new(address: ZilAddress, client: Arc<T>) -> Self {
294 Self { address, client }
295 }
296
297 pub fn connect<S: Signer>(&self, client: Arc<T>) -> Self {
298 Self {
299 address: self.address.clone(),
300 client,
301 }
302 }
303
304 /// Call a transition of the contract.
305 ///
306 /// Arguments:
307 ///
308 /// * `transition`: A string representing the name of the transition to be called.
309 /// * `args`: A vector of ScillaVariable objects, which represents the arguments to be passed to the
310 /// transition being called.
311 /// * `overridden_params`: An optional parameter that allows you to override the default transaction
312 /// parameters. If not provided, it will use the default transaction parameters.
313 pub async fn call(
314 &self,
315 transition: &str,
316 args: Vec<ScillaVariable>,
317 overridden_params: Option<TransactionParams>,
318 ) -> Result<GetTransactionResponse, Error> {
319 TransitionCall::new(transition, &self.address, self.client.clone())
320 .overridden_params(overridden_params.unwrap_or_default())
321 .args(args)
322 .call()
323 .await
324 }
325
326 /// The function `get_field` retrieves a specific field from a smart contract state and parses it into a
327 /// specified type.
328 ///
329 /// Arguments:
330 ///
331 /// * `field_name`: The `field_name` parameter is a string that represents the name of the field you
332 /// want to retrieve from the smart contract state.
333 pub async fn get_field<F: FromStr>(&self, field_name: &str) -> Result<F, Error> {
334 let state = self.client.get_smart_contract_state(&self.address).await?;
335 if let JsonValue::Object(object) = state {
336 if let Some(value) = object.get(field_name) {
337 return value
338 .to_string()
339 .parse::<F>()
340 .map_err(|_| Error::FailedToParseContractField(field_name.to_string()));
341 }
342 }
343 Err(Error::NoSuchFieldInContractState(field_name.to_string()))
344 }
345
346 /// The function `get_init` retrieves the initialization parameters of a smart contract.
347 pub async fn get_init(&self) -> Result<Vec<ScillaVariable>, Error> {
348 self.client.get_smart_contract_init(&self.address).await
349 }
350
351 /// The function `get_state` retrieves the state of a smart contract asynchronously.
352 pub async fn get_state<S: Send + DeserializeOwned>(&self) -> Result<S, Error> {
353 self.client.get_smart_contract_state(&self.address).await
354 }
355}
356
357pub fn compress_contract(code: &str) -> Result<String, Error> {
358 let remove_comments_regex = Regex::new(r"\(\*.*?\*\)")?;
359 let replace_whitespace_regex = Regex::new(r"(?m)(^[ \t]*\r?\n)|([ \t]+$)")?;
360 let code = remove_comments_regex.replace_all(code, "");
361 let code = replace_whitespace_regex.replace_all(&code, "").to_string();
362 Ok(code)
363}
364
365#[cfg(test)]
366mod tests {
367 use crate::contract::compress_contract;
368
369 #[test]
370 fn compression_1_works() {
371 let code = r#"(***************************************************)
372(* The contract definition *)
373(***************************************************)
374contract HelloWorld
375(owner: ByStr20)"#;
376 let compressed = compress_contract(code).unwrap();
377 assert_eq!(
378 &compressed,
379 r#"contract HelloWorld
380(owner: ByStr20)"#
381 );
382 }
383
384 #[test]
385 fn compression_2_works() {
386 let code = r#"(*something*)contract HelloWorld
387(owner: ByStr20)"#;
388 let compressed = compress_contract(code).unwrap();
389 assert_eq!(
390 &compressed,
391 r#"contract HelloWorld
392(owner: ByStr20)"#
393 );
394 }
395
396 #[test]
397 fn compression_3_works() {
398 let code = r#"contract HelloWorld (* a dummy comment*)
399(owner: ByStr20)"#;
400 let compressed = compress_contract(code).unwrap();
401 assert_eq!(
402 &compressed,
403 r#"contract HelloWorld
404(owner: ByStr20)"#
405 );
406 }
407
408 #[test]
409 fn compression_4_works() {
410 let code = r#"contract WithComment (*contract name*)
411()
412(*fields*)
413field welcome_msg : String = "" (*welcome*) (*another comment*) "#;
414 let compressed = compress_contract(code).unwrap();
415 assert_eq!(
416 &compressed,
417 r#"contract WithComment
418()
419field welcome_msg : String = """#
420 );
421 }
422}
423
424/*
425
426 it("#4", async function () {
427 const code = `contract WithComment (*contract name*)
428()
429(*fields*)
430field welcome_msg : String = "" (*welcome*) (*another comment*) `;
431 const compressed = compressContract(code);
432 expect(compressed).to.be.eq(`contract WithComment
433()
434field welcome_msg : String = ""`);
435 });
436});
437 */
438
439include!(concat!(env!("OUT_DIR"), "/scilla_contracts.rs"));