This site requires Javascript to be enabled.

Managing complex state and maps

In more advanced smart contract solutions, you may need to store and manage complex data structures. While simple key-value pairs might suffice for basic applications, more complex use cases often require storing extensive data, such as user balances, configurations, or multi-dimensional data sets.

This guide will walk you through managing complex state using maps and indexed maps in CosmWasm, using the example of tracking CW20 token balances and more advanced use cases.

Storing data with maps

Maps in CosmWasm allow you to associate keys with values, providing a flexible way to manage structured data. For instance, in a CW20 token contract, you can track token balances by associating addresses with their respective balances using a Map data structure.

Here’s how you define a Map in the state.rs file to store balances:

use cw_storage_plus::Map;use cosmwasm_std::{Addr, Uint128};pub const BALANCES: Map<&Addr, Uint128> = Map::new("balance");

Key points

  • Map: This is a generic type provided by the cw-storage-plus crate that allows you to store key-value pairs. The key is an address (&Addr), and the value is the balance (Uint128).
  • "balance": This is the storage key prefix for the map. Each entry in the map will be stored under this prefix, allowing for efficient retrieval and management of balances.

Interacting with maps

To effectively manage the data stored in a Map, you need to understand how to read, update, and remove entries. Below is an example of how to update a user’s balance after a token transfer, using the BALANCES map.

Example: updating a balance

Here’s a snippet from the CW20 base contract that demonstrates how to interact with the BALANCES map:

let rcpt_addr = deps.api.addr_validate(&recipient)?;BALANCES.update(    deps.storage,    &info.sender,    |balance: Option<Uint128>| -> StdResult<_> {        Ok(balance.unwrap_or_default().checked_sub(amount)?)    },)?;

Breaking down the code:

  1. Address Validation:
    • deps.api.addr_validate(&recipient)? ensures that the recipient's address is valid. If it isn’t, the function returns an error, preventing further execution.
    • rcpt_addr is the validated recipient address, which acts as the key in the map.
  2. Updating the Balance:
    • deps.storage: The storage interface is passed in, which allows access to the contract's persistent state.
    • BALANCES.update: The update method is called on the BALANCES map. This method allows you to modify the value associated with a specific key (in this case, the sender's address).
    • Anonymous Function (Lambda): The update function takes a closure (anonymous function) that receives the current balance (if any) and returns a new balance.
    • balance.unwrap_or_default(): If the balance doesn’t exist (i.e., the key is not found), it defaults to zero.
    • checked_sub(amount): Safely subtracts the specified amount from the balance. If the subtraction would result in a negative balance, it returns an error.
  3. Error Handling:
    • The ? operator is used throughout the code to handle errors. If any operation fails (e.g., if the balance is insufficient), the function returns an error, and the transaction is rolled back.

Reading data from maps

To retrieve a user’s balance from the BALANCES map, you can use the load method:

let balance = BALANCES.load(deps.storage, &rcpt_addr)?;

Explanation

  • BALANCES.load: Retrieves the value (balance) associated with the given address (rcpt_addr). If the address has no balance recorded, it will return an error.
  • Error Handling: Ensure you handle potential errors, such as when an address does not have a balance entry.

Advanced usage: indexed maps

For more complex queries and operations, such as retrieving data based on multiple criteria or filtering results, you can use IndexedMap. This data structure is particularly useful when you need to query the state efficiently based on secondary keys or indices.

What is IndexedMap?

IndexedMap is a powerful extension of Map that allows you to define multiple indices over your data. This enables efficient querying and sorting based on these indices.

Example: using IndexedMap

Imagine you want to store and retrieve user balances along with additional metadata, such as the user's registration time and account status. Here's how you could define such a state structure using IndexedMap:

use cw_storage_plus::{IndexedMap, IndexList, MultiIndex};use cosmwasm_std::{Addr, Uint128};#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]pub struct UserInfo {    pub balance: Uint128,    pub registered_at: u64,    pub is_active: bool,}// Define indices for the indexed mappub struct UserIndexes<'a> {    pub balance: MultiIndex<'a, Uint128, UserInfo, Addr>,    pub registered_at: MultiIndex<'a, u64, UserInfo, Addr>,}impl<'a> IndexList<UserInfo> for UserIndexes<'a> {    fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn MultiIndex<UserInfo>> + '_> {        let v: Vec<&dyn MultiIndex<UserInfo>> = vec![&self.balance, &self.registered_at];        Box::new(v.into_iter())    }}// Initialize the indexed mappub const USERS: IndexedMap<&Addr, UserInfo, UserIndexes> = IndexedMap::new(    "users",    UserIndexes {        balance: MultiIndex::new(            |d| d.balance,            "users",            "users__balance",        ),        registered_at: MultiIndex::new(            |d| d.registered_at,            "users",            "users__registered_at",        ),    },);

Explanation

  • IndexedMap: This data structure allows you to create a map with secondary indices, enabling efficient queries based on those indices.
  • UserInfo: A struct that contains information about the user, including their balance, registration time, and active status.
  • UserIndexes: This struct defines the indices for the IndexedMap. Each MultiIndex corresponds to a field in UserInfo that you want to index by.
  • MultiIndex: This type allows you to create an index on a specific field within your data structure. For example, you can index users by their balance or registration time.

Querying with IndexedMap

With IndexedMap, you can perform efficient queries based on the defined indices. For example, you can retrieve all users registered after a certain date:

let registered_since = 1609459200; // Example timestamplet query = USERS    .idx    .registered_at    .range(deps.storage, Some(registered_since), None, cosmwasm_std::Order::Ascending)?    .collect::<StdResult<Vec<_>>>()?;

Key Points

  • Range Queries: The range method allows you to query entries within a specified range, such as all users registered after a certain date.
  • Order: You can specify the order of the results, such as ascending or descending.

Practical tips for developers

  1. Use IndexedMap for Advanced Queries: When you need to perform complex queries or filter data based on multiple criteria, consider using IndexedMap. It provides efficient indexing and querying capabilities.
  2. Design Efficient Indices: Think carefully about the indices you define in IndexedMap. The right indices can significantly improve the performance of your contract, especially when dealing with large datasets.
  3. Modularize Your State Logic: Keep your state management logic organized and modular. For instance, separate your map and indexed map definitions into a dedicated state.rs file.
  4. Error Handling: Always handle potential errors when interacting with state, especially when performing updates or queries. This ensures your contract remains robust and reliable.