1use crate::types::contract_groups::{
2 contract_group_info_from_proto, members_page_from_response, members_query_from_request,
3 memberships_from_response, ContractGroupInfo, ContractGroupMembersPage,
4 ContractGroupMembershipsForContract,
5};
6use crate::types::contract_moderation::{
7 entries_from_response, fee_pots_from_response, list_from_request, lists_from_request,
8 moderation_action_counts_from_response, reason_from_response, removals_from_response,
9 removals_query_from_request, team_action_signers_from_response,
10 team_action_status_from_request, team_actions_from_response, team_actions_query_from_request,
11 warnings_from_response, ContractBan, ContractDocumentRemovals, ContractFeePots,
12 ContractModerationActionCounts, ContractModerationEntries, ContractModerationList,
13 ContractModerationListStatuses, ContractModerationStatus, ContractSuspension,
14 ContractTeamActionSigners, ContractTeamActions,
15};
16use crate::types::data_contracts_latest_versions::{
17 DataContractLatestVersion, DataContractsLatestVersions,
18};
19use crate::types::evonode_status::EvoNodeStatus;
20use crate::types::identity_keys_remaining_budgets::IdentityKeysRemainingBudgets;
21use crate::types::CurrentQuorumsInfo;
22use crate::Error;
23use dapi_grpc::platform::v0::ResponseMetadata;
24use dapi_grpc::platform::v0::{self as platform};
25use dapi_grpc::platform::v0::get_contract_moderation_action_counts_response::get_contract_moderation_action_counts_response_v0::Result as ModerationActionCountsResult;
26use dapi_grpc::tonic::async_trait;
27use dpp::bls_signatures::PublicKey as BlsPublicKey;
28use dpp::core_types::validator::v0::ValidatorV0;
29use dpp::core_types::validator_set::v0::ValidatorSetV0;
30use dpp::core_types::validator_set::ValidatorSet;
31use dpp::dashcore::hashes::Hash;
32use dpp::dashcore::{Network, ProTxHash, PubkeyHash, QuorumHash};
33use dpp::data_contract::DataContract;
34use dpp::identifier::Identifier;
35use dpp::serialization::PlatformDeserializableWithPotentialValidationFromVersionedStructureUntrusted;
36use dpp::version::PlatformVersion;
37use std::collections::BTreeMap;
38
39fn parse_hash_32(field: &str, bytes: &[u8]) -> Result<[u8; 32], Error> {
40 bytes.try_into().map_err(|_| Error::ProtocolError {
41 error: format!(
42 "Invalid {field} length: expected 32 bytes, received {}",
43 bytes.len()
44 ),
45 })
46}
47
48pub trait FromUnproved<Req> {
73 type Request;
75 type Response;
77
78 fn maybe_from_unproved<I: Into<Self::Request>, O: Into<Self::Response>>(
93 request: I,
94 response: O,
95 network: Network,
96 platform_version: &PlatformVersion,
97 ) -> Result<Option<Self>, Error>
98 where
99 Self: Sized,
100 {
101 Self::maybe_from_unproved_with_metadata(request, response, network, platform_version)
102 .map(|maybe_result| maybe_result.0)
103 }
104
105 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
120 request: I,
121 response: O,
122 network: Network,
123 platform_version: &PlatformVersion,
124 ) -> Result<(Option<Self>, ResponseMetadata), Error>
125 where
126 Self: Sized;
127
128 fn from_unproved<I: Into<Self::Request>, O: Into<Self::Response>>(
143 request: I,
144 response: O,
145 network: Network,
146 platform_version: &PlatformVersion,
147 ) -> Result<Self, Error>
148 where
149 Self: Sized,
150 {
151 Self::maybe_from_unproved(request, response, network, platform_version)?
152 .ok_or(Error::NotFound)
153 }
154
155 fn from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
170 request: I,
171 response: O,
172 network: Network,
173 platform_version: &PlatformVersion,
174 ) -> Result<(Self, ResponseMetadata), Error>
175 where
176 Self: Sized,
177 {
178 let (main_item, response_metadata) =
179 Self::maybe_from_unproved_with_metadata(request, response, network, platform_version)?;
180 Ok((main_item.ok_or(Error::NotFound)?, response_metadata))
181 }
182}
183
184impl FromUnproved<platform::GetCurrentQuorumsInfoRequest> for CurrentQuorumsInfo {
185 type Request = platform::GetCurrentQuorumsInfoRequest;
186 type Response = platform::GetCurrentQuorumsInfoResponse;
187
188 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
189 _request: I,
190 response: O,
191 _network: Network,
192 _platform_version: &PlatformVersion,
193 ) -> Result<(Option<Self>, ResponseMetadata), Error>
194 where
195 Self: Sized,
196 {
197 let response: platform::GetCurrentQuorumsInfoResponse = response.into();
199
200 let metadata = match &response.version {
202 Some(platform::get_current_quorums_info_response::Version::V0(ref v0)) => {
203 v0.metadata.clone()
204 }
205 None => None,
206 }
207 .ok_or(Error::EmptyResponseMetadata)?;
208
209 let info = match response.version.ok_or(Error::EmptyVersion)? {
211 platform::get_current_quorums_info_response::Version::V0(v0) => {
212 let quorum_hashes = v0
214 .quorum_hashes
215 .into_iter()
216 .map(|q_hash| parse_hash_32("quorum_hash", &q_hash))
217 .collect::<Result<Vec<[u8; 32]>, Error>>()?;
218
219 let current_quorum_hash =
221 parse_hash_32("current_quorum_hash", &v0.current_quorum_hash)?;
222
223 let last_block_proposer =
224 parse_hash_32("last_block_proposer", &v0.last_block_proposer)?;
225
226 let validator_sets =
228 v0.validator_sets
229 .into_iter()
230 .map(|vs| {
231 let quorum_hash =
233 parse_hash_32("validator_set.quorum_hash", &vs.quorum_hash)?;
234
235 let members = vs
237 .members
238 .into_iter()
239 .map(|member| {
240 let pro_tx_hash = ProTxHash::from_slice(&member.pro_tx_hash)
241 .map_err(|_| Error::ProtocolError {
242 error: "Invalid ProTxHash format".to_string(),
243 })?;
244 let validator = ValidatorV0 {
245 pro_tx_hash,
246 public_key: None, node_ip: member.node_ip,
248 node_id: PubkeyHash::from_slice(&[0; 20]).expect("expected to make pub key hash from 20 byte empty array"), core_port: 0, platform_http_port: 0, platform_p2p_port: 0, is_banned: member.is_banned,
253 };
254 Ok((pro_tx_hash, validator))
255 })
256 .collect::<Result<BTreeMap<ProTxHash, ValidatorV0>, Error>>()?;
257
258 Ok(ValidatorSet::V0(ValidatorSetV0 {
259 quorum_hash: QuorumHash::from_slice(quorum_hash.as_slice())
260 .map_err(|_| Error::ProtocolError {
261 error: "Invalid Quorum Hash format".to_string(),
262 })?,
263 quorum_index: None, core_height: vs.core_height,
265 members,
266 threshold_public_key: BlsPublicKey::try_from(
267 vs.threshold_public_key.as_slice(),
268 )
269 .map_err(|_| Error::ProtocolError {
270 error: "Invalid BlsPublicKey format".to_string(),
271 })?,
272 }))
273 })
274 .collect::<Result<Vec<ValidatorSet>, Error>>()?;
275
276 Ok::<CurrentQuorumsInfo, Error>(CurrentQuorumsInfo {
278 quorum_hashes,
279 current_quorum_hash,
280 validator_sets,
281 last_block_proposer,
282 last_platform_block_height: metadata.height,
283 last_core_block_height: metadata.core_chain_locked_height,
284 })
285 }
286 }?;
287
288 Ok((Some(info), metadata))
289 }
290}
291
292#[async_trait]
293impl FromUnproved<platform::GetStatusRequest> for EvoNodeStatus {
294 type Request = platform::GetStatusRequest;
295 type Response = platform::GetStatusResponse;
296
297 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
298 _request: I,
299 response: O,
300 _network: Network,
301 _platform_version: &PlatformVersion,
302 ) -> Result<(Option<Self>, ResponseMetadata), Error>
303 where
304 Self: Sized,
305 {
306 let status = Self::try_from(response.into())?;
307 Ok((Some(status), Default::default()))
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use dapi_grpc::platform::v0::{
316 get_current_quorums_info_response, get_status_response, ResponseMetadata,
317 };
318 use dpp::bls_signatures::{Bls12381G2Impl, SecretKey};
319 use dpp::version::PlatformVersion;
320
321 fn generate_valid_bls_public_key_bytes(seed: u8) -> Vec<u8> {
324 let mut secret_bytes = [0u8; 32];
325 secret_bytes[31] = seed.max(1); let sk: SecretKey<Bls12381G2Impl> =
327 SecretKey::<Bls12381G2Impl>::from_be_bytes(&secret_bytes)
328 .into_option()
329 .expect("valid secret key");
330 sk.public_key().0.to_compressed().to_vec()
331 }
332
333 fn build_valid_quorums_info_response() -> platform::GetCurrentQuorumsInfoResponse {
336 let quorum_hash = vec![1u8; 32];
337 let current_quorum_hash = vec![2u8; 32];
338 let last_block_proposer = vec![3u8; 32];
339 let pro_tx_hash = vec![4u8; 32];
340 let threshold_public_key = generate_valid_bls_public_key_bytes(42);
341
342 let member = get_current_quorums_info_response::ValidatorV0 {
343 pro_tx_hash: pro_tx_hash.clone(),
344 node_ip: "127.0.0.1".to_string(),
345 is_banned: false,
346 };
347
348 let validator_set = get_current_quorums_info_response::ValidatorSetV0 {
349 quorum_hash: quorum_hash.clone(),
350 core_height: 100,
351 members: vec![member],
352 threshold_public_key,
353 };
354
355 let v0 = get_current_quorums_info_response::GetCurrentQuorumsInfoResponseV0 {
356 quorum_hashes: vec![quorum_hash],
357 current_quorum_hash,
358 validator_sets: vec![validator_set],
359 last_block_proposer,
360 metadata: Some(ResponseMetadata {
361 height: 500,
362 core_chain_locked_height: 200,
363 epoch: 10,
364 time_ms: 1234567890,
365 protocol_version: 1,
366 chain_id: "dash-testnet-1".to_string(),
367 }),
368 };
369
370 platform::GetCurrentQuorumsInfoResponse {
371 version: Some(get_current_quorums_info_response::Version::V0(v0)),
372 }
373 }
374
375 fn build_valid_status_response() -> platform::GetStatusResponse {
377 use dapi_grpc::platform::v0::get_status_response::get_status_response_v0;
378
379 let software = get_status_response_v0::version::Software {
380 dapi: "1.0.0".to_string(),
381 drive: Some("2.0.0".to_string()),
382 tenderdash: Some("0.14.0".to_string()),
383 };
384
385 let tenderdash_protocol =
386 get_status_response_v0::version::protocol::Tenderdash { p2p: 8, block: 11 };
387
388 let drive_protocol = get_status_response_v0::version::protocol::Drive {
389 latest: 5,
390 current: 4,
391 next_epoch: 5,
392 };
393
394 let protocol = get_status_response_v0::version::Protocol {
395 tenderdash: Some(tenderdash_protocol),
396 drive: Some(drive_protocol),
397 };
398
399 let version = get_status_response_v0::Version {
400 software: Some(software),
401 protocol: Some(protocol),
402 };
403
404 let time = get_status_response_v0::Time {
405 local: 1700000000,
406 block: Some(1699999900),
407 genesis: Some(1690000000),
408 epoch: Some(42),
409 };
410
411 let node = get_status_response_v0::Node {
412 id: vec![10u8; 20],
413 pro_tx_hash: Some(vec![11u8; 32]),
414 };
415
416 let chain = get_status_response_v0::Chain {
417 catching_up: false,
418 latest_block_hash: vec![20u8; 32],
419 latest_app_hash: vec![21u8; 32],
420 latest_block_height: 1000,
421 earliest_block_hash: vec![22u8; 32],
422 earliest_app_hash: vec![23u8; 32],
423 earliest_block_height: 1,
424 max_peer_block_height: 1001,
425 core_chain_locked_height: Some(500),
426 };
427
428 let network = get_status_response_v0::Network {
429 chain_id: "dash-testnet-1".to_string(),
430 peers_count: 25,
431 listening: true,
432 };
433
434 let state_sync = get_status_response_v0::StateSync {
435 total_synced_time: 3600,
436 remaining_time: 120,
437 total_snapshots: 5,
438 chunk_process_avg_time: 50,
439 snapshot_height: 900,
440 snapshot_chunks_count: 100,
441 backfilled_blocks: 800,
442 backfill_blocks_total: 1000,
443 };
444
445 let v0 = get_status_response::GetStatusResponseV0 {
446 version: Some(version),
447 node: Some(node),
448 chain: Some(chain),
449 network: Some(network),
450 state_sync: Some(state_sync),
451 time: Some(time),
452 };
453
454 platform::GetStatusResponse {
455 version: Some(get_status_response::Version::V0(v0)),
456 }
457 }
458
459 #[test]
460 fn test_current_quorums_info_valid_response() {
461 let request = platform::GetCurrentQuorumsInfoRequest { version: None };
462 let response = build_valid_quorums_info_response();
463 let platform_version = PlatformVersion::latest();
464
465 let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
466 request,
467 response,
468 Network::Testnet,
469 platform_version,
470 );
471
472 let (maybe_info, metadata) = result.expect("should parse valid response");
473 let info = maybe_info.expect("should contain CurrentQuorumsInfo");
474
475 assert_eq!(info.quorum_hashes.len(), 1);
476 assert_eq!(info.quorum_hashes[0], [1u8; 32]);
477 assert_eq!(info.current_quorum_hash, [2u8; 32]);
478 assert_eq!(info.last_block_proposer, [3u8; 32]);
479 assert_eq!(info.validator_sets.len(), 1);
480 assert_eq!(info.last_platform_block_height, 500);
481 assert_eq!(info.last_core_block_height, 200);
482 assert_eq!(metadata.height, 500);
483 assert_eq!(metadata.core_chain_locked_height, 200);
484 }
485
486 #[test]
487 fn test_current_quorums_info_invalid_quorum_hash_length() {
488 let mut response = build_valid_quorums_info_response();
489
490 if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
492 v0.quorum_hashes = vec![vec![0u8; 16]]; }
494
495 let request = platform::GetCurrentQuorumsInfoRequest { version: None };
496 let platform_version = PlatformVersion::latest();
497
498 let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
499 request,
500 response,
501 Network::Testnet,
502 platform_version,
503 );
504
505 let err = result.expect_err("should fail for invalid quorum_hash length");
506 let err_string = err.to_string();
507 assert!(
508 err_string.contains("Invalid quorum_hash length"),
509 "unexpected error: {err_string}"
510 );
511 }
512
513 #[test]
514 fn test_current_quorums_info_invalid_current_quorum_hash_length() {
515 let mut response = build_valid_quorums_info_response();
516
517 if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
519 v0.current_quorum_hash = vec![0u8; 10]; }
521
522 let request = platform::GetCurrentQuorumsInfoRequest { version: None };
523 let platform_version = PlatformVersion::latest();
524
525 let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
526 request,
527 response,
528 Network::Testnet,
529 platform_version,
530 );
531
532 let err = result.expect_err("should fail for invalid current_quorum_hash length");
533 let err_string = err.to_string();
534 assert!(
535 err_string.contains("Invalid current_quorum_hash length"),
536 "unexpected error: {err_string}"
537 );
538 }
539
540 #[test]
541 fn test_current_quorums_info_invalid_last_block_proposer() {
542 let mut response = build_valid_quorums_info_response();
543
544 if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
546 v0.last_block_proposer = vec![0u8; 5]; }
548
549 let request = platform::GetCurrentQuorumsInfoRequest { version: None };
550 let platform_version = PlatformVersion::latest();
551
552 let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
553 request,
554 response,
555 Network::Testnet,
556 platform_version,
557 );
558
559 let err = result.expect_err("should fail for invalid last_block_proposer length");
560 let err_string = err.to_string();
561 assert!(
562 err_string.contains("Invalid last_block_proposer length"),
563 "unexpected error: {err_string}"
564 );
565 }
566
567 #[test]
568 fn test_current_quorums_info_rejects_malformed_nested_quorum_hashes() {
569 for invalid_length in [0, 1, 31, 33, 1024] {
570 let mut response = build_valid_quorums_info_response();
571 if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) =
572 response.version
573 {
574 v0.validator_sets[0].quorum_hash = vec![0u8; invalid_length];
575 }
576
577 let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
578 platform::GetCurrentQuorumsInfoRequest { version: None },
579 response,
580 Network::Testnet,
581 PlatformVersion::latest(),
582 );
583
584 let error = result.expect_err("malformed nested quorum hash must return an error");
585 assert!(
586 matches!(error, Error::ProtocolError { ref error } if error.contains("validator_set.quorum_hash")),
587 "unexpected error for length {invalid_length}: {error:?}"
588 );
589 }
590 }
591
592 #[test]
593 fn test_current_quorums_info_none_metadata() {
594 let mut response = build_valid_quorums_info_response();
595
596 if let Some(get_current_quorums_info_response::Version::V0(ref mut v0)) = response.version {
598 v0.metadata = None;
599 }
600
601 let request = platform::GetCurrentQuorumsInfoRequest { version: None };
602 let platform_version = PlatformVersion::latest();
603
604 let result = CurrentQuorumsInfo::maybe_from_unproved_with_metadata(
605 request,
606 response,
607 Network::Testnet,
608 platform_version,
609 );
610
611 let err = result.expect_err("should fail when metadata is missing");
612 let err_string = err.to_string();
613 assert!(
614 err_string.contains("empty response metadata"),
615 "unexpected error: {err_string}"
616 );
617 }
618
619 #[test]
620 fn test_evo_node_status_valid_response() {
621 let request = platform::GetStatusRequest { version: None };
622 let response = build_valid_status_response();
623 let platform_version = PlatformVersion::latest();
624
625 let result = EvoNodeStatus::maybe_from_unproved_with_metadata(
626 request,
627 response,
628 Network::Testnet,
629 platform_version,
630 );
631
632 let (maybe_status, _metadata) = result.expect("should parse valid status response");
633 let status = maybe_status.expect("should contain EvoNodeStatus");
634
635 let software = status.version.software.as_ref().unwrap();
637 assert_eq!(software.dapi, "1.0.0");
638 assert_eq!(software.drive.as_deref(), Some("2.0.0"));
639 assert_eq!(software.tenderdash.as_deref(), Some("0.14.0"));
640
641 let protocol = status.version.protocol.as_ref().unwrap();
642 let td = protocol.tenderdash.as_ref().unwrap();
643 assert_eq!(td.p2p, 8);
644 assert_eq!(td.block, 11);
645 let drv = protocol.drive.as_ref().unwrap();
646 assert_eq!(drv.latest, 5);
647 assert_eq!(drv.current, 4);
648 assert_eq!(drv.next_epoch, 5);
649
650 assert_eq!(status.node.id, vec![10u8; 20]);
652 assert_eq!(status.node.pro_tx_hash, Some(vec![11u8; 32]));
653
654 assert!(!status.chain.catching_up);
656 assert_eq!(status.chain.latest_block_height, 1000);
657 assert_eq!(status.chain.core_chain_locked_height, Some(500));
658
659 assert_eq!(status.network.chain_id, "dash-testnet-1");
661 assert_eq!(status.network.peers_count, 25);
662 assert!(status.network.listening);
663
664 assert_eq!(status.time.local, 1700000000);
666 assert_eq!(status.time.block, Some(1699999900));
667 assert_eq!(status.time.epoch, Some(42));
668 }
669}
670
671impl FromUnproved<platform::GetDataContractsLatestVersionsRequest> for DataContractsLatestVersions {
672 type Request = platform::GetDataContractsLatestVersionsRequest;
673 type Response = platform::GetDataContractsLatestVersionsResponse;
674
675 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
676 _request: I,
677 response: O,
678 _network: Network,
679 platform_version: &PlatformVersion,
680 ) -> Result<(Option<Self>, ResponseMetadata), Error>
681 where
682 Self: Sized,
683 {
684 use platform::get_data_contracts_latest_versions_response::get_data_contracts_latest_versions_response_v0::Result as V0Result;
685
686 let response: Self::Response = response.into();
687
688 let platform::get_data_contracts_latest_versions_response::Version::V0(v0) =
689 response.version.ok_or(Error::EmptyVersion)?;
690 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
691
692 let versions = match v0.result {
693 Some(V0Result::DataContractsLatestVersions(versions)) => versions
694 .entries
695 .into_iter()
696 .map(|entry| {
697 let id = Identifier::from_bytes(&entry.identifier).map_err(|e| {
698 Error::ProtocolError {
699 error: e.to_string(),
700 }
701 })?;
702 let latest_version = entry
703 .version
704 .map(|version| {
705 let data_contract = entry
706 .data_contract
707 .map(|bytes| {
708 DataContract::versioned_deserialize_untrusted(
709 &bytes,
710 false,
711 platform_version,
712 )
713 })
714 .transpose()
715 .map_err(|e| Error::ProtocolError {
716 error: e.to_string(),
717 })?;
718 Ok::<_, Error>(DataContractLatestVersion {
719 version,
720 data_contract,
721 })
722 })
723 .transpose()?;
724 Ok((id, latest_version))
725 })
726 .collect::<Result<DataContractsLatestVersions, Error>>()
727 .map(Some)?,
728 Some(V0Result::Proof(_)) => {
729 return Err(Error::ResponseDecodeError {
730 error: "expected unproved data contract versions, got a proof".to_string(),
731 })
732 }
733 None => None,
734 };
735
736 Ok((versions, metadata))
737 }
738}
739
740#[cfg(test)]
741mod data_contracts_latest_versions_tests {
742 use super::*;
743 use dapi_grpc::platform::v0::get_data_contracts_latest_versions_response::{
744 get_data_contracts_latest_versions_response_v0::Result as V0Result,
745 DataContractLatestVersionEntry, DataContractsLatestVersions as ProtoLatestVersions,
746 GetDataContractsLatestVersionsResponseV0, Version,
747 };
748 use dpp::data_contract::accessors::v0::DataContractV0Getters;
749 use dpp::serialization::PlatformSerializableWithPlatformVersion;
750 use dpp::tests::fixtures::get_data_contract_fixture;
751
752 fn response(result: Option<V0Result>) -> platform::GetDataContractsLatestVersionsResponse {
753 platform::GetDataContractsLatestVersionsResponse {
754 version: Some(Version::V0(GetDataContractsLatestVersionsResponseV0 {
755 result,
756 metadata: Some(ResponseMetadata::default()),
757 })),
758 }
759 }
760
761 fn parse(
762 response: platform::GetDataContractsLatestVersionsResponse,
763 ) -> Result<Option<DataContractsLatestVersions>, Error> {
764 <DataContractsLatestVersions as FromUnproved<
765 platform::GetDataContractsLatestVersionsRequest,
766 >>::maybe_from_unproved(
767 platform::GetDataContractsLatestVersionsRequest::default(),
768 response,
769 Network::Testnet,
770 PlatformVersion::latest(),
771 )
772 }
773
774 #[test]
775 fn version_only_entries_carry_no_contract_and_missing_ids_map_to_none() {
776 let versions = parse(response(Some(V0Result::DataContractsLatestVersions(
777 ProtoLatestVersions {
778 entries: vec![
779 DataContractLatestVersionEntry {
780 identifier: vec![1; 32],
781 version: Some(7),
782 data_contract: None,
783 },
784 DataContractLatestVersionEntry {
785 identifier: vec![2; 32],
786 version: None,
787 data_contract: None,
788 },
789 ],
790 },
791 ))))
792 .expect("parse")
793 .expect("present");
794
795 assert_eq!(versions.0.len(), 2);
796 assert_eq!(versions.version_of(&Identifier::new([1; 32])), Some(7));
797 assert!(versions.0[&Identifier::new([1; 32])]
798 .as_ref()
799 .expect("found")
800 .data_contract
801 .is_none());
802 assert_eq!(versions.0[&Identifier::new([2; 32])], None);
803 assert_eq!(versions.version_of(&Identifier::new([3; 32])), None);
804 }
805
806 #[test]
807 fn included_contract_is_deserialized() {
808 let platform_version = PlatformVersion::latest();
809 let contract = get_data_contract_fixture(None, 0, platform_version.protocol_version)
810 .data_contract_owned();
811 let bytes = contract
812 .serialize_to_bytes_with_platform_version(platform_version)
813 .expect("serialize");
814
815 let versions = parse(response(Some(V0Result::DataContractsLatestVersions(
816 ProtoLatestVersions {
817 entries: vec![DataContractLatestVersionEntry {
818 identifier: contract.id().to_vec(),
819 version: Some(contract.version()),
820 data_contract: Some(bytes),
821 }],
822 },
823 ))))
824 .expect("parse")
825 .expect("present");
826
827 let entry = versions.0[&contract.id()].clone().expect("found");
828 assert_eq!(entry.version, contract.version());
829 assert_eq!(entry.data_contract, Some(contract));
830 }
831
832 #[test]
833 fn a_proof_where_values_were_expected_is_rejected() {
834 let err = parse(response(Some(V0Result::Proof(platform::Proof::default())))).unwrap_err();
835 assert!(
836 matches!(err, Error::ResponseDecodeError { .. }),
837 "got: {err:?}"
838 );
839 }
840
841 #[test]
842 fn missing_metadata_is_rejected() {
843 let response = platform::GetDataContractsLatestVersionsResponse {
844 version: Some(Version::V0(GetDataContractsLatestVersionsResponseV0 {
845 result: None,
846 metadata: None,
847 })),
848 };
849 let err = parse(response).unwrap_err();
850 assert!(matches!(err, Error::EmptyResponseMetadata), "got: {err:?}");
851 }
852}
853
854impl FromUnproved<platform::GetContractGroupInfoRequest> for ContractGroupInfo {
855 type Request = platform::GetContractGroupInfoRequest;
856 type Response = platform::GetContractGroupInfoResponse;
857
858 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
859 _request: I,
860 response: O,
861 _network: Network,
862 _platform_version: &PlatformVersion,
863 ) -> Result<(Option<Self>, ResponseMetadata), Error>
864 where
865 Self: Sized,
866 {
867 use platform::get_contract_group_info_response::get_contract_group_info_response_v0::Result as V0Result;
868
869 let response: Self::Response = response.into();
870
871 let platform::get_contract_group_info_response::Version::V0(v0) =
872 response.version.ok_or(Error::EmptyVersion)?;
873 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
874
875 let info = match v0.result {
876 Some(V0Result::ContractGroupInfo(info)) => Some(contract_group_info_from_proto(info)?),
877 Some(V0Result::Proof(_)) => {
878 return Err(Error::ResponseDecodeError {
879 error: "expected unproved contract group info, got a proof".to_string(),
880 })
881 }
882 None => None,
883 };
884
885 Ok((info, metadata))
886 }
887}
888
889impl FromUnproved<platform::GetContractModerationStatusRequest> for ContractModerationListStatuses {
890 type Request = platform::GetContractModerationStatusRequest;
891 type Response = platform::GetContractModerationStatusResponse;
892
893 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
894 request: I,
895 response: O,
896 _network: Network,
897 _platform_version: &PlatformVersion,
898 ) -> Result<(Option<Self>, ResponseMetadata), Error>
899 where
900 Self: Sized,
901 {
902 use platform::get_contract_moderation_status_response::get_contract_moderation_status_response_v0::Result as V0Result;
903
904 let request: Self::Request = request.into();
907 let platform::get_contract_moderation_status_request::Version::V0(request_v0) =
908 request.version.ok_or(Error::EmptyVersion)?;
909 let lists = lists_from_request(&request_v0.lists)?;
910
911 let response: Self::Response = response.into();
912
913 let platform::get_contract_moderation_status_response::Version::V0(v0) =
914 response.version.ok_or(Error::EmptyVersion)?;
915 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
916
917 let status = match v0.result {
918 Some(V0Result::Status(status)) => {
919 let covered: Vec<ContractModerationList> = status
922 .lists
923 .iter()
924 .filter_map(|list| list_from_request(*list, "lists").ok())
925 .collect();
926 if let Some(missing) = lists.iter().find(|list| !covered.contains(list)) {
927 return Err(Error::ResponseDecodeError {
928 error: format!(
929 "contract moderation status does not cover the {missing} asked for"
930 ),
931 });
932 }
933 let banned = match status.banned {
934 Some(banned) => banned,
935 None if lists.contains(&ContractModerationList::Banlist) => {
936 return Err(Error::ResponseDecodeError {
937 error: "contract moderation status covers the banlist but does not \
938 say whether the identity is banned"
939 .to_string(),
940 })
941 }
942 None => false,
943 };
944 let ban = (banned && lists.contains(&ContractModerationList::Banlist))
947 .then(|| reason_from_response(status.ban_reason))
948 .transpose()?
949 .map(|reason| ContractBan { reason });
950 let suspension = status
951 .suspended_until
952 .filter(|_| lists.contains(&ContractModerationList::Suspensions))
953 .map(|until| {
954 reason_from_response(status.suspension_reason)
955 .map(|reason| ContractSuspension { until, reason })
956 })
957 .transpose()?;
958 let warnings = if lists.contains(&ContractModerationList::Warnings) {
961 warnings_from_response(status.warnings)?
962 } else {
963 vec![]
964 };
965 Some(ContractModerationListStatuses::from_status(
966 &lists,
967 &ContractModerationStatus {
968 ban,
969 suspension,
970 warnings,
971 },
972 ))
973 }
974 Some(V0Result::Proof(_)) => {
975 return Err(Error::ResponseDecodeError {
976 error: "expected unproved contract moderation status, got a proof".to_string(),
977 })
978 }
979 None => None,
980 };
981
982 Ok((status, metadata))
983 }
984}
985
986impl FromUnproved<platform::GetContractModerationEntriesRequest> for ContractModerationEntries {
987 type Request = platform::GetContractModerationEntriesRequest;
988 type Response = platform::GetContractModerationEntriesResponse;
989
990 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
991 _request: I,
992 response: O,
993 _network: Network,
994 _platform_version: &PlatformVersion,
995 ) -> Result<(Option<Self>, ResponseMetadata), Error>
996 where
997 Self: Sized,
998 {
999 use platform::get_contract_moderation_entries_response::get_contract_moderation_entries_response_v0::Result as V0Result;
1000
1001 let response: Self::Response = response.into();
1002
1003 let platform::get_contract_moderation_entries_response::Version::V0(v0) =
1004 response.version.ok_or(Error::EmptyVersion)?;
1005 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1006
1007 let entries = match v0.result {
1008 Some(V0Result::Entries(entries)) => Some(entries_from_response(entries.entries)?),
1009 Some(V0Result::Proof(_)) => {
1010 return Err(Error::ResponseDecodeError {
1011 error: "expected unproved contract moderation entries, got a proof".to_string(),
1012 })
1013 }
1014 None => None,
1015 };
1016
1017 Ok((entries, metadata))
1018 }
1019}
1020
1021impl FromUnproved<platform::GetContractDocumentRemovalsRequest> for ContractDocumentRemovals {
1022 type Request = platform::GetContractDocumentRemovalsRequest;
1023 type Response = platform::GetContractDocumentRemovalsResponse;
1024
1025 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1026 request: I,
1027 response: O,
1028 _network: Network,
1029 platform_version: &PlatformVersion,
1030 ) -> Result<(Option<Self>, ResponseMetadata), Error>
1031 where
1032 Self: Sized,
1033 {
1034 use platform::get_contract_document_removals_response::get_contract_document_removals_response_v0::Result as V0Result;
1035
1036 let request: Self::Request = request.into();
1037 let response: Self::Response = response.into();
1038
1039 let platform::get_contract_document_removals_request::Version::V0(request_v0) =
1042 request.version.ok_or(Error::EmptyVersion)?;
1043 let query = removals_query_from_request(
1044 request_v0.document_type_name,
1045 request_v0.selection,
1046 platform_version,
1047 )?;
1048
1049 let platform::get_contract_document_removals_response::Version::V0(v0) =
1050 response.version.ok_or(Error::EmptyVersion)?;
1051 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1052
1053 let removals = match v0.result {
1054 Some(V0Result::Removals(removals)) => {
1055 Some(removals_from_response(removals.removals, &query)?)
1056 }
1057 Some(V0Result::Proof(_)) => {
1058 return Err(Error::ResponseDecodeError {
1059 error: "expected unproved contract document removals, got a proof".to_string(),
1060 })
1061 }
1062 None => None,
1063 };
1064
1065 Ok((removals, metadata))
1066 }
1067}
1068
1069impl FromUnproved<platform::GetContractTeamActionsRequest> for ContractTeamActions {
1070 type Request = platform::GetContractTeamActionsRequest;
1071 type Response = platform::GetContractTeamActionsResponse;
1072
1073 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1074 request: I,
1075 response: O,
1076 _network: Network,
1077 platform_version: &PlatformVersion,
1078 ) -> Result<(Option<Self>, ResponseMetadata), Error>
1079 where
1080 Self: Sized,
1081 {
1082 use platform::get_contract_team_actions_response::get_contract_team_actions_response_v0::Result as V0Result;
1083
1084 let request: Self::Request = request.into();
1085 let response: Self::Response = response.into();
1086
1087 let platform::get_contract_team_actions_request::Version::V0(request_v0) =
1090 request.version.ok_or(Error::EmptyVersion)?;
1091 let query = team_actions_query_from_request(
1092 request_v0.status,
1093 request_v0.start_at_action_id,
1094 request_v0.count,
1095 platform_version,
1096 )?;
1097
1098 let platform::get_contract_team_actions_response::Version::V0(v0) =
1099 response.version.ok_or(Error::EmptyVersion)?;
1100 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1101
1102 let actions = match v0.result {
1103 Some(V0Result::Actions(actions)) => {
1104 Some(team_actions_from_response(actions.actions, &query)?)
1105 }
1106 Some(V0Result::Proof(_)) => {
1107 return Err(Error::ResponseDecodeError {
1108 error: "expected unproved contract team actions, got a proof".to_string(),
1109 })
1110 }
1111 None => None,
1112 };
1113
1114 Ok((actions, metadata))
1115 }
1116}
1117
1118impl FromUnproved<platform::GetContractTeamActionSignersRequest> for ContractTeamActionSigners {
1119 type Request = platform::GetContractTeamActionSignersRequest;
1120 type Response = platform::GetContractTeamActionSignersResponse;
1121
1122 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1123 request: I,
1124 response: O,
1125 _network: Network,
1126 _platform_version: &PlatformVersion,
1127 ) -> Result<(Option<Self>, ResponseMetadata), Error>
1128 where
1129 Self: Sized,
1130 {
1131 use platform::get_contract_team_action_signers_response::get_contract_team_action_signers_response_v0::Result as V0Result;
1132
1133 let request: Self::Request = request.into();
1134 let response: Self::Response = response.into();
1135
1136 let platform::get_contract_team_action_signers_request::Version::V0(request_v0) =
1137 request.version.ok_or(Error::EmptyVersion)?;
1138 team_action_status_from_request(request_v0.status)?;
1139
1140 let platform::get_contract_team_action_signers_response::Version::V0(v0) =
1141 response.version.ok_or(Error::EmptyVersion)?;
1142 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1143
1144 let signers = match v0.result {
1145 Some(V0Result::Signers(signers)) => {
1146 Some(team_action_signers_from_response(signers.signer_ids)?)
1147 }
1148 Some(V0Result::Proof(_)) => {
1149 return Err(Error::ResponseDecodeError {
1150 error: "expected unproved contract team action signers, got a proof"
1151 .to_string(),
1152 })
1153 }
1154 None => None,
1155 };
1156
1157 Ok((signers, metadata))
1158 }
1159}
1160
1161impl FromUnproved<platform::GetContractModerationActionCountsRequest>
1162 for ContractModerationActionCounts
1163{
1164 type Request = platform::GetContractModerationActionCountsRequest;
1165 type Response = platform::GetContractModerationActionCountsResponse;
1166
1167 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1168 _request: I,
1169 response: O,
1170 _network: Network,
1171 _platform_version: &PlatformVersion,
1172 ) -> Result<(Option<Self>, ResponseMetadata), Error>
1173 where
1174 Self: Sized,
1175 {
1176 let response: Self::Response = response.into();
1177 let platform::get_contract_moderation_action_counts_response::Version::V0(v0) =
1178 response.version.ok_or(Error::EmptyVersion)?;
1179 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1180
1181 let counts = match v0.result {
1182 Some(ModerationActionCountsResult::Counts(counts)) => {
1183 Some(moderation_action_counts_from_response(counts.counts)?)
1184 }
1185 Some(ModerationActionCountsResult::Proof(_)) => {
1186 return Err(Error::ResponseDecodeError {
1187 error: "expected unproved contract moderation action counts, got a proof"
1188 .to_string(),
1189 })
1190 }
1191 None => None,
1192 };
1193
1194 Ok((counts, metadata))
1195 }
1196}
1197
1198impl FromUnproved<platform::GetContractFeePotsRequest> for ContractFeePots {
1199 type Request = platform::GetContractFeePotsRequest;
1200 type Response = platform::GetContractFeePotsResponse;
1201
1202 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1203 _request: I,
1204 response: O,
1205 _network: Network,
1206 _platform_version: &PlatformVersion,
1207 ) -> Result<(Option<Self>, ResponseMetadata), Error>
1208 where
1209 Self: Sized,
1210 {
1211 use platform::get_contract_fee_pots_response::get_contract_fee_pots_response_v0::Result as V0Result;
1212
1213 let response: Self::Response = response.into();
1214
1215 let platform::get_contract_fee_pots_response::Version::V0(v0) =
1216 response.version.ok_or(Error::EmptyVersion)?;
1217 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1218
1219 let pots = match v0.result {
1220 Some(V0Result::Pots(pots)) => Some(fee_pots_from_response(pots)?),
1221 Some(V0Result::Proof(_)) => {
1222 return Err(Error::ResponseDecodeError {
1223 error: "expected unproved contract fee pots, got a proof".to_string(),
1224 })
1225 }
1226 None => None,
1227 };
1228
1229 Ok((pots, metadata))
1230 }
1231}
1232
1233impl FromUnproved<platform::GetIdentityKeysRemainingBudgetsRequest>
1234 for IdentityKeysRemainingBudgets
1235{
1236 type Request = platform::GetIdentityKeysRemainingBudgetsRequest;
1237 type Response = platform::GetIdentityKeysRemainingBudgetsResponse;
1238
1239 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1240 _request: I,
1241 response: O,
1242 _network: Network,
1243 _platform_version: &PlatformVersion,
1244 ) -> Result<(Option<Self>, ResponseMetadata), Error>
1245 where
1246 Self: Sized,
1247 {
1248 use platform::get_identity_keys_remaining_budgets_response::get_identity_keys_remaining_budgets_response_v0::Result as V0Result;
1249
1250 let response: Self::Response = response.into();
1251
1252 let platform::get_identity_keys_remaining_budgets_response::Version::V0(v0) =
1253 response.version.ok_or(Error::EmptyVersion)?;
1254 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1255
1256 let budgets = match v0.result {
1257 Some(V0Result::KeysRemainingBudgets(budgets)) => Some(
1258 budgets
1259 .entries
1260 .into_iter()
1261 .map(|entry| (entry.key_id, entry.remaining_budget))
1262 .collect(),
1263 ),
1264 Some(V0Result::Proof(_)) => {
1265 return Err(Error::ResponseDecodeError {
1266 error: "expected unproved remaining budgets, got a proof".to_string(),
1267 })
1268 }
1269 None => None,
1270 };
1271
1272 Ok((budgets, metadata))
1273 }
1274}
1275
1276impl FromUnproved<platform::GetContractGroupMembersRequest> for ContractGroupMembersPage {
1277 type Request = platform::GetContractGroupMembersRequest;
1278 type Response = platform::GetContractGroupMembersResponse;
1279
1280 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1281 request: I,
1282 response: O,
1283 _network: Network,
1284 _platform_version: &PlatformVersion,
1285 ) -> Result<(Option<Self>, ResponseMetadata), Error>
1286 where
1287 Self: Sized,
1288 {
1289 let request: Self::Request = request.into();
1290 let response: Self::Response = response.into();
1291
1292 let platform::get_contract_group_members_request::Version::V0(request_v0) =
1293 request.version.ok_or(Error::EmptyVersion)?;
1294 let query = members_query_from_request(request_v0.members)?;
1295
1296 let platform::get_contract_group_members_response::Version::V0(v0) =
1297 response.version.ok_or(Error::EmptyVersion)?;
1298 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1299
1300 let page = v0
1301 .result
1302 .map(|result| members_page_from_response(&query, result))
1303 .transpose()?;
1304
1305 Ok((page, metadata))
1306 }
1307}
1308
1309impl FromUnproved<platform::GetContractGroupsForContractRequest>
1310 for ContractGroupMembershipsForContract
1311{
1312 type Request = platform::GetContractGroupsForContractRequest;
1313 type Response = platform::GetContractGroupsForContractResponse;
1314
1315 fn maybe_from_unproved_with_metadata<I: Into<Self::Request>, O: Into<Self::Response>>(
1316 _request: I,
1317 response: O,
1318 _network: Network,
1319 _platform_version: &PlatformVersion,
1320 ) -> Result<(Option<Self>, ResponseMetadata), Error>
1321 where
1322 Self: Sized,
1323 {
1324 use platform::get_contract_groups_for_contract_response::get_contract_groups_for_contract_response_v0::Result as V0Result;
1325
1326 let response: Self::Response = response.into();
1327
1328 let platform::get_contract_groups_for_contract_response::Version::V0(v0) =
1329 response.version.ok_or(Error::EmptyVersion)?;
1330 let metadata = v0.metadata.ok_or(Error::EmptyResponseMetadata)?;
1331
1332 let memberships = match v0.result {
1333 Some(V0Result::ContractGroupMemberships(memberships)) => {
1334 Some(memberships_from_response(memberships)?)
1335 }
1336 Some(V0Result::Proof(_)) => {
1337 return Err(Error::ResponseDecodeError {
1338 error: "expected unproved contract group memberships, got a proof".to_string(),
1339 })
1340 }
1341 None => None,
1342 };
1343
1344 Ok((memberships, metadata))
1345 }
1346}
1347
1348#[cfg(test)]
1349mod contract_groups_tests {
1350 use super::*;
1351 use crate::types::contract_groups::{ContractGroupMembersQuery, ContractGroupOwner};
1352 use dapi_grpc::platform::v0::get_contract_group_info_response::{
1353 get_contract_group_info_response_v0::Result as InfoResult,
1354 ContractGroupInfo as ContractGroupInfoProto, GetContractGroupInfoResponseV0,
1355 Version as InfoVersion,
1356 };
1357 use dapi_grpc::platform::v0::get_contract_group_members_request::get_contract_group_members_request_v0::Members;
1358 use dapi_grpc::platform::v0::get_contract_group_members_request::{
1359 ContractMembersQuery, GetContractGroupMembersRequestV0, TokenMembersQuery,
1360 Version as MembersRequestVersion,
1361 };
1362 use dapi_grpc::platform::v0::get_contract_group_members_response::{
1363 get_contract_group_members_response_v0::Result as MembersResult, ContractMembers,
1364 GetContractGroupMembersResponseV0, TokenMembers, Version as MembersVersion,
1365 };
1366 use dapi_grpc::platform::v0::get_contract_groups_for_contract_response::{
1367 get_contract_groups_for_contract_response_v0::Result as MembershipsResult,
1368 ContractGroupMemberships, DocumentTypeMemberships, GetContractGroupsForContractResponseV0,
1369 TokenMemberships, Version as MembershipsVersion,
1370 };
1371 use dapi_grpc::platform::v0::ContractGroupTokenMember;
1372 use std::collections::BTreeSet;
1373
1374 fn info_response(result: Option<InfoResult>) -> platform::GetContractGroupInfoResponse {
1375 platform::GetContractGroupInfoResponse {
1376 version: Some(InfoVersion::V0(GetContractGroupInfoResponseV0 {
1377 result,
1378 metadata: Some(ResponseMetadata::default()),
1379 })),
1380 }
1381 }
1382
1383 fn parse_info(
1384 response: platform::GetContractGroupInfoResponse,
1385 ) -> Result<Option<ContractGroupInfo>, Error> {
1386 <ContractGroupInfo as FromUnproved<platform::GetContractGroupInfoRequest>>::maybe_from_unproved(
1387 platform::GetContractGroupInfoRequest::default(),
1388 response,
1389 Network::Testnet,
1390 PlatformVersion::latest(),
1391 )
1392 }
1393
1394 #[test]
1395 fn info_with_no_admins_is_a_single_owner() {
1396 let info = parse_info(info_response(Some(InfoResult::ContractGroupInfo(
1397 ContractGroupInfoProto {
1398 owner_id: vec![1; 32],
1399 admin_ids: vec![],
1400 name: Some("alpha".to_string()),
1401 description: None,
1402 },
1403 ))))
1404 .expect("parse")
1405 .expect("present");
1406
1407 assert_eq!(
1408 info.owner(),
1409 &ContractGroupOwner::SingleOwner(Identifier::new([1; 32]))
1410 );
1411 assert_eq!(info.name(), Some("alpha"));
1412 assert_eq!(info.description(), None);
1413 }
1414
1415 #[test]
1416 fn info_with_admins_is_an_owner_and_admins() {
1417 let info = parse_info(info_response(Some(InfoResult::ContractGroupInfo(
1418 ContractGroupInfoProto {
1419 owner_id: vec![1; 32],
1420 admin_ids: vec![vec![3; 32], vec![2; 32]],
1421 name: None,
1422 description: Some("shared".to_string()),
1423 },
1424 ))))
1425 .expect("parse")
1426 .expect("present");
1427
1428 assert_eq!(
1429 info.owner(),
1430 &ContractGroupOwner::OwnerAndAdmins {
1431 owner: Identifier::new([1; 32]),
1432 admins: BTreeSet::from([Identifier::new([2; 32]), Identifier::new([3; 32])]),
1433 }
1434 );
1435 assert_eq!(info.description(), Some("shared"));
1436 }
1437
1438 #[test]
1439 fn info_absent_group_and_bad_owner_and_proof() {
1440 assert_eq!(parse_info(info_response(None)).expect("parse"), None);
1441
1442 let err = parse_info(info_response(Some(InfoResult::ContractGroupInfo(
1443 ContractGroupInfoProto {
1444 owner_id: vec![1; 4],
1445 admin_ids: vec![],
1446 name: None,
1447 description: None,
1448 },
1449 ))))
1450 .unwrap_err();
1451 assert!(
1452 matches!(err, Error::ProtocolError { .. }),
1453 "a malformed response field is the node's error: {err:?}"
1454 );
1455
1456 let err = parse_info(info_response(Some(InfoResult::Proof(
1457 platform::Proof::default(),
1458 ))))
1459 .unwrap_err();
1460 assert!(
1461 matches!(err, Error::ResponseDecodeError { .. }),
1462 "got: {err:?}"
1463 );
1464 }
1465
1466 fn members_request(members: Members) -> platform::GetContractGroupMembersRequest {
1467 platform::GetContractGroupMembersRequest {
1468 version: Some(MembersRequestVersion::V0(
1469 GetContractGroupMembersRequestV0 {
1470 contract_group_id: vec![1; 32],
1471 members: Some(members),
1472 limit: None,
1473 prove: false,
1474 },
1475 )),
1476 }
1477 }
1478
1479 fn members_response(
1480 result: Option<MembersResult>,
1481 ) -> platform::GetContractGroupMembersResponse {
1482 platform::GetContractGroupMembersResponse {
1483 version: Some(MembersVersion::V0(GetContractGroupMembersResponseV0 {
1484 result,
1485 metadata: Some(ResponseMetadata::default()),
1486 })),
1487 }
1488 }
1489
1490 fn parse_members(
1491 request: platform::GetContractGroupMembersRequest,
1492 response: platform::GetContractGroupMembersResponse,
1493 ) -> Result<Option<ContractGroupMembersPage>, Error> {
1494 <ContractGroupMembersPage as FromUnproved<platform::GetContractGroupMembersRequest>>::maybe_from_unproved(
1495 request,
1496 response,
1497 Network::Testnet,
1498 PlatformVersion::latest(),
1499 )
1500 }
1501
1502 #[test]
1503 fn members_page_of_the_requested_kind_is_read() {
1504 let page = parse_members(
1505 members_request(Members::Tokens(TokenMembersQuery { start_after: None })),
1506 members_response(Some(MembersResult::Tokens(TokenMembers {
1507 tokens: vec![ContractGroupTokenMember {
1508 contract_id: vec![5; 32],
1509 token_position: 2,
1510 }],
1511 }))),
1512 )
1513 .expect("parse")
1514 .expect("present");
1515
1516 assert_eq!(
1517 page,
1518 ContractGroupMembersPage::Tokens(vec![(Identifier::new([5; 32]), 2)])
1519 );
1520 assert_eq!(
1521 page.next_query(),
1522 Some(ContractGroupMembersQuery::Tokens {
1523 start_after: Some((Identifier::new([5; 32]), 2)),
1524 })
1525 );
1526 }
1527
1528 #[test]
1529 fn members_page_of_another_kind_is_rejected() {
1530 let err = parse_members(
1531 members_request(Members::Contracts(ContractMembersQuery {
1532 start_after: None,
1533 })),
1534 members_response(Some(MembersResult::Tokens(TokenMembers::default()))),
1535 )
1536 .unwrap_err();
1537 assert!(
1538 matches!(err, Error::ResponseDecodeError { .. }),
1539 "got: {err:?}"
1540 );
1541
1542 let err = parse_members(
1543 members_request(Members::Contracts(ContractMembersQuery {
1544 start_after: None,
1545 })),
1546 members_response(Some(MembersResult::Proof(platform::Proof::default()))),
1547 )
1548 .unwrap_err();
1549 assert!(
1550 matches!(err, Error::ResponseDecodeError { .. }),
1551 "got: {err:?}"
1552 );
1553 }
1554
1555 #[test]
1556 fn members_page_contracts_are_read_in_order() {
1557 let page = parse_members(
1558 members_request(Members::Contracts(ContractMembersQuery {
1559 start_after: None,
1560 })),
1561 members_response(Some(MembersResult::Contracts(ContractMembers {
1562 contract_ids: vec![vec![1; 32], vec![2; 32]],
1563 }))),
1564 )
1565 .expect("parse")
1566 .expect("present");
1567
1568 assert_eq!(
1569 page,
1570 ContractGroupMembersPage::Contracts(vec![
1571 Identifier::new([1; 32]),
1572 Identifier::new([2; 32])
1573 ])
1574 );
1575 }
1576
1577 #[test]
1578 fn memberships_are_read_into_sets() {
1579 let response = platform::GetContractGroupsForContractResponse {
1580 version: Some(MembershipsVersion::V0(
1581 GetContractGroupsForContractResponseV0 {
1582 result: Some(MembershipsResult::ContractGroupMemberships(
1583 ContractGroupMemberships {
1584 contract_group_ids: vec![vec![1; 32]],
1585 document_types: vec![DocumentTypeMemberships {
1586 document_type_name: "note".to_string(),
1587 contract_group_ids: vec![vec![2; 32], vec![1; 32]],
1588 }],
1589 tokens: vec![TokenMemberships {
1590 token_position: 7,
1591 contract_group_ids: vec![vec![3; 32]],
1592 }],
1593 },
1594 )),
1595 metadata: Some(ResponseMetadata::default()),
1596 },
1597 )),
1598 };
1599
1600 let memberships = <ContractGroupMembershipsForContract as FromUnproved<
1601 platform::GetContractGroupsForContractRequest,
1602 >>::maybe_from_unproved(
1603 platform::GetContractGroupsForContractRequest::default(),
1604 response,
1605 Network::Testnet,
1606 PlatformVersion::latest(),
1607 )
1608 .expect("parse")
1609 .expect("present");
1610
1611 assert_eq!(
1612 memberships.contract,
1613 BTreeSet::from([Identifier::new([1; 32])])
1614 );
1615 assert_eq!(
1616 memberships.document_types["note"],
1617 BTreeSet::from([Identifier::new([1; 32]), Identifier::new([2; 32])])
1618 );
1619 assert_eq!(
1620 memberships.tokens[&7],
1621 BTreeSet::from([Identifier::new([3; 32])])
1622 );
1623 assert_eq!(
1624 memberships.all_contract_group_ids(),
1625 BTreeSet::from([
1626 Identifier::new([1; 32]),
1627 Identifier::new([2; 32]),
1628 Identifier::new([3; 32])
1629 ])
1630 );
1631 }
1632}
1633
1634#[cfg(test)]
1635mod contract_moderation_tests {
1636 use super::*;
1637 use crate::types::contract_moderation::ContractModerationReason;
1638 use dapi_grpc::platform::v0::get_contract_document_removals_request::get_contract_document_removals_request_v0::Selection as RemovalsSelection;
1639 use dapi_grpc::platform::v0::get_contract_document_removals_request::{
1640 DocumentIds as RemovalsDocumentIds, GetContractDocumentRemovalsRequestV0,
1641 Page as RemovalsPage, Version as RemovalsRequestVersion,
1642 };
1643 use dapi_grpc::platform::v0::get_contract_document_removals_response::{
1644 get_contract_document_removals_response_v0::Result as RemovalsResult,
1645 ContractDocumentRemoval as ContractDocumentRemovalProto,
1646 ContractDocumentRemovals as ContractDocumentRemovalsProto,
1647 GetContractDocumentRemovalsResponseV0, Version as RemovalsResponseVersion,
1648 };
1649 use dapi_grpc::platform::v0::get_contract_moderation_status_request::{
1650 GetContractModerationStatusRequestV0, Version as StatusRequestVersion,
1651 };
1652 use dapi_grpc::platform::v0::get_contract_moderation_status_response::{
1653 get_contract_moderation_status_response_v0::Result as StatusResult,
1654 ContractModerationStatus as ContractModerationStatusProto,
1655 GetContractModerationStatusResponseV0, Version as StatusResponseVersion,
1656 };
1657 use dapi_grpc::platform::v0::get_contract_team_action_signers_request::{
1658 GetContractTeamActionSignersRequestV0, Version as SignersRequestVersion,
1659 };
1660 use dapi_grpc::platform::v0::get_contract_team_action_signers_response::{
1661 get_contract_team_action_signers_response_v0::Result as SignersResult,
1662 ContractTeamActionSigners as ContractTeamActionSignersProto,
1663 GetContractTeamActionSignersResponseV0, Version as SignersResponseVersion,
1664 };
1665 use dapi_grpc::platform::v0::get_contract_team_actions_request::{
1666 ActionStatus, GetContractTeamActionsRequestV0, Version as TeamActionsRequestVersion,
1667 };
1668 use dapi_grpc::platform::v0::get_contract_team_actions_response::{
1669 contract_team_action, get_contract_team_actions_response_v0::Result as TeamActionsResult,
1670 ContractTeamAction as ContractTeamActionProto,
1671 ContractTeamActions as ContractTeamActionsProto, DeleteSettledDocument,
1672 GetContractTeamActionsResponseV0, Version as TeamActionsResponseVersion,
1673 };
1674 use dapi_grpc::platform::v0::get_contract_moderation_action_counts_request::{
1675 GetContractModerationActionCountsRequestV0, Version as CountsRequestVersion,
1676 };
1677 use dapi_grpc::platform::v0::get_contract_moderation_action_counts_response::{
1678 ContractModerationActionCount, ContractModerationActionCounts as CountsProto,
1679 GetContractModerationActionCountsResponseV0, Version as CountsResponseVersion,
1680 };
1681 use dapi_grpc::platform::v0::ContractModerationReason as ContractModerationReasonProto;
1682 use dapi_grpc::platform::v0::ContractWarning as ContractWarningProto;
1683 use dapi_grpc::platform::v0::ResponseMetadata;
1684 use dpp::dashcore::Network;
1685 use dpp::version::PlatformVersion;
1686
1687 const BANLIST: i32 = 1;
1688 const SUSPENSIONS: i32 = 2;
1689 const WARNINGS: i32 = 3;
1690
1691 fn status(
1692 requested: Vec<i32>,
1693 response: ContractModerationStatusProto,
1694 ) -> Result<Option<ContractModerationListStatuses>, Error> {
1695 let request = platform::GetContractModerationStatusRequest {
1696 version: Some(StatusRequestVersion::V0(
1697 GetContractModerationStatusRequestV0 {
1698 contract_id: vec![1; 32],
1699 identity_id: vec![2; 32],
1700 lists: requested,
1701 prove: false,
1702 },
1703 )),
1704 };
1705 let response = platform::GetContractModerationStatusResponse {
1706 version: Some(StatusResponseVersion::V0(
1707 GetContractModerationStatusResponseV0 {
1708 result: Some(StatusResult::Status(response)),
1709 metadata: Some(ResponseMetadata::default()),
1710 },
1711 )),
1712 };
1713 ContractModerationListStatuses::maybe_from_unproved_with_metadata(
1714 request,
1715 response,
1716 Network::Testnet,
1717 PlatformVersion::latest(),
1718 )
1719 .map(|(status, _)| status)
1720 }
1721
1722 #[test]
1723 fn should_report_the_lists_the_response_covers() {
1724 let statuses = status(
1725 vec![BANLIST, SUSPENSIONS],
1726 ContractModerationStatusProto {
1727 banned: Some(true),
1728 suspended_until: Some(7),
1729 lists: vec![BANLIST, SUSPENSIONS],
1730 ban_reason: Some(ContractModerationReasonProto {
1731 code: None,
1732 text: "spam".to_string(),
1733 documents: vec![],
1734 reason_document_id: None,
1735 }),
1736 suspension_reason: Some(ContractModerationReasonProto {
1737 code: Some(9),
1738 text: "flooding".to_string(),
1739 documents: vec![],
1740 reason_document_id: None,
1741 }),
1742 warnings: vec![],
1743 },
1744 )
1745 .expect("expected the status to convert")
1746 .expect("expected a status");
1747
1748 assert_eq!(statuses.banned(), Some(true));
1749 assert_eq!(statuses.suspended_until(), Some(Some(7)));
1750 assert_eq!(statuses.warnings(), None);
1752 assert_eq!(
1753 statuses.ban().flatten().map(|ban| &ban.reason),
1754 Some(&ContractModerationReason::from_text("spam"))
1755 );
1756 assert_eq!(
1757 statuses.suspension().flatten().map(|entry| &entry.reason),
1758 Some(&ContractModerationReason {
1759 code: Some(9),
1760 text: "flooding".to_string(),
1761 documents: vec![],
1762 reason_document_id: None,
1763 })
1764 );
1765 }
1766
1767 #[test]
1768 fn should_refuse_an_entry_without_a_reason_or_with_a_code_past_u16() {
1769 let no_reason = status(
1770 vec![BANLIST],
1771 ContractModerationStatusProto {
1772 banned: Some(true),
1773 lists: vec![BANLIST],
1774 ..Default::default()
1775 },
1776 );
1777 assert!(matches!(no_reason, Err(Error::ResponseDecodeError { .. })));
1778
1779 let wide_code = status(
1780 vec![SUSPENSIONS],
1781 ContractModerationStatusProto {
1782 suspended_until: Some(7),
1783 lists: vec![SUSPENSIONS],
1784 suspension_reason: Some(ContractModerationReasonProto {
1785 code: Some(u16::MAX as u32 + 1),
1786 text: String::new(),
1787 documents: vec![],
1788 reason_document_id: None,
1789 }),
1790 ..Default::default()
1791 },
1792 );
1793 assert!(matches!(wide_code, Err(Error::ResponseDecodeError { .. })));
1794
1795 let clean = status(
1797 vec![BANLIST, SUSPENSIONS],
1798 ContractModerationStatusProto {
1799 banned: Some(false),
1800 lists: vec![BANLIST, SUSPENSIONS],
1801 ..Default::default()
1802 },
1803 )
1804 .expect("expected the status to convert")
1805 .expect("expected a status");
1806 assert_eq!(clean.ban(), Some(None));
1807 assert_eq!(clean.suspension(), Some(None));
1808 }
1809
1810 #[test]
1811 fn should_read_the_warnings_when_the_warning_list_was_asked_for() {
1812 let warning = |warned_at: u64, text: &str| ContractWarningProto {
1813 warned_at,
1814 reason: Some(ContractModerationReasonProto {
1815 code: None,
1816 text: text.to_string(),
1817 documents: vec![],
1818 reason_document_id: None,
1819 }),
1820 };
1821 let warned = status(
1822 vec![WARNINGS],
1823 ContractModerationStatusProto {
1824 lists: vec![WARNINGS],
1825 warnings: vec![warning(5, "first strike"), warning(6, "second strike")],
1826 ..Default::default()
1827 },
1828 )
1829 .expect("expected the status to convert")
1830 .expect("expected a status");
1831 let warnings = warned.warnings().expect("expected the warning list");
1832 assert_eq!(warnings.len(), 2);
1833 assert_eq!(warnings[1].warned_at, 6);
1834 assert_eq!(warnings[1].reason.text, "second strike");
1835 assert_eq!(warned.banned(), None);
1836
1837 let clean = status(
1839 vec![WARNINGS],
1840 ContractModerationStatusProto {
1841 lists: vec![WARNINGS],
1842 ..Default::default()
1843 },
1844 )
1845 .expect("expected the status to convert")
1846 .expect("expected a status");
1847 assert_eq!(clean.warnings(), Some(&[][..]));
1848
1849 let no_reason = status(
1851 vec![WARNINGS],
1852 ContractModerationStatusProto {
1853 lists: vec![WARNINGS],
1854 warnings: vec![ContractWarningProto {
1855 warned_at: 5,
1856 reason: None,
1857 }],
1858 ..Default::default()
1859 },
1860 );
1861 assert!(matches!(no_reason, Err(Error::ResponseDecodeError { .. })));
1862 }
1863
1864 #[test]
1865 fn should_refuse_a_status_that_does_not_cover_a_list_asked_for() {
1866 let uncovered = status(
1869 vec![BANLIST, SUSPENSIONS],
1870 ContractModerationStatusProto {
1871 lists: vec![SUSPENSIONS],
1872 ..Default::default()
1873 },
1874 );
1875 assert!(matches!(uncovered, Err(Error::ResponseDecodeError { .. })));
1876
1877 let covered_but_unset = status(
1878 vec![BANLIST],
1879 ContractModerationStatusProto {
1880 lists: vec![BANLIST],
1881 ..Default::default()
1882 },
1883 );
1884 assert!(matches!(
1885 covered_but_unset,
1886 Err(Error::ResponseDecodeError { .. })
1887 ));
1888 }
1889
1890 fn removals(
1891 selection: Option<RemovalsSelection>,
1892 result: Option<RemovalsResult>,
1893 ) -> Result<Option<ContractDocumentRemovals>, Error> {
1894 let request = platform::GetContractDocumentRemovalsRequest {
1895 version: Some(RemovalsRequestVersion::V0(
1896 GetContractDocumentRemovalsRequestV0 {
1897 contract_id: vec![1; 32],
1898 document_type_name: "post".to_string(),
1899 selection,
1900 prove: false,
1901 },
1902 )),
1903 };
1904 let response = platform::GetContractDocumentRemovalsResponse {
1905 version: Some(RemovalsResponseVersion::V0(
1906 GetContractDocumentRemovalsResponseV0 {
1907 result,
1908 metadata: Some(ResponseMetadata::default()),
1909 },
1910 )),
1911 };
1912 ContractDocumentRemovals::maybe_from_unproved_with_metadata(
1913 request,
1914 response,
1915 Network::Testnet,
1916 PlatformVersion::latest(),
1917 )
1918 .map(|(removals, _)| removals)
1919 }
1920
1921 fn removal_proto(seed: u8) -> ContractDocumentRemovalProto {
1922 ContractDocumentRemovalProto {
1923 document_id: vec![seed; 32],
1924 document_owner_id: vec![seed + 0x10; 32],
1925 moderator_id: vec![0x77; 32],
1926 removed_at: 1_000 + u64::from(seed),
1927 document_hash: vec![seed + 0x20; 32],
1928 restoration: None,
1929 reason: Some(ContractModerationReasonProto {
1930 code: None,
1931 text: "spam".to_string(),
1932 documents: vec![],
1933 reason_document_id: None,
1934 }),
1935 kept_fields: Default::default(),
1936 }
1937 }
1938
1939 #[test]
1940 fn should_read_the_removals_the_request_asked_for() {
1941 let page = Some(RemovalsSelection::Page(RemovalsPage {
1942 start_after: None,
1943 limit: Some(2),
1944 }));
1945 let read = removals(
1946 page.clone(),
1947 Some(RemovalsResult::Removals(ContractDocumentRemovalsProto {
1948 removals: vec![removal_proto(1), removal_proto(2)],
1949 })),
1950 )
1951 .expect("expected the removals to convert")
1952 .expect("expected removals");
1953 assert_eq!(read.removals().len(), 2);
1954 assert_eq!(read.removals()[0].document_id, Identifier::from([1; 32]));
1955 assert_eq!(
1956 read.removals()[1].removal.moderator_id,
1957 Identifier::from([0x77; 32])
1958 );
1959
1960 let over_limit = removals(
1962 Some(RemovalsSelection::Page(RemovalsPage {
1963 start_after: None,
1964 limit: Some(1),
1965 })),
1966 Some(RemovalsResult::Removals(ContractDocumentRemovalsProto {
1967 removals: vec![removal_proto(1), removal_proto(2)],
1968 })),
1969 );
1970 assert!(matches!(over_limit, Err(Error::ResponseDecodeError { .. })));
1971 assert!(matches!(
1972 removals(None, None),
1973 Err(Error::RequestError { .. })
1974 ));
1975
1976 let proved = removals(page, Some(RemovalsResult::Proof(Default::default())));
1978 assert!(matches!(proved, Err(Error::ResponseDecodeError { .. })));
1979 }
1980
1981 #[test]
1982 fn should_refuse_a_removal_of_a_document_the_request_did_not_name() {
1983 let by_ids = Some(RemovalsSelection::DocumentIds(RemovalsDocumentIds {
1984 document_ids: vec![vec![1; 32], vec![3; 32]],
1985 }));
1986 let unasked = removals(
1987 by_ids.clone(),
1988 Some(RemovalsResult::Removals(ContractDocumentRemovalsProto {
1989 removals: vec![removal_proto(2)],
1990 })),
1991 );
1992 assert!(matches!(unasked, Err(Error::ResponseDecodeError { .. })));
1993
1994 let partial = removals(
1996 by_ids,
1997 Some(RemovalsResult::Removals(ContractDocumentRemovalsProto {
1998 removals: vec![removal_proto(1)],
1999 })),
2000 )
2001 .expect("expected the removals to convert")
2002 .expect("expected removals");
2003 assert_eq!(partial.removals().len(), 1);
2004 }
2005
2006 fn team_actions(
2007 count: Option<u32>,
2008 result: Option<TeamActionsResult>,
2009 ) -> Result<Option<ContractTeamActions>, Error> {
2010 let request = platform::GetContractTeamActionsRequest {
2011 version: Some(TeamActionsRequestVersion::V0(
2012 GetContractTeamActionsRequestV0 {
2013 contract_id: vec![1; 32],
2014 status: ActionStatus::Active as i32,
2015 start_at_action_id: None,
2016 count,
2017 prove: false,
2018 },
2019 )),
2020 };
2021 let response = platform::GetContractTeamActionsResponse {
2022 version: Some(TeamActionsResponseVersion::V0(
2023 GetContractTeamActionsResponseV0 {
2024 result,
2025 metadata: Some(ResponseMetadata::default()),
2026 },
2027 )),
2028 };
2029 ContractTeamActions::maybe_from_unproved_with_metadata(
2030 request,
2031 response,
2032 Network::Testnet,
2033 PlatformVersion::latest(),
2034 )
2035 .map(|(actions, _)| actions)
2036 }
2037
2038 fn team_action_proto(seed: u8) -> ContractTeamActionProto {
2039 ContractTeamActionProto {
2040 action_id: vec![seed; 32],
2041 proposer_id: vec![0x77; 32],
2042 proposed_at: 1_000,
2043 event: Some(contract_team_action::Event::DeleteSettledDocument(
2044 DeleteSettledDocument {
2045 document_type_name: "post".to_string(),
2046 document_id: vec![seed + 0x10; 32],
2047 document_last_modified_at: 10,
2048 document_revision: Some(2),
2049 reason: Some(ContractModerationReasonProto {
2050 code: None,
2051 text: "doxxing".to_string(),
2052 documents: vec![],
2053 reason_document_id: None,
2054 }),
2055 },
2056 )),
2057 approval_count: u32::from(seed),
2058 }
2059 }
2060
2061 #[test]
2062 fn should_read_the_team_actions_the_request_asked_for() {
2063 let read = team_actions(
2064 Some(2),
2065 Some(TeamActionsResult::Actions(ContractTeamActionsProto {
2066 actions: vec![team_action_proto(1), team_action_proto(2)],
2067 })),
2068 )
2069 .expect("expected the team actions to convert")
2070 .expect("expected team actions");
2071 assert_eq!(read.actions().len(), 2);
2072 assert_eq!(read.actions()[0].action_id, Identifier::from([1; 32]));
2073 assert_eq!(
2074 read.actions()[0].action.proposer_id,
2075 Identifier::from([0x77; 32])
2076 );
2077
2078 team_actions(
2080 Some(1),
2081 Some(TeamActionsResult::Actions(ContractTeamActionsProto {
2082 actions: vec![team_action_proto(1), team_action_proto(2)],
2083 })),
2084 )
2085 .expect_err("expected a page too long to be refused");
2086 assert_eq!(team_actions(None, None).expect("expected no error"), None);
2087 team_actions(None, Some(TeamActionsResult::Proof(Default::default())))
2088 .expect_err("expected a proof to be refused");
2089 }
2090
2091 #[test]
2092 fn should_read_the_signers_of_a_team_action() {
2093 let request = platform::GetContractTeamActionSignersRequest {
2094 version: Some(SignersRequestVersion::V0(
2095 GetContractTeamActionSignersRequestV0 {
2096 contract_id: vec![1; 32],
2097 status: ActionStatus::Closed as i32,
2098 action_id: vec![2; 32],
2099 prove: false,
2100 },
2101 )),
2102 };
2103 let response = platform::GetContractTeamActionSignersResponse {
2104 version: Some(SignersResponseVersion::V0(
2105 GetContractTeamActionSignersResponseV0 {
2106 result: Some(SignersResult::Signers(ContractTeamActionSignersProto {
2107 signer_ids: vec![vec![3; 32], vec![4; 32]],
2108 })),
2109 metadata: Some(ResponseMetadata::default()),
2110 },
2111 )),
2112 };
2113 let (signers, _) = ContractTeamActionSigners::maybe_from_unproved_with_metadata(
2114 request,
2115 response,
2116 Network::Testnet,
2117 PlatformVersion::latest(),
2118 )
2119 .expect("expected the signers to convert");
2120 assert_eq!(
2121 signers.expect("expected signers").signers(),
2122 &[Identifier::from([3; 32]), Identifier::from([4; 32])]
2123 );
2124 }
2125
2126 #[test]
2127 fn should_read_the_moderation_action_counts_of_a_contract() {
2128 let counts = |result: Option<ModerationActionCountsResult>| {
2129 ContractModerationActionCounts::maybe_from_unproved_with_metadata(
2130 platform::GetContractModerationActionCountsRequest {
2131 version: Some(CountsRequestVersion::V0(
2132 GetContractModerationActionCountsRequestV0 {
2133 contract_id: vec![1; 32],
2134 prove: false,
2135 },
2136 )),
2137 },
2138 platform::GetContractModerationActionCountsResponse {
2139 version: Some(CountsResponseVersion::V0(
2140 GetContractModerationActionCountsResponseV0 {
2141 result,
2142 metadata: Some(ResponseMetadata::default()),
2143 },
2144 )),
2145 },
2146 Network::Testnet,
2147 PlatformVersion::latest(),
2148 )
2149 .map(|(counts, _)| counts)
2150 };
2151 assert_eq!(
2152 counts(Some(ModerationActionCountsResult::Counts(CountsProto {
2153 counts: vec![ContractModerationActionCount {
2154 identity_id: vec![3; 32],
2155 count: 7,
2156 }],
2157 })))
2158 .expect("expected the counts to convert")
2159 .expect("expected counts")
2160 .counts(),
2161 &BTreeMap::from([(Identifier::from([3; 32]), 7)])
2162 );
2163 assert_eq!(counts(None).expect("expected no error"), None);
2164 counts(Some(
2165 ModerationActionCountsResult::Proof(Default::default()),
2166 ))
2167 .expect_err("expected a proof to be refused");
2168 }
2169}