Working with time

The concept of time in the blockchain is tricky - as in every distributed system, it is not easy to synchronize the clocks of all the nodes.

However, there is the notion of a time that is even monotonic - which means that it should never go "backward" between executions. Also, what is important is - time is always unique throughout the whole transaction - and even the entire block, which is built of multiple transactions.

The time is encoded in the Env type in its block field, which looks like this:


#![allow(unused)]
fn main() {
pub struct BlockInfo {
    pub height: u64,
    pub time: Timestamp,
    pub chain_id: String,
}
}

You can see the time field, which is the timestamp of the processed block. The height field is also worth mentioning - it contains a sequence number of the processed block. It is sometimes more useful than time, as it is guaranteed that the height field is guaranteed to increase between blocks, while two blocks may be executed with the same time (even though it is rather not probable).

Also, many transactions might be executed in a single block. That means that if we need a unique id for the execution of a particular message, we should look for something more. This thing is a transaction field of the Env type:


#![allow(unused)]
fn main() {
pub struct TransactionInfo {
    pub index: u32,
}
}

The index here contains a unique index of the transaction in the block. That means that to get the unique identifier of a transaction through the whole block, we can use the (height, transaction_index) pair.

Join time

We want to use the time in our system to keep track of the join time of admins. We don't yet add new members to the group, but we can already set the join time of initial admins. Let's start updating our state:


#![allow(unused)]
fn main() {
use cosmwasm_std::{Addr, Timestamp};
use cw_storage_plus::Map;
use cw_storage_plus::Item;

pub const ADMINS: Map<&Addr, Timestamp> = Map::new("admins");
pub const DONATION_DENOM: Item<String> = Item::new("donation_denom");
}

As you can see, our admins set became a proper map - we will assign the join time to every admin.

Now we need to update how we initialize a map - we stored the Empty data previously, but it nevermore matches our value type. Let's check an updated instantiation function:

You might argue to create a separate structure for the value of this map, so in the future, if we would need to add something there, but in my opinion, it would be premature - we can also change the entire value type in the future, as it would be the same breaking change.

Now we need to update how we initialize a map - we stored the Empty data previously, but it nevermore matches our value type. Let's check an updated instantiation function:


#![allow(unused)]
fn main() {
use crate::state::{ADMINS, DONATION_DENOM};
use cosmwasm_std::{
    DepsMut, Env, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    deps: DepsMut,
    env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    for addr in msg.admins {
        let admin = deps.api.addr_validate(&addr)?;
        ADMINS.save(deps.storage, &admin, &env.block.time)?;
    }
    DONATION_DENOM.save(deps.storage, &msg.donation_denom)?;

    Ok(Response::new())
}
}

Instead of storing &Empty {} as an admin value, we store the join time, which we read from &env.block.time. Also, note that I removed the underscore from the name of the env block - it was there only to ensure the Rust compiler the variable is purposely unused and not some kind of a bug.

Finally, remember to remove any obsolete Empty imports through the project - the compiler should help you point out unused imports.

Query and tests

The last thing to add regarding join time is the new query asking for the join time of a particular admin. Everything you need to do that was already discussed, I'll leave it for you as an exercise. The query variant should look like:


#![allow(unused)]
fn main() {
#[returns(JoinTimeResp)]
JoinTime { admin: String },
}

And the example response type:


#![allow(unused)]
fn main() {
#[cw_serde]
pub struct JoinTimeResp {
    pub joined: Timestamp,
}
}

You may question that in response type, I suggest always returning a joined value, but what to do when no such admin is added? Well, in such a case, I would rely on the fact that load function returns a descriptive error of missing value in storage - however, feel free to define your own error for such a case or even make the joined field optional, and be returned if requested admin exists.

Finally, there would be a good idea to make a test for new functionality - call a new query right after instantiation to verify initial admins has proper join time (possibly by extending the existing instantiation test).

One thing you might need help with in tests might be how to get the time of execution. Using any OS-time would be doomed to fail - instead, you can call the block_info function to reach the BlockInfo structure containing the block state at a particular moment in the app - calling it just before instantiation would make you sure you are working with the same state which would be simulated on the call.