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