51 个版本 (5 个稳定版)
2.0.0 | 2024年3月14日 |
---|---|
2.0.0-beta.0 | 2024年1月23日 |
1.2.0 | 2023年11月14日 |
1.1.0 | 2023年6月13日 |
0.3.2 | 2020年10月28日 |
#1462 in 魔法豆
55,784 次每月下载
用于 518 个crate (357直接使用)
365KB
8K SLoC
cw-storage-plus
: 为CosmWasm提供的存储抽象
该库已在许多生产质量合约中得到广泛使用。代码已证明其稳定性与强大功能。尚未经过审计,Confio不承担任何责任,但我们认为它足够成熟,可以成为您合约的 标准存储层。
使用概述
我们引入了两个主要类来在 cosmwasm_std::Storage
上提供高效抽象。它们是 Item
,它是对一个数据库键的封装,提供了一些辅助函数,以与它交互而不需要处理原始字节。还有 Map
,允许你在前缀下存储多个唯一的类型对象,通过简单的或复合(例如,(&[u8], &[u8])
)主键索引。
Item
使用一个 Item
的方法非常简单。您只需提供正确的类型以及一个未被任何其他项目使用的数据库键。然后它将为您提供与这类数据交互的便捷接口。
如果您之前使用过 Singleton
,最大的变化是我们不再在内部存储 Storage
,这意味着我们不需要对象的读写变体,只需要一种类型。此外,我们使用 const fn
来创建 Item
,允许其作为一个全局编译时常量来定义,而不是每次都必须构造的函数,这既节省了气体又节省了输入。
示例用法
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Config {
pub owner: String,
pub max_tokens: i32,
}
// note const constructor rather than 2 functions with Singleton
const CONFIG: Item<Config> = Item::new("config");
fn demo() -> StdResult<()> {
let mut store = MockStorage::new();
// may_load returns Option<T>, so None if data is missing
// load returns T and Err(StdError::NotFound{}) if data is missing
let empty = CONFIG.may_load(&store)?;
assert_eq!(None, empty);
let cfg = Config {
owner: "admin".to_string(),
max_tokens: 1234,
};
CONFIG.save(&mut store, &cfg)?;
let loaded = CONFIG.load(&store)?;
assert_eq!(cfg, loaded);
// update an item with a closure (includes read and write)
// returns the newly saved value
let output = CONFIG.update(&mut store, |mut c| -> StdResult<_> {
c.max_tokens *= 2;
Ok(c)
})?;
assert_eq!(2468, output.max_tokens);
// you can error in an update and nothing is saved
let failed = CONFIG.update(&mut store, |_| -> StdResult<_> {
Err(StdError::generic_err("failure mode"))
});
assert!(failed.is_err());
// loading data will show the first update was saved
let loaded = CONFIG.load(&store)?;
let expected = Config {
owner: "admin".to_string(),
max_tokens: 2468,
};
assert_eq!(expected, loaded);
// we can remove data as well
CONFIG.remove(&mut store);
let empty = CONFIG.may_load(&store)?;
assert_eq!(None, empty);
Ok(())
}
映射
Map
的使用稍微复杂一些,但仍然非常简单。您可以将其想象为一个基于存储的 BTreeMap
,允许具有类型值的键值查找。此外,我们不仅支持简单的二进制键(如 &[u8]
),还支持组合元组。这允许我们通过示例将津贴作为复合键存储,即 (owner, spender)
来查找余额。
除了直接查找之外,我们还有一种在以太坊中找不到的超能力——迭代。没错,您可以在一个 Map
中列出所有项,或者只列出其中的一部分。我们还可以高效地允许对这些项进行分页,从上次查询结束的点开始,以低气体成本。这需要在 cw-storage-plus
中启用 iterator
功能(它在 cosmwasm-std
中也自动启用,并且默认启用)。
如果您之前使用过 Bucket
,最大的变化是我们不再在内部存储 Storage
,这意味着我们不需要对象的读写变体,只需要一种类型。此外,我们使用 const fn
来创建 Bucket
,允许其作为一个全局编译时常量来定义,而不是每次都必须构造的函数,这既节省了气体又节省了输入。此外,复合索引(元组)更符合人体工程学且更能表达意图,范围接口也得到了改进。
以下是一个使用正常(简单)键的示例
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct Data {
pub name: String,
pub age: i32,
}
const PEOPLE: Map<&str, Data> = Map::new("people");
fn demo() -> StdResult<()> {
let mut store = MockStorage::new();
let data = Data {
name: "John".to_string(),
age: 32,
};
// load and save with extra key argument
let empty = PEOPLE.may_load(&store, "john")?;
assert_eq!(None, empty);
PEOPLE.save(&mut store, "john", &data)?;
let loaded = PEOPLE.load(&store, "john")?;
assert_eq!(data, loaded);
// nothing on another key
let missing = PEOPLE.may_load(&store, "jack")?;
assert_eq!(None, missing);
// update function for new or existing keys
let birthday = |d: Option<Data>| -> StdResult<Data> {
match d {
Some(one) => Ok(Data {
name: one.name,
age: one.age + 1,
}),
None => Ok(Data {
name: "Newborn".to_string(),
age: 0,
}),
}
};
let old_john = PEOPLE.update(&mut store, "john", birthday)?;
assert_eq!(33, old_john.age);
assert_eq!("John", old_john.name.as_str());
let new_jack = PEOPLE.update(&mut store, "jack", birthday)?;
assert_eq!(0, new_jack.age);
assert_eq!("Newborn", new_jack.name.as_str());
// update also changes the store
assert_eq!(old_john, PEOPLE.load(&store, "john")?);
assert_eq!(new_jack, PEOPLE.load(&store, "jack")?);
// removing leaves us empty
PEOPLE.remove(&mut store, "john");
let empty = PEOPLE.may_load(&store, "john")?;
assert_eq!(None, empty);
Ok(())
}
键类型
Map
的键可以是实现了 PrimaryKey
特性的任何内容。已经提供了一系列 PrimaryKey
的实现(请参阅 keys.rs)
impl<'a> PrimaryKey<'a> for &'a [u8]
impl<'a> PrimaryKey<'a> for &'a str
impl<'a> PrimaryKey<'a> for Vec<u8>
impl<'a> PrimaryKey<'a> for String
impl<'a> PrimaryKey<'a> for Addr
impl<'a, constN: usize> PrimaryKey<'a> for [u8; N]
impl<'a, T:Prefixer<'a>> Prefixer<'a> for &'a T
impl<'a, T:PrimaryKey<'a> +Prefixer<'a>, U:PrimaryKey<'a>> PrimaryKey<'a> for (T, U)
impl<'a, T:PrimaryKey<'a> +Prefixer<'a>, U:PrimaryKey<'a> +Prefixer<'a>, V:PrimaryKey<'a>> PrimaryKey<'a> for (T, U, V)
PrimaryKey
为无符号整数实现了,上限为u128
PrimaryKey
为有符号整数实现了,上限为i128
这意味着字节和字符串切片、字节向量和字符串都可以方便地用作键。此外,还可以使用一些其他类型,例如地址和地址引用、成对和三重组合,以及整数类型。
如果键代表一个地址,我们建议在存储中使用 &Addr
作为键,而不是 String
或字符串切片。这意味着通过 addr_validate
在通过消息传入的任何地址上进行地址验证,以确保它是有效的地址,而不是后来会失败的随机文本。在 deps.api
中的 pub fn addr_validate(&self, &str) -> Addr
可以用于地址验证,然后返回的 Addr
可以方便地用作 Map
或类似结构中的键。
使用引用(即借用值)而不是值作为键(即 &Addr
而不是 Addr
),通常可以节省一些在键的读取/写入过程中的克隆操作。
复合键
有时我们希望使用多个项作为键。例如,当根据账户所有者和支出者存储津贴时。我们可以在调用之前尝试手动连接它们,但这可能导致冲突,而且对我们来说有点底层。另外,通过明确分离键,我们可以轻松提供辅助工具来进行前缀范围查询,例如“显示某个所有者的所有津贴”(复合键的第一部分)。就像你从你最喜欢的数据库中期望的那样。
以下是使用复合键的方法。只需定义一个元组作为键,并在使用单个键的任何地方使用它。
// Note the tuple for primary key. We support one slice, or a 2 or 3-tuple.
// Adding longer tuples is possible, but unlikely to be needed.
const ALLOWANCE: Map<(&str, &str), u64> = Map::new("allow");
fn demo() -> StdResult<()> {
let mut store = MockStorage::new();
// save and load on a composite key
let empty = ALLOWANCE.may_load(&store, ("owner", "spender"))?;
assert_eq!(None, empty);
ALLOWANCE.save(&mut store, ("owner", "spender"), &777)?;
let loaded = ALLOWANCE.load(&store, ("owner", "spender"))?;
assert_eq!(777, loaded);
// doesn't appear under other key (even if a concat would be the same)
let different = ALLOWANCE.may_load(&store, ("owners", "pender")).unwrap();
assert_eq!(None, different);
// simple update
ALLOWANCE.update(&mut store, ("owner", "spender"), |v| {
Ok(v.unwrap_or_default() + 222)
})?;
let loaded = ALLOWANCE.load(&store, ("owner", "spender"))?;
assert_eq!(999, loaded);
Ok(())
}
路径
在幕后,当访问键时,我们会从 Map
创建一个 Path
。例如,PEOPLE.load(&store, "jack") == PEOPLE.key("jack").load()
。 Map.key()
返回一个 Path
,它具有与 Item
相同的接口,重新使用计算出的路径到这个键。
对于简单键,这只是一个更少的输入和更少的气体,如果你为许多调用使用相同的键。然而,对于复合键,例如 ("owner", "spender")
,这将大大减少输入。强烈建议在任何需要使用复合键的地方,即使只使用两次。
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct Data {
pub name: String,
pub age: i32,
}
const PEOPLE: Map<&str, Data> = Map::new("people");
const ALLOWANCE: Map<(&str, &str), u64> = Map::new("allow");
fn demo() -> StdResult<()> {
let mut store = MockStorage::new();
let data = Data {
name: "John".to_string(),
age: 32,
};
// create a Path one time to use below
let john = PEOPLE.key("john");
// Use this just like an Item above
let empty = john.may_load(&store)?;
assert_eq!(None, empty);
john.save(&mut store, &data)?;
let loaded = john.load(&store)?;
assert_eq!(data, loaded);
john.remove(&mut store);
let empty = john.may_load(&store)?;
assert_eq!(None, empty);
// Same for composite keys, just use both parts in `key()`.
// Notice how much less verbose than the above example.
let allow = ALLOWANCE.key(("owner", "spender"));
allow.save(&mut store, &1234)?;
let loaded = allow.load(&store)?;
assert_eq!(1234, loaded);
allow.update(&mut store, |x| Ok(x.unwrap_or_default() * 2))?;
let loaded = allow.load(&store)?;
assert_eq!(2468, loaded);
Ok(())
}
前缀
除了从映射中获取特定项外,我们还可以遍历映射(或映射的子集)。这使我们能够回答诸如“显示所有令牌”等问题,并提供了一些便捷的Bound
辅助函数,以便轻松实现分页或自定义范围。
通用格式是调用map.prefix(k)
来获取Prefix
,其中k
比正常键少一个项(如果map.key()
获取了[u8], &[u8])
,那么map.prefix()
将获取&[u8]
。如果map.key()
获取了&[u8]
,那么map.prefix()
将获取()
)。一旦我们有了前缀空间,我们就可以通过range(store, min, max, order)
来遍历所有项。它支持Order::
Ascending(升序)或Order::
Descending(降序)。min
是下界,而max
是上界。
如果min
和max
界限是None
,则range
将返回前缀下的所有项。您可以使用.take(n)
来限制结果为n
个项并开始分页。您还可以将min
界限设置为例如Bound::
exclusive(last_value)以从最后一个值开始迭代所有项。与take
结合使用,我们很容易实现分页支持。您还可以在想要包含任何完美匹配时使用Bound::
inclusive(x)。
Bound
Bound
是构建类型安全的键或子键迭代界限的辅助工具。它还支持原始(Vec<u8>
)界限规范,用于您不希望或无法使用类型界限的情况。
#[derive(Clone, Debug)]
pub enum Bound<'a, K: PrimaryKey<'a>> {
Inclusive((K, PhantomData<&'a bool>)),
Exclusive((K, PhantomData<&'a bool>)),
InclusiveRaw(Vec<u8>),
ExclusiveRaw(Vec<u8>),
}
为了更好地理解API,请查看以下示例
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct Data {
pub name: String,
pub age: i32,
}
const PEOPLE: Map<&str, Data> = Map::new("people");
const ALLOWANCE: Map<(&str, &str), u64> = Map::new("allow");
fn demo() -> StdResult<()> {
let mut store = MockStorage::new();
// save and load on two keys
let data = Data { name: "John".to_string(), age: 32 };
PEOPLE.save(&mut store, "john", &data)?;
let data2 = Data { name: "Jim".to_string(), age: 44 };
PEOPLE.save(&mut store, "jim", &data2)?;
// iterate over them all
let all: StdResult<Vec<_>> = PEOPLE
.range(&store, None, None, Order::Ascending)
.collect();
assert_eq!(
all?,
vec![("jim".to_vec(), data2), ("john".to_vec(), data.clone())]
);
// or just show what is after jim
let all: StdResult<Vec<_>> = PEOPLE
.range(
&store,
Some(Bound::exclusive("jim")),
None,
Order::Ascending,
)
.collect();
assert_eq!(all?, vec![("john".to_vec(), data)]);
// save and load on three keys, one under different owner
ALLOWANCE.save(&mut store, ("owner", "spender"), &1000)?;
ALLOWANCE.save(&mut store, ("owner", "spender2"), &3000)?;
ALLOWANCE.save(&mut store, ("owner2", "spender"), &5000)?;
// get all under one key
let all: StdResult<Vec<_>> = ALLOWANCE
.prefix("owner")
.range(&store, None, None, Order::Ascending)
.collect();
assert_eq!(
all?,
vec![("spender".to_vec(), 1000), ("spender2".to_vec(), 3000)]
);
// Or ranges between two items (even reverse)
let all: StdResult<Vec<_>> = ALLOWANCE
.prefix("owner")
.range(
&store,
Some(Bound::exclusive("spender")),
Some(Bound::inclusive("spender2")),
Order::Descending,
)
.collect();
assert_eq!(all?, vec![("spender2".to_vec(), 3000)]);
Ok(())
}
注意:关于在MultiIndex
上定义和使用类型安全的界限,请参阅下文中的类型安全的MultiIndex
界限。
IndexedMap
让我们通过一个例子来查看IndexedMap
的定义和使用,这个例子最初来自cw721-base
合约。
定义
pub struct TokenIndexes<'a> {
pub owner: MultiIndex<'a, Addr, TokenInfo, String>,
}
impl<'a> IndexList<TokenInfo> for TokenIndexes<'a> {
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<TokenInfo>> + '_> {
let v: Vec<&dyn Index<TokenInfo>> = vec![&self.owner];
Box::new(v.into_iter())
}
}
pub fn tokens<'a>() -> IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>> {
let indexes = TokenIndexes {
owner: MultiIndex::new(
|d: &TokenInfo| d.owner.clone(),
"tokens",
"tokens__owner",
),
};
IndexedMap::new("tokens", indexes)
}
让我们逐一讨论
pub struct TokenIndexes<'a> {
pub owner: MultiIndex<'a, Addr, TokenInfo, String>,
}
这些是索引定义。这里只有一个索引,称为owner
。可能会有更多,作为TokenIndexes
结构体的公共成员。我们看到owner
索引是一个MultiIndex
。多索引可以作为键值重复。主键在内部作为多索引键的最后一个元素使用,以区分重复的索引值。正如其名所示,这是一个按所有者对代币进行索引的索引。鉴于一个所有者可以有多个代币,我们需要一个MultiIndex
来列出/遍历他拥有的所有代币。
TokenInfo
数据最初将按token_id
(这是一个字符串值)存储。您可以在创建代币的代码中看到这一点
tokens().update(deps.storage, &msg.token_id, |old| match old {
Some(_) => Err(ContractError::Claimed {}),
None => Ok(token),
})?;
(顺便提一下,这里使用update
而不是save
,以避免覆盖已存在的代币)。
由于token_id
是一个字符串值,我们指定String
作为MultiIndex
定义的最后一个参数。这样,主键的反序列化将完成到正确的类型(一个拥有字符串)。
注意:在特定情况下,对于MultiIndex
和类型安全界限的最新实现,最后一个类型参数的定义对于正确使用类型安全界限至关重要。请参阅下文中的类型安全的MultiIndex
界限。
然后,此TokenInfo
数据将通过代币所有者owner
(这是一个Addr
)进行索引。这样我们就可以列出所有者拥有的所有代币。这就是为什么owner
索引键是Addr
的原因。
这里另一个重要的事情是,键(及其组件,在复合键的情况下)必须实现PrimaryKey
特质。您可以看到Addr
实现了PrimaryKey
impl<'a> PrimaryKey<'a> for Addr {
type Prefix = ();
type SubPrefix = ();
type Suffix = Self;
type SuperSuffix = Self;
fn key(&self) -> Vec<Key> {
// this is simple, we don't add more prefixes
vec![Key::Ref(self.as_bytes())]
}
}
现在我们可以通过查看剩余的代码来了解它是如何工作的
impl<'a> IndexList<TokenInfo> for TokenIndexes<'a> {
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<TokenInfo>> + '_> {
let v: Vec<&dyn Index<TokenInfo>> = vec![&self.owner];
Box::new(v.into_iter())
}
}
这为TokenIndexes
实现了IndexList
特质。
注意:此代码几乎是模板化的,并且对于内部需要。不要尝试自定义此代码;只需返回所有索引的列表。实现此特质有两个目的(实际上是一个目的):它允许通过get_indexes
查询索引,并允许TokenIndexes
被视为一个IndexList
。这样,它就可以在IndexedMap
构造函数下作为参数传递
pub fn tokens<'a>() -> IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>> {
let indexes = TokenIndexes {
owner: MultiIndex::new(
|d: &TokenInfo| d.owner.clone(),
"tokens",
"tokens__owner",
),
};
IndexedMap::new("tokens", indexes)
}
在这里tokens()
只是一个辅助函数,它简化了我们的IndexedMap
构造。首先创建索引(es),然后创建并返回IndexedMap
。
在创建索引的过程中,我们必须为每个索引提供一个索引函数
owner: MultiIndex::new(|d: &TokenInfo| d.owner.clone(),
这是将原始映射的值取出并从中创建索引键的那个。当然,这要求索引键所需元素必须存在于值中。除了索引函数外,我们还必须提供pk的命名空间和新的索引的命名空间。
之后,我们只需创建并返回 IndexedMap
IndexedMap::new("tokens", indexes)
当然,这里的pk命名空间必须与创建索引时使用的命名空间相匹配。此外,我们将我们的 TokenIndexes
(作为 IndexList
-类型参数)作为第二个参数传递。以这种方式连接底层 Map
的pk,与定义的索引。
因此,IndexedMap
(以及其他 Indexed*
类型)只是 Map
的包装/扩展,提供了一些索引函数和命名空间来在原始 Map
数据上创建索引。它还实现了在值存储/更新/删除期间调用这些索引函数,这样您就可以忘记它,只需使用索引数据。
用法
使用示例,其中 owner
是作为参数传递的 String
值,而 start_after
和 limit
可选地定义了分页范围
注意这里使用了 prefix()
,如上文所述的 Map
部分。
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let owner_addr = deps.api.addr_validate(&owner)?;
let res: Result<Vec<_>, _> = tokens()
.idx
.owner
.prefix(owner_addr)
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.collect();
let tokens = res?;
现在 tokens
包含了给定 owner
的 (token_id, TokenInfo)
对。在 range_raw()
的情况下,pk值是 Vec<u8>
,但将使用 range()
反序列化为正确的类型;前提是pk反序列化类型(在本例中是 String
)已在 MultiIndex
定义中正确指定(见下文 索引键反序列化)。
另一个类似的示例,但只返回(原始)token_id
,使用 keys_raw()
方法
let pks: Vec<_> = tokens()
.idx
.owner
.prefix(owner_addr)
.keys_raw(
deps.storage,
start,
None,
Order::Ascending,
)
.take(limit)
.collect();
现在 pks
包含了给定 owner
的 token_id
值(作为原始 Vec<u8>
)。通过使用 keys
,可以获取反序列化的键,如下一节所述。
索引键反序列化
对于 UniqueIndex
和 MultiIndex
,需要指定主键(PK
)类型,以便将主键反序列化到它。此 PK
类型指定对于 MultiIndex
类型安全的范围也很重要,因为主键是多索引键的一部分。参见下一节,多索引的类型安全范围。
注意:此规范仍然是一个手动(因此容易出错)的过程/设置,将来(如果可能)将自动化(https://github.com/CosmWasm/cw-plus/issues/531)。
多索引的类型安全范围
在MultiIndex
的特定情况下,主键(PK
)类型参数也定义了索引键(即与主键对应的部分)的(部分)边界类型。因此,为了正确使用多索引范围的类型安全边界,确保这个PK
类型被正确定义是至关重要的,使其与主键类型或其(通常是拥有的)反序列化变体相匹配。
Deque
Deque
的使用相当直接。从概念上讲,它类似于Rust std的存储后端版本的Deque
,可以用作队列或栈。它允许你在两端推送和弹出元素,也可以读取第一个或最后一个元素而不修改deque。你还可以直接读取特定索引。
示例用法
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct Data {
pub name: String,
pub age: i32,
}
const DATA: Deque<Data> = Deque::new("data");
fn demo() -> StdResult<()> {
let mut store = MockStorage::new();
// read methods return a wrapped Option<T>, so None if the deque is empty
let empty = DATA.front(&store)?;
assert_eq!(None, empty);
// some example entries
let p1 = Data {
name: "admin".to_string(),
age: 1234,
};
let p2 = Data {
name: "user".to_string(),
age: 123,
};
// use it like a queue by pushing and popping at opposite ends
DATA.push_back(&mut store, &p1)?;
DATA.push_back(&mut store, &p2)?;
let admin = DATA.pop_front(&mut store)?;
assert_eq!(admin.as_ref(), Some(&p1));
let user = DATA.pop_front(&mut store)?;
assert_eq!(user.as_ref(), Some(&p2));
// or push and pop at the same end to use it as a stack
DATA.push_back(&mut store, &p1)?;
DATA.push_back(&mut store, &p2)?;
let user = DATA.pop_back(&mut store)?;
assert_eq!(user.as_ref(), Some(&p2));
let admin = DATA.pop_back(&mut store)?;
assert_eq!(admin.as_ref(), Some(&p1));
// you can also iterate over it
DATA.push_front(&mut store, &p1)?;
DATA.push_front(&mut store, &p2)?;
let all: StdResult<Vec<_>> = DATA.iter(&store)?.collect();
assert_eq!(all?, [p2, p1]);
// or access an index directly
assert_eq!(DATA.get(&store, 0)?, Some(p2));
assert_eq!(DATA.get(&store, 1)?, Some(p1));
assert_eq!(DATA.get(&store, 3)?, None);
Ok(())
}
依赖项
~3.5–7MB
~142K SLoC