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"));