Skip to main content

drive_proof_verifier/proof/
token_perpetual_distribution_last_claim.rs

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