1#[cfg(feature = "state-transitions")]
2use crate::contract_group::ContractGroupMember;
3use crate::identifier::Identifier;
4use crate::identity::identity_public_key::contract_bounds::ContractBounds::{
5 ContractGroup, SingleContract, SingleContractDocumentType,
6};
7#[cfg(feature = "json-conversion")]
8use crate::serialization::JsonConvertible;
9#[cfg(feature = "value-conversion")]
10use crate::serialization::ValueConvertible;
11#[cfg(feature = "state-transitions")]
12use crate::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods;
13#[cfg(feature = "state-transitions")]
14use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods;
15#[cfg(feature = "state-transitions")]
16use crate::state_transition::batch_transition::batched_transition::BatchedTransitionRef;
17#[cfg(feature = "state-transitions")]
18use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods;
19use crate::ProtocolError;
20use bincode::{Decode, DecodeUntrusted, Encode};
21use serde::{Deserialize, Serialize};
22
23pub type ContractBoundsType = u8;
24
25#[cfg_attr(feature = "json-conversion", derive(JsonConvertible))]
34#[repr(u8)]
35#[derive(
36 Debug,
37 PartialEq,
38 Eq,
39 Clone,
40 Serialize,
41 Deserialize,
42 Encode,
43 Decode,
44 Ord,
45 PartialOrd,
46 Hash,
47 DecodeUntrusted,
48)]
49#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
50#[serde(tag = "$type", rename_all = "camelCase")]
51pub enum ContractBounds {
52 #[serde(rename = "singleContract")]
54 SingleContract { id: Identifier } = 0,
55 #[serde(rename = "documentType", rename_all = "camelCase")]
57 SingleContractDocumentType {
58 id: Identifier,
59 document_type_name: String,
60 } = 1,
61 #[serde(rename = "contractGroup")]
64 ContractGroup { id: Identifier } = 2,
65}
66
67#[cfg(feature = "state-transitions")]
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum BatchedTransitionBoundsCheck {
71 Allowed,
73 Denied,
75 RequiresContractGroupMembership {
79 contract_group_id: Identifier,
80 contract_id: Identifier,
81 member: ContractGroupMember,
82 },
83}
84
85impl ContractBounds {
86 pub fn new_from_type(
88 contract_bounds_type: u8,
89 identifier: Vec<u8>,
90 document_type: String,
91 ) -> Result<Self, ProtocolError> {
92 Ok(match contract_bounds_type {
93 0 => SingleContract {
94 id: Identifier::from_bytes(identifier.as_slice())?,
95 },
96 1 => SingleContractDocumentType {
97 id: Identifier::from_bytes(identifier.as_slice())?,
98 document_type_name: document_type,
99 },
100 2 => ContractGroup {
101 id: Identifier::from_bytes(identifier.as_slice())?,
102 },
103 _ => {
104 return Err(ProtocolError::InvalidKeyContractBoundsError(format!(
105 "unrecognized contract bounds type: {}",
106 contract_bounds_type
107 )))
108 }
109 })
110 }
111
112 pub fn contract_bounds_type(&self) -> ContractBoundsType {
114 match self {
115 SingleContract { .. } => 0,
116 SingleContractDocumentType { .. } => 1,
117 ContractGroup { .. } => 2,
118 }
119 }
120
121 pub fn contract_bounds_type_from_str(str: &str) -> Result<ContractBoundsType, ProtocolError> {
122 match str {
123 "singleContract" => Ok(0),
124 "documentType" => Ok(1),
125 "contractGroup" => Ok(2),
126 _ => Err(ProtocolError::DecodingError(String::from(
127 "Expected type to be one of singleContract, documentType or contractGroup",
128 ))),
129 }
130 }
131 pub fn contract_bounds_type_string(&self) -> &str {
133 match self {
134 SingleContract { .. } => "singleContract",
135 SingleContractDocumentType { .. } => "documentType",
136 ContractGroup { .. } => "contractGroup",
137 }
138 }
139
140 pub fn identifier(&self) -> &Identifier {
143 match self {
144 SingleContract { id } => id,
145 SingleContractDocumentType { id, .. } => id,
146 ContractGroup { id } => id,
147 }
148 }
149
150 pub fn contract_id(&self) -> Option<&Identifier> {
152 match self {
153 SingleContract { id } | SingleContractDocumentType { id, .. } => Some(id),
154 ContractGroup { .. } => None,
155 }
156 }
157
158 pub fn contract_group_id(&self) -> Option<&Identifier> {
160 match self {
161 SingleContract { .. } | SingleContractDocumentType { .. } => None,
162 ContractGroup { id } => Some(id),
163 }
164 }
165
166 pub fn document_type(&self) -> Option<&String> {
168 match self {
169 SingleContract { .. } => None,
170 SingleContractDocumentType {
171 document_type_name: document_type,
172 ..
173 } => Some(document_type),
174 ContractGroup { .. } => None,
175 }
176 }
177
178 #[cfg(feature = "state-transitions")]
184 pub fn check_batched_transition(
185 &self,
186 transition: BatchedTransitionRef<'_>,
187 ) -> BatchedTransitionBoundsCheck {
188 use BatchedTransitionBoundsCheck::{Allowed, Denied, RequiresContractGroupMembership};
189 let allowed_if = |inside: bool| if inside { Allowed } else { Denied };
190 match (self, transition) {
191 (SingleContract { id }, BatchedTransitionRef::Document(document)) => {
192 allowed_if(document.data_contract_id() == *id)
193 }
194 (SingleContract { id }, BatchedTransitionRef::Token(token)) => {
195 allowed_if(token.data_contract_id() == *id)
196 }
197 (
198 SingleContractDocumentType {
199 id,
200 document_type_name,
201 },
202 BatchedTransitionRef::Document(document),
203 ) => allowed_if(
204 document.data_contract_id() == *id
205 && document.document_type_name() == document_type_name.as_str(),
206 ),
207 (SingleContractDocumentType { .. }, BatchedTransitionRef::Token(_)) => Denied,
208 (ContractGroup { id }, BatchedTransitionRef::Document(document)) => {
209 RequiresContractGroupMembership {
210 contract_group_id: *id,
211 contract_id: document.data_contract_id(),
212 member: ContractGroupMember::DocumentType(
213 document.document_type_name().to_string(),
214 ),
215 }
216 }
217 (ContractGroup { id }, BatchedTransitionRef::Token(token)) => {
218 RequiresContractGroupMembership {
219 contract_group_id: *id,
220 contract_id: token.data_contract_id(),
221 member: ContractGroupMember::Token(token.base().token_contract_position()),
222 }
223 }
224 }
225 }
226}
227
228#[cfg(test)]
229mod core_tests {
230 use super::*;
231
232 #[test]
234 fn test_new_from_type_single_contract() {
235 let id_bytes = vec![0xAAu8; 32];
236 let bounds =
237 ContractBounds::new_from_type(0, id_bytes.clone(), "ignored".to_string()).unwrap();
238 assert!(matches!(bounds, ContractBounds::SingleContract { .. }));
239 assert_eq!(bounds.contract_bounds_type(), 0);
240 assert_eq!(bounds.contract_bounds_type_string(), "singleContract");
241 assert_eq!(bounds.identifier().as_bytes(), id_bytes.as_slice());
242 assert!(bounds.document_type().is_none());
244 }
245
246 #[test]
247 fn test_new_from_type_single_contract_document_type() {
248 let id_bytes = vec![0xBBu8; 32];
249 let bounds = ContractBounds::new_from_type(1, id_bytes.clone(), "myDoc".to_string())
250 .expect("expected to construct SingleContractDocumentType");
251 assert!(matches!(
252 bounds,
253 ContractBounds::SingleContractDocumentType { .. }
254 ));
255 assert_eq!(bounds.contract_bounds_type(), 1);
256 assert_eq!(bounds.contract_bounds_type_string(), "documentType");
257 assert_eq!(bounds.identifier().as_bytes(), id_bytes.as_slice());
258 assert_eq!(bounds.document_type().map(String::as_str), Some("myDoc"));
259 }
260
261 #[test]
262 fn should_build_contract_group_bounds_from_type_two() {
263 let id_bytes = vec![0xEEu8; 32];
264 let bounds = ContractBounds::new_from_type(2, id_bytes.clone(), "ignored".to_string())
265 .expect("expected to construct ContractGroup");
266 assert!(matches!(bounds, ContractBounds::ContractGroup { .. }));
267 assert_eq!(bounds.contract_bounds_type(), 2);
268 assert_eq!(bounds.contract_bounds_type_string(), "contractGroup");
269 assert_eq!(bounds.identifier().as_bytes(), id_bytes.as_slice());
270 assert_eq!(
271 bounds.contract_group_id().map(|id| id.as_slice()),
272 Some(id_bytes.as_slice())
273 );
274 assert!(bounds.contract_id().is_none());
275 assert!(bounds.document_type().is_none());
276 assert_eq!(
277 ContractBounds::contract_bounds_type_from_str("contractGroup").unwrap(),
278 2
279 );
280 }
281
282 #[test]
283 fn should_expose_the_contract_id_only_for_contract_bounds() {
284 let id = Identifier::from([0x12u8; 32]);
285 let single = ContractBounds::SingleContract { id };
286 let typed = ContractBounds::SingleContractDocumentType {
287 id,
288 document_type_name: "note".to_string(),
289 };
290 assert_eq!(single.contract_id(), Some(&id));
291 assert_eq!(typed.contract_id(), Some(&id));
292 assert!(single.contract_group_id().is_none());
293 assert!(typed.contract_group_id().is_none());
294 }
295
296 #[test]
298 fn test_new_from_type_unrecognized_type_returns_error() {
299 let id_bytes = vec![0xCCu8; 32];
300 let err = ContractBounds::new_from_type(99, id_bytes, "".to_string()).unwrap_err();
301 match err {
302 ProtocolError::InvalidKeyContractBoundsError(msg) => {
303 assert!(msg.contains("99"), "expected error message to mention 99");
304 }
305 other => panic!("expected InvalidKeyContractBoundsError, got {:?}", other),
306 }
307 }
308
309 #[test]
311 fn test_new_from_type_invalid_identifier_length_returns_error() {
312 let short = vec![0x01u8; 10];
314 assert!(ContractBounds::new_from_type(0, short, "".to_string()).is_err());
315 }
316
317 #[test]
319 fn test_contract_bounds_type_from_str_single_contract() {
320 assert_eq!(
321 ContractBounds::contract_bounds_type_from_str("singleContract").unwrap(),
322 0
323 );
324 }
325
326 #[test]
327 fn test_contract_bounds_type_from_str_document_type() {
328 assert_eq!(
329 ContractBounds::contract_bounds_type_from_str("documentType").unwrap(),
330 1
331 );
332 }
333
334 #[test]
335 fn test_contract_bounds_type_from_str_unknown_returns_error() {
336 let err = ContractBounds::contract_bounds_type_from_str("garbage").unwrap_err();
337 match err {
338 ProtocolError::DecodingError(_) => {}
339 other => panic!("expected ProtocolError::DecodingError, got {:?}", other),
340 }
341 }
342
343 #[test]
345 fn test_contract_bounds_equality_and_clone() {
346 let id = Identifier::from([0x11u8; 32]);
347 let a = ContractBounds::SingleContract { id };
348 let b = a.clone();
349 assert_eq!(a, b);
350
351 let different = ContractBounds::SingleContractDocumentType {
352 id,
353 document_type_name: "foo".to_string(),
354 };
355 assert_ne!(a, different);
356 }
357
358 #[test]
359 fn test_contract_bounds_type_string_roundtrip_with_from_str() {
360 let id_bytes = vec![0xD0u8; 32];
363 let sc = ContractBounds::new_from_type(0, id_bytes.clone(), "".to_string()).unwrap();
364 let sctd = ContractBounds::new_from_type(1, id_bytes, "docType".to_string()).unwrap();
365
366 assert_eq!(
367 ContractBounds::contract_bounds_type_from_str(sc.contract_bounds_type_string())
368 .unwrap(),
369 sc.contract_bounds_type()
370 );
371 assert_eq!(
372 ContractBounds::contract_bounds_type_from_str(sctd.contract_bounds_type_string())
373 .unwrap(),
374 sctd.contract_bounds_type()
375 );
376 }
377}
378
379#[cfg(all(test, feature = "json-conversion"))]
380mod tests {
381 use super::*;
382 use crate::serialization::JsonConvertible;
383
384 #[test]
385 fn contract_bounds_single_contract_json_round_trip() {
386 let id = Identifier::from([0xABu8; 32]);
387 let bounds = ContractBounds::SingleContract { id };
388
389 let json = bounds.to_json().expect("to_json should succeed");
390 assert!(
391 json["id"].is_string(),
392 "Identifier should be a base58 string, got: {:?}",
393 json["id"]
394 );
395
396 let expected_base58 = id.to_string(platform_value::string_encoding::Encoding::Base58);
397 assert_eq!(json["id"].as_str().unwrap(), expected_base58);
398
399 let restored = ContractBounds::from_json(json).expect("from_json should succeed");
400 assert_eq!(bounds, restored);
401 }
402
403 #[test]
404 fn contract_bounds_document_type_json_round_trip() {
405 let id = Identifier::from([0xCDu8; 32]);
406 let bounds = ContractBounds::SingleContractDocumentType {
407 id,
408 document_type_name: "myDocument".to_string(),
409 };
410
411 let json = bounds.to_json().expect("to_json should succeed");
412 assert!(json["id"].is_string());
413 assert_eq!(json["documentTypeName"].as_str().unwrap(), "myDocument");
414
415 let restored = ContractBounds::from_json(json).expect("from_json should succeed");
416 assert_eq!(bounds, restored);
417 }
418
419 #[test]
420 fn should_round_trip_contract_group_bounds_through_json() {
421 let id = Identifier::from([0xEFu8; 32]);
422 let bounds = ContractBounds::ContractGroup { id };
423
424 let json = bounds.to_json().expect("to_json should succeed");
425 assert_eq!(json["$type"].as_str().unwrap(), "contractGroup");
426 assert_eq!(
427 json["id"].as_str().unwrap(),
428 id.to_string(platform_value::string_encoding::Encoding::Base58)
429 );
430 assert!(json.get("documentTypeName").is_none());
431
432 let restored = ContractBounds::from_json(json).expect("from_json should succeed");
433 assert_eq!(bounds, restored);
434
435 let obj = bounds.to_object().expect("to_object should succeed");
436 assert_eq!(
437 ContractBounds::from_object(obj).expect("from_object"),
438 bounds
439 );
440 }
441
442 #[test]
443 fn contract_bounds_value_round_trip() {
444 let id = Identifier::from([0x55u8; 32]);
445 let bounds = ContractBounds::SingleContractDocumentType {
446 id,
447 document_type_name: "note".to_string(),
448 };
449
450 let obj = bounds.to_object().expect("to_object should succeed");
451 let restored = ContractBounds::from_object(obj).expect("from_object should succeed");
452 assert_eq!(bounds, restored);
453 }
454}
455
456#[cfg(all(test, feature = "state-transitions"))]
457mod batched_transition_tests {
458 use super::{BatchedTransitionBoundsCheck, ContractBounds};
459 use crate::contract_group::ContractGroupMember;
460 use crate::identifier::Identifier;
461 use crate::state_transition::batch_transition::batched_transition::{
462 document_create_transition::DocumentCreateTransitionV0,
463 document_delete_transition::DocumentDeleteTransitionV0,
464 document_index_only_delete_transition::DocumentIndexOnlyDeleteTransitionV0,
465 document_purchase_transition::DocumentPurchaseTransitionV0,
466 document_replace_transition::DocumentReplaceTransitionV0,
467 document_transfer_transition::DocumentTransferTransitionV0,
468 document_update_price_transition::DocumentUpdatePriceTransitionV0,
469 token_transfer_transition::TokenTransferTransitionV0, BatchedTransitionRef,
470 DocumentTransition, TokenTransition,
471 };
472
473 fn documents() -> Vec<DocumentTransition> {
475 vec![
476 DocumentTransition::Create(DocumentCreateTransitionV0::default().into()),
477 DocumentTransition::Replace(DocumentReplaceTransitionV0::default().into()),
478 DocumentTransition::Delete(DocumentDeleteTransitionV0::default().into()),
479 DocumentTransition::IndexOnlyDelete(
480 DocumentIndexOnlyDeleteTransitionV0::default().into(),
481 ),
482 DocumentTransition::Transfer(DocumentTransferTransitionV0::default().into()),
483 DocumentTransition::UpdatePrice(DocumentUpdatePriceTransitionV0::default().into()),
484 DocumentTransition::Purchase(DocumentPurchaseTransitionV0::default().into()),
485 ]
486 }
487
488 fn tokens() -> Vec<TokenTransition> {
489 vec![
490 TokenTransition::Burn(Default::default()),
491 TokenTransition::Mint(Default::default()),
492 TokenTransition::Transfer(TokenTransferTransitionV0::default().into()),
493 TokenTransition::Freeze(Default::default()),
494 TokenTransition::Unfreeze(Default::default()),
495 TokenTransition::DestroyFrozenFunds(Default::default()),
496 TokenTransition::Claim(Default::default()),
497 TokenTransition::EmergencyAction(Default::default()),
498 TokenTransition::ConfigUpdate(Default::default()),
499 TokenTransition::DirectPurchase(Default::default()),
500 TokenTransition::SetPriceForDirectPurchase(Default::default()),
501 ]
502 }
503
504 #[test]
505 fn single_contract_bounds_cover_every_operation_on_that_contract_only() {
506 let bounds = ContractBounds::SingleContract {
507 id: Identifier::from([0; 32]),
508 };
509 let foreign = ContractBounds::SingleContract {
510 id: Identifier::from([1; 32]),
511 };
512 for document in documents() {
513 let member = BatchedTransitionRef::Document(&document);
514 assert_eq!(
515 bounds.check_batched_transition(member),
516 BatchedTransitionBoundsCheck::Allowed,
517 "{document:?}"
518 );
519 assert_eq!(
520 foreign.check_batched_transition(member),
521 BatchedTransitionBoundsCheck::Denied,
522 "{document:?}"
523 );
524 }
525 for token in tokens() {
526 let member = BatchedTransitionRef::Token(&token);
527 assert_eq!(
528 bounds.check_batched_transition(member),
529 BatchedTransitionBoundsCheck::Allowed,
530 "{token:?}"
531 );
532 assert_eq!(
533 foreign.check_batched_transition(member),
534 BatchedTransitionBoundsCheck::Denied,
535 "{token:?}"
536 );
537 }
538 }
539
540 #[test]
541 fn document_type_bounds_cover_that_type_only_and_never_tokens() {
542 let bounds = ContractBounds::SingleContractDocumentType {
543 id: Identifier::from([0; 32]),
544 document_type_name: String::new(),
545 };
546 let other_type = ContractBounds::SingleContractDocumentType {
547 id: Identifier::from([0; 32]),
548 document_type_name: "other".to_string(),
549 };
550 for document in documents() {
551 let member = BatchedTransitionRef::Document(&document);
552 assert_eq!(
553 bounds.check_batched_transition(member),
554 BatchedTransitionBoundsCheck::Allowed,
555 "{document:?}"
556 );
557 assert_eq!(
558 other_type.check_batched_transition(member),
559 BatchedTransitionBoundsCheck::Denied,
560 "{document:?}"
561 );
562 }
563 for token in tokens() {
564 let member = BatchedTransitionRef::Token(&token);
565 assert_eq!(
566 bounds.check_batched_transition(member),
567 BatchedTransitionBoundsCheck::Denied,
568 "token operations are contract-wide: {token:?}"
569 );
570 }
571 }
572
573 #[test]
574 fn should_defer_contract_group_bounds_to_state_with_the_member_to_look_up() {
575 let group_id = Identifier::from([7; 32]);
576 let bounds = ContractBounds::ContractGroup { id: group_id };
577 for document in documents() {
578 let member = BatchedTransitionRef::Document(&document);
579 assert_eq!(
580 bounds.check_batched_transition(member),
581 BatchedTransitionBoundsCheck::RequiresContractGroupMembership {
582 contract_group_id: group_id,
583 contract_id: Identifier::from([0; 32]),
584 member: ContractGroupMember::DocumentType(String::new()),
585 },
586 "{document:?}"
587 );
588 }
589 for token in tokens() {
590 let member = BatchedTransitionRef::Token(&token);
591 assert_eq!(
592 bounds.check_batched_transition(member),
593 BatchedTransitionBoundsCheck::RequiresContractGroupMembership {
594 contract_group_id: group_id,
595 contract_id: Identifier::from([0; 32]),
596 member: ContractGroupMember::Token(0),
597 },
598 "{token:?}"
599 );
600 }
601 }
602}