diff --git a/.github/package-filters/rs-packages-direct.yml b/.github/package-filters/rs-packages-direct.yml index d9ea1b64067..441c8137023 100644 --- a/.github/package-filters/rs-packages-direct.yml +++ b/.github/package-filters/rs-packages-direct.yml @@ -115,6 +115,9 @@ rs-dapi-client: platform-encryption: - packages/rs-platform-encryption/** +dash-platform-queries: + - packages/dash-platform-queries/** + dash-sdk: - packages/rs-sdk/** diff --git a/.github/package-filters/rs-packages-no-workflows.yml b/.github/package-filters/rs-packages-no-workflows.yml index 3825b065eef..90835d0429f 100644 --- a/.github/package-filters/rs-packages-no-workflows.yml +++ b/.github/package-filters/rs-packages-no-workflows.yml @@ -127,9 +127,13 @@ rs-dapi-client: &dapi_client platform-encryption: &platform_encryption - packages/rs-platform-encryption/** +dash-platform-queries: &platform_queries + - packages/dash-platform-queries/** + dash-sdk: &sdk - packages/rs-drive-proof-verifier/** - packages/rs-sdk/** + - *platform_queries - *dash_async - *context_provider - *sdk_trusted_context_provider diff --git a/.github/package-filters/rs-packages.yml b/.github/package-filters/rs-packages.yml index f38a77fa931..6fae2aa84ab 100644 --- a/.github/package-filters/rs-packages.yml +++ b/.github/package-filters/rs-packages.yml @@ -151,10 +151,15 @@ platform-encryption: &platform_encryption - .github/workflows/tests* - packages/rs-platform-encryption/** +dash-platform-queries: &platform_queries + - .github/workflows/tests* + - packages/dash-platform-queries/** + dash-sdk: &sdk - .github/workflows/tests* - packages/rs-drive-proof-verifier/** - packages/rs-sdk/** + - *platform_queries - *dash_async - *context_provider - *sdk_trusted_context_provider diff --git a/.github/workflows/tests-rs-nightly-long-running.yml b/.github/workflows/tests-rs-nightly-long-running.yml index 460ba1b8d76..f65b87a37ec 100644 --- a/.github/workflows/tests-rs-nightly-long-running.yml +++ b/.github/workflows/tests-rs-nightly-long-running.yml @@ -21,7 +21,16 @@ jobs: fail-fast: false matrix: package: - [dash-sdk, rs-dapi-client, rs-dapi, dapi-grpc, dpp, drive-abci, drive-proof-verifier] + [ + dash-sdk, + rs-dapi-client, + rs-dapi, + dapi-grpc, + dpp, + drive-abci, + drive-proof-verifier, + dash-platform-queries, + ] steps: - name: Check out repo uses: actions/checkout@v4 diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index 6e40a7618f4..1c159c2c706 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -193,20 +193,29 @@ jobs: cargo install cargo-machete 2>/dev/null || true cargo machete - # The transport-free cut is how embedders with their own networking + # The transport-free cuts are how embedders with their own networking # (Dash Core's platform GUI, explorers) consume verification: feature - # unification hides transport regressions in whole-workspace builds, so - # check the standalone graphs and assert the networking stack stays out - # of the proof-verification tree (native) and out of wasm builds. - - name: Check transport-free feature cut + # unification hides regressions in whole-workspace builds, so check the + # standalone graphs and assert the networking stack stays out of the + # proof-verification tree (native) and out of wasm builds. + - name: Check transport-free feature cuts run: | cargo check -p dapi-grpc --no-default-features --features core,platform,client --locked cargo check -p drive-proof-verifier --locked - for banned in hyper rustls tower; do - if cargo tree -p drive-proof-verifier -e normal -i "$banned" 2>/dev/null | grep -q .; then - echo "::error::$banned leaked into drive-proof-verifier's dependency tree" - exit 1 - fi + cargo check -p dash-platform-queries --locked + # Native graphs: assert the networking transport stack stays out. + # `tonic` itself is present (dapi-grpc's generated client types) but + # without its transport feature — which is exactly what the absence + # of hyper/rustls/tower proves. tokio is deliberately NOT asserted + # absent: dash-context-provider depends on dash-async, which uses it + # on native targets, and that edge predates the queries-crate split. + for native_package in drive-proof-verifier dash-platform-queries; do + for banned in hyper rustls tower; do + if cargo tree -p "$native_package" -e normal -i "$banned" 2>/dev/null | grep -q .; then + echo "::error::$banned leaked into $native_package's dependency tree" + exit 1 + fi + done done for banned in hyper rustls tower mio; do for wasm_package in dash-sdk wasm-sdk; do diff --git a/Cargo.lock b/Cargo.lock index 6775bdd0c17..0a5f13e2142 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1702,6 +1702,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dash-platform-queries" +version = "4.2.0-dev.1" +dependencies = [ + "dapi-grpc", + "dash-context-provider", + "dash-platform-macros", + "dpp", + "drive", + "drive-proof-verifier", + "hex", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "dash-sdk" version = "4.2.0-dev.1" @@ -1719,6 +1736,7 @@ dependencies = [ "dash-context-provider", "dash-network-seeds", "dash-platform-macros", + "dash-platform-queries", "derive_more 1.0.0", "dotenvy", "dpp", diff --git a/Cargo.toml b/Cargo.toml index 5238bf2a982..70d1a17c4f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "packages/wasm-dpp2", "packages/rs-dapi-client", "packages/rs-dash-async", + "packages/dash-platform-queries", "packages/rs-sdk", "packages/strategy-tests", "packages/simple-signer", diff --git a/Dockerfile b/Dockerfile index d98f260209f..846150203f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -415,6 +415,7 @@ COPY --parents \ packages/wasm-dpp2 \ packages/wasm-drive-verify \ packages/rs-dapi-client \ + packages/dash-platform-queries \ packages/rs-sdk \ packages/rs-sdk-ffi \ packages/rs-unified-sdk-ffi \ @@ -541,6 +542,7 @@ COPY --parents \ packages/wasm-dpp2 \ packages/wasm-drive-verify \ packages/rs-dapi-client \ + packages/dash-platform-queries \ packages/rs-sdk \ packages/rs-sdk-ffi \ packages/rs-unified-sdk-ffi \ @@ -690,6 +692,7 @@ COPY --parents \ packages/rs-dash-platform-macros \ packages/rs-drive \ packages/rs-drive-proof-verifier \ + packages/dash-platform-queries \ packages/rs-sdk \ packages/rs-sdk-trusted-context-provider \ # Common @@ -962,6 +965,7 @@ COPY --parents \ packages/wasm-dpp2 \ packages/wasm-drive-verify \ packages/rs-dapi-client \ + packages/dash-platform-queries \ packages/rs-sdk \ packages/rs-sdk-ffi \ packages/rs-unified-sdk-ffi \ diff --git a/packages/check-features/src/main.rs b/packages/check-features/src/main.rs index ce31fce686d..8cba6485410 100644 --- a/packages/check-features/src/main.rs +++ b/packages/check-features/src/main.rs @@ -10,6 +10,7 @@ fn main() { ("rs-drive", vec![]), ("rs-drive-proof-verifier", vec![]), ("rs-platform-wallet", vec![]), + ("dash-platform-queries", vec![]), ]; for (specific_crate, to_ignore) in crates { diff --git a/packages/dash-platform-queries/Cargo.toml b/packages/dash-platform-queries/Cargo.toml new file mode 100644 index 00000000000..f3d76e7c747 --- /dev/null +++ b/packages/dash-platform-queries/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "dash-platform-queries" +description = "Transport-free query building and proof decoding core shared by Dash Platform SDK embedders" +version.workspace = true +edition = "2021" +rust-version.workspace = true +license = "MIT" + +[features] +default = [] +mocks = [ + "dep:serde", + "dep:serde_json", + "dapi-grpc/mocks", + "drive/serde", + "dpp/serde-conversion", +] + +[dependencies] +dapi-grpc = { path = "../dapi-grpc", default-features = false, features = [ + "platform", + "client", +] } +dash-context-provider = { path = "../rs-context-provider", default-features = false } +dash-platform-macros = { path = "../rs-dash-platform-macros" } +dpp = { path = "../rs-dpp", default-features = false, features = [ + "platform-value-cbor", + "state-transitions", + "state-transition-validation", +] } +drive = { path = "../rs-drive", default-features = false, features = [ + "verify", +] } +drive-proof-verifier = { path = "../rs-drive-proof-verifier", default-features = false } +hex = { version = "0.4.3" } +serde = { version = "1.0.219", default-features = false, features = [ + "rc", +], optional = true } +serde_json = { version = "1.0", optional = true } +thiserror = "2.0.17" +tracing = { version = "0.1.41" } + +[dev-dependencies] +dpp = { path = "../rs-dpp", default-features = false, features = [ + "fixtures-and-mocks", +] } + +[package.metadata.cargo-machete] +ignored = [ + # Used inside the `dash_platform_macros::Mockable` derive expansion under + # the `mocks` feature; machete cannot see through proc-macro output. + "serde_json", +] diff --git a/packages/dash-platform-queries/README.md b/packages/dash-platform-queries/README.md new file mode 100644 index 00000000000..a40be496986 --- /dev/null +++ b/packages/dash-platform-queries/README.md @@ -0,0 +1,57 @@ +# dash-platform-queries + +Transport-free query core of the Dash Platform SDK. + +This crate carries the pieces of `dash-sdk` that build queries, encode them +onto the wire format, and decode/verify proved responses — with **no +transport implementation**: no `rs-dapi-client` and no tonic native +channel/TLS stack. Shared generated types and context-provider utilities +remain dependencies. `dash-sdk` depends on it and re-exports everything at +the historical paths, so SDK users need no changes. + +## Who this is for + +Embedders that bring their own transport and trust context and only need the +verification/query layer: + +- **Dash Core's platform GUI** — fetches over its own gRPC-Web transport, + serves quorum keys from its locally synced LLMQ state via a + [`ContextProvider`](../rs-context-provider), and verifies every response + proof with [`drive-proof-verifier`](../rs-drive-proof-verifier). +- Block explorers, Electrum-style servers, hardware-wallet tooling — anything + that talks to DAPI its own way but must not trust responses. + +If you want networking, retries, and a managed connection pool, use +`dash-sdk` — it consumes this crate internally. + +## What's here + +- [`documents::DocumentQuery`] — rich document query builder with wire + encoding for both request versions. +- Aggregate proof helpers (count/sum/average/ranked) shared with `dash-sdk`. +- DPNS username helpers — label normalization/validation and the + convertibility/contested checks shared with `dash-sdk`. +- `transition::validation` — structural validation for state transitions + ahead of signing. + +Wire-request decoding (`DocumentQuery::try_from_request`), request-driven +proof verification, and pure DPNS/DashPay document builders arrive in the +next slice of this series. + +## Feature flags + +- `mocks` — serde support for the types used in dump/replay test vectors + (forwarded by `dash-sdk`'s `mocks`). + +The dependency tree is checked in CI to stay free of the transport stack +(`hyper`, `rustls`, `tower`); see the "Check transport-free feature cuts" +step in `.github/workflows/tests-rs-workspace.yml`. + +"Transport-free" means no networking stack, not an async-runtime-free graph: +`tokio` is still reachable on native targets through +`dash-context-provider` → `dash-async`, exactly as it already was for +`drive-proof-verifier` before this crate existed. `tonic` is present too, but +only for `dapi-grpc`'s generated message/client types — its transport feature +stays off, which is what the `hyper`/`rustls`/`tower` assertions prove. On +`wasm32-unknown-unknown` none of that is pulled in; the wasm assertions in the +same CI step also ban `mio`. diff --git a/packages/dash-platform-queries/src/block_info_from_metadata.rs b/packages/dash-platform-queries/src/block_info_from_metadata.rs new file mode 100644 index 00000000000..bfa2cea5b28 --- /dev/null +++ b/packages/dash-platform-queries/src/block_info_from_metadata.rs @@ -0,0 +1,47 @@ +use crate::Error; +use dapi_grpc::platform::v0::ResponseMetadata; +use dpp::block::block_info::BlockInfo; +use dpp::block::epoch::MAX_EPOCH; +use drive::error::proof::ProofError; + +/// Constructs a `BlockInfo` structure from the provided response metadata. This function +/// translates metadata received from a platform response into a format that is specific to the +/// application's needs, particularly focusing on block-related information. It ensures that +/// the epoch value from the metadata does not exceed `MAX_EPOCH`, +/// as this is a constraint for the `Epoch` type used in the `BlockInfo` structure. +/// +/// # Parameters +/// - `response_metadata`: A reference to `ResponseMetadata` obtained from a platform response. +/// This metadata includes various block-related information such as time in milliseconds, +/// height, core chain locked height, and epoch. +/// +/// # Returns +/// If successful, returns `Ok(BlockInfo)` where `BlockInfo` contains: +/// - `time_ms`: The timestamp of the block in milliseconds. +/// - `height`: The height of the block. +/// - `core_height`: The core chain locked height, indicating the height of the block in the core blockchain that is considered final and securely linked to this block. +/// - `epoch`: The epoch number, converted to an `Epoch` struct via a 16-bit number. +/// +/// # Errors +/// Returns an error if: +/// - The `epoch` value in the response metadata exceeds `MAX_EPOCH`. This is considered a data validity error as it indicates Platform returned an unexpectedly high epoch number. +/// +/// The function encapsulates errors into the application's own `Error` type, providing a unified interface for error handling across the application. +pub fn block_info_from_metadata(response_metadata: &ResponseMetadata) -> Result { + if response_metadata.epoch > MAX_EPOCH as u32 { + return Err( + drive::error::Error::Proof(ProofError::InvalidMetadata(format!( + "platform returned an epoch {} that was higher than the maximum allowed epoch", + response_metadata.epoch + ))) + .into(), + ); + } + + Ok(BlockInfo { + time_ms: response_metadata.time_ms, + height: response_metadata.height, + core_height: response_metadata.core_chain_locked_height, + epoch: (response_metadata.epoch as u16).try_into()?, + }) +} diff --git a/packages/rs-sdk/src/platform/documents/average_proof_helpers.rs b/packages/dash-platform-queries/src/documents/average_proof_helpers.rs similarity index 99% rename from packages/rs-sdk/src/platform/documents/average_proof_helpers.rs rename to packages/dash-platform-queries/src/documents/average_proof_helpers.rs index ec73d18fba0..5ab4e9bf77a 100644 --- a/packages/rs-sdk/src/platform/documents/average_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/average_proof_helpers.rs @@ -19,7 +19,7 @@ //! [`DocumentAverage`]: drive_proof_verifier::DocumentAverage //! [`DocumentSplitAverages`]: drive_proof_verifier::DocumentSplitAverages -use crate::platform::documents::document_query::DocumentQuery; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; use dash_context_provider::ContextProvider; diff --git a/packages/rs-sdk/src/platform/documents/count_proof_helpers.rs b/packages/dash-platform-queries/src/documents/count_proof_helpers.rs similarity index 99% rename from packages/rs-sdk/src/platform/documents/count_proof_helpers.rs rename to packages/dash-platform-queries/src/documents/count_proof_helpers.rs index 9b99fcdee48..e19e9074f53 100644 --- a/packages/rs-sdk/src/platform/documents/count_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/count_proof_helpers.rs @@ -13,7 +13,7 @@ //! [`DocumentCount`]: drive_proof_verifier::DocumentCount //! [`DocumentSplitCounts`]: drive_proof_verifier::DocumentSplitCounts -use crate::platform::documents::document_query::DocumentQuery; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; use dash_context_provider::ContextProvider; diff --git a/packages/rs-sdk/src/platform/documents/document_average.rs b/packages/dash-platform-queries/src/documents/document_average.rs similarity index 96% rename from packages/rs-sdk/src/platform/documents/document_average.rs rename to packages/dash-platform-queries/src/documents/document_average.rs index 340b7ca1a4e..9a24a365b77 100644 --- a/packages/rs-sdk/src/platform/documents/document_average.rs +++ b/packages/dash-platform-queries/src/documents/document_average.rs @@ -13,11 +13,8 @@ //! absent branch — same forward-compat for absence proofs as count) //! contribute 0 to both axes via `filter_map(|e| e.)`. -use crate::platform::documents::average_proof_helpers::{ - assert_select_is_avg, verify_average_query, -}; -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::Fetch; +use crate::documents::average_proof_helpers::{assert_select_is_avg, verify_average_query}; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -101,11 +98,6 @@ impl FromProof for DocumentAverage { } } -impl Fetch for DocumentAverage { - type Query = super::document_query::DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} - #[cfg(test)] mod tests { //! Unit tests for the AVG fold. The fold logic is extracted diff --git a/packages/rs-sdk/src/platform/documents/document_count.rs b/packages/dash-platform-queries/src/documents/document_count.rs similarity index 85% rename from packages/rs-sdk/src/platform/documents/document_count.rs rename to packages/dash-platform-queries/src/documents/document_count.rs index 8f46f8c9c90..1ea5899ebd8 100644 --- a/packages/rs-sdk/src/platform/documents/document_count.rs +++ b/packages/dash-platform-queries/src/documents/document_count.rs @@ -13,9 +13,8 @@ //! queried-but-absent branch) contribute 0 to the sum via //! `filter_map(|e| e.count)`. -use crate::platform::documents::count_proof_helpers::{assert_select_is_count, verify_count_query}; -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::Fetch; +use crate::documents::count_proof_helpers::{assert_select_is_count, verify_count_query}; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -47,8 +46,3 @@ impl FromProof for DocumentCount { Ok((count, mtd, proof)) } } - -impl Fetch for DocumentCount { - type Query = DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} diff --git a/packages/rs-sdk/src/platform/documents/document_having_entries.rs b/packages/dash-platform-queries/src/documents/document_having_entries.rs similarity index 96% rename from packages/rs-sdk/src/platform/documents/document_having_entries.rs rename to packages/dash-platform-queries/src/documents/document_having_entries.rs index 6b6522e00b0..33b4997895f 100644 --- a/packages/rs-sdk/src/platform/documents/document_having_entries.rs +++ b/packages/dash-platform-queries/src/documents/document_having_entries.rs @@ -54,7 +54,7 @@ //! //! `SELECT COUNT(*) GROUP BY hashtag HAVING $count > 100 ORDER BY $count DESC LIMIT 100` //! -//! ```rust,no_run +//! ```rust,ignore //! use dash_sdk::{Sdk, platform::{DataContract, DocumentQuery, Fetch, Identifier}}; //! use dash_sdk::drive::query::{ //! HavingAggregate, HavingAggregateFunction, HavingClause, HavingOperator, @@ -96,9 +96,8 @@ //! } //! ``` -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::documents::having_proof_helpers::verify_having_query; -use crate::platform::Fetch; +use crate::documents::document_query::DocumentQuery; +use crate::documents::having_proof_helpers::verify_having_query; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -133,11 +132,6 @@ impl FromProof for DocumentHavingEntries { } } -impl Fetch for DocumentHavingEntries { - type Query = DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} - #[cfg(test)] mod tests { //! Offline tests for the having client surface: the request→wire @@ -147,8 +141,8 @@ mod tests { //! `having_range_tests`, where a populated Drive exists. use super::*; - use crate::platform::documents::document_query::RankingDirection; - use crate::platform::documents::having_proof_helpers::assert_having_shape; + use crate::documents::document_query::RankingDirection; + use crate::documents::having_proof_helpers::assert_having_shape; use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::select as proto_select; use dapi_grpc::platform::v0::get_documents_request::{ having_aggregate, having_clause, GetDocumentsRequestV1, Version as RequestVersion, diff --git a/packages/rs-sdk/src/platform/documents/document_history_query.rs b/packages/dash-platform-queries/src/documents/document_history_query.rs similarity index 100% rename from packages/rs-sdk/src/platform/documents/document_history_query.rs rename to packages/dash-platform-queries/src/documents/document_history_query.rs diff --git a/packages/rs-sdk/src/platform/documents/document_query.rs b/packages/dash-platform-queries/src/documents/document_query.rs similarity index 96% rename from packages/rs-sdk/src/platform/documents/document_query.rs rename to packages/dash-platform-queries/src/documents/document_query.rs index 679dcd9f2c9..523a1364de3 100644 --- a/packages/rs-sdk/src/platform/documents/document_query.rs +++ b/packages/dash-platform-queries/src/documents/document_query.rs @@ -2,8 +2,7 @@ use std::sync::Arc; -use crate::platform::Fetch; -use crate::{error::Error, sdk::Sdk}; +use crate::error::Error; use dapi_grpc::platform::v0::get_documents_request::Version::{V0, V1}; use dapi_grpc::platform::v0::{ self as platform_proto, @@ -189,25 +188,6 @@ impl DocumentQuery { Self::from(d) } - /// Create new document query for provided document type name and data contract ID. - /// - /// Note that this method will fetch data contract first. - pub async fn new_with_data_contract_id( - api: &Sdk, - data_contract_id: Identifier, - document_type_name: &str, - ) -> Result { - let data_contract = - DataContract::fetch(api, data_contract_id) - .await? - .ok_or(Error::MissingDependency( - "DataContract".to_string(), - format!("data contract {} not found", data_contract_id), - ))?; - - Self::new(data_contract, document_type_name) - } - /// Point to a specific document ID. pub fn with_document_id(self, document_id: &Identifier) -> Self { let clause = WhereClause { @@ -333,7 +313,7 @@ impl DocumentQuery { /// /// # The 5th-best group /// - /// ```rust,no_run + /// ```rust,ignore /// # use dash_sdk::platform::{DataContract, DocumentQuery}; /// # use dash_sdk::platform::documents::document_query::RankingDirection; /// # use dash_sdk::drive::query::SelectProjection; @@ -1095,20 +1075,3 @@ fn value_to_proto_at_depth(value: Value, depth: u8) -> Result for DocumentQuery { - fn query( - &self, - settings: &crate::platform::QuerySettings<'_>, - ) -> Result { - GetDocumentsRequest::try_from_platform_versioned(self.clone(), settings.protocol_version) - } -} diff --git a/packages/rs-sdk/src/platform/documents/document_ranked_entries.rs b/packages/dash-platform-queries/src/documents/document_ranked_entries.rs similarity index 97% rename from packages/rs-sdk/src/platform/documents/document_ranked_entries.rs rename to packages/dash-platform-queries/src/documents/document_ranked_entries.rs index f5a80ee3a59..5f9461e1ea3 100644 --- a/packages/rs-sdk/src/platform/documents/document_ranked_entries.rs +++ b/packages/dash-platform-queries/src/documents/document_ranked_entries.rs @@ -85,7 +85,7 @@ //! //! `SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 5` //! -//! ```rust,no_run +//! ```rust,ignore //! use dash_sdk::{Sdk, platform::{DataContract, DocumentQuery, Fetch, Identifier}}; //! use dash_sdk::drive::query::SelectProjection; //! use dash_sdk::platform::documents::document_query::RankingDirection; @@ -132,7 +132,7 @@ //! //! `SELECT AVG(grade) GROUP BY restaurantId ORDER BY avg(grade) DESC LIMIT 1 OFFSET 4` //! -//! ```rust,no_run +//! ```rust,ignore //! # use dash_sdk::platform::{DataContract, DocumentQuery}; //! # use dash_sdk::platform::documents::document_query::RankingDirection; //! # use dash_sdk::drive::query::SelectProjection; @@ -147,9 +147,8 @@ //! # } //! ``` -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::documents::ranked_proof_helpers::verify_ranked_query; -use crate::platform::Fetch; +use crate::documents::document_query::DocumentQuery; +use crate::documents::ranked_proof_helpers::verify_ranked_query; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -183,11 +182,6 @@ impl FromProof for DocumentRankedEntries { } } -impl Fetch for DocumentRankedEntries { - type Query = DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} - #[cfg(test)] mod tests { //! Offline tests for the ranked client surface: the ordering @@ -206,8 +200,8 @@ mod tests { //! not exist offline. use super::*; - use crate::platform::documents::document_query::RankingDirection; - use crate::platform::documents::ranked_proof_helpers::assert_ranked_shape; + use crate::documents::document_query::RankingDirection; + use crate::documents::ranked_proof_helpers::assert_ranked_shape; use dapi_grpc::platform::v0::get_documents_request::get_documents_request_v1::select as proto_select; use dapi_grpc::platform::v0::get_documents_request::{ order_clause, GetDocumentsRequestV1, OrderClause as ProtoOrderClause, diff --git a/packages/rs-sdk/src/platform/documents/document_split_averages.rs b/packages/dash-platform-queries/src/documents/document_split_averages.rs similarity index 85% rename from packages/rs-sdk/src/platform/documents/document_split_averages.rs rename to packages/dash-platform-queries/src/documents/document_split_averages.rs index f15f1695151..29a3ab9252d 100644 --- a/packages/rs-sdk/src/platform/documents/document_split_averages.rs +++ b/packages/dash-platform-queries/src/documents/document_split_averages.rs @@ -12,11 +12,8 @@ //! impl passes the verified entries through unchanged, mapping //! `AverageEntry` to `SplitAverageEntry`. -use crate::platform::documents::average_proof_helpers::{ - assert_select_is_avg, verify_average_query, -}; -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::Fetch; +use crate::documents::average_proof_helpers::{assert_select_is_avg, verify_average_query}; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -57,8 +54,3 @@ impl FromProof for DocumentSplitAverages { Ok((split, mtd, proof)) } } - -impl Fetch for DocumentSplitAverages { - type Query = super::document_query::DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} diff --git a/packages/rs-sdk/src/platform/documents/document_split_counts.rs b/packages/dash-platform-queries/src/documents/document_split_counts.rs similarity index 89% rename from packages/rs-sdk/src/platform/documents/document_split_counts.rs rename to packages/dash-platform-queries/src/documents/document_split_counts.rs index 79fb3455354..18eb2ab664f 100644 --- a/packages/rs-sdk/src/platform/documents/document_split_counts.rs +++ b/packages/dash-platform-queries/src/documents/document_split_counts.rs @@ -31,9 +31,8 @@ //! ranges are simply absent — the range itself is unbounded so //! there's no enumerable key set to ever-emit. -use crate::platform::documents::count_proof_helpers::{assert_select_is_count, verify_count_query}; -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::Fetch; +use crate::documents::count_proof_helpers::{assert_select_is_count, verify_count_query}; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -62,8 +61,3 @@ impl FromProof for DocumentSplitCounts { Ok((entries.map(DocumentSplitCounts::from_verified), mtd, proof)) } } - -impl Fetch for DocumentSplitCounts { - type Query = DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} diff --git a/packages/rs-sdk/src/platform/documents/document_split_sums.rs b/packages/dash-platform-queries/src/documents/document_split_sums.rs similarity index 85% rename from packages/rs-sdk/src/platform/documents/document_split_sums.rs rename to packages/dash-platform-queries/src/documents/document_split_sums.rs index fc5d1203304..50cdd6943ae 100644 --- a/packages/rs-sdk/src/platform/documents/document_split_sums.rs +++ b/packages/dash-platform-queries/src/documents/document_split_sums.rs @@ -12,9 +12,8 @@ //! passes the verified entries through unchanged, mapping //! `SumEntry` to `SplitSumEntry`. -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::documents::sum_proof_helpers::{assert_select_is_sum, verify_sum_query}; -use crate::platform::Fetch; +use crate::documents::document_query::DocumentQuery; +use crate::documents::sum_proof_helpers::{assert_select_is_sum, verify_sum_query}; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -54,8 +53,3 @@ impl FromProof for DocumentSplitSums { Ok((split, mtd, proof)) } } - -impl Fetch for DocumentSplitSums { - type Query = super::document_query::DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} diff --git a/packages/rs-sdk/src/platform/documents/document_sum.rs b/packages/dash-platform-queries/src/documents/document_sum.rs similarity index 95% rename from packages/rs-sdk/src/platform/documents/document_sum.rs rename to packages/dash-platform-queries/src/documents/document_sum.rs index c4265ec9ece..d88d57f530d 100644 --- a/packages/rs-sdk/src/platform/documents/document_sum.rs +++ b/packages/dash-platform-queries/src/documents/document_sum.rs @@ -18,9 +18,8 @@ //! can switch to `DocumentSplitSums` (which preserves per-branch //! `i64`s and lets the caller pick its own arithmetic). -use crate::platform::documents::document_query::DocumentQuery; -use crate::platform::documents::sum_proof_helpers::{assert_select_is_sum, verify_sum_query}; -use crate::platform::Fetch; +use crate::documents::document_query::DocumentQuery; +use crate::documents::sum_proof_helpers::{assert_select_is_sum, verify_sum_query}; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dash_context_provider::ContextProvider; use dpp::dashcore::Network; @@ -86,11 +85,6 @@ impl FromProof for DocumentSum { } } -impl Fetch for DocumentSum { - type Query = super::document_query::DocumentQuery; - type Request = dapi_grpc::platform::v0::GetDocumentsRequest; -} - #[cfg(test)] mod tests { //! Unit tests for the SUM fold. The fold logic is extracted diff --git a/packages/rs-sdk/src/platform/documents/having_proof_helpers.rs b/packages/dash-platform-queries/src/documents/having_proof_helpers.rs similarity index 99% rename from packages/rs-sdk/src/platform/documents/having_proof_helpers.rs rename to packages/dash-platform-queries/src/documents/having_proof_helpers.rs index d989c2ab254..c751e30ca09 100644 --- a/packages/rs-sdk/src/platform/documents/having_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/having_proof_helpers.rs @@ -16,7 +16,7 @@ //! //! [`DocumentHavingEntries`]: drive_proof_verifier::DocumentHavingEntries -use crate::platform::documents::document_query::DocumentQuery; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; use dash_context_provider::ContextProvider; diff --git a/packages/dash-platform-queries/src/documents/mod.rs b/packages/dash-platform-queries/src/documents/mod.rs new file mode 100644 index 00000000000..56eb5b40757 --- /dev/null +++ b/packages/dash-platform-queries/src/documents/mod.rs @@ -0,0 +1,33 @@ +pub(crate) mod average_proof_helpers; +pub(crate) mod count_proof_helpers; +/// `FromProof` impl for the average-side aggregate result. Returns +/// `(count, sum)`; client divides. +pub mod document_average; +pub mod document_count; +/// `FromProof` impl for the having-range (`GROUP BY … HAVING +/// LIMIT n`) result — one entry per matching group, in +/// axis order, with proof-attested completeness. Requires an index +/// declaring `rankedCountable` / `rankedSummable` / `rankedAverageable` +/// (protocol version 14+). +pub mod document_having_entries; +pub mod document_history_query; +pub mod document_query; +/// `FromProof` impl for the ranked (`GROUP BY … ORDER BY LIMIT n +/// [OFFSET m]`) result — one entry per returned group, in ranking order, +/// plus the rank the page starts at. Requires an index declaring +/// `rankedCountable` / `rankedSummable` / `rankedAverageable` +/// (protocol version 14+). +pub mod document_ranked_entries; +/// `FromProof` impl for the average-side per-entry result. Mirrors +/// `document_split_sums`. +pub mod document_split_averages; +pub mod document_split_counts; +/// `FromProof` impl for the sum-side per-entry result. Mirrors +/// `document_split_counts`. +pub mod document_split_sums; +/// `FromProof` impl for the sum-side aggregate result. Mirrors +/// `document_count`. Lights up alongside grovedb PR 670. +pub mod document_sum; +pub(crate) mod having_proof_helpers; +pub(crate) mod ranked_proof_helpers; +pub(crate) mod sum_proof_helpers; diff --git a/packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs b/packages/dash-platform-queries/src/documents/ranked_proof_helpers.rs similarity index 99% rename from packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs rename to packages/dash-platform-queries/src/documents/ranked_proof_helpers.rs index 1aa2fb817eb..f442842fb4c 100644 --- a/packages/rs-sdk/src/platform/documents/ranked_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/ranked_proof_helpers.rs @@ -19,7 +19,7 @@ //! //! [`DocumentRankedEntries`]: drive_proof_verifier::DocumentRankedEntries -use crate::platform::documents::document_query::DocumentQuery; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; use dash_context_provider::ContextProvider; diff --git a/packages/rs-sdk/src/platform/documents/sum_proof_helpers.rs b/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs similarity index 99% rename from packages/rs-sdk/src/platform/documents/sum_proof_helpers.rs rename to packages/dash-platform-queries/src/documents/sum_proof_helpers.rs index 17005e06301..fadfd00cfd8 100644 --- a/packages/rs-sdk/src/platform/documents/sum_proof_helpers.rs +++ b/packages/dash-platform-queries/src/documents/sum_proof_helpers.rs @@ -19,7 +19,7 @@ //! [`DocumentSum`]: drive_proof_verifier::DocumentSum //! [`DocumentSplitSums`]: drive_proof_verifier::DocumentSplitSums -use crate::platform::documents::document_query::DocumentQuery; +use crate::documents::document_query::DocumentQuery; use dapi_grpc::platform::v0::{GetDocumentsResponse, Proof, ResponseMetadata}; use dapi_grpc::platform::VersionedGrpcResponse; use dash_context_provider::ContextProvider; diff --git a/packages/dash-platform-queries/src/dpns_usernames.rs b/packages/dash-platform-queries/src/dpns_usernames.rs new file mode 100644 index 00000000000..3f452519b29 --- /dev/null +++ b/packages/dash-platform-queries/src/dpns_usernames.rs @@ -0,0 +1,196 @@ +//! Transport-free DPNS username helpers. +//! +//! The Sdk-bound DPNS surface (registration, availability checks, name +//! resolution) lives in `dash-sdk`; these free functions are pure string +//! validation/normalization shared with embedders. + +/// Convert a string to homograph-safe characters by replacing 'o', 'i', and 'l' +/// with '0', '1', and '1' respectively to prevent homograph attacks +pub fn convert_to_homograph_safe_chars(input: &str) -> String { + input + .chars() + .map(|c| match c { + 'o' | 'O' => '0', + 'i' | 'I' => '1', + 'l' | 'L' => '1', + _ => c.to_ascii_lowercase(), + }) + .collect() +} + +/// Check if a username is valid according to DPNS rules +/// +/// A username is valid if: +/// - It's between 3 and 63 characters long +/// - It starts and ends with alphanumeric characters (a-zA-Z0-9) +/// - It contains only alphanumeric characters and hyphens +/// - It doesn't have consecutive hyphens (enforced by the pattern) +/// +/// Pattern: `^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$` +/// +/// # Arguments +/// +/// * `label` - The username label to check (e.g., "alice") +/// +/// # Returns +/// +/// Returns `true` if the username is valid, `false` otherwise +pub fn is_valid_username(label: &str) -> bool { + // Check length + if label.len() < 3 || label.len() > 63 { + return false; + } + + let chars: Vec = label.chars().collect(); + + // Check first character (must be alphanumeric) + if !chars[0].is_ascii_alphanumeric() { + return false; + } + + // Check last character (must be alphanumeric) + if !chars[chars.len() - 1].is_ascii_alphanumeric() { + return false; + } + + // Check middle characters (can be alphanumeric or hyphen) + for &ch in &chars[1..chars.len() - 1] { + if !ch.is_ascii_alphanumeric() && ch != '-' { + return false; + } + } + + // Additional check: no consecutive hyphens (good practice) + for i in 0..chars.len() - 1 { + if chars[i] == '-' && chars[i + 1] == '-' { + return false; + } + } + + true +} + +/// Check if a username is contested (requires masternode voting) +/// +/// A username is contested if its normalized label: +/// - Is between 3 and 19 characters long (inclusive) +/// - Contains only lowercase letters a-z, digits 0-1, and hyphens +/// +/// # Arguments +/// +/// * `label` - The username label to check (e.g., "alice") +/// +/// # Returns +/// +/// Returns `true` if the username would be contested, `false` otherwise +pub fn is_contested_username(label: &str) -> bool { + let normalized = convert_to_homograph_safe_chars(label); + + // Check length + if normalized.len() < 3 || normalized.len() > 19 { + return false; + } + + // Check if all characters match the pattern [a-z01-] + normalized + .chars() + .all(|c| matches!(c, 'a'..='z' | '0' | '1' | '-')) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_convert_to_homograph_safe_chars() { + assert_eq!(convert_to_homograph_safe_chars("alice"), "a11ce"); + assert_eq!(convert_to_homograph_safe_chars("bob"), "b0b"); + assert_eq!(convert_to_homograph_safe_chars("COOL"), "c001"); + assert_eq!(convert_to_homograph_safe_chars("test123"), "test123"); + } + + #[test] + fn test_is_valid_username() { + // Valid usernames + assert!(is_valid_username("abc")); + assert!(is_valid_username("alice")); + assert!(is_valid_username("Alice123")); + assert!(is_valid_username("dash-p2p")); + assert!(is_valid_username("test-name-123")); + assert!(is_valid_username("a-b-c")); + assert!(is_valid_username("user2024")); + assert!(is_valid_username("CryptoKing")); + assert!(is_valid_username("web3-developer")); + assert!(is_valid_username("a".repeat(63).as_str())); // Max length + + // Invalid - too short + assert!(!is_valid_username("ab")); + assert!(!is_valid_username("a")); + assert!(!is_valid_username("")); + + // Invalid - too long + assert!(!is_valid_username("a".repeat(64).as_str())); + + // Invalid - starts with hyphen + assert!(!is_valid_username("-alice")); + assert!(!is_valid_username("-test")); + + // Invalid - ends with hyphen + assert!(!is_valid_username("alice-")); + assert!(!is_valid_username("test-")); + + // Invalid - starts and ends with hyphen + assert!(!is_valid_username("-alice-")); + + // Invalid - contains invalid characters + assert!(!is_valid_username("alice_bob")); // underscore + assert!(!is_valid_username("alice.bob")); // dot + assert!(!is_valid_username("alice@dash")); // at sign + assert!(!is_valid_username("alice!")); // exclamation + assert!(!is_valid_username("alice bob")); // space + assert!(!is_valid_username("alice#1")); // hash + assert!(!is_valid_username("alice$")); // dollar + assert!(!is_valid_username("alice%20")); // percent + + // Invalid - consecutive hyphens + assert!(!is_valid_username("alice--bob")); + assert!(!is_valid_username("test---name")); + } + + #[test] + fn test_is_contested_username() { + // Contested usernames (3-19 chars, only [a-z01-]) + assert!(is_contested_username("abc")); + assert!(is_contested_username("alice")); // becomes "a11ce" + assert!(is_contested_username("b0b")); + assert!(is_contested_username("cool")); // becomes "c001" + assert!(is_contested_username("a-b-c")); + assert!(is_contested_username("hello")); // becomes "he110" + assert!(is_contested_username("world")); // becomes "w0r1d" + assert!(is_contested_username("dash")); + assert!(is_contested_username("a11ce")); // already normalized + assert!(is_contested_username("dash-dao")); // becomes "dash-da0" + + // Not contested - too short + assert!(!is_contested_username("ab")); + assert!(!is_contested_username("io")); // becomes "10" which is 2 chars + assert!(!is_contested_username("a")); + + // Not contested - too long (20+ chars) + assert!(!is_contested_username("twenty-characters-ab")); // 20 chars + assert!(!is_contested_username( + "this-is-a-very-long-username-that-exceeds-limit" + )); + + // Not contested - contains invalid characters after normalization + assert!(!is_contested_username("alice2")); // contains '2' + assert!(!is_contested_username("alice_bob")); // contains '_' + assert!(!is_contested_username("alice.bob")); // contains '.' + assert!(!is_contested_username("alice@dash")); // contains '@' + assert!(!is_contested_username("alice!")); // contains '!' + assert!(!is_contested_username("test123")); // contains '2' and '3' + assert!(!is_contested_username("dash-p2p")); // contains 'p' and '2' + assert!(!is_contested_username("user5")); // contains '5' + assert!(!is_contested_username("name_with_underscore")); // contains '_' + } +} diff --git a/packages/dash-platform-queries/src/error.rs b/packages/dash-platform-queries/src/error.rs new file mode 100644 index 00000000000..0d8727763ce --- /dev/null +++ b/packages/dash-platform-queries/src/error.rs @@ -0,0 +1,45 @@ +//! Errors produced by the transport-free query core. + +use dpp::consensus::ConsensusError; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::ProtocolError; + +/// Error type for the transport-free query core. +/// +/// `dash-sdk` converts this into its own `Error` via `From`, so code that +/// moved here from the SDK keeps working behind `?` at its old call sites. +// Same allowance rs-sdk's Error carries: ProtocolError dominates the size. +#[allow(clippy::large_enum_variant)] +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Query is not configured properly for the target platform version + #[error("SDK misconfigured: {0}")] + Config(String), + /// Drive error + #[error("Drive error: {0}")] + Drive(#[from] drive::error::Error), + /// DPP error + #[error("Protocol error: {0}")] + Protocol(#[from] ProtocolError), +} + +impl From for Error { + fn from(value: ConsensusError) -> Self { + Self::Protocol(ProtocolError::ConsensusError(Box::new(value))) + } +} + +impl From for Error { + fn from(value: SimpleConsensusValidationResult) -> Self { + value + .errors + .into_iter() + .next() + .map(Error::from) + .unwrap_or_else(|| { + Error::Protocol(ProtocolError::CorruptedCodeExecution( + "state transition structure validation failed without an error".to_string(), + )) + }) + } +} diff --git a/packages/dash-platform-queries/src/lib.rs b/packages/dash-platform-queries/src/lib.rs new file mode 100644 index 00000000000..9da47cf6435 --- /dev/null +++ b/packages/dash-platform-queries/src/lib.rs @@ -0,0 +1,23 @@ +//! Transport-free query core of the Dash Platform SDK. +//! +//! This crate carries the pieces of `dash-sdk` that build queries, encode +//! them onto the wire format, and decode/verify proved responses — without a +//! transport implementation (no `rs-dapi-client` and no tonic native +//! channel/TLS stack). Shared generated types and context-provider utilities +//! remain dependencies. Embedders that bring their own transport can depend +//! on this crate alone; `dash-sdk` re-exports everything here at its historical +//! paths. + +// Same allowance the code carried in rs-sdk, whose crate root allows +// `result_large_err` for the dpp/drive error types threaded through here. +#![allow(clippy::result_large_err)] + +pub mod block_info_from_metadata; +pub mod documents; +pub mod dpns_usernames; +pub mod error; +pub mod mock; +pub mod transition; +pub mod types; + +pub use error::Error; diff --git a/packages/dash-platform-queries/src/mock.rs b/packages/dash-platform-queries/src/mock.rs new file mode 100644 index 00000000000..115e8445d38 --- /dev/null +++ b/packages/dash-platform-queries/src/mock.rs @@ -0,0 +1,7 @@ +//! Mocking support. +//! +//! The `dash_platform_macros::Mockable` derive expands to an impl of +//! `crate::mock::Mockable`, so every crate that derives it must expose the +//! trait at this path. The trait itself lives in `dapi-grpc` and is defined +//! even when mocks are disabled — serialization then just returns `None`. +pub use dapi_grpc::mock::Mockable; diff --git a/packages/dash-platform-queries/src/transition/mod.rs b/packages/dash-platform-queries/src/transition/mod.rs new file mode 100644 index 00000000000..3a0f1376adb --- /dev/null +++ b/packages/dash-platform-queries/src/transition/mod.rs @@ -0,0 +1,2 @@ +//! Transport-free state transition helpers. +pub mod validation; diff --git a/packages/dash-platform-queries/src/transition/validation.rs b/packages/dash-platform-queries/src/transition/validation.rs new file mode 100644 index 00000000000..164a98befeb --- /dev/null +++ b/packages/dash-platform-queries/src/transition/validation.rs @@ -0,0 +1,42 @@ +use crate::Error; +use dpp::{ + consensus::{basic::BasicError, ConsensusError}, + state_transition::{StateTransition, StateTransitionStructureValidation}, + version::PlatformVersion, +}; + +/// Checks if an error is an UnsupportedFeatureError +fn is_unsupported_feature_error(error: &ConsensusError) -> bool { + matches!( + error, + ConsensusError::BasicError(BasicError::UnsupportedFeatureError(_)) + ) +} + +/// Ensures a state transition passes structure validation before broadcasting. +/// +/// Note: UnsupportedFeatureError is allowed to pass through, as it indicates +/// that structure validation is not implemented for that state transition type +/// (e.g., identity-based state transitions). The platform will still perform +/// validation during execution. +pub fn ensure_valid_state_transition_structure( + state_transition: &StateTransition, + platform_version: &PlatformVersion, +) -> Result<(), Error> { + let validation_result = state_transition.validate_structure(platform_version); + if validation_result.is_valid() { + Ok(()) + } else { + // Allow UnsupportedFeatureError to pass through - this means structure + // validation is not implemented for this state transition type + let all_unsupported_feature_errors = validation_result + .errors + .iter() + .all(is_unsupported_feature_error); + if all_unsupported_feature_errors { + Ok(()) + } else { + Err(validation_result.into()) + } + } +} diff --git a/packages/dash-platform-queries/src/types/finalized_epoch.rs b/packages/dash-platform-queries/src/types/finalized_epoch.rs new file mode 100644 index 00000000000..af1c273d320 --- /dev/null +++ b/packages/dash-platform-queries/src/types/finalized_epoch.rs @@ -0,0 +1,37 @@ +//! Finalized epoch related types and helpers +use dpp::block::epoch::EpochIndex; + +/// Query used to fetch multiple finalized epochs from Platform. +#[derive(Clone, Debug)] +pub struct FinalizedEpochQuery { + /// Starting epoch index. + pub start_epoch_index: EpochIndex, + /// Whether to include the start epoch. + pub start_epoch_index_included: bool, + /// Ending epoch index. + pub end_epoch_index: EpochIndex, + /// Whether to include the end epoch. + pub end_epoch_index_included: bool, +} + +impl Default for FinalizedEpochQuery { + fn default() -> Self { + Self { + start_epoch_index: 0, + start_epoch_index_included: true, + end_epoch_index: 0, + end_epoch_index_included: true, + } + } +} + +impl From<(EpochIndex, EpochIndex)> for FinalizedEpochQuery { + fn from((start, end): (EpochIndex, EpochIndex)) -> Self { + Self { + start_epoch_index: start, + start_epoch_index_included: true, + end_epoch_index: end, + end_epoch_index_included: true, + } + } +} diff --git a/packages/dash-platform-queries/src/types/mod.rs b/packages/dash-platform-queries/src/types/mod.rs new file mode 100644 index 00000000000..fa3157966fc --- /dev/null +++ b/packages/dash-platform-queries/src/types/mod.rs @@ -0,0 +1,2 @@ +//! Transport-free query types for various dpp objects. +pub mod finalized_epoch; diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 8f8ba61d9df..651a7b6a11f 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -25,6 +25,7 @@ grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a dash-async = { path = "../rs-dash-async" } dash-context-provider = { path = "../rs-context-provider", default-features = false } dash-platform-macros = { path = "../rs-dash-platform-macros" } +dash-platform-queries = { path = "../dash-platform-queries" } platform-encryption = { path = "../rs-platform-encryption" } http = { version = "1.1" } ciborium = { version = "0.2.2" } @@ -89,6 +90,7 @@ spv-client = [ mocks = [ "dep:serde", "dep:serde_json", + "dash-platform-queries/mocks", "rs-dapi-client/mocks", "rs-dapi-client/dump", "dpp/document-cbor-conversion", diff --git a/packages/rs-sdk/README.md b/packages/rs-sdk/README.md index 9a33a75b7b2..63c60515dc9 100644 --- a/packages/rs-sdk/README.md +++ b/packages/rs-sdk/README.md @@ -42,6 +42,18 @@ connection to Platform. You can see examples of mocking in [mock_fetch.rs](tests/fetch/mock_fetch.rs) and [mock_fetch_many.rs](tests/fetch/mock_fetch_many.rs). +## Transport-free consumption + +The query-building, wire-encoding, and proof-verification layers of this SDK +live in the [`dash-platform-queries`](../dash-platform-queries) crate, which +this crate depends on and re-exports at the historical paths. Embedders that +bring their own transport and trust context (Dash Core's platform GUI, block +explorers) can depend on `dash-platform-queries` + `drive-proof-verifier` +directly and get typed, proof-verified results without `rs-dapi-client` or +tonic's native channel/TLS stack in their dependency tree. Shared generated +types and context-provider utilities remain dependencies. See that crate's +README for details. + ## Examples You can find quick start example in `examples/` folder. Examples must be configured by setting constants. diff --git a/packages/rs-sdk/src/error.rs b/packages/rs-sdk/src/error.rs index 4e4ab3a9a18..cc8309ebcd4 100644 --- a/packages/rs-sdk/src/error.rs +++ b/packages/rs-sdk/src/error.rs @@ -133,6 +133,16 @@ pub enum Error { NoAvailableAddressesToRetry(Box), } +impl From for Error { + fn from(value: dash_platform_queries::Error) -> Self { + match value { + dash_platform_queries::Error::Config(msg) => Self::Config(msg), + dash_platform_queries::Error::Drive(e) => Self::Drive(e), + dash_platform_queries::Error::Protocol(e) => Self::Protocol(e), + } + } +} + /// State transition broadcast error #[derive(Debug, thiserror::Error)] #[error("state transition broadcast error: {message}")] diff --git a/packages/rs-sdk/src/lib.rs b/packages/rs-sdk/src/lib.rs index cb92f01d8d0..0299351a8dd 100644 --- a/packages/rs-sdk/src/lib.rs +++ b/packages/rs-sdk/src/lib.rs @@ -90,6 +90,7 @@ pub use error::Error; pub use sdk::{RequestSettings, Sdk, SdkBuilder}; pub use dapi_grpc; +pub use dash_platform_queries; pub use dpp; #[cfg(feature = "core_spv")] pub use dpp::dash_spv; diff --git a/packages/rs-sdk/src/platform.rs b/packages/rs-sdk/src/platform.rs index 9e2cbdf89af..d6a3213036e 100644 --- a/packages/rs-sdk/src/platform.rs +++ b/packages/rs-sdk/src/platform.rs @@ -32,6 +32,9 @@ pub use dash_context_provider::ContextProvider; pub use dash_context_provider::MockContextProvider; pub use documents::document_history_query::DocumentHistoryQuery; pub use documents::document_query::DocumentQuery; +/// Sdk-bound constructors for [`DocumentQuery`]. Must be in scope to call +/// [`DocumentQuery::new_with_data_contract_id`], which fetches the contract. +pub use documents::DocumentQuerySdk; pub use dpp::{ self as dpp, document::Document, @@ -45,7 +48,7 @@ pub use { fetch_unproved::FetchUnproved, query::{ IdentityKeysQuery, LimitQuery, ProposerBlockCountByIdsQuery, Query, QueryStartInfo, - RecentAddressBalanceChangesQuery, RecentCompactedAddressBalanceChangesQuery, + RecentAddressBalanceChangesQuery, RecentCompactedAddressBalanceChangesQuery, WireQuery, DEFAULT_EPOCH_QUERY_LIMIT, }, query_settings::QuerySettings, diff --git a/packages/rs-sdk/src/platform/block_info_from_metadata.rs b/packages/rs-sdk/src/platform/block_info_from_metadata.rs index bfa2cea5b28..c341fe5ed17 100644 --- a/packages/rs-sdk/src/platform/block_info_from_metadata.rs +++ b/packages/rs-sdk/src/platform/block_info_from_metadata.rs @@ -1,47 +1,30 @@ -use crate::Error; +//! SDK-facing wrapper over the transport-free block-info helper. +//! +//! The implementation lives in +//! [`dash_platform_queries::block_info_from_metadata`] so embedders that skip +//! `dash-sdk` can use it. This module keeps the historical `dash-sdk` +//! signature — `Result` — because a `From` +//! conversion on the error type does not preserve explicit return types, +//! direct variant matching, or function-pointer signatures for existing +//! callers. + +use crate::error::Error; use dapi_grpc::platform::v0::ResponseMetadata; use dpp::block::block_info::BlockInfo; -use dpp::block::epoch::MAX_EPOCH; -use drive::error::proof::ProofError; -/// Constructs a `BlockInfo` structure from the provided response metadata. This function -/// translates metadata received from a platform response into a format that is specific to the -/// application's needs, particularly focusing on block-related information. It ensures that -/// the epoch value from the metadata does not exceed `MAX_EPOCH`, -/// as this is a constraint for the `Epoch` type used in the `BlockInfo` structure. -/// -/// # Parameters -/// - `response_metadata`: A reference to `ResponseMetadata` obtained from a platform response. -/// This metadata includes various block-related information such as time in milliseconds, -/// height, core chain locked height, and epoch. +/// Constructs a [`BlockInfo`] from the provided response metadata. /// -/// # Returns -/// If successful, returns `Ok(BlockInfo)` where `BlockInfo` contains: -/// - `time_ms`: The timestamp of the block in milliseconds. -/// - `height`: The height of the block. -/// - `core_height`: The core chain locked height, indicating the height of the block in the core blockchain that is considered final and securely linked to this block. -/// - `epoch`: The epoch number, converted to an `Epoch` struct via a 16-bit number. +/// Thin forwarder over +/// [`dash_platform_queries::block_info_from_metadata::block_info_from_metadata`]; +/// see there for the full contract. The only difference is the error type, +/// which stays [`crate::Error`] for source compatibility. /// /// # Errors -/// Returns an error if: -/// - The `epoch` value in the response metadata exceeds `MAX_EPOCH`. This is considered a data validity error as it indicates Platform returned an unexpectedly high epoch number. /// -/// The function encapsulates errors into the application's own `Error` type, providing a unified interface for error handling across the application. +/// Returns an error if the metadata's `epoch` exceeds +/// [`MAX_EPOCH`](dpp::block::epoch::MAX_EPOCH), which means Platform returned +/// an unexpectedly high epoch number. pub fn block_info_from_metadata(response_metadata: &ResponseMetadata) -> Result { - if response_metadata.epoch > MAX_EPOCH as u32 { - return Err( - drive::error::Error::Proof(ProofError::InvalidMetadata(format!( - "platform returned an epoch {} that was higher than the maximum allowed epoch", - response_metadata.epoch - ))) - .into(), - ); - } - - Ok(BlockInfo { - time_ms: response_metadata.time_ms, - height: response_metadata.height, - core_height: response_metadata.core_chain_locked_height, - epoch: (response_metadata.epoch as u16).try_into()?, - }) + dash_platform_queries::block_info_from_metadata::block_info_from_metadata(response_metadata) + .map_err(Error::from) } diff --git a/packages/rs-sdk/src/platform/delegate.rs b/packages/rs-sdk/src/platform/delegate.rs index f58ecb03652..fddf6f5f62d 100644 --- a/packages/rs-sdk/src/platform/delegate.rs +++ b/packages/rs-sdk/src/platform/delegate.rs @@ -26,6 +26,8 @@ #[macro_export] macro_rules! delegate_transport_request_variant { ($request:ty, $response:ty, $($variant:ident),+) => { + impl $crate::platform::query::WireQuery for $request {} + impl $crate::platform::dapi::transport::TransportRequest for $request { type Client = $crate::platform::dapi::transport::PlatformGrpcClient; diff --git a/packages/rs-sdk/src/platform/documents/document_query_sdk.rs b/packages/rs-sdk/src/platform/documents/document_query_sdk.rs new file mode 100644 index 00000000000..b51b1dd7c2f --- /dev/null +++ b/packages/rs-sdk/src/platform/documents/document_query_sdk.rs @@ -0,0 +1,69 @@ +//! Sdk-bound surface of [`DocumentQuery`]. +//! +//! [`DocumentQuery`] itself is transport-free and lives in +//! `dash-platform-queries`; this module holds the pieces that need an +//! [`Sdk`]: the contract-fetching constructor and the rich→wire +//! [`Query`](crate::platform::Query) encoding step. + +use crate::platform::documents::document_query::DocumentQuery; +use crate::platform::Fetch; +use crate::{error::Error, sdk::Sdk}; +use dapi_grpc::platform::v0 as platform_proto; +use dapi_grpc::platform::v0::GetDocumentsRequest; +use dpp::prelude::{DataContract, Identifier}; +use dpp::version::TryFromPlatformVersioned; + +/// Sdk-bound extension methods for [`DocumentQuery`]. +/// +/// Kept as an extension trait because [`DocumentQuery`] is defined in the +/// transport-free `dash-platform-queries` crate, so its Sdk-dependent +/// constructor cannot be an inherent method there. Bring this trait into +/// scope to keep calling `DocumentQuery::new_with_data_contract_id(...)`. +#[allow(async_fn_in_trait)] +pub trait DocumentQuerySdk: Sized { + /// Create new document query for provided document type name and data contract ID. + /// + /// Note that this method will fetch data contract first. + async fn new_with_data_contract_id( + api: &Sdk, + data_contract_id: Identifier, + document_type_name: &str, + ) -> Result; +} + +impl DocumentQuerySdk for DocumentQuery { + async fn new_with_data_contract_id( + api: &Sdk, + data_contract_id: Identifier, + document_type_name: &str, + ) -> Result { + let data_contract = + DataContract::fetch(api, data_contract_id) + .await? + .ok_or(Error::MissingDependency( + "DataContract".to_string(), + format!("data contract {} not found", data_contract_id), + ))?; + + Self::new(data_contract, document_type_name).map_err(Error::from) + } +} + +/// Encode a [`DocumentQuery`] onto the wire using the SDK's +/// currently-known [`dpp::version::PlatformVersion`] for V0 vs V1 dispatch. +/// +/// The [`Fetch`] / [`FetchMany`](crate::platform::FetchMany) trampolines for +/// [`dpp::document::Document`] (and the document aggregate views) split +/// `Fetch::Query = DocumentQuery` (rich, what `FromProof` binds to) from +/// `Fetch::Request = GetDocumentsRequest` (wire); this impl is the +/// rich→wire step the trampoline invokes via +/// `Query::query(&rich, &sdk.query_settings())`. +impl crate::platform::Query for DocumentQuery { + fn query( + &self, + settings: &crate::platform::QuerySettings<'_>, + ) -> Result { + GetDocumentsRequest::try_from_platform_versioned(self.clone(), settings.protocol_version) + .map_err(Error::from) + } +} diff --git a/packages/rs-sdk/src/platform/documents/fetch_bindings.rs b/packages/rs-sdk/src/platform/documents/fetch_bindings.rs new file mode 100644 index 00000000000..5d0acd3a698 --- /dev/null +++ b/packages/rs-sdk/src/platform/documents/fetch_bindings.rs @@ -0,0 +1,53 @@ +//! [`Fetch`] bindings for the document aggregate views. +//! +//! The `FromProof` decoding for these types moved to the transport-free +//! `dash-platform-queries` crate together with [`DocumentQuery`]; the +//! [`Fetch`] trait is Sdk-bound, so its impls stay here. + +use crate::platform::documents::document_query::DocumentQuery; +use crate::platform::Fetch; +use dapi_grpc::platform::v0::GetDocumentsRequest; +use drive_proof_verifier::{ + DocumentAverage, DocumentCount, DocumentHavingEntries, DocumentRankedEntries, + DocumentSplitAverages, DocumentSplitCounts, DocumentSplitSums, DocumentSum, +}; + +impl Fetch for DocumentCount { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentSum { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentAverage { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentSplitCounts { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentSplitSums { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentSplitAverages { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentRankedEntries { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} + +impl Fetch for DocumentHavingEntries { + type Query = DocumentQuery; + type Request = GetDocumentsRequest; +} diff --git a/packages/rs-sdk/src/platform/documents/mod.rs b/packages/rs-sdk/src/platform/documents/mod.rs index 5f7531c7d51..67acd8df70a 100644 --- a/packages/rs-sdk/src/platform/documents/mod.rs +++ b/packages/rs-sdk/src/platform/documents/mod.rs @@ -1,34 +1,18 @@ -pub(super) mod average_proof_helpers; -pub(super) mod count_proof_helpers; -/// `Fetch` impl for the average-side aggregate result. Returns -/// `(count, sum)`; client divides. -pub mod document_average; -pub mod document_count; -/// `Fetch` impl for the having-range (`GROUP BY … HAVING -/// LIMIT n`) result — one entry per matching group, in -/// axis order, with proof-attested completeness. Requires an index -/// declaring `rankedCountable` / `rankedSummable` / `rankedAverageable` -/// (protocol version 14+). -pub mod document_having_entries; -pub mod document_history_query; -pub mod document_query; -/// `Fetch` impl for the ranked (`GROUP BY … ORDER BY LIMIT n -/// [OFFSET m]`) result — one entry per returned group, in ranking order, -/// plus the rank the page starts at. Requires an index declaring -/// `rankedCountable` / `rankedSummable` / `rankedAverageable` -/// (protocol version 14+). -pub mod document_ranked_entries; -/// `Fetch` impl for the average-side per-entry result. Mirrors -/// `document_split_sums`. -pub mod document_split_averages; -pub mod document_split_counts; -/// `Fetch` impl for the sum-side per-entry result. Mirrors -/// `document_split_counts`. -pub mod document_split_sums; -/// `Fetch` impl for the sum-side aggregate result. Mirrors -/// `document_count`. Lights up alongside grovedb PR 670. -pub mod document_sum; -pub(super) mod having_proof_helpers; -pub(super) mod ranked_proof_helpers; -pub(super) mod sum_proof_helpers; +//! Document query surface. +//! +//! The transport-free core (query types, wire encoding, proof decoding) +//! lives in the `dash-platform-queries` crate and is re-exported here at +//! its historical paths; this module keeps the Sdk-bound pieces — `Fetch` +//! bindings, the contract-fetching constructor, and transition builders. + +pub use dash_platform_queries::documents::{ + document_average, document_count, document_having_entries, document_history_query, + document_query, document_ranked_entries, document_split_averages, document_split_counts, + document_split_sums, document_sum, +}; + +pub mod document_query_sdk; +mod fetch_bindings; pub mod transitions; + +pub use document_query_sdk::DocumentQuerySdk; diff --git a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs index 4d6ba1f660f..6de3e2950d1 100644 --- a/packages/rs-sdk/src/platform/dpns_usernames/mod.rs +++ b/packages/rs-sdk/src/platform/dpns_usernames/mod.rs @@ -2,6 +2,9 @@ mod contested_queries; mod queries; pub use contested_queries::ContestedDpnsUsername; +pub use dash_platform_queries::dpns_usernames::{ + convert_to_homograph_safe_chars, is_contested_username, is_valid_username, +}; pub use queries::DpnsUsername; use crate::platform::transition::put_document::PutDocument; @@ -21,20 +24,6 @@ use dpp::prelude::Identifier; use std::collections::BTreeMap; use std::sync::Arc; -/// Convert a string to homograph-safe characters by replacing 'o', 'i', and 'l' -/// with '0', '1', and '1' respectively to prevent homograph attacks -pub fn convert_to_homograph_safe_chars(input: &str) -> String { - input - .chars() - .map(|c| match c { - 'o' | 'O' => '0', - 'i' | 'I' => '1', - 'l' | 'L' => '1', - _ => c.to_ascii_lowercase(), - }) - .collect() -} - fn extract_dpns_label(name: &str) -> &str { if let Some(dot_pos) = name.rfind('.') { let (label_part, suffix) = name.split_at(dot_pos); @@ -56,85 +45,6 @@ fn normalize_dpns_label(input: &str) -> String { convert_to_homograph_safe_chars(extract_dpns_label(input)) } -/// Check if a username is valid according to DPNS rules -/// -/// A username is valid if: -/// - It's between 3 and 63 characters long -/// - It starts and ends with alphanumeric characters (a-zA-Z0-9) -/// - It contains only alphanumeric characters and hyphens -/// - It doesn't have consecutive hyphens (enforced by the pattern) -/// -/// Pattern: `^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$` -/// -/// # Arguments -/// -/// * `label` - The username label to check (e.g., "alice") -/// -/// # Returns -/// -/// Returns `true` if the username is valid, `false` otherwise -pub fn is_valid_username(label: &str) -> bool { - // Check length - if label.len() < 3 || label.len() > 63 { - return false; - } - - let chars: Vec = label.chars().collect(); - - // Check first character (must be alphanumeric) - if !chars[0].is_ascii_alphanumeric() { - return false; - } - - // Check last character (must be alphanumeric) - if !chars[chars.len() - 1].is_ascii_alphanumeric() { - return false; - } - - // Check middle characters (can be alphanumeric or hyphen) - for &ch in &chars[1..chars.len() - 1] { - if !ch.is_ascii_alphanumeric() && ch != '-' { - return false; - } - } - - // Additional check: no consecutive hyphens (good practice) - for i in 0..chars.len() - 1 { - if chars[i] == '-' && chars[i + 1] == '-' { - return false; - } - } - - true -} - -/// Check if a username is contested (requires masternode voting) -/// -/// A username is contested if its normalized label: -/// - Is between 3 and 19 characters long (inclusive) -/// - Contains only lowercase letters a-z, digits 0-1, and hyphens -/// -/// # Arguments -/// -/// * `label` - The username label to check (e.g., "alice") -/// -/// # Returns -/// -/// Returns `true` if the username would be contested, `false` otherwise -pub fn is_contested_username(label: &str) -> bool { - let normalized = convert_to_homograph_safe_chars(label); - - // Check length - if normalized.len() < 3 || normalized.len() > 19 { - return false; - } - - // Check if all characters match the pattern [a-z01-] - normalized - .chars() - .all(|c| matches!(c, 'a'..='z' | '0' | '1' | '-')) -} - /// Hash a buffer twice using SHA256 (double SHA256) fn hash_double(data: Vec) -> [u8; 32] { use dpp::dashcore::hashes::{sha256d, Hash}; @@ -521,14 +431,6 @@ impl Sdk { mod tests { use super::*; - #[test] - fn test_convert_to_homograph_safe_chars() { - assert_eq!(convert_to_homograph_safe_chars("alice"), "a11ce"); - assert_eq!(convert_to_homograph_safe_chars("bob"), "b0b"); - assert_eq!(convert_to_homograph_safe_chars("COOL"), "c001"); - assert_eq!(convert_to_homograph_safe_chars("test123"), "test123"); - } - #[test] fn test_normalize_dpns_label_strips_dash_suffix_case_insensitively() { // Bare label and full name normalize to the same value, regardless @@ -562,89 +464,4 @@ mod tests { assert_eq!(extract_dpns_label("alice.eth"), "alice.eth"); assert_eq!(extract_dpns_label(".dash"), ""); } - - #[test] - fn test_is_valid_username() { - // Valid usernames - assert!(is_valid_username("abc")); - assert!(is_valid_username("alice")); - assert!(is_valid_username("Alice123")); - assert!(is_valid_username("dash-p2p")); - assert!(is_valid_username("test-name-123")); - assert!(is_valid_username("a-b-c")); - assert!(is_valid_username("user2024")); - assert!(is_valid_username("CryptoKing")); - assert!(is_valid_username("web3-developer")); - assert!(is_valid_username("a".repeat(63).as_str())); // Max length - - // Invalid - too short - assert!(!is_valid_username("ab")); - assert!(!is_valid_username("a")); - assert!(!is_valid_username("")); - - // Invalid - too long - assert!(!is_valid_username("a".repeat(64).as_str())); - - // Invalid - starts with hyphen - assert!(!is_valid_username("-alice")); - assert!(!is_valid_username("-test")); - - // Invalid - ends with hyphen - assert!(!is_valid_username("alice-")); - assert!(!is_valid_username("test-")); - - // Invalid - starts and ends with hyphen - assert!(!is_valid_username("-alice-")); - - // Invalid - contains invalid characters - assert!(!is_valid_username("alice_bob")); // underscore - assert!(!is_valid_username("alice.bob")); // dot - assert!(!is_valid_username("alice@dash")); // at sign - assert!(!is_valid_username("alice!")); // exclamation - assert!(!is_valid_username("alice bob")); // space - assert!(!is_valid_username("alice#1")); // hash - assert!(!is_valid_username("alice$")); // dollar - assert!(!is_valid_username("alice%20")); // percent - - // Invalid - consecutive hyphens - assert!(!is_valid_username("alice--bob")); - assert!(!is_valid_username("test---name")); - } - - #[test] - fn test_is_contested_username() { - // Contested usernames (3-19 chars, only [a-z01-]) - assert!(is_contested_username("abc")); - assert!(is_contested_username("alice")); // becomes "a11ce" - assert!(is_contested_username("b0b")); - assert!(is_contested_username("cool")); // becomes "c001" - assert!(is_contested_username("a-b-c")); - assert!(is_contested_username("hello")); // becomes "he110" - assert!(is_contested_username("world")); // becomes "w0r1d" - assert!(is_contested_username("dash")); - assert!(is_contested_username("a11ce")); // already normalized - assert!(is_contested_username("dash-dao")); // becomes "dash-da0" - - // Not contested - too short - assert!(!is_contested_username("ab")); - assert!(!is_contested_username("io")); // becomes "10" which is 2 chars - assert!(!is_contested_username("a")); - - // Not contested - too long (20+ chars) - assert!(!is_contested_username("twenty-characters-ab")); // 20 chars - assert!(!is_contested_username( - "this-is-a-very-long-username-that-exceeds-limit" - )); - - // Not contested - contains invalid characters after normalization - assert!(!is_contested_username("alice2")); // contains '2' - assert!(!is_contested_username("alice_bob")); // contains '_' - assert!(!is_contested_username("alice.bob")); // contains '.' - assert!(!is_contested_username("alice@dash")); // contains '@' - assert!(!is_contested_username("alice!")); // contains '!' - assert!(!is_contested_username("test123")); // contains '2' and '3' - assert!(!is_contested_username("dash-p2p")); // contains 'p' and '2' - assert!(!is_contested_username("user5")); // contains '5' - assert!(!is_contested_username("name_with_underscore")); // contains '_' - } } diff --git a/packages/rs-sdk/src/platform/identities_contract_keys_query.rs b/packages/rs-sdk/src/platform/identities_contract_keys_query.rs index 02ede03136f..e939e5b2d82 100644 --- a/packages/rs-sdk/src/platform/identities_contract_keys_query.rs +++ b/packages/rs-sdk/src/platform/identities_contract_keys_query.rs @@ -88,6 +88,8 @@ impl Query for IdentitiesContractKeysQuery { } } +impl crate::platform::query::WireQuery for IdentitiesContractKeysQuery {} + impl TransportRequest for IdentitiesContractKeysQuery { type Client = ::Client; type Response = ::Response; diff --git a/packages/rs-sdk/src/platform/query.rs b/packages/rs-sdk/src/platform/query.rs index fb52158c03d..52d7119cf54 100644 --- a/packages/rs-sdk/src/platform/query.rs +++ b/packages/rs-sdk/src/platform/query.rs @@ -99,8 +99,8 @@ pub trait Query: Send + Debug + Clone { /// /// * `settings` - A [`QuerySettings`](crate::platform::QuerySettings) borrowing the encoder /// inputs from the SDK: protocol version (used by encoders that pick wire shapes - /// per version — today only [`DocumentQuery`]'s V0/V1 split), `prove` flag, - /// and request settings. Construct from an SDK via + /// per version — today only [`DocumentQuery`]'s V0/V1 split) and the `prove` flag. + /// Construct from an SDK via /// [`Sdk::query_settings`](crate::Sdk::query_settings), or directly in unit tests /// that want to exercise the encoder without spinning up an `Sdk`. /// @@ -110,9 +110,107 @@ pub trait Query: Send + Debug + Clone { fn query(&self, settings: &crate::platform::QuerySettings<'_>) -> Result; } +/// Marker for wire proto request types that serve as their own [`Query`] +/// through the blanket identity impl below. +/// +/// This local marker exists for trait coherence: [`DocumentQuery`] moved to +/// the transport-free `dash-platform-queries` crate, so it is now foreign to +/// this crate. A blanket bounded only by the (equally foreign) +/// [`TransportRequest`] trait would conflict with the explicit +/// `impl Query for DocumentQuery` — rustc must assume some +/// future upstream crate could implement `TransportRequest` for +/// `DocumentQuery`. Because `WireQuery` is local and only ever implemented +/// explicitly (never via a blanket), the compiler can prove the two impl +/// sets disjoint. +/// +/// When adding a new endpoint whose request proto is used directly as its +/// own query (`Fetch::Query = Fetch::Request`), add the proto to the +/// `impl_wire_query!` list below; a missing entry fails to compile at the +/// fetch call site with a `WireQuery is not satisfied` error. +/// +/// # Breaking change for downstream crates +/// +/// Before this bound existed, *every* [`TransportRequest`] got `Query for +/// T` automatically. A downstream crate with its own `TransportRequest` type +/// must now opt in explicitly — one line, no members: +/// +/// ```rust,ignore +/// impl dash_sdk::platform::WireQuery for MyCustomRequest {} +/// ``` +pub trait WireQuery {} + +macro_rules! impl_wire_query { + ($($request:ty),+ $(,)?) => { + $(impl WireQuery for $request {})+ + }; +} + +impl_wire_query!( + proto::BroadcastStateTransitionRequest, + proto::GetAddressInfoRequest, + proto::GetAddressesBranchStateRequest, + proto::GetAddressesInfosRequest, + proto::GetAddressesTrunkStateRequest, + proto::GetConsensusParamsRequest, + proto::GetContestedResourceIdentityVotesRequest, + proto::GetContestedResourceVoteStateRequest, + proto::GetContestedResourceVotersForIdentityRequest, + proto::GetContestedResourcesRequest, + proto::GetCurrentQuorumsInfoRequest, + proto::GetDataContractHistoryRequest, + proto::GetDataContractRequest, + proto::GetDataContractsRequest, + proto::GetDocumentHistoryRequest, + proto::GetDocumentsRequest, + proto::GetEpochsInfoRequest, + proto::GetEvonodesProposedEpochBlocksByIdsRequest, + proto::GetEvonodesProposedEpochBlocksByRangeRequest, + proto::GetFinalizedEpochInfosRequest, + proto::GetGroupActionSignersRequest, + proto::GetGroupActionsRequest, + proto::GetGroupInfoRequest, + proto::GetGroupInfosRequest, + proto::GetIdentitiesBalancesRequest, + proto::GetIdentitiesContractKeysRequest, + proto::GetIdentitiesTokenBalancesRequest, + proto::GetIdentitiesTokenInfosRequest, + proto::GetIdentityBalanceAndRevisionRequest, + proto::GetIdentityBalanceRequest, + proto::GetIdentityByNonUniquePublicKeyHashRequest, + proto::GetIdentityByPublicKeyHashRequest, + proto::GetIdentityContractNonceRequest, + proto::GetIdentityKeysRequest, + proto::GetIdentityNonceRequest, + proto::GetIdentityRequest, + proto::GetIdentityTokenBalancesRequest, + proto::GetIdentityTokenInfosRequest, + proto::GetMostRecentShieldedAnchorRequest, + proto::GetPathElementsRequest, + proto::GetPrefundedSpecializedBalanceRequest, + proto::GetProtocolVersionUpgradeStateRequest, + proto::GetProtocolVersionUpgradeVoteStatusRequest, + proto::GetRecentAddressBalanceChangesRequest, + proto::GetRecentCompactedAddressBalanceChangesRequest, + proto::GetShieldedAnchorsRequest, + proto::GetShieldedEncryptedNotesRequest, + proto::GetShieldedNotesCountRequest, + proto::GetShieldedNullifiersRequest, + proto::GetShieldedPoolStateRequest, + proto::GetStatusRequest, + proto::GetTokenContractInfoRequest, + proto::GetTokenDirectPurchasePricesRequest, + proto::GetTokenPerpetualDistributionLastClaimRequest, + proto::GetTokenPreProgrammedDistributionsRequest, + proto::GetTokenStatusesRequest, + proto::GetTokenTotalSupplyRequest, + proto::GetTotalCreditsInPlatformRequest, + proto::GetVotePollsByEndDateRequest, + proto::WaitForStateTransitionResultRequest, +); + impl Query for T where - T: TransportRequest + Sized + Send + Sync + Clone + Debug, + T: TransportRequest + WireQuery + Sized + Send + Sync + Clone + Debug, T::Response: Send + Sync + Debug, { fn query(&self, settings: &crate::platform::QuerySettings<'_>) -> Result { diff --git a/packages/rs-sdk/src/platform/transition/validation.rs b/packages/rs-sdk/src/platform/transition/validation.rs index 846d9ddae2d..d095afb0bb5 100644 --- a/packages/rs-sdk/src/platform/transition/validation.rs +++ b/packages/rs-sdk/src/platform/transition/validation.rs @@ -1,42 +1,7 @@ -use crate::Error; -use dpp::{ - consensus::{basic::BasicError, ConsensusError}, - state_transition::{StateTransition, StateTransitionStructureValidation}, - version::PlatformVersion, -}; - -/// Checks if an error is an UnsupportedFeatureError -fn is_unsupported_feature_error(error: &ConsensusError) -> bool { - matches!( - error, - ConsensusError::BasicError(BasicError::UnsupportedFeatureError(_)) - ) -} - -/// Ensures a state transition passes structure validation before broadcasting. -/// -/// Note: UnsupportedFeatureError is allowed to pass through, as it indicates -/// that structure validation is not implemented for that state transition type -/// (e.g., identity-based state transitions). The platform will still perform -/// validation during execution. -pub(crate) fn ensure_valid_state_transition_structure( - state_transition: &StateTransition, - platform_version: &PlatformVersion, -) -> Result<(), Error> { - let validation_result = state_transition.validate_structure(platform_version); - if validation_result.is_valid() { - Ok(()) - } else { - // Allow UnsupportedFeatureError to pass through - this means structure - // validation is not implemented for this state transition type - let all_unsupported_feature_errors = validation_result - .errors - .iter() - .all(is_unsupported_feature_error); - if all_unsupported_feature_errors { - Ok(()) - } else { - Err(validation_result.into()) - } - } -} +//! Re-export of the transport-free structure validation helper. +//! +//! The implementation moved to `dash-platform-queries`; broadcast paths in +//! this crate keep importing it from here. It returns the query core's +//! error type, which converts into [`crate::Error`] via `From` at the `?` +//! call sites. +pub(crate) use dash_platform_queries::transition::validation::ensure_valid_state_transition_structure; diff --git a/packages/rs-sdk/src/platform/types/evonode.rs b/packages/rs-sdk/src/platform/types/evonode.rs index 1ccc5553f4f..4bba5ca8e10 100644 --- a/packages/rs-sdk/src/platform/types/evonode.rs +++ b/packages/rs-sdk/src/platform/types/evonode.rs @@ -51,6 +51,8 @@ impl Mockable for EvoNode { serde_json::ser::to_vec(self).ok() } } +impl crate::platform::query::WireQuery for EvoNode {} + impl TransportRequest for EvoNode { type Client = PlatformGrpcClient; type Response = proto::GetStatusResponse; diff --git a/packages/rs-sdk/src/platform/types/finalized_epoch.rs b/packages/rs-sdk/src/platform/types/finalized_epoch.rs index e9e25f8071d..134943ec707 100644 --- a/packages/rs-sdk/src/platform/types/finalized_epoch.rs +++ b/packages/rs-sdk/src/platform/types/finalized_epoch.rs @@ -4,40 +4,7 @@ use crate::Error; use dapi_grpc::platform::v0::{get_finalized_epoch_infos_request, GetFinalizedEpochInfosRequest}; use dpp::block::epoch::EpochIndex; -/// Query used to fetch multiple finalized epochs from Platform. -#[derive(Clone, Debug)] -pub struct FinalizedEpochQuery { - /// Starting epoch index. - pub start_epoch_index: EpochIndex, - /// Whether to include the start epoch. - pub start_epoch_index_included: bool, - /// Ending epoch index. - pub end_epoch_index: EpochIndex, - /// Whether to include the end epoch. - pub end_epoch_index_included: bool, -} - -impl Default for FinalizedEpochQuery { - fn default() -> Self { - Self { - start_epoch_index: 0, - start_epoch_index_included: true, - end_epoch_index: 0, - end_epoch_index_included: true, - } - } -} - -impl From<(EpochIndex, EpochIndex)> for FinalizedEpochQuery { - fn from((start, end): (EpochIndex, EpochIndex)) -> Self { - Self { - start_epoch_index: start, - start_epoch_index_included: true, - end_epoch_index: end, - end_epoch_index_included: true, - } - } -} +pub use dash_platform_queries::types::finalized_epoch::FinalizedEpochQuery; impl Query for FinalizedEpochQuery { fn query( diff --git a/packages/rs-sdk/src/sdk.rs b/packages/rs-sdk/src/sdk.rs index a9f76afbf5d..c1498e5506f 100644 --- a/packages/rs-sdk/src/sdk.rs +++ b/packages/rs-sdk/src/sdk.rs @@ -581,8 +581,8 @@ impl Sdk { self.proofs } - /// Build a [`QuerySettings`] borrowing this SDK's protocol version, - /// request settings, and `prove` flag. + /// Build a [`QuerySettings`] borrowing this SDK's protocol version + /// and `prove` flag. /// /// Hand the resulting context to [`crate::platform::Query::query`] when /// you need to encode a user-facing query into a wire `TransportRequest` diff --git a/packages/rs-sdk/tests/fetch/document.rs b/packages/rs-sdk/tests/fetch/document.rs index df3b3da576f..3edb4501325 100644 --- a/packages/rs-sdk/tests/fetch/document.rs +++ b/packages/rs-sdk/tests/fetch/document.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use super::{common::setup_logs, config::Config}; -use dash_sdk::platform::{DocumentQuery, Fetch, FetchMany}; +use dash_sdk::platform::{DocumentQuery, DocumentQuerySdk, Fetch, FetchMany}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::document::{Document, DocumentV0Getters}; use dpp::platform_value::string_encoding::Encoding; diff --git a/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs b/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs index 65fdb6994c0..a8c4480aeee 100644 --- a/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs +++ b/packages/rs-sdk/tests/fetch/document_query_v0_v1.rs @@ -140,9 +140,10 @@ fn v0_wire_shape_with_forced_v0_platform_version() { #[test] fn v0_rejects_count_star_projection() { let q = build_basic_document_query().with_select(SelectProjection::count_star()); - let err = q - .try_into_request_for_version(v0_dispatch_version()) - .expect_err("count_star on v0 must reject"); + let err = SdkError::from( + q.try_into_request_for_version(v0_dispatch_version()) + .expect_err("count_star on v0 must reject"), + ); match err { SdkError::Config(msg) => assert!( msg.contains("v3.1+"), @@ -155,9 +156,10 @@ fn v0_rejects_count_star_projection() { #[test] fn v0_rejects_group_by() { let q = build_basic_document_query().with_group_by("a"); - let err = q - .try_into_request_for_version(v0_dispatch_version()) - .expect_err("group_by on v0 must reject"); + let err = SdkError::from( + q.try_into_request_for_version(v0_dispatch_version()) + .expect_err("group_by on v0 must reject"), + ); assert!(matches!(err, SdkError::Config(_))); } @@ -174,9 +176,10 @@ fn v0_rejects_having() { operator: HavingOperator::GreaterThan, right: HavingRightOperand::Value(Value::U64(0)), }]); - let err = q - .try_into_request_for_version(v0_dispatch_version()) - .expect_err("having on v0 must reject"); + let err = SdkError::from( + q.try_into_request_for_version(v0_dispatch_version()) + .expect_err("having on v0 must reject"), + ); assert!(matches!(err, SdkError::Config(_))); } diff --git a/packages/rs-sdk/tests/fetch/mock_fetch.rs b/packages/rs-sdk/tests/fetch/mock_fetch.rs index a8c98b4d575..38d42e719ff 100644 --- a/packages/rs-sdk/tests/fetch/mock_fetch.rs +++ b/packages/rs-sdk/tests/fetch/mock_fetch.rs @@ -2,7 +2,7 @@ use super::common::{bootstrap_mock_sdk_to_latest, mock_data_contract, mock_document_type}; use dash_sdk::{ - platform::{DocumentQuery, Fetch}, + platform::{DocumentQuery, DocumentQuerySdk, Fetch}, Sdk, SdkBuilder, }; use dpp::{ diff --git a/packages/wasm-sdk/src/error.rs b/packages/wasm-sdk/src/error.rs index e37159fa6c8..bebc02a33d2 100644 --- a/packages/wasm-sdk/src/error.rs +++ b/packages/wasm-sdk/src/error.rs @@ -121,6 +121,15 @@ impl WasmSdkError { } } +impl From for WasmSdkError { + fn from(err: dash_sdk::dash_platform_queries::Error) -> Self { + // Route through the SDK's own conversion so the transport-free query + // core's errors keep the exact mapping they had when they were + // `SdkError` variants. + SdkError::from(err).into() + } +} + impl From for WasmSdkError { fn from(err: SdkError) -> Self { use SdkError::*;