- Docs
- Smart Contracts
- Complex state
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:
- 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.
- 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.
- 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.
- The
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
. EachMultiIndex
corresponds to a field inUserInfo
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
- 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. - 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. - 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. - Error Handling: Always handle potential errors when interacting with state, especially when performing updates or queries. This ensures your contract remains robust and reliable.