Skip to main content

drive_proof_verifier/
unproved.rs

1use crate::types::contract_groups::{
2    contract_group_info_from_proto, members_page_from_response, members_query_from_request,
3    memberships_from_response, ContractGroupInfo, ContractGroupMembersPage,
4    ContractGroupMembershipsForContract,
5};
6use crate::types::contract_moderation::{
7    entries_from_response, fee_pots_from_response, list_from_request, lists_from_request,
8    moderation_action_counts_from_response, reason_from_response, removals_from_response,
9    removals_query_from_request, team_action_signers_from_response,
10    team_action_status_from_request, team_actions_from_response, team_actions_query_from_request,
11    warnings_from_response, ContractBan, ContractDocumentRemovals, ContractFeePots,
12    ContractModerationActionCounts, ContractModerationEntries, ContractModerationList,
13    ContractModerationListStatuses, ContractModerationStatus, ContractSuspension,
14    ContractTeamActionSigners, ContractTeamActions,
15};
16use crate::types::data_contracts_latest_versions::{
17    DataContractLatestVersion, DataContractsLatestVersions,
18};
19use crate::types::evonode_status::EvoNodeStatus;
20use crate::types::identity_keys_remaining_budgets::IdentityKeysRemainingBudgets;
21use crate::types::CurrentQuorumsInfo;
22use crate::Error;
23use dapi_grpc::platform::v0::ResponseMetadata;
24use dapi_grpc::platform::v0::{self as platform};
25use dapi_grpc::platform::v0::get_contract_moderation_action_counts_response::get_contract_moderation_action_counts_response_v0::Result as ModerationActionCountsResult;
26use dapi_grpc::tonic::async_trait;
27use dpp::bls_signatures::PublicKey as BlsPublicKey;
28use dpp::core_types::validator::v0::ValidatorV0;
29use dpp::core_types::validator_set::v0::ValidatorSetV0;
30use dpp::core_types::validator_set::ValidatorSet;
31use dpp::dashcore::hashes::Hash;
32use dpp::dashcore::{Network, ProTxHash, PubkeyHash, QuorumHash};
33use dpp::data_contract::DataContract;
34use dpp::identifier::Identifier;
35use dpp::serialization::PlatformDeserializableWithPotentialValidationFromVersionedStructureUntrusted;
36use dpp::version::PlatformVersion;
37use std::collections::BTreeMap;
38
39fn parse_hash_32(field: &str, bytes: &[u8]) -> Result<[u8; 32], Error> {
40    bytes.try_into().map_err(|_| Error::ProtocolError {
41        error: format!(
42            "Invalid {field} length: expected 32 bytes, received {}",
43            bytes.len()
44        ),
45    })
46}
47
48/// Trait for parsing unproved responses from the Platform.
49///
50/// This trait defines methods for extracting data from responses received from the Platform
51/// without the need for cryptographic proof validation. It is primarily used for scenarios where
52/// the proof data is not available or not required, and only the data itself is needed.
53///
54/// ## Associated Types
55///
56/// - `Request`: The type of the request sent to the server. This represents the format of the
57///   data that the platform expects when making a query.
58/// - `Response`: The type of the response received from the server. This represents the format of
59///   the data returned by the platform after executing the query.
60///
61/// ## Methods
62///
63/// - `maybe_from_unproved`: Parses the response to retrieve the requested object, if any.
64/// - `maybe_from_unproved_with_metadata`: Parses the response to retrieve the requested object
65///   along with response metadata, if any.
66/// - `from_unproved`: Retrieves the requested object from the response, returning an error if the
67///   object is not found.
68/// - `from_unproved_with_metadata`: Retrieves the requested object from the response along with
69///   metadata, returning an error if the object is not found.
70///
71/// ```
72pub trait FromUnproved<Req> {
73    /// Request type for which this trait is implemented.
74    type Request;
75    /// Response type for which this trait is implemented.
76    type Response;
77
78    /// Parse the received response and retrieve the requested object, if any.
79    ///
80    /// # Arguments
81    ///
82    /// * `request`: The request sent to the server.
83    /// * `response`: The response received from the server.
84    /// * `network`: The network we are using (Mainnet/Testnet/Devnet/Regtest).
85    /// * `platform_version`: The platform version that should be used.
86    ///
87    /// # Returns
88    ///
89    /// * `Ok(Some(object))` when the requested object was found in the response.
90    /// * `Ok(None)` when the requested object was not found.
91    /// * `Err(Error)` when parsing fails or data is invalid.
92    fn maybe_from_unproved<I: Into<Self::Request>, O: Into<Self::Response>>(
93        request: I,
94        response: O,
95        network: Network,
96        platform_version: &PlatformVersion,
97    ) -> Result<Option<Self>, Error>
98    where
99        Self: Sized,
100    {
101        Self::maybe_from_unproved_with_metadata(request, response, network, platform_version)
102            .map(|maybe_result| maybe_result.0)
103    }
104
105    /// Parse the received response and retrieve the requested object along with metadata, if any.
106    ///
107    /// # Arguments
108    ///
109    /// * `request`: The request sent to the server.
110    /// * `response`: The response received from the server.
111    /// * `network`: The network we are using (Mainnet/Testnet/Devnet/Regtest).
112    /// * `platform_version`: The platform version that should be used.
113    ///
114    /// # Returns
115    ///
116    /// * `Ok((Some(object), metadata))` when the requested object was found.
117    /// * `Ok((None, metadata))` when the requested object was not found.
118    /// * `Err(Error)` when parsing fails or data is invalid.
119    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
120        request: I,
121        response: O,
122        network: Network,
123        platform_version: &PlatformVersion,
124    ) -> Result<(Option<Self>, ResponseMetadata), Error>
125    where
126        Self: Sized;
127
128    /// Retrieve the requested object from the response.
129    ///
130    /// # Arguments
131    ///
132    /// * `request`: The request sent to the server.
133    /// * `response`: The response received from the server.
134    /// * `network`: The network we are using.
135    /// * `platform_version`: The platform version that should be used.
136    ///
137    /// # Returns
138    ///
139    /// * `Ok(object)` when the requested object was found.
140    /// * `Err(Error::NotFound)` when the requested object was not found.
141    /// * `Err(Error)` when parsing fails or data is invalid.
142    fn from_unproved<I: Into<Self::Request>, O: Into<Self::Response>>(
143        request: I,
144        response: O,
145        network: Network,
146        platform_version: &PlatformVersion,
147    ) -> Result<Self, Error>
148    where
149        Self: Sized,
150    {
151        Self::maybe_from_unproved(request, response, network, platform_version)?
152            .ok_or(Error::NotFound)
153    }
154
155    /// Retrieve the requested object from the response along with metadata.
156    ///
157    /// # Arguments
158    ///
159    /// * `request`: The request sent to the server.
160    /// * `response`: The response received from the server.
161    /// * `network`: The network we are using.
162    /// * `platform_version`: The platform version that should be used.
163    ///
164    /// # Returns
165    ///
166    /// * `Ok((object, metadata))` when the requested object was found.
167    /// * `Err(Error::NotFound)` when the requested object was not found.
168    /// * `Err(Error)` when parsing fails or data is invalid.
169    fn from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
170        request: I,
171        response: O,
172        network: Network,
173        platform_version: &PlatformVersion,
174    ) -> Result<(Self, ResponseMetadata), Error>
175    where
176        Self: Sized,
177    {
178        let (main_item, response_metadata) =
179            Self::maybe_from_unproved_with_metadata(request, response, network, platform_version)?;
180        Ok((main_item.ok_or(Error::NotFound)?, response_metadata))
181    }
182}
183
184impl FromUnproved<platform::GetCurrentQuorumsInfoRequest> for CurrentQuorumsInfo {
185    type Request = platform::GetCurrentQuorumsInfoRequest;
186    type Response = platform::GetCurrentQuorumsInfoResponse;
187
188    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
189        _request: I,
190        response: O,
191        _network: Network,
192        _platform_version: &PlatformVersion,
193    ) -> Result<(Option<Self>, ResponseMetadata), Error>
194    where
195        Self: Sized,
196    {
197        // Convert the response into a GetCurrentQuorumsInfoResponse
198        let response: platform::GetCurrentQuorumsInfoResponse = response.into();
199
200        // Extract metadata from the response
201        let metadata = match &response.version {
202            Some(platform::get_current_quorums_info_response::Version::V0(ref v0)) => {
203                v0.metadata.clone()
204            }
205            None => None,
206        }
207        .ok_or(Error::EmptyResponseMetadata)?;
208
209        // Parse response based on the version field
210        let info = match response.version.ok_or(Error::EmptyVersion)? {
211            platform::get_current_quorums_info_response::Version::V0(v0) => {
212                // Extract quorum hashes
213                let quorum_hashes = v0
214                    .quorum_hashes
215                    .into_iter()
216                    .map(|q_hash| parse_hash_32("quorum_hash", &q_hash))
217                    .collect::<Result<Vec<[u8; 32]>, Error>>()?;
218
219                // Extract current quorum hash
220                let current_quorum_hash =
221                    parse_hash_32("current_quorum_hash", &v0.current_quorum_hash)?;
222
223                let last_block_proposer =
224                    parse_hash_32("last_block_proposer", &v0.last_block_proposer)?;
225
226                // Extract validator sets
227                let validator_sets =
228                    v0.validator_sets
229                        .into_iter()
230                        .map(|vs| {
231                            // Parse the ValidatorSetV0
232                            let quorum_hash =
233                                parse_hash_32("validator_set.quorum_hash", &vs.quorum_hash)?;
234
235                            // Parse ValidatorV0 members
236                            let members = vs
237                                .members
238                                .into_iter()
239                                .map(|member| {
240                                    let pro_tx_hash = ProTxHash::from_slice(&member.pro_tx_hash)
241                                        .map_err(|_| Error::ProtocolError {
242                                            error: "Invalid ProTxHash format".to_string(),
243                                        })?;
244                                    let validator = ValidatorV0 {
245                                        pro_tx_hash,
246                                        public_key: None, // Assuming it's not provided here
247                                        node_ip: member.node_ip,
248                                        node_id: PubkeyHash::from_slice(&[0; 20]).expect("expected to make pub key hash from 20 byte empty array"), // Placeholder, since not provided
249                                        core_port: 0, // Placeholder, since not provided
250                                        platform_http_port: 0, // Placeholder, since not provided
251                                        platform_p2p_port: 0, // Placeholder, since not provided
252                                        is_banned: member.is_banned,
253                                    };
254                                    Ok((pro_tx_hash, validator))
255                                })
256                                .collect::<Result<BTreeMap<ProTxHash, ValidatorV0>, Error>>()?;
257
258                            Ok(ValidatorSet::V0(ValidatorSetV0 {
259                                quorum_hash: QuorumHash::from_slice(quorum_hash.as_slice())
260                                    .map_err(|_| Error::ProtocolError {
261                                        error: "Invalid Quorum Hash format".to_string(),
262                                    })?,
263                                quorum_index: None, // Assuming it's not provided here
264                                core_height: vs.core_height,
265                                members,
266                                threshold_public_key: BlsPublicKey::try_from(
267                                    vs.threshold_public_key.as_slice(),
268                                )
269                                .map_err(|_| Error::ProtocolError {
270                                    error: "Invalid BlsPublicKey format".to_string(),
271                                })?,
272                            }))
273                        })
274                        .collect::<Result<Vec<ValidatorSet>, Error>>()?;
275
276                // Create the CurrentQuorumsInfo struct
277                Ok::<CurrentQuorumsInfo, Error>(CurrentQuorumsInfo {
278                    quorum_hashes,
279                    current_quorum_hash,
280                    validator_sets,
281                    last_block_proposer,
282                    last_platform_block_height: metadata.height,
283                    last_core_block_height: metadata.core_chain_locked_height,
284                })
285            }
286        }?;
287
288        Ok((Some(info), metadata))
289    }
290}
291
292#[async_trait]
293impl FromUnproved<platform::GetStatusRequest> for EvoNodeStatus {
294    type Request = platform::GetStatusRequest;
295    type Response = platform::GetStatusResponse;
296
297    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
298        _request: I,
299        response: O,
300        _network: Network,
301        _platform_version: &PlatformVersion,
302    ) -> Result<(Option<Self>, ResponseMetadata), Error>
303    where
304        Self: Sized,
305    {
306        let status = Self::try_from(response.into())?;
307        // we use default response metadata, as this request does not return any metadata
308        Ok((Some(status), Default::default()))
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use dapi_grpc::platform::v0::{
316        get_current_quorums_info_response, get_status_response, ResponseMetadata,
317    };
318    use dpp::bls_signatures::{Bls12381G2Impl, SecretKey};
319    use dpp::version::PlatformVersion;
320
321    /// Generate a valid BLS public key as compressed bytes (48 bytes) from a
322    /// deterministic secret key derived from the given seed byte.
323    fn generate_valid_bls_public_key_bytes(seed: u8) -> Vec<u8> {
324        let mut secret_bytes = [0u8; 32];
325        secret_bytes[31] = seed.max(1); // ensure nonzero
326        let sk: SecretKey<Bls12381G2Impl> =
327            SecretKey::<Bls12381G2Impl>::from_be_bytes(&secret_bytes)
328                .into_option()
329                .expect("valid secret key");
330        sk.public_key().0.to_compressed().to_vec()
331    }
332
333    /// Helper: build a valid GetCurrentQuorumsInfoResponse with one quorum hash,
334    /// one validator set with one member, and metadata.
335    fn build_valid_quorums_info_response() -> platform::GetCurrentQuorumsInfoResponse {
336        let quorum_hash = vec![1u8; 32];
337        let current_quorum_hash = vec![2u8; 32];
338        let last_block_proposer = vec![3u8; 32];
339        let pro_tx_hash = vec![4u8; 32];
340        let threshold_public_key = generate_valid_bls_public_key_bytes(42);
341
342        let member = get_current_quorums_info_response::ValidatorV0 {
343            pro_tx_hash: pro_tx_hash.clone(),
344            node_ip: "127.0.0.1".to_string(),
345            is_banned: false,
346        };
347
348        let validator_set = get_current_quorums_info_response::ValidatorSetV0 {
349            quorum_hash: quorum_hash.clone(),
350            core_height: 100,
351            members: vec![member],
352            threshold_public_key,
353        };
354
355        let v0 = get_current_quorums_info_response::GetCurrentQuorumsInfoResponseV0 {
356            quorum_hashes: vec![quorum_hash],
357            current_quorum_hash,
358            validator_sets: vec![validator_set],
359            last_block_proposer,
360            metadata: Some(ResponseMetadata {
361                height: 500,
362                core_chain_locked_height: 200,
363                epoch: 10,
364                time_ms: 1234567890,
365                protocol_version: 1,
366                chain_id: "dash-testnet-1".to_string(),
367            }),
368        };
369
370        platform::GetCurrentQuorumsInfoResponse {
371            version: Some(get_current_quorums_info_response::Version::V0(v0)),
372        }
373    }
374
375    /// Helper: build a valid GetStatusResponse V0 with all inner fields populated.
376    fn build_valid_status_response() -> platform::GetStatusResponse {
377        use dapi_grpc::platform::v0::get_status_response::get_status_response_v0;
378
379        let software = get_status_response_v0::version::Software {
380            dapi: "1.0.0".to_string(),
381            drive: Some("2.0.0".to_string()),
382            tenderdash: Some("0.14.0".to_string()),
383        };
384
385        let tenderdash_protocol =
386            get_status_response_v0::version::protocol::Tenderdash { p2p: 8, block: 11 };
387
388        let drive_protocol = get_status_response_v0::version::protocol::Drive {
389            latest: 5,
390            current: 4,
391            next_epoch: 5,
392        };
393
394        let protocol = get_status_response_v0::version::Protocol {
395            tenderdash: Some(tenderdash_protocol),
396            drive: Some(drive_protocol),
397        };
398
399        let version = get_status_response_v0::Version {
400            software: Some(software),
401            protocol: Some(protocol),
402        };
403
404        let time = get_status_response_v0::Time {
405            local: 1700000000,
406            block: Some(1699999900),
407            genesis: Some(1690000000),
408            epoch: Some(42),
409        };
410
411        let node = get_status_response_v0::Node {
412            id: vec![10u8; 20],
413            pro_tx_hash: Some(vec![11u8; 32]),
414        };
415
416        let chain = get_status_response_v0::Chain {
417            catching_up: false,
418            latest_block_hash: vec![20u8; 32],
419            latest_app_hash: vec![21u8; 32],
420            latest_block_height: 1000,
421            earliest_block_hash: vec![22u8; 32],
422            earliest_app_hash: vec![23u8; 32],
423            earliest_block_height: 1,
424            max_peer_block_height: 1001,
425            core_chain_locked_height: Some(500),
426        };
427
428        let network = get_status_response_v0::Network {
429            chain_id: "dash-testnet-1".to_string(),
430            peers_count: 25,
431            listening: true,
432        };
433
434        let state_sync = get_status_response_v0::StateSync {
435            total_synced_time: 3600,
436            remaining_time: 120,
437            total_snapshots: 5,
438            chunk_process_avg_time: 50,
439            snapshot_height: 900,
440            snapshot_chunks_count: 100,
441            backfilled_blocks: 800,
442            backfill_blocks_total: 1000,
443        };
444
445        let v0 = get_status_response::GetStatusResponseV0 {
446            version: Some(version),
447            node: Some(node),
448            chain: Some(chain),
449            network: Some(network),
450            state_sync: Some(state_sync),
451            time: Some(time),
452        };
453
454        platform::GetStatusResponse {
455            version: Some(get_status_response::Version::V0(v0)),
456        }
457    }
458
459    #[test]
460    fn test_current_quorums_info_valid_response() {
461        let request = platform::GetCurrentQuorumsInfoRequest { version: None };
462        let response = build_valid_quorums_info_response();
463        let platform_version = PlatformVersion::latest();
464
465        let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
466            request,
467            response,
468            Network::Testnet,
469            platform_version,
470        );
471
472        let (maybe_info, metadata) = result.expect("should parse valid response");
473        let info = maybe_info.expect("should contain CurrentQuorumsInfo");
474
475        assert_eq!(info.quorum_hashes.len(), 1);
476        assert_eq!(info.quorum_hashes[0], [1u8; 32]);
477        assert_eq!(info.current_quorum_hash, [2u8; 32]);
478        assert_eq!(info.last_block_proposer, [3u8; 32]);
479        assert_eq!(info.validator_sets.len(), 1);
480        assert_eq!(info.last_platform_block_height, 500);
481        assert_eq!(info.last_core_block_height, 200);
482        assert_eq!(metadata.height, 500);
483        assert_eq!(metadata.core_chain_locked_height, 200);
484    }
485
486    #[test]
487    fn test_current_quorums_info_invalid_quorum_hash_length() {
488        let mut response = build_valid_quorums_info_response();
489
490        // Inject an invalid quorum_hash that is not 32 bytes
491        if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
492            v0.quorum_hashes = vec![vec![0u8; 16]]; // 16 bytes instead of 32
493        }
494
495        let request = platform::GetCurrentQuorumsInfoRequest { version: None };
496        let platform_version = PlatformVersion::latest();
497
498        let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
499            request,
500            response,
501            Network::Testnet,
502            platform_version,
503        );
504
505        let err = result.expect_err("should fail for invalid quorum_hash length");
506        let err_string = err.to_string();
507        assert!(
508            err_string.contains("Invalid quorum_hash length"),
509            "unexpected error: {err_string}"
510        );
511    }
512
513    #[test]
514    fn test_current_quorums_info_invalid_current_quorum_hash_length() {
515        let mut response = build_valid_quorums_info_response();
516
517        // Inject an invalid current_quorum_hash that is not 32 bytes
518        if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
519            v0.current_quorum_hash = vec![0u8; 10]; // 10 bytes instead of 32
520        }
521
522        let request = platform::GetCurrentQuorumsInfoRequest { version: None };
523        let platform_version = PlatformVersion::latest();
524
525        let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
526            request,
527            response,
528            Network::Testnet,
529            platform_version,
530        );
531
532        let err = result.expect_err("should fail for invalid current_quorum_hash length");
533        let err_string = err.to_string();
534        assert!(
535            err_string.contains("Invalid current_quorum_hash length"),
536            "unexpected error: {err_string}"
537        );
538    }
539
540    #[test]
541    fn test_current_quorums_info_invalid_last_block_proposer() {
542        let mut response = build_valid_quorums_info_response();
543
544        // Inject an invalid last_block_proposer that is not 32 bytes
545        if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
546            v0.last_block_proposer = vec![0u8; 5]; // 5 bytes instead of 32
547        }
548
549        let request = platform::GetCurrentQuorumsInfoRequest { version: None };
550        let platform_version = PlatformVersion::latest();
551
552        let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
553            request,
554            response,
555            Network::Testnet,
556            platform_version,
557        );
558
559        let err = result.expect_err("should fail for invalid last_block_proposer length");
560        let err_string = err.to_string();
561        assert!(
562            err_string.contains("Invalid last_block_proposer length"),
563            "unexpected error: {err_string}"
564        );
565    }
566
567    #[test]
568    fn test_current_quorums_info_rejects_malformed_nested_quorum_hashes() {
569        for invalid_length in [0, 1, 31, 33, 1024] {
570            let mut response = build_valid_quorums_info_response();
571            if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) =
572                response.version
573            {
574                v0.validator_sets[0].quorum_hash = vec![0u8; invalid_length];
575            }
576
577            let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
578                platform::GetCurrentQuorumsInfoRequest { version: None },
579                response,
580                Network::Testnet,
581                PlatformVersion::latest(),
582            );
583
584            let error = result.expect_err("malformed nested quorum hash must return an error");
585            assert!(
586                matches!(error, Error::ProtocolError { ref error } if error.contains("validator_set.quorum_hash")),
587                "unexpected error for length {invalid_length}: {error:?}"
588            );
589        }
590    }
591
592    #[test]
593    fn test_current_quorums_info_none_metadata() {
594        let mut response = build_valid_quorums_info_response();
595
596        // Remove metadata from the response
597        if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
598            v0.metadata = None;
599        }
600
601        let request = platform::GetCurrentQuorumsInfoRequest { version: None };
602        let platform_version = PlatformVersion::latest();
603
604        let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
605            request,
606            response,
607            Network::Testnet,
608            platform_version,
609        );
610
611        let err = result.expect_err("should fail when metadata is missing");
612        let err_string = err.to_string();
613        assert!(
614            err_string.contains("empty response metadata"),
615            "unexpected error: {err_string}"
616        );
617    }
618
619    #[test]
620    fn test_evo_node_status_valid_response() {
621        let request = platform::GetStatusRequest { version: None };
622        let response = build_valid_status_response();
623        let platform_version = PlatformVersion::latest();
624
625        let result = EvoNodeStatus::maybe_from_unproved_with_metadata(
626            request,
627            response,
628            Network::Testnet,
629            platform_version,
630        );
631
632        let (maybe_status, _metadata) = result.expect("should parse valid status response");
633        let status = maybe_status.expect("should contain EvoNodeStatus");
634
635        // Verify version fields
636        let software = status.version.software.as_ref().unwrap();
637        assert_eq!(software.dapi, "1.0.0");
638        assert_eq!(software.drive.as_deref(), Some("2.0.0"));
639        assert_eq!(software.tenderdash.as_deref(), Some("0.14.0"));
640
641        let protocol = status.version.protocol.as_ref().unwrap();
642        let td = protocol.tenderdash.as_ref().unwrap();
643        assert_eq!(td.p2p, 8);
644        assert_eq!(td.block, 11);
645        let drv = protocol.drive.as_ref().unwrap();
646        assert_eq!(drv.latest, 5);
647        assert_eq!(drv.current, 4);
648        assert_eq!(drv.next_epoch, 5);
649
650        // Verify node fields
651        assert_eq!(status.node.id, vec![10u8; 20]);
652        assert_eq!(status.node.pro_tx_hash, Some(vec![11u8; 32]));
653
654        // Verify chain fields
655        assert!(!status.chain.catching_up);
656        assert_eq!(status.chain.latest_block_height, 1000);
657        assert_eq!(status.chain.core_chain_locked_height, Some(500));
658
659        // Verify network fields
660        assert_eq!(status.network.chain_id, "dash-testnet-1");
661        assert_eq!(status.network.peers_count, 25);
662        assert!(status.network.listening);
663
664        // Verify time fields
665        assert_eq!(status.time.local, 1700000000);
666        assert_eq!(status.time.block, Some(1699999900));
667        assert_eq!(status.time.epoch, Some(42));
668    }
669}
670
671impl FromUnproved<platform::GetDataContractsLatestVersionsRequest> for DataContractsLatestVersions {
672    type Request = platform::GetDataContractsLatestVersionsRequest;
673    type Response = platform::GetDataContractsLatestVersionsResponse;
674
675    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
676        _request: I,
677        response: O,
678        _network: Network,
679        platform_version: &PlatformVersion,
680    ) -> Result<(Option<Self>, ResponseMetadata), Error>
681    where
682        Self: Sized,
683    {
684        use platform::get_data_contracts_latest_versions_response::get_data_contracts_latest_versions_response_v0::Result as V0Result;
685
686        let response: Self::Response = response.into();
687
688        let platform::get_data_contracts_latest_versions_response::Version::V0(v0) =
689            response.version.ok_or(Error::EmptyVersion)?;
690        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
691
692        let versions = match v0.result {
693            Some(V0Result::DataContractsLatestVersions(versions)) => versions
694                .entries
695                .into_iter()
696                .map(|entry| {
697                    let id = Identifier::from_bytes(&entry.identifier).map_err(|e| {
698                        Error::ProtocolError {
699                            error: e.to_string(),
700                        }
701                    })?;
702                    let latest_version = entry
703                        .version
704                        .map(|version| {
705                            let data_contract = entry
706                                .data_contract
707                                .map(|bytes| {
708                                    DataContract::versioned_deserialize_untrusted(
709                                        &bytes,
710                                        false,
711                                        platform_version,
712                                    )
713                                })
714                                .transpose()
715                                .map_err(|e| Error::ProtocolError {
716                                    error: e.to_string(),
717                                })?;
718                            Ok::<_, Error>(DataContractLatestVersion {
719                                version,
720                                data_contract,
721                            })
722                        })
723                        .transpose()?;
724                    Ok((id, latest_version))
725                })
726                .collect::<Result<DataContractsLatestVersions, Error>>()
727                .map(Some)?,
728            Some(V0Result::Proof(_)) => {
729                return Err(Error::ResponseDecodeError {
730                    error: "expected unproved data contract versions, got a proof".to_string(),
731                })
732            }
733            None => None,
734        };
735
736        Ok((versions, metadata))
737    }
738}
739
740#[cfg(test)]
741mod data_contracts_latest_versions_tests {
742    use super::*;
743    use dapi_grpc::platform::v0::get_data_contracts_latest_versions_response::{
744        get_data_contracts_latest_versions_response_v0::Result as V0Result,
745        DataContractLatestVersionEntry, DataContractsLatestVersions as ProtoLatestVersions,
746        GetDataContractsLatestVersionsResponseV0, Version,
747    };
748    use dpp::data_contract::accessors::v0::DataContractV0Getters;
749    use dpp::serialization::PlatformSerializableWithPlatformVersion;
750    use dpp::tests::fixtures::get_data_contract_fixture;
751
752    fn response(result: Option<V0Result>) -> platform::GetDataContractsLatestVersionsResponse {
753        platform::GetDataContractsLatestVersionsResponse {
754            version: Some(Version::V0(GetDataContractsLatestVersionsResponseV0 {
755                result,
756                metadata: Some(ResponseMetadata::default()),
757            })),
758        }
759    }
760
761    fn parse(
762        response: platform::GetDataContractsLatestVersionsResponse,
763    ) -> Result<Option<DataContractsLatestVersions>, Error> {
764        <DataContractsLatestVersions as FromUnproved<
765            platform::GetDataContractsLatestVersionsRequest,
766        >>::maybe_from_unproved(
767            platform::GetDataContractsLatestVersionsRequest::default(),
768            response,
769            Network::Testnet,
770            PlatformVersion::latest(),
771        )
772    }
773
774    #[test]
775    fn version_only_entries_carry_no_contract_and_missing_ids_map_to_none() {
776        let versions = parse(response(Some(V0Result::DataContractsLatestVersions(
777            ProtoLatestVersions {
778                entries: vec![
779                    DataContractLatestVersionEntry {
780                        identifier: vec![1; 32],
781                        version: Some(7),
782                        data_contract: None,
783                    },
784                    DataContractLatestVersionEntry {
785                        identifier: vec![2; 32],
786                        version: None,
787                        data_contract: None,
788                    },
789                ],
790            },
791        ))))
792        .expect("parse")
793        .expect("present");
794
795        assert_eq!(versions.0.len(), 2);
796        assert_eq!(versions.version_of(&Identifier::new([1; 32])), Some(7));
797        assert!(versions.0[&Identifier::new([1; 32])]
798            .as_ref()
799            .expect("found")
800            .data_contract
801            .is_none());
802        assert_eq!(versions.0[&Identifier::new([2; 32])], None);
803        assert_eq!(versions.version_of(&Identifier::new([3; 32])), None);
804    }
805
806    #[test]
807    fn included_contract_is_deserialized() {
808        let platform_version = PlatformVersion::latest();
809        let contract = get_data_contract_fixture(None, 0, platform_version.protocol_version)
810            .data_contract_owned();
811        let bytes = contract
812            .serialize_to_bytes_with_platform_version(platform_version)
813            .expect("serialize");
814
815        let versions = parse(response(Some(V0Result::DataContractsLatestVersions(
816            ProtoLatestVersions {
817                entries: vec![DataContractLatestVersionEntry {
818                    identifier: contract.id().to_vec(),
819                    version: Some(contract.version()),
820                    data_contract: Some(bytes),
821                }],
822            },
823        ))))
824        .expect("parse")
825        .expect("present");
826
827        let entry = versions.0[&contract.id()].clone().expect("found");
828        assert_eq!(entry.version, contract.version());
829        assert_eq!(entry.data_contract, Some(contract));
830    }
831
832    #[test]
833    fn a_proof_where_values_were_expected_is_rejected() {
834        let err = parse(response(Some(V0Result::Proof(platform::Proof::default())))).unwrap_err();
835        assert!(
836            matches!(err, Error::ResponseDecodeError { .. }),
837            "got: {err:?}"
838        );
839    }
840
841    #[test]
842    fn missing_metadata_is_rejected() {
843        let response = platform::GetDataContractsLatestVersionsResponse {
844            version: Some(Version::V0(GetDataContractsLatestVersionsResponseV0 {
845                result: None,
846                metadata: None,
847            })),
848        };
849        let err = parse(response).unwrap_err();
850        assert!(matches!(err, Error::EmptyResponseMetadata), "got: {err:?}");
851    }
852}
853
854impl FromUnproved<platform::GetContractGroupInfoRequest> for ContractGroupInfo {
855    type Request = platform::GetContractGroupInfoRequest;
856    type Response = platform::GetContractGroupInfoResponse;
857
858    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
859        _request: I,
860        response: O,
861        _network: Network,
862        _platform_version: &PlatformVersion,
863    ) -> Result<(Option<Self>, ResponseMetadata), Error>
864    where
865        Self: Sized,
866    {
867        use platform::get_contract_group_info_response::get_contract_group_info_response_v0::Result as V0Result;
868
869        let response: Self::Response = response.into();
870
871        let platform::get_contract_group_info_response::Version::V0(v0) =
872            response.version.ok_or(Error::EmptyVersion)?;
873        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
874
875        let info = match v0.result {
876            Some(V0Result::ContractGroupInfo(info)) => Some(contract_group_info_from_proto(info)?),
877            Some(V0Result::Proof(_)) => {
878                return Err(Error::ResponseDecodeError {
879                    error: "expected unproved contract group info, got a proof".to_string(),
880                })
881            }
882            None => None,
883        };
884
885        Ok((info, metadata))
886    }
887}
888
889impl FromUnproved<platform::GetContractModerationStatusRequest> for ContractModerationListStatuses {
890    type Request = platform::GetContractModerationStatusRequest;
891    type Response = platform::GetContractModerationStatusResponse;
892
893    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
894        request: I,
895        response: O,
896        _network: Network,
897        _platform_version: &PlatformVersion,
898    ) -> Result<(Option<Self>, ResponseMetadata), Error>
899    where
900        Self: Sized,
901    {
902        use platform::get_contract_moderation_status_response::get_contract_moderation_status_response_v0::Result as V0Result;
903
904        // The response holds a flat status. Only the lists the request named are reported, and
905        // only when the response says it read them.
906        let request: Self::Request = request.into();
907        let platform::get_contract_moderation_status_request::Version::V0(request_v0) =
908            request.version.ok_or(Error::EmptyVersion)?;
909        let lists = lists_from_request(&request_v0.lists)?;
910
911        let response: Self::Response = response.into();
912
913        let platform::get_contract_moderation_status_response::Version::V0(v0) =
914            response.version.ok_or(Error::EmptyVersion)?;
915        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
916
917        let status = match v0.result {
918            Some(V0Result::Status(status)) => {
919                // The response says which lists it read. A list asked for and not among them
920                // was not read, so nothing may be reported about it, least of all "not banned".
921                let covered: Vec<ContractModerationList> = status
922                    .lists
923                    .iter()
924                    .filter_map(|list| list_from_request(*list, "lists").ok())
925                    .collect();
926                if let Some(missing) = lists.iter().find(|list| !covered.contains(list)) {
927                    return Err(Error::ResponseDecodeError {
928                        error: format!(
929                            "contract moderation status does not cover the {missing} asked for"
930                        ),
931                    });
932                }
933                let banned = match status.banned {
934                    Some(banned) => banned,
935                    None if lists.contains(&ContractModerationList::Banlist) => {
936                        return Err(Error::ResponseDecodeError {
937                            error: "contract moderation status covers the banlist but does not \
938                                    say whether the identity is banned"
939                                .to_string(),
940                        })
941                    }
942                    None => false,
943                };
944                // An entry comes with its reason. Only the lists asked for are decoded, since
945                // only they are reported.
946                let ban = (banned && lists.contains(&ContractModerationList::Banlist))
947                    .then(|| reason_from_response(status.ban_reason))
948                    .transpose()?
949                    .map(|reason| ContractBan { reason });
950                let suspension = status
951                    .suspended_until
952                    .filter(|_| lists.contains(&ContractModerationList::Suspensions))
953                    .map(|until| {
954                        reason_from_response(status.suspension_reason)
955                            .map(|reason| ContractSuspension { until, reason })
956                    })
957                    .transpose()?;
958                // The warnings are read only when the warning list was asked for: an empty
959                // list on the wire says "none" for a list read and nothing for one not read.
960                let warnings = if lists.contains(&ContractModerationList::Warnings) {
961                    warnings_from_response(status.warnings)?
962                } else {
963                    vec![]
964                };
965                Some(ContractModerationListStatuses::from_status(
966                    &lists,
967                    &ContractModerationStatus {
968                        ban,
969                        suspension,
970                        warnings,
971                    },
972                ))
973            }
974            Some(V0Result::Proof(_)) => {
975                return Err(Error::ResponseDecodeError {
976                    error: "expected unproved contract moderation status, got a proof".to_string(),
977                })
978            }
979            None => None,
980        };
981
982        Ok((status, metadata))
983    }
984}
985
986impl FromUnproved<platform::GetContractModerationEntriesRequest> for ContractModerationEntries {
987    type Request = platform::GetContractModerationEntriesRequest;
988    type Response = platform::GetContractModerationEntriesResponse;
989
990    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
991        _request: I,
992        response: O,
993        _network: Network,
994        _platform_version: &PlatformVersion,
995    ) -> Result<(Option<Self>, ResponseMetadata), Error>
996    where
997        Self: Sized,
998    {
999        use platform::get_contract_moderation_entries_response::get_contract_moderation_entries_response_v0::Result as V0Result;
1000
1001        let response: Self::Response = response.into();
1002
1003        let platform::get_contract_moderation_entries_response::Version::V0(v0) =
1004            response.version.ok_or(Error::EmptyVersion)?;
1005        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1006
1007        let entries = match v0.result {
1008            Some(V0Result::Entries(entries)) => Some(entries_from_response(entries.entries)?),
1009            Some(V0Result::Proof(_)) => {
1010                return Err(Error::ResponseDecodeError {
1011                    error: "expected unproved contract moderation entries, got a proof".to_string(),
1012                })
1013            }
1014            None => None,
1015        };
1016
1017        Ok((entries, metadata))
1018    }
1019}
1020
1021impl FromUnproved<platform::GetContractDocumentRemovalsRequest> for ContractDocumentRemovals {
1022    type Request = platform::GetContractDocumentRemovalsRequest;
1023    type Response = platform::GetContractDocumentRemovalsResponse;
1024
1025    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1026        request: I,
1027        response: O,
1028        _network: Network,
1029        platform_version: &PlatformVersion,
1030    ) -> Result<(Option<Self>, ResponseMetadata), Error>
1031    where
1032        Self: Sized,
1033    {
1034        use platform::get_contract_document_removals_response::get_contract_document_removals_response_v0::Result as V0Result;
1035
1036        let request: Self::Request = request.into();
1037        let response: Self::Response = response.into();
1038
1039        // The request bounds what the response may hold, so it is read back here too, under
1040        // the same rules the node applied.
1041        let platform::get_contract_document_removals_request::Version::V0(request_v0) =
1042            request.version.ok_or(Error::EmptyVersion)?;
1043        let query = removals_query_from_request(
1044            request_v0.document_type_name,
1045            request_v0.selection,
1046            platform_version,
1047        )?;
1048
1049        let platform::get_contract_document_removals_response::Version::V0(v0) =
1050            response.version.ok_or(Error::EmptyVersion)?;
1051        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1052
1053        let removals = match v0.result {
1054            Some(V0Result::Removals(removals)) => {
1055                Some(removals_from_response(removals.removals, &query)?)
1056            }
1057            Some(V0Result::Proof(_)) => {
1058                return Err(Error::ResponseDecodeError {
1059                    error: "expected unproved contract document removals, got a proof".to_string(),
1060                })
1061            }
1062            None => None,
1063        };
1064
1065        Ok((removals, metadata))
1066    }
1067}
1068
1069impl FromUnproved<platform::GetContractTeamActionsRequest> for ContractTeamActions {
1070    type Request = platform::GetContractTeamActionsRequest;
1071    type Response = platform::GetContractTeamActionsResponse;
1072
1073    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1074        request: I,
1075        response: O,
1076        _network: Network,
1077        platform_version: &PlatformVersion,
1078    ) -> Result<(Option<Self>, ResponseMetadata), Error>
1079    where
1080        Self: Sized,
1081    {
1082        use platform::get_contract_team_actions_response::get_contract_team_actions_response_v0::Result as V0Result;
1083
1084        let request: Self::Request = request.into();
1085        let response: Self::Response = response.into();
1086
1087        // The request bounds what the response may hold, so it is read back here too, under
1088        // the same rules the node applied.
1089        let platform::get_contract_team_actions_request::Version::V0(request_v0) =
1090            request.version.ok_or(Error::EmptyVersion)?;
1091        let query = team_actions_query_from_request(
1092            request_v0.status,
1093            request_v0.start_at_action_id,
1094            request_v0.count,
1095            platform_version,
1096        )?;
1097
1098        let platform::get_contract_team_actions_response::Version::V0(v0) =
1099            response.version.ok_or(Error::EmptyVersion)?;
1100        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1101
1102        let actions = match v0.result {
1103            Some(V0Result::Actions(actions)) => {
1104                Some(team_actions_from_response(actions.actions, &query)?)
1105            }
1106            Some(V0Result::Proof(_)) => {
1107                return Err(Error::ResponseDecodeError {
1108                    error: "expected unproved contract team actions, got a proof".to_string(),
1109                })
1110            }
1111            None => None,
1112        };
1113
1114        Ok((actions, metadata))
1115    }
1116}
1117
1118impl FromUnproved<platform::GetContractTeamActionSignersRequest> for ContractTeamActionSigners {
1119    type Request = platform::GetContractTeamActionSignersRequest;
1120    type Response = platform::GetContractTeamActionSignersResponse;
1121
1122    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1123        request: I,
1124        response: O,
1125        _network: Network,
1126        _platform_version: &PlatformVersion,
1127    ) -> Result<(Option<Self>, ResponseMetadata), Error>
1128    where
1129        Self: Sized,
1130    {
1131        use platform::get_contract_team_action_signers_response::get_contract_team_action_signers_response_v0::Result as V0Result;
1132
1133        let request: Self::Request = request.into();
1134        let response: Self::Response = response.into();
1135
1136        let platform::get_contract_team_action_signers_request::Version::V0(request_v0) =
1137            request.version.ok_or(Error::EmptyVersion)?;
1138        team_action_status_from_request(request_v0.status)?;
1139
1140        let platform::get_contract_team_action_signers_response::Version::V0(v0) =
1141            response.version.ok_or(Error::EmptyVersion)?;
1142        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1143
1144        let signers = match v0.result {
1145            Some(V0Result::Signers(signers)) => {
1146                Some(team_action_signers_from_response(signers.signer_ids)?)
1147            }
1148            Some(V0Result::Proof(_)) => {
1149                return Err(Error::ResponseDecodeError {
1150                    error: "expected unproved contract team action signers, got a proof"
1151                        .to_string(),
1152                })
1153            }
1154            None => None,
1155        };
1156
1157        Ok((signers, metadata))
1158    }
1159}
1160
1161impl FromUnproved<platform::GetContractModerationActionCountsRequest>
1162    for ContractModerationActionCounts
1163{
1164    type Request = platform::GetContractModerationActionCountsRequest;
1165    type Response = platform::GetContractModerationActionCountsResponse;
1166
1167    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1168        _request: I,
1169        response: O,
1170        _network: Network,
1171        _platform_version: &PlatformVersion,
1172    ) -> Result<(Option<Self>, ResponseMetadata), Error>
1173    where
1174        Self: Sized,
1175    {
1176        let response: Self::Response = response.into();
1177        let platform::get_contract_moderation_action_counts_response::Version::V0(v0) =
1178            response.version.ok_or(Error::EmptyVersion)?;
1179        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1180
1181        let counts = match v0.result {
1182            Some(ModerationActionCountsResult::Counts(counts)) => {
1183                Some(moderation_action_counts_from_response(counts.counts)?)
1184            }
1185            Some(ModerationActionCountsResult::Proof(_)) => {
1186                return Err(Error::ResponseDecodeError {
1187                    error: "expected unproved contract moderation action counts, got a proof"
1188                        .to_string(),
1189                })
1190            }
1191            None => None,
1192        };
1193
1194        Ok((counts, metadata))
1195    }
1196}
1197
1198impl FromUnproved<platform::GetContractFeePotsRequest> for ContractFeePots {
1199    type Request = platform::GetContractFeePotsRequest;
1200    type Response = platform::GetContractFeePotsResponse;
1201
1202    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1203        _request: I,
1204        response: O,
1205        _network: Network,
1206        _platform_version: &PlatformVersion,
1207    ) -> Result<(Option<Self>, ResponseMetadata), Error>
1208    where
1209        Self: Sized,
1210    {
1211        use platform::get_contract_fee_pots_response::get_contract_fee_pots_response_v0::Result as V0Result;
1212
1213        let response: Self::Response = response.into();
1214
1215        let platform::get_contract_fee_pots_response::Version::V0(v0) =
1216            response.version.ok_or(Error::EmptyVersion)?;
1217        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1218
1219        let pots = match v0.result {
1220            Some(V0Result::Pots(pots)) => Some(fee_pots_from_response(pots)?),
1221            Some(V0Result::Proof(_)) => {
1222                return Err(Error::ResponseDecodeError {
1223                    error: "expected unproved contract fee pots, got a proof".to_string(),
1224                })
1225            }
1226            None => None,
1227        };
1228
1229        Ok((pots, metadata))
1230    }
1231}
1232
1233impl FromUnproved<platform::GetIdentityKeysRemainingBudgetsRequest>
1234    for IdentityKeysRemainingBudgets
1235{
1236    type Request = platform::GetIdentityKeysRemainingBudgetsRequest;
1237    type Response = platform::GetIdentityKeysRemainingBudgetsResponse;
1238
1239    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1240        _request: I,
1241        response: O,
1242        _network: Network,
1243        _platform_version: &PlatformVersion,
1244    ) -> Result<(Option<Self>, ResponseMetadata), Error>
1245    where
1246        Self: Sized,
1247    {
1248        use platform::get_identity_keys_remaining_budgets_response::get_identity_keys_remaining_budgets_response_v0::Result as V0Result;
1249
1250        let response: Self::Response = response.into();
1251
1252        let platform::get_identity_keys_remaining_budgets_response::Version::V0(v0) =
1253            response.version.ok_or(Error::EmptyVersion)?;
1254        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1255
1256        let budgets = match v0.result {
1257            Some(V0Result::KeysRemainingBudgets(budgets)) => Some(
1258                budgets
1259                    .entries
1260                    .into_iter()
1261                    .map(|entry| (entry.key_id, entry.remaining_budget))
1262                    .collect(),
1263            ),
1264            Some(V0Result::Proof(_)) => {
1265                return Err(Error::ResponseDecodeError {
1266                    error: "expected unproved remaining budgets, got a proof".to_string(),
1267                })
1268            }
1269            None => None,
1270        };
1271
1272        Ok((budgets, metadata))
1273    }
1274}
1275
1276impl FromUnproved<platform::GetContractGroupMembersRequest> for ContractGroupMembersPage {
1277    type Request = platform::GetContractGroupMembersRequest;
1278    type Response = platform::GetContractGroupMembersResponse;
1279
1280    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1281        request: I,
1282        response: O,
1283        _network: Network,
1284        _platform_version: &PlatformVersion,
1285    ) -> Result<(Option<Self>, ResponseMetadata), Error>
1286    where
1287        Self: Sized,
1288    {
1289        let request: Self::Request = request.into();
1290        let response: Self::Response = response.into();
1291
1292        let platform::get_contract_group_members_request::Version::V0(request_v0) =
1293            request.version.ok_or(Error::EmptyVersion)?;
1294        let query = members_query_from_request(request_v0.members)?;
1295
1296        let platform::get_contract_group_members_response::Version::V0(v0) =
1297            response.version.ok_or(Error::EmptyVersion)?;
1298        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1299
1300        let page = v0
1301            .result
1302            .map(|result| members_page_from_response(&query, result))
1303            .transpose()?;
1304
1305        Ok((page, metadata))
1306    }
1307}
1308
1309impl FromUnproved<platform::GetContractGroupsForContractRequest>
1310    for ContractGroupMembershipsForContract
1311{
1312    type Request = platform::GetContractGroupsForContractRequest;
1313    type Response = platform::GetContractGroupsForContractResponse;
1314
1315    fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1316        _request: I,
1317        response: O,
1318        _network: Network,
1319        _platform_version: &PlatformVersion,
1320    ) -> Result<(Option<Self>, ResponseMetadata), Error>
1321    where
1322        Self: Sized,
1323    {
1324        use platform::get_contract_groups_for_contract_response::get_contract_groups_for_contract_response_v0::Result as V0Result;
1325
1326        let response: Self::Response = response.into();
1327
1328        let platform::get_contract_groups_for_contract_response::Version::V0(v0) =
1329            response.version.ok_or(Error::EmptyVersion)?;
1330        let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1331
1332        let memberships = match v0.result {
1333            Some(V0Result::ContractGroupMemberships(memberships)) => {
1334                Some(memberships_from_response(memberships)?)
1335            }
1336            Some(V0Result::Proof(_)) => {
1337                return Err(Error::ResponseDecodeError {
1338                    error: "expected unproved contract group memberships, got a proof".to_string(),
1339                })
1340            }
1341            None => None,
1342        };
1343
1344        Ok((memberships, metadata))
1345    }
1346}
1347
1348#[cfg(test)]
1349mod contract_groups_tests {
1350    use super::*;
1351    use crate::types::contract_groups::{ContractGroupMembersQuery, ContractGroupOwner};
1352    use dapi_grpc::platform::v0::get_contract_group_info_response::{
1353        get_contract_group_info_response_v0::Result as InfoResult,
1354        ContractGroupInfo as ContractGroupInfoProto, GetContractGroupInfoResponseV0,
1355        Version as InfoVersion,
1356    };
1357    use dapi_grpc::platform::v0::get_contract_group_members_request::get_contract_group_members_request_v0::Members;
1358    use dapi_grpc::platform::v0::get_contract_group_members_request::{
1359        ContractMembersQuery, GetContractGroupMembersRequestV0, TokenMembersQuery,
1360        Version as MembersRequestVersion,
1361    };
1362    use dapi_grpc::platform::v0::get_contract_group_members_response::{
1363        get_contract_group_members_response_v0::Result as MembersResult, ContractMembers,
1364        GetContractGroupMembersResponseV0, TokenMembers, Version as MembersVersion,
1365    };
1366    use dapi_grpc::platform::v0::get_contract_groups_for_contract_response::{
1367        get_contract_groups_for_contract_response_v0::Result as MembershipsResult,
1368        ContractGroupMemberships, DocumentTypeMemberships, GetContractGroupsForContractResponseV0,
1369        TokenMemberships, Version as MembershipsVersion,
1370    };
1371    use dapi_grpc::platform::v0::ContractGroupTokenMember;
1372    use std::collections::BTreeSet;
1373
1374    fn info_response(result: Option<InfoResult>) -> platform::GetContractGroupInfoResponse {
1375        platform::GetContractGroupInfoResponse {
1376            version: Some(InfoVersion::V0(GetContractGroupInfoResponseV0 {
1377                result,
1378                metadata: Some(ResponseMetadata::default()),
1379            })),
1380        }
1381    }
1382
1383    fn parse_info(
1384        response: platform::GetContractGroupInfoResponse,
1385    ) -> Result<Option<ContractGroupInfo>, Error> {
1386        <ContractGroupInfo as FromUnproved<platform::GetContractGroupInfoRequest>>::maybe_from_unproved(
1387            platform::GetContractGroupInfoRequest::default(),
1388            response,
1389            Network::Testnet,
1390            PlatformVersion::latest(),
1391        )
1392    }
1393
1394    #[test]
1395    fn info_with_no_admins_is_a_single_owner() {
1396        let info = parse_info(info_response(Some(InfoResult::ContractGroupInfo(
1397            ContractGroupInfoProto {
1398                owner_id: vec![1; 32],
1399                admin_ids: vec![],
1400                name: Some("alpha".to_string()),
1401                description: None,
1402            },
1403        ))))
1404        .expect("parse")
1405        .expect("present");
1406
1407        assert_eq!(
1408            info.owner(),
1409            &ContractGroupOwner::SingleOwner(Identifier::new([1; 32]))
1410        );
1411        assert_eq!(info.name(), Some("alpha"));
1412        assert_eq!(info.description(), None);
1413    }
1414
1415    #[test]
1416    fn info_with_admins_is_an_owner_and_admins() {
1417        let info = parse_info(info_response(Some(InfoResult::ContractGroupInfo(
1418            ContractGroupInfoProto {
1419                owner_id: vec![1; 32],
1420                admin_ids: vec![vec![3; 32], vec![2; 32]],
1421                name: None,
1422                description: Some("shared".to_string()),
1423            },
1424        ))))
1425        .expect("parse")
1426        .expect("present");
1427
1428        assert_eq!(
1429            info.owner(),
1430            &ContractGroupOwner::OwnerAndAdmins {
1431                owner: Identifier::new([1; 32]),
1432                admins: BTreeSet::from([Identifier::new([2; 32]), Identifier::new([3; 32])]),
1433            }
1434        );
1435        assert_eq!(info.description(), Some("shared"));
1436    }
1437
1438    #[test]
1439    fn info_absent_group_and_bad_owner_and_proof() {
1440        assert_eq!(parse_info(info_response(None)).expect("parse"), None);
1441
1442        let err = parse_info(info_response(Some(InfoResult::ContractGroupInfo(
1443            ContractGroupInfoProto {
1444                owner_id: vec![1; 4],
1445                admin_ids: vec![],
1446                name: None,
1447                description: None,
1448            },
1449        ))))
1450        .unwrap_err();
1451        assert!(
1452            matches!(err, Error::ProtocolError { .. }),
1453            "a malformed response field is the node's error: {err:?}"
1454        );
1455
1456        let err = parse_info(info_response(Some(InfoResult::Proof(
1457            platform::Proof::default(),
1458        ))))
1459        .unwrap_err();
1460        assert!(
1461            matches!(err, Error::ResponseDecodeError { .. }),
1462            "got: {err:?}"
1463        );
1464    }
1465
1466    fn members_request(members: Members) -> platform::GetContractGroupMembersRequest {
1467        platform::GetContractGroupMembersRequest {
1468            version: Some(MembersRequestVersion::V0(
1469                GetContractGroupMembersRequestV0 {
1470                    contract_group_id: vec![1; 32],
1471                    members: Some(members),
1472                    limit: None,
1473                    prove: false,
1474                },
1475            )),
1476        }
1477    }
1478
1479    fn members_response(
1480        result: Option<MembersResult>,
1481    ) -> platform::GetContractGroupMembersResponse {
1482        platform::GetContractGroupMembersResponse {
1483            version: Some(MembersVersion::V0(GetContractGroupMembersResponseV0 {
1484                result,
1485                metadata: Some(ResponseMetadata::default()),
1486            })),
1487        }
1488    }
1489
1490    fn parse_members(
1491        request: platform::GetContractGroupMembersRequest,
1492        response: platform::GetContractGroupMembersResponse,
1493    ) -> Result<Option<ContractGroupMembersPage>, Error> {
1494        <ContractGroupMembersPage as FromUnproved<platform::GetContractGroupMembersRequest>>::maybe_from_unproved(
1495            request,
1496            response,
1497            Network::Testnet,
1498            PlatformVersion::latest(),
1499        )
1500    }
1501
1502    #[test]
1503    fn members_page_of_the_requested_kind_is_read() {
1504        let page = parse_members(
1505            members_request(Members::Tokens(TokenMembersQuery { start_after: None })),
1506            members_response(Some(MembersResult::Tokens(TokenMembers {
1507                tokens: vec![ContractGroupTokenMember {
1508                    contract_id: vec![5; 32],
1509                    token_position: 2,
1510                }],
1511            }))),
1512        )
1513        .expect("parse")
1514        .expect("present");
1515
1516        assert_eq!(
1517            page,
1518            ContractGroupMembersPage::Tokens(vec![(Identifier::new([5; 32]), 2)])
1519        );
1520        assert_eq!(
1521            page.next_query(),
1522            Some(ContractGroupMembersQuery::Tokens {
1523                start_after: Some((Identifier::new([5; 32]), 2)),
1524            })
1525        );
1526    }
1527
1528    #[test]
1529    fn members_page_of_another_kind_is_rejected() {
1530        let err = parse_members(
1531            members_request(Members::Contracts(ContractMembersQuery {
1532                start_after: None,
1533            })),
1534            members_response(Some(MembersResult::Tokens(TokenMembers::default()))),
1535        )
1536        .unwrap_err();
1537        assert!(
1538            matches!(err, Error::ResponseDecodeError { .. }),
1539            "got: {err:?}"
1540        );
1541
1542        let err = parse_members(
1543            members_request(Members::Contracts(ContractMembersQuery {
1544                start_after: None,
1545            })),
1546            members_response(Some(MembersResult::Proof(platform::Proof::default()))),
1547        )
1548        .unwrap_err();
1549        assert!(
1550            matches!(err, Error::ResponseDecodeError { .. }),
1551            "got: {err:?}"
1552        );
1553    }
1554
1555    #[test]
1556    fn members_page_contracts_are_read_in_order() {
1557        let page = parse_members(
1558            members_request(Members::Contracts(ContractMembersQuery {
1559                start_after: None,
1560            })),
1561            members_response(Some(MembersResult::Contracts(ContractMembers {
1562                contract_ids: vec![vec![1; 32], vec![2; 32]],
1563            }))),
1564        )
1565        .expect("parse")
1566        .expect("present");
1567
1568        assert_eq!(
1569            page,
1570            ContractGroupMembersPage::Contracts(vec![
1571                Identifier::new([1; 32]),
1572                Identifier::new([2; 32])
1573            ])
1574        );
1575    }
1576
1577    #[test]
1578    fn memberships_are_read_into_sets() {
1579        let response = platform::GetContractGroupsForContractResponse {
1580            version: Some(MembershipsVersion::V0(
1581                GetContractGroupsForContractResponseV0 {
1582                    result: Some(MembershipsResult::ContractGroupMemberships(
1583                        ContractGroupMemberships {
1584                            contract_group_ids: vec![vec![1; 32]],
1585                            document_types: vec![DocumentTypeMemberships {
1586                                document_type_name: "note".to_string(),
1587                                contract_group_ids: vec![vec![2; 32], vec![1; 32]],
1588                            }],
1589                            tokens: vec![TokenMemberships {
1590                                token_position: 7,
1591                                contract_group_ids: vec![vec![3; 32]],
1592                            }],
1593                        },
1594                    )),
1595                    metadata: Some(ResponseMetadata::default()),
1596                },
1597            )),
1598        };
1599
1600        let memberships = <ContractGroupMembershipsForContract as FromUnproved<
1601            platform::GetContractGroupsForContractRequest,
1602        >>::maybe_from_unproved(
1603            platform::GetContractGroupsForContractRequest::default(),
1604            response,
1605            Network::Testnet,
1606            PlatformVersion::latest(),
1607        )
1608        .expect("parse")
1609        .expect("present");
1610
1611        assert_eq!(
1612            memberships.contract,
1613            BTreeSet::from([Identifier::new([1; 32])])
1614        );
1615        assert_eq!(
1616            memberships.document_types["note"],
1617            BTreeSet::from([Identifier::new([1; 32]), Identifier::new([2; 32])])
1618        );
1619        assert_eq!(
1620            memberships.tokens[&7],
1621            BTreeSet::from([Identifier::new([3; 32])])
1622        );
1623        assert_eq!(
1624            memberships.all_contract_group_ids(),
1625            BTreeSet::from([
1626                Identifier::new([1; 32]),
1627                Identifier::new([2; 32]),
1628                Identifier::new([3; 32])
1629            ])
1630        );
1631    }
1632}
1633
1634#[cfg(test)]
1635mod contract_moderation_tests {
1636    use super::*;
1637    use crate::types::contract_moderation::ContractModerationReason;
1638    use dapi_grpc::platform::v0::get_contract_document_removals_request::get_contract_document_removals_request_v0::Selection as RemovalsSelection;
1639    use dapi_grpc::platform::v0::get_contract_document_removals_request::{
1640        DocumentIds as RemovalsDocumentIds, GetContractDocumentRemovalsRequestV0,
1641        Page as RemovalsPage, Version as RemovalsRequestVersion,
1642    };
1643    use dapi_grpc::platform::v0::get_contract_document_removals_response::{
1644        get_contract_document_removals_response_v0::Result as RemovalsResult,
1645        ContractDocumentRemoval as ContractDocumentRemovalProto,
1646        ContractDocumentRemovals as ContractDocumentRemovalsProto,
1647        GetContractDocumentRemovalsResponseV0, Version as RemovalsResponseVersion,
1648    };
1649    use dapi_grpc::platform::v0::get_contract_moderation_status_request::{
1650        GetContractModerationStatusRequestV0, Version as StatusRequestVersion,
1651    };
1652    use dapi_grpc::platform::v0::get_contract_moderation_status_response::{
1653        get_contract_moderation_status_response_v0::Result as StatusResult,
1654        ContractModerationStatus as ContractModerationStatusProto,
1655        GetContractModerationStatusResponseV0, Version as StatusResponseVersion,
1656    };
1657    use dapi_grpc::platform::v0::get_contract_team_action_signers_request::{
1658        GetContractTeamActionSignersRequestV0, Version as SignersRequestVersion,
1659    };
1660    use dapi_grpc::platform::v0::get_contract_team_action_signers_response::{
1661        get_contract_team_action_signers_response_v0::Result as SignersResult,
1662        ContractTeamActionSigners as ContractTeamActionSignersProto,
1663        GetContractTeamActionSignersResponseV0, Version as SignersResponseVersion,
1664    };
1665    use dapi_grpc::platform::v0::get_contract_team_actions_request::{
1666        ActionStatus, GetContractTeamActionsRequestV0, Version as TeamActionsRequestVersion,
1667    };
1668    use dapi_grpc::platform::v0::get_contract_team_actions_response::{
1669        contract_team_action, get_contract_team_actions_response_v0::Result as TeamActionsResult,
1670        ContractTeamAction as ContractTeamActionProto,
1671        ContractTeamActions as ContractTeamActionsProto, DeleteSettledDocument,
1672        GetContractTeamActionsResponseV0, Version as TeamActionsResponseVersion,
1673    };
1674    use dapi_grpc::platform::v0::get_contract_moderation_action_counts_request::{
1675        GetContractModerationActionCountsRequestV0, Version as CountsRequestVersion,
1676    };
1677    use dapi_grpc::platform::v0::get_contract_moderation_action_counts_response::{
1678        ContractModerationActionCount, ContractModerationActionCounts as CountsProto,
1679        GetContractModerationActionCountsResponseV0, Version as CountsResponseVersion,
1680    };
1681    use dapi_grpc::platform::v0::ContractModerationReason as ContractModerationReasonProto;
1682    use dapi_grpc::platform::v0::ContractWarning as ContractWarningProto;
1683    use dapi_grpc::platform::v0::ResponseMetadata;
1684    use dpp::dashcore::Network;
1685    use dpp::version::PlatformVersion;
1686
1687    const BANLIST: i32 = 1;
1688    const SUSPENSIONS: i32 = 2;
1689    const WARNINGS: i32 = 3;
1690
1691    fn status(
1692        requested: Vec<i32>,
1693        response: ContractModerationStatusProto,
1694    ) -> Result<Option<ContractModerationListStatuses>, Error> {
1695        let request = platform::GetContractModerationStatusRequest {
1696            version: Some(StatusRequestVersion::V0(
1697                GetContractModerationStatusRequestV0 {
1698                    contract_id: vec![1; 32],
1699                    identity_id: vec![2; 32],
1700                    lists: requested,
1701                    prove: false,
1702                },
1703            )),
1704        };
1705        let response = platform::GetContractModerationStatusResponse {
1706            version: Some(StatusResponseVersion::V0(
1707                GetContractModerationStatusResponseV0 {
1708                    result: Some(StatusResult::Status(response)),
1709                    metadata: Some(ResponseMetadata::default()),
1710                },
1711            )),
1712        };
1713        ContractModerationListStatuses::maybe_from_unproved_with_metadata(
1714            request,
1715            response,
1716            Network::Testnet,
1717            PlatformVersion::latest(),
1718        )
1719        .map(|(status, _)| status)
1720    }
1721
1722    #[test]
1723    fn should_report_the_lists_the_response_covers() {
1724        let statuses = status(
1725            vec![BANLIST, SUSPENSIONS],
1726            ContractModerationStatusProto {
1727                banned: Some(true),
1728                suspended_until: Some(7),
1729                lists: vec![BANLIST, SUSPENSIONS],
1730                ban_reason: Some(ContractModerationReasonProto {
1731                    code: None,
1732                    text: "spam".to_string(),
1733                    documents: vec![],
1734                    reason_document_id: None,
1735                }),
1736                suspension_reason: Some(ContractModerationReasonProto {
1737                    code: Some(9),
1738                    text: "flooding".to_string(),
1739                    documents: vec![],
1740                    reason_document_id: None,
1741                }),
1742                warnings: vec![],
1743            },
1744        )
1745        .expect("expected the status to convert")
1746        .expect("expected a status");
1747
1748        assert_eq!(statuses.banned(), Some(true));
1749        assert_eq!(statuses.suspended_until(), Some(Some(7)));
1750        // The warning list was not asked for, so nothing is said about it.
1751        assert_eq!(statuses.warnings(), None);
1752        assert_eq!(
1753            statuses.ban().flatten().map(|ban| &ban.reason),
1754            Some(&ContractModerationReason::from_text("spam"))
1755        );
1756        assert_eq!(
1757            statuses.suspension().flatten().map(|entry| &entry.reason),
1758            Some(&ContractModerationReason {
1759                code: Some(9),
1760                text: "flooding".to_string(),
1761                documents: vec![],
1762                reason_document_id: None,
1763            })
1764        );
1765    }
1766
1767    #[test]
1768    fn should_refuse_an_entry_without_a_reason_or_with_a_code_past_u16() {
1769        let no_reason = status(
1770            vec![BANLIST],
1771            ContractModerationStatusProto {
1772                banned: Some(true),
1773                lists: vec![BANLIST],
1774                ..Default::default()
1775            },
1776        );
1777        assert!(matches!(no_reason, Err(Error::ResponseDecodeError { .. })));
1778
1779        let wide_code = status(
1780            vec![SUSPENSIONS],
1781            ContractModerationStatusProto {
1782                suspended_until: Some(7),
1783                lists: vec![SUSPENSIONS],
1784                suspension_reason: Some(ContractModerationReasonProto {
1785                    code: Some(u16::MAX as u32 + 1),
1786                    text: String::new(),
1787                    documents: vec![],
1788                    reason_document_id: None,
1789                }),
1790                ..Default::default()
1791            },
1792        );
1793        assert!(matches!(wide_code, Err(Error::ResponseDecodeError { .. })));
1794
1795        // A clean identity has no reason to give.
1796        let clean = status(
1797            vec![BANLIST, SUSPENSIONS],
1798            ContractModerationStatusProto {
1799                banned: Some(false),
1800                lists: vec![BANLIST, SUSPENSIONS],
1801                ..Default::default()
1802            },
1803        )
1804        .expect("expected the status to convert")
1805        .expect("expected a status");
1806        assert_eq!(clean.ban(), Some(None));
1807        assert_eq!(clean.suspension(), Some(None));
1808    }
1809
1810    #[test]
1811    fn should_read_the_warnings_when_the_warning_list_was_asked_for() {
1812        let warning = |warned_at: u64, text: &str| ContractWarningProto {
1813            warned_at,
1814            reason: Some(ContractModerationReasonProto {
1815                code: None,
1816                text: text.to_string(),
1817                documents: vec![],
1818                reason_document_id: None,
1819            }),
1820        };
1821        let warned = status(
1822            vec![WARNINGS],
1823            ContractModerationStatusProto {
1824                lists: vec![WARNINGS],
1825                warnings: vec![warning(5, "first strike"), warning(6, "second strike")],
1826                ..Default::default()
1827            },
1828        )
1829        .expect("expected the status to convert")
1830        .expect("expected a status");
1831        let warnings = warned.warnings().expect("expected the warning list");
1832        assert_eq!(warnings.len(), 2);
1833        assert_eq!(warnings[1].warned_at, 6);
1834        assert_eq!(warnings[1].reason.text, "second strike");
1835        assert_eq!(warned.banned(), None);
1836
1837        // No warnings on a list that was read: none, not unknown.
1838        let clean = status(
1839            vec![WARNINGS],
1840            ContractModerationStatusProto {
1841                lists: vec![WARNINGS],
1842                ..Default::default()
1843            },
1844        )
1845        .expect("expected the status to convert")
1846        .expect("expected a status");
1847        assert_eq!(clean.warnings(), Some(&[][..]));
1848
1849        // A warning without a reason is refused like an entry without one.
1850        let no_reason = status(
1851            vec![WARNINGS],
1852            ContractModerationStatusProto {
1853                lists: vec![WARNINGS],
1854                warnings: vec![ContractWarningProto {
1855                    warned_at: 5,
1856                    reason: None,
1857                }],
1858                ..Default::default()
1859            },
1860        );
1861        assert!(matches!(no_reason, Err(Error::ResponseDecodeError { .. })));
1862    }
1863
1864    #[test]
1865    fn should_refuse_a_status_that_does_not_cover_a_list_asked_for() {
1866        // The banlist was asked for and the response does not say it was read: reporting "not
1867        // banned" would be a guess.
1868        let uncovered = status(
1869            vec![BANLIST, SUSPENSIONS],
1870            ContractModerationStatusProto {
1871                lists: vec![SUSPENSIONS],
1872                ..Default::default()
1873            },
1874        );
1875        assert!(matches!(uncovered, Err(Error::ResponseDecodeError { .. })));
1876
1877        let covered_but_unset = status(
1878            vec![BANLIST],
1879            ContractModerationStatusProto {
1880                lists: vec![BANLIST],
1881                ..Default::default()
1882            },
1883        );
1884        assert!(matches!(
1885            covered_but_unset,
1886            Err(Error::ResponseDecodeError { .. })
1887        ));
1888    }
1889
1890    fn removals(
1891        selection: Option<RemovalsSelection>,
1892        result: Option<RemovalsResult>,
1893    ) -> Result<Option<ContractDocumentRemovals>, Error> {
1894        let request = platform::GetContractDocumentRemovalsRequest {
1895            version: Some(RemovalsRequestVersion::V0(
1896                GetContractDocumentRemovalsRequestV0 {
1897                    contract_id: vec![1; 32],
1898                    document_type_name: "post".to_string(),
1899                    selection,
1900                    prove: false,
1901                },
1902            )),
1903        };
1904        let response = platform::GetContractDocumentRemovalsResponse {
1905            version: Some(RemovalsResponseVersion::V0(
1906                GetContractDocumentRemovalsResponseV0 {
1907                    result,
1908                    metadata: Some(ResponseMetadata::default()),
1909                },
1910            )),
1911        };
1912        ContractDocumentRemovals::maybe_from_unproved_with_metadata(
1913            request,
1914            response,
1915            Network::Testnet,
1916            PlatformVersion::latest(),
1917        )
1918        .map(|(removals, _)| removals)
1919    }
1920
1921    fn removal_proto(seed: u8) -> ContractDocumentRemovalProto {
1922        ContractDocumentRemovalProto {
1923            document_id: vec![seed; 32],
1924            document_owner_id: vec![seed + 0x10; 32],
1925            moderator_id: vec![0x77; 32],
1926            removed_at: 1_000 + u64::from(seed),
1927            document_hash: vec![seed + 0x20; 32],
1928            restoration: None,
1929            reason: Some(ContractModerationReasonProto {
1930                code: None,
1931                text: "spam".to_string(),
1932                documents: vec![],
1933                reason_document_id: None,
1934            }),
1935            kept_fields: Default::default(),
1936        }
1937    }
1938
1939    #[test]
1940    fn should_read_the_removals_the_request_asked_for() {
1941        let page = Some(RemovalsSelection::Page(RemovalsPage {
1942            start_after: None,
1943            limit: Some(2),
1944        }));
1945        let read = removals(
1946            page.clone(),
1947            Some(RemovalsResult::Removals(ContractDocumentRemovalsProto {
1948                removals: vec![removal_proto(1), removal_proto(2)],
1949            })),
1950        )
1951        .expect("expected the removals to convert")
1952        .expect("expected removals");
1953        assert_eq!(read.removals().len(), 2);
1954        assert_eq!(read.removals()[0].document_id, Identifier::from([1; 32]));
1955        assert_eq!(
1956            read.removals()[1].removal.moderator_id,
1957            Identifier::from([0x77; 32])
1958        );
1959
1960        // A page of more records than the request allowed, and a request that selects nothing.
1961        let over_limit = removals(
1962            Some(RemovalsSelection::Page(RemovalsPage {
1963                start_after: None,
1964                limit: Some(1),
1965            })),
1966            Some(RemovalsResult::Removals(ContractDocumentRemovalsProto {
1967                removals: vec![removal_proto(1), removal_proto(2)],
1968            })),
1969        );
1970        assert!(matches!(over_limit, Err(Error::ResponseDecodeError { .. })));
1971        assert!(matches!(
1972            removals(None, None),
1973            Err(Error::RequestError { .. })
1974        ));
1975
1976        // The unproved path never reads a proof.
1977        let proved = removals(page, Some(RemovalsResult::Proof(Default::default())));
1978        assert!(matches!(proved, Err(Error::ResponseDecodeError { .. })));
1979    }
1980
1981    #[test]
1982    fn should_refuse_a_removal_of_a_document_the_request_did_not_name() {
1983        let by_ids = Some(RemovalsSelection::DocumentIds(RemovalsDocumentIds {
1984            document_ids: vec![vec![1; 32], vec![3; 32]],
1985        }));
1986        let unasked = removals(
1987            by_ids.clone(),
1988            Some(RemovalsResult::Removals(ContractDocumentRemovalsProto {
1989                removals: vec![removal_proto(2)],
1990            })),
1991        );
1992        assert!(matches!(unasked, Err(Error::ResponseDecodeError { .. })));
1993
1994        // An id with no record is simply left out, which is an answer.
1995        let partial = removals(
1996            by_ids,
1997            Some(RemovalsResult::Removals(ContractDocumentRemovalsProto {
1998                removals: vec![removal_proto(1)],
1999            })),
2000        )
2001        .expect("expected the removals to convert")
2002        .expect("expected removals");
2003        assert_eq!(partial.removals().len(), 1);
2004    }
2005
2006    fn team_actions(
2007        count: Option<u32>,
2008        result: Option<TeamActionsResult>,
2009    ) -> Result<Option<ContractTeamActions>, Error> {
2010        let request = platform::GetContractTeamActionsRequest {
2011            version: Some(TeamActionsRequestVersion::V0(
2012                GetContractTeamActionsRequestV0 {
2013                    contract_id: vec![1; 32],
2014                    status: ActionStatus::Active as i32,
2015                    start_at_action_id: None,
2016                    count,
2017                    prove: false,
2018                },
2019            )),
2020        };
2021        let response = platform::GetContractTeamActionsResponse {
2022            version: Some(TeamActionsResponseVersion::V0(
2023                GetContractTeamActionsResponseV0 {
2024                    result,
2025                    metadata: Some(ResponseMetadata::default()),
2026                },
2027            )),
2028        };
2029        ContractTeamActions::maybe_from_unproved_with_metadata(
2030            request,
2031            response,
2032            Network::Testnet,
2033            PlatformVersion::latest(),
2034        )
2035        .map(|(actions, _)| actions)
2036    }
2037
2038    fn team_action_proto(seed: u8) -> ContractTeamActionProto {
2039        ContractTeamActionProto {
2040            action_id: vec![seed; 32],
2041            proposer_id: vec![0x77; 32],
2042            proposed_at: 1_000,
2043            event: Some(contract_team_action::Event::DeleteSettledDocument(
2044                DeleteSettledDocument {
2045                    document_type_name: "post".to_string(),
2046                    document_id: vec![seed + 0x10; 32],
2047                    document_last_modified_at: 10,
2048                    document_revision: Some(2),
2049                    reason: Some(ContractModerationReasonProto {
2050                        code: None,
2051                        text: "doxxing".to_string(),
2052                        documents: vec![],
2053                        reason_document_id: None,
2054                    }),
2055                },
2056            )),
2057            approval_count: u32::from(seed),
2058        }
2059    }
2060
2061    #[test]
2062    fn should_read_the_team_actions_the_request_asked_for() {
2063        let read = team_actions(
2064            Some(2),
2065            Some(TeamActionsResult::Actions(ContractTeamActionsProto {
2066                actions: vec![team_action_proto(1), team_action_proto(2)],
2067            })),
2068        )
2069        .expect("expected the team actions to convert")
2070        .expect("expected team actions");
2071        assert_eq!(read.actions().len(), 2);
2072        assert_eq!(read.actions()[0].action_id, Identifier::from([1; 32]));
2073        assert_eq!(
2074            read.actions()[0].action.proposer_id,
2075            Identifier::from([0x77; 32])
2076        );
2077
2078        // More actions than the page holds, a missing result and a proof are refused or empty
2079        team_actions(
2080            Some(1),
2081            Some(TeamActionsResult::Actions(ContractTeamActionsProto {
2082                actions: vec![team_action_proto(1), team_action_proto(2)],
2083            })),
2084        )
2085        .expect_err("expected a page too long to be refused");
2086        assert_eq!(team_actions(None, None).expect("expected no error"), None);
2087        team_actions(None, Some(TeamActionsResult::Proof(Default::default())))
2088            .expect_err("expected a proof to be refused");
2089    }
2090
2091    #[test]
2092    fn should_read_the_signers_of_a_team_action() {
2093        let request = platform::GetContractTeamActionSignersRequest {
2094            version: Some(SignersRequestVersion::V0(
2095                GetContractTeamActionSignersRequestV0 {
2096                    contract_id: vec![1; 32],
2097                    status: ActionStatus::Closed as i32,
2098                    action_id: vec![2; 32],
2099                    prove: false,
2100                },
2101            )),
2102        };
2103        let response = platform::GetContractTeamActionSignersResponse {
2104            version: Some(SignersResponseVersion::V0(
2105                GetContractTeamActionSignersResponseV0 {
2106                    result: Some(SignersResult::Signers(ContractTeamActionSignersProto {
2107                        signer_ids: vec![vec![3; 32], vec![4; 32]],
2108                    })),
2109                    metadata: Some(ResponseMetadata::default()),
2110                },
2111            )),
2112        };
2113        let (signers, _) = ContractTeamActionSigners::maybe_from_unproved_with_metadata(
2114            request,
2115            response,
2116            Network::Testnet,
2117            PlatformVersion::latest(),
2118        )
2119        .expect("expected the signers to convert");
2120        assert_eq!(
2121            signers.expect("expected signers").signers(),
2122            &[Identifier::from([3; 32]), Identifier::from([4; 32])]
2123        );
2124    }
2125
2126    #[test]
2127    fn should_read_the_moderation_action_counts_of_a_contract() {
2128        let counts = |result: Option<ModerationActionCountsResult>| {
2129            ContractModerationActionCounts::maybe_from_unproved_with_metadata(
2130                platform::GetContractModerationActionCountsRequest {
2131                    version: Some(CountsRequestVersion::V0(
2132                        GetContractModerationActionCountsRequestV0 {
2133                            contract_id: vec![1; 32],
2134                            prove: false,
2135                        },
2136                    )),
2137                },
2138                platform::GetContractModerationActionCountsResponse {
2139                    version: Some(CountsResponseVersion::V0(
2140                        GetContractModerationActionCountsResponseV0 {
2141                            result,
2142                            metadata: Some(ResponseMetadata::default()),
2143                        },
2144                    )),
2145                },
2146                Network::Testnet,
2147                PlatformVersion::latest(),
2148            )
2149            .map(|(counts, _)| counts)
2150        };
2151        assert_eq!(
2152            counts(Some(ModerationActionCountsResult::Counts(CountsProto {
2153                counts: vec![ContractModerationActionCount {
2154                    identity_id: vec![3; 32],
2155                    count: 7,
2156                }],
2157            })))
2158            .expect("expected the counts to convert")
2159            .expect("expected counts")
2160            .counts(),
2161            &BTreeMap::from([(Identifier::from([3; 32]), 7)])
2162        );
2163        assert_eq!(counts(None).expect("expected no error"), None);
2164        counts(Some(
2165            ModerationActionCountsResult::Proof(Default::default()),
2166        ))
2167        .expect_err("expected a proof to be refused");
2168    }
2169}