Skip to content

Implement RawRange query #2471

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
2 changes: 2 additions & 0 deletions docs/CAPABILITIES-BUILT-IN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion packages/go-gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
] }
Expand Down
19 changes: 16 additions & 3 deletions packages/go-gen/src/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("\"`")
}
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 10 additions & 5 deletions packages/go-gen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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::<Vec<_>>();
let expected_no_ws = expected.split_whitespace().collect::<Vec<_>>();
Expand All @@ -210,6 +211,7 @@ mod tests {
);
}

#[track_caller]
fn assert_code_eq_ignore_docs(actual: String, expected: &str) {
let actual_filtered = actual
.lines()
Expand Down Expand Up @@ -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"`
}"#,
);
Expand Down Expand Up @@ -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);
Expand All @@ -355,6 +357,7 @@ mod tests {
// wasm
compare_codes!(cosmwasm_std::ContractInfoResponse);
compare_codes!(cosmwasm_std::CodeInfoResponse);
compare_codes!(cosmwasm_std::RawRangeResponse);
}

#[test]
Expand Down Expand Up @@ -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<Vec<String>>,
nested: Vec<Option<Vec<String>>>,
}
Expand All @@ -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"`
}"#,
);
}
Expand Down
15 changes: 13 additions & 2 deletions packages/go-gen/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
""
Expand Down Expand Up @@ -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"),
}
}
Expand Down Expand Up @@ -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"),
Expand Down
6 changes: 6 additions & 0 deletions packages/go-gen/tests/cosmwasm_std__RawRangeResponse.go
Original file line number Diff line number Diff line change
@@ -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"`
}
18 changes: 18 additions & 0 deletions packages/go-gen/tests/cosmwasm_std__WasmQuery.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
5 changes: 4 additions & 1 deletion packages/std/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions packages/std/src/exports/exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion packages/std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
127 changes: 127 additions & 0 deletions packages/std/src/query/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Binary>,
/// Exclusive end bound. This is the key after the last key you would like to get data for.
end: Option<Binary>,
/// 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<String>,
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]
Expand Down Expand Up @@ -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<RawRangeEntry>,
/// `None` if there are no more key-value pairs within the given key range.
pub next_key: Option<Binary>,
}

impl_hidden_constructor!(
RawRangeResponse,
data: Vec<RawRangeEntry>,
next_key: Option<Binary>
);

pub type RawRangeEntry = (Binary, Binary);

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -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=="}"#,
);
}
}
Loading