Flipper Internals
In this article, we take a deep dive into the code shown in the Flipper example, where we will explain in more detail all the Odra-specific sections of the code.
Header
use odra::Var;
Pretty straightforward. Odra wraps the code of the specific blockchains SDKs into its own implementation
that can be reused between targets. In the above case, we're importing Var
, which is responsible
for storing simple values on the blockchain's storage.
Struct
/// A module definition. Each module struct consists of Vars and Mappings
/// or/and other modules.
#[odra::module]
pub struct Flipper {
/// The module itself does not store the value,
/// it's a proxy that writes/reads value to/from the host.
value: Var<bool>,
}
In Odra, all contracts are also modules, which can be reused between contracts. That's why we need
to mark the struct with the #[odra::module]
attribute. In the struct definition itself, we state all
the fields of the contract. Those fields can be regular Rust data types, however - those will not
be persisted on the blockchain. They can also be Odra modules - defined in your project or coming
from Odra itself. Finally, to make the data persistent on the blockchain, you can use something like
Var<T>
showed above. To learn more about storage interaction, take a look at the
next article.
Impl
/// Module implementation.
///
/// To generate entrypoints,
/// an implementation block must be marked as #[odra::module].
#[odra::module]
impl Flipper {
/// Odra constructor.
///
/// Initializes the contract with the value of value.
pub fn init(&mut self) {
self.value.set(false);
}
...
Similarly to the struct, we mark the impl
section with the #[odra::module]
attribute. Odra will take all
pub
functions from this section and create contract endpoints from them. So, if you wish to have
functions that are not available for calling outside the contract, do not make them public. Alternatively,
you can create a separate impl
section without the attribute - all functions defined there, even marked
with pub
will be not callable.
The function named init
is the constructor of the contract. This function will be limited to only
to a single call, all further calls to it will result in an error. The init
function is optional,
if your contract does not need any initialization, you can skip it.
...
/// Replaces the current value with the passed argument.
pub fn set(&mut self, value: bool) {
self.value.set(value);
}
/// Replaces the current value with the opposite value.
pub fn flip(&mut self) {
self.value.set(!self.get());
}
...
The endpoints above show you how to interact with the simplest type of storage - Var<T>
. The data
saved there using set
function will be persisted in the blockchain.
Tests
#[cfg(test)]
mod tests {
use crate::flipper::FlipperHostRef;
use odra::host::{Deployer, NoArgs};
#[test]
fn flipping() {
let env = odra_test::env();
// To test a module we need to deploy it. Autogenerated `FlipperHostRef`
// implements `Deployer` trait, so we can use it to deploy the module.
let mut contract = FlipperHostRef::deploy(&env, NoArgs);
assert!(!contract.get());
contract.flip();
assert!(contract.get());
}
...
You can write tests in any way you prefer and know in Rust. In the example above we are deploying the
contract using Deployer::deploy
function called on FlipperHostRef
- a piece of code generated
by the #[odra::module]
. Because the module implements the constructor but does not accept any arguments,
as the second argument of the deploy function, we pass NoArgs
- one of the implementations of
the InitArgs
trait provided with the framework.
The contract will be deployed on the VM you chose while running cargo odra test
.
What's next
Now let's take a look at the different types of storage that Odra provides and how to use them.