Skip to main content

drive_proof_verifier/proof/
groups.rs

1use crate::error::MapGroveDbError;
2use crate::types::groups::{GroupActionSigners, GroupActions, Groups};
3use crate::verify::{
4    supported_grovedb_proof_bytes, verify_page_drive_proves, verify_tenderdash_proof,
5};
6use crate::{ContextProvider, Error, FromProof};
7use dapi_grpc::platform::v0::{
8    get_group_action_signers_request, get_group_actions_request, get_group_info_request,
9    get_group_infos_request, GetGroupActionSignersRequest, GetGroupActionSignersResponse,
10    GetGroupActionsRequest, GetGroupActionsResponse, GetGroupInfoRequest, GetGroupInfoResponse,
11    GetGroupInfosRequest, GetGroupInfosResponse, Proof, ResponseMetadata,
12};
13use dapi_grpc::platform::VersionedGrpcResponse;
14use dpp::dashcore::Network;
15use dpp::data_contract::group::{Group, GroupMemberPower};
16use dpp::data_contract::GroupContractPosition;
17use dpp::group::group_action::GroupAction;
18use dpp::group::group_action_status::GroupActionStatus;
19use dpp::identifier::Identifier;
20use dpp::version::PlatformVersion;
21use drive::drive::Drive;
22use indexmap::IndexMap;
23
24impl FromProof<GetGroupInfoRequest> for Group {
25    type Request = GetGroupInfoRequest;
26    type Response = GetGroupInfoResponse;
27
28    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
29        request: I,
30        response: O,
31        _network: Network,
32        platform_version: &PlatformVersion,
33        provider: &'a dyn ContextProvider,
34    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
35    where
36        Self: Sized + 'a,
37    {
38        let request: Self::Request = request.into();
39        let response: Self::Response = response.into();
40
41        let (contract_id, group_contract_position) = match request
42            .version
43            .ok_or(Error::EmptyVersion)?
44        {
45            get_group_info_request::Version::V0(v0) => {
46                let contract_id =
47                    Identifier::try_from(v0.contract_id).map_err(|error| Error::RequestError {
48                        error: format!("can't convert contract_id to identifier: {error}"),
49                    })?;
50
51                let group_contract_position = v0.group_contract_position as GroupContractPosition;
52
53                (contract_id, group_contract_position)
54            }
55        };
56
57        let metadata = response
58            .metadata()
59            .or(Err(Error::EmptyResponseMetadata))?
60            .clone();
61
62        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
63
64        let (root_hash, result) = Drive::verify_group_info(
65            supported_grovedb_proof_bytes(&proof)?,
66            contract_id,
67            group_contract_position,
68            false,
69            platform_version,
70        )
71        .map_drive_error(&proof, &metadata)?;
72
73        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
74
75        Ok((result, metadata, proof))
76    }
77}
78
79impl FromProof<GetGroupInfosRequest> for Groups {
80    type Request = GetGroupInfosRequest;
81    type Response = GetGroupInfosResponse;
82
83    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
84        request: I,
85        response: O,
86        _network: Network,
87        platform_version: &PlatformVersion,
88        provider: &'a dyn ContextProvider,
89    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
90    where
91        Self: Sized + 'a,
92    {
93        let request: Self::Request = request.into();
94        let response: Self::Response = response.into();
95
96        let (contract_id, start_at_group_contract_position, count) = match request
97            .version
98            .ok_or(Error::EmptyVersion)?
99        {
100            get_group_infos_request::Version::V0(v0) => {
101                let contract_id =
102                    Identifier::try_from(v0.contract_id).map_err(|error| Error::RequestError {
103                        error: format!("can't convert contract_id to identifier: {error}"),
104                    })?;
105
106                let start_group_contract_position =
107                    v0.start_at_group_contract_position.map(|start_position| {
108                        (
109                            start_position.start_group_contract_position as GroupContractPosition,
110                            start_position.start_group_contract_position_included,
111                        )
112                    });
113
114                let count = v0.count.map(|count| count as u16);
115
116                (contract_id, start_group_contract_position, count)
117            }
118        };
119
120        let metadata = response
121            .metadata()
122            .or(Err(Error::EmptyResponseMetadata))?
123            .clone();
124
125        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
126
127        let grovedb_proof = supported_grovedb_proof_bytes(&proof)?;
128        let (root_hash, result) = verify_page_drive_proves(count, |count| {
129            Drive::verify_group_infos_in_contract(
130                grovedb_proof,
131                contract_id,
132                start_at_group_contract_position,
133                count,
134                false,
135                platform_version,
136            )
137        })
138        // Make value optional
139        .map(
140            |(root_hash, result): (_, IndexMap<GroupContractPosition, Group>)| {
141                let optional_value_map = result
142                    .into_iter()
143                    .map(|(action_id, group_action)| (action_id, Some(group_action)))
144                    .collect::<Groups>();
145                (root_hash, optional_value_map)
146            },
147        )
148        .map_drive_error(&proof, &metadata)?;
149
150        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
151
152        Ok((Some(result), metadata, proof))
153    }
154}
155
156impl FromProof<GetGroupActionsRequest> for GroupActions {
157    type Request = GetGroupActionsRequest;
158    type Response = GetGroupActionsResponse;
159
160    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
161        request: I,
162        response: O,
163        _network: Network,
164        platform_version: &PlatformVersion,
165        provider: &'a dyn ContextProvider,
166    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
167    where
168        Self: Sized + 'a,
169    {
170        let request: Self::Request = request.into();
171        let response: Self::Response = response.into();
172
173        let (contract_id, group_contract_position, status, start_at_action_id, count) =
174            match request.version.ok_or(Error::EmptyVersion)? {
175                get_group_actions_request::Version::V0(v0) => {
176                    let contract_id = Identifier::try_from(v0.contract_id).map_err(|error| {
177                        Error::RequestError {
178                            error: format!("can't convert contract_id to identifier: {error}"),
179                        }
180                    })?;
181
182                    let start_at_action_id =
183                        v0.start_at_action_id
184                            .map(|start_at_action_id| {
185                                let start_action_id =
186                                    Identifier::try_from(start_at_action_id.start_action_id)
187                                        .map_err(|error| Error::RequestError {
188                                            error: format!(
189                                    "can't convert start_action_id to identifier: {error}"
190                                ),
191                                        })?;
192
193                                Ok::<_, Error>((
194                                    start_action_id,
195                                    start_at_action_id.start_action_id_included,
196                                ))
197                            })
198                            .transpose()?;
199
200                    let group_contract_position =
201                        v0.group_contract_position as GroupContractPosition;
202
203                    let count = v0.count.map(|count| count as u16);
204
205                    let status = GroupActionStatus::try_from(v0.status).map_err(|error| {
206                        Error::RequestError {
207                            error: format!("can't convert status to GroupActionStatus: {error}"),
208                        }
209                    })?;
210
211                    (
212                        contract_id,
213                        group_contract_position,
214                        status,
215                        start_at_action_id,
216                        count,
217                    )
218                }
219            };
220
221        let metadata = response
222            .metadata()
223            .or(Err(Error::EmptyResponseMetadata))?
224            .clone();
225
226        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
227
228        let grovedb_proof = supported_grovedb_proof_bytes(&proof)?;
229        let (root_hash, result) = verify_page_drive_proves(count, |count| {
230            Drive::verify_action_infos_in_contract(
231                grovedb_proof,
232                contract_id,
233                group_contract_position,
234                status,
235                start_at_action_id,
236                count,
237                false,
238                platform_version,
239            )
240        })
241        // Make value optional
242        .map(
243            |(root_hash, result): (_, IndexMap<Identifier, GroupAction>)| {
244                let optional_value_map = result
245                    .into_iter()
246                    .map(|(action_id, group_action)| (action_id, Some(group_action)))
247                    .collect::<GroupActions>();
248                (root_hash, optional_value_map)
249            },
250        )
251        .map_drive_error(&proof, &metadata)?;
252
253        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
254
255        Ok((Some(result), metadata, proof))
256    }
257}
258
259impl FromProof<GetGroupActionSignersRequest> for GroupActionSigners {
260    type Request = GetGroupActionSignersRequest;
261    type Response = GetGroupActionSignersResponse;
262
263    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
264        request: I,
265        response: O,
266        _network: Network,
267        platform_version: &PlatformVersion,
268        provider: &'a dyn ContextProvider,
269    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
270    where
271        Self: Sized + 'a,
272    {
273        let request: Self::Request = request.into();
274        let response: Self::Response = response.into();
275
276        let (contract_id, group_contract_position, status, action_id) = match request
277            .version
278            .ok_or(Error::EmptyVersion)?
279        {
280            get_group_action_signers_request::Version::V0(v0) => {
281                let contract_id =
282                    Identifier::try_from(v0.contract_id).map_err(|error| Error::RequestError {
283                        error: format!("can't convert contract_id to identifier: {error}"),
284                    })?;
285
286                let action_id =
287                    Identifier::try_from(v0.action_id).map_err(|error| Error::RequestError {
288                        error: format!("can't convert action_id to identifier: {error}"),
289                    })?;
290
291                let group_contract_position = v0.group_contract_position as GroupContractPosition;
292
293                let status = GroupActionStatus::try_from(v0.status).map_err(|error| {
294                    Error::RequestError {
295                        error: format!("can't convert status to GroupActionStatus: {error}"),
296                    }
297                })?;
298
299                (contract_id, group_contract_position, status, action_id)
300            }
301        };
302
303        let metadata = response
304            .metadata()
305            .or(Err(Error::EmptyResponseMetadata))?
306            .clone();
307
308        let proof = response.proof_owned().or(Err(Error::NoProofInResult))?;
309
310        let (root_hash, result) = Drive::verify_action_signers(
311            supported_grovedb_proof_bytes(&proof)?,
312            contract_id,
313            group_contract_position,
314            status,
315            action_id,
316            false,
317            platform_version,
318        )
319        // Make value optional
320        .map(
321            |(root_hash, result): (_, IndexMap<Identifier, GroupMemberPower>)| {
322                let optional_value_map = result
323                    .into_iter()
324                    .map(|(action_id, group_action)| (action_id, Some(group_action)))
325                    .collect::<GroupActionSigners>();
326                (root_hash, optional_value_map)
327            },
328        )
329        .map_drive_error(&proof, &metadata)?;
330
331        verify_tenderdash_proof(&proof, &metadata, &root_hash, provider)?;
332
333        Ok((Some(result), metadata, proof))
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use dapi_grpc::platform::v0::get_group_action_signers_request::{
341        GetGroupActionSignersRequestV0, Version as SignersReqVersion,
342    };
343    use dapi_grpc::platform::v0::get_group_actions_request::{
344        GetGroupActionsRequestV0, StartAtActionId, Version as ActionsReqVersion,
345    };
346    use dapi_grpc::platform::v0::get_group_actions_response::{
347        get_group_actions_response_v0::Result as ActionsRespResult, GetGroupActionsResponseV0,
348        Version as ActionsRespVersion,
349    };
350    use dapi_grpc::platform::v0::get_group_info_request::{
351        GetGroupInfoRequestV0, Version as InfoReqVersion,
352    };
353    use dapi_grpc::platform::v0::get_group_info_response::{
354        get_group_info_response_v0::Result as InfoRespResult, GetGroupInfoResponseV0,
355        Version as InfoRespVersion,
356    };
357    use dapi_grpc::platform::v0::get_group_infos_request::{
358        GetGroupInfosRequestV0, StartAtGroupContractPosition, Version as InfosReqVersion,
359    };
360    use dapi_grpc::platform::v0::get_group_infos_response::{
361        get_group_infos_response_v0::Result as InfosRespResult, GetGroupInfosResponseV0,
362        Version as InfosRespVersion,
363    };
364    use dash_context_provider::ContextProviderError;
365    use dpp::data_contract::TokenConfiguration;
366    use dpp::prelude::{CoreBlockHeight, DataContract};
367    use std::sync::Arc;
368
369    struct UnreachableProvider;
370
371    impl ContextProvider for UnreachableProvider {
372        fn get_data_contract(
373            &self,
374            _id: &Identifier,
375            _pv: &PlatformVersion,
376        ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
377            panic!("should not be called")
378        }
379        fn get_token_configuration(
380            &self,
381            _id: &Identifier,
382        ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
383            panic!("should not be called")
384        }
385        fn get_quorum_public_key(
386            &self,
387            _qt: u32,
388            _qh: [u8; 32],
389            _h: u32,
390        ) -> Result<[u8; 48], ContextProviderError> {
391            panic!("should not be called")
392        }
393        fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
394            panic!("should not be called")
395        }
396    }
397
398    fn pv() -> &'static PlatformVersion {
399        PlatformVersion::latest()
400    }
401
402    // -------- GetGroupInfoRequest / Group --------
403
404    #[test]
405    fn group_info_empty_version_on_request_missing() {
406        let request = GetGroupInfoRequest { version: None };
407        let response = GetGroupInfoResponse {
408            version: Some(InfoRespVersion::V0(GetGroupInfoResponseV0 {
409                result: Some(InfoRespResult::Proof(Proof::default())),
410                metadata: Some(ResponseMetadata::default()),
411            })),
412        };
413        let err = <Group as FromProof<_>>::maybe_from_proof(
414            request,
415            response,
416            Network::Testnet,
417            pv(),
418            &UnreachableProvider,
419        )
420        .unwrap_err();
421        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
422    }
423
424    #[test]
425    fn group_info_request_error_on_bad_contract_id() {
426        let request = GetGroupInfoRequest {
427            version: Some(InfoReqVersion::V0(GetGroupInfoRequestV0 {
428                contract_id: vec![0u8; 7], // wrong length
429                group_contract_position: 0,
430                prove: true,
431            })),
432        };
433        let response = GetGroupInfoResponse::default();
434        let err = <Group as FromProof<_>>::maybe_from_proof(
435            request,
436            response,
437            Network::Testnet,
438            pv(),
439            &UnreachableProvider,
440        )
441        .unwrap_err();
442        match err {
443            Error::RequestError { error } => assert!(error.contains("contract_id"), "got: {error}"),
444            other => panic!("expected RequestError, got: {other:?}"),
445        }
446    }
447
448    #[test]
449    fn group_info_empty_response_metadata() {
450        let request = GetGroupInfoRequest {
451            version: Some(InfoReqVersion::V0(GetGroupInfoRequestV0 {
452                contract_id: vec![0u8; 32],
453                group_contract_position: 0,
454                prove: true,
455            })),
456        };
457        let response = GetGroupInfoResponse { version: None };
458        let err = <Group as FromProof<_>>::maybe_from_proof(
459            request,
460            response,
461            Network::Testnet,
462            pv(),
463            &UnreachableProvider,
464        )
465        .unwrap_err();
466        assert!(matches!(err, Error::EmptyResponseMetadata), "got: {err:?}");
467    }
468
469    #[test]
470    fn group_info_no_proof_when_result_missing() {
471        let request = GetGroupInfoRequest {
472            version: Some(InfoReqVersion::V0(GetGroupInfoRequestV0 {
473                contract_id: vec![0u8; 32],
474                group_contract_position: 0,
475                prove: true,
476            })),
477        };
478        let response = GetGroupInfoResponse {
479            version: Some(InfoRespVersion::V0(GetGroupInfoResponseV0 {
480                result: None,
481                metadata: Some(ResponseMetadata::default()),
482            })),
483        };
484        let err = <Group as FromProof<_>>::maybe_from_proof(
485            request,
486            response,
487            Network::Testnet,
488            pv(),
489            &UnreachableProvider,
490        )
491        .unwrap_err();
492        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
493    }
494
495    // -------- GetGroupInfosRequest / Groups --------
496
497    #[test]
498    fn group_infos_empty_version_on_request_missing() {
499        let request = GetGroupInfosRequest { version: None };
500        let response = GetGroupInfosResponse::default();
501        let err = <Groups as FromProof<_>>::maybe_from_proof(
502            request,
503            response,
504            Network::Testnet,
505            pv(),
506            &UnreachableProvider,
507        )
508        .unwrap_err();
509        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
510    }
511
512    #[test]
513    fn group_infos_request_error_on_bad_contract_id() {
514        let request = GetGroupInfosRequest {
515            version: Some(InfosReqVersion::V0(GetGroupInfosRequestV0 {
516                contract_id: vec![0u8; 12], // wrong length
517                start_at_group_contract_position: Some(StartAtGroupContractPosition {
518                    start_group_contract_position: 0,
519                    start_group_contract_position_included: true,
520                }),
521                count: Some(10),
522                prove: true,
523            })),
524        };
525        let response = GetGroupInfosResponse::default();
526        let err = <Groups as FromProof<_>>::maybe_from_proof(
527            request,
528            response,
529            Network::Testnet,
530            pv(),
531            &UnreachableProvider,
532        )
533        .unwrap_err();
534        match err {
535            Error::RequestError { error } => assert!(error.contains("contract_id"), "got: {error}"),
536            other => panic!("expected RequestError, got: {other:?}"),
537        }
538    }
539
540    #[test]
541    fn group_infos_empty_response_metadata() {
542        let request = GetGroupInfosRequest {
543            version: Some(InfosReqVersion::V0(GetGroupInfosRequestV0 {
544                contract_id: vec![0u8; 32],
545                start_at_group_contract_position: None,
546                count: None,
547                prove: true,
548            })),
549        };
550        let response = GetGroupInfosResponse {
551            version: Some(InfosRespVersion::V0(GetGroupInfosResponseV0 {
552                result: Some(InfosRespResult::Proof(Proof::default())),
553                metadata: None,
554            })),
555        };
556        let err = <Groups as FromProof<_>>::maybe_from_proof(
557            request,
558            response,
559            Network::Testnet,
560            pv(),
561            &UnreachableProvider,
562        )
563        .unwrap_err();
564        assert!(matches!(err, Error::EmptyResponseMetadata), "got: {err:?}");
565    }
566
567    // -------- GetGroupActionsRequest / GroupActions --------
568
569    #[test]
570    fn group_actions_empty_version_on_request_missing() {
571        let request = GetGroupActionsRequest { version: None };
572        let response = GetGroupActionsResponse::default();
573        let err = <GroupActions as FromProof<_>>::maybe_from_proof(
574            request,
575            response,
576            Network::Testnet,
577            pv(),
578            &UnreachableProvider,
579        )
580        .unwrap_err();
581        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
582    }
583
584    #[test]
585    fn group_actions_request_error_on_bad_contract_id() {
586        let request = GetGroupActionsRequest {
587            version: Some(ActionsReqVersion::V0(GetGroupActionsRequestV0 {
588                contract_id: vec![0u8; 3],
589                group_contract_position: 0,
590                status: 0,
591                start_at_action_id: None,
592                count: None,
593                prove: true,
594            })),
595        };
596        let response = GetGroupActionsResponse::default();
597        let err = <GroupActions as FromProof<_>>::maybe_from_proof(
598            request,
599            response,
600            Network::Testnet,
601            pv(),
602            &UnreachableProvider,
603        )
604        .unwrap_err();
605        match err {
606            Error::RequestError { error } => assert!(error.contains("contract_id"), "got: {error}"),
607            other => panic!("expected RequestError, got: {other:?}"),
608        }
609    }
610
611    #[test]
612    fn group_actions_request_error_on_bad_start_action_id() {
613        let request = GetGroupActionsRequest {
614            version: Some(ActionsReqVersion::V0(GetGroupActionsRequestV0 {
615                contract_id: vec![0u8; 32],
616                group_contract_position: 0,
617                status: 0,
618                start_at_action_id: Some(StartAtActionId {
619                    start_action_id: vec![0u8; 9], // wrong length
620                    start_action_id_included: true,
621                }),
622                count: None,
623                prove: true,
624            })),
625        };
626        let response = GetGroupActionsResponse::default();
627        let err = <GroupActions as FromProof<_>>::maybe_from_proof(
628            request,
629            response,
630            Network::Testnet,
631            pv(),
632            &UnreachableProvider,
633        )
634        .unwrap_err();
635        match err {
636            Error::RequestError { error } => {
637                assert!(error.contains("start_action_id"), "got: {error}")
638            }
639            other => panic!("expected RequestError, got: {other:?}"),
640        }
641    }
642
643    #[test]
644    fn group_actions_request_error_on_bad_status() {
645        let request = GetGroupActionsRequest {
646            version: Some(ActionsReqVersion::V0(GetGroupActionsRequestV0 {
647                contract_id: vec![0u8; 32],
648                group_contract_position: 0,
649                status: 999, // invalid status
650                start_at_action_id: None,
651                count: None,
652                prove: true,
653            })),
654        };
655        let response = GetGroupActionsResponse::default();
656        let err = <GroupActions as FromProof<_>>::maybe_from_proof(
657            request,
658            response,
659            Network::Testnet,
660            pv(),
661            &UnreachableProvider,
662        )
663        .unwrap_err();
664        match err {
665            Error::RequestError { error } => {
666                assert!(error.contains("GroupActionStatus"), "got: {error}")
667            }
668            other => panic!("expected RequestError, got: {other:?}"),
669        }
670    }
671
672    #[test]
673    fn group_actions_empty_response_metadata() {
674        let request = GetGroupActionsRequest {
675            version: Some(ActionsReqVersion::V0(GetGroupActionsRequestV0 {
676                contract_id: vec![0u8; 32],
677                group_contract_position: 0,
678                status: 0,
679                start_at_action_id: None,
680                count: None,
681                prove: true,
682            })),
683        };
684        let response = GetGroupActionsResponse {
685            version: Some(ActionsRespVersion::V0(GetGroupActionsResponseV0 {
686                result: Some(ActionsRespResult::Proof(Proof::default())),
687                metadata: None,
688            })),
689        };
690        let err = <GroupActions as FromProof<_>>::maybe_from_proof(
691            request,
692            response,
693            Network::Testnet,
694            pv(),
695            &UnreachableProvider,
696        )
697        .unwrap_err();
698        assert!(matches!(err, Error::EmptyResponseMetadata), "got: {err:?}");
699    }
700
701    // -------- GetGroupActionSignersRequest / GroupActionSigners --------
702
703    #[test]
704    fn group_action_signers_empty_version_on_request_missing() {
705        let request = GetGroupActionSignersRequest { version: None };
706        let response = GetGroupActionSignersResponse::default();
707        let err = <GroupActionSigners as FromProof<_>>::maybe_from_proof(
708            request,
709            response,
710            Network::Testnet,
711            pv(),
712            &UnreachableProvider,
713        )
714        .unwrap_err();
715        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
716    }
717
718    #[test]
719    fn group_action_signers_request_error_on_bad_contract_id() {
720        let request = GetGroupActionSignersRequest {
721            version: Some(SignersReqVersion::V0(GetGroupActionSignersRequestV0 {
722                contract_id: vec![0u8; 9], // bad
723                group_contract_position: 0,
724                status: 0,
725                action_id: vec![1u8; 32],
726                prove: true,
727            })),
728        };
729        let response = GetGroupActionSignersResponse::default();
730        let err = <GroupActionSigners as FromProof<_>>::maybe_from_proof(
731            request,
732            response,
733            Network::Testnet,
734            pv(),
735            &UnreachableProvider,
736        )
737        .unwrap_err();
738        match err {
739            Error::RequestError { error } => assert!(error.contains("contract_id"), "got: {error}"),
740            other => panic!("expected RequestError, got: {other:?}"),
741        }
742    }
743
744    #[test]
745    fn group_action_signers_request_error_on_bad_action_id() {
746        let request = GetGroupActionSignersRequest {
747            version: Some(SignersReqVersion::V0(GetGroupActionSignersRequestV0 {
748                contract_id: vec![0u8; 32],
749                group_contract_position: 0,
750                status: 0,
751                action_id: vec![1u8; 3], // bad
752                prove: true,
753            })),
754        };
755        let response = GetGroupActionSignersResponse::default();
756        let err = <GroupActionSigners as FromProof<_>>::maybe_from_proof(
757            request,
758            response,
759            Network::Testnet,
760            pv(),
761            &UnreachableProvider,
762        )
763        .unwrap_err();
764        match err {
765            Error::RequestError { error } => assert!(error.contains("action_id"), "got: {error}"),
766            other => panic!("expected RequestError, got: {other:?}"),
767        }
768    }
769
770    #[test]
771    fn group_action_signers_request_error_on_bad_status() {
772        let request = GetGroupActionSignersRequest {
773            version: Some(SignersReqVersion::V0(GetGroupActionSignersRequestV0 {
774                contract_id: vec![0u8; 32],
775                group_contract_position: 0,
776                status: 42, // invalid
777                action_id: vec![1u8; 32],
778                prove: true,
779            })),
780        };
781        let response = GetGroupActionSignersResponse::default();
782        let err = <GroupActionSigners as FromProof<_>>::maybe_from_proof(
783            request,
784            response,
785            Network::Testnet,
786            pv(),
787            &UnreachableProvider,
788        )
789        .unwrap_err();
790        match err {
791            Error::RequestError { error } => {
792                assert!(error.contains("GroupActionStatus"), "got: {error}")
793            }
794            other => panic!("expected RequestError, got: {other:?}"),
795        }
796    }
797}