Collections 是一个旨在简化模块状态处理体验的库。 Cosmos SDK 模块通过 KVStore 接口来处理其状态。使用 KVStore 的问题在于,它迫使你把状态看作字节级的键值对,但实际上绝大多数状态都来自复杂的具体 golang 对象(字符串、整数、结构体等)。 Collections 允许你像处理普通 golang 对象一样处理状态,让你无需在代码中把状态当作原始字节来思考。 它还允许你迁移现有状态,而不会造成状态破坏,避免你被迫进行繁琐而复杂的链状态迁移。

安装

要在你的 cosmos-sdk 链项目中安装 collections,请运行以下命令:
go get cosmossdk.io/collections

核心类型

Collections 提供了 5 种不同的状态操作 API,接下来的章节会逐一介绍。这些 API 包括:
  • Map:用于处理带类型的任意键值对。
  • KeySet:用于仅处理带类型的键。
  • Item:用于处理单个带类型的值。
  • Sequence:表示一个单调递增的数字。
  • IndexedMap:结合了 Map 和 KeySet,提供具备索引能力的 Map。

预备组件

在深入不同的 collection 类型及其能力之前,有必要先介绍每个 collection 共享的三个组件。实际上,当你实例化某个 collection 类型时,例如使用 collections.NewMap/collections.NewItem/...,你会发现都需要传入一些公共参数。 例如,代码如下:
package collections

import (
    
    "cosmossdk.io/collections"
    store "cosmossdk.io/core/store"
)

var AllowListPrefix = collections.NewPrefix(0)

type Keeper struct {
    Schema    collections.Schema
	AllowList collections.KeySet[string]
}

func NewKeeper(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

return Keeper{
    AllowList: collections.NewKeySet(sb, AllowListPrefix, "allow_list", collections.StringKey),
}
}
下面我们来分析这些共享参数,它们的作用,以及为什么需要它们。

SchemaBuilder

传入的第一个参数是 SchemaBuilder。 SchemaBuilder 是一个用于跟踪模块全部状态的结构。collections 本身并不依赖它来处理状态,但它为客户端提供了一种动态且基于反射的方式来探索模块状态。 我们通过向 SchemaBuilder 传入一个 store.KVStoreService 来实例化它,这个 store service 可以通过依赖注入或 runtime.NewKVStoreService 获得。 然后,我们需要把 schema builder 传给 keeper 中实例化的每一个 collection 类型,在这里就是 AllowList。 创建完所有 collections 后,调用 sb.Build() 来校验前缀唯一性并完成 schema 构建。将返回的 collections.Schema 存入 keeper 的 Schema 字段:
k := Keeper{
    AllowList: collections.NewKeySet(sb, AllowListPrefix, "allow_list", collections.StringKey),
}
schema, err := sb.Build()
if err != nil {
    panic(err)
}
k.Schema = schema
return k
本文中的代码示例展示的是 collection 的实例化模式;为简洁起见,省略了 sb.Build() 调用。在生产代码中,sb.Build() 是必需的。

前缀

传给 KeySet 的第二个参数是 collections.Prefix。前缀表示模块 KVStore 中的一个分区,某个特定 collection 的所有状态都会保存在这里。 由于一个模块可以包含多个 collections,因此通常会有如下情况:
  • 模块参数会成为一个 collections.Item
  • AllowList 是一个 collections.KeySet
我们不希望某个 collection 覆盖另一个 collection 的状态,因此需要为它传入前缀,用来定义该 collection 所拥有的存储分区。 如果你之前已经构建过模块,那么这里的前缀就相当于你在 types/keys.go 文件中创建的那些项,例如:Link 你原来的写法:
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
}
)
现在会变成:
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)
)

规则

collections.NewPrefix 接受 int、string 或 []byte。为了提升磁盘空间效率,推荐使用单调递增的 int(取值范围 0–255)。 同一模块中,一个 collection 绝不能 与另一个 collection 共享相同前缀,并且一个 collection 的前缀 绝不能 以另一个 collection 的前缀作为开头,例如:
prefix1 := collections.NewPrefix("prefix")

prefix2 := collections.NewPrefix("prefix") // THIS IS BAD!
prefix1 := collections.NewPrefix("a")

prefix2 := collections.NewPrefix("aa") // prefix2 starts with the same as prefix1: BAD!!!

人类可读名称

传给 collection 的第三个参数是一个字符串,也就是人类可读的名称。 它的作用是让那些并不了解模块在状态中存储了什么的客户端,也能理解该 collection 的职责。

规则

模块中的每个 collection 必须 具有唯一的人类可读名称。

键和值编解码器

collection 在键或值的类型上是泛型的。 这让 collections 本身保持简单,但也意味着理论上我们可以把任何能表示为 go 类型的内容存入 collection。我们不受限于某一种编码方式(无论是 proto、json 还是其他格式)。 因此,collection 需要知道如何把你的键和值转换为字节。 这通过 KeyCodec 和 ValueCodec 实现,它们是你在使用 collections.NewMap/collections.NewItem/... 这些实例化函数创建 collection 时需要传入的参数。 注意:通常来说,你几乎不需要自己实现 Key/ValueCodec,因为 SDK 和 collections 库已经提供了默认、安全且高性能的实现。 只有在迁移到 collections 且存在状态布局不兼容时,你才可能需要自行实现它们。 我们来看一个例子:
package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
	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(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

return Keeper{
    IDs: collections.NewMap(sb, IDsPrefix, "ids", collections.StringKey, collections.Uint64Value),
}
}
现在我们实例化了一个 map,其中键是 string,值是 uint64。 NewMap 函数的前三个参数我们已经了解过了。 第四个参数是 KeyCodec。由于这个 Map 的键是 string,所以我们传入一个能够处理字符串键的 KeyCodec。 第五个参数是 ValueCodec。由于这个 Map 的值是 uint64,所以我们传入一个能够处理 uint64 值的 ValueCodec。 Collections 已经为 golang 基本类型提供了所有必需的实现。 再来看一个更贴近我们在 cosmos SDK 中实际构建内容的例子。假设我们要创建一个 collections.Map,把账户地址映射到对应的基础账户。也就是说,我们希望将 sdk.AccAddress 映射到 auth.BaseAccount(它是一个 proto):
package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

return Keeper{
    Accounts: collections.NewMap(sb, AccountsPrefix, "accounts",
			sdk.AccAddressKey, codec.CollValue[authtypes.BaseAccount](cdc)),
}
}
正如这里所示,由于我们的 collections.Map 将 sdk.AccAddress 映射到 authtypes.BaseAccount, 我们使用 sdk.AccAddressKey 作为 AccAddress 的 KeyCodec 实现,并使用 codec.CollValue 来编码 proto 类型 BaseAccount。 一般来说,你总能在导入该类型所使用的 go.mod 路径下找到对应类型的键和值编解码器。 如果你想编码 proto 值,请参考编解码器函数 codec.CollValue,它允许你编码任何实现了 proto.Message 接口的类型。

Map

接下来我们分析第一个也是最重要的 collection 类型:collections.Map。 其他所有类型都建立在它之上。

使用场景

collections.Map 用于将任意键映射到任意值。

示例

通过一个例子来解释 collections.Map 的能力会更容易:
package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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 方法

Set 会把给定的 AccAddress(键)映射到 auth.BaseAccount(值)。 在底层,collections.Map 会使用键和值编解码器将键和值转换为字节。 然后,它会把前缀追加到键字节前面,并将结果存入模块的 KVStore。

Has 方法

Has 方法用于报告给定的键是否存在于存储中。

Get 方法

Get 方法接收 AccAddress,如果存在则返回关联的 auth.BaseAccount,否则返回错误。

Remove 方法

Remove 方法接收 AccAddress 并将其从存储中删除。如果目标不存在,它不会返回错误;如果你希望在删除前先检查是否存在,请使用 Has 方法。

迭代

迭代有单独的章节。

KeySet

第二种集合类型是 collections.KeySet,顾名思义,它只维护一组键,不包含值。

实现上的一点趣味

collections.KeySet 本质上只是一个带有 key 但没有 value 的 collections.Map。 其内部的值始终相同,并表示为空字节切片 []byte{}。

示例

和前面一样,我们通过一个示例来了解这种集合类型:
package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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
}
我们注意到的第一个区别是,KeySet 只需要指定一个类型参数,也就是键(这里是 sdk.ValAddress)。 我们注意到的第二个区别是,KeySet 的 NewKeySet 函数不需要 我们指定 ValueCodec,只需要 KeyCodec。这是因为 KeySet 只保存键而不保存值。 下面来看看这些方法。

Has 方法

Has 让我们能够判断某个键是否存在于 collections.KeySet 中,工作方式与 collections.Map.Has 相同。

Set 方法

Set 会将给定的键插入到 KeySet 中。

Remove 方法

Remove 会从 KeySet 中移除给定的键;如果该键不存在,它不会报错。 如果需要在移除前先检查是否存在,则需要配合 Has 方法使用。

Item

第三种集合类型是 collections.Item。 它只存储单个条目,例如非常适合用于参数,因为状态中始终只有一份参数实例。

实现上的一点趣味

collections.Item 本质上只是一个没有 key、只有 value 的 collections.Map。 这个键就是集合的前缀!

示例

package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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)
}
我们注意到的第一个关键区别是,这里只指定了一个类型参数,也就是要存储的值。 第二个关键区别是,我们不需要指定 KeyCodec,因为这里只存储一个条目,我们已经知道这个键 并且它是常量。

迭代

KVStore 的一个关键特性是对键进行迭代。 处理键的集合类型(也就是 Map、KeySet 和 IndexedMap)允许你以安全且类型化的方式 遍历键。它们共享同一套 API,唯一的区别在于 KeySet 返回的是不同类型的 Iterator,因为 KeySet 只处理键。
所有集合都共享相同的 Iterator 语义。
我们来看一下 Map.Iterate 方法:
func (m Map[K, V])

Iterate(ctx context.Context, ranger Ranger[K]) (Iterator[K, V], error)
它接收一个 collections.Ranger[K],这是一个用于告诉 map 如何遍历键的 API。 和前面一样,这里我们不需要自己实现任何东西,因为 collections 已经提供了一些通用的 Ranger 实现, 足以满足你对范围操作的全部需求。

示例

我们有一个 collections.Map,它使用 uint64 ID 来映射账户。
package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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
}
下面我们来分析示例中的每个方法,以及它如何使用 Iterate 和返回的 Iterator API。

GetAllAccounts

在 GetAllAccounts 中,我们向 Iterate 传入了一个空的 Ranger。这意味着返回的 Iterator 将包含 集合中所有现有的键。 随后,我们使用返回的 Iterator API 中的 Values 方法,将所有值收集到一个切片中。 Iterator 还提供了其他方法,例如 Keys() 用于只收集键而不收集值,KeyValues 用于收集 所有键和值。

IterateAccountsBetween

这里我们使用 collections.Range 辅助器来细化范围。 我们通过 StartInclusive 指定起点,通过 EndExclusive 指定终点,然后 再通过 Descending 指示它按逆序返回结果。 然后我们将这个范围指令传给 Iterate,得到一个 Iterator,其中只会包含 我们在范围中指定的结果。 接着我们再次使用 Iterator 的 Values 方法来收集所有结果。 collections.Range 还提供了 Prefix API,但它并不适用于所有键类型, 例如 uint64 因为长度固定,不能做前缀匹配;但 string 类型的键 可以使用前缀。

IterateAccounts

这里我们演示如何从 Iterator 中惰性地收集值。
Keys/Values/KeyValues 会完整消费并关闭 Iterator,这里我们需要显式调用 defer iterator.Close()。
Iterator 还暴露了 Value 和 Key 方法,如果不需要同时获取两者,就可以只取当前值或当前键。
对于这种 callback 模式,collections 提供了一个 Walk API。

复合键

到目前为止,我们处理的都只是简单键,比如 uint64、账户地址等。 还有一些更复杂的场景,我们需要处理复合键。 当一个键由多个键组成时,它就是复合键。例如,银行余额会以复合键 (AccAddress, string) 存储,其中第一部分是持有币的地址,第二部分是 denom。 例如,假设地址 BOB 持有 10atom,15osmo,它在状态中的存储方式如下:
(bob, atom) => 10
(bob, osmos) => 15
这样一来,就可以高效地获取某个地址某个特定 denom 的余额,只需要 getting (address, denom); 或者通过对 (address) 做前缀匹配来获取该地址的全部余额。 现在我们来看一下,如何使用 collections 来处理复合键。

示例

在这个示例中,我们将演示在处理类似 bank 的余额场景时如何使用 collections。 余额是一个 (address, denom) => math.Int 的映射,在这里我们的复合键就是 (address, denom)。

复合键集合的实例化

package collections

import (
    
	"cosmossdk.io/collections"
    "cosmossdk.io/math"
	store "cosmossdk.io/core/store"
	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(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

return Keeper{
    Balances: collections.NewMap(
			sb, BalancesPrefix, "balances",
			collections.PairKeyCodec(sdk.AccAddressKey, collections.StringKey),
			sdk.IntValue,
		),
}
}

Map 键定义

首先可以看到,为了定义一个由两个元素组成的复合键,我们使用了 collections.Pair 类型:
collections.Map[collections.Pair[sdk.AccAddress, string], math.Int]
collections.Pair 定义了一个由另外两个键组成的键,在我们的例子中,第一部分是 sdk.AccAddress,第二 部分是 string。

Key Codec 的实例化

用于实例化的参数始终相同,唯一变化的是 KeyCodec 的实例化方式。 由于这个键由两个键组成,我们使用 collections.PairKeyCodec,它会生成 一个由两个 key codec 组成的 KeyCodec。第一个会编码键的第一部分,第二个会 编码键的第二部分。

使用复合键集合

让我们基于之前使用过的示例继续展开:
var BalancesPrefix = collections.NewPrefix(1)

type Keeper struct {
    Schema   collections.Schema
	Balances collections.Map[collections.Pair[sdk.AccAddress, string], math.Int]
}

func NewKeeper(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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

正如这里所示,我们正在为某个地址设置特定 denom 的余额。 我们使用 collections.Join 函数来生成复合键。 collections.Join 会返回一个 collections.Pair(它正是我们的 collections.Map 的键) collections.Pair 包含我们拼接在一起的两个键,它还暴露了两个方法:K1 用于获取键的第 1 部分,K2 用于获取第 2 部分。 和往常一样,我们使用 collections.Map.Set 方法将复合键映射到对应的值(这里是 math.Int)

GetBalance

要从复合键集合中获取一个值,我们只需使用 collections.Join 来组装这个键。

GetAllAddressBalances

我们使用 collections.PrefixedPairRange 遍历所有以前面给定地址开头的键。 具体来说,这次遍历会返回属于该地址的所有余额。 第一步是实例化一个 PrefixedPairRange,它是一个 Ranger 实现,专门用于辅助 Pair 键的遍历。
rng := collections.NewPrefixedPairRange[sdk.AccAddress, string](address)
正如这里所示,我们传入了 collections.Pair 的类型参数,因为 golang 在泛型上的类型推断不像其他语言那样宽松,所以我们需要显式说明 pair 键的类型分别是什么。

GetAllAddressesBalancesBetween

这里展示了如何进一步细化范围,通过指定键的第二部分之间的区间来进一步限制结果(在本例中是字符串类型的 denoms)。

IndexedMap

collections.IndexedMap 是一种底层使用 collections.Map 的集合,并带有一个结构体,该结构体中包含我们需要定义的索引。

示例

假设我们有一个 auth.BaseAccount 结构体,形式如下:
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"`
}
首先,当我们把账户保存到 state 中时,会使用主键 sdk.AccAddress 来映射它们。 如果它是一个 collections.Map,那么它会是 collections.Map[sdk.AccAddress, authtypes.BaseAccount]。 然后,我们还希望不仅能通过 sdk.AccAddress 获取账户,也能通过它的 AccountNumber 获取。 所以可以认为,我们要创建一个 Index,把 BaseAccount 映射到它的 AccountNumber。 我们还知道这个 Index 是唯一索引。唯一意味着,只能有一个 BaseAccount 映射到某个特定的 AccountNumber。 首先,我们从定义包含索引的对象开始:
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
},
		),
}
}
我们创建了一个 AccountIndexes 结构体,其中包含一个字段:Number。这个字段表示我们的 AccountNumber 索引。 AccountNumber 是 authtypes.BaseAccount 的一个字段,类型是 uint64。 接着可以看到,在 AccountIndexes 结构体中,Number 字段被定义为:
*indexes.Unique[uint64, sdk.AccAddress, authtypes.BaseAccount]
其中第一个类型参数是 uint64,也就是我们索引字段的类型。 第二个类型参数是主键 sdk.AccAddress。 第三个类型参数则是我们实际存储的对象 authtypes.BaseAccount。 然后我们创建一个 NewAccountIndexes 函数,用于实例化并返回 AccountsIndexes 结构体。 这个函数接收一个 SchemaBuilder。随后我们实例化 indexes.Unique,下面来分析一下传给 indexes.NewUnique 的参数。

注意:索引列表

AccountsIndexes 结构体包含这些索引,NewIndexedMap 函数会通过反射从这个结构体中推断出索引,这只会在初始化时发生,因此不会带来明显的计算开销。如果你希望显式声明索引:可以在 AccountsIndexes 结构体中实现 Indexes 接口:
func (a AccountsIndexes)

IndexesList() []collections.Index[sdk.AccAddress, authtypes.BaseAccount] {
    return []collections.Index[sdk.AccAddress, authtypes.BaseAccount]{
    a.Number
}
}

实例化 indexes.Unique

前 3 个参数我们已经了解过了,分别是:SchemaBuilder、Prefix(也就是索引前缀,用于维护 Number 索引的索引键关系的分区),以及 Number 索引的人类可读名称。 第二个参数是 collections.Uint64Key,它是一个处理 uint64 键的 key codec。我们传入它,是因为要建立索引的键本身就是一个 uint64 键(账户编号)。接着,第五个参数传入主键 codec,在我们的例子里是 sdk.AccAddress(记住:我们映射的是 sdk.AccAddress => BaseAccount)。 最后一个参数传入的是一个函数:给定 BaseAccount,返回它的 AccountNumber。 完成这些之后,我们就可以继续实例化 IndexedMap。
var AccountsPrefix = collections.NewPrefix(0)

type Keeper struct {
    Schema   collections.Schema
	Accounts *collections.IndexedMap[sdk.AccAddress, authtypes.BaseAccount, AccountsIndexes]
}

func NewKeeper(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

return Keeper{
    Accounts: collections.NewIndexedMap(
			sb, AccountsPrefix, "accounts",
			sdk.AccAddressKey, codec.CollValue[authtypes.BaseAccount](cdc),
			NewAccountIndexes(sb),
		),
}
}
正如这里所示,目前我们所做的事情与对 collections.Map 所做的相同。 我们传入 SchemaBuilder、计划存储 sdk.AccAddress 与 authtypes.BaseAccount 映射关系的 Prefix、人类可读名称,以及对应的 sdk.AccAddress key codec 和 authtypes.BaseAccount value codec。 然后通过 NewAccountIndexes 传入我们实例化好的 AccountIndexes。 完整示例:
package docs

import (
    
	"cosmossdk.io/collections"
    "cosmossdk.io/collections/indexes"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

return Keeper{
    Accounts: collections.NewIndexedMap(
			sb, AccountsPrefix, "accounts",
			sdk.AccAddressKey, codec.CollValue[authtypes.BaseAccount](cdc),
			NewAccountIndexes(sb),
		),
}
}

使用 IndexedMap

虽然实例化 collections.IndexedMap 比较繁琐,但实际使用起来非常顺畅。 我们来看完整示例,并在此基础上扩展一些用例。
package docs

import (
    
	"cosmossdk.io/collections"
    "cosmossdk.io/collections/indexes"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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
}

值为接口的集合

尽管 cosmos-sdk 正在逐步减少对接口注册表的使用,但仍然有一些地方在使用它。 为了支持旧代码,我们必须支持值为接口的集合。 通用的 codec.CollValue 无法处理接口值,因此我们需要使用一个特殊类型 codec.CollInterfaceValue。 codec.CollInterfaceValue 接收一个 codec.BinaryCodec 作为参数,并使用它将值作为接口进行编组和解组。 codec.CollInterfaceValue 位于 codec 包中,其导入路径为 github.com/cosmos/cosmos-sdk/codec。

实例化值为接口的集合

为了实例化值为接口的集合,我们需要使用 codec.CollInterfaceValue,而不是 codec.CollValue。
package example

import (
    
    "cosmossdk.io/collections"
    store "cosmossdk.io/core/store"
    "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, storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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)
}

三元键

collections.Triple 是一种由三个键组成的特殊键类型,与 collections.Pair 相同。 我们来看一个示例。
package example

import (
    
 "context"
    "cosmossdk.io/collections"
 store "cosmossdk.io/core/store"
)

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(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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())
})
}

高级用法

替代值编解码器

codec.AltValueCodec 允许集合使用与编码时不同的编解码器来解码值。 本质上,它可以解码同一个具体值的两种不同字节表示形式。 它可以用于将值从一种字节表示惰性迁移到另一种字节表示,前提是新的表示形式 无法解码旧的表示形式。 在 x/bank 中可以看到一个具体示例:余额最初存储为 Coin,随后迁移为 Int。
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
})
上面的示例展示了如何创建一个 AltValueCodec,使其既能解码 sdk.Int 值,也能解码 sdk.Coin 值。所提供的 解码函数会在默认解码器失败时作为回退使用。当该值再次被编码回状态中时, 它会使用默认编码器。这样就可以将值惰性迁移到新的字节表示形式。
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:
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:
package collections

import (
    
    "cosmossdk.io/collections"
    store "cosmossdk.io/core/store"
)

var AllowListPrefix = collections.NewPrefix(0)

type Keeper struct {
    Schema    collections.Schema
	AllowList collections.KeySet[string]
}

func NewKeeper(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

return Keeper{
    AllowList: collections.NewKeySet(sb, AllowListPrefix, "allow_list", collections.StringKey),
}
}
Let’s analyze 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 store.KVStoreService, which is the module’s store service obtained via dependency injection or runtime.NewKVStoreService. We then need to pass the schema builder to every collection type we instantiate in our keeper, in our case the AllowList. After creating all collections, call sb.Build() to validate prefix uniqueness and finalize the schema. Store the returned collections.Schema in the keeper’s Schema field:
k := Keeper{
    AllowList: collections.NewKeySet(sb, AllowListPrefix, "allow_list", collections.StringKey),
}
schema, err := sb.Build()
if err != nil {
    panic(err)
}
k.Schema = schema
return k
The code examples in this document show the collection instantiation patterns but omit the sb.Build() call for brevity. In production code, sb.Build() is required.

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: Link your old:
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:
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 int, string or []byte. It is good practice to use a monotonically increasing int (values 0–255) 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:
prefix1 := collections.NewPrefix("prefix")

prefix2 := collections.NewPrefix("prefix") // THIS IS BAD!
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 humanized 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:
package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
	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(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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):
package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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 analyze 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:
package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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. It will prepend to our bytes key the 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:
package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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

package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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.
Every collection shares the same Iterator semantics.
Let’s have a look at the Map.Iterate method:
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.
package collections

import (
    
	"cosmossdk.io/collections"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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 analyze 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 specialize 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.
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.
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 showcase 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

package collections

import (
    
	"cosmossdk.io/collections"
    "cosmossdk.io/math"
	store "cosmossdk.io/core/store"
	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(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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:
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:
var BalancesPrefix = collections.NewPrefix(1)

type Keeper struct {
    Schema   collections.Schema
	Balances collections.Map[collections.Pair[sdk.AccAddress, string], math.Int]
}

func NewKeeper(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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.
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 specialize 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:
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:
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:
*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 analyze 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:
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.
var AccountsPrefix = collections.NewPrefix(0)

type Keeper struct {
    Schema   collections.Schema
	Accounts *collections.IndexedMap[sdk.AccAddress, authtypes.BaseAccount, AccountsIndexes]
}

func NewKeeper(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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:
package docs

import (
    
	"cosmossdk.io/collections"
    "cosmossdk.io/collections/indexes"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

return Keeper{
    Accounts: collections.NewIndexedMap(
			sb, AccountsPrefix, "accounts",
			sdk.AccAddressKey, codec.CollValue[authtypes.BaseAccount](cdc),
			NewAccountIndexes(sb),
		),
}
}

Working with IndexedMaps

While instantiating collections.IndexedMap is tedious, working with them is extremely smooth. Let’s take the full example, and expand it with some use-cases.
package docs

import (
    
	"cosmossdk.io/collections"
    "cosmossdk.io/collections/indexes"
	store "cosmossdk.io/core/store"
    "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(storeService store.KVStoreService, cdc codec.BinaryCodec)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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.CollInterfaceValue. codec.CollInterfaceValue takes a codec.BinaryCodec as an argument, and uses it to marshal and unmarshal values as interfaces. The codec.CollInterfaceValue 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.CollInterfaceValue instead of codec.CollValue.
package example

import (
    
    "cosmossdk.io/collections"
    store "cosmossdk.io/core/store"
    "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, storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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.
package example

import (
    
 "context"
    "cosmossdk.io/collections"
 store "cosmossdk.io/core/store"
)

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(storeService store.KVStoreService)

Keeper {
    sb := collections.NewSchemaBuilder(storeService)

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.
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.