Skip to main content

drive_proof_verifier/proof/
token_perpetual_distribution_last_claim.rs

1use crate::verify::supported_grovedb_proof_bytes;
2use dapi_grpc::platform::v0::{
3    get_token_perpetual_distribution_last_claim_request::Version as RequestVersion,
4    get_token_perpetual_distribution_last_claim_response::{
5        get_token_perpetual_distribution_last_claim_response_v0, Version as ResponseVersion,
6    },
7    GetTokenPerpetualDistributionLastClaimResponse, Proof, ResponseMetadata,
8};
9use dpp::{
10    dashcore::Network,
11    data_contract::associated_token::{
12        token_configuration::accessors::v0::TokenConfigurationV0Getters,
13        token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters,
14        token_perpetual_distribution::{
15            methods::v0::TokenPerpetualDistributionV0Accessors,
16            reward_distribution_moment::RewardDistributionMoment,
17        },
18    },
19    prelude::Identifier,
20    version::PlatformVersion,
21};
22use drive::drive::Drive;
23use get_token_perpetual_distribution_last_claim_response_v0::Result as RespResult;
24
25use crate::{verify::verify_tenderdash_proof, ContextProvider, Error};
26
27use super::FromProof;
28use dapi_grpc::platform::v0::GetTokenPerpetualDistributionLastClaimRequest;
29
30impl FromProof<GetTokenPerpetualDistributionLastClaimRequest> for RewardDistributionMoment {
31    type Request = GetTokenPerpetualDistributionLastClaimRequest;
32    type Response = GetTokenPerpetualDistributionLastClaimResponse;
33
34    /// Parse & verify the last‑claim proof returned by Platform.
35    fn maybe_from_proof_with_metadata<'a, I: Into<Self::Request>, O: Into<Self::Response>>(
36        request: I,
37        response: O,
38        _network: Network,
39        platform_version: &PlatformVersion,
40        provider: &'a dyn ContextProvider,
41    ) -> Result<(Option<Self>, ResponseMetadata, Proof), Error>
42    where
43        Self: Sized + 'a,
44    {
45        let request = request.into();
46        let response = response.into();
47
48        let RequestVersion::V0(req_v0) = request.version.ok_or(Error::EmptyVersion)?;
49
50        let token_id: [u8; 32] =
51            req_v0
52                .token_id
53                .as_slice()
54                .try_into()
55                .map_err(|_| Error::RequestError {
56                    error: "token_id must be 32 bytes".into(),
57                })?;
58
59        let identity_id: [u8; 32] =
60            req_v0
61                .identity_id
62                .as_slice()
63                .try_into()
64                .map_err(|_| Error::RequestError {
65                    error: "identity_id must be 32 bytes".into(),
66                })?;
67
68        let ResponseVersion::V0(resp_v0) = response.version.ok_or(Error::EmptyVersion)?;
69
70        let metadata = resp_v0
71            .metadata
72            .clone()
73            .ok_or(Error::EmptyResponseMetadata)?;
74
75        let result = resp_v0.result.clone().ok_or(Error::NoProofInResult)?;
76
77        match result {
78            RespResult::Proof(proof_msg) => {
79                let maybe_distribution_type = {
80                    let token_id_identifier = Identifier::from_vec(req_v0.token_id.clone())
81                        .map_err(|_| Error::RequestError {
82                            error: "token_id must be 32 bytes".into(),
83                        })?;
84
85                    let maybe_token_config =
86                        provider.get_token_configuration(&token_id_identifier)?;
87                    let maybe_dist_type = maybe_token_config
88                        .as_ref()
89                        .and_then(|cfg| cfg.distribution_rules().perpetual_distribution())
90                        .map(|perp| perp.distribution_type().clone());
91
92                    maybe_dist_type
93                };
94
95                match maybe_distribution_type {
96                    Some(distribution_type) => {
97                        let (root_hash, moment_opt) =
98                            Drive::verify_token_perpetual_distribution_last_paid_time(
99                                supported_grovedb_proof_bytes(&proof_msg)?,
100                                token_id,
101                                identity_id,
102                                &distribution_type,
103                                false,
104                                platform_version,
105                            )?;
106
107                        verify_tenderdash_proof(&proof_msg, &metadata, &root_hash, provider)?;
108
109                        // May be None if identity has not yet claimed
110                        Ok((moment_opt, metadata, proof_msg))
111                    }
112                    None => Err(Error::RequestError {
113                        error: "Token distribution type not found with get_token_distribution()"
114                            .into(),
115                    }),
116                }
117            }
118
119            RespResult::LastClaim(_) => Err(Error::RequestError {
120                error: "Non-proof LastClaim response is not supported in rs-sdk".into(),
121            }),
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::FromProof;
130    use dapi_grpc::platform::v0::get_token_perpetual_distribution_last_claim_request::{
131        GetTokenPerpetualDistributionLastClaimRequestV0, Version as ReqVersion,
132    };
133    use dapi_grpc::platform::v0::get_token_perpetual_distribution_last_claim_response::{
134        get_token_perpetual_distribution_last_claim_response_v0::LastClaimInfo,
135        GetTokenPerpetualDistributionLastClaimResponseV0, Version as RespVersion,
136    };
137    use dash_context_provider::ContextProviderError;
138    use dpp::data_contract::TokenConfiguration;
139    use dpp::prelude::{CoreBlockHeight, DataContract};
140    use std::sync::Arc;
141
142    /// Provider that panics — tests must error out before reaching it.
143    struct UnreachableProvider;
144
145    impl ContextProvider for UnreachableProvider {
146        fn get_data_contract(
147            &self,
148            _id: &Identifier,
149            _pv: &PlatformVersion,
150        ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
151            panic!("should not be called")
152        }
153        fn get_token_configuration(
154            &self,
155            _id: &Identifier,
156        ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
157            panic!("should not be called")
158        }
159        fn get_quorum_public_key(
160            &self,
161            _qt: u32,
162            _qh: [u8; 32],
163            _h: u32,
164        ) -> Result<[u8; 48], ContextProviderError> {
165            panic!("should not be called")
166        }
167        fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
168            panic!("should not be called")
169        }
170    }
171
172    /// Provider that reports no token configuration — used to exercise the
173    /// "Token distribution type not found" error branch.
174    struct NoTokenConfigProvider;
175
176    impl ContextProvider for NoTokenConfigProvider {
177        fn get_data_contract(
178            &self,
179            _id: &Identifier,
180            _pv: &PlatformVersion,
181        ) -> Result<Option<Arc<DataContract>>, ContextProviderError> {
182            Ok(None)
183        }
184        fn get_token_configuration(
185            &self,
186            _id: &Identifier,
187        ) -> Result<Option<TokenConfiguration>, ContextProviderError> {
188            Ok(None) // no config ⇒ distribution type is None
189        }
190        fn get_quorum_public_key(
191            &self,
192            _qt: u32,
193            _qh: [u8; 32],
194            _h: u32,
195        ) -> Result<[u8; 48], ContextProviderError> {
196            // Unreachable in these tests: we fail before tenderdash proof verification.
197            Ok([0u8; 48])
198        }
199        fn get_platform_activation_height(&self) -> Result<CoreBlockHeight, ContextProviderError> {
200            Ok(1)
201        }
202    }
203
204    fn pv() -> &'static PlatformVersion {
205        PlatformVersion::latest()
206    }
207
208    fn make_response_with_proof() -> GetTokenPerpetualDistributionLastClaimResponse {
209        GetTokenPerpetualDistributionLastClaimResponse {
210            version: Some(RespVersion::V0(
211                GetTokenPerpetualDistributionLastClaimResponseV0 {
212                    result: Some(RespResult::Proof(Proof::default())),
213                    metadata: Some(ResponseMetadata::default()),
214                },
215            )),
216        }
217    }
218
219    fn make_request(
220        token_id: Vec<u8>,
221        identity_id: Vec<u8>,
222    ) -> GetTokenPerpetualDistributionLastClaimRequest {
223        GetTokenPerpetualDistributionLastClaimRequest {
224            version: Some(ReqVersion::V0(
225                GetTokenPerpetualDistributionLastClaimRequestV0 {
226                    token_id,
227                    contract_info: None,
228                    identity_id,
229                    prove: true,
230                },
231            )),
232        }
233    }
234
235    #[test]
236    fn empty_version_when_request_has_no_version() {
237        let request = GetTokenPerpetualDistributionLastClaimRequest { version: None };
238        let response = make_response_with_proof();
239        let err = <RewardDistributionMoment as FromProof<_>>::maybe_from_proof(
240            request,
241            response,
242            Network::Testnet,
243            pv(),
244            &UnreachableProvider,
245        )
246        .unwrap_err();
247        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
248    }
249
250    #[test]
251    fn request_error_when_token_id_wrong_length() {
252        let request = make_request(vec![0u8; 16], vec![1u8; 32]);
253        let response = make_response_with_proof();
254        let err = <RewardDistributionMoment as FromProof<_>>::maybe_from_proof(
255            request,
256            response,
257            Network::Testnet,
258            pv(),
259            &UnreachableProvider,
260        )
261        .unwrap_err();
262        match err {
263            Error::RequestError { error } => assert!(
264                error.contains("token_id"),
265                "error should mention token_id, got: {error}"
266            ),
267            other => panic!("expected RequestError, got: {other:?}"),
268        }
269    }
270
271    #[test]
272    fn request_error_when_identity_id_wrong_length() {
273        let request = make_request(vec![0u8; 32], vec![1u8; 10]);
274        let response = make_response_with_proof();
275        let err = <RewardDistributionMoment as FromProof<_>>::maybe_from_proof(
276            request,
277            response,
278            Network::Testnet,
279            pv(),
280            &UnreachableProvider,
281        )
282        .unwrap_err();
283        match err {
284            Error::RequestError { error } => assert!(
285                error.contains("identity_id"),
286                "error should mention identity_id, got: {error}"
287            ),
288            other => panic!("expected RequestError, got: {other:?}"),
289        }
290    }
291
292    #[test]
293    fn empty_version_when_response_has_no_version() {
294        let request = make_request(vec![0u8; 32], vec![1u8; 32]);
295        let response = GetTokenPerpetualDistributionLastClaimResponse { version: None };
296        let err = <RewardDistributionMoment as FromProof<_>>::maybe_from_proof(
297            request,
298            response,
299            Network::Testnet,
300            pv(),
301            &UnreachableProvider,
302        )
303        .unwrap_err();
304        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
305    }
306
307    #[test]
308    fn empty_response_metadata_when_metadata_missing() {
309        let request = make_request(vec![0u8; 32], vec![1u8; 32]);
310        let response = GetTokenPerpetualDistributionLastClaimResponse {
311            version: Some(RespVersion::V0(
312                GetTokenPerpetualDistributionLastClaimResponseV0 {
313                    result: Some(RespResult::Proof(Proof::default())),
314                    metadata: None,
315                },
316            )),
317        };
318        let err = <RewardDistributionMoment as FromProof<_>>::maybe_from_proof(
319            request,
320            response,
321            Network::Testnet,
322            pv(),
323            &UnreachableProvider,
324        )
325        .unwrap_err();
326        assert!(matches!(err, Error::EmptyResponseMetadata), "got: {err:?}");
327    }
328
329    #[test]
330    fn no_proof_in_result_when_result_missing() {
331        let request = make_request(vec![0u8; 32], vec![1u8; 32]);
332        let response = GetTokenPerpetualDistributionLastClaimResponse {
333            version: Some(RespVersion::V0(
334                GetTokenPerpetualDistributionLastClaimResponseV0 {
335                    result: None,
336                    metadata: Some(ResponseMetadata::default()),
337                },
338            )),
339        };
340        let err = <RewardDistributionMoment as FromProof<_>>::maybe_from_proof(
341            request,
342            response,
343            Network::Testnet,
344            pv(),
345            &UnreachableProvider,
346        )
347        .unwrap_err();
348        assert!(matches!(err, Error::NoProofInResult), "got: {err:?}");
349    }
350
351    #[test]
352    fn request_error_when_last_claim_variant_returned_instead_of_proof() {
353        // This branch explicitly rejects direct LastClaim responses.
354        let request = make_request(vec![0u8; 32], vec![1u8; 32]);
355        let response = GetTokenPerpetualDistributionLastClaimResponse {
356            version: Some(RespVersion::V0(
357                GetTokenPerpetualDistributionLastClaimResponseV0 {
358                    result: Some(RespResult::LastClaim(LastClaimInfo { paid_at: None })),
359                    metadata: Some(ResponseMetadata::default()),
360                },
361            )),
362        };
363        let err = <RewardDistributionMoment as FromProof<_>>::maybe_from_proof(
364            request,
365            response,
366            Network::Testnet,
367            pv(),
368            &UnreachableProvider,
369        )
370        .unwrap_err();
371        match err {
372            Error::RequestError { error } => assert!(
373                error.contains("Non-proof LastClaim"),
374                "error should mention Non-proof LastClaim, got: {error}"
375            ),
376            other => panic!("expected RequestError, got: {other:?}"),
377        }
378    }
379
380    #[test]
381    fn request_error_when_token_config_returns_none() {
382        // token_id is valid 32 bytes; provider returns None for the token config.
383        // The code builds `maybe_distribution_type = None` and errors with
384        // "Token distribution type not found".
385        let request = make_request(vec![42u8; 32], vec![7u8; 32]);
386        let response = make_response_with_proof();
387        let err = <RewardDistributionMoment as FromProof<_>>::maybe_from_proof(
388            request,
389            response,
390            Network::Testnet,
391            pv(),
392            &NoTokenConfigProvider,
393        )
394        .unwrap_err();
395        match err {
396            Error::RequestError { error } => assert!(
397                error.contains("Token distribution type not found"),
398                "error should mention 'Token distribution type not found', got: {error}"
399            ),
400            other => panic!("expected RequestError, got: {other:?}"),
401        }
402    }
403}