Expand description
Interact with scilla contracts.
This module provides everything you need to work with contracts. From deployment to call transitions and get fields.
One of the coolest features of zilliqa-rs is
generating rust code for your scilla contracts during build time.
It means if your contract has a transition like transfer,
you can call it the same as a normal rust function.
If it has a parameter of an address, you must pass an address to this function.
And this means all of the beauties of type-checking of rust come to working with scilla contracts.
§Generating rust code from scilla contracts
We want to deploy a simple contract named HelloWorld and call its setHello transition.
First, we need to create a folder next to src. Let’s call it
contracts. Then we move HelloWorld.scilla to this folder.
To let zilliqa-rs scilla-to-rust code generation know about the
contracts path, we need to export the CONTRACTS_PATH environment
variable.
The simplest way is to create a .cargo/config.toml file
and change it like:
[env]
CONTRACTS_PATH = {value = "contracts", relative = true}setting relative to true is crucial - this tells cargo that
the contracts directory is relative to the crate directory.
If you now build your project with cargo build, the build.rs in
this package will parse any contracts in the CONTRACTS_PATH and
generate a corresponding rust struct whose implementation will allow
you to deploy or call those contracts.
We generate three things for each contract:
<contract>State- a struct to represent the state of a contract.<contract>Init- a struct to represent the initialisation parameters of a contract.<contract>- an implementation which allows you to deploy, query, or call the contract.
The generated code for HelloWorld.scilla is something like this:
impl<T: Middleware> HelloWorld<T> {
pub async fn deploy(client: Arc<T> , owner: ZilAddress) -> Result<Self, Error> {
}
pub fn address(&self) -> &ZilAddress {
}
pub fn set_hello(&self , msg: String) -> RefMut<'_, transition_call::TransitionCall<T>> {
}
pub fn get_hello(&self ) -> RefMut<'_, transition_call::TransitionCall<T>> {
}
pub async fn welcome_msg(&self) -> Result<String, Error> {
}
pub async fn owner(&self) -> Result<ZilAddress, Error> {
}
}- The
deployfunction deploys the contract to the network. Because HelloWorld.scilla accepts an address,owner, as a deployment parameter, thedeployfunction needs that too. - The
addressfunction returns the address of the deployed contract. set_hellocorresponds tosetHellotransition in the contract. Again, because the transition accepts a string parameter, theset_hellofunction does too.get_hellocorresponds to thegetHellotransition.- The contract has a field named,
welcome_msg, to get the value of this field, thewelcome_msgfunction should be called. - The contract has an immutable state named,
ownerand we passed the value during deployment. To get the value of the owner, we need to callowner
All contracts will have the functions:
deploy- to deploy the contract.address- to retrieve the contract’s address once deployed.new- to create an instance of the contract object for deployment.get_state- to retrieve the contract state (modelled as a..Statestruct).
For details, you can run cargo doc and then look at the generated documentation.
§Deploying the contract
Here is the code example to deploy HelloWorld:
use std::sync::Arc;
use zilliqa_rs::{
contract,
providers::{Http, Provider},
signers::LocalWallet,
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
const END_POINT: &str = "http://localhost:5555";
let wallet = "d96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba".parse::<LocalWallet>()?;
let provider = Provider::<Http>::try_from(END_POINT)?
.with_chain_id(222)
.with_signer(wallet.clone());
let contract = contract::HelloWorld::deploy(Arc::new(provider), wallet.address.clone()).await?;
Ok(())
}Instead of deploy, you can use deploy_compressed if you like to deploy a compressed version of the contract.
Alternatively, 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:
use std::sync::Arc;
use zilliqa_rs::{
contract,
providers::{Http, Provider},
signers::LocalWallet,
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
const END_POINT: &str = "http://localhost:5555";
let wallet = "d96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba".parse::<LocalWallet>()?;
let provider = Arc::new(Provider::<Http>::try_from(END_POINT)?
.with_chain_id(222)
.with_signer(wallet.clone()));
let contract = contract::HelloWorld::deploy(provider.clone(), wallet.address.clone()).await?;
// Create a new instance by using the address of a deployed contract.
let contract2 = contract::HelloWorld::attach(contract.address().clone(), provider.clone());
Ok(())
}In the above code, we first deploy HelloWorld as before, then we use its address to create a new instance of HelloWorld.
Instead of using rust binding, it’s possible to use ContractFactory::deploy_from_file or ContractFactory::deploy_str functions to deploy a contract manually.
§Calling a transition
The HelloWorld contract has a setHello transition. It can be called in rust like:
use std::sync::Arc;
use zilliqa_rs::{
contract,
core::BNum,
providers::{Http, Provider},
signers::LocalWallet,
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
const END_POINT: &str = "http://localhost:5555";
let wallet = "d96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba".parse::<LocalWallet>()?;
let provider = Provider::<Http>::try_from(END_POINT)?
.with_chain_id(222)
.with_signer(wallet.clone());
let contract = contract::HelloWorld::deploy(Arc::new(provider), wallet.address.clone()).await?;
contract.set_hello("Salaam".to_string()).call().await?;
Ok(())
}If a transition needs some parameters, like here, You must pass them too, otherwise you won’t be able to compile the code.
§Calling a transaction with custom parameters for nonce, amount, etc.
It’s possible to override default transaction parameters such as nonce and amount. Here we call accept_zil transition of
SendZil contract:
use zilliqa_rs::{contract, middlewares::Middleware, core::parse_zil, signers::LocalWallet, providers::{Http, Provider}};
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
const END_POINT: &str = "http://localhost:5555";
let wallet = "e53d1c3edaffc7a7bab5418eb836cf75819a82872b4a1a0f1c7fcf5c3e020b89".parse::<LocalWallet>()?;
let provider = Arc::new(Provider::<Http>::try_from(END_POINT)?
.with_chain_id(222)
.with_signer(wallet.clone()));
let contract = contract::SendZil::deploy(provider.clone()).await?;
// Override the amount before sending the transaction.
contract.accept_zil().amount(parse_zil("0.5")?).call().await?;
assert_eq!(provider.get_balance(contract.address()).await?.balance, parse_zil("0.5")?);
Ok(())
}It’s possible to call a transition without using rust binding. You need to call BaseContract::call and provide needed parameters for the transition.
§Getting the contract’s state
The HelloWorld contract has a welcome_msg field.
You can get the latest value of this field by calling welcome_msg function:
use std::sync::Arc;
use zilliqa_rs::{
contract,
providers::{Http, Provider},
signers::LocalWallet,
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
const END_POINT: &str = "http://localhost:5555";
let wallet = "d96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba".parse::<LocalWallet>()?;
let provider = Provider::<Http>::try_from(END_POINT)?
.with_chain_id(222)
.with_signer(wallet.clone());
let contract = contract::HelloWorld::deploy(Arc::new(provider), wallet.address.clone()).await?;
let hello = contract.welcome_msg().await?;
assert_eq!(hello, "Hello world!".to_string());
contract.set_hello("Salaam".to_string()).call().await?;
let hello = contract.welcome_msg().await?;
assert_eq!(hello, "Salaam".to_string());
Ok(())
}Re-exports§
pub use factory::Factory as ContractFactory;pub use scilla_value::*;pub use transition_call::*;