The newly introduced time-based actions feature is one of my favourite additions to Hive Stream since I first published the library. In this post, we will detail how they work and what makes them so cool.
To my knowledge, no other smart contract solutions support time-based actions aka crons. For example, if you're working with Ethereum and you want to make a contract and action get executed at a specified interval or point in time, you need to use something like Ethereum Alarm Clock. I am sure for other blockchains, it is a similar story.
At their core, a time-based action is simply wanting to make a contract call at a certain point of time in the future. Say you have a game that distributes awards every day or adjusts metrics in your city building game, time-based actions allow you to do that.
Likely use-cases might be to run a contract action that runs every day and generates a reward pool for your in-game prize winnings or a weekly action that clears out stale transactions from your database.
At its core...
Every three seconds the streamer will query the blockchain for the next block via its block number. The Hive blockchain produces new blocks every three seconds, but in any instances where it takes longer, the streamer will retry after a brief delay.
Because we know we are always fetching blocks, we can use this to our advantage. The blockchain allows us to query it for metrics such as the latest block number and more importantly, the latest block time. For time-based actions work, you need a consistent base time for comparison. While the name makes them sound fancy, time-based actions are cron jobs (only not operating at a system level).
Firstly, we take this base blockchain time which is updated every three seconds. This is why the lowest possible form of reliable time-based action is a block processing time value of three-seconds. In reality, not many (if any) people will be requiring a contract action to be called less than three seconds (a timer would be a better use for that).
For there to be actions, we need a method which the user can call to register their actions which will be called. This is handled in the registerAction method.
Before we explain the registration part, we need to talk about a very important storage adapter call which is called loadActions. This is a call which our adapter should return any persisted registered actions or an empty array, in this case we are seeing the code for the file-based storage adapter which stores values in a JSON file. We parse the JSON file and returns the actions property.
Now, going back to the registerActions code once again. We load any persisted actions (explained above) and then we iterate over them (if any exist). For every action, we create a new TimeAction object which is a model which describes the shape of our action.
We also do some checks to ensure that actions with unique ID's are not being registered multiple times. Finally, we pushed the actions into an array stored on the streamer instance.
It all starts with the getLatestBlock method which calls the getDynamicGlobalProperties method which returns a few values, but the only one we want is the time property which is then converted to a Javascript date object and stored in the streamer. Nothing overly complicated or special.
The reason we use this date/time is for the reason that we need a consistent time value. Working with dates and time values is notoriously difficult, timezones make things even more complicated. For the sake of comparison, we need to be consistent and never rely on the system time.
Where the magic is put into practice...
Then we move on to the processActions method which is called every 3 seconds, regardless of whether or not we have any actions to call, it's consistently called.
A little more happens here. First, we use the Moment.js date library to convert our blockchain time value into a moment object (for comparison). Then we begin to loop over any actions we have registered in the actions property on the streamer.
An action has three distinct properties; date, timeValue, name and contractMethod. The date is a date object we also convert to a Moment object for comparison, this is the date the action was registered. The frequency constant is the value we use to determine how often the action should run.
And then we need to go and find the contract that this action is referencing. We use the contractName in our action to try and find a matching contract which we will then call. If we don't find a contract or the referenced contract method does not exist, we just keep looping our actions using continue.
Here is where use the aforementioned date objects and contract lookups to make the call. Using a switch/case statement, we can define all of the valid intervals for our actions. The above is the 3s or block action time. We use the Moment diff method to determine the difference between our registered date and the current block time.
In this case, if the amount of time has passed in more than 3 seconds, we call the contract action and pass in any payload. Finally, we reset the action date so it can run at the next specified interval.
You will notice that the pattern just repeats, swapping out the comparator value for seconds, minutes, hours, days and weeks. The above for running actions hourly looks the same as the previous code, it just checks if an hour has passed and calls the contract method.
Now, I am not the worlds best coder, but the above results in quite a consistent and functional time-based/cron implementation.