Skip to main content

drive_proof_verifier/proof/
groups.rs

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