From 57df1d9e23f23ef47f50beca4614cb52a896698f Mon Sep 17 00:00:00 2001 From: Shane Vitarana Date: Wed, 20 Apr 2022 13:34:46 -0400 Subject: [PATCH 01/17] Move test helpers into a test section --- packages/storage-plus/src/indexed_map.rs | 4 +-- packages/storage-plus/src/indexed_snapshot.rs | 4 +-- packages/storage-plus/src/indexes/mod.rs | 36 ++++++++++--------- packages/storage-plus/src/lib.rs | 4 +-- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/storage-plus/src/indexed_map.rs b/packages/storage-plus/src/indexed_map.rs index 55d572958..ca2ba8b78 100644 --- a/packages/storage-plus/src/indexed_map.rs +++ b/packages/storage-plus/src/indexed_map.rs @@ -277,8 +277,8 @@ where mod test { use super::*; - use crate::indexes::index_string_tuple; - use crate::{index_tuple, MultiIndex, UniqueIndex}; + use crate::indexes::test::{index_string_tuple, index_tuple}; + use crate::{MultiIndex, UniqueIndex}; use cosmwasm_std::testing::MockStorage; use cosmwasm_std::{MemoryStorage, Order}; use serde::{Deserialize, Serialize}; diff --git a/packages/storage-plus/src/indexed_snapshot.rs b/packages/storage-plus/src/indexed_snapshot.rs index 2b152abb3..89987551e 100644 --- a/packages/storage-plus/src/indexed_snapshot.rs +++ b/packages/storage-plus/src/indexed_snapshot.rs @@ -302,8 +302,8 @@ where mod test { use super::*; - use crate::indexes::index_string_tuple; - use crate::{index_tuple, Index, MultiIndex, UniqueIndex}; + use crate::indexes::test::{index_string_tuple, index_tuple}; + use crate::{Index, MultiIndex, UniqueIndex}; use cosmwasm_std::testing::MockStorage; use cosmwasm_std::{MemoryStorage, Order}; use serde::{Deserialize, Serialize}; diff --git a/packages/storage-plus/src/indexes/mod.rs b/packages/storage-plus/src/indexes/mod.rs index ad7e8b46a..7c6bed1d6 100644 --- a/packages/storage-plus/src/indexes/mod.rs +++ b/packages/storage-plus/src/indexes/mod.rs @@ -11,22 +11,6 @@ use serde::Serialize; use cosmwasm_std::{StdResult, Storage}; -pub fn index_string(data: &str) -> Vec { - data.as_bytes().to_vec() -} - -pub fn index_tuple(name: &str, age: u32) -> (Vec, u32) { - (index_string(name), age) -} - -pub fn index_triple(name: &str, age: u32, pk: Vec) -> (Vec, u32, Vec) { - (index_string(name), age, pk) -} - -pub fn index_string_tuple(data1: &str, data2: &str) -> (Vec, Vec) { - (index_string(data1), index_string(data2)) -} - // Note: we cannot store traits with generic functions inside `Box`, // so I pull S: Storage to a top-level pub trait Index @@ -36,3 +20,23 @@ where fn save(&self, store: &mut dyn Storage, pk: &[u8], data: &T) -> StdResult<()>; fn remove(&self, store: &mut dyn Storage, pk: &[u8], old_data: &T) -> StdResult<()>; } + +#[cfg(test)] +pub mod test { + + pub fn index_string(data: &str) -> Vec { + data.as_bytes().to_vec() + } + + pub fn index_tuple(name: &str, age: u32) -> (Vec, u32) { + (index_string(name), age) + } + + pub fn index_triple(name: &str, age: u32, pk: Vec) -> (Vec, u32, Vec) { + (index_string(name), age, pk) + } + + pub fn index_string_tuple(data1: &str, data2: &str) -> (Vec, Vec) { + (index_string(data1), index_string(data2)) + } +} diff --git a/packages/storage-plus/src/lib.rs b/packages/storage-plus/src/lib.rs index 7035d9a25..5562073c0 100644 --- a/packages/storage-plus/src/lib.rs +++ b/packages/storage-plus/src/lib.rs @@ -25,11 +25,11 @@ pub use indexed_map::{IndexList, IndexedMap}; #[cfg(feature = "iterator")] pub use indexed_snapshot::IndexedSnapshotMap; #[cfg(feature = "iterator")] +pub use indexes::Index; +#[cfg(feature = "iterator")] pub use indexes::MultiIndex; #[cfg(feature = "iterator")] pub use indexes::UniqueIndex; -#[cfg(feature = "iterator")] -pub use indexes::{index_string, index_string_tuple, index_triple, index_tuple, Index}; pub use int_key::CwIntKey; pub use item::Item; pub use keys::{Key, Prefixer, PrimaryKey}; From bdef5d6a2391db54fca3d6d8e226abdf8c1c1be2 Mon Sep 17 00:00:00 2001 From: Shane Vitarana Date: Wed, 20 Apr 2022 13:41:03 -0400 Subject: [PATCH 02/17] Removed dead code --- packages/storage-plus/src/indexes/mod.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/storage-plus/src/indexes/mod.rs b/packages/storage-plus/src/indexes/mod.rs index 7c6bed1d6..e2639ac83 100644 --- a/packages/storage-plus/src/indexes/mod.rs +++ b/packages/storage-plus/src/indexes/mod.rs @@ -32,10 +32,6 @@ pub mod test { (index_string(name), age) } - pub fn index_triple(name: &str, age: u32, pk: Vec) -> (Vec, u32, Vec) { - (index_string(name), age, pk) - } - pub fn index_string_tuple(data1: &str, data2: &str) -> (Vec, Vec) { (index_string(data1), index_string(data2)) } From d4ec5c7e039908cb722f4b9785b58c309e7fc4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kuras?= Date: Fri, 22 Apr 2022 14:58:56 +0200 Subject: [PATCH 03/17] Removed documentation from Cargo.toml --- packages/controllers/Cargo.toml | 1 - packages/cw1/Cargo.toml | 1 - packages/cw1155/Cargo.toml | 1 - packages/cw2/Cargo.toml | 1 - packages/cw20/Cargo.toml | 1 - packages/cw3/Cargo.toml | 1 - packages/cw4/Cargo.toml | 1 - packages/multi-test/Cargo.toml | 1 - packages/storage-plus/Cargo.toml | 1 - packages/utils/Cargo.toml | 1 - 10 files changed, 10 deletions(-) diff --git a/packages/controllers/Cargo.toml b/packages/controllers/Cargo.toml index ed8607745..5dc42a747 100644 --- a/packages/controllers/Cargo.toml +++ b/packages/controllers/Cargo.toml @@ -7,7 +7,6 @@ description = "Common controllers we can reuse in many contracts" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/packages/cw1/Cargo.toml b/packages/cw1/Cargo.toml index f3584ce5b..a4ff1b55d 100644 --- a/packages/cw1/Cargo.toml +++ b/packages/cw1/Cargo.toml @@ -7,7 +7,6 @@ description = "Definition and types for the CosmWasm-1 interface" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" [dependencies] cosmwasm-std = { version = "1.0.0-beta8" } diff --git a/packages/cw1155/Cargo.toml b/packages/cw1155/Cargo.toml index 2ce6796aa..140166170 100644 --- a/packages/cw1155/Cargo.toml +++ b/packages/cw1155/Cargo.toml @@ -7,7 +7,6 @@ description = "Definition and types for the CosmWasm-1155 interface" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" [dependencies] cw-utils = { path = "../../packages/utils", version = "0.13.2" } diff --git a/packages/cw2/Cargo.toml b/packages/cw2/Cargo.toml index 5b0ea1997..10974798a 100644 --- a/packages/cw2/Cargo.toml +++ b/packages/cw2/Cargo.toml @@ -7,7 +7,6 @@ description = "Definition and types for the CosmWasm-2 interface" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" [dependencies] cosmwasm-std = { version = "1.0.0-beta8", default-features = false } diff --git a/packages/cw20/Cargo.toml b/packages/cw20/Cargo.toml index 353ab8d9b..88738a164 100644 --- a/packages/cw20/Cargo.toml +++ b/packages/cw20/Cargo.toml @@ -7,7 +7,6 @@ description = "Definition and types for the CosmWasm-20 interface" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" [dependencies] cw-utils = { path = "../../packages/utils", version = "0.13.2" } diff --git a/packages/cw3/Cargo.toml b/packages/cw3/Cargo.toml index 6a9d45b41..97bf2144f 100644 --- a/packages/cw3/Cargo.toml +++ b/packages/cw3/Cargo.toml @@ -7,7 +7,6 @@ description = "CosmWasm-3 Interface: On-Chain MultiSig/Voting contracts" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" [dependencies] cw-utils = { path = "../../packages/utils", version = "0.13.2" } diff --git a/packages/cw4/Cargo.toml b/packages/cw4/Cargo.toml index 9d9c972bb..bed66b033 100644 --- a/packages/cw4/Cargo.toml +++ b/packages/cw4/Cargo.toml @@ -7,7 +7,6 @@ description = "CosmWasm-4 Interface: Groups Members" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" [dependencies] cw-storage-plus = { path = "../storage-plus", version = "0.13.2" } diff --git a/packages/multi-test/Cargo.toml b/packages/multi-test/Cargo.toml index 82505a7ba..96d0e09b1 100644 --- a/packages/multi-test/Cargo.toml +++ b/packages/multi-test/Cargo.toml @@ -7,7 +7,6 @@ description = "Test helpers for multi-contract interactions" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] diff --git a/packages/storage-plus/Cargo.toml b/packages/storage-plus/Cargo.toml index 6656060d1..e51ab096c 100644 --- a/packages/storage-plus/Cargo.toml +++ b/packages/storage-plus/Cargo.toml @@ -7,7 +7,6 @@ description = "Enhanced storage engines" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" [features] default = ["iterator"] diff --git a/packages/utils/Cargo.toml b/packages/utils/Cargo.toml index 26e25d475..965d63024 100644 --- a/packages/utils/Cargo.toml +++ b/packages/utils/Cargo.toml @@ -7,7 +7,6 @@ description = "Common helpers for other cw specs" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 5c1b39f68195c6b10c43529ba788d9a0746db6a3 Mon Sep 17 00:00:00 2001 From: Luke Park Date: Tue, 28 Jun 2022 22:19:06 +0900 Subject: [PATCH 04/17] fix: relocate contents --- packages/cw20/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/cw20/README.md b/packages/cw20/README.md index 970db35ff..35be5f736 100644 --- a/packages/cw20/README.md +++ b/packages/cw20/README.md @@ -144,6 +144,14 @@ the minter is a smart contract. This should be enabled with all blockchains that have iterator support. It allows us to get lists of results with pagination. +### Queries + +`AllAllowances{owner, start_after, limit}` - Returns the list of all non-expired allowances +by the given owner. `start_after` and `limit` provide pagination. + +`AllAccounts{start_after, limit}` - Returns the list of all accounts that have been created on +the contract (just the addresses). `start_after` and `limit` provide pagination. + ## Marketing This allows us to attach more metadata on the token to help with displaying the token in @@ -173,11 +181,3 @@ account, this will update some marketing-related metadata on the contract. `DownloadLogo{}` - If the token's logo was previously uploaded to the blockchain (see `UploadLogo` message), then it returns the raw data to be displayed in a browser. Return type is `DownloadLogoResponse{ mime_type, data }`. - -### Queries - -`AllAllowances{owner, start_after, limit}` - Returns the list of all non-expired allowances -by the given owner. `start_after` and `limit` provide pagination. - -`AllAccounts{start_after, limit}` - Returns the list of all accounts that have been created on -the contract (just the addresses). `start_after` and `limit` provide pagination. From 0d4eae49966cad7c4bfd0c0b5cdec840f623f1ea Mon Sep 17 00:00:00 2001 From: Zeke Medley Date: Wed, 6 Jul 2022 01:15:14 -0700 Subject: [PATCH 05/17] Add ability to unset minter in UpdateMinter message. --- contracts/cw20-base/src/contract.rs | 66 ++++++++++++++++++++++++----- packages/cw20/README.md | 5 +++ packages/cw20/src/msg.rs | 6 ++- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/contracts/cw20-base/src/contract.rs b/contracts/cw20-base/src/contract.rs index 2bdf482cb..09904fe15 100644 --- a/contracts/cw20-base/src/contract.rs +++ b/contracts/cw20-base/src/contract.rs @@ -393,7 +393,7 @@ pub fn execute_update_minter( deps: DepsMut, _env: Env, info: MessageInfo, - new_minter: String, + new_minter: Option, ) -> Result { let mut config = TOKEN_INFO .may_load(deps.storage)? @@ -404,18 +404,27 @@ pub fn execute_update_minter( return Err(ContractError::Unauthorized {}); } - let minter = deps.api.addr_validate(&new_minter)?; - let minter_data = MinterData { - minter, - cap: mint.cap, - }; - config.mint = Some(minter_data); + let minter_data = new_minter + .map(|new_minter| deps.api.addr_validate(&new_minter)) + .transpose()? + .map(|minter| MinterData { + minter, + cap: mint.cap, + }); + + config.mint = minter_data; TOKEN_INFO.save(deps.storage, &config)?; Ok(Response::default() .add_attribute("action", "update_minter") - .add_attribute("new_minter", new_minter)) + .add_attribute( + "new_minter", + config + .mint + .map(|m| m.minter.into_string()) + .unwrap_or_else(|| "None".to_string()), + )) } pub fn execute_update_marketing( @@ -927,7 +936,7 @@ mod tests { let new_minter = "new_minter"; let msg = ExecuteMsg::UpdateMinter { - new_minter: new_minter.to_string(), + new_minter: Some(new_minter.to_string()), }; let info = mock_info(&minter, &[]); @@ -956,7 +965,7 @@ mod tests { ); let msg = ExecuteMsg::UpdateMinter { - new_minter: String::from("new_minter"), + new_minter: Some("new_minter".to_string()), }; let info = mock_info("not the minter", &[]); @@ -965,6 +974,43 @@ mod tests { assert_eq!(err, ContractError::Unauthorized {}); } + #[test] + fn unset_minter() { + let mut deps = mock_dependencies(); + let minter = String::from("minter"); + let cap = None; + do_instantiate_with_minter( + deps.as_mut(), + &String::from("genesis"), + Uint128::new(1234), + &minter, + cap, + ); + + let msg = ExecuteMsg::UpdateMinter { new_minter: None }; + + let info = mock_info(&minter, &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env.clone(), info, msg); + assert!(res.is_ok()); + let query_minter_msg = QueryMsg::Minter {}; + let res = query(deps.as_ref(), env, query_minter_msg); + let mint: Option = from_binary(&res.unwrap()).unwrap(); + + // Check that mint information was removed. + assert_eq!(mint, None); + + // Check that old minter can no longer mint. + let msg = ExecuteMsg::Mint { + recipient: String::from("lucky"), + amount: Uint128::new(222), + }; + let info = mock_info("minter", &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + } + #[test] fn no_one_mints_if_minter_unset() { let mut deps = mock_dependencies(); diff --git a/packages/cw20/README.md b/packages/cw20/README.md index 35be5f736..f4705625b 100644 --- a/packages/cw20/README.md +++ b/packages/cw20/README.md @@ -128,6 +128,11 @@ minter address and handle updating the ACL there. this will create `amount` new tokens (updating total supply) and add them to the balance of `recipient`, as long as it does not exceed the cap. +`UpdateMinter { new_minter: Option }` - Callable only by the +current minter. If `new_minter` is `Some(address)` the minter is set +to the specified address, otherwise the minter is removed and no +future minters may be set. + ### Queries `Minter{}` - Returns who and how much can be minted. Return type is diff --git a/packages/cw20/src/msg.rs b/packages/cw20/src/msg.rs index 4cd6fba63..f2f9708c9 100644 --- a/packages/cw20/src/msg.rs +++ b/packages/cw20/src/msg.rs @@ -55,8 +55,10 @@ pub enum Cw20ExecuteMsg { /// Only with the "mintable" extension. If authorized, creates amount new tokens /// and adds to the recipient balance. Mint { recipient: String, amount: Uint128 }, - /// Only with the "mintable" extension. The current minter may set a new minter. - UpdateMinter { new_minter: String }, + /// Only with the "mintable" extension. The current minter may set + /// a new minter. Setting the minter to None will remove the + /// token's minter forever. + UpdateMinter { new_minter: Option }, /// Only with the "marketing" extension. If authorized, updates marketing metadata. /// Setting None/null for any of these will leave it unchanged. /// Setting Some("") will clear this field on the contract storage From 10ec43fa4d0f27fc6520b7c1ef47a804374ec6f3 Mon Sep 17 00:00:00 2001 From: Zeke Medley Date: Wed, 6 Jul 2022 01:23:02 -0700 Subject: [PATCH 06/17] Use into_iter() instead of iter().cloned(). --- packages/controllers/src/claim.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/controllers/src/claim.rs b/packages/controllers/src/claim.rs index 60025fce3..01083fecb 100644 --- a/packages/controllers/src/claim.rs +++ b/packages/controllers/src/claim.rs @@ -65,7 +65,7 @@ impl<'a> Claims<'a> { let mut to_send = Uint128::zero(); self.0.update(storage, addr, |claim| -> StdResult<_> { let (_send, waiting): (Vec<_>, _) = - claim.unwrap_or_default().iter().cloned().partition(|c| { + claim.unwrap_or_default().into_iter().partition(|c| { // if mature and we can pay fully, then include in _send if c.release_at.is_expired(block) { if let Some(limit) = cap { From 0f98cab091b91029f3417ddf930c7710cf2c282b Mon Sep 17 00:00:00 2001 From: Mike Purvis Date: Wed, 13 Jul 2022 09:36:17 -0700 Subject: [PATCH 07/17] broken links: sandynet and NFTs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bcb3e607e..31569da36 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ many custom contracts. If you don't know what CosmWasm is, please check out [our homepage](https://cosmwasm.com) and [our documentation](https://docs.cosmwasm.com) to get more background. -We are running a [public testnet](https://github.com/CosmWasm/testnets/blob/master/sandynet-1/README.md) +We are running [public testnets](https://github.com/CosmWasm/testnets#running) you can use to test out any contracts. **Warning** None of these contracts have been audited and no liability is @@ -62,7 +62,7 @@ cleaner (before: `expires: {at_height: {height: 12345}}` after The most reusable components are the various cwXYZ specifications under `packages`. Each one defines a standard interface for different domains, e.g. [cw20](./packages/cw20/README.md) for fungible tokens, -[cw721](./packages/cw721/README.md) for non-fungible tokens, +[cw721](https://github.com/CosmWasm/cw-nfts/blob/main/packages/cw721/README.md) for non-fungible tokens, [cw1](./packages/cw1/README.md) for "proxy contracts", etc. The interface comes with a human description in the READMEs, as well as Rust types that can be imported. From 13e0f9b6afa86b4c78547aecd2eecf61054d4db5 Mon Sep 17 00:00:00 2001 From: Mike Purvis Date: Wed, 13 Jul 2022 09:49:05 -0700 Subject: [PATCH 08/17] tiny grammar and typo in "Zeppelin" --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31569da36..c32b553ca 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ and depth of discussion can give an idea how much review was present. After that, fuzzing it (ideally with an intelligent fuzzer that understands the domain) can be valuable. And beyond that formal verification can provide even more assurance -(but is very time consuming and expensive). +(but is very time-consuming and expensive). ### Code Coverage @@ -213,7 +213,7 @@ relevant `Cargo.toml` file for clarity. All *specifications* will always be Apache-2.0. All contracts that are meant to be *building blocks* will also be Apache-2.0. This is along -the lines of Open Zepellin or other public references. +the lines of Open Zeppelin or other public references. Contracts that are "ready to deploy" may be licensed under AGPL 3.0 to encourage anyone using them to contribute back any improvements they From 297d1208131441cbec052bbe9e2832ac48be5677 Mon Sep 17 00:00:00 2001 From: Zeke Medley Date: Thu, 14 Jul 2022 13:59:10 -0700 Subject: [PATCH 09/17] Add entry to CHANGELOG.md. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecdbf257a..5135a2c3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ [Full Changelog](https://github.com/CosmWasm/cw-plus/compare/v0.13.4...HEAD) +**Merged pull requests:** + +- Add ability to unset minter in UpdateMinter message. [\#748](https://github.com/CosmWasm/cw-plus/pull/748) ([ezekiiel](https://github.com/ezekiiel)) + ## [v0.13.4](https://github.com/CosmWasm/cw-plus/tree/v0.13.4) (2022-06-02) [Full Changelog](https://github.com/CosmWasm/cw-plus/compare/v0.13.3...v0.13.4) From fc8d166c4d8234138ff0b1b5957ffddbcfcc151c Mon Sep 17 00:00:00 2001 From: Pakorn Nathong Date: Tue, 14 Jun 2022 11:31:12 +0700 Subject: [PATCH 10/17] Add storage macro package --- Cargo.lock | 34 +- packages/storage-macro/Cargo.toml | 25 + packages/storage-macro/NOTICE | 14 + packages/storage-macro/README.md | 637 +++++++++++++++++++++ packages/storage-macro/src/lib.rs | 37 ++ packages/storage-macro/tests/index_list.rs | 60 ++ 6 files changed, 795 insertions(+), 12 deletions(-) create mode 100644 packages/storage-macro/Cargo.toml create mode 100644 packages/storage-macro/NOTICE create mode 100644 packages/storage-macro/README.md create mode 100644 packages/storage-macro/src/lib.rs create mode 100644 packages/storage-macro/tests/index_list.rs diff --git a/Cargo.lock b/Cargo.lock index b7cb3c849..390bf2ab1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,6 +406,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw-storage-macro" +version = "0.13.4" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "serde", + "syn", +] + [[package]] name = "cw-storage-plus" version = "0.13.4" @@ -1022,11 +1032,11 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" -version = "1.0.38" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -1343,13 +1353,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.94" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -1410,16 +1420,16 @@ dependencies = [ ] [[package]] -name = "unicode-width" -version = "0.1.9" +name = "unicode-ident" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] -name = "unicode-xid" -version = "0.2.3" +name = "unicode-width" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "version_check" diff --git a/packages/storage-macro/Cargo.toml b/packages/storage-macro/Cargo.toml new file mode 100644 index 000000000..75cd97aa5 --- /dev/null +++ b/packages/storage-macro/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "cw-storage-macro" +version = "0.13.4" +authors = ["yoisha <48324733+y-pakorn@users.noreply.github.com>"] +edition = "2018" +description = "Macro helper for storage package" +license = "Apache-2.0" +repository = "https://github.com/CosmWasm/cw-plus" +homepage = "https://cosmwasm.com" +documentation = "https://docs.cosmwasm.com" + +[features] +default = ["iterator"] +iterator = ["cosmwasm-std/iterator"] + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "1.0.96", features = ["full"] } + +[dev-dependencies] +cw-storage-plus = { version = "0.13.4", path = "../storage-plus" } +cosmwasm-std = { version = "1.0.0", default-features = false } +serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/packages/storage-macro/NOTICE b/packages/storage-macro/NOTICE new file mode 100644 index 000000000..838a67f47 --- /dev/null +++ b/packages/storage-macro/NOTICE @@ -0,0 +1,14 @@ +CW-Storage-Plus: Enhanced/experimental storage engines for CosmWasm +Copyright (C) 2020 Confio OÜ + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/storage-macro/README.md b/packages/storage-macro/README.md new file mode 100644 index 000000000..3e2691df1 --- /dev/null +++ b/packages/storage-macro/README.md @@ -0,0 +1,637 @@ +# CW-Storage-Plus: Enhanced storage engines for CosmWasm + +After building `cosmwasm-storage`, we realized many of the design decisions were +limiting us and producing a lot of needless boilerplate. The decision was made to leave +those APIs stable for anyone wanting a very basic abstraction on the KV-store and to +build a much more powerful and complex ORM layer that can provide powerful accessors +using complex key types, which are transparently turned into bytes. + +This led to a number of breaking API changes in this package of the course of several +releases as we updated this with lots of experience, user feedback, and deep dives to harness +the full power of generics. + +**Status: beta** + +As of `cw-storage-plus` `v0.12` the API should be quite stable. +There are no major API breaking issues pending, and all API changes will be documented +in [`MIGRATING.md`](../../MIGRATING.md). + +This has been heavily used in many production-quality contracts. +The code has demonstrated itself to be stable and powerful. +It has not been audited, and Confio assumes no liability, but we consider it mature enough +to be the **standard storage layer** for your contracts. + +## Usage Overview + +We introduce two main classes to provide a productive abstraction +on top of `cosmwasm_std::Storage`. They are `Item`, which is +a typed wrapper around one database key, providing some helper functions +for interacting with it without dealing with raw bytes. And `Map`, +which allows you to store multiple unique typed objects under a prefix, +indexed by a simple (`&[u8]`) or compound (eg. `(&[u8], &[u8])`) primary key. + +These correspond to the concepts represented in `cosmwasm_storage` by +`Singleton` and `Bucket`, but with a re-designed API and implementation +to require less typing for developers and less gas usage in the contracts. + +## Item + +The usage of an [`Item`](./src/item.rs) is pretty straight-forward. +You must simply provide the proper type, as well as a database key not +used by any other item. Then it will provide you with a nice interface +to interact with such data. + +If you are coming from using `Singleton`, the biggest change is that +we no longer store `Storage` inside, meaning we don't need read and write +variants of the object, just one type. Furthermore, we use `const fn` +to create the `Item`, allowing it to be defined as a global compile-time +constant rather than a function that must be constructed each time, +which saves gas as well as typing. + +Example Usage: + +```rust +#[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 = Item::new("config"); + +fn demo() -> StdResult<()> { + let mut store = MockStorage::new(); + + // may_load returns Option, 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 + +The usage of a [`Map`](./src/map.rs) is a little more complex, but +is still pretty straight-forward. You can imagine it as a storage-backed +`BTreeMap`, allowing key-value lookups with typed values. In addition, +we support not only simple binary keys (`&[u8]`), but tuples, which are +combined. This allows us to store allowances as composite keys +eg. `(owner, spender)` to look up the balance. + +Beyond direct lookups, we have a super-power not found in Ethereum - +iteration. That's right, you can list all items in a `Map`, or only +part of them. We can efficiently allow pagination over these items as +well, starting at the point the last query ended, with low gas costs. +This requires the `iterator` feature to be enabled in `cw-storage-plus` +(which automatically enables it in `cosmwasm-std` as well, and which is +enabled by default). + +If you are coming from using `Bucket`, the biggest change is that +we no longer store `Storage` inside, meaning we don't need read and write +variants of the object, just one type. Furthermore, we use `const fn` +to create the `Bucket`, allowing it to be defined as a global compile-time +constant rather than a function that must be constructed each time, +which saves gas as well as typing. In addition, the composite indexes +(tuples) are more ergonomic and expressive of intention, and the range +interface has been improved. + +Here is an example with normal (simple) keys: + +```rust +#[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| -> StdResult { + 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(()) +} +``` + +### Key types + +A `Map` key can be anything that implements the `PrimaryKey` trait. There are a series of implementations of +`PrimaryKey` already provided (see [keys.rs](./src/keys.rs)): + + - `impl<'a> PrimaryKey<'a> for &'a [u8]` + - `impl<'a> PrimaryKey<'a> for &'a str` + - `impl<'a> PrimaryKey<'a> for Vec` + - `impl<'a> PrimaryKey<'a> for String` + - `impl<'a> PrimaryKey<'a> for Addr` + - `impl<'a> PrimaryKey<'a> for &'a Addr` + - `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` implemented for unsigned integers up to `u64` + - `PrimaryKey` implemented for signed integers up to `i64` + +That means that byte and string slices, byte vectors, and strings, can be conveniently used as keys. +Moreover, some other types can be used as well, like addresses and address references, pairs, triples, and +integer types. + +If the key represents an address, we suggest using `&Addr` for keys in storage, instead of `String` or string slices. +This implies doing address validation through `addr_validate` on any address passed in via a message, to ensure it's a +legitimate address, and not random text which will fail later. +`pub fn addr_validate(&self, &str) -> Addr` in `deps.api` can be used for address validation, and the returned `Addr` +can then be conveniently used as key in a `Map` or similar structure. + +It's also convenient to use references (i.e. borrowed values) instead of values for keys (i.e. `&Addr` instead of `Addr`), +as that will typically save some cloning during key reading / writing. + +### Composite Keys + +There are times when we want to use multiple items as a key. For example, when +storing allowances based on account owner and spender. We could try to manually +concatenate them before calling, but that can lead to overlap, and is a bit +low-level for us. Also, by explicitly separating the keys, we can easily provide +helpers to do range queries over a prefix, such as "show me all allowances for +one owner" (first part of the composite key). Just like you'd expect from your +favorite database. + +Here's how we use it with composite keys. Just define a tuple as a key and use that +everywhere you used a byte slice above. + +```rust +// 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(()) +} +``` + +### Path + +Under the scenes, we create a `Path` from the `Map` when accessing a key. +`PEOPLE.load(&store, b"jack") == PEOPLE.key(b"jack").load()`. +`Map.key()` returns a `Path`, which has the same interface as `Item`, +re-using the calculated path to this key. + +For simple keys, this is just a bit less typing and a bit less gas if you +use the same key for many calls. However, for composite keys, like +`(b"owner", b"spender")` it is **much** less typing. And highly recommended anywhere +you will use a composite key even twice: + +```rust +#[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(()) +} +``` + +### Prefix + +In addition to getting one particular item out of a map, we can iterate over the map +(or a subset of the map). This let us answer questions like "show me all tokens", +and we provide some nice [`Bound`](#bound) helpers to easily allow pagination or custom ranges. + +The general format is to get a `Prefix` by calling `map.prefix(k)`, where `k` is exactly +one less item than the normal key (If `map.key()` took `(&[u8], &[u8])`, then `map.prefix()` takes `&[u8]`. +If `map.key()` took `&[u8]`, `map.prefix()` takes `()`). Once we have a prefix space, we can iterate +over all items with `range(store, min, max, order)`. It supports `Order::Ascending` or `Order::Descending`. +`min` is the lower bound and `max` is the higher bound. + +If the `min` and `max` bounds are `None`, `range` will return all items under the prefix. You can use `.take(n)` to +limit the results to `n` items and start doing pagination. You can also set the `min` bound to +eg. `Bound::exclusive(last_value)` to start iterating over all items *after* the last value. Combined with +`take`, we easily have pagination support. You can also use `Bound::inclusive(x)` when you want to include any +perfect matches. + +### Bound + +`Bound` is a helper to build type-safe bounds on the keys or sub-keys you want to iterate over. +It also supports a raw (`Vec`) bounds specification, for the cases you don't want or can't use typed bounds. + +```rust +#[derive(Clone, Debug)] +pub enum Bound<'a, K: PrimaryKey<'a>> { + Inclusive((K, PhantomData<&'a bool>)), + Exclusive((K, PhantomData<&'a bool>)), + InclusiveRaw(Vec), + ExclusiveRaw(Vec), +} +``` + +To better understand the API, please check the following example: +```rust +#[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> = 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> = 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> = 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> = ALLOWANCE + .prefix("owner") + .range( + &store, + Some(Bound::exclusive("spender1")), + Some(Bound::inclusive("spender2")), + Order::Descending, + ) + .collect(); + assert_eq!(all?, vec![("spender2".to_vec(), 3000)]); + + Ok(()) +} +``` + +**NB**: For properly defining and using type-safe bounds over a `MultiIndex`, see [Type-safe bounds over `MultiIndex`](#type-safe-bounds-over-multiindex), +below. + +## IndexedMap + +Let's see one example of `IndexedMap` definition and usage, originally taken from the `cw721-base` contract. + +### Definition + +```rust +pub struct TokenIndexes<'a> { + pub owner: MultiIndex<'a, Addr, TokenInfo, String>, +} + +impl<'a> IndexList for TokenIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = 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) +} +``` + +Let's discuss this piece by piece: +```rust +pub struct TokenIndexes<'a> { + pub owner: MultiIndex<'a, Addr, TokenInfo, String>, +} +``` + +These are the index definitions. Here there's only one index, called `owner`. There could be more, as public +members of the `TokenIndexes` struct. +We see that the `owner` index is a `MultiIndex`. A multi-index can have repeated values as keys. The primary key is +used internally as the last element of the multi-index key, to disambiguate repeated index values. +Like the name implies, this is an index over tokens, by owner. Given that an owner can have multiple tokens, +we need a `MultiIndex` to be able to list / iterate over all the tokens he has. + +The `TokenInfo` data will originally be stored by `token_id` (which is a string value). +You can see this in the token creation code: +```rust + tokens().update(deps.storage, &msg.token_id, |old| match old { + Some(_) => Err(ContractError::Claimed {}), + None => Ok(token), + })?; +``` +(Incidentally, this is using `update` instead of `save`, to avoid overwriting an already existing token). + +Given that `token_id` is a string value, we specify `String` as the last argument of the `MultiIndex` definition. +That way, the deserialization of the primary key will be done to the right type (an owned string). + +**NB**: In the particular case of a `MultiIndex`, and with the latest implementation of type-safe bounds, the definition of +this last type parameter is crucial, for properly using type-safe bounds. +See [Type-safe bounds over `MultiIndex`](#type-safe-bounds-over-multiindex), below. + +Then, this `TokenInfo` data will be indexed by token `owner` (which is an `Addr`). So that we can list all the tokens +an owner has. That's why the `owner` index key is `Addr`. + +Other important thing here is that the key (and its components, in the case of a composite key) must implement +the `PrimaryKey` trait. You can see that `Addr` does implement `PrimaryKey`: + +```rust +impl<'a> PrimaryKey<'a> for Addr { + type Prefix = (); + type SubPrefix = (); + type Suffix = Self; + type SuperSuffix = Self; + + fn key(&self) -> Vec { + // this is simple, we don't add more prefixes + vec![Key::Ref(self.as_bytes())] + } +} +``` + +--- + +We can now see how it all works, taking a look at the remaining code: + +```rust +impl<'a> IndexList for TokenIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner]; + Box::new(v.into_iter()) + } +} +``` + +This implements the `IndexList` trait for `TokenIndexes`. + +**NB**: this code is more or less boiler-plate, and needed for the internals. Do not try to customize this; +just return a list of all indexes. +Implementing this trait serves two purposes (which are really one and the same): it allows the indexes +to be queried through `get_indexes`, and, it allows `TokenIndexes` to be treated as an `IndexList`. So that +it can be passed as a parameter during `IndexedMap` construction, below: + +```rust +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) +} +``` + +Here `tokens()` is just a helper function, that simplifies the `IndexedMap` construction for us. First the +index (es) is (are) created, and then, the `IndexedMap` is created and returned. + +During index creation, we must supply an index function per index +```rust + owner: MultiIndex::new(|d: &TokenInfo| d.owner.clone(), +``` +which is the one that will take the value of the original map and create the index key from it. +Of course, this requires that the elements required for the index key are present in the value. +Besides the index function, we must also supply the namespace of the pk, and the one for the new index. + +--- + +After that, we just create and return the `IndexedMap`: + +```rust + IndexedMap::new("tokens", indexes) +``` + +Here of course, the namespace of the pk must match the one used during index(es) creation. And, we pass our +`TokenIndexes` (as an `IndexList`-type parameter) as second argument. Connecting in this way the underlying `Map` +for the pk, with the defined indexes. + +So, `IndexedMap` (and the other `Indexed*` types) is just a wrapper / extension around `Map`, that provides +a number of index functions and namespaces to create indexes over the original `Map` data. It also implements +calling these index functions during value storage / update / removal, so that you can forget about it, +and just use the indexed data. + +### Usage + +An example of use, where `owner` is a `String` value passed as a parameter, and `start_after` and `limit` optionally +define the pagination range: + +Notice this uses `prefix()`, explained above in the `Map` section. + +```rust + 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, _> = tokens() + .idx + .owner + .prefix(owner_addr) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect(); + let tokens = res?; +``` +Now `tokens` contains `(token_id, TokenInfo)` pairs for the given `owner`. +The pk values are `Vec` in the case of `range_raw()`, but will be deserialized to the proper type using +`range()`; provided that the pk deserialization type (`String`, in this case) is correctly specified +in the `MultiIndex` definition (see [Index keys deserialization](#index-keys-deserialization), +below). + +Another example that is similar, but returning only the (raw) `token_id`s, using the `keys_raw()` method: +```rust + let pks: Vec<_> = tokens() + .idx + .owner + .prefix(owner_addr) + .keys_raw( + deps.storage, + start, + None, + Order::Ascending, + ) + .take(limit) + .collect(); +``` +Now `pks` contains `token_id` values (as raw `Vec`s) for the given `owner`. By using `keys` instead, +a deserialized key can be obtained, as detailed in the next section. + +### Index keys deserialization + +For `UniqueIndex` and `MultiIndex`, the primary key (`PK`) type needs to be specified, in order to deserialize +the primary key to it. +This `PK` type specification is also important for `MultiIndex` type-safe bounds, as the primary key +is part of the multi-index key. See next section, [Type-safe bounds over MultiIndex](#type-safe-bounds-over-multiindex). + +**NB**: This specification is still a manual (and therefore error-prone) process / setup, that will (if possible) +be automated in the future (https://github.com/CosmWasm/cw-plus/issues/531). + +### Type-safe bounds over MultiIndex + +In the particular case of `MultiIndex`, the primary key (`PK`) type parameter also defines the type of the (partial) bounds over +the index key (the part that corresponds to the primary key, that is). +So, to correctly use type-safe bounds over multi-indexes ranges, it is fundamental for this `PK` type +to be correctly defined, so that it matches the primary key type, or its (typically owned) deserialization variant. diff --git a/packages/storage-macro/src/lib.rs b/packages/storage-macro/src/lib.rs new file mode 100644 index 000000000..366723ca5 --- /dev/null +++ b/packages/storage-macro/src/lib.rs @@ -0,0 +1,37 @@ +use proc_macro::TokenStream; +use syn::{ + Ident, + __private::{quote::quote, Span}, + parse_macro_input, ItemStruct, +}; + +#[proc_macro_attribute] +pub fn index_list(attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemStruct); + + let ty = Ident::new(&attr.to_string(), Span::call_site()); + let struct_ty = input.ident.clone(); + + let names = input + .fields + .clone() + .into_iter() + .map(|e| { + let name = e.ident.unwrap(); + quote! { &self.#name } + }) + .collect::>(); + + let expanded = quote! { + #input + + impl cw_storage_plus::IndexList<#ty> for #struct_ty<'_> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn cw_storage_plus::Index<#ty>> = vec![#(#names),*]; + Box::new(v.into_iter()) + } + } + }; + + TokenStream::from(expanded) +} diff --git a/packages/storage-macro/tests/index_list.rs b/packages/storage-macro/tests/index_list.rs new file mode 100644 index 000000000..3a192691b --- /dev/null +++ b/packages/storage-macro/tests/index_list.rs @@ -0,0 +1,60 @@ +use cosmwasm_std::{testing::MockStorage, Addr}; +use cw_storage_macro::index_list; +use cw_storage_plus::{IndexedMap, MultiIndex, UniqueIndex}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +struct TestStruct { + id: u64, + id2: u32, + addr: Addr, +} + +#[index_list(TestStruct)] +struct TestIndexes<'a> { + id: MultiIndex<'a, u32, TestStruct, u64>, + addr: UniqueIndex<'a, Addr, TestStruct>, +} + +#[test] +fn compile() { + let _: IndexedMap = IndexedMap::new( + "t", + TestIndexes { + id: MultiIndex::new(|t| t.id2, "t", "t_2"), + addr: UniqueIndex::new(|t| t.addr.clone(), "t_addr"), + }, + ); +} + +#[test] +fn works() { + let mut storage = MockStorage::new(); + let idm: IndexedMap = IndexedMap::new( + "t", + TestIndexes { + id: MultiIndex::new(|t| t.id2, "t", "t_2"), + addr: UniqueIndex::new(|t| t.addr.clone(), "t_addr"), + }, + ); + + idm.save( + &mut storage, + 0, + &TestStruct { + id: 0, + id2: 100, + addr: Addr::unchecked("1"), + }, + ) + .unwrap(); + + assert_eq!( + idm.load(&storage, 0).unwrap(), + TestStruct { + id: 0, + id2: 100, + addr: Addr::unchecked("1"), + } + ); +} From fbc308280a6ac4498c2fd3b2630adf8d273afcf1 Mon Sep 17 00:00:00 2001 From: Pakorn Nathong Date: Tue, 14 Jun 2022 11:39:49 +0700 Subject: [PATCH 11/17] Add reexport macro to cw-storage-plus package --- Cargo.lock | 1 + packages/storage-plus/Cargo.toml | 2 ++ packages/storage-plus/src/lib.rs | 7 +++++++ 3 files changed, 10 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 390bf2ab1..2d69171e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,6 +422,7 @@ version = "0.13.4" dependencies = [ "cosmwasm-std", "criterion", + "cw-storage-macro", "rand", "schemars", "serde", diff --git a/packages/storage-plus/Cargo.toml b/packages/storage-plus/Cargo.toml index 5370e5eb2..524890ef1 100644 --- a/packages/storage-plus/Cargo.toml +++ b/packages/storage-plus/Cargo.toml @@ -11,6 +11,7 @@ homepage = "https://cosmwasm.com" [features] default = ["iterator"] iterator = ["cosmwasm-std/iterator"] +macro = ["cw-storage-macro"] [lib] # See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options @@ -20,6 +21,7 @@ bench = false cosmwasm-std = { version = "1.0.0", default-features = false } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } +cw-storage-macro = { version = "0.13.4", optional = true, path = "../storage-macro" } [dev-dependencies] criterion = { version = "0.3", features = [ "html_reports" ] } diff --git a/packages/storage-plus/src/lib.rs b/packages/storage-plus/src/lib.rs index 5562073c0..d6f93efb3 100644 --- a/packages/storage-plus/src/lib.rs +++ b/packages/storage-plus/src/lib.rs @@ -40,3 +40,10 @@ pub use path::Path; pub use prefix::{range_with_prefix, Prefix}; #[cfg(feature = "iterator")] pub use snapshot::{SnapshotItem, SnapshotMap, Strategy}; + +#[cfg(all(feature = "iterator", feature = "macro"))] +#[macro_use] +extern crate cw_storage_macro; +#[cfg(all(feature = "iterator", feature = "macro"))] +#[doc(hidden)] +pub use cw_storage_macro::*; From 6743898aa05ab4b6e6337ffdcc5818bd40018b8d Mon Sep 17 00:00:00 2001 From: Pakorn Nathong Date: Tue, 14 Jun 2022 11:52:57 +0700 Subject: [PATCH 12/17] Update notice and readme --- packages/storage-macro/NOTICE | 2 +- packages/storage-macro/README.md | 641 +------------------------------ 2 files changed, 14 insertions(+), 629 deletions(-) diff --git a/packages/storage-macro/NOTICE b/packages/storage-macro/NOTICE index 838a67f47..a2d3c8655 100644 --- a/packages/storage-macro/NOTICE +++ b/packages/storage-macro/NOTICE @@ -1,4 +1,4 @@ -CW-Storage-Plus: Enhanced/experimental storage engines for CosmWasm +CW-Storage-Macro: Macro helper for storage package Copyright (C) 2020 Confio OÜ Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/packages/storage-macro/README.md b/packages/storage-macro/README.md index 3e2691df1..200c2dfa4 100644 --- a/packages/storage-macro/README.md +++ b/packages/storage-macro/README.md @@ -1,637 +1,22 @@ -# CW-Storage-Plus: Enhanced storage engines for CosmWasm +# CW-Storage-Plus: Macro helper for storage package -After building `cosmwasm-storage`, we realized many of the design decisions were -limiting us and producing a lot of needless boilerplate. The decision was made to leave -those APIs stable for anyone wanting a very basic abstraction on the KV-store and to -build a much more powerful and complex ORM layer that can provide powerful accessors -using complex key types, which are transparently turned into bytes. +Procedural macros helper for interacting with cw-storage-plus and cosmwasm-storage. -This led to a number of breaking API changes in this package of the course of several -releases as we updated this with lots of experience, user feedback, and deep dives to harness -the full power of generics. +## Current features -**Status: beta** - -As of `cw-storage-plus` `v0.12` the API should be quite stable. -There are no major API breaking issues pending, and all API changes will be documented -in [`MIGRATING.md`](../../MIGRATING.md). - -This has been heavily used in many production-quality contracts. -The code has demonstrated itself to be stable and powerful. -It has not been audited, and Confio assumes no liability, but we consider it mature enough -to be the **standard storage layer** for your contracts. - -## Usage Overview - -We introduce two main classes to provide a productive abstraction -on top of `cosmwasm_std::Storage`. They are `Item`, which is -a typed wrapper around one database key, providing some helper functions -for interacting with it without dealing with raw bytes. And `Map`, -which allows you to store multiple unique typed objects under a prefix, -indexed by a simple (`&[u8]`) or compound (eg. `(&[u8], &[u8])`) primary key. - -These correspond to the concepts represented in `cosmwasm_storage` by -`Singleton` and `Bucket`, but with a re-designed API and implementation -to require less typing for developers and less gas usage in the contracts. - -## Item - -The usage of an [`Item`](./src/item.rs) is pretty straight-forward. -You must simply provide the proper type, as well as a database key not -used by any other item. Then it will provide you with a nice interface -to interact with such data. - -If you are coming from using `Singleton`, the biggest change is that -we no longer store `Storage` inside, meaning we don't need read and write -variants of the object, just one type. Furthermore, we use `const fn` -to create the `Item`, allowing it to be defined as a global compile-time -constant rather than a function that must be constructed each time, -which saves gas as well as typing. - -Example Usage: - -```rust -#[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 = Item::new("config"); - -fn demo() -> StdResult<()> { - let mut store = MockStorage::new(); - - // may_load returns Option, 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 - -The usage of a [`Map`](./src/map.rs) is a little more complex, but -is still pretty straight-forward. You can imagine it as a storage-backed -`BTreeMap`, allowing key-value lookups with typed values. In addition, -we support not only simple binary keys (`&[u8]`), but tuples, which are -combined. This allows us to store allowances as composite keys -eg. `(owner, spender)` to look up the balance. - -Beyond direct lookups, we have a super-power not found in Ethereum - -iteration. That's right, you can list all items in a `Map`, or only -part of them. We can efficiently allow pagination over these items as -well, starting at the point the last query ended, with low gas costs. -This requires the `iterator` feature to be enabled in `cw-storage-plus` -(which automatically enables it in `cosmwasm-std` as well, and which is -enabled by default). - -If you are coming from using `Bucket`, the biggest change is that -we no longer store `Storage` inside, meaning we don't need read and write -variants of the object, just one type. Furthermore, we use `const fn` -to create the `Bucket`, allowing it to be defined as a global compile-time -constant rather than a function that must be constructed each time, -which saves gas as well as typing. In addition, the composite indexes -(tuples) are more ergonomic and expressive of intention, and the range -interface has been improved. - -Here is an example with normal (simple) keys: - -```rust -#[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| -> StdResult { - 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(()) -} -``` - -### Key types - -A `Map` key can be anything that implements the `PrimaryKey` trait. There are a series of implementations of -`PrimaryKey` already provided (see [keys.rs](./src/keys.rs)): - - - `impl<'a> PrimaryKey<'a> for &'a [u8]` - - `impl<'a> PrimaryKey<'a> for &'a str` - - `impl<'a> PrimaryKey<'a> for Vec` - - `impl<'a> PrimaryKey<'a> for String` - - `impl<'a> PrimaryKey<'a> for Addr` - - `impl<'a> PrimaryKey<'a> for &'a Addr` - - `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` implemented for unsigned integers up to `u64` - - `PrimaryKey` implemented for signed integers up to `i64` - -That means that byte and string slices, byte vectors, and strings, can be conveniently used as keys. -Moreover, some other types can be used as well, like addresses and address references, pairs, triples, and -integer types. - -If the key represents an address, we suggest using `&Addr` for keys in storage, instead of `String` or string slices. -This implies doing address validation through `addr_validate` on any address passed in via a message, to ensure it's a -legitimate address, and not random text which will fail later. -`pub fn addr_validate(&self, &str) -> Addr` in `deps.api` can be used for address validation, and the returned `Addr` -can then be conveniently used as key in a `Map` or similar structure. - -It's also convenient to use references (i.e. borrowed values) instead of values for keys (i.e. `&Addr` instead of `Addr`), -as that will typically save some cloning during key reading / writing. - -### Composite Keys - -There are times when we want to use multiple items as a key. For example, when -storing allowances based on account owner and spender. We could try to manually -concatenate them before calling, but that can lead to overlap, and is a bit -low-level for us. Also, by explicitly separating the keys, we can easily provide -helpers to do range queries over a prefix, such as "show me all allowances for -one owner" (first part of the composite key). Just like you'd expect from your -favorite database. - -Here's how we use it with composite keys. Just define a tuple as a key and use that -everywhere you used a byte slice above. - -```rust -// 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(()) -} -``` - -### Path - -Under the scenes, we create a `Path` from the `Map` when accessing a key. -`PEOPLE.load(&store, b"jack") == PEOPLE.key(b"jack").load()`. -`Map.key()` returns a `Path`, which has the same interface as `Item`, -re-using the calculated path to this key. - -For simple keys, this is just a bit less typing and a bit less gas if you -use the same key for many calls. However, for composite keys, like -`(b"owner", b"spender")` it is **much** less typing. And highly recommended anywhere -you will use a composite key even twice: +Auto generate IndexList impl for your indexes struct. ```rust -#[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(()) +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +struct TestStruct { + id: u64, + id2: u32, + addr: Addr, } -``` - -### Prefix - -In addition to getting one particular item out of a map, we can iterate over the map -(or a subset of the map). This let us answer questions like "show me all tokens", -and we provide some nice [`Bound`](#bound) helpers to easily allow pagination or custom ranges. - -The general format is to get a `Prefix` by calling `map.prefix(k)`, where `k` is exactly -one less item than the normal key (If `map.key()` took `(&[u8], &[u8])`, then `map.prefix()` takes `&[u8]`. -If `map.key()` took `&[u8]`, `map.prefix()` takes `()`). Once we have a prefix space, we can iterate -over all items with `range(store, min, max, order)`. It supports `Order::Ascending` or `Order::Descending`. -`min` is the lower bound and `max` is the higher bound. - -If the `min` and `max` bounds are `None`, `range` will return all items under the prefix. You can use `.take(n)` to -limit the results to `n` items and start doing pagination. You can also set the `min` bound to -eg. `Bound::exclusive(last_value)` to start iterating over all items *after* the last value. Combined with -`take`, we easily have pagination support. You can also use `Bound::inclusive(x)` when you want to include any -perfect matches. -### Bound - -`Bound` is a helper to build type-safe bounds on the keys or sub-keys you want to iterate over. -It also supports a raw (`Vec`) bounds specification, for the cases you don't want or can't use typed bounds. - -```rust -#[derive(Clone, Debug)] -pub enum Bound<'a, K: PrimaryKey<'a>> { - Inclusive((K, PhantomData<&'a bool>)), - Exclusive((K, PhantomData<&'a bool>)), - InclusiveRaw(Vec), - ExclusiveRaw(Vec), +#[index_list(TestStruct)] // <- Add this line right here,. +struct TestIndexes<'a> { + id: MultiIndex<'a, u32, TestStruct, u64>, + addr: UniqueIndex<'a, Addr, TestStruct>, } ``` - -To better understand the API, please check the following example: -```rust -#[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> = 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> = 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> = 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> = ALLOWANCE - .prefix("owner") - .range( - &store, - Some(Bound::exclusive("spender1")), - Some(Bound::inclusive("spender2")), - Order::Descending, - ) - .collect(); - assert_eq!(all?, vec![("spender2".to_vec(), 3000)]); - - Ok(()) -} -``` - -**NB**: For properly defining and using type-safe bounds over a `MultiIndex`, see [Type-safe bounds over `MultiIndex`](#type-safe-bounds-over-multiindex), -below. - -## IndexedMap - -Let's see one example of `IndexedMap` definition and usage, originally taken from the `cw721-base` contract. - -### Definition - -```rust -pub struct TokenIndexes<'a> { - pub owner: MultiIndex<'a, Addr, TokenInfo, String>, -} - -impl<'a> IndexList for TokenIndexes<'a> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = 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) -} -``` - -Let's discuss this piece by piece: -```rust -pub struct TokenIndexes<'a> { - pub owner: MultiIndex<'a, Addr, TokenInfo, String>, -} -``` - -These are the index definitions. Here there's only one index, called `owner`. There could be more, as public -members of the `TokenIndexes` struct. -We see that the `owner` index is a `MultiIndex`. A multi-index can have repeated values as keys. The primary key is -used internally as the last element of the multi-index key, to disambiguate repeated index values. -Like the name implies, this is an index over tokens, by owner. Given that an owner can have multiple tokens, -we need a `MultiIndex` to be able to list / iterate over all the tokens he has. - -The `TokenInfo` data will originally be stored by `token_id` (which is a string value). -You can see this in the token creation code: -```rust - tokens().update(deps.storage, &msg.token_id, |old| match old { - Some(_) => Err(ContractError::Claimed {}), - None => Ok(token), - })?; -``` -(Incidentally, this is using `update` instead of `save`, to avoid overwriting an already existing token). - -Given that `token_id` is a string value, we specify `String` as the last argument of the `MultiIndex` definition. -That way, the deserialization of the primary key will be done to the right type (an owned string). - -**NB**: In the particular case of a `MultiIndex`, and with the latest implementation of type-safe bounds, the definition of -this last type parameter is crucial, for properly using type-safe bounds. -See [Type-safe bounds over `MultiIndex`](#type-safe-bounds-over-multiindex), below. - -Then, this `TokenInfo` data will be indexed by token `owner` (which is an `Addr`). So that we can list all the tokens -an owner has. That's why the `owner` index key is `Addr`. - -Other important thing here is that the key (and its components, in the case of a composite key) must implement -the `PrimaryKey` trait. You can see that `Addr` does implement `PrimaryKey`: - -```rust -impl<'a> PrimaryKey<'a> for Addr { - type Prefix = (); - type SubPrefix = (); - type Suffix = Self; - type SuperSuffix = Self; - - fn key(&self) -> Vec { - // this is simple, we don't add more prefixes - vec![Key::Ref(self.as_bytes())] - } -} -``` - ---- - -We can now see how it all works, taking a look at the remaining code: - -```rust -impl<'a> IndexList for TokenIndexes<'a> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.owner]; - Box::new(v.into_iter()) - } -} -``` - -This implements the `IndexList` trait for `TokenIndexes`. - -**NB**: this code is more or less boiler-plate, and needed for the internals. Do not try to customize this; -just return a list of all indexes. -Implementing this trait serves two purposes (which are really one and the same): it allows the indexes -to be queried through `get_indexes`, and, it allows `TokenIndexes` to be treated as an `IndexList`. So that -it can be passed as a parameter during `IndexedMap` construction, below: - -```rust -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) -} -``` - -Here `tokens()` is just a helper function, that simplifies the `IndexedMap` construction for us. First the -index (es) is (are) created, and then, the `IndexedMap` is created and returned. - -During index creation, we must supply an index function per index -```rust - owner: MultiIndex::new(|d: &TokenInfo| d.owner.clone(), -``` -which is the one that will take the value of the original map and create the index key from it. -Of course, this requires that the elements required for the index key are present in the value. -Besides the index function, we must also supply the namespace of the pk, and the one for the new index. - ---- - -After that, we just create and return the `IndexedMap`: - -```rust - IndexedMap::new("tokens", indexes) -``` - -Here of course, the namespace of the pk must match the one used during index(es) creation. And, we pass our -`TokenIndexes` (as an `IndexList`-type parameter) as second argument. Connecting in this way the underlying `Map` -for the pk, with the defined indexes. - -So, `IndexedMap` (and the other `Indexed*` types) is just a wrapper / extension around `Map`, that provides -a number of index functions and namespaces to create indexes over the original `Map` data. It also implements -calling these index functions during value storage / update / removal, so that you can forget about it, -and just use the indexed data. - -### Usage - -An example of use, where `owner` is a `String` value passed as a parameter, and `start_after` and `limit` optionally -define the pagination range: - -Notice this uses `prefix()`, explained above in the `Map` section. - -```rust - 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, _> = tokens() - .idx - .owner - .prefix(owner_addr) - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .collect(); - let tokens = res?; -``` -Now `tokens` contains `(token_id, TokenInfo)` pairs for the given `owner`. -The pk values are `Vec` in the case of `range_raw()`, but will be deserialized to the proper type using -`range()`; provided that the pk deserialization type (`String`, in this case) is correctly specified -in the `MultiIndex` definition (see [Index keys deserialization](#index-keys-deserialization), -below). - -Another example that is similar, but returning only the (raw) `token_id`s, using the `keys_raw()` method: -```rust - let pks: Vec<_> = tokens() - .idx - .owner - .prefix(owner_addr) - .keys_raw( - deps.storage, - start, - None, - Order::Ascending, - ) - .take(limit) - .collect(); -``` -Now `pks` contains `token_id` values (as raw `Vec`s) for the given `owner`. By using `keys` instead, -a deserialized key can be obtained, as detailed in the next section. - -### Index keys deserialization - -For `UniqueIndex` and `MultiIndex`, the primary key (`PK`) type needs to be specified, in order to deserialize -the primary key to it. -This `PK` type specification is also important for `MultiIndex` type-safe bounds, as the primary key -is part of the multi-index key. See next section, [Type-safe bounds over MultiIndex](#type-safe-bounds-over-multiindex). - -**NB**: This specification is still a manual (and therefore error-prone) process / setup, that will (if possible) -be automated in the future (https://github.com/CosmWasm/cw-plus/issues/531). - -### Type-safe bounds over MultiIndex - -In the particular case of `MultiIndex`, the primary key (`PK`) type parameter also defines the type of the (partial) bounds over -the index key (the part that corresponds to the primary key, that is). -So, to correctly use type-safe bounds over multi-indexes ranges, it is fundamental for this `PK` type -to be correctly defined, so that it matches the primary key type, or its (typically owned) deserialization variant. From 5a5b173a00297728a4a14f97fe47d689aa1ce6a8 Mon Sep 17 00:00:00 2001 From: Pakorn Nathong Date: Wed, 6 Jul 2022 22:43:21 +0700 Subject: [PATCH 13/17] move struct position to cover code coverage --- packages/storage-macro/tests/index_list.rs | 39 ++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/storage-macro/tests/index_list.rs b/packages/storage-macro/tests/index_list.rs index 3a192691b..13ee1985e 100644 --- a/packages/storage-macro/tests/index_list.rs +++ b/packages/storage-macro/tests/index_list.rs @@ -3,21 +3,21 @@ use cw_storage_macro::index_list; use cw_storage_plus::{IndexedMap, MultiIndex, UniqueIndex}; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -struct TestStruct { - id: u64, - id2: u32, - addr: Addr, -} - -#[index_list(TestStruct)] -struct TestIndexes<'a> { - id: MultiIndex<'a, u32, TestStruct, u64>, - addr: UniqueIndex<'a, Addr, TestStruct>, -} - #[test] fn compile() { + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] + struct TestStruct { + id: u64, + id2: u32, + addr: Addr, + } + + #[index_list(TestStruct)] + struct TestIndexes<'a> { + id: MultiIndex<'a, u32, TestStruct, u64>, + addr: UniqueIndex<'a, Addr, TestStruct>, + } + let _: IndexedMap = IndexedMap::new( "t", TestIndexes { @@ -29,6 +29,19 @@ fn compile() { #[test] fn works() { + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] + struct TestStruct { + id: u64, + id2: u32, + addr: Addr, + } + + #[index_list(TestStruct)] + struct TestIndexes<'a> { + id: MultiIndex<'a, u32, TestStruct, u64>, + addr: UniqueIndex<'a, Addr, TestStruct>, + } + let mut storage = MockStorage::new(); let idm: IndexedMap = IndexedMap::new( "t", From 3f29e20cf1f39edc924b62e0d0296cfdb61f5f1b Mon Sep 17 00:00:00 2001 From: Pakorn Nathong Date: Wed, 6 Jul 2022 22:49:30 +0700 Subject: [PATCH 14/17] remove macro crate features --- packages/storage-macro/Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/storage-macro/Cargo.toml b/packages/storage-macro/Cargo.toml index 75cd97aa5..d8b8ac754 100644 --- a/packages/storage-macro/Cargo.toml +++ b/packages/storage-macro/Cargo.toml @@ -9,10 +9,6 @@ repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" documentation = "https://docs.cosmwasm.com" -[features] -default = ["iterator"] -iterator = ["cosmwasm-std/iterator"] - [lib] proc-macro = true From 049bcb0aa618d24d553871bcf0cd92a8e8e2d00d Mon Sep 17 00:00:00 2001 From: yoisha <48324733+y-pakorn@users.noreply.github.com> Date: Sun, 17 Jul 2022 20:51:38 +0700 Subject: [PATCH 15/17] Apply suggestions from code review Co-authored-by: Mauro Lacy --- packages/storage-macro/Cargo.toml | 2 +- packages/storage-macro/NOTICE | 4 ++-- packages/storage-macro/README.md | 6 +++--- packages/storage-macro/tests/index_list.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/storage-macro/Cargo.toml b/packages/storage-macro/Cargo.toml index d8b8ac754..b29a59f10 100644 --- a/packages/storage-macro/Cargo.toml +++ b/packages/storage-macro/Cargo.toml @@ -3,7 +3,7 @@ name = "cw-storage-macro" version = "0.13.4" authors = ["yoisha <48324733+y-pakorn@users.noreply.github.com>"] edition = "2018" -description = "Macro helper for storage package" +description = "Macro helpers for storage-plus" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-plus" homepage = "https://cosmwasm.com" diff --git a/packages/storage-macro/NOTICE b/packages/storage-macro/NOTICE index a2d3c8655..4ae1ccfe5 100644 --- a/packages/storage-macro/NOTICE +++ b/packages/storage-macro/NOTICE @@ -1,5 +1,5 @@ -CW-Storage-Macro: Macro helper for storage package -Copyright (C) 2020 Confio OÜ +CW-Storage-Macro: Macro helpers for storage-plus +Copyright (C) 2022 Confio GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/packages/storage-macro/README.md b/packages/storage-macro/README.md index 200c2dfa4..d21d9f571 100644 --- a/packages/storage-macro/README.md +++ b/packages/storage-macro/README.md @@ -1,10 +1,10 @@ -# CW-Storage-Plus: Macro helper for storage package +# CW-Storage-Plus: Macro helpers for storage-plus Procedural macros helper for interacting with cw-storage-plus and cosmwasm-storage. ## Current features -Auto generate IndexList impl for your indexes struct. +Auto generate an `IndexList` impl for your indexes struct. ```rust #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] @@ -14,7 +14,7 @@ struct TestStruct { addr: Addr, } -#[index_list(TestStruct)] // <- Add this line right here,. +#[index_list(TestStruct)] // <- Add this line right here. struct TestIndexes<'a> { id: MultiIndex<'a, u32, TestStruct, u64>, addr: UniqueIndex<'a, Addr, TestStruct>, diff --git a/packages/storage-macro/tests/index_list.rs b/packages/storage-macro/tests/index_list.rs index 13ee1985e..7f063f532 100644 --- a/packages/storage-macro/tests/index_list.rs +++ b/packages/storage-macro/tests/index_list.rs @@ -21,7 +21,7 @@ fn compile() { let _: IndexedMap = IndexedMap::new( "t", TestIndexes { - id: MultiIndex::new(|t| t.id2, "t", "t_2"), + id: MultiIndex::new(|t| t.id2, "t", "t_id2"), addr: UniqueIndex::new(|t| t.addr.clone(), "t_addr"), }, ); From 07f6cacc9e2ebfe1e80e76921b1cf986e8f0c25a Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Sun, 17 Jul 2022 19:34:25 +0200 Subject: [PATCH 16/17] Add cfg(test) / mod test to storage-macro tests --- packages/storage-macro/tests/index_list.rs | 131 +++++++++++---------- 1 file changed, 67 insertions(+), 64 deletions(-) diff --git a/packages/storage-macro/tests/index_list.rs b/packages/storage-macro/tests/index_list.rs index 7f063f532..d81120733 100644 --- a/packages/storage-macro/tests/index_list.rs +++ b/packages/storage-macro/tests/index_list.rs @@ -1,73 +1,76 @@ -use cosmwasm_std::{testing::MockStorage, Addr}; -use cw_storage_macro::index_list; -use cw_storage_plus::{IndexedMap, MultiIndex, UniqueIndex}; -use serde::{Deserialize, Serialize}; +#[cfg(test)] +mod test { + use cosmwasm_std::{testing::MockStorage, Addr}; + use cw_storage_macro::index_list; + use cw_storage_plus::{IndexedMap, MultiIndex, UniqueIndex}; + use serde::{Deserialize, Serialize}; -#[test] -fn compile() { - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] - struct TestStruct { - id: u64, - id2: u32, - addr: Addr, - } - - #[index_list(TestStruct)] - struct TestIndexes<'a> { - id: MultiIndex<'a, u32, TestStruct, u64>, - addr: UniqueIndex<'a, Addr, TestStruct>, - } + #[test] + fn compile() { + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] + struct TestStruct { + id: u64, + id2: u32, + addr: Addr, + } - let _: IndexedMap = IndexedMap::new( - "t", - TestIndexes { - id: MultiIndex::new(|t| t.id2, "t", "t_id2"), - addr: UniqueIndex::new(|t| t.addr.clone(), "t_addr"), - }, - ); -} + #[index_list(TestStruct)] + struct TestIndexes<'a> { + id: MultiIndex<'a, u32, TestStruct, u64>, + addr: UniqueIndex<'a, Addr, TestStruct>, + } -#[test] -fn works() { - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] - struct TestStruct { - id: u64, - id2: u32, - addr: Addr, + let _: IndexedMap = IndexedMap::new( + "t", + TestIndexes { + id: MultiIndex::new(|t| t.id2, "t", "t_id2"), + addr: UniqueIndex::new(|t| t.addr.clone(), "t_addr"), + }, + ); } - #[index_list(TestStruct)] - struct TestIndexes<'a> { - id: MultiIndex<'a, u32, TestStruct, u64>, - addr: UniqueIndex<'a, Addr, TestStruct>, - } + #[test] + fn works() { + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] + struct TestStruct { + id: u64, + id2: u32, + addr: Addr, + } + + #[index_list(TestStruct)] + struct TestIndexes<'a> { + id: MultiIndex<'a, u32, TestStruct, u64>, + addr: UniqueIndex<'a, Addr, TestStruct>, + } - let mut storage = MockStorage::new(); - let idm: IndexedMap = IndexedMap::new( - "t", - TestIndexes { - id: MultiIndex::new(|t| t.id2, "t", "t_2"), - addr: UniqueIndex::new(|t| t.addr.clone(), "t_addr"), - }, - ); + let mut storage = MockStorage::new(); + let idm: IndexedMap = IndexedMap::new( + "t", + TestIndexes { + id: MultiIndex::new(|t| t.id2, "t", "t_2"), + addr: UniqueIndex::new(|t| t.addr.clone(), "t_addr"), + }, + ); - idm.save( - &mut storage, - 0, - &TestStruct { - id: 0, - id2: 100, - addr: Addr::unchecked("1"), - }, - ) - .unwrap(); + idm.save( + &mut storage, + 0, + &TestStruct { + id: 0, + id2: 100, + addr: Addr::unchecked("1"), + }, + ) + .unwrap(); - assert_eq!( - idm.load(&storage, 0).unwrap(), - TestStruct { - id: 0, - id2: 100, - addr: Addr::unchecked("1"), - } - ); + assert_eq!( + idm.load(&storage, 0).unwrap(), + TestStruct { + id: 0, + id2: 100, + addr: Addr::unchecked("1"), + } + ); + } } From 43ad1e8edcecf9bc85f67d534915eb9c1b9981a5 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Sun, 17 Jul 2022 19:35:41 +0200 Subject: [PATCH 17/17] Rename index_list tests for clarity --- packages/storage-macro/tests/index_list.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/storage-macro/tests/index_list.rs b/packages/storage-macro/tests/index_list.rs index d81120733..57eff5685 100644 --- a/packages/storage-macro/tests/index_list.rs +++ b/packages/storage-macro/tests/index_list.rs @@ -6,7 +6,7 @@ mod test { use serde::{Deserialize, Serialize}; #[test] - fn compile() { + fn index_list_compiles() { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] struct TestStruct { id: u64, @@ -30,7 +30,7 @@ mod test { } #[test] - fn works() { + fn index_list_works() { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] struct TestStruct { id: u64,