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 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 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 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 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) }
190 fn get_quorum_public_key(
191 &self,
192 _qt: u32,
193 _qh: [u8; 32],
194 _h: u32,
195 ) -> Result<[u8; 48], ContextProviderError> {
196 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 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 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}