diff --git a/docs/CAPABILITIES-BUILT-IN.md b/docs/CAPABILITIES-BUILT-IN.md index 992fc99af4..ae3f38c4a4 100644 --- a/docs/CAPABILITIES-BUILT-IN.md +++ b/docs/CAPABILITIES-BUILT-IN.md @@ -33,3 +33,5 @@ might define others. `migrate` entrypoint, as well as IBC Fees support with `IbcMsg::PayPacketFee`, `IbcMsg::PayPacketFeeAsync` and `IbcQuery::FeeEnabledChannel`. Only chains running CosmWasm `2.2.0` or higher support this. +- `cosmwasm_3_0` enables `WasmQuery::RawRange`. Only chains running CosmWasm + `3.0.0` or higher support this. diff --git a/packages/go-gen/Cargo.toml b/packages/go-gen/Cargo.toml index 993b0b0967..fb11eabebe 100644 --- a/packages/go-gen/Cargo.toml +++ b/packages/go-gen/Cargo.toml @@ -10,7 +10,7 @@ release = false [dependencies] cosmwasm-std = { version = "3.0.0-ibc2.0", path = "../std", features = [ - "cosmwasm_2_2", + "cosmwasm_3_0", "staking", "stargate", ] } diff --git a/packages/go-gen/src/go.rs b/packages/go-gen/src/go.rs index 5f762ec689..726a3948a1 100644 --- a/packages/go-gen/src/go.rs +++ b/packages/go-gen/src/go.rs @@ -50,8 +50,16 @@ impl Display for GoField { self.ty, self.rust_name )?; - if let Nullability::OmitEmpty | Nullability::Nullable = self.ty.nullability { - f.write_str(",omitempty")?; + match self.ty.nullability { + Nullability::OmitEmpty => { + f.write_str(",omitempty")?; + } + Nullability::Nullable if !self.ty.is_slice() => { + // if the type is nullable, we need to use a pointer type + // and add `omitempty` to the json tag + f.write_str(",omitempty")?; + } + _ => {} } f.write_str("\"`") } @@ -100,12 +108,17 @@ impl GoType { ]; BASIC_GO_TYPES.contains(&&*self.name) } + + pub fn is_slice(&self) -> bool { + self.name.starts_with("[]") + } } impl Display for GoType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.nullability == Nullability::Nullable && !self.is_basic_type() { + if self.nullability == Nullability::Nullable && !self.is_basic_type() && !self.is_slice() { // if the type is nullable and not a basic type, use a pointer + // slices are already pointers, so we don't need to do anything for them f.write_char('*')?; } f.write_str(&self.name) diff --git a/packages/go-gen/src/main.rs b/packages/go-gen/src/main.rs index 1174884428..c5243eb20a 100644 --- a/packages/go-gen/src/main.rs +++ b/packages/go-gen/src/main.rs @@ -11,7 +11,7 @@ mod schema; mod utils; fn main() -> Result<()> { - let root = cosmwasm_schema::schema_for!(cosmwasm_std::Reply); + let root = cosmwasm_schema::schema_for!(cosmwasm_std::RawRangeResponse); let code = generate_go(root)?; println!("{}", code); @@ -198,6 +198,7 @@ mod tests { use super::*; + #[track_caller] fn assert_code_eq(actual: String, expected: &str) { let actual_no_ws = actual.split_whitespace().collect::>(); let expected_no_ws = expected.split_whitespace().collect::>(); @@ -210,6 +211,7 @@ mod tests { ); } + #[track_caller] fn assert_code_eq_ignore_docs(actual: String, expected: &str) { let actual_filtered = actual .lines() @@ -251,7 +253,7 @@ mod tests { Binary []byte `json:"binary"` Checksum Checksum `json:"checksum"` HexBinary string `json:"hex_binary"` - NestedBinary Array[*[]byte] `json:"nested_binary"` + NestedBinary Array[[]byte] `json:"nested_binary"` Uint128 string `json:"uint128"` }"#, ); @@ -340,7 +342,7 @@ mod tests { compare_codes!(cosmwasm_std::SupplyResponse); compare_codes!(cosmwasm_std::BalanceResponse); compare_codes!(cosmwasm_std::DenomMetadataResponse); - // compare_codes!(cosmwasm_std::AllDenomMetadataResponse); // uses `[]byte` instead of `*[]byte` + // compare_codes!(cosmwasm_std::AllDenomMetadataResponse); // uses slice instead of `Array` type // staking compare_codes!(cosmwasm_std::BondedDenomResponse); compare_codes!(cosmwasm_std::AllDelegationsResponse); @@ -355,6 +357,7 @@ mod tests { // wasm compare_codes!(cosmwasm_std::ContractInfoResponse); compare_codes!(cosmwasm_std::CodeInfoResponse); + compare_codes!(cosmwasm_std::RawRangeResponse); } #[test] @@ -475,6 +478,8 @@ mod tests { #[cw_serde] struct D { + // this should not get an `omitempty` because that prevents us from distinguishing between + // `None` and `Some(vec![])` d: Option>, nested: Vec>>, } @@ -483,8 +488,8 @@ mod tests { code, r#" type D struct { - D *[]string `json:"d,omitempty"` - Nested Array[*[]string] `json:"nested"` + D []string `json:"d"` + Nested Array[[]string] `json:"nested"` }"#, ); } diff --git a/packages/go-gen/src/schema.rs b/packages/go-gen/src/schema.rs index 198675b99c..7caf6f6e6c 100644 --- a/packages/go-gen/src/schema.rs +++ b/packages/go-gen/src/schema.rs @@ -212,8 +212,8 @@ pub fn type_from_instance_type( // for nullable array item types, we have to use a pointer type, even for basic types, // so we can pass null as elements - // otherwise they would just be omitted from the array - let maybe_ptr = if item_type.nullability == Nullability::Nullable { + // otherwise they would just be omitted from the array, unless it's a slice itself + let maybe_ptr = if item_type.nullability == Nullability::Nullable && !item_type.is_slice() { "*" } else { "" @@ -246,6 +246,16 @@ pub fn array_item_type( Some(SingleOrVec::Single(array_validation)) => { schema_object_type(array_validation.object()?, type_context, additional_structs) } + Some(SingleOrVec::Vec(v)) + if v.len() == 1 || v.len() > 1 && v.windows(2).all(|w| w[0] == w[1]) => + { + // all items are the same type + schema_object_type( + v.first().unwrap().object()?, + type_context, + additional_structs, + ) + } _ => bail!("array type with non-singular item type is not supported"), } } @@ -288,6 +298,7 @@ pub fn custom_type_of(ty: &str) -> Option<&str> { "Uint128" => Some("string"), "Uint256" => Some("string"), "Uint512" => Some("string"), + "Order" => Some("string"), "Int64" => Some("Int64"), "Int128" => Some("string"), "Int256" => Some("string"), diff --git a/packages/go-gen/tests/cosmwasm_std__RawRangeResponse.go b/packages/go-gen/tests/cosmwasm_std__RawRangeResponse.go new file mode 100644 index 0000000000..11dbc5718c --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__RawRangeResponse.go @@ -0,0 +1,6 @@ +type RawRangeResponse struct { + // The key-value pairs + Data Array[Array[[]byte]] `json:"data"` + // `None` if there are no more key-value pairs within the given key range. + NextKey []byte `json:"next_key"` +} \ No newline at end of file diff --git a/packages/go-gen/tests/cosmwasm_std__WasmQuery.go b/packages/go-gen/tests/cosmwasm_std__WasmQuery.go index 84f3f453c1..c6a3c88c89 100644 --- a/packages/go-gen/tests/cosmwasm_std__WasmQuery.go +++ b/packages/go-gen/tests/cosmwasm_std__WasmQuery.go @@ -22,9 +22,27 @@ type CodeInfoQuery struct { CodeID uint64 `json:"code_id"` } +type RawRangeQuery struct { + // The address of the contract to query + ContractAddr string `json:"contract_addr"` + // Exclusive end bound. This is the key after the last key you would like to get data for. + End []byte `json:"end"` + // Maximum number of elements to return. + // + // Make sure to set a reasonable limit to avoid running out of memory or into the deserialization limits of the VM. Also keep in mind that these limitations depend on the full JSON size of the response type. + Limit uint16 `json:"limit"` + // The order in which you want to receive the key-value pairs. + Order string `json:"order"` + // Inclusive start bound. This is the first key you would like to get data for. + // + // If `start` is lexicographically greater than or equal to `end`, an empty range is described, mo matter of the order. + Start []byte `json:"start"` +} + type WasmQuery struct { Smart *SmartQuery `json:"smart,omitempty"` Raw *RawQuery `json:"raw,omitempty"` ContractInfo *ContractInfoQuery `json:"contract_info,omitempty"` CodeInfo *CodeInfoQuery `json:"code_info,omitempty"` + RawRange *RawRangeQuery `json:"raw_range,omitempty"` } \ No newline at end of file diff --git a/packages/std/Cargo.toml b/packages/std/Cargo.toml index cfb146e98a..66b8d81001 100644 --- a/packages/std/Cargo.toml +++ b/packages/std/Cargo.toml @@ -9,7 +9,7 @@ license = "Apache-2.0" readme = "README.md" [package.metadata.docs.rs] -features = ["cosmwasm_2_2", "staking", "stargate", "ibc2"] +features = ["cosmwasm_3_0", "staking", "stargate", "ibc2"] [features] default = ["exports", "iterator", "std"] @@ -55,6 +55,9 @@ cosmwasm_2_1 = ["cosmwasm_2_0"] # This enables functionality that is only available on 2.2 chains. # It adds `IbcMsg::PayPacketFee` and `IbcMsg::PayPacketFeeAsync`. cosmwasm_2_2 = ["cosmwasm_2_1"] +# This enables functionality that is only available on 3.0 chains. +# It adds `WasmQuery::RawRange`. +cosmwasm_3_0 = ["cosmwasm_2_2"] [dependencies] base64 = "0.22.0" diff --git a/packages/std/src/exports/exports.rs b/packages/std/src/exports/exports.rs index cb8f2c0314..9be7ab2aac 100644 --- a/packages/std/src/exports/exports.rs +++ b/packages/std/src/exports/exports.rs @@ -82,6 +82,10 @@ extern "C" fn requires_cosmwasm_2_1() {} #[no_mangle] extern "C" fn requires_cosmwasm_2_2() {} +#[cfg(feature = "cosmwasm_3_0")] +#[no_mangle] +extern "C" fn requires_cosmwasm_3_0() {} + /// interface_version_* exports mark which Wasm VM interface level this contract is compiled for. /// They can be checked by cosmwasm_vm. /// Update this whenever the Wasm VM interface breaks. diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs index 48b3b244ca..05604c1663 100644 --- a/packages/std/src/lib.rs +++ b/packages/std/src/lib.rs @@ -99,8 +99,10 @@ pub use crate::query::{ DelegationTotalRewardsResponse, DelegatorReward, DelegatorValidatorsResponse, DelegatorWithdrawAddressResponse, DenomMetadataResponse, DistributionQuery, FeeEnabledChannelResponse, FullDelegation, GrpcQuery, IbcQuery, PortIdResponse, QueryRequest, - StakingQuery, SupplyResponse, Validator, ValidatorResponse, WasmQuery, + RawRangeEntry, RawRangeResponse, StakingQuery, SupplyResponse, Validator, ValidatorResponse, + WasmQuery, }; + #[cfg(all(feature = "stargate", feature = "cosmwasm_1_2"))] pub use crate::results::WeightedVoteOption; pub use crate::results::{ diff --git a/packages/std/src/query/wasm.rs b/packages/std/src/query/wasm.rs index f9cab55857..dc1e5aed31 100644 --- a/packages/std/src/query/wasm.rs +++ b/packages/std/src/query/wasm.rs @@ -2,7 +2,11 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::prelude::*; +#[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))] +use crate::storage_keys::{range_to_bounds, ToByteVec}; use crate::{Addr, Binary, Checksum}; +#[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))] +use core::ops::RangeBounds; use super::query_response::QueryResponseType; @@ -32,6 +36,60 @@ pub enum WasmQuery { /// Returns a [`CodeInfoResponse`] with metadata of the code #[cfg(feature = "cosmwasm_1_2")] CodeInfo { code_id: u64 }, + /// Queries a range of keys from the storage of a (different) contract, + /// returning a [`RawRangeResponse`]. + /// + /// This is a low-level query that allows you to query the storage of another contract. + /// Please keep in mind that the contract you are querying might change its storage layout using + /// migrations, which could break your queries, so it is recommended to only use this for + /// contracts you control. + #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))] + RawRange { + /// The address of the contract to query + contract_addr: String, + /// Inclusive start bound. This is the first key you would like to get data for. + /// + /// If `start` is lexicographically greater than or equal to `end`, + /// an empty range is described, mo matter of the order. + start: Option, + /// Exclusive end bound. This is the key after the last key you would like to get data for. + end: Option, + /// Maximum number of elements to return. + /// + /// Make sure to set a reasonable limit to avoid running out of memory or into + /// the deserialization limits of the VM. Also keep in mind that these limitations depend + /// on the full JSON size of the response type. + limit: u16, + /// The order in which you want to receive the key-value pairs. + order: crate::Order, + }, +} + +impl WasmQuery { + /// Creates a new [`WasmQuery::RawRange`] from the given parameters. + /// + /// This takes a [`RangeBounds`] to allow for specifying the range in a more idiomatic way. + #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))] + pub fn raw_range<'a, R, B>( + contract_addr: impl Into, + range: R, + limit: u16, + order: crate::Order, + ) -> Self + where + R: RangeBounds<&'a B>, + B: ToByteVec + ?Sized + 'a, + { + let (start, end) = range_to_bounds(&range); + + WasmQuery::RawRange { + contract_addr: contract_addr.into(), + start: start.map(Binary::new), + end: end.map(Binary::new), + limit, + order, + } + } } #[non_exhaustive] @@ -88,6 +146,23 @@ impl_hidden_constructor!( impl QueryResponseType for CodeInfoResponse {} +#[non_exhaustive] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct RawRangeResponse { + /// The key-value pairs + pub data: Vec, + /// `None` if there are no more key-value pairs within the given key range. + pub next_key: Option, +} + +impl_hidden_constructor!( + RawRangeResponse, + data: Vec, + next_key: Option +); + +pub type RawRangeEntry = (Binary, Binary); + #[cfg(test)] mod tests { use super::*; @@ -152,4 +227,56 @@ mod tests { r#"{"code_id":67,"creator":"jane","checksum":"f7bb7b18fb01bbf425cf4ed2cd4b7fb26a019a7fc75a4dc87e8a0b768c501f00"}"#, ); } + + #[test] + #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))] + fn raw_range_constructor_works() { + use crate::Order; + + let query = WasmQuery::raw_range( + "contract_addr", + &b"asdf"[..]..&b"asdz"[..], + 100, + Order::Ascending, + ); + + assert_eq!( + query, + WasmQuery::RawRange { + contract_addr: "contract_addr".to_string(), + start: Some(Binary::from(b"asdf")), + end: Some(Binary::from(b"asdz")), + limit: 100, + order: Order::Ascending, + } + ); + + let query = WasmQuery::raw_range("contract_addr", b"asdf"..=b"asdz", 100, Order::Ascending); + assert_eq!( + query, + WasmQuery::RawRange { + contract_addr: "contract_addr".to_string(), + start: Some(Binary::from(b"asdf")), + end: Some(Binary::from(b"asdz\0")), + limit: 100, + order: Order::Ascending, + } + ); + } + + #[test] + fn raw_range_response_serialization() { + let response = RawRangeResponse { + data: vec![ + (Binary::from(b"key"), Binary::from(b"value")), + (Binary::from(b"foo"), Binary::from(b"bar")), + ], + next_key: Some(Binary::from(b"next")), + }; + let json = to_json_binary(&response).unwrap(); + assert_eq!( + String::from_utf8_lossy(&json), + r#"{"data":[["a2V5","dmFsdWU="],["Zm9v","YmFy"]],"next_key":"bmV4dA=="}"#, + ); + } } diff --git a/packages/std/src/results/cosmos_msg.rs b/packages/std/src/results/cosmos_msg.rs index 85bbe23d15..600a5aa3f6 100644 --- a/packages/std/src/results/cosmos_msg.rs +++ b/packages/std/src/results/cosmos_msg.rs @@ -626,7 +626,7 @@ mod tests { } #[test] - #[cfg(feature = "cosmwasm_1_3")] + #[cfg(all(feature = "cosmwasm_1_3", feature = "staking"))] fn msg_distribution_serializes_to_correct_json() { // FundCommunityPool let fund_coins = vec![coin(200, "feathers"), coin(200, "stones")]; diff --git a/packages/std/src/storage_keys/mod.rs b/packages/std/src/storage_keys/mod.rs index 630feaa18f..415c2a605e 100644 --- a/packages/std/src/storage_keys/mod.rs +++ b/packages/std/src/storage_keys/mod.rs @@ -1,5 +1,9 @@ mod length_prefixed; +#[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))] +mod range; // Please note that the entire storage_keys module is public. So be careful // when adding elements here. pub use length_prefixed::{namespace_with_key, to_length_prefixed, to_length_prefixed_nested}; +#[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))] +pub(crate) use range::{range_to_bounds, ToByteVec}; diff --git a/packages/std/src/storage_keys/range.rs b/packages/std/src/storage_keys/range.rs new file mode 100644 index 0000000000..91b85e4547 --- /dev/null +++ b/packages/std/src/storage_keys/range.rs @@ -0,0 +1,141 @@ +use core::ops::{Bound, RangeBounds}; + +use crate::Binary; + +pub trait ToByteVec { + fn to_byte_vec(&self) -> Vec; +} +impl ToByteVec for Vec { + fn to_byte_vec(&self) -> Vec { + self.clone() + } +} +impl ToByteVec for [u8] { + fn to_byte_vec(&self) -> Vec { + self.to_vec() + } +} +impl ToByteVec for [u8; N] { + fn to_byte_vec(&self) -> Vec { + self.to_vec() + } +} +impl ToByteVec for Binary { + fn to_byte_vec(&self) -> Vec { + self.to_vec() + } +} + +/// Converts any range to start and end bounds for ranging through storage. +/// The start bound is inclusive, the end bound is exclusive. +pub fn range_to_bounds<'a, R, B>(range: &R) -> (Option>, Option>) +where + R: RangeBounds<&'a B>, + B: ToByteVec + 'a + ?Sized, +{ + let start = match range.start_bound() { + Bound::Included(start) => Some(start.to_byte_vec()), + Bound::Excluded(start) => Some(key_after(start.to_byte_vec())), + Bound::Unbounded => None, + }; + let end = match range.end_bound() { + Bound::Included(end) => Some(key_after(end.to_byte_vec())), + Bound::Excluded(end) => Some(end.to_byte_vec()), + Bound::Unbounded => None, + }; + (start, end) +} + +/// Returns the key after the given key. +/// +/// Reuses the given vector. +fn key_after(mut key: Vec) -> Vec { + key.push(0); + key +} + +#[cfg(test)] +mod tests { + use crate::{testing::MockStorage, Order, Storage}; + + use super::*; + + #[test] + fn range_to_bounds_works() { + let mut storage = MockStorage::new(); + + let keys: &[&[u8]] = &[ + &[1, 2, 3], + &[1, 2, 4], + &[1, 2, 5], + &[1, 2, 6], + &[1, 2, 7], + &[1, 2, 7, 0], + &[1, 2, 7, 1], + &[1, 2, 7, 2], + &[1, 2, 8], + &[1, 2, 8, 0], + &[1, 2, 8, 1], + ]; + // map every key to its index + for (i, &key) in keys.iter().enumerate() { + storage.set(key, &[i as u8]); + } + + // check the range between any two keys inside the storage + for (idx0, &key0) in keys.iter().enumerate() { + for (idx1, &key1) in keys.iter().enumerate() { + // key0..key1 should have idx0..idx1 as values + assert_range(&storage, key0..key1, (idx0..idx1).map(|idx| idx as u8)); + + // key0..=key1 should have idx0..=idx1 as values + assert_range(&storage, key0..=key1, (idx0..=idx1).map(|idx| idx as u8)); + } + + // key0.. should have idx0.. as values + assert_range(&storage, key0.., (idx0..keys.len()).map(|idx| idx as u8)); + // ..key0 should have 0..idx0 as values + assert_range(&storage, ..key0, (0..idx0).map(|idx| idx as u8)); + // ..=key0 should have 0..=idx0 as values + assert_range(&storage, ..=key0, (0..=idx0).map(|idx| idx as u8)); + } + + // 0..not_in_storage should have range from start to last key before not_in_storage + let zero: &[u8] = &[0u8]; + let not_in_storage = &[1u8, 2, 7, 3]; + assert_range(&storage, zero..not_in_storage, 0u8..=7); + assert_range(&storage, zero..=not_in_storage, 0u8..=7); + + // 0..after_last_key should have full range + let after_last_key: &[u8] = &[1u8, 2, 8, 2]; + assert_range(&storage, zero..after_last_key, 0u8..keys.len() as u8); + assert_range(&storage, zero..=after_last_key, 0u8..keys.len() as u8); + + // full range + assert_range(&storage, .., 0u8..keys.len() as u8); + + fn assert_range<'a>( + storage: &MockStorage, + range: impl RangeBounds<&'a [u8]>, + expected_values: impl DoubleEndedIterator + Clone, + ) { + let (s, e) = range_to_bounds(&range); + // ascending + let values = storage + .range_values(s.as_deref(), e.as_deref(), Order::Ascending) + .collect::>(); + assert_eq!( + values, + expected_values.clone().map(|v| vec![v]).collect::>() + ); + // descending + let values = storage + .range_values(s.as_deref(), e.as_deref(), Order::Descending) + .collect::>(); + assert_eq!( + values, + expected_values.rev().map(|v| vec![v]).collect::>() + ); + } + } +} diff --git a/packages/std/src/testing/mock.rs b/packages/std/src/testing/mock.rs index b3f57c039f..2363398fa4 100644 --- a/packages/std/src/testing/mock.rs +++ b/packages/std/src/testing/mock.rs @@ -946,6 +946,10 @@ impl Default for WasmQuerier { WasmQuery::CodeInfo { code_id, .. } => { SystemError::NoSuchCode { code_id: *code_id } } + #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))] + WasmQuery::RawRange { contract_addr, .. } => SystemError::NoSuchContract { + addr: contract_addr.clone(), + }, }; SystemResult::Err(err) }); @@ -1424,7 +1428,7 @@ mod tests { use crate::coins; #[cfg(feature = "cosmwasm_1_3")] use crate::DenomUnit; - use crate::{coin, instantiate2_address, ContractInfoResponse, HexBinary, Response}; + use crate::{coin, instantiate2_address, ContractInfoResponse, HexBinary, Response, Storage}; #[cfg(feature = "staking")] use crate::{Decimal, Delegation}; use base64::{engine::general_purpose, Engine}; @@ -2734,11 +2738,29 @@ mod tests { } } + #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))] + { + // By default, querier errors for WasmQuery::RawRange + let system_err = querier + .query(&WasmQuery::RawRange { + contract_addr: any_addr.clone(), + start: None, + end: None, + limit: 10, + order: crate::Order::Ascending, + }) + .unwrap_err(); + match system_err { + SystemError::NoSuchContract { addr } => assert_eq!(addr, any_addr), + err => panic!("Unexpected error: {err:?}"), + } + } + querier.update_handler(|request| { let api = MockApi::default(); let contract1 = api.addr_make("contract1"); - let mut storage1 = BTreeMap::::default(); - storage1.insert(b"the key".into(), b"the value".into()); + let mut storage1 = MockStorage::new(); + storage1.set(b"the key", b"the value"); match request { WasmQuery::Raw { contract_addr, key } => { @@ -2749,7 +2771,7 @@ mod tests { }; if addr == contract1 { if let Some(value) = storage1.get(key) { - SystemResult::Ok(ContractResult::Ok(value.clone())) + SystemResult::Ok(ContractResult::Ok(Binary::new(value))) } else { SystemResult::Ok(ContractResult::Ok(Binary::default())) } @@ -2822,6 +2844,47 @@ mod tests { SystemResult::Err(SystemError::NoSuchCode { code_id }) } } + #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))] + WasmQuery::RawRange { + contract_addr, + start, + end, + limit, + order, + } => { + let Ok(addr) = api.addr_validate(contract_addr) else { + return SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.clone(), + }); + }; + if addr == contract1 { + let mut data: Vec<_> = storage1 + .range( + start.as_ref().map(Binary::as_slice), + end.as_ref().map(Binary::as_slice), + *order, + ) + .take(*limit as usize + 1) // take one more entry than limit + .map(|(key, value)| (Binary::new(key), Binary::new(value))) + .collect(); + + // if we have more than limit, there are more entries to fetch + let next_key = if data.len() > *limit as usize { + data.pop().map(|(key, _)| key) + } else { + None + }; + let raw_range_response = crate::RawRangeResponse { data, next_key }; + + SystemResult::Ok(ContractResult::Ok( + to_json_binary(&raw_range_response).unwrap(), + )) + } else { + SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.clone(), + }) + } + } } }); @@ -2871,7 +2934,7 @@ mod tests { // WasmQuery::ContractInfo let result = querier.query(&WasmQuery::ContractInfo { - contract_addr: contract_addr.into(), + contract_addr: contract_addr.clone().into(), }); match result { SystemResult::Ok(ContractResult::Ok(value)) => assert_eq!( @@ -2894,6 +2957,24 @@ mod tests { res => panic!("Unexpected result: {res:?}"), } } + + #[cfg(all(feature = "cosmwasm_3_0", feature = "iterator"))] + { + let result = querier.query(&WasmQuery::RawRange { + contract_addr: contract_addr.clone().into(), + start: Some(Binary::from(b"the key")), + end: Some(Binary::from(b"the keyasdf")), + limit: 10, + order: crate::Order::Ascending, + }); + match result { + SystemResult::Ok(ContractResult::Ok(value)) => assert_eq!( + value.as_slice(), + br#"{"data":[["dGhlIGtleQ==","dGhlIHZhbHVl"]],"next_key":null}"# + ), + res => panic!("Unexpected result: {res:?}"), + } + } } #[test] diff --git a/packages/vm/src/testing/instance.rs b/packages/vm/src/testing/instance.rs index 232dcfadc7..f23fe4915b 100644 --- a/packages/vm/src/testing/instance.rs +++ b/packages/vm/src/testing/instance.rs @@ -98,7 +98,7 @@ impl MockInstanceOptions<'_> { fn default_capabilities() -> HashSet { #[allow(unused_mut)] let mut out = capabilities_from_csv( - "ibc2,iterator,staking,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4,cosmwasm_2_0,cosmwasm_2_1,cosmwasm_2_2", + "ibc2,iterator,staking,cosmwasm_1_1,cosmwasm_1_2,cosmwasm_1_3,cosmwasm_1_4,cosmwasm_2_0,cosmwasm_2_1,cosmwasm_2_2,cosmwasm_3_0", ); #[cfg(feature = "stargate")] out.insert("stargate".to_string());