Skip to main content

drive_proof_verifier/proof/
contract_moderation.rs

1//! Proof verification of the contract moderation queries and of the fee pots query.
2
3use crate::error::MapGroveDbError;
4use crate::types::contract_moderation::{
5    entries_query_from_request, identifier_from_request, lists_from_request,
6    removals_query_from_request, team_action_status_from_request, team_actions_query_from_request,
7    ContractDocumentRemovals, ContractFeePots, ContractModerationActionCounts,
8    ContractModerationEntries, ContractModerationListStatuses, ContractTeamActionSigners,
9    ContractTeamActions, CONTRACT_FEE_POTS_QUERIED,
10};
11use crate::verify::{supported_grovedb_proof_bytes, verify_tenderdash_proof};
12use crate::{ContextProvider, Error, FromProof};
13use dapi_grpc::platform::v0::{
14    get_contract_document_removals_request, get_contract_fee_pots_request,
15    get_contract_moderation_action_counts_request, get_contract_moderation_entries_request,
16    get_contract_moderation_status_request, get_contract_team_action_signers_request,
17    get_contract_team_actions_request, GetContractDocumentRemovalsRequest,
18    GetContractDocumentRemovalsResponse, GetContractFeePotsRequest, GetContractFeePotsResponse,
19    GetContractModerationActionCountsRequest, GetContractModerationActionCountsResponse,
20    GetContractModerationEntriesRequest, GetContractModerationEntriesResponse,
21    GetContractModerationStatusRequest, GetContractModerationStatusResponse,
22    GetContractTeamActionSignersRequest, GetContractTeamActionSignersResponse,
23    GetContractTeamActionsRequest, GetContractTeamActionsResponse, Proof, ResponseMetadata,
24};
25use dapi_grpc::platform::VersionedGrpcResponse;
26use dpp::dashcore::Network;
27use dpp::version::PlatformVersion;
28use drive::drive::Drive;
29
30impl FromProof<GetContractModerationStatusRequest> for ContractModerationListStatuses {
31    type Request = GetContractModerationStatusRequest;
32    type Response = GetContractModerationStatusResponse;
33
34    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
35        request: I,
36        response: O,
37        _network: Network,
38        platform_version: &PlatformVersion,
39        provider: &'a dyn ContextProvider,
40    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
41    where
42        Self: Sized + 'a,
43    {
44        let request: Self::Request = request.into();
45        let response: Self::Response = response.into();
46
47        let get_contract_moderation_status_request::Version::V0(v0) =
48            request.version.ok_or(Error::EmptyVersion)?;
49        let contract_id = identifier_from_request(&v0.contract_id, "contract_id")?;
50        let identity_id = identifier_from_request(&v0.identity_id, "identity_id")?;
51        let lists = lists_from_request(&v0.lists)?;
52
53        let metadata = response
54            .metadata()
55            .or(Err(Error::EmptyResponseMetadata))?
56            .clone();
57        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
58
59        let (root_hash, statuses) = Drive::verify_contract_moderation_status(
60            supported_grovedb_proof_bytes(&proof)?,
61            contract_id,
62            identity_id,
63            &lists,
64            false,
65            platform_version,
66        )
67        .map_drive_error(&proof, &metadata)?;
68
69        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
70
71        // An absent entry is a status too: the identity is not on that list. Only the lists
72        // queried were proved, and the verifier reports only those.
73        Ok((Some(statuses), metadata, proof))
74    }
75}
76
77impl FromProof<GetContractModerationEntriesRequest> for ContractModerationEntries {
78    type Request = GetContractModerationEntriesRequest;
79    type Response = GetContractModerationEntriesResponse;
80
81    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
82        request: I,
83        response: O,
84        _network: Network,
85        platform_version: &PlatformVersion,
86        provider: &'a dyn ContextProvider,
87    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
88    where
89        Self: Sized + 'a,
90    {
91        let request: Self::Request = request.into();
92        let response: Self::Response = response.into();
93
94        let get_contract_moderation_entries_request::Version::V0(v0) =
95            request.version.ok_or(Error::EmptyVersion)?;
96        let contract_id = identifier_from_request(&v0.contract_id, "contract_id")?;
97        let query = entries_query_from_request(
98            v0.list,
99            v0.start_after.as_deref(),
100            v0.limit,
101            platform_version,
102        )?;
103
104        let metadata = response
105            .metadata()
106            .or(Err(Error::EmptyResponseMetadata))?
107            .clone();
108        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
109
110        let (root_hash, entries) = Drive::verify_contract_moderation_entries(
111            supported_grovedb_proof_bytes(&proof)?,
112            contract_id,
113            &query,
114            platform_version,
115        )
116        .map_drive_error(&proof, &metadata)?;
117
118        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
119
120        // An empty list proves as an empty page, so the page itself is always the answer.
121        Ok((Some(ContractModerationEntries(entries)), metadata, proof))
122    }
123}
124
125impl FromProof<GetContractFeePotsRequest> for ContractFeePots {
126    type Request = GetContractFeePotsRequest;
127    type Response = GetContractFeePotsResponse;
128
129    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
130        request: I,
131        response: O,
132        _network: Network,
133        platform_version: &PlatformVersion,
134        provider: &'a dyn ContextProvider,
135    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
136    where
137        Self: Sized + 'a,
138    {
139        let request: Self::Request = request.into();
140        let response: Self::Response = response.into();
141
142        let get_contract_fee_pots_request::Version::V0(v0) =
143            request.version.ok_or(Error::EmptyVersion)?;
144        let contract_id = identifier_from_request(&v0.contract_id, "contract_id")?;
145
146        let metadata = response
147            .metadata()
148            .or(Err(Error::EmptyResponseMetadata))?
149            .clone();
150        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
151
152        let (root_hash, pots) = Drive::verify_contract_fee_pots(
153            supported_grovedb_proof_bytes(&proof)?,
154            contract_id,
155            &CONTRACT_FEE_POTS_QUERIED,
156            false,
157            platform_version,
158        )
159        .map_drive_error(&proof, &metadata)?;
160
161        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
162
163        // A pot nothing was ever paid into proves as absent and reads as zero credits, so the
164        // pots themselves are always the answer. The proof says nothing about the contract: a
165        // node refuses the query for a contract it does not hold before it proves anything.
166        Ok((Some(pots), metadata, proof))
167    }
168}
169
170impl FromProof<GetContractDocumentRemovalsRequest> for ContractDocumentRemovals {
171    type Request = GetContractDocumentRemovalsRequest;
172    type Response = GetContractDocumentRemovalsResponse;
173
174    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
175        request: I,
176        response: O,
177        _network: Network,
178        platform_version: &PlatformVersion,
179        provider: &'a dyn ContextProvider,
180    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
181    where
182        Self: Sized + 'a,
183    {
184        let request: Self::Request = request.into();
185        let response: Self::Response = response.into();
186
187        let get_contract_document_removals_request::Version::V0(v0) =
188            request.version.ok_or(Error::EmptyVersion)?;
189        let contract_id = identifier_from_request(&v0.contract_id, "contract_id")?;
190        let query =
191            removals_query_from_request(v0.document_type_name, v0.selection, platform_version)?;
192
193        let metadata = response
194            .metadata()
195            .or(Err(Error::EmptyResponseMetadata))?
196            .clone();
197        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
198
199        let (root_hash, removals) = Drive::verify_contract_document_removals(
200            supported_grovedb_proof_bytes(&proof)?,
201            contract_id,
202            &query,
203            false,
204            platform_version,
205        )
206        .map_drive_error(&proof, &metadata)?;
207
208        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
209
210        // A document type nobody moderated proves as no records at all, and an id with no
211        // record is proved absent rather than missing, so the records are always the answer.
212        Ok((Some(ContractDocumentRemovals(removals)), metadata, proof))
213    }
214}
215
216impl FromProof<GetContractTeamActionsRequest> for ContractTeamActions {
217    type Request = GetContractTeamActionsRequest;
218    type Response = GetContractTeamActionsResponse;
219
220    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
221        request: I,
222        response: O,
223        _network: Network,
224        platform_version: &PlatformVersion,
225        provider: &'a dyn ContextProvider,
226    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
227    where
228        Self: Sized + 'a,
229    {
230        let request: Self::Request = request.into();
231        let response: Self::Response = response.into();
232
233        let get_contract_team_actions_request::Version::V0(v0) =
234            request.version.ok_or(Error::EmptyVersion)?;
235        let contract_id = identifier_from_request(&v0.contract_id, "contract_id")?;
236        let query = team_actions_query_from_request(
237            v0.status,
238            v0.start_at_action_id,
239            v0.count,
240            platform_version,
241        )?;
242
243        let metadata = response
244            .metadata()
245            .or(Err(Error::EmptyResponseMetadata))?
246            .clone();
247        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
248
249        let (root_hash, actions) = Drive::verify_contract_team_actions(
250            supported_grovedb_proof_bytes(&proof)?,
251            contract_id,
252            &query,
253            false,
254            platform_version,
255        )
256        .map_drive_error(&proof, &metadata)?;
257
258        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
259
260        // A page past the last action proves as no actions at all, so the actions are always
261        // the answer.
262        Ok((Some(ContractTeamActions(actions)), metadata, proof))
263    }
264}
265
266impl FromProof<GetContractTeamActionSignersRequest> for ContractTeamActionSigners {
267    type Request = GetContractTeamActionSignersRequest;
268    type Response = GetContractTeamActionSignersResponse;
269
270    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
271        request: I,
272        response: O,
273        _network: Network,
274        platform_version: &PlatformVersion,
275        provider: &'a dyn ContextProvider,
276    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
277    where
278        Self: Sized + 'a,
279    {
280        let request: Self::Request = request.into();
281        let response: Self::Response = response.into();
282
283        let get_contract_team_action_signers_request::Version::V0(v0) =
284            request.version.ok_or(Error::EmptyVersion)?;
285        let contract_id = identifier_from_request(&v0.contract_id, "contract_id")?;
286        let action_id = identifier_from_request(&v0.action_id, "action_id")?;
287        let status = team_action_status_from_request(v0.status)?;
288
289        let metadata = response
290            .metadata()
291            .or(Err(Error::EmptyResponseMetadata))?
292            .clone();
293        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
294
295        let (root_hash, signers) = Drive::verify_contract_team_action_signers(
296            supported_grovedb_proof_bytes(&proof)?,
297            contract_id,
298            status,
299            action_id,
300            false,
301            platform_version,
302        )
303        .map_drive_error(&proof, &metadata)?;
304
305        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
306
307        // An action that is not there with that status proves as no approvals, so the approvals
308        // are always the answer.
309        Ok((Some(ContractTeamActionSigners(signers)), metadata, proof))
310    }
311}
312
313impl FromProof<GetContractModerationActionCountsRequest> for ContractModerationActionCounts {
314    type Request = GetContractModerationActionCountsRequest;
315    type Response = GetContractModerationActionCountsResponse;
316
317    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
318        request: I,
319        response: O,
320        _network: Network,
321        platform_version: &PlatformVersion,
322        provider: &'a dyn ContextProvider,
323    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
324    where
325        Self: Sized + 'a,
326    {
327        let request: Self::Request = request.into();
328        let response: Self::Response = response.into();
329
330        let get_contract_moderation_action_counts_request::Version::V0(v0) =
331            request.version.ok_or(Error::EmptyVersion)?;
332        let contract_id = identifier_from_request(&v0.contract_id, "contract_id")?;
333
334        let metadata = response
335            .metadata()
336            .or(Err(Error::EmptyResponseMetadata))?
337            .clone();
338        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
339
340        let (root_hash, counts) = Drive::verify_contract_moderation_action_counts(
341            supported_grovedb_proof_bytes(&proof)?,
342            contract_id,
343            false,
344            platform_version,
345        )
346        .map_drive_error(&proof, &metadata)?;
347
348        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
349
350        // No member acted since the last payout proves as no counts, so the counts are always
351        // the answer.
352        Ok((
353            Some(ContractModerationActionCounts(counts)),
354            metadata,
355            proof,
356        ))
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use dapi_grpc::platform::v0::get_contract_document_removals_request::get_contract_document_removals_request_v0::Selection;
364    use dapi_grpc::platform::v0::get_contract_document_removals_request::{
365        DocumentIds, GetContractDocumentRemovalsRequestV0, Page,
366    };
367    use dapi_grpc::platform::v0::get_contract_document_removals_response::{
368        get_contract_document_removals_response_v0::Result as RemovalsResult,
369        GetContractDocumentRemovalsResponseV0, Version as RemovalsResponseVersion,
370    };
371    use dapi_grpc::platform::v0::get_contract_fee_pots_request::GetContractFeePotsRequestV0;
372    use dapi_grpc::platform::v0::get_contract_fee_pots_response::{
373        get_contract_fee_pots_response_v0::Result as FeePotsResult, GetContractFeePotsResponseV0,
374        Version as FeePotsResponseVersion,
375    };
376    use dapi_grpc::platform::v0::get_contract_moderation_action_counts_request::GetContractModerationActionCountsRequestV0;
377    use dapi_grpc::platform::v0::get_contract_moderation_action_counts_response::{
378        get_contract_moderation_action_counts_response_v0::Result as CountsResult,
379        GetContractModerationActionCountsResponseV0, Version as CountsResponseVersion,
380    };
381    use dapi_grpc::platform::v0::get_contract_moderation_entries_request::GetContractModerationEntriesRequestV0;
382    use dapi_grpc::platform::v0::get_contract_moderation_entries_response::{
383        get_contract_moderation_entries_response_v0::Result as EntriesResult,
384        GetContractModerationEntriesResponseV0, Version as EntriesResponseVersion,
385    };
386    use dapi_grpc::platform::v0::get_contract_moderation_status_request::GetContractModerationStatusRequestV0;
387    use dapi_grpc::platform::v0::get_contract_moderation_status_response::{
388        get_contract_moderation_status_response_v0::Result as StatusResult,
389        GetContractModerationStatusResponseV0, Version as StatusResponseVersion,
390    };
391    use dapi_grpc::platform::v0::get_contract_team_action_signers_request::GetContractTeamActionSignersRequestV0;
392    use dapi_grpc::platform::v0::get_contract_team_action_signers_response::{
393        get_contract_team_action_signers_response_v0::Result as SignersResult,
394        GetContractTeamActionSignersResponseV0, Version as SignersResponseVersion,
395    };
396    use dapi_grpc::platform::v0::get_contract_team_actions_request::{
397        ActionStatus, GetContractTeamActionsRequestV0,
398    };
399    use dapi_grpc::platform::v0::get_contract_team_actions_response::{
400        get_contract_team_actions_response_v0::Result as TeamActionsResult,
401        GetContractTeamActionsResponseV0, Version as TeamActionsResponseVersion,
402    };
403    use dash_context_provider::ContextProviderError;
404    use dpp::data_contract::TokenConfiguration;
405    use dpp::identifier::Identifier;
406    use dpp::prelude::{CoreBlockHeight, DataContract};
407    use std::sync::Arc;
408
409    /// Context provider that panics if called: every test here fails before the Tenderdash
410    /// proof is checked, so the provider must stay unreachable.
411    struct UnreachableProvider;
412
413    impl ContextProvider for UnreachableProvider {
414        fn get_data_contract(
415            &self,
416            _id: &Identifier,
417            _pv: &PlatformVersion,
418        ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
419            panic!("context provider should not be called")
420        }
421
422        fn get_token_configuration(
423            &self,
424            _id: &Identifier,
425        ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
426            panic!("context provider should not be called")
427        }
428
429        fn get_quorum_public_key(
430            &self,
431            _qt: u32,
432            _qh: [u8; 32],
433            _h: u32,
434        ) -> Result<[u8; 48], ContextProviderError> {
435            panic!("context provider should not be called")
436        }
437
438        fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
439            panic!("context provider should not be called")
440        }
441    }
442
443    fn status_request(
444        contract_id: Vec<u8>,
445        identity_id: Vec<u8>,
446        lists: Vec<i32>,
447    ) -> GetContractModerationStatusRequest {
448        GetContractModerationStatusRequest {
449            version: Some(get_contract_moderation_status_request::Version::V0(
450                GetContractModerationStatusRequestV0 {
451                    contract_id,
452                    identity_id,
453                    lists,
454                    prove: true,
455                },
456            )),
457        }
458    }
459
460    fn status_response(result: Option<StatusResult>) -> GetContractModerationStatusResponse {
461        GetContractModerationStatusResponse {
462            version: Some(StatusResponseVersion::V0(
463                GetContractModerationStatusResponseV0 {
464                    result,
465                    metadata: Some(ResponseMetadata::default()),
466                },
467            )),
468        }
469    }
470
471    fn entries_request(
472        contract_id: Vec<u8>,
473        list: i32,
474        start_after: Option<Vec<u8>>,
475        limit: Option<u32>,
476    ) -> GetContractModerationEntriesRequest {
477        GetContractModerationEntriesRequest {
478            version: Some(get_contract_moderation_entries_request::Version::V0(
479                GetContractModerationEntriesRequestV0 {
480                    contract_id,
481                    list,
482                    start_after,
483                    limit,
484                    prove: true,
485                },
486            )),
487        }
488    }
489
490    fn entries_response(result: Option<EntriesResult>) -> GetContractModerationEntriesResponse {
491        GetContractModerationEntriesResponse {
492            version: Some(EntriesResponseVersion::V0(
493                GetContractModerationEntriesResponseV0 {
494                    result,
495                    metadata: Some(ResponseMetadata::default()),
496                },
497            )),
498        }
499    }
500
501    fn status_error(
502        request: GetContractModerationStatusRequest,
503        response: GetContractModerationStatusResponse,
504    ) -> Error {
505        <ContractModerationListStatuses as FromProof<_>>::maybe_from_proof(
506            request,
507            response,
508            Network::Testnet,
509            PlatformVersion::latest(),
510            &UnreachableProvider,
511        )
512        .unwrap_err()
513    }
514
515    fn entries_error(
516        request: GetContractModerationEntriesRequest,
517        response: GetContractModerationEntriesResponse,
518    ) -> Error {
519        <ContractModerationEntries as FromProof<_>>::maybe_from_proof(
520            request,
521            response,
522            Network::Testnet,
523            PlatformVersion::latest(),
524            &UnreachableProvider,
525        )
526        .unwrap_err()
527    }
528
529    #[test]
530    fn status_should_fail_with_empty_version_when_request_has_no_version() {
531        let err = status_error(
532            GetContractModerationStatusRequest { version: None },
533            status_response(Some(StatusResult::Proof(Proof::default()))),
534        );
535        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
536    }
537
538    #[test]
539    fn status_should_reject_malformed_requests() {
540        let id = vec![1; 32];
541        for (request, needle) in [
542            (
543                status_request(vec![1; 5], id.clone(), vec![1]),
544                "contract_id",
545            ),
546            (
547                status_request(id.clone(), vec![1; 5], vec![1]),
548                "identity_id",
549            ),
550            (
551                status_request(id.clone(), id.clone(), vec![]),
552                "at least one",
553            ),
554            (status_request(id.clone(), id.clone(), vec![1, 1]), "twice"),
555            (
556                status_request(id.clone(), id.clone(), vec![9]),
557                "not a moderation list",
558            ),
559            (
560                status_request(id.clone(), id.clone(), vec![0]),
561                "not a moderation list",
562            ),
563        ] {
564            let err = status_error(
565                request,
566                status_response(Some(StatusResult::Proof(Proof::default()))),
567            );
568            assert!(
569                matches!(&err, Error::RequestError { error } if error.contains(needle)),
570                "{needle}: {err:?}"
571            );
572        }
573    }
574
575    #[test]
576    fn status_should_fail_without_proof_when_response_carries_none() {
577        let err = status_error(
578            status_request(vec![1; 32], vec![2; 32], vec![1]),
579            status_response(None),
580        );
581        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
582    }
583
584    #[test]
585    fn status_should_fail_on_a_proof_that_does_not_verify() {
586        let err = status_error(
587            status_request(vec![1; 32], vec![2; 32], vec![1, 2]),
588            status_response(Some(StatusResult::Proof(Proof::default()))),
589        );
590        assert!(
591            !matches!(err, Error::RequestError { .. } | Error::NoProofInResult),
592            "got: {err:?}"
593        );
594    }
595
596    #[test]
597    fn entries_should_fail_with_empty_version_when_request_has_no_version() {
598        let err = entries_error(
599            GetContractModerationEntriesRequest { version: None },
600            entries_response(Some(EntriesResult::Proof(Proof::default()))),
601        );
602        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
603    }
604
605    #[test]
606    fn entries_should_reject_malformed_requests() {
607        let id = vec![1; 32];
608        for (request, needle) in [
609            (entries_request(vec![1; 5], 1, None, None), "contract_id"),
610            (
611                entries_request(id.clone(), 9, None, None),
612                "not a moderation list",
613            ),
614            (
615                entries_request(id.clone(), 0, None, None),
616                "not a moderation list",
617            ),
618            (
619                entries_request(id.clone(), 1, Some(vec![1; 5]), None),
620                "start_after",
621            ),
622            (
623                entries_request(id.clone(), 1, None, Some(70_000)),
624                "out of bounds",
625            ),
626        ] {
627            let err = entries_error(
628                request,
629                entries_response(Some(EntriesResult::Proof(Proof::default()))),
630            );
631            assert!(
632                matches!(&err, Error::RequestError { error } if error.contains(needle)),
633                "{needle}: {err:?}"
634            );
635        }
636    }
637
638    #[test]
639    fn entries_should_fail_without_proof_when_response_carries_none() {
640        let err = entries_error(
641            entries_request(vec![1; 32], 1, None, None),
642            entries_response(None),
643        );
644        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
645    }
646
647    #[test]
648    fn entries_should_fail_on_a_proof_that_does_not_verify() {
649        let err = entries_error(
650            entries_request(vec![1; 32], 2, Some(vec![2; 32]), Some(10)),
651            entries_response(Some(EntriesResult::Proof(Proof::default()))),
652        );
653        assert!(
654            !matches!(err, Error::RequestError { .. } | Error::NoProofInResult),
655            "got: {err:?}"
656        );
657    }
658
659    fn fee_pots_request(contract_id: Vec<u8>) -> GetContractFeePotsRequest {
660        GetContractFeePotsRequest {
661            version: Some(get_contract_fee_pots_request::Version::V0(
662                GetContractFeePotsRequestV0 {
663                    contract_id,
664                    prove: true,
665                },
666            )),
667        }
668    }
669
670    fn fee_pots_response(result: Option<FeePotsResult>) -> GetContractFeePotsResponse {
671        GetContractFeePotsResponse {
672            version: Some(FeePotsResponseVersion::V0(GetContractFeePotsResponseV0 {
673                result,
674                metadata: Some(ResponseMetadata::default()),
675            })),
676        }
677    }
678
679    fn fee_pots_error(
680        request: GetContractFeePotsRequest,
681        response: GetContractFeePotsResponse,
682    ) -> Error {
683        <ContractFeePots as FromProof<_>>::maybe_from_proof(
684            request,
685            response,
686            Network::Testnet,
687            PlatformVersion::latest(),
688            &UnreachableProvider,
689        )
690        .unwrap_err()
691    }
692
693    #[test]
694    fn fee_pots_should_fail_with_empty_version_when_request_has_no_version() {
695        let err = fee_pots_error(
696            GetContractFeePotsRequest { version: None },
697            fee_pots_response(Some(FeePotsResult::Proof(Proof::default()))),
698        );
699        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
700    }
701
702    #[test]
703    fn fee_pots_should_reject_a_malformed_contract_id() {
704        let err = fee_pots_error(
705            fee_pots_request(vec![1; 5]),
706            fee_pots_response(Some(FeePotsResult::Proof(Proof::default()))),
707        );
708        assert!(
709            matches!(&err, Error::RequestError { error } if error.contains("contract_id")),
710            "got: {err:?}"
711        );
712    }
713
714    #[test]
715    fn fee_pots_should_fail_without_proof_when_response_carries_none() {
716        let err = fee_pots_error(fee_pots_request(vec![1; 32]), fee_pots_response(None));
717        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
718    }
719
720    #[test]
721    fn fee_pots_should_fail_on_a_proof_that_does_not_verify() {
722        let err = fee_pots_error(
723            fee_pots_request(vec![1; 32]),
724            fee_pots_response(Some(FeePotsResult::Proof(Proof::default()))),
725        );
726        assert!(
727            !matches!(err, Error::RequestError { .. } | Error::NoProofInResult),
728            "got: {err:?}"
729        );
730    }
731
732    fn removals_request(
733        contract_id: Vec<u8>,
734        document_type_name: &str,
735        selection: Option<Selection>,
736    ) -> GetContractDocumentRemovalsRequest {
737        GetContractDocumentRemovalsRequest {
738            version: Some(get_contract_document_removals_request::Version::V0(
739                GetContractDocumentRemovalsRequestV0 {
740                    contract_id,
741                    document_type_name: document_type_name.to_string(),
742                    selection,
743                    prove: true,
744                },
745            )),
746        }
747    }
748
749    fn removals_response(result: Option<RemovalsResult>) -> GetContractDocumentRemovalsResponse {
750        GetContractDocumentRemovalsResponse {
751            version: Some(RemovalsResponseVersion::V0(
752                GetContractDocumentRemovalsResponseV0 {
753                    result,
754                    metadata: Some(ResponseMetadata::default()),
755                },
756            )),
757        }
758    }
759
760    fn removals_error(
761        request: GetContractDocumentRemovalsRequest,
762        response: GetContractDocumentRemovalsResponse,
763    ) -> Error {
764        <ContractDocumentRemovals as FromProof<_>>::maybe_from_proof(
765            request,
766            response,
767            Network::Testnet,
768            PlatformVersion::latest(),
769            &UnreachableProvider,
770        )
771        .unwrap_err()
772    }
773
774    fn removals_page(start_after: Option<Vec<u8>>, limit: Option<u32>) -> Option<Selection> {
775        Some(Selection::Page(Page { start_after, limit }))
776    }
777
778    #[test]
779    fn removals_should_fail_with_empty_version_when_request_has_no_version() {
780        let err = removals_error(
781            GetContractDocumentRemovalsRequest { version: None },
782            removals_response(Some(RemovalsResult::Proof(Proof::default()))),
783        );
784        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
785    }
786
787    #[test]
788    fn removals_should_reject_malformed_requests() {
789        let id = vec![1; 32];
790        for (request, needle) in [
791            (
792                removals_request(vec![1; 5], "post", removals_page(None, None)),
793                "contract_id",
794            ),
795            (
796                removals_request(id.clone(), "post", None),
797                "either document_ids or page must be set",
798            ),
799            (
800                removals_request(
801                    id.clone(),
802                    "post",
803                    Some(Selection::DocumentIds(DocumentIds {
804                        document_ids: vec![],
805                    })),
806                ),
807                "must name between 1 and",
808            ),
809            (
810                removals_request(
811                    id.clone(),
812                    "post",
813                    Some(Selection::DocumentIds(DocumentIds {
814                        document_ids: vec![id.clone(), id.clone()],
815                    })),
816                ),
817                "name a document id twice",
818            ),
819            (
820                removals_request(id.clone(), "post", removals_page(Some(vec![1; 5]), None)),
821                "start_after",
822            ),
823            (
824                removals_request(id.clone(), "post", removals_page(None, Some(70_000))),
825                "limit must be between 1 and",
826            ),
827        ] {
828            let err = removals_error(
829                request,
830                removals_response(Some(RemovalsResult::Proof(Proof::default()))),
831            );
832            assert!(
833                matches!(&err, Error::RequestError { error } if error.contains(needle)),
834                "{needle}: {err:?}"
835            );
836        }
837    }
838
839    #[test]
840    fn removals_should_fail_without_proof_when_response_carries_none() {
841        let err = removals_error(
842            removals_request(vec![1; 32], "post", removals_page(None, None)),
843            removals_response(None),
844        );
845        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
846    }
847
848    #[test]
849    fn removals_should_fail_on_a_proof_that_does_not_verify() {
850        let err = removals_error(
851            removals_request(
852                vec![1; 32],
853                "post",
854                Some(Selection::DocumentIds(DocumentIds {
855                    document_ids: vec![vec![2; 32]],
856                })),
857            ),
858            removals_response(Some(RemovalsResult::Proof(Proof::default()))),
859        );
860        assert!(
861            !matches!(err, Error::RequestError { .. } | Error::NoProofInResult),
862            "got: {err:?}"
863        );
864    }
865
866    fn team_actions_request(
867        contract_id: Vec<u8>,
868        status: i32,
869        count: Option<u32>,
870    ) -> GetContractTeamActionsRequest {
871        GetContractTeamActionsRequest {
872            version: Some(get_contract_team_actions_request::Version::V0(
873                GetContractTeamActionsRequestV0 {
874                    contract_id,
875                    status,
876                    start_at_action_id: None,
877                    count,
878                    prove: true,
879                },
880            )),
881        }
882    }
883
884    fn team_actions_response(result: Option<TeamActionsResult>) -> GetContractTeamActionsResponse {
885        GetContractTeamActionsResponse {
886            version: Some(TeamActionsResponseVersion::V0(
887                GetContractTeamActionsResponseV0 {
888                    result,
889                    metadata: Some(ResponseMetadata::default()),
890                },
891            )),
892        }
893    }
894
895    fn team_actions_error(
896        request: GetContractTeamActionsRequest,
897        response: GetContractTeamActionsResponse,
898    ) -> Error {
899        <ContractTeamActions as FromProof<_>>::maybe_from_proof(
900            request,
901            response,
902            Network::Testnet,
903            PlatformVersion::latest(),
904            &UnreachableProvider,
905        )
906        .unwrap_err()
907    }
908
909    fn signers_request(
910        contract_id: Vec<u8>,
911        status: i32,
912        action_id: Vec<u8>,
913    ) -> GetContractTeamActionSignersRequest {
914        GetContractTeamActionSignersRequest {
915            version: Some(get_contract_team_action_signers_request::Version::V0(
916                GetContractTeamActionSignersRequestV0 {
917                    contract_id,
918                    status,
919                    action_id,
920                    prove: true,
921                },
922            )),
923        }
924    }
925
926    fn signers_response(result: Option<SignersResult>) -> GetContractTeamActionSignersResponse {
927        GetContractTeamActionSignersResponse {
928            version: Some(SignersResponseVersion::V0(
929                GetContractTeamActionSignersResponseV0 {
930                    result,
931                    metadata: Some(ResponseMetadata::default()),
932                },
933            )),
934        }
935    }
936
937    fn signers_error(
938        request: GetContractTeamActionSignersRequest,
939        response: GetContractTeamActionSignersResponse,
940    ) -> Error {
941        <ContractTeamActionSigners as FromProof<_>>::maybe_from_proof(
942            request,
943            response,
944            Network::Testnet,
945            PlatformVersion::latest(),
946            &UnreachableProvider,
947        )
948        .unwrap_err()
949    }
950
951    #[test]
952    fn should_fail_team_action_queries_with_empty_version_when_request_has_no_version() {
953        let err = team_actions_error(
954            GetContractTeamActionsRequest { version: None },
955            team_actions_response(Some(TeamActionsResult::Proof(Proof::default()))),
956        );
957        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
958        let err = signers_error(
959            GetContractTeamActionSignersRequest { version: None },
960            signers_response(Some(SignersResult::Proof(Proof::default()))),
961        );
962        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
963    }
964
965    #[test]
966    fn should_reject_malformed_team_action_requests() {
967        let id = vec![1; 32];
968        let active = ActionStatus::Active as i32;
969        for (request, needle) in [
970            (
971                team_actions_request(vec![1; 5], active, None),
972                "contract_id",
973            ),
974            (
975                team_actions_request(id.clone(), 7, None),
976                "is not an action status",
977            ),
978            (
979                team_actions_request(id.clone(), active, Some(70_000)),
980                "limit must be between 1 and",
981            ),
982        ] {
983            let err = team_actions_error(
984                request,
985                team_actions_response(Some(TeamActionsResult::Proof(Proof::default()))),
986            );
987            assert!(
988                matches!(&err, Error::RequestError { error } if error.contains(needle)),
989                "{needle}: {err:?}"
990            );
991        }
992        for (request, needle) in [
993            (
994                signers_request(vec![1; 5], active, id.clone()),
995                "contract_id",
996            ),
997            (signers_request(id.clone(), active, vec![1; 5]), "action_id"),
998            (
999                signers_request(id.clone(), 7, id.clone()),
1000                "is not an action status",
1001            ),
1002        ] {
1003            let err = signers_error(
1004                request,
1005                signers_response(Some(SignersResult::Proof(Proof::default()))),
1006            );
1007            assert!(
1008                matches!(&err, Error::RequestError { error } if error.contains(needle)),
1009                "{needle}: {err:?}"
1010            );
1011        }
1012    }
1013
1014    #[test]
1015    fn should_fail_team_action_queries_without_proof_when_response_carries_none() {
1016        let active = ActionStatus::Active as i32;
1017        let err = team_actions_error(
1018            team_actions_request(vec![1; 32], active, None),
1019            team_actions_response(None),
1020        );
1021        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
1022        let err = signers_error(
1023            signers_request(vec![1; 32], active, vec![2; 32]),
1024            signers_response(None),
1025        );
1026        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
1027    }
1028
1029    #[test]
1030    fn should_fail_on_team_action_proofs_that_do_not_verify() {
1031        let active = ActionStatus::Active as i32;
1032        let err = team_actions_error(
1033            team_actions_request(vec![1; 32], active, None),
1034            team_actions_response(Some(TeamActionsResult::Proof(Proof::default()))),
1035        );
1036        assert!(
1037            !matches!(err, Error::RequestError { .. } | Error::NoProofInResult),
1038            "got: {err:?}"
1039        );
1040        let err = signers_error(
1041            signers_request(vec![1; 32], active, vec![2; 32]),
1042            signers_response(Some(SignersResult::Proof(Proof::default()))),
1043        );
1044        assert!(
1045            !matches!(err, Error::RequestError { .. } | Error::NoProofInResult),
1046            "got: {err:?}"
1047        );
1048    }
1049
1050    fn counts_request(contract_id: Vec<u8>) -> GetContractModerationActionCountsRequest {
1051        GetContractModerationActionCountsRequest {
1052            version: Some(get_contract_moderation_action_counts_request::Version::V0(
1053                GetContractModerationActionCountsRequestV0 {
1054                    contract_id,
1055                    prove: true,
1056                },
1057            )),
1058        }
1059    }
1060
1061    fn counts_response(result: Option<CountsResult>) -> GetContractModerationActionCountsResponse {
1062        GetContractModerationActionCountsResponse {
1063            version: Some(CountsResponseVersion::V0(
1064                GetContractModerationActionCountsResponseV0 {
1065                    result,
1066                    metadata: Some(ResponseMetadata::default()),
1067                },
1068            )),
1069        }
1070    }
1071
1072    fn counts_error(
1073        request: GetContractModerationActionCountsRequest,
1074        response: GetContractModerationActionCountsResponse,
1075    ) -> Error {
1076        <ContractModerationActionCounts as FromProof<_>>::maybe_from_proof(
1077            request,
1078            response,
1079            Network::Testnet,
1080            PlatformVersion::latest(),
1081            &UnreachableProvider,
1082        )
1083        .unwrap_err()
1084    }
1085
1086    #[test]
1087    fn should_check_the_request_and_the_proof_of_moderation_action_counts() {
1088        let proof = || counts_response(Some(CountsResult::Proof(Proof::default())));
1089        let err = counts_error(
1090            GetContractModerationActionCountsRequest { version: None },
1091            proof(),
1092        );
1093        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
1094
1095        let err = counts_error(counts_request(vec![1; 5]), proof());
1096        assert!(
1097            matches!(&err, Error::RequestError { error } if error.contains("contract_id")),
1098            "got: {err:?}"
1099        );
1100
1101        let err = counts_error(counts_request(vec![1; 32]), counts_response(None));
1102        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
1103
1104        // A proof that does not verify fails in the proof check, past the request's
1105        let err = counts_error(counts_request(vec![1; 32]), proof());
1106        assert!(
1107            !matches!(err, Error::RequestError { .. } | Error::NoProofInResult),
1108            "got: {err:?}"
1109        );
1110    }
1111}