I need to get the system time in my pallet for which I am able to get the epoch format like 1590512778000. Now I need to convert this time to the date-time. I tried many ways but none worked. Can you please suggest any way to do it?
2 Answers
I use the approach shown below in the example function convert_moment_to_u64_in_milliseconds
, where I perform conversions in a pallet from the system time of Moment
type into its corresponding Unix epoch timestamp format like 1590512778000 of type u64
.
I then use the Chrono library as shown below in the example function convert_u64_in_milliseconds_to_date
to convert this Unix epoch timestamp to a date-time format of type Date
, which is an i64
. Note that it may be more seamless to have a function convert_moment_to_i64_in_milliseconds
that converts directly from Moment
into i64
.
I do this in the off-chain workers (OCW) function offchain_worker
where I throttle fetching data from an API endpoint that contains Unix epoch timestamp values rather than their equivalent block number.
In OCW you only have read-access to the block state but cannot perform any on-chain state changes since whilst alterations are not forbidden, they are not persisted after the worker has finished.
So I use OCW to perform such long-running tasks like asynchronous API requests and computations, including converting the u64
Unix epoch timestamp values that I receive from an API into the Date
type.
I then send unsigned transactions from OCW so validators may generate transactions that feed my results on-chain.
I then retrieve the results that they fed into the on-chain state using the on_initialize
or on_finalize
function and there I perform further on-chain state altering changes (only where necessary so as not to bloat on-chain storage) since those functions have both read-access and write-access to the block state.
To add the Chrono dependency in Cargo.toml of a pallet where I want to use it:
[dependencies]
chrono = { version = '0.4.19', default_features = false }
[features]
default = ['std']
std = [
'chrono/std',
...
To import it into the lib.rs file of that pallet:
use chrono::{
NaiveDateTime,
};
To store the date in storage I use:
#[pallet::storage]
#[pallet::getter(fn my_date)]
pub(super) type MyDate<T: Config> = StorageValue<_, Date;
To get the T::Moment
of the current timestamp:
let timestamp: <T as pallet_timestamp::Config>::Moment = <pallet_timestamp::Pallet<T>>::get();
To convert the T::Moment
into a u64
Unix epoch timestamp:
// Private functions
impl<T: Config> Pallet<T> {
fn convert_moment_to_u64_in_milliseconds(date: T::Moment) -> Result<u64, DispatchError> {
let date_as_u64_millis;
if let Some(_date_as_u64) = TryInto::<u64>::try_into(date).ok() {
date_as_u64_millis = _date_as_u64;
} else {
return Err(DispatchError::Other("Unable to convert Moment to i64 for date"));
}
return Ok(date_as_u64_millis);
}
...
To convert from u64
Unix epoch timestamp in milliseconds into the Date
type:
// Private functions
impl<T: Config> Pallet<T> {
fn convert_u64_in_milliseconds_to_date(date_as_u64_millis: u64) -> Result<Date, DispatchError> {
let date_as_u64_secs = date_as_u64_millis.clone() / 1000u64;
// https://docs.rs/chrono/0.4.6/chrono/naive/struct.NaiveDateTime.html#method.from_timestamp
let date = NaiveDateTime::from_timestamp(i64::try_from(date_as_u64_secs).unwrap(), 0).date();
// To get the 'start' of the given date use `date.and_hms(0, 0, 0).timestamp() * 1000`
let date_millis = date.timestamp() * 1000;
return Ok(date_millis);
}
...
To determine the amount of days between the two dates (such as between the date you made an API request and the current system time) you may just find the difference between the Unix epoch timestamp values:
if let Some(_period_millis) = current_date_millis.checked_sub(requested_date_millis) {
...
let mut period_millis_u64 = 0u64; // initialize
if let Some(_period_millis_u64) = TryInto::<u64>::try_into(_period_millis).ok() {
period_millis_u64 = _period_millis_u64;
} else {
return Err(DispatchError::Other("Unable to convert i32 to u64 for period_millis"));
}
...
And if you've stored a period measured in days on-chain as a u64
in storage value PeriodDays
and you want to convert that to its Unix epoch timestamp, you could try:
let millis_per_day = 86400000u64; // milliseconds per day
let mut period_days = 0u64; // initialize
if let Some(_period_days) = <PeriodDays<T>>::get() {
period_days = _period_days;
} else {
return Err(DispatchError::Other("Unable to get period_days"));;
}
let mut period_millis = 0u64; // initialize
if let Some(_period_millis) = period_days.checked_mul(millis_per_day) {
period_millis = _period_millis;
} else {
return Err(DispatchError::Other("Unable to multiply to determine period_millis"));
}
An additional function may be necessary to convert the Unix epoch timestamp from i64
in milliseconds (rather than u64
) into the Date
type.
The on_initialize
, on_finalize
, and offchain_worker
functions have a block_number
argument. It is worth noting that when the value of block_number
is 1 the corresponding timestamp value is still 0, which corresponds to 1970-01-01.
In a pallet you may also obtain the current block with let current_block = <system::Pallet<T>>::block_number();
.
Note that I use this resource to convert between Unix epoch timestamp and a human-readable date.
In your tests you may set the timestamp that you want with Timestamp::set_timestamp(1590512778000u64);
, which corresponds to Tuesday, 26 May 2020 17:06:18 (GMT), where 1590512778000 is the Unix epoch timestamp in milliseconds, and you may set the current block number with MyTestModule::on_initialize(1);
.
In JavaScript, this is extremely easy:
let date = new Date(1590512778000);
date.toString()
> "Tue May 26 2020 11:06:18 GMT-0600 (Mountain Daylight Time)"
Within the runtime itself, I would not really recommend doing any kind of date parsing, or really any use of dates within your runtime logic.
Time is not a native concept inside the blockchain. Even the timestamp pallet in Substrate is data which is fed into the blockchain by the block producer, and only has a "soft consensus" around it.
If you want to do something like schedule an event at a future time, the best thing to do is use the Block Number, which is totally a native part of the blockchain, and a concept that is very easy to use.
Most Substrate blockchains have a fixed block time, for example 6 seconds, so you could program 1 hour as:
let one_hour: T::BlockNumber = 600;