Skip to main content

dpp/identity/identity_public_key/contract_bounds/
mod.rs

1use crate::identifier::Identifier;
2use crate::identity::identity_public_key::contract_bounds::ContractBounds::{
3    SingleContract, SingleContractDocumentType,
4};
5#[cfg(feature = "json-conversion")]
6use crate::serialization::JsonConvertible;
7#[cfg(feature = "value-conversion")]
8use crate::serialization::ValueConvertible;
9use crate::ProtocolError;
10use bincode::{Decode, Encode};
11use serde::{Deserialize, Serialize};
12
13pub type ContractBoundsType = u8;
14
15/// A contract bounds is the bounds that the key has influence on.
16/// For authentication keys the bounds mean that the keys can only be used to sign
17/// within the specified contract.
18/// For encryption decryption this tells clients to only use these keys for specific
19/// contracts.
20///
21#[cfg_attr(feature = "json-conversion", derive(JsonConvertible))]
22#[repr(u8)]
23#[derive(
24    Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Encode, Decode, Ord, PartialOrd, Hash,
25)]
26#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
27#[serde(tag = "type", rename_all = "camelCase")]
28pub enum ContractBounds {
29    /// this key can only be used within a specific contract
30    #[serde(rename = "singleContract")]
31    SingleContract { id: Identifier } = 0,
32    /// this key can only be used within a specific contract and for a specific document type
33    #[serde(rename = "documentType", rename_all = "camelCase")]
34    SingleContractDocumentType {
35        id: Identifier,
36        document_type_name: String,
37    } = 1,
38    // /// this key can only be used within contracts owned by a specified owner
39    // #[serde(rename = "multipleContractsOfSameOwner")]
40    // MultipleContractsOfSameOwner { owner_id: Identifier } = 2,
41}
42
43impl ContractBounds {
44    /// Creates a new contract bounds for the key
45    pub fn new_from_type(
46        contract_bounds_type: u8,
47        identifier: Vec<u8>,
48        document_type: String,
49    ) -> Result<Self, ProtocolError> {
50        Ok(match contract_bounds_type {
51            0 => SingleContract {
52                id: Identifier::from_bytes(identifier.as_slice())?,
53            },
54            1 => SingleContractDocumentType {
55                id: Identifier::from_bytes(identifier.as_slice())?,
56                document_type_name: document_type,
57            },
58            _ => {
59                return Err(ProtocolError::InvalidKeyContractBoundsError(format!(
60                    "unrecognized contract bounds type: {}",
61                    contract_bounds_type
62                )))
63            }
64        })
65    }
66
67    /// Gets the contract bounds type
68    pub fn contract_bounds_type(&self) -> ContractBoundsType {
69        match self {
70            SingleContract { .. } => 0,
71            SingleContractDocumentType { .. } => 1,
72            // MultipleContractsOfSameOwner { .. } => 2,
73        }
74    }
75
76    pub fn contract_bounds_type_from_str(str: &str) -> Result<ContractBoundsType, ProtocolError> {
77        match str {
78            "singleContract" => Ok(0),
79            "documentType" => Ok(1),
80            _ => Err(ProtocolError::DecodingError(String::from(
81                "Expected type to be one of none, singleContract or singleContractDocumentType",
82            ))),
83        }
84    }
85    /// Gets the contract bounds type
86    pub fn contract_bounds_type_string(&self) -> &str {
87        match self {
88            SingleContract { .. } => "singleContract",
89            SingleContractDocumentType { .. } => "documentType",
90            // MultipleContractsOfSameOwner { .. } => "multipleContractsOfSameOwner",
91        }
92    }
93
94    /// Gets the identifier
95    pub fn identifier(&self) -> &Identifier {
96        match self {
97            SingleContract { id } => id,
98            SingleContractDocumentType { id, .. } => id,
99            // MultipleContractsOfSameOwner { owner_id } => owner_id,
100        }
101    }
102
103    /// Gets the document type
104    pub fn document_type(&self) -> Option<&String> {
105        match self {
106            SingleContract { .. } => None,
107            SingleContractDocumentType {
108                document_type_name: document_type,
109                ..
110            } => Some(document_type),
111            // MultipleContractsOfSameOwner { .. } => None,
112        }
113    }
114    //
115    // /// Gets the cbor value
116    // pub fn to_cbor_value(&self) -> CborValue {
117    //     let mut pk_map = CborCanonicalMap::new();
118    //
119    //     let contract_bounds_type = self.contract_bounds_type();
120    //     pk_map.insert("type", self.contract_bounds_type_string());
121    //
122    //     pk_map.insert("identifier", self.identifier().to_buffer_vec());
123    //
124    //     if contract_bounds_type == 1 {
125    //         pk_map.insert("documentType", self.document_type().unwrap().clone());
126    //     }
127    //     pk_map.to_value_sorted()
128    // }
129    //
130    // /// Gets the cbor value
131    // pub fn from_cbor_value(cbor_value: &CborValue) -> Result<Self, ProtocolError> {
132    //     let key_value_map = cbor_value.as_map().ok_or_else(|| {
133    //         ProtocolError::DecodingError(String::from(
134    //             "Expected identity public key to be a key value map",
135    //         ))
136    //     })?;
137    //
138    //     let contract_bounds_type_string =
139    //         key_value_map.as_string("type", "Contract bounds must have a type")?;
140    //     let contract_bounds_type =
141    //         Self::contract_bounds_type_from_str(contract_bounds_type_string.as_str())?;
142    //     let contract_bounds_identifier = if contract_bounds_type > 0 {
143    //         key_value_map.as_vec(
144    //             "identifier",
145    //             "Contract bounds must have an identifier if it is not type 0",
146    //         )?
147    //     } else {
148    //         vec![]
149    //     };
150    //     let contract_bounds_document_type = if contract_bounds_type == 2 {
151    //         key_value_map.as_string(
152    //             "documentType",
153    //             "Contract bounds must have a document type if it is type 2",
154    //         )?
155    //     } else {
156    //         String::new()
157    //     };
158    //     ContractBounds::new_from_type(
159    //         contract_bounds_type,
160    //         contract_bounds_identifier,
161    //         contract_bounds_document_type,
162    //     )
163    // }
164}
165
166#[cfg(test)]
167mod core_tests {
168    use super::*;
169
170    // -- new_from_type: valid types --
171    #[test]
172    fn test_new_from_type_single_contract() {
173        let id_bytes = vec![0xAAu8; 32];
174        let bounds =
175            ContractBounds::new_from_type(0, id_bytes.clone(), "ignored".to_string()).unwrap();
176        assert!(matches!(bounds, ContractBounds::SingleContract { .. }));
177        assert_eq!(bounds.contract_bounds_type(), 0);
178        assert_eq!(bounds.contract_bounds_type_string(), "singleContract");
179        assert_eq!(bounds.identifier().as_bytes(), id_bytes.as_slice());
180        // document_type is None for SingleContract regardless of what we passed in.
181        assert!(bounds.document_type().is_none());
182    }
183
184    #[test]
185    fn test_new_from_type_single_contract_document_type() {
186        let id_bytes = vec![0xBBu8; 32];
187        let bounds = ContractBounds::new_from_type(1, id_bytes.clone(), "myDoc".to_string())
188            .expect("expected to construct SingleContractDocumentType");
189        assert!(matches!(
190            bounds,
191            ContractBounds::SingleContractDocumentType { .. }
192        ));
193        assert_eq!(bounds.contract_bounds_type(), 1);
194        assert_eq!(bounds.contract_bounds_type_string(), "documentType");
195        assert_eq!(bounds.identifier().as_bytes(), id_bytes.as_slice());
196        assert_eq!(bounds.document_type().map(String::as_str), Some("myDoc"));
197    }
198
199    // -- new_from_type: invalid type --
200    #[test]
201    fn test_new_from_type_unrecognized_type_returns_error() {
202        let id_bytes = vec![0xCCu8; 32];
203        let err = ContractBounds::new_from_type(99, id_bytes, "".to_string()).unwrap_err();
204        match err {
205            ProtocolError::InvalidKeyContractBoundsError(msg) => {
206                assert!(msg.contains("99"), "expected error message to mention 99");
207            }
208            other => panic!("expected InvalidKeyContractBoundsError, got {:?}", other),
209        }
210    }
211
212    // -- new_from_type: identifier wrong length --
213    #[test]
214    fn test_new_from_type_invalid_identifier_length_returns_error() {
215        // Identifier::from_bytes requires exactly 32 bytes.
216        let short = vec![0x01u8; 10];
217        assert!(ContractBounds::new_from_type(0, short, "".to_string()).is_err());
218    }
219
220    // -- contract_bounds_type_from_str --
221    #[test]
222    fn test_contract_bounds_type_from_str_single_contract() {
223        assert_eq!(
224            ContractBounds::contract_bounds_type_from_str("singleContract").unwrap(),
225            0
226        );
227    }
228
229    #[test]
230    fn test_contract_bounds_type_from_str_document_type() {
231        assert_eq!(
232            ContractBounds::contract_bounds_type_from_str("documentType").unwrap(),
233            1
234        );
235    }
236
237    #[test]
238    fn test_contract_bounds_type_from_str_unknown_returns_error() {
239        let err = ContractBounds::contract_bounds_type_from_str("garbage").unwrap_err();
240        match err {
241            ProtocolError::DecodingError(_) => {}
242            other => panic!("expected ProtocolError::DecodingError, got {:?}", other),
243        }
244    }
245
246    // -- equality / clone / hash (derives) --
247    #[test]
248    fn test_contract_bounds_equality_and_clone() {
249        let id = Identifier::from([0x11u8; 32]);
250        let a = ContractBounds::SingleContract { id };
251        let b = a.clone();
252        assert_eq!(a, b);
253
254        let different = ContractBounds::SingleContractDocumentType {
255            id,
256            document_type_name: "foo".to_string(),
257        };
258        assert_ne!(a, different);
259    }
260
261    #[test]
262    fn test_contract_bounds_type_string_roundtrip_with_from_str() {
263        // The string form produced by the variant should round-trip through from_str
264        // back to its numeric discriminant.
265        let id_bytes = vec![0xD0u8; 32];
266        let sc = ContractBounds::new_from_type(0, id_bytes.clone(), "".to_string()).unwrap();
267        let sctd = ContractBounds::new_from_type(1, id_bytes, "docType".to_string()).unwrap();
268
269        assert_eq!(
270            ContractBounds::contract_bounds_type_from_str(sc.contract_bounds_type_string())
271                .unwrap(),
272            sc.contract_bounds_type()
273        );
274        assert_eq!(
275            ContractBounds::contract_bounds_type_from_str(sctd.contract_bounds_type_string())
276                .unwrap(),
277            sctd.contract_bounds_type()
278        );
279    }
280}
281
282#[cfg(all(test, feature = "json-conversion"))]
283mod tests {
284    use super::*;
285    use crate::serialization::JsonConvertible;
286
287    #[test]
288    fn contract_bounds_single_contract_json_round_trip() {
289        let id = Identifier::from([0xABu8; 32]);
290        let bounds = ContractBounds::SingleContract { id };
291
292        let json = bounds.to_json().expect("to_json should succeed");
293        assert!(
294            json["id"].is_string(),
295            "Identifier should be a base58 string, got: {:?}",
296            json["id"]
297        );
298
299        let expected_base58 = id.to_string(platform_value::string_encoding::Encoding::Base58);
300        assert_eq!(json["id"].as_str().unwrap(), expected_base58);
301
302        let restored = ContractBounds::from_json(json).expect("from_json should succeed");
303        assert_eq!(bounds, restored);
304    }
305
306    #[test]
307    fn contract_bounds_document_type_json_round_trip() {
308        let id = Identifier::from([0xCDu8; 32]);
309        let bounds = ContractBounds::SingleContractDocumentType {
310            id,
311            document_type_name: "myDocument".to_string(),
312        };
313
314        let json = bounds.to_json().expect("to_json should succeed");
315        assert!(json["id"].is_string());
316        assert_eq!(json["documentTypeName"].as_str().unwrap(), "myDocument");
317
318        let restored = ContractBounds::from_json(json).expect("from_json should succeed");
319        assert_eq!(bounds, restored);
320    }
321
322    #[test]
323    fn contract_bounds_value_round_trip() {
324        let id = Identifier::from([0x55u8; 32]);
325        let bounds = ContractBounds::SingleContractDocumentType {
326            id,
327            document_type_name: "note".to_string(),
328        };
329
330        let obj = bounds.to_object().expect("to_object should succeed");
331        let restored = ContractBounds::from_object(obj).expect("from_object should succeed");
332        assert_eq!(bounds, restored);
333    }
334}