1use crate::data_contract::{DocumentName, TokenContractPosition};
13use crate::prelude::IdentityNonce;
14use crate::util::hash::hash_double;
15use crate::ProtocolError;
16use bincode::{Decode, DecodeUntrusted, Encode};
17use derive_more::From;
18use platform_serialization_derive::{
19 PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
20};
21use platform_value::Identifier;
22#[cfg(feature = "serde-conversion")]
23use serde::{Deserialize, Serialize};
24use std::collections::BTreeSet;
25use std::fmt;
26
27pub const CONTRACT_GROUP_ID_DOMAIN: &[u8] = b"contract_group";
29
30pub fn generate_contract_group_id(
36 owner_id: &Identifier,
37 identity_nonce: IdentityNonce,
38) -> Identifier {
39 let mut bytes = CONTRACT_GROUP_ID_DOMAIN.to_vec();
40 bytes.extend_from_slice(owner_id.as_slice());
41 bytes.extend_from_slice(&identity_nonce.to_be_bytes());
42 Identifier::from(hash_double(bytes))
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
51#[cfg_attr(
52 feature = "serde-conversion",
53 derive(Serialize, Deserialize),
54 serde(rename_all = "camelCase")
55)]
56pub enum ContractGroupOwner {
57 SingleOwner(Identifier),
59 OwnerAndAdmins {
62 owner: Identifier,
64 admins: BTreeSet<Identifier>,
66 },
67}
68
69impl ContractGroupOwner {
70 pub fn owner_id(&self) -> &Identifier {
72 match self {
73 ContractGroupOwner::SingleOwner(owner_id) => owner_id,
74 ContractGroupOwner::OwnerAndAdmins { owner, .. } => owner,
75 }
76 }
77
78 pub fn admin_ids(&self) -> Option<&BTreeSet<Identifier>> {
80 match self {
81 ContractGroupOwner::SingleOwner(_) => None,
82 ContractGroupOwner::OwnerAndAdmins { admins, .. } => Some(admins),
83 }
84 }
85
86 pub fn admin_count(&self) -> usize {
88 self.admin_ids().map_or(0, BTreeSet::len)
89 }
90
91 pub fn may_add_members(&self, identity_id: &Identifier) -> bool {
93 self.owner_id() == identity_id
94 || self
95 .admin_ids()
96 .is_some_and(|admins| admins.contains(identity_id))
97 }
98}
99
100impl fmt::Display for ContractGroupOwner {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match self {
103 ContractGroupOwner::SingleOwner(owner_id) => write!(f, "single owner {}", owner_id),
104 ContractGroupOwner::OwnerAndAdmins { owner, admins } => {
105 write!(f, "owner {} with {} admins", owner, admins.len())
106 }
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, DecodeUntrusted)]
116#[cfg_attr(
117 feature = "serde-conversion",
118 derive(Serialize, Deserialize),
119 serde(rename_all = "camelCase")
120)]
121pub enum ContractGroupMember {
122 Contract,
124 DocumentType(DocumentName),
126 Token(TokenContractPosition),
128}
129
130impl fmt::Display for ContractGroupMember {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 match self {
133 ContractGroupMember::Contract => write!(f, "contract"),
134 ContractGroupMember::DocumentType(name) => write!(f, "document type {}", name),
135 ContractGroupMember::Token(position) => write!(f, "token {}", position),
136 }
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, DecodeUntrusted)]
142#[cfg_attr(
143 feature = "serde-conversion",
144 derive(Serialize, Deserialize),
145 serde(rename_all = "camelCase")
146)]
147pub struct ContractGroupMembership {
148 pub contract_group_id: Identifier,
151 pub member: ContractGroupMember,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
160#[cfg_attr(
161 feature = "serde-conversion",
162 derive(Serialize, Deserialize),
163 serde(rename_all = "camelCase")
164)]
165pub struct ContractGroupRegistration {
166 #[cfg_attr(feature = "serde-conversion", serde(default))]
170 pub admins: BTreeSet<Identifier>,
171 pub name: Option<String>,
173 pub description: Option<String>,
176}
177
178#[derive(
180 Debug,
181 Clone,
182 PartialEq,
183 Eq,
184 Encode,
185 Decode,
186 DecodeUntrusted,
187 PlatformSerialize,
188 PlatformDeserializeTrusted,
189 PlatformDeserializeUntrusted,
190 From,
191)]
192#[cfg_attr(
193 feature = "serde-conversion",
194 derive(Serialize, Deserialize),
195 serde(tag = "$formatVersion")
196)]
197#[platform_serialize(unversioned)]
198pub enum ContractGroupInfo {
199 #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))]
201 V0(ContractGroupInfoV0),
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
206#[cfg_attr(
207 feature = "serde-conversion",
208 derive(Serialize, Deserialize),
209 serde(rename_all = "camelCase")
210)]
211pub struct ContractGroupInfoV0 {
212 pub owner: ContractGroupOwner,
214 pub name: Option<String>,
216 pub description: Option<String>,
218}
219
220impl ContractGroupInfo {
221 pub fn owner(&self) -> &ContractGroupOwner {
223 match self {
224 ContractGroupInfo::V0(info) => &info.owner,
225 }
226 }
227
228 pub fn name(&self) -> Option<&str> {
230 match self {
231 ContractGroupInfo::V0(info) => info.name.as_deref(),
232 }
233 }
234
235 pub fn description(&self) -> Option<&str> {
237 match self {
238 ContractGroupInfo::V0(info) => info.description.as_deref(),
239 }
240 }
241}
242
243impl From<(Identifier, ContractGroupRegistration)> for ContractGroupInfo {
244 fn from((owner_id, registration): (Identifier, ContractGroupRegistration)) -> Self {
246 let ContractGroupRegistration {
247 admins,
248 name,
249 description,
250 } = registration;
251 let owner = if admins.is_empty() {
252 ContractGroupOwner::SingleOwner(owner_id)
253 } else {
254 ContractGroupOwner::OwnerAndAdmins {
255 owner: owner_id,
256 admins,
257 }
258 };
259 ContractGroupInfo::V0(ContractGroupInfoV0 {
260 owner,
261 name,
262 description,
263 })
264 }
265}
266
267impl From<(Identifier, &ContractGroupRegistration)> for ContractGroupInfo {
268 fn from((owner_id, registration): (Identifier, &ContractGroupRegistration)) -> Self {
269 (owner_id, registration.clone()).into()
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::data_contract::DataContract;
277
278 #[test]
279 fn should_derive_a_contract_group_id_distinct_from_the_contract_id() {
280 let owner_id = Identifier::from([7u8; 32]);
281 let nonce = 42;
282
283 let group_id = generate_contract_group_id(&owner_id, nonce);
284 let contract_id = DataContract::generate_data_contract_id_v0(owner_id, nonce);
285
286 assert_ne!(group_id, contract_id);
287 assert_eq!(group_id, generate_contract_group_id(&owner_id, nonce));
288 assert_ne!(group_id, generate_contract_group_id(&owner_id, nonce + 1));
289 assert_ne!(
290 group_id,
291 generate_contract_group_id(&Identifier::from([8u8; 32]), nonce)
292 );
293 }
294
295 #[test]
296 fn should_resolve_who_may_add_members_for_both_owner_kinds() {
297 let alice = Identifier::from([1u8; 32]);
298 let bob = Identifier::from([2u8; 32]);
299 let carol = Identifier::from([3u8; 32]);
300
301 let single = ContractGroupOwner::SingleOwner(alice);
302 assert_eq!(single.owner_id(), &alice);
303 assert!(single.may_add_members(&alice));
304 assert!(!single.may_add_members(&bob));
305 assert_eq!(single.admin_count(), 0);
306
307 let with_admins = ContractGroupOwner::OwnerAndAdmins {
308 owner: alice,
309 admins: BTreeSet::from([bob]),
310 };
311 assert_eq!(with_admins.owner_id(), &alice);
312 assert!(with_admins.may_add_members(&alice));
313 assert!(with_admins.may_add_members(&bob));
314 assert!(!with_admins.may_add_members(&carol));
315 assert_eq!(with_admins.admin_count(), 1);
316 }
317
318 #[test]
319 fn should_store_a_single_owner_when_the_registration_names_no_admins() {
320 let owner_id = Identifier::from([1u8; 32]);
321 let info: ContractGroupInfo = (
322 owner_id,
323 ContractGroupRegistration {
324 admins: BTreeSet::new(),
325 name: None,
326 description: None,
327 },
328 )
329 .into();
330 assert_eq!(info.owner(), &ContractGroupOwner::SingleOwner(owner_id));
331 }
332
333 #[test]
334 fn should_round_trip_the_stored_info_through_bincode() {
335 use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
336
337 let info: ContractGroupInfo = (
338 Identifier::from([1u8; 32]),
339 ContractGroupRegistration {
340 admins: BTreeSet::from([Identifier::from([2u8; 32])]),
341 name: Some("cardgame".to_string()),
342 description: None,
343 },
344 )
345 .into();
346
347 let bytes = info.serialize_to_bytes().expect("serialize");
348 let decoded = ContractGroupInfo::deserialize_from_bytes_untrusted(&bytes)
349 .expect("deserialize untrusted");
350
351 assert_eq!(decoded, info);
352 assert_eq!(decoded.name(), Some("cardgame"));
353 assert_eq!(decoded.description(), None);
354 assert_eq!(decoded.owner().owner_id(), &Identifier::from([1u8; 32]));
355 assert_eq!(decoded.owner().admin_count(), 1);
356 }
357}