diff --git a/packages/rs-sdk/src/platform.rs b/packages/rs-sdk/src/platform.rs index e5631646ea6..e25dd68515a 100644 --- a/packages/rs-sdk/src/platform.rs +++ b/packages/rs-sdk/src/platform.rs @@ -38,6 +38,7 @@ pub use { fetch_many::FetchMany, fetch_unproved::FetchUnproved, query::{ - LimitQuery, ProposerBlockCountByIdsQuery, Query, QueryStartInfo, DEFAULT_EPOCH_QUERY_LIMIT, + IdentityKeysQuery, LimitQuery, ProposerBlockCountByIdsQuery, Query, QueryStartInfo, + DEFAULT_EPOCH_QUERY_LIMIT, }, }; diff --git a/packages/rs-sdk/src/platform/query.rs b/packages/rs-sdk/src/platform/query.rs index 16e0eef3882..a27682398af 100644 --- a/packages/rs-sdk/src/platform/query.rs +++ b/packages/rs-sdk/src/platform/query.rs @@ -23,7 +23,7 @@ use dapi_grpc::platform::v0::{ GetCurrentQuorumsInfoRequest, GetEpochsInfoRequest, GetEvonodesProposedEpochBlocksByIdsRequest, GetEvonodesProposedEpochBlocksByRangeRequest, GetIdentityKeysRequest, GetPathElementsRequest, GetProtocolVersionUpgradeStateRequest, GetProtocolVersionUpgradeVoteStatusRequest, - GetTotalCreditsInPlatformRequest, KeyRequestType, + GetTotalCreditsInPlatformRequest, KeyRequestType, SpecificKeys, }; use dapi_grpc::platform::v0::{ get_status_request, GetContestedResourceIdentityVotesRequest, @@ -31,6 +31,7 @@ use dapi_grpc::platform::v0::{ GetTokenPerpetualDistributionLastClaimRequest, GetVotePollsByEndDateRequest, }; use dpp::dashcore_rpc::dashcore::{hashes::Hash, ProTxHash}; +use dpp::identity::KeyID; use dpp::version::PlatformVersionError; use dpp::{block::epoch::EpochIndex, prelude::Identifier}; use drive::query::contested_resource_votes_given_by_identity_query::ContestedResourceVotesGivenByIdentityQuery; @@ -183,6 +184,85 @@ impl Query for Identifier { } } +/// Query for specific identity keys by their IDs +#[derive(Debug, Clone)] +pub struct IdentityKeysQuery { + /// Identity ID to fetch keys from + pub identity_id: Identifier, + /// Specific key IDs to fetch + pub key_ids: Vec, + /// Optional limit for the number of keys to return + pub limit: Option, + /// Optional offset for pagination + pub offset: Option, +} + +impl IdentityKeysQuery { + /// Create a new query for specific identity keys + /// + /// # Arguments + /// + /// * `identity_id` - The identity to fetch keys from + /// * `key_ids` - The specific key IDs to fetch + /// + /// # Example + /// + /// ```rust + /// use dash_sdk::platform::{Identifier, IdentityKeysQuery}; + /// + /// let identity_id = Identifier::new([1; 32]); + /// let key_ids = vec![0, 1, 2]; // Fetch keys with IDs 0, 1, and 2 + /// let query = IdentityKeysQuery::new(identity_id, key_ids); + /// ``` + pub fn new(identity_id: Identifier, key_ids: Vec) -> Self { + Self { + identity_id, + key_ids, + limit: None, + offset: None, + } + } + + /// Set a limit on the number of keys to return + pub fn with_limit(mut self, limit: u32) -> Self { + self.limit = Some(limit); + self + } + + /// Set an offset for pagination + pub fn with_offset(mut self, offset: u32) -> Self { + self.offset = Some(offset); + self + } +} + +impl Query for IdentityKeysQuery { + /// Get specific keys for an identity. + fn query(self, prove: bool) -> Result { + if !prove { + unimplemented!("queries without proofs are not supported yet"); + } + + Ok(GetIdentityKeysRequest { + version: Some(get_identity_keys_request::Version::V0( + GetIdentityKeysRequestV0 { + identity_id: self.identity_id.to_vec(), + prove, + limit: self.limit, + offset: self.offset, + request_type: Some(KeyRequestType { + request: Some(proto::key_request_type::Request::SpecificKeys( + SpecificKeys { + key_ids: self.key_ids.into_iter().collect(), + }, + )), + }), + }, + )), + }) + } +} + impl Query for DriveDocumentQuery<'_> { fn query(self, prove: bool) -> Result { if !prove { diff --git a/packages/rs-sdk/tests/fetch/identity.rs b/packages/rs-sdk/tests/fetch/identity.rs index dd564623f4a..6fb8af58137 100644 --- a/packages/rs-sdk/tests/fetch/identity.rs +++ b/packages/rs-sdk/tests/fetch/identity.rs @@ -1,10 +1,11 @@ use dash_sdk::platform::types::identity::{NonUniquePublicKeyHashQuery, PublicKeyHash}; -use dash_sdk::platform::{Fetch, FetchMany}; +use dash_sdk::platform::{Fetch, FetchMany, IdentityKeysQuery}; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dpp::prelude::IdentityPublicKey; use dpp::{identity::hash::IdentityPublicKeyHashMethodsV0, prelude::Identity}; use drive_proof_verifier::types::{IdentityBalance, IdentityBalanceAndRevision}; +use std::collections::BTreeSet; use super::{common::setup_logs, config::Config}; @@ -117,6 +118,74 @@ async fn test_identity_public_keys_all_read() { } } +/// Given some existing identity ID and selected key IDs, when I fetch specific identity keys, I get only those keys. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_identity_public_keys_specific_read() { + setup_logs(); + + let cfg = Config::new(); + let identity_id: dpp::prelude::Identifier = cfg.existing_identity_id; + + let sdk = cfg + .setup_api("test_identity_public_keys_specific_read") + .await; + + let all_public_keys = IdentityPublicKey::fetch_many(&sdk, identity_id) + .await + .expect("fetch identity public keys"); + + assert!( + !all_public_keys.is_empty(), + "identity must expose at least one public key" + ); + + let requested_key_ids = vec![0, 2]; + + let query = IdentityKeysQuery::new(identity_id, requested_key_ids.clone()); + + let fetched_subset = IdentityPublicKey::fetch_many(&sdk, query) + .await + .expect("fetch selected identity public keys"); + + assert_eq!( + fetched_subset.len(), + requested_key_ids.len(), + "number of fetched keys should match the requested set" + ); + + let requested_key_set: BTreeSet = requested_key_ids.iter().copied().collect(); + + for key_id in &requested_key_ids { + let expected = all_public_keys + .get(key_id) + .and_then(|value| value.as_ref()) + .expect("expected key in base dataset"); + + let actual = fetched_subset + .get(key_id) + .and_then(|value| value.as_ref()) + .expect("expected key in fetched subset"); + + assert_eq!( + actual, expected, + "fetched key {} does not match the original key", + key_id + ); + } + + let unexpected: Vec = fetched_subset + .keys() + .filter(|key| !requested_key_set.contains(key)) + .copied() + .collect(); + + assert!( + unexpected.is_empty(), + "subset should not include unrequested keys: {:?}", + unexpected + ); +} + /// Given some non-unique public key, when I fetch identity that uses this key, I get associated identities containing this key. #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fetch_identity_by_non_unique_public_keys() { diff --git a/packages/rs-sdk/tests/vectors/test_identity_public_keys_specific_read/.gitkeep b/packages/rs-sdk/tests/vectors/test_identity_public_keys_specific_read/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/rs-sdk/tests/vectors/test_identity_public_keys_specific_read/msg_GetIdentityKeysRequest_a2eb219e18333048b42849f8f8f8378a0e4cc52357eb5e477c2f0e347cdbf48a.json b/packages/rs-sdk/tests/vectors/test_identity_public_keys_specific_read/msg_GetIdentityKeysRequest_a2eb219e18333048b42849f8f8f8378a0e4cc52357eb5e477c2f0e347cdbf48a.json new file mode 100644 index 00000000000..2a70e7f6bbc Binary files /dev/null and b/packages/rs-sdk/tests/vectors/test_identity_public_keys_specific_read/msg_GetIdentityKeysRequest_a2eb219e18333048b42849f8f8f8378a0e4cc52357eb5e477c2f0e347cdbf48a.json differ diff --git a/packages/rs-sdk/tests/vectors/test_identity_public_keys_specific_read/msg_GetIdentityKeysRequest_e72db3c9386781ed78db57de2ca48c04146560bb24a5224d9090b567e78d272e.json b/packages/rs-sdk/tests/vectors/test_identity_public_keys_specific_read/msg_GetIdentityKeysRequest_e72db3c9386781ed78db57de2ca48c04146560bb24a5224d9090b567e78d272e.json new file mode 100644 index 00000000000..f5b03df915c Binary files /dev/null and b/packages/rs-sdk/tests/vectors/test_identity_public_keys_specific_read/msg_GetIdentityKeysRequest_e72db3c9386781ed78db57de2ca48c04146560bb24a5224d9090b567e78d272e.json differ diff --git a/packages/rs-sdk/tests/vectors/test_identity_public_keys_specific_read/quorum_pubkey-106-21baa6993291f924461caabc476b24aa053e85963ae5c920082bc94e9c544f44.json b/packages/rs-sdk/tests/vectors/test_identity_public_keys_specific_read/quorum_pubkey-106-21baa6993291f924461caabc476b24aa053e85963ae5c920082bc94e9c544f44.json new file mode 100644 index 00000000000..204d710b295 --- /dev/null +++ b/packages/rs-sdk/tests/vectors/test_identity_public_keys_specific_read/quorum_pubkey-106-21baa6993291f924461caabc476b24aa053e85963ae5c920082bc94e9c544f44.json @@ -0,0 +1 @@ +8ea8f1a8f5a17e04776eaa4f5341466503e8cbf2edc6e3298d6441ea69e0c9b4ec1cccc2d2d81f261fd2260589654a53 \ No newline at end of file