Create an actor
As we previously mentioned, all of our native actors will have to be created in the actors
folder.
An actor takes the shape of a Rust crate. In the starter you cloned you should already be able to find one crate, hello-world
.
Configuring the actor crate
To proceed, we can use the script at the root of the repository to create a new actor and its test:
$ ./create-actor.sh basic-actor
$ cd actors/basic-actor/ && ls -la
total 36
.
..
Cargo.lock
Cargo.toml
src
First, let’s have a look to the Cargo.toml
:
[package]
name = "basic-actor"
version = "0.1.0"
edition = "2021"
[target.'cfg(target_arch = "wasm32")'.dependencies]
cid = { version = "0.8.5", default-features = false }
frc42_dispatch = "3.1.0"
fvm_sdk = { version = "3.0.0" }
fvm_shared = { version = "3.1.0" }
fvm_ipld_blockstore = "0.1.1"
fvm_ipld_encoding = { version = "0.3.3" }
serde = { version = "1.0.136", features = ["derive"] }
serde_tuple = { version = "0.5.0" }
thiserror = { version = "1.0.31" }
[lib]
crate-type = ["cdylib"]
Two things to note:
- Dependencies are specified under
target.'cfg(target_arch = "wasm32")'
. This is because our crate will end up being compiled to Wasm, so we can focus on specifying dependencies for this target. - We are specifying
crate-type = ["cdylib"]
. This is mandatory to be able to properly compile to thewasm32
target.
Actor’s development
For this tutorial, we will create an actor that stores a value updated through the call of an Add()
method. It
should also be possible to read the current value through a Read()
method.
Layout
Our main logic for the actor will be located in src/actor.rs
. We also have some utilities located in src/utils.rs
.
In utils.rs
, there are two methods that are interesting to note:
ActorState::load()
&ActorState::save()
: Method implemented over the structure representing the inner state of an actor. They are used to read and write the current state.return_ipld()
: Method used to format a payload into an IPLD block and pass its ID as a return value for the received message.
In actor.rs
- -
invoke()
: Method that serves as the main entry point for our actor. It contains amatch
where each variant represents a call to an inner method of our actor. Constructor()
: Method called at instantiation time of our actor in the machine.
Storage: ActorState
To add both Add()
and Read()
to our actor we need to create the related methods and add their variant to our
match in invoke()
. But first, let’s change the state associated to our actor.
We need to change our state to store a counter
instead of the who_am_i
property. Let’s update utils.rs
:
#[derive(Serialize_tuple, Deserialize_tuple)]
pub struct ActorState {
value: u64,
}
Then, update the Constructor()
in actor.rs
to properly initialize the state:
// `Constructor` for the actor, called at every instantiation.
#[allow(non_snake_case)]
fn Constructor() {
let state = crate::utils::ActorState {
value: 0,
};
let cid = state.save();
fvm_sdk::sself::set_root(&cid).unwrap();
}
View method: Read
Let’s add the match variant and the method to read the current state:
// ...
#[no_mangle]
fn invoke(_input: u32) -> u32 {
let method_num = fvm_sdk::message::method_number();
match_method!(
method_num,
{
"Constructor" => {
Constructor();
NO_DATA_BLOCK_ID
},
"Read" => {
Read()
},
_ => {
fvm_sdk::vm::abort(
ExitCode::USR_UNHANDLED_MESSAGE.value(),
Some("Unknown method number"),
);
}
}
)
}
// ...
// `Read` returns the current value of our state value.
#[allow(non_snake_case)]
fn Read() -> u32 {
let state = crate::utils::ActorState::load(&fvm_sdk::sself::root().unwrap());
crate::utils::return_ipld(&state.value).unwrap()
}
By leveraging the return_ipld()
, we can return the value of ActorState::value
to the caller.
Write method: Add
Finally, let’s add our Add()
method to update the state of the actor. We are aiming to update the value of
ActorState::value
everytime the method is called by adding the passed value to the current state value.
For this, we will use deserialize_params
from utils.rs
. It takes the u32
value passed to the invoke()
method and
deserialize it in the type expected by the receiving variable.
We can now implement our Add()
method and its invoke()
match variant in actor.rs
:
#[no_mangle]
fn invoke(input: u32) -> u32 {
let method_num = fvm_sdk::message::method_number();
match_method!(
method_num,
{
"Constructor" => {
Constructor();
NO_DATA_BLOCK_ID
},
"Read" => {
Read()
},
"Add" => {
let to_add: u64 = crate::utils::deserialize_params(input);
Add(to_add);
NO_DATA_BLOCK_ID
},
_ => {
fvm_sdk::vm::abort(
ExitCode::USR_UNHANDLED_MESSAGE.value(),
Some("Unknown method number"),
);
}
}
)
}
// ...
// `Add` will add the passed value to the current state.
#[allow(non_snake_case)]
fn Add(to_add: u64) {
// Load the current state.
let mut state = crate::utils::ActorState::load(&fvm_sdk::sself::root().unwrap());
// Add message value.
state.value += to_add;
// Save updated state.
let cid = state.save();
fvm_sdk::sself::set_root(&cid).unwrap();
}
With this method implemented, the interface of our actor corresponds to our intended design. However, to ensure that things works as intended we need to add tests to our project. This is what we will focus on during the next step.