Explore the basic idea of function overloading based on number of arguments in rust. For this purpose we make use of declarative macros to implement a basic overloaded add function.
DRAFT CONTENT
This is not the final version of this article. I will be completing this once I have some fresh time in my hand
Quick Recap
Although you might already be familier with the terms I am mentioning below, Let’s build a quick recap to bring the context in this article so that anyone with starting or very less knowledge.
Dispatchable Call
In simple terms, dispatchable calls are any function that are exposed for user to interact with. In substrate, they are defined inside impl block prefixed with pallet::call
macro. In addition, these are some characteristics of dispatchable call in subtstrate:
They always have weight attached to them
They always have at least one paramater of type
OriginFor<T>
which defines the origination ( weather it is signed, root or none or even any other custom added origin)They always return one of
DispatchResult
orDispatchResultWithPostInfo
typeThey can read and write to onchain storage and only write to offchain-storage
Dispatchable calls are sent from
TransactionPool
so it might be possible that some transaction might be filtered beforehand they reach the call ( This is usually done bySignedExtension
trait)
Offchain Worker
To put it in easy words, offchain worker are any piece of logic that run seperatly from onchain logic in a way that it neither do nor is able to interfare with block production. In essence, they are kind of seperately running execution isolated from runtime logic but substrate makes it easy by making them easily writeable within the same place of onchain logic. Now again these are quick points about offchain workers:
- Their primary function is to discard the load from onchain logic by withdrawing the task that could otherwise require longer than the block execution time.
- They can access additional functionality that onchain are restricted to including making http requests, getting node local timestamp and so on.
- They have both read and write access to offchain storage
- They only have read access to onchain storage. When required write access they have to send the transaction to do so ( that’s why they can be taken as some binary running separately from node itself )
- They can be somehow relatable to traditional oracle concept of blockchain but note that these two entities are completely different and serve different use cases
- They are defined in
Hook
trait implying block of pallet which posses[pallet::hooks]
macro as well
What’s deal with sending an extrinsic to dispatchable from offchain worker
As I already mentioned, any operation that mutate the state of blockchain have to be done withing onchain runtime logic. This also applies to offchain as even though they are written closely while developing, they run separately from onchain part. Furthermore, it also applies that the behaviour of sending transaction from offchain and sending one from node frontend can be viewed as exactly same thing.
Again, one way to tell weather the source for this transaction is from offchain worker is to https://docs.substrate.io/rustdocs/latest/sp_runtime/transaction_validity/enum.TransactionSource.html
Setting things up
I will assume that you have set you template to use offchain worker and configured keys from which we will send transaction later on. If not, you can create a template from following reference:
- https://gnunicorn.github.io/substrate-offchain-cb/
- https://github.com/paritytech/substrate/tree/master/frame/examples/offchain-worker
Hooks
In general term of programming, hooks are defined as a way to insert extra peice of code after a certain state or point of execution. They are somewhat similar to subscribing for an event and executing the logic after on. There is already been an explanitation on difference & simalirities between hooks and events here:
In context of substrate it is no different. Some example of hooks may be running custom logic when new block is produces, a block is imported and so on.
In fatc, offchain worker is also a hook that is run in seperate context when a block is imported by node. Offchain worker can be configured on when to run during starting the node. This can be done with --offchain-worker
parameter. From node-template
:
–offchain-worker Should execute offchain workers on every block. By default it’s only enabled for nodes that are authoring new blocks. [default: WhenValidating] [possible values: Always, Never, WhenValidating]
Implementing hooks in pallet
In your pallet, hooks can be implemented by defining an impl block as in:
#[pallet::call]
impl<T: Config> for Self {
// *--snip
}
#[pallet::hooks]
impl<T: Config> Hooks for Self {
// Define hooks here
}
For using offchain worker, we have to implement a function named offchain_worker
which is actually a hook for offchain-worker as it’s name implies. So we can do something like:
#[pallet::hooks]
impl<T: Config> Hooks for Self {
fn offchain_worker(_block_number: T::BlockNumber) {
log::info!("\n\n======> Hello from offchin worker....");
}
}
Simple right? Make sure you name the function as is. Also we can see that offchain worker recive a paramater of type T::BlockNumber
which is actually the block number on which this offchain worker is running. And this function return nothing.
Seeing the offchain worker in action
To see what have we accomplished
cargo run --release -- --dev --tmp --offchain-worker Always
Even if you are running the binary directly by passing --offchain-worker Always
this makes sure that offchain worker always runs and we can see the output
While running the node you should see the message we are logging. An output on my machine was:
Defining an example dispatchable call
This must be straight forward just define a exmaple dispatchable call as in:
#[pallet::call]
#[pallet::weight(10_000)]
fn example_call(origin: OriginFor<T>, data: i32) -> DispatchResult {
log::info!("\n====> An example dispatcable function was called with data: {} ...", data);
if ensure_signed(origin).is_ok() {
log::info!("Origin is signed..");
} else if ensure_root(origin).is_ok() {
log::info!("Origin is root..");
} else {
log::info!("Origin is none..");
}
Ok(())
}
We defined a dispatchable call with weight 10,000 unit
and logged a message depending on the origin. This call recived a simple i32
value and simply log it.
Calling the dispatchable from offchain worker
Now at this point, we have a dispatchable to call and offchain worker running. Now before trying to call the dispatchable we just wrote. Here is a thing to catch: Offchain worker always send origin with either signed
or none
it can never be root. Remember we have mentioned that offchain worker are running seperatly from runtime, and it is guranteed by substrate that root origin can only be produced within the runtime only.
Given that we need an account public address to send signed transaction we have to get one. There are various accounts already preconfigured in substrate template namely Alice
, Bob
and so on. For the purpose of this demo, we can use any of them.
Inside offchain_worker try to get any of those accounts by:
fn offchain_worker(_block_number: T::BlockNumber) {
// *---snip
let signer = Signer::any_account();
}
This will give us one of the account preconfigures in template. Now we can use this to send a signed transaction by using .send_signed_extrinsic
method which recived a closure that will return a call to dispatch
let signer = Signer::any_account();
let call_result = signer.send_signed_extrinsic(|account_pubkey| {
log::info!("Sending a signed transaction from account {}", account_pubkey);
Call::example_call {
data: 0,
}
});
This way we can send a signed transaction to our function example_call
we defined earlier. We also log a pubick key of account before sending a transaction.
As of now, we don’t know weather the call was dispatched sucessfully or not. As we are doing nothing with the result call_result
in our case. Here, call_result
will have type Option<Result<>>. These the possible value of
call_result`
None
call_result
will be none when there was no account to send transaction fromSome(Err(_))
This specify that the transaction was sent but the transaction itself failed. i.e transaction returned
DispatchError
or any other error.Some(Ok(_))
This is returned when the extrinsic was called ans the extrinisc also retrned an
Ok()