cosmos-sdk/collections/README.md

1211 lines
39 KiB
Markdown

# Collections
Collections is a library meant to simplify the experience with respect to module state handling.
Cosmos SDK modules handle their state using the `KVStore` interface. The problem with working with
`KVStore` is that it forces you to think of state as a bytes KV pairings when in reality the majority of
state comes from complex concrete golang objects (strings, ints, structs, etc.).
Collections allows you to work with state as if they were normal golang objects and removes the need
for you to think of your state as raw bytes in your code.
It also allows you to migrate your existing state without causing any state breakage that forces you into
tedious and complex chain state migrations.
## Installation
To install collections in your cosmos-sdk chain project, run the following command:
```shell
go get cosmossdk.io/collections
```
## Core types
Collections offers 5 different APIs to work with state, which will be explored in the next sections, these APIs are:
* ``Map``: to work with typed arbitrary KV pairings.
* ``KeySet``: to work with just typed keys
* ``Item``: to work with just one typed value
* ``Sequence``: which is a monotonically increasing number.
* ``IndexedMap``: which combines ``Map`` and `KeySet` to provide a `Map` with indexing capabilities.
## Preliminary components
Before exploring the different collections types and their capability it is necessary to introduce
the three components that every collection shares. In fact when instantiating a collection type by doing, for example,
```collections.NewMap/collections.NewItem/...``` you will find yourself having to pass them some common arguments.
For example, in code:
```go
package collections
import (
"cosmossdk.io/collections"
storetypes "cosmossdk.io/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
var AllowListPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
AllowList collections.KeySet[string]
}
func NewKeeper(storeKey *storetypes.KVStoreKey) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
AllowList: collections.NewKeySet(sb, AllowListPrefix, "allow_list", collections.StringKey),
}
}
```
Let's analyse the shared arguments, what they do, and why we need them.
### SchemaBuilder
The first argument passed is the ``SchemaBuilder``
`SchemaBuilder` is a structure that keeps track of all the state of a module, it is not required by the collections
to deal with state but it offers a dynamic and reflective way for clients to explore a module's state.
We instantiate a ``SchemaBuilder`` by passing it a function that given the modules store key returns the module's specific store.
We then need to pass the schema builder to every collection type we instantiate in our keeper, in our case the `AllowList`.
### Prefix
The second argument passed to our ``KeySet`` is a `collections.Prefix`, a prefix represents a partition of the module's `KVStore`
where all the state of a specific collection will be saved.
Since a module can have multiple collections, the following is expected:
* module params will become a `collections.Item`
* the `AllowList` is a `collections.KeySet`
We don't want a collection to write over the state of the other collection so we pass it a prefix, which defines a storage
partition owned by the collection.
If you already built modules, the prefix translates to the items you were creating in your ``types/keys.go`` file, example: https://github.com/cosmos/cosmos-sdk/blob/v0.52.0-rc.1/x/feegrant/key.go#L16~L22
your old:
```go
var (
// FeeAllowanceKeyPrefix is the set of the kvstore for fee allowance data
// - 0x00<allowance_key_bytes>: allowance
FeeAllowanceKeyPrefix = []byte{0x00}
// FeeAllowanceQueueKeyPrefix is the set of the kvstore for fee allowance keys data
// - 0x01<allowance_prefix_queue_key_bytes>: <empty value>
FeeAllowanceQueueKeyPrefix = []byte{0x01}
)
```
becomes:
```go
var (
// FeeAllowanceKeyPrefix is the set of the kvstore for fee allowance data
// - 0x00<allowance_key_bytes>: allowance
FeeAllowanceKeyPrefix = collections.NewPrefix(0)
// FeeAllowanceQueueKeyPrefix is the set of the kvstore for fee allowance keys data
// - 0x01<allowance_prefix_queue_key_bytes>: <empty value>
FeeAllowanceQueueKeyPrefix = collections.NewPrefix(1)
)
```
#### Rules
``collections.NewPrefix`` accepts either `uint8`, `string` or `[]bytes` it's good practice to use an always increasing `uint8`for disk space efficiency.
A collection **MUST NOT** share the same prefix as another collection in the same module, and a collection prefix **MUST NEVER** start with the same prefix as another, examples:
```go
prefix1 := collections.NewPrefix("prefix")
prefix2 := collections.NewPrefix("prefix") // THIS IS BAD!
```
```go
prefix1 := collections.NewPrefix("a")
prefix2 := collections.NewPrefix("aa") // prefix2 starts with the same as prefix1: BAD!!!
```
### Human-Readable Name
The third parameter we pass to a collection is a string, which is a human-readable name.
It is needed to make the role of a collection understandable by clients who have no clue about
what a module is storing in state.
#### Rules
Each collection in a module **MUST** have a unique humanised name.
## Key and Value Codecs
A collection is generic over the type you can use as keys or values.
This makes collections dumb, but also means that hypothetically we can store everything
that can be a go type into a collection. We are not bounded to any type of encoding (be it proto, json or whatever)
So a collection needs to be given a way to understand how to convert your keys and values to bytes.
This is achieved through ``KeyCodec`` and `ValueCodec`, which are arguments that you pass to your
collections when you're instantiating them using the ```collections.NewMap/collections.NewItem/...```
instantiation functions.
NOTE: Generally speaking you will never be required to implement your own ``Key/ValueCodec`` as
the SDK and collections libraries already come with default, safe and fast implementation of those.
You might need to implement them only if you're migrating to collections and there are state layout incompatibilities.
Let's explore an example:
```go
package collections
import (
"cosmossdk.io/collections"
storetypes "cosmossdk.io/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
var IDsPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
IDs collections.Map[string, uint64]
}
func NewKeeper(storeKey *storetypes.KVStoreKey) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
IDs: collections.NewMap(sb, IDsPrefix, "ids", collections.StringKey, collections.Uint64Value),
}
}
```
We're now instantiating a map where the key is string and the value is `uint64`.
We already know the first three arguments of the ``NewMap`` function.
The fourth parameter is our `KeyCodec`, we know that the ``Map`` has `string` as key so we pass it a `KeyCodec` that handles strings as keys.
The fifth parameter is our `ValueCodec`, we know that the `Map` has a `uint64` as value so we pass it a `ValueCodec` that handles uint64.
Collections already comes with all the required implementations for golang primitive types.
Let's make another example, this falls closer to what we build using cosmos SDK, let's say we want
to create a `collections.Map` that maps account addresses to their base account. So we want to map an `sdk.AccAddress` to an `auth.BaseAccount` (which is a proto):
```go
package collections
import (
"cosmossdk.io/collections"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)
var AccountsPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
Accounts collections.Map[sdk.AccAddress, authtypes.BaseAccount]
}
func NewKeeper(storeKey *storetypes.KVStoreKey, cdc codec.BinaryCodec) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Accounts: collections.NewMap(sb, AccountsPrefix, "accounts",
sdk.AccAddressKey, codec.CollValue[authtypes.BaseAccount](cdc)),
}
}
```
As we can see here since our `collections.Map` maps `sdk.AccAddress` to `authtypes.BaseAccount`,
we use the `sdk.AccAddressKey` which is the `KeyCodec` implementation for `AccAddress` and we use `codec.CollValue` to
encode our proto type `BaseAccount`.
Generally speaking you will always find the respective key and value codecs for types in the `go.mod` path you're using
to import that type. If you want to encode proto values refer to the codec `codec.CollValue` function, which allows you
to encode any type implement the `proto.Message` interface.
## Map
We analyse the first and most important collection type, the ``collections.Map``.
This is the type that everything else builds on top of.
### Use case
A `collections.Map` is used to map arbitrary keys with arbitrary values.
### Example
It's easier to explain a `collections.Map` capabilities through an example:
```go
package collections
import (
"cosmossdk.io/collections"
storetypes "cosmossdk.io/store/types"
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)
var AccountsPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
Accounts collections.Map[sdk.AccAddress, authtypes.BaseAccount]
}
func NewKeeper(storeKey *storetypes.KVStoreKey, cdc codec.BinaryCodec) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Accounts: collections.NewMap(sb, AccountsPrefix, "accounts",
sdk.AccAddressKey, codec.CollValue[authtypes.BaseAccount](cdc)),
}
}
func (k Keeper) CreateAccount(ctx sdk.Context, addr sdk.AccAddress, account authtypes.BaseAccount) error {
has, err := k.Accounts.Has(ctx, addr)
if err != nil {
return err
}
if has {
return fmt.Errorf("account already exists: %s", addr)
}
err = k.Accounts.Set(ctx, addr, account)
if err != nil {
return err
}
return nil
}
func (k Keeper) GetAccount(ctx sdk.Context, addr sdk.AccAddress) (authtypes.BaseAccount, error) {
acc, err := k.Accounts.Get(ctx, addr)
if err != nil {
return authtypes.BaseAccount{}, err
}
return acc, nil
}
func (k Keeper) RemoveAccount(ctx sdk.Context, addr sdk.AccAddress) error {
err := k.Accounts.Remove(ctx, addr)
if err != nil {
return err
}
return nil
}
```
#### Set method
Set maps with the provided `AccAddress` (the key) to the `auth.BaseAccount` (the value).
Under the hood the `collections.Map` will convert the key and value to bytes using the [key and value codec](README.md#key-and-value-codecs).
It will prepend to our bytes key the [prefix](README.md#prefix) and store it in the KVStore of the module.
#### Has method
The has method reports if the provided key exists in the store.
#### Get method
The get method accepts the `AccAddress` and returns the associated `auth.BaseAccount` if it exists, otherwise it errors.
#### Remove method
The remove method accepts the `AccAddress` and removes it from the store. It won't report errors
if it does not exist, to check for existence before removal use the ``Has`` method.
#### Iteration
Iteration has a separate section.
## KeySet
The second type of collection is `collections.KeySet`, as the word suggests it maintains
only a set of keys without values.
#### Implementation curiosity
A `collections.KeySet` is just a `collections.Map` with a `key` but no value.
The value internally is always the same and is represented as an empty byte slice ```[]byte{}```.
### Example
As always we explore the collection type through an example:
```go
package collections
import (
"cosmossdk.io/collections"
storetypes "cosmossdk.io/store/types"
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
var ValidatorsSetPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
ValidatorsSet collections.KeySet[sdk.ValAddress]
}
func NewKeeper(storeKey *storetypes.KVStoreKey) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
ValidatorsSet: collections.NewKeySet(sb, ValidatorsSetPrefix, "validators_set", sdk.ValAddressKey),
}
}
func (k Keeper) AddValidator(ctx sdk.Context, validator sdk.ValAddress) error {
has, err := k.ValidatorsSet.Has(ctx, validator)
if err != nil {
return err
}
if has {
return fmt.Errorf("validator already in set: %s", validator)
}
err = k.ValidatorsSet.Set(ctx, validator)
if err != nil {
return err
}
return nil
}
func (k Keeper) RemoveValidator(ctx sdk.Context, validator sdk.ValAddress) error {
err := k.ValidatorsSet.Remove(ctx, validator)
if err != nil {
return err
}
return nil
}
```
The first difference we notice is that `KeySet` needs use to specify only one type parameter: the key (`sdk.ValAddress` in this case).
The second difference we notice is that `KeySet` in its `NewKeySet` function does not require
us to specify a `ValueCodec` but only a `KeyCodec`. This is because a `KeySet` only saves keys and not values.
Let's explore the methods.
#### Has method
Has allows us to understand if a key is present in the `collections.KeySet` or not, functions in the same way as `collections.Map.Has
`
#### Set method
Set inserts the provided key in the `KeySet`.
#### Remove method
Remove removes the provided key from the `KeySet`, it does not error if the key does not exist,
if existence check before removal is required it needs to be coupled with the `Has` method.
## Item
The third type of collection is the `collections.Item`.
It stores only one single item, it's useful for example for parameters, there's only one instance
of parameters in state always.
#### implementation curiosity
A `collections.Item` is just a `collections.Map` with no key but just a value.
The key is the prefix of the collection!
### Example
```go
package collections
import (
"cosmossdk.io/collections"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "cosmossdk.io/x/staking/types"
)
var ParamsPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
Params collections.Item[stakingtypes.Params]
}
func NewKeeper(storeKey *storetypes.KVStoreKey, cdc codec.BinaryCodec) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Params: collections.NewItem(sb, ParamsPrefix, "params", codec.CollValue[stakingtypes.Params](cdc)),
}
}
func (k Keeper) UpdateParams(ctx sdk.Context, params stakingtypes.Params) error {
err := k.Params.Set(ctx, params)
if err != nil {
return err
}
return nil
}
func (k Keeper) GetParams(ctx sdk.Context) (stakingtypes.Params, error) {
return k.Params.Get(ctx)
}
```
The first key difference we notice is that we specify only one type parameter, which is the value we're storing.
The second key difference is that we don't specify the `KeyCodec`, since we store only one item we already know the key
and the fact that it is constant.
## Iteration
One of the key features of the ``KVStore`` is iterating over keys.
Collections which deal with keys (so `Map`, `KeySet` and `IndexedMap`) allow you to iterate
over keys in a safe and typed way. They all share the same API, the only difference being
that ``KeySet`` returns a different type of `Iterator` because `KeySet` only deals with keys.
:::note
Every collection shares the same `Iterator` semantics.
:::
Let's have a look at the `Map.Iterate` method:
```go
func (m Map[K, V]) Iterate(ctx context.Context, ranger Ranger[K]) (Iterator[K, V], error)
```
It accepts a `collections.Ranger[K]`, which is an API that instructs map on how to iterate over keys.
As always we don't need to implement anything here as `collections` already provides some generic `Ranger` implementers
that expose all you need to work with ranges.
### Example
We have a `collections.Map` that maps accounts using `uint64` IDs.
```go
package collections
import (
"cosmossdk.io/collections"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)
var AccountsPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
Accounts collections.Map[uint64, authtypes.BaseAccount]
}
func NewKeeper(storeKey *storetypes.KVStoreKey, cdc codec.BinaryCodec) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Accounts: collections.NewMap(sb, AccountsPrefix, "accounts", collections.Uint64Key, codec.CollValue[authtypes.BaseAccount](cdc)),
}
}
func (k Keeper) GetAllAccounts(ctx sdk.Context) ([]authtypes.BaseAccount, error) {
// passing a nil Ranger equals to: iterate over every possible key
iter, err := k.Accounts.Iterate(ctx, nil)
if err != nil {
return nil, err
}
accounts, err := iter.Values()
if err != nil {
return nil, err
}
return accounts, err
}
func (k Keeper) IterateAccountsBetween(ctx sdk.Context, start, end uint64) ([]authtypes.BaseAccount, error) {
// The collections.Range API offers a lot of capabilities
// like defining where the iteration starts or ends.
rng := new(collections.Range[uint64]).
StartInclusive(start).
EndExclusive(end).
Descending()
iter, err := k.Accounts.Iterate(ctx, rng)
if err != nil {
return nil, err
}
accounts, err := iter.Values()
if err != nil {
return nil, err
}
return accounts, nil
}
func (k Keeper) IterateAccounts(ctx sdk.Context, do func(id uint64, acc authtypes.BaseAccount) (stop bool)) error {
iter, err := k.Accounts.Iterate(ctx, nil)
if err != nil {
return err
}
defer iter.Close()
for ; iter.Valid(); iter.Next() {
kv, err := iter.KeyValue()
if err != nil {
return err
}
if do(kv.Key, kv.Value) {
break
}
}
return nil
}
```
Let's analyse each method in the example and how it makes use of the `Iterate` and the returned `Iterator` API.
#### GetAllAccounts
In `GetAllAccounts` we pass to our `Iterate` a nil `Ranger`. This means that the returned `Iterator` will include
all the existing keys within the collection.
Then we use the `Values` method from the returned `Iterator` API to collect all the values into a slice.
`Iterator` offers other methods such as `Keys()` to collect only the keys and not the values and `KeyValues` to collect
all the keys and values.
#### IterateAccountsBetween
Here we make use of the `collections.Range` helper to specialise our range.
We make it start in a point through `StartInclusive` and end in the other with `EndExclusive`, then
we instruct it to report us results in reverse order through `Descending`
Then we pass the range instruction to `Iterate` and get an `Iterator`, which will contain only the results
we specified in the range.
Then we use again the `Values` method of the `Iterator` to collect all the results.
`collections.Range` also offers a `Prefix` API which is not applicable to all keys types,
for example uint64 cannot be prefix because it is of constant size, but a `string` key
can be prefixed.
#### IterateAccounts
Here we showcase how to lazily collect values from an Iterator.
:::note
`Keys/Values/KeyValues` fully consume and close the `Iterator`, here we need to explicitly do a `defer iterator.Close()` call.
:::
`Iterator` also exposes a `Value` and `Key` method to collect only the current value or key, if collecting both is not needed.
:::note
For this `callback` pattern, collections expose a `Walk` API.
:::
## Composite keys
So far we've worked only with simple keys, like `uint64`, the account address, etc.
There are some more complex cases in, which we need to deal with composite keys.
A key is composite when it is composed of multiple keys, for example bank balances as stored as the composite key
`(AccAddress, string)` where the first part is the address holding the coins and the second part is the denom.
Example, let's say address `BOB` holds `10atom,15osmo`, this is how it is stored in state:
```
(bob, atom) => 10
(bob, osmos) => 15
```
Now this allows to efficiently get a specific denom balance of an address, by simply `getting` `(address, denom)`, or getting all the balances
of an address by prefixing over `(address)`.
Let's see now how we can work with composite keys using collections.
### Example
In our example we will show-case how we can use collections when we are dealing with balances, similar to bank,
a balance is a mapping between `(address, denom) => math.Int` the composite key in our case is `(address, denom)`.
## Instantiation of a composite key collection
```go
package collections
import (
"cosmossdk.io/collections"
"cosmossdk.io/math"
storetypes "cosmossdk.io/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
var BalancesPrefix = collections.NewPrefix(1)
type Keeper struct {
Schema collections.Schema
Balances collections.Map[collections.Pair[sdk.AccAddress, string], math.Int]
}
func NewKeeper(storeKey *storetypes.KVStoreKey) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Balances: collections.NewMap(
sb, BalancesPrefix, "balances",
collections.PairKeyCodec(sdk.AccAddressKey, collections.StringKey),
sdk.IntValue,
),
}
}
```
#### The Map Key definition
First of all we can see that in order to define a composite key of two elements we use the `collections.Pair` type:
```go
collections.Map[collections.Pair[sdk.AccAddress, string], math.Int]
```
`collections.Pair` defines a key composed of two other keys, in our case the first part is `sdk.AccAddress`, the second
part is `string`.
#### The Key Codec instantiation
The arguments to instantiate are always the same, the only thing that changes is how we instantiate
the ``KeyCodec``, since this key is composed of two keys we use `collections.PairKeyCodec`, which generates
a `KeyCodec` composed of two key codecs. The first one will encode the first part of the key, the second one will
encode the second part of the key.
### Working with composite key collections
Let's expand on the example we used before:
```go
var BalancesPrefix = collections.NewPrefix(1)
type Keeper struct {
Schema collections.Schema
Balances collections.Map[collections.Pair[sdk.AccAddress, string], math.Int]
}
func NewKeeper(storeKey *storetypes.KVStoreKey) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Balances: collections.NewMap(
sb, BalancesPrefix, "balances",
collections.PairKeyCodec(sdk.AccAddressKey, collections.StringKey),
sdk.IntValue,
),
}
}
func (k Keeper) SetBalance(ctx sdk.Context, address sdk.AccAddress, denom string, amount math.Int) error {
key := collections.Join(address, denom)
return k.Balances.Set(ctx, key, amount)
}
func (k Keeper) GetBalance(ctx sdk.Context, address sdk.AccAddress, denom string) (math.Int, error) {
return k.Balances.Get(ctx, collections.Join(address, denom))
}
func (k Keeper) GetAllAddressBalances(ctx sdk.Context, address sdk.AccAddress) (sdk.Coins, error) {
balances := sdk.NewCoins()
rng := collections.NewPrefixedPairRange[sdk.AccAddress, string](address)
iter, err := k.Balances.Iterate(ctx, rng)
if err != nil {
return nil, err
}
kvs, err := iter.KeyValues()
if err != nil {
return nil, err
}
for _, kv := range kvs {
balances = balances.Add(sdk.NewCoin(kv.Key.K2(), kv.Value))
}
return balances, nil
}
func (k Keeper) GetAllAddressBalancesBetween(ctx sdk.Context, address sdk.AccAddress, startDenom, endDenom string) (sdk.Coins, error) {
rng := collections.NewPrefixedPairRange[sdk.AccAddress, string](address).
StartInclusive(startDenom).
EndInclusive(endDenom)
iter, err := k.Balances.Iterate(ctx, rng)
if err != nil {
return nil, err
}
...
}
```
#### SetBalance
As we can see here we're setting the balance of an address for a specific denom.
We use the `collections.Join` function to generate the composite key.
`collections.Join` returns a `collections.Pair` (which is the key of our `collections.Map`)
`collections.Pair` contains the two keys we have joined, it also exposes two methods: `K1` to fetch the 1st part of the
key and `K2` to fetch the second part.
As always, we use the `collections.Map.Set` method to map the composite key to our value (`math.Int` in this case)
#### GetBalance
To get a value in composite key collection, we simply use `collections.Join` to compose the key.
#### GetAllAddressBalances
We use `collections.PrefixedPairRange` to iterate over all the keys starting with the provided address.
Concretely the iteration will report all the balances belonging to the provided address.
The first part is that we instantiate a `PrefixedPairRange`, which is a `Ranger` implementer aimed to help
in `Pair` keys iterations.
```go
rng := collections.NewPrefixedPairRange[sdk.AccAddress, string](address)
```
As we can see here we're passing the type parameters of the `collections.Pair` because golang type inference
with respect to generics is not as permissive as other languages, so we need to explicitly say what are the types of the pair key.
#### GetAllAddressesBalancesBetween
This showcases how we can further specialise our range to limit the results further, by specifying
the range between the second part of the key (in our case the denoms, which are strings).
## IndexedMap
`collections.IndexedMap` is a collection that uses under the hood a `collections.Map`, and has a struct, which contains the indexes that we need to define.
### Example
Let's say we have an `auth.BaseAccount` struct which looks like the following:
```go
type BaseAccount struct {
AccountNumber uint64 `protobuf:"varint,3,opt,name=account_number,json=accountNumber,proto3" json:"account_number,omitempty"`
Sequence uint64 `protobuf:"varint,4,opt,name=sequence,proto3" json:"sequence,omitempty"`
}
```
First of all, when we save our accounts in state we map them using a primary key `sdk.AccAddress`.
If it were to be a `collections.Map` it would be `collections.Map[sdk.AccAddress, authtypes.BaseAccount]`.
Then we also want to be able to get an account not only by its `sdk.AccAddress`, but also by its `AccountNumber`.
So we can say we want to create an `Index` that maps our `BaseAccount` to its `AccountNumber`.
We also know that this `Index` is unique. Unique means that there can only be one `BaseAccount` that maps to a specific
`AccountNumber`.
First of all, we start by defining the object that contains our index:
```go
var AccountsNumberIndexPrefix = collections.NewPrefix(1)
type AccountsIndexes struct {
Number *indexes.Unique[uint64, sdk.AccAddress, authtypes.BaseAccount]
}
func NewAccountIndexes(sb *collections.SchemaBuilder) AccountsIndexes {
return AccountsIndexes{
Number: indexes.NewUnique(
sb, AccountsNumberIndexPrefix, "accounts_by_number",
collections.Uint64Key, sdk.AccAddressKey,
func(_ sdk.AccAddress, v authtypes.BaseAccount) (uint64, error) {
return v.AccountNumber, nil
},
),
}
}
```
We create an `AccountIndexes` struct which contains a field: `Number`. This field represents our `AccountNumber` index.
`AccountNumber` is a field of `authtypes.BaseAccount` and it's a `uint64`.
Then we can see in our `AccountIndexes` struct the `Number` field is defined as:
```go
*indexes.Unique[uint64, sdk.AccAddress, authtypes.BaseAccount]
```
Where the first type parameter is `uint64`, which is the field type of our index.
The second type parameter is the primary key `sdk.AccAddress`.
And the third type parameter is the actual object we're storing `authtypes.BaseAccount`.
Then we create a `NewAccountIndexes` function that instantiates and returns the `AccountsIndexes` struct.
The function takes a `SchemaBuilder`. Then we instantiate our `indexes.Unique`, let's analyse the arguments we pass to
`indexes.NewUnique`.
#### NOTE: indexes list
The `AccountsIndexes` struct contains the indexes, the `NewIndexedMap` function will infer the indexes form that struct
using reflection, this happens only at init and is not computationally expensive. In case you want to explicitly declare
indexes: implement the `Indexes` interface in the `AccountsIndexes` struct:
```go
func (a AccountsIndexes) IndexesList() []collections.Index[sdk.AccAddress, authtypes.BaseAccount] {
return []collections.Index[sdk.AccAddress, authtypes.BaseAccount]{a.Number}
}
```
#### Instantiating a `indexes.Unique`
The first three arguments, we already know them, they are: `SchemaBuilder`, `Prefix` which is our index prefix (the partition
where index keys relationship for the `Number` index will be maintained), and the human name for the `Number` index.
The second argument is a `collections.Uint64Key` which is a key codec to deal with `uint64` keys, we pass that because
the key we're trying to index is a `uint64` key (the account number), and then we pass as fifth argument the primary key codec,
which in our case is `sdk.AccAddress` (remember: we're mapping `sdk.AccAddress` => `BaseAccount`).
Then as last parameter we pass a function that: given the `BaseAccount` returns its `AccountNumber`.
After this we can proceed instantiating our `IndexedMap`.
```go
var AccountsPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
Accounts *collections.IndexedMap[sdk.AccAddress, authtypes.BaseAccount, AccountsIndexes]
}
func NewKeeper(storeKey *storetypes.KVStoreKey, cdc codec.BinaryCodec) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Accounts: collections.NewIndexedMap(
sb, AccountsPrefix, "accounts",
sdk.AccAddressKey, codec.CollValue[authtypes.BaseAccount](cdc),
NewAccountIndexes(sb),
),
}
}
```
As we can see here what we do, for now, is the same thing as we did for `collections.Map`.
We pass it the `SchemaBuilder`, the `Prefix` where we plan to store the mapping between `sdk.AccAddress` and `authtypes.BaseAccount`,
the human name and the respective `sdk.AccAddress` key codec and `authtypes.BaseAccount` value codec.
Then we pass the instantiation of our `AccountIndexes` through `NewAccountIndexes`.
Full example:
```go
package docs
import (
"cosmossdk.io/collections"
"cosmossdk.io/collections/indexes"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)
var AccountsNumberIndexPrefix = collections.NewPrefix(1)
type AccountsIndexes struct {
Number *indexes.Unique[uint64, sdk.AccAddress, authtypes.BaseAccount]
}
func (a AccountsIndexes) IndexesList() []collections.Index[sdk.AccAddress, authtypes.BaseAccount] {
return []collections.Index[sdk.AccAddress, authtypes.BaseAccount]{a.Number}
}
func NewAccountIndexes(sb *collections.SchemaBuilder) AccountsIndexes {
return AccountsIndexes{
Number: indexes.NewUnique(
sb, AccountsNumberIndexPrefix, "accounts_by_number",
collections.Uint64Key, sdk.AccAddressKey,
func(_ sdk.AccAddress, v authtypes.BaseAccount) (uint64, error) {
return v.AccountNumber, nil
},
),
}
}
var AccountsPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
Accounts *collections.IndexedMap[sdk.AccAddress, authtypes.BaseAccount, AccountsIndexes]
}
func NewKeeper(storeKey *storetypes.KVStoreKey, cdc codec.BinaryCodec) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Accounts: collections.NewIndexedMap(
sb, AccountsPrefix, "accounts",
sdk.AccAddressKey, codec.CollValue[authtypes.BaseAccount](cdc),
NewAccountIndexes(sb),
),
}
}
```
### Working with IndexedMaps
Whilst instantiating `collections.IndexedMap` is tedious, working with them is extremely smooth.
Let's take the full example, and expand it with some use-cases.
```go
package docs
import (
"cosmossdk.io/collections"
"cosmossdk.io/collections/indexes"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)
var AccountsNumberIndexPrefix = collections.NewPrefix(1)
type AccountsIndexes struct {
Number *indexes.Unique[uint64, sdk.AccAddress, authtypes.BaseAccount]
}
func (a AccountsIndexes) IndexesList() []collections.Index[sdk.AccAddress, authtypes.BaseAccount] {
return []collections.Index[sdk.AccAddress, authtypes.BaseAccount]{a.Number}
}
func NewAccountIndexes(sb *collections.SchemaBuilder) AccountsIndexes {
return AccountsIndexes{
Number: indexes.NewUnique(
sb, AccountsNumberIndexPrefix, "accounts_by_number",
collections.Uint64Key, sdk.AccAddressKey,
func(_ sdk.AccAddress, v authtypes.BaseAccount) (uint64, error) {
return v.AccountNumber, nil
},
),
}
}
var AccountsPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
Accounts *collections.IndexedMap[sdk.AccAddress, authtypes.BaseAccount, AccountsIndexes]
}
func NewKeeper(storeKey *storetypes.KVStoreKey, cdc codec.BinaryCodec) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Accounts: collections.NewIndexedMap(
sb, AccountsPrefix, "accounts",
sdk.AccAddressKey, codec.CollValue[authtypes.BaseAccount](cdc),
NewAccountIndexes(sb),
),
}
}
func (k Keeper) CreateAccount(ctx sdk.Context, addr sdk.AccAddress) error {
nextAccountNumber := k.getNextAccountNumber()
newAcc := authtypes.BaseAccount{
AccountNumber: nextAccountNumber,
Sequence: 0,
}
return k.Accounts.Set(ctx, addr, newAcc)
}
func (k Keeper) RemoveAccount(ctx sdk.Context, addr sdk.AccAddress) error {
return k.Accounts.Remove(ctx, addr)
}
func (k Keeper) GetAccountByNumber(ctx sdk.Context, accNumber uint64) (sdk.AccAddress, authtypes.BaseAccount, error) {
accAddress, err := k.Accounts.Indexes.Number.MatchExact(ctx, accNumber)
if err != nil {
return nil, authtypes.BaseAccount{}, err
}
acc, err := k.Accounts.Get(ctx, accAddress)
return accAddress, acc, nil
}
func (k Keeper) GetAccountsByNumber(ctx sdk.Context, startAccNum, endAccNum uint64) ([]authtypes.BaseAccount, error) {
rng := new(collections.Range[uint64]).
StartInclusive(startAccNum).
EndInclusive(endAccNum)
iter, err := k.Accounts.Indexes.Number.Iterate(ctx, rng)
if err != nil {
return nil, err
}
return indexes.CollectValues(ctx, k.Accounts, iter)
}
func (k Keeper) getNextAccountNumber() uint64 {
return 0
}
```
## Collections with interfaces as values
Although cosmos-sdk is shifting away from the usage of interface registry, there are still some places where it is used.
In order to support old code, we have to support collections with interface values.
The generic `codec.CollValue` is not able to handle interface values, so we need to use a special type `codec.CollValueInterface`.
`codec.CollValueInterface` takes a `codec.BinaryCodec` as an argument, and uses it to marshal and unmarshal values as interfaces.
The `codec.CollValueInterface` lives in the `codec` package, whose import path is `github.com/cosmos/cosmos-sdk/codec`.
### Instantiating Collections with interface values
In order to instantiate a collection with interface values, we need to use `codec.CollValueInterface` instead of `codec.CollValue`.
```go
package example
import (
"cosmossdk.io/collections"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
)
var AccountsPrefix = collections.NewPrefix(0)
type Keeper struct {
Schema collections.Schema
Accounts *collections.Map[sdk.AccAddress, sdk.AccountI]
}
func NewKeeper(cdc codec.BinaryCodec, storeKey *storetypes.KVStoreKey) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Accounts: collections.NewMap(
sb, AccountsPrefix, "accounts",
sdk.AccAddressKey, codec.CollInterfaceValue[sdk.AccountI](cdc),
),
}
}
func (k Keeper) SaveBaseAccount(ctx sdk.Context, account authtypes.BaseAccount) error {
return k.Accounts.Set(ctx, account.GetAddress(), account)
}
func (k Keeper) SaveModuleAccount(ctx sdk.Context, account authtypes.ModuleAccount) error {
return k.Accounts.Set(ctx, account.GetAddress(), account)
}
func (k Keeper) GetAccount(ctx sdk.context, addr sdk.AccAddress) (sdk.AccountI, error) {
return k.Accounts.Get(ctx, addr)
}
```
## Triple key
The `collections.Triple` is a special type of key composed of three keys, it's identical to `collections.Pair`.
Let's see an example.
```go
package example
import (
"context"
"cosmossdk.io/collections"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
)
type AccAddress = string
type ValAddress = string
type Keeper struct {
// let's simulate we have redelegations which are stored as a triple key composed of
// the delegator, the source validator and the destination validator.
Redelegations collections.KeySet[collections.Triple[AccAddress, ValAddress, ValAddress]]
}
func NewKeeper(storeKey *storetypes.KVStoreKey) Keeper {
sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey))
return Keeper{
Redelegations: collections.NewKeySet(sb, collections.NewPrefix(0), "redelegations", collections.TripleKeyCodec(collections.StringKey, collections.StringKey, collections.StringKey)
}
}
// RedelegationsByDelegator iterates over all the redelegations of a given delegator and calls onResult providing
// each redelegation from source validator towards the destination validator.
func (k Keeper) RedelegationsByDelegator(ctx context.Context, delegator AccAddress, onResult func(src, dst ValAddress) (stop bool, err error)) error {
rng := collections.NewPrefixedTripleRange[AccAddress, ValAddress, ValAddress](delegator)
return k.Redelegations.Walk(ctx, rng, func(key collections.Triple[AccAddress, ValAddress, ValAddress]) (stop bool, err error) {
return onResult(key.K2(), key.K3())
})
}
// RedelegationsByDelegatorAndValidator iterates over all the redelegations of a given delegator and its source validator and calls onResult for each
// destination validator.
func (k Keeper) RedelegationsByDelegatorAndValidator(ctx context.Context, delegator AccAddress, validator ValAddress, onResult func(dst ValAddress) (stop bool, err error)) error {
rng := collections.NewSuperPrefixedTripleRange[AccAddress, ValAddress, ValAddress](delegator, validator)
return k.Redelegations.Walk(ctx, rng, func(key collections.Triple[AccAddress, ValAddress, ValAddress]) (stop bool, err error) {
return onResult(key.K3())
})
}
```
## Advanced Usages
### Alternative Value Codec
The `codec.AltValueCodec` allows a collection to decode values using a different codec than the one used to encode them.
Basically it enables to decode two different byte representations of the same concrete value.
It can be used to lazily migrate values from one bytes representation to another, as long as the new representation is
not able to decode the old one.
A concrete example can be found in `x/bank` where the balance was initially stored as `Coin` and then migrated to `Int`.
```go
var BankBalanceValueCodec = codec.NewAltValueCodec(sdk.IntValue, func(b []byte) (sdk.Int, error) {
coin := sdk.Coin{}
err := coin.Unmarshal(b)
if err != nil {
return sdk.Int{}, err
}
return coin.Amount, nil
})
```
The above example shows how to create an `AltValueCodec` that can decode both `sdk.Int` and `sdk.Coin` values. The provided
decoder function will be used as a fallback in case the default decoder fails. When the value will be encoded back into state
it will use the default encoder. This allows to lazily migrate values to a new bytes representation.