Skip to main content

xmtp_mls/groups/
intents.rs

1use super::{
2    group_membership::GroupMembership,
3    group_permissions::{MembershipPolicies, MetadataPolicies, PermissionsPolicies},
4};
5use crate::groups::mls_ext::WelcomePointersExtension;
6use openmls::key_packages::KeyPackage;
7use openmls::prelude::{
8    MlsMessageOut,
9    tls_codec::{Error as TlsCodecError, Serialize},
10};
11use prost::{Message, bytes::Bytes};
12use std::collections::{HashMap, HashSet};
13use thiserror::Error;
14use xmtp_common::types::Address;
15use xmtp_configuration::{
16    GROUP_KEY_ROTATION_INTERVAL_NS, WELCOME_POINTEE_ENCRYPTION_AEAD_TYPES_EXTENSION_ID,
17};
18use xmtp_id::key_package::{
19    KeyPackageVerificationError, VerifiedKeyPackageV2, WrapperAlgorithm, WrapperEncryptionExtension,
20};
21use xmtp_mls_common::{
22    app_data::{
23        component_id::ComponentId,
24        fields::{FieldWrite, WriteOperation},
25    },
26    group_mutable_metadata::MetadataField,
27};
28use xmtp_proto::{
29    ConversionError,
30    xmtp::mls::database::{
31        AccountAddresses, AddressesOrInstallationIds as AddressesOrInstallationIdsProtoWrapper,
32        CommitPendingProposalsData, InstallationIds, PostCommitAction as PostCommitActionProto,
33        ProposeGroupContextExtensionData, ProposeMemberUpdateData, ReaddInstallationsData,
34        SendMessageData, UpdateAdminListsData, UpdateGroupMembershipData, UpdateMetadataData,
35        UpdatePermissionData,
36        addresses_or_installation_ids::AddressesOrInstallationIds as AddressesOrInstallationIdsProto,
37        commit_pending_proposals_data,
38        post_commit_action::{
39            Installation as InstallationProto, Kind as PostCommitActionKind,
40            SendWelcomes as SendWelcomesProto,
41        },
42        propose_group_context_extension_data, propose_member_update_data,
43        readd_installations_data::{
44            V1 as ReaddInstallationsV1, Version as ReaddInstallationsVersion,
45        },
46        send_message_data::{V1 as SendMessageV1, Version as SendMessageVersion},
47        update_admin_lists_data::{V1 as UpdateAdminListsV1, Version as UpdateAdminListsVersion},
48        update_group_membership_data::{
49            V1 as UpdateGroupMembershipV1, Version as UpdateGroupMembershipVersion,
50        },
51        update_metadata_data::{V1 as UpdateMetadataV1, Version as UpdateMetadataVersion},
52        update_permission_data::{
53            self, V1 as UpdatePermissionV1, Version as UpdatePermissionVersion,
54        },
55    },
56    xmtp::mls::message_contents::ComponentType,
57};
58
59mod queue;
60pub use queue::*;
61
62#[derive(Debug, Error)]
63pub enum IntentError {
64    #[error("conversion error: {0}")]
65    Conversion(#[from] xmtp_proto::ConversionError),
66    #[error("key package verification: {0}")]
67    KeyPackageVerification(#[from] KeyPackageVerificationError),
68    #[error("TLS Codec error: {0}")]
69    TlsError(#[from] TlsCodecError),
70    #[error(transparent)]
71    Storage(#[from] xmtp_db::StorageError),
72    #[error("missing update permission")]
73    MissingUpdatePermissionVersion,
74    #[error("missing payload")]
75    MissingPayload,
76    #[error("missing update admin version")]
77    MissingUpdateAdminVersion,
78    #[error("missing post commit action")]
79    MissingPostCommit,
80    #[error("unsupported permission version")]
81    UnsupportedPermissionVersion,
82    #[error("unknown permission update type")]
83    UnknownPermissionUpdateType,
84    #[error("unknown value for PermissionPolicyOption")]
85    UnknownPermissionPolicyOption,
86    #[error("unknown value for AdminListActionType")]
87    UnknownAdminListAction,
88    #[error("intent payload error: {0}")]
89    Generic(String),
90}
91
92impl From<prost::DecodeError> for IntentError {
93    fn from(error: prost::DecodeError) -> Self {
94        IntentError::Conversion(xmtp_proto::ConversionError::Decode(error))
95    }
96}
97
98#[derive(Debug, Clone)]
99pub struct SendMessageIntentData {
100    pub message: Vec<u8>,
101}
102
103impl SendMessageIntentData {
104    pub fn new(message: Vec<u8>) -> Self {
105        Self { message }
106    }
107
108    pub(crate) fn to_bytes(&self) -> Vec<u8> {
109        SendMessageData {
110            version: Some(SendMessageVersion::V1(SendMessageV1 {
111                payload_bytes: self.message.clone(),
112            })),
113        }
114        .encode_to_vec()
115    }
116
117    pub(crate) fn from_bytes(data: &[u8]) -> Result<Self, IntentError> {
118        let msg = SendMessageData::decode(data)?;
119        let payload_bytes = match msg.version {
120            Some(SendMessageVersion::V1(v1)) => v1.payload_bytes,
121            None => return Err(IntentError::MissingPayload),
122        };
123
124        Ok(Self::new(payload_bytes))
125    }
126}
127
128impl From<SendMessageIntentData> for Vec<u8> {
129    fn from(intent: SendMessageIntentData) -> Self {
130        intent.to_bytes()
131    }
132}
133
134#[derive(Debug, PartialEq, Eq, Clone)]
135pub enum AddressesOrInstallationIds {
136    AccountAddresses(Vec<String>),
137    InstallationIds(Vec<Vec<u8>>),
138}
139
140impl From<AddressesOrInstallationIds> for AddressesOrInstallationIdsProtoWrapper {
141    fn from(address_or_id: AddressesOrInstallationIds) -> Self {
142        match address_or_id {
143            AddressesOrInstallationIds::AccountAddresses(account_addresses) => {
144                AddressesOrInstallationIdsProtoWrapper {
145                    addresses_or_installation_ids: Some(
146                        AddressesOrInstallationIdsProto::AccountAddresses(AccountAddresses {
147                            account_addresses,
148                        }),
149                    ),
150                }
151            }
152            AddressesOrInstallationIds::InstallationIds(installation_ids) => {
153                AddressesOrInstallationIdsProtoWrapper {
154                    addresses_or_installation_ids: Some(
155                        AddressesOrInstallationIdsProto::InstallationIds(InstallationIds {
156                            installation_ids,
157                        }),
158                    ),
159                }
160            }
161        }
162    }
163}
164
165impl TryFrom<AddressesOrInstallationIdsProtoWrapper> for AddressesOrInstallationIds {
166    type Error = IntentError;
167
168    fn try_from(wrapper: AddressesOrInstallationIdsProtoWrapper) -> Result<Self, Self::Error> {
169        match wrapper.addresses_or_installation_ids {
170            Some(AddressesOrInstallationIdsProto::AccountAddresses(addrs)) => Ok(
171                AddressesOrInstallationIds::AccountAddresses(addrs.account_addresses),
172            ),
173            Some(AddressesOrInstallationIdsProto::InstallationIds(ids)) => Ok(
174                AddressesOrInstallationIds::InstallationIds(ids.installation_ids),
175            ),
176            _ => Err(IntentError::MissingPayload),
177        }
178    }
179}
180
181impl From<Vec<Address>> for AddressesOrInstallationIds {
182    fn from(addrs: Vec<Address>) -> Self {
183        AddressesOrInstallationIds::AccountAddresses(addrs)
184    }
185}
186
187impl From<Vec<Vec<u8>>> for AddressesOrInstallationIds {
188    fn from(installation_ids: Vec<Vec<u8>>) -> Self {
189        AddressesOrInstallationIds::InstallationIds(installation_ids)
190    }
191}
192
193#[derive(Debug, Clone)]
194pub struct UpdateMetadataIntentData {
195    pub field_name: String,
196    pub field_value: String,
197    /// Compare-and-swap guard. When set, publishing abandons this intent
198    /// unless the field's currently committed value still equals this.
199    /// `None` keeps the historical last-writer-wins behavior.
200    pub expected_field_value: Option<String>,
201}
202
203impl UpdateMetadataIntentData {
204    pub fn new(field_name: String, field_value: String) -> Self {
205        Self {
206            field_name,
207            field_value,
208            expected_field_value: None,
209        }
210    }
211
212    /// A guarded update: publishing abandons the intent (marking it
213    /// [`xmtp_db::group_intent::IntentState::Superseded`]) unless the
214    /// committed value still equals `expected_field_value`.
215    pub fn new_guarded(
216        field_name: String,
217        field_value: String,
218        expected_field_value: String,
219    ) -> Self {
220        Self {
221            field_name,
222            field_value,
223            expected_field_value: Some(expected_field_value),
224        }
225    }
226
227    pub fn new_update_group_name(group_name: String) -> Self {
228        Self::new(MetadataField::GroupName.to_string(), group_name)
229    }
230
231    pub fn new_update_group_image_url_square(group_image_url_square: String) -> Self {
232        Self::new(
233            MetadataField::GroupImageUrlSquare.to_string(),
234            group_image_url_square,
235        )
236    }
237
238    pub fn new_update_group_description(group_description: String) -> Self {
239        Self::new(MetadataField::Description.to_string(), group_description)
240    }
241
242    pub fn new_update_app_data(app_data: String, expected_app_data: Option<String>) -> Self {
243        match expected_app_data {
244            Some(expected) => {
245                Self::new_guarded(MetadataField::AppData.to_string(), app_data, expected)
246            }
247            None => Self::new(MetadataField::AppData.to_string(), app_data),
248        }
249    }
250
251    pub fn new_update_conversation_message_disappear_from_ns(from_ns: i64) -> Self {
252        Self::new(
253            MetadataField::MessageDisappearFromNS.to_string(),
254            from_ns.to_string(),
255        )
256    }
257    pub fn new_update_conversation_message_disappear_in_ns(in_ns: i64) -> Self {
258        Self::new(
259            MetadataField::MessageDisappearInNS.to_string(),
260            in_ns.to_string(),
261        )
262    }
263
264    pub fn new_update_group_min_version_to_match_self(min_version: String) -> Self {
265        Self::new(
266            MetadataField::MinimumSupportedProtocolVersion.to_string(),
267            min_version,
268        )
269    }
270
271    pub fn new_update_commit_log_signer(commit_log_signer: xmtp_cryptography::Secret) -> Self {
272        Self::new(
273            MetadataField::CommitLogSigner.to_string(),
274            hex::encode(commit_log_signer.as_slice()),
275        )
276    }
277}
278
279impl From<UpdateMetadataIntentData> for Vec<u8> {
280    fn from(intent: UpdateMetadataIntentData) -> Self {
281        let mut buf = Vec::new();
282
283        UpdateMetadataData {
284            version: Some(UpdateMetadataVersion::V1(UpdateMetadataV1 {
285                field_name: intent.field_name.to_string(),
286                field_value: intent.field_value.clone(),
287                expected_field_value: intent.expected_field_value.clone(),
288            })),
289        }
290        .encode(&mut buf)
291        .expect("encode error");
292
293        buf
294    }
295}
296
297impl TryFrom<Vec<u8>> for UpdateMetadataIntentData {
298    type Error = IntentError;
299
300    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
301        let msg = UpdateMetadataData::decode(Bytes::from(data))?;
302
303        let field_name = match msg.version {
304            Some(UpdateMetadataVersion::V1(ref v1)) => v1.field_name.clone(),
305            None => return Err(IntentError::MissingPayload),
306        };
307        let field_value = match msg.version {
308            Some(UpdateMetadataVersion::V1(ref v1)) => v1.field_value.clone(),
309            None => return Err(IntentError::MissingPayload),
310        };
311        // Absent on intents queued before the guard existed, and on payloads
312        // written by a client that predates it — both mean last-writer-wins.
313        let expected_field_value = match msg.version {
314            Some(UpdateMetadataVersion::V1(ref v1)) => v1.expected_field_value.clone(),
315            None => return Err(IntentError::MissingPayload),
316        };
317
318        Ok(match expected_field_value {
319            Some(expected) => Self::new_guarded(field_name, field_value, expected),
320            None => Self::new(field_name, field_value),
321        })
322    }
323}
324
325#[derive(Debug, Default, Clone)]
326pub struct UpdateGroupMembershipResult {
327    pub added_members: HashMap<String, u64>,
328    pub removed_members: Vec<String>,
329    pub failed_installations: Vec<Vec<u8>>,
330}
331
332impl UpdateGroupMembershipResult {
333    pub fn new(
334        added_members: HashMap<String, u64>,
335        removed_members: Vec<String>,
336        failed_installations: Vec<Vec<u8>>,
337    ) -> Self {
338        Self {
339            added_members,
340            removed_members,
341            failed_installations,
342        }
343    }
344}
345
346impl From<UpdateGroupMembershipIntentData> for UpdateGroupMembershipResult {
347    fn from(value: UpdateGroupMembershipIntentData) -> Self {
348        UpdateGroupMembershipResult::new(
349            value.membership_updates,
350            value.removed_members,
351            value.failed_installations,
352        )
353    }
354}
355
356#[derive(Debug, Clone)]
357pub(crate) struct UpdateGroupMembershipIntentData {
358    pub membership_updates: HashMap<String, u64>,
359    pub removed_members: Vec<String>,
360    pub failed_installations: Vec<Vec<u8>>,
361}
362
363impl UpdateGroupMembershipIntentData {
364    pub fn new(
365        membership_updates: HashMap<String, u64>,
366        removed_members: Vec<String>,
367        failed_installations: Vec<Vec<u8>>,
368    ) -> Self {
369        Self {
370            membership_updates,
371            removed_members,
372            failed_installations,
373        }
374    }
375
376    pub fn is_empty(&self) -> bool {
377        self.membership_updates.is_empty()
378            && self.removed_members.is_empty()
379            && self.failed_installations.is_empty()
380    }
381
382    pub fn apply_to_group_membership(&self, group_membership: &GroupMembership) -> GroupMembership {
383        tracing::info!("old group membership: {:?}", group_membership.members);
384        let mut new_membership = group_membership.clone();
385        for (inbox_id, sequence_id) in self.membership_updates.iter() {
386            new_membership.add(inbox_id.clone(), *sequence_id);
387        }
388
389        for inbox_id in self.removed_members.iter() {
390            new_membership.remove(inbox_id)
391        }
392
393        new_membership.failed_installations = new_membership
394            .failed_installations
395            .into_iter()
396            .chain(self.failed_installations.iter().cloned())
397            .collect::<HashSet<_>>()
398            .into_iter()
399            .collect();
400
401        tracing::info!("updated group membership: {:?}", new_membership.members);
402        new_membership
403    }
404}
405
406impl From<UpdateGroupMembershipIntentData> for Vec<u8> {
407    fn from(intent: UpdateGroupMembershipIntentData) -> Self {
408        let mut buf = Vec::new();
409
410        UpdateGroupMembershipData {
411            version: Some(UpdateGroupMembershipVersion::V1(UpdateGroupMembershipV1 {
412                membership_updates: intent.membership_updates,
413                removed_members: intent.removed_members,
414                failed_installations: intent.failed_installations,
415            })),
416        }
417        .encode(&mut buf)
418        .expect("encode error");
419
420        buf
421    }
422}
423
424impl TryFrom<Vec<u8>> for UpdateGroupMembershipIntentData {
425    type Error = IntentError;
426
427    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
428        if let UpdateGroupMembershipData {
429            version: Some(UpdateGroupMembershipVersion::V1(v1)),
430        } = UpdateGroupMembershipData::decode(data.as_slice())?
431        {
432            Ok(Self::new(
433                v1.membership_updates,
434                v1.removed_members,
435                v1.failed_installations,
436            ))
437        } else {
438            Err(IntentError::MissingPayload)
439        }
440    }
441}
442
443impl<'a> TryFrom<&'a [u8]> for UpdateGroupMembershipIntentData {
444    type Error = IntentError;
445
446    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
447        if let UpdateGroupMembershipData {
448            version: Some(UpdateGroupMembershipVersion::V1(v1)),
449        } = UpdateGroupMembershipData::decode(data)?
450        {
451            Ok(Self::new(
452                v1.membership_updates,
453                v1.removed_members,
454                v1.failed_installations,
455            ))
456        } else {
457            Err(IntentError::MissingPayload)
458        }
459    }
460}
461#[repr(i32)]
462#[derive(Debug, Clone, PartialEq)]
463pub enum AdminListActionType {
464    Add = 1,         // Matches ADD_ADMIN in Protobuf
465    Remove = 2,      // Matches REMOVE_ADMIN in Protobuf
466    AddSuper = 3,    // Matches ADD_SUPER_ADMIN in Protobuf
467    RemoveSuper = 4, // Matches REMOVE_SUPER_ADMIN in Protobuf
468}
469
470impl TryFrom<i32> for AdminListActionType {
471    type Error = IntentError;
472
473    fn try_from(value: i32) -> Result<Self, Self::Error> {
474        match value {
475            1 => Ok(AdminListActionType::Add),
476            2 => Ok(AdminListActionType::Remove),
477            3 => Ok(AdminListActionType::AddSuper),
478            4 => Ok(AdminListActionType::RemoveSuper),
479            _ => Err(IntentError::UnknownAdminListAction),
480        }
481    }
482}
483
484#[derive(Debug, Clone)]
485pub struct UpdateAdminListIntentData {
486    pub action_type: AdminListActionType,
487    pub inbox_id: String,
488}
489
490impl UpdateAdminListIntentData {
491    pub fn new(action_type: AdminListActionType, inbox_id: String) -> Self {
492        Self {
493            action_type,
494            inbox_id,
495        }
496    }
497}
498
499impl From<UpdateAdminListIntentData> for Vec<u8> {
500    fn from(intent: UpdateAdminListIntentData) -> Self {
501        let mut buf = Vec::new();
502        let action_type = intent.action_type as i32;
503
504        UpdateAdminListsData {
505            version: Some(UpdateAdminListsVersion::V1(UpdateAdminListsV1 {
506                admin_list_update_type: action_type,
507                inbox_id: intent.inbox_id,
508            })),
509        }
510        .encode(&mut buf)
511        .expect("encode error");
512
513        buf
514    }
515}
516
517impl TryFrom<Vec<u8>> for UpdateAdminListIntentData {
518    type Error = IntentError;
519
520    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
521        let msg = UpdateAdminListsData::decode(Bytes::from(data))?;
522
523        let action_type: AdminListActionType = match msg.version {
524            Some(UpdateAdminListsVersion::V1(ref v1)) => {
525                AdminListActionType::try_from(v1.admin_list_update_type)?
526            }
527            None => return Err(IntentError::MissingUpdateAdminVersion),
528        };
529        let inbox_id = match msg.version {
530            Some(UpdateAdminListsVersion::V1(ref v1)) => v1.inbox_id.clone(),
531            None => return Err(IntentError::MissingUpdateAdminVersion),
532        };
533
534        Ok(Self::new(action_type, inbox_id))
535    }
536}
537
538#[repr(i32)]
539#[derive(Debug, Clone, PartialEq)]
540pub enum PermissionUpdateType {
541    AddMember = 1,      // Matches ADD_MEMBER in Protobuf
542    RemoveMember = 2,   // Matches REMOVE_MEMBER in Protobuf
543    AddAdmin = 3,       // Matches ADD_ADMIN in Protobuf
544    RemoveAdmin = 4,    // Matches REMOVE_ADMIN in Protobuf
545    UpdateMetadata = 5, // Matches UPDATE_METADATA in Protobuf
546}
547
548impl TryFrom<i32> for PermissionUpdateType {
549    type Error = IntentError;
550
551    fn try_from(value: i32) -> Result<Self, Self::Error> {
552        match value {
553            1 => Ok(PermissionUpdateType::AddMember),
554            2 => Ok(PermissionUpdateType::RemoveMember),
555            3 => Ok(PermissionUpdateType::AddAdmin),
556            4 => Ok(PermissionUpdateType::RemoveAdmin),
557            5 => Ok(PermissionUpdateType::UpdateMetadata),
558            _ => Err(IntentError::UnknownPermissionUpdateType),
559        }
560    }
561}
562
563#[repr(i32)]
564#[derive(Debug, Clone, PartialEq)]
565pub enum PermissionPolicyOption {
566    Allow = 1,          // Matches ADD_MEMBER in Protobuf
567    Deny = 2,           // Matches REMOVE_MEMBER in Protobuf
568    AdminOnly = 3,      // Matches ADD_ADMIN in Protobuf
569    SuperAdminOnly = 4, // Matches REMOVE_ADMIN in Protobuf
570}
571
572impl TryFrom<i32> for PermissionPolicyOption {
573    type Error = IntentError;
574
575    fn try_from(value: i32) -> Result<Self, Self::Error> {
576        match value {
577            1 => Ok(PermissionPolicyOption::Allow),
578            2 => Ok(PermissionPolicyOption::Deny),
579            3 => Ok(PermissionPolicyOption::AdminOnly),
580            4 => Ok(PermissionPolicyOption::SuperAdminOnly),
581            _ => Err(IntentError::UnknownPermissionPolicyOption),
582        }
583    }
584}
585
586impl From<PermissionPolicyOption> for MembershipPolicies {
587    fn from(value: PermissionPolicyOption) -> Self {
588        match value {
589            PermissionPolicyOption::Allow => MembershipPolicies::allow(),
590            PermissionPolicyOption::Deny => MembershipPolicies::deny(),
591            PermissionPolicyOption::AdminOnly => MembershipPolicies::allow_if_actor_admin(),
592            PermissionPolicyOption::SuperAdminOnly => {
593                MembershipPolicies::allow_if_actor_super_admin()
594            }
595        }
596    }
597}
598
599impl From<PermissionPolicyOption> for MetadataPolicies {
600    fn from(value: PermissionPolicyOption) -> Self {
601        match value {
602            PermissionPolicyOption::Allow => MetadataPolicies::allow(),
603            PermissionPolicyOption::Deny => MetadataPolicies::deny(),
604            PermissionPolicyOption::AdminOnly => MetadataPolicies::allow_if_actor_admin(),
605            PermissionPolicyOption::SuperAdminOnly => {
606                MetadataPolicies::allow_if_actor_super_admin()
607            }
608        }
609    }
610}
611
612impl From<PermissionPolicyOption> for PermissionsPolicies {
613    fn from(value: PermissionPolicyOption) -> Self {
614        match value {
615            PermissionPolicyOption::Allow => {
616                tracing::error!(
617                    "PermissionPolicyOption::Allow is not allowed for PermissionsPolicies, set to super_admin only instead"
618                );
619                PermissionsPolicies::allow_if_actor_super_admin()
620            }
621            PermissionPolicyOption::Deny => PermissionsPolicies::deny(),
622            PermissionPolicyOption::AdminOnly => PermissionsPolicies::allow_if_actor_admin(),
623            PermissionPolicyOption::SuperAdminOnly => {
624                PermissionsPolicies::allow_if_actor_super_admin()
625            }
626        }
627    }
628}
629
630#[derive(Debug, Clone)]
631pub struct UpdatePermissionIntentData {
632    pub update_type: PermissionUpdateType,
633    pub policy_option: PermissionPolicyOption,
634    pub metadata_field_name: Option<String>,
635}
636
637impl UpdatePermissionIntentData {
638    pub fn new(
639        update_type: PermissionUpdateType,
640        policy_option: PermissionPolicyOption,
641        metadata_field_name: Option<String>,
642    ) -> Self {
643        Self {
644            update_type,
645            policy_option,
646            metadata_field_name,
647        }
648    }
649}
650
651impl From<UpdatePermissionIntentData> for Vec<u8> {
652    fn from(intent: UpdatePermissionIntentData) -> Self {
653        let mut buf = Vec::new();
654        let update_type = intent.update_type as i32;
655        let policy_option = intent.policy_option as i32;
656
657        UpdatePermissionData {
658            version: Some(UpdatePermissionVersion::V1(UpdatePermissionV1 {
659                permission_update_type: update_type,
660                permission_policy_option: policy_option,
661                metadata_field_name: intent.metadata_field_name,
662            })),
663        }
664        .encode(&mut buf)
665        .expect("encode error");
666
667        buf
668    }
669}
670
671impl TryFrom<Vec<u8>> for UpdatePermissionIntentData {
672    type Error = IntentError;
673
674    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
675        let msg = UpdatePermissionData::decode(Bytes::from(data))?;
676        let Some(UpdatePermissionVersion::V1(update_permission_data::V1 {
677            permission_update_type,
678            permission_policy_option,
679            metadata_field_name,
680        })) = msg.version
681        else {
682            return Err(IntentError::UnsupportedPermissionVersion);
683        };
684        let update_type: PermissionUpdateType = permission_update_type.try_into()?;
685        let policy_option: PermissionPolicyOption = permission_policy_option.try_into()?;
686        Ok(Self::new(update_type, policy_option, metadata_field_name))
687    }
688}
689
690pub(crate) struct ReaddInstallationsIntentData {
691    pub readded_installations: Vec<Vec<u8>>,
692}
693
694impl ReaddInstallationsIntentData {
695    pub fn new(readded_installations: Vec<Vec<u8>>) -> Self {
696        Self {
697            readded_installations,
698        }
699    }
700}
701
702impl From<ReaddInstallationsIntentData> for Vec<u8> {
703    fn from(intent: ReaddInstallationsIntentData) -> Self {
704        let mut buf = Vec::new();
705        ReaddInstallationsData {
706            version: Some(ReaddInstallationsVersion::V1(ReaddInstallationsV1 {
707                readded_installations: intent.readded_installations,
708            })),
709        }
710        .encode(&mut buf)
711        .expect("encode error");
712
713        buf
714    }
715}
716
717impl TryFrom<Vec<u8>> for ReaddInstallationsIntentData {
718    type Error = IntentError;
719
720    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
721        if let ReaddInstallationsData {
722            version: Some(ReaddInstallationsVersion::V1(v1)),
723        } = ReaddInstallationsData::decode(data.as_slice())?
724        {
725            Ok(Self::new(v1.readded_installations))
726        } else {
727            Err(IntentError::MissingPayload)
728        }
729    }
730}
731
732impl TryFrom<&[u8]> for ReaddInstallationsIntentData {
733    type Error = IntentError;
734
735    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
736        if let ReaddInstallationsData {
737            version: Some(ReaddInstallationsVersion::V1(v1)),
738        } = ReaddInstallationsData::decode(data)?
739        {
740            Ok(Self::new(v1.readded_installations))
741        } else {
742            Err(IntentError::MissingPayload)
743        }
744    }
745}
746
747/// Intent data for proposing member updates (adds and/or removes) to a group
748#[derive(Debug, Clone)]
749pub(crate) struct ProposeMemberUpdateIntentData {
750    pub add_inbox_ids: Vec<String>,
751    pub remove_inbox_ids: Vec<String>,
752}
753
754impl ProposeMemberUpdateIntentData {
755    pub fn new(add_inbox_ids: Vec<String>, remove_inbox_ids: Vec<String>) -> Self {
756        Self {
757            add_inbox_ids,
758            remove_inbox_ids,
759        }
760    }
761}
762
763impl TryFrom<ProposeMemberUpdateIntentData> for Vec<u8> {
764    type Error = IntentError;
765    fn try_from(intent: ProposeMemberUpdateIntentData) -> Result<Self, Self::Error> {
766        let decode_inbox_ids =
767            |inbox_ids: Vec<String>, item: &'static str| -> Result<Vec<Vec<u8>>, IntentError> {
768                inbox_ids
769                    .into_iter()
770                    .map(|s| {
771                        hex::decode(&s)
772                            .map_err(|_| xmtp_proto::ConversionError::InvalidValue {
773                                item,
774                                expected: "hex encoded string",
775                                got: s,
776                            })
777                            .map_err(Into::into)
778                    })
779                    .collect::<Result<Vec<_>, _>>()
780            };
781        let proposal = ProposeMemberUpdateData {
782            version: Some(propose_member_update_data::Version::V1(
783                propose_member_update_data::V1 {
784                    add_inbox_ids: decode_inbox_ids(intent.add_inbox_ids, "add_inbox_ids")?,
785                    remove_inbox_ids: decode_inbox_ids(
786                        intent.remove_inbox_ids,
787                        "remove_inbox_ids",
788                    )?,
789                },
790            )),
791        }
792        .encode_to_vec();
793        Ok(proposal)
794    }
795}
796
797impl TryFrom<&[u8]> for ProposeMemberUpdateIntentData {
798    type Error = IntentError;
799
800    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
801        let proto = ProposeMemberUpdateData::decode(data)?;
802        let decode_inbox_ids = |inbox_ids: Vec<Vec<u8>>| -> Vec<String> {
803            inbox_ids
804                .into_iter()
805                .map(|b| hex::encode(&b))
806                .collect::<Vec<_>>()
807        };
808        match proto.version {
809            Some(propose_member_update_data::Version::V1(v1)) => {
810                let add_inbox_ids = decode_inbox_ids(v1.add_inbox_ids);
811                let remove_inbox_ids = decode_inbox_ids(v1.remove_inbox_ids);
812                Ok(Self::new(add_inbox_ids, remove_inbox_ids))
813            }
814            None => Err(IntentError::MissingPayload),
815        }
816    }
817}
818
819/// Intent data for proposing group context extension updates (proposal-by-reference flow)
820#[derive(Debug, Clone)]
821pub(crate) struct ProposeGroupContextExtensionsIntentData {
822    /// The serialized extensions bytes
823    pub extensions_bytes: Vec<u8>,
824}
825
826impl ProposeGroupContextExtensionsIntentData {
827    pub fn new(extensions_bytes: Vec<u8>) -> Self {
828        Self { extensions_bytes }
829    }
830}
831
832impl From<ProposeGroupContextExtensionsIntentData> for Vec<u8> {
833    fn from(intent: ProposeGroupContextExtensionsIntentData) -> Self {
834        ProposeGroupContextExtensionData {
835            version: Some(propose_group_context_extension_data::Version::V1(
836                propose_group_context_extension_data::V1 {
837                    group_context_extension: intent.extensions_bytes,
838                },
839            )),
840        }
841        .encode_to_vec()
842    }
843}
844
845impl TryFrom<&[u8]> for ProposeGroupContextExtensionsIntentData {
846    type Error = IntentError;
847
848    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
849        let proto = ProposeGroupContextExtensionData::decode(data)?;
850        match proto.version {
851            Some(propose_group_context_extension_data::Version::V1(v1)) => {
852                Ok(Self::new(v1.group_context_extension))
853            }
854            None => Err(IntentError::MissingPayload),
855        }
856    }
857}
858
859/// Intent data for committing pending proposals (proposal-by-reference flow)
860#[derive(Debug, Clone, Default)]
861pub(crate) struct CommitPendingProposalsIntentData {
862    // Empty for now - commits all pending proposals in the proposal store
863}
864
865impl CommitPendingProposalsIntentData {
866    pub fn new() -> Self {
867        Self {}
868    }
869}
870
871impl From<CommitPendingProposalsIntentData> for Vec<u8> {
872    fn from(_intent: CommitPendingProposalsIntentData) -> Self {
873        CommitPendingProposalsData {
874            version: Some(commit_pending_proposals_data::Version::V1(
875                commit_pending_proposals_data::V1 {
876                    proposal_hashes: vec![],
877                },
878            )),
879        }
880        .encode_to_vec()
881    }
882}
883
884impl TryFrom<&[u8]> for CommitPendingProposalsIntentData {
885    type Error = IntentError;
886
887    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
888        // Handle empty data for backwards compatibility and default case
889        if data.is_empty() {
890            return Ok(Self::new());
891        }
892        let proto = CommitPendingProposalsData::decode(data)?;
893        match proto.version {
894            Some(commit_pending_proposals_data::Version::V1(_)) => Ok(Self::new()),
895            None => Err(IntentError::MissingPayload),
896        }
897    }
898}
899
900#[derive(Debug, Clone)]
901pub enum PostCommitAction {
902    SendWelcomes(SendWelcomesAction),
903}
904
905#[derive(Debug)]
906pub struct MembershipDiffWithKeyPackages {
907    pub new_installations: Vec<Installation>,
908    pub new_key_packages: Vec<KeyPackage>,
909    pub removed_installations: HashSet<Vec<u8>>,
910    pub failed_installations: Vec<Vec<u8>>,
911}
912
913impl MembershipDiffWithKeyPackages {
914    pub fn new(
915        new_installations: Vec<Installation>,
916        new_key_packages: Vec<KeyPackage>,
917        removed_installations: HashSet<Vec<u8>>,
918        failed_installations: Vec<Vec<u8>>,
919    ) -> MembershipDiffWithKeyPackages {
920        MembershipDiffWithKeyPackages {
921            new_installations,
922            new_key_packages,
923            removed_installations,
924            failed_installations,
925        }
926    }
927}
928
929#[derive(Debug, Clone)]
930pub struct Installation {
931    pub(crate) installation_key: Vec<u8>,
932    pub(crate) hpke_public_key: Vec<u8>,
933    pub(crate) welcome_wrapper_algorithm: WrapperAlgorithm,
934    pub(crate) welcome_pointee_encryption_aead_types: WelcomePointersExtension,
935}
936
937impl Installation {
938    pub fn from_verified_key_package(
939        key_package: &VerifiedKeyPackageV2,
940    ) -> Result<Self, IntentError> {
941        let wrapper_encryption = WrapperEncryptionExtension::advertised_by(&key_package.inner)?;
942
943        let welcome_pointee_encryption_aead_types = key_package
944            .inner
945            .extensions()
946            .unknown(WELCOME_POINTEE_ENCRYPTION_AEAD_TYPES_EXTENSION_ID)
947            .map(|ext| ext.0.as_slice().try_into())
948            .transpose()?;
949
950        Ok(Self {
951            installation_key: key_package.installation_id(),
952            hpke_public_key: wrapper_encryption.pub_key_bytes,
953            welcome_wrapper_algorithm: wrapper_encryption.algorithm,
954            welcome_pointee_encryption_aead_types: welcome_pointee_encryption_aead_types
955                .unwrap_or_else(WelcomePointersExtension::empty),
956        })
957    }
958}
959
960impl From<Installation> for InstallationProto {
961    fn from(installation: Installation) -> Self {
962        Self {
963            installation_key: installation.installation_key,
964            hpke_public_key: installation.hpke_public_key,
965            welcome_wrapper_algorithm: installation.welcome_wrapper_algorithm.into(),
966            welcome_pointee_encryption_aead_types: Some(
967                installation.welcome_pointee_encryption_aead_types.into(),
968            ),
969        }
970    }
971}
972
973impl TryFrom<InstallationProto> for Installation {
974    type Error = ConversionError;
975    fn try_from(installation: InstallationProto) -> Result<Self, Self::Error> {
976        Ok(Self {
977            installation_key: installation.installation_key,
978            hpke_public_key: installation.hpke_public_key,
979            welcome_wrapper_algorithm: installation.welcome_wrapper_algorithm.try_into()?,
980            welcome_pointee_encryption_aead_types: installation
981                .welcome_pointee_encryption_aead_types
982                .map(Into::into)
983                .unwrap_or_else(WelcomePointersExtension::empty),
984        })
985    }
986}
987
988#[derive(Debug, Clone)]
989pub struct SendWelcomesAction {
990    pub installations: Vec<Installation>,
991    pub welcome_message: Vec<u8>,
992}
993
994impl SendWelcomesAction {
995    pub fn new(installations: Vec<Installation>, welcome_message: Vec<u8>) -> Self {
996        Self {
997            installations,
998            welcome_message,
999        }
1000    }
1001
1002    pub(crate) fn to_bytes(&self) -> Vec<u8> {
1003        PostCommitActionProto {
1004            kind: Some(PostCommitActionKind::SendWelcomes(SendWelcomesProto {
1005                installations: self
1006                    .installations
1007                    .clone()
1008                    .into_iter()
1009                    .map(|i| i.into())
1010                    .collect(),
1011                welcome_message: self.welcome_message.clone(),
1012            })),
1013        }
1014        .encode_to_vec()
1015    }
1016}
1017
1018impl PostCommitAction {
1019    pub(crate) fn to_bytes(&self) -> Vec<u8> {
1020        match self {
1021            PostCommitAction::SendWelcomes(action) => action.to_bytes(),
1022        }
1023    }
1024
1025    pub(crate) fn from_bytes(data: &[u8]) -> Result<Self, IntentError> {
1026        let decoded = PostCommitActionProto::decode(data)?
1027            .kind
1028            .ok_or(IntentError::MissingPostCommit)?;
1029        match decoded {
1030            PostCommitActionKind::SendWelcomes(proto) => {
1031                Ok(Self::SendWelcomes(SendWelcomesAction::new(
1032                    proto
1033                        .installations
1034                        .into_iter()
1035                        .map(|i| i.try_into())
1036                        .collect::<Result<_, _>>()?,
1037                    proto.welcome_message,
1038                )))
1039            }
1040        }
1041    }
1042
1043    pub(crate) fn from_welcome(
1044        welcome: MlsMessageOut,
1045        installations: Vec<Installation>,
1046    ) -> Result<Self, IntentError> {
1047        let welcome_bytes = welcome.tls_serialize_detached()?;
1048
1049        Ok(Self::SendWelcomes(SendWelcomesAction::new(
1050            installations,
1051            welcome_bytes,
1052        )))
1053    }
1054}
1055
1056impl TryFrom<Vec<u8>> for PostCommitAction {
1057    type Error = IntentError;
1058
1059    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
1060        PostCommitAction::from_bytes(data.as_slice())
1061    }
1062}
1063
1064/// Payload of [`crate::groups::intents::queue::QueueIntent::app_data_update`].
1065///
1066/// Serialized as the local-only `xmtp.mls.database.AppDataUpdateData`
1067/// proto (`proto/mls/database/intents.proto`); it never crosses the MLS
1068/// wire. Component IDs are `u32` in the proto and narrowed back here;
1069/// a value above `u16::MAX` or an unknown version variant fails closed.
1070#[derive(Debug, Clone, PartialEq, Eq)]
1071pub enum AppDataUpdateIntentData {
1072    /// One verbatim `AppDataUpdate` payload (V1). Its meaning follows the
1073    /// target component's registered `ComponentType`: a scalar's new
1074    /// value, or a TLS-encoded map or set delta.
1075    Payload { component_id: u16, payload: Vec<u8> },
1076    /// Typed metadata field writes, committed together (V2). The
1077    /// publisher resolves them with
1078    /// [`xmtp_mls_common::app_data::fields::FieldSnapshot::resolve_writes`]
1079    /// against the committed registry.
1080    Fields(Vec<FieldWrite>),
1081}
1082
1083impl AppDataUpdateIntentData {
1084    /// Write `payload` to `component_id`.
1085    pub fn new(component_id: u16, payload: Vec<u8>) -> Self {
1086        Self::Payload {
1087            component_id,
1088            payload,
1089        }
1090    }
1091}
1092
1093impl From<AppDataUpdateIntentData> for Vec<u8> {
1094    fn from(intent: AppDataUpdateIntentData) -> Self {
1095        use xmtp_proto::xmtp::mls::database::{
1096            AppDataUpdateData,
1097            app_data_update_data::{
1098                FieldWrite as FieldWriteProto, V1, V2, Version, field_write::Operation,
1099            },
1100        };
1101        let version = match intent {
1102            AppDataUpdateIntentData::Payload {
1103                component_id,
1104                payload,
1105            } => Version::V1(V1 {
1106                component_id: component_id.into(),
1107                payload,
1108            }),
1109            AppDataUpdateIntentData::Fields(writes) => Version::V2(V2 {
1110                writes: writes
1111                    .into_iter()
1112                    .map(|write| FieldWriteProto {
1113                        component_id: write.component_id.as_u16().into(),
1114                        component_type: write.component_type.into(),
1115                        operation: Some(match write.operation {
1116                            WriteOperation::Update(payload) => Operation::Update(payload),
1117                            WriteOperation::Remove => Operation::Remove(true),
1118                            WriteOperation::SetOwn(value) => Operation::SetOwn(value),
1119                            WriteOperation::ClearOwn => Operation::ClearOwn(true),
1120                        }),
1121                    })
1122                    .collect(),
1123            }),
1124        };
1125        AppDataUpdateData {
1126            version: Some(version),
1127        }
1128        .encode_to_vec()
1129    }
1130}
1131
1132impl TryFrom<Vec<u8>> for AppDataUpdateIntentData {
1133    type Error = IntentError;
1134
1135    fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
1136        Self::try_from(data.as_slice())
1137    }
1138}
1139
1140impl TryFrom<&[u8]> for AppDataUpdateIntentData {
1141    type Error = IntentError;
1142
1143    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
1144        use xmtp_proto::xmtp::mls::database::{
1145            AppDataUpdateData,
1146            app_data_update_data::{Version, field_write::Operation},
1147        };
1148        let invalid =
1149            |reason: String| IntentError::Generic(format!("AppDataUpdateIntentData {reason}"));
1150        let component_id = |id: u32| {
1151            u16::try_from(id).map_err(|_| invalid(format!("component_id {id} exceeds u16 range")))
1152        };
1153        match AppDataUpdateData::decode(data)?.version {
1154            Some(Version::V1(v1)) => Ok(Self::Payload {
1155                component_id: component_id(v1.component_id)?,
1156                payload: v1.payload,
1157            }),
1158            Some(Version::V2(v2)) => v2
1159                .writes
1160                .into_iter()
1161                .map(|write| {
1162                    Ok(FieldWrite {
1163                        component_id: ComponentId::new(component_id(write.component_id)?),
1164                        component_type: ComponentType::try_from(write.component_type).map_err(
1165                            |_| {
1166                                invalid(format!(
1167                                    "component_type {} is unknown",
1168                                    write.component_type
1169                                ))
1170                            },
1171                        )?,
1172                        operation: match write.operation {
1173                            Some(Operation::Update(payload)) => WriteOperation::Update(payload),
1174                            Some(Operation::Remove(_)) => WriteOperation::Remove,
1175                            Some(Operation::SetOwn(value)) => WriteOperation::SetOwn(value),
1176                            Some(Operation::ClearOwn(_)) => WriteOperation::ClearOwn,
1177                            None => return Err(invalid("field write has no operation".into())),
1178                        },
1179                    })
1180                })
1181                .collect::<Result<_, _>>()
1182                .map(Self::Fields),
1183            None => Err(invalid("missing version oneof variant".into())),
1184        }
1185    }
1186}
1187
1188#[cfg(test)]
1189mod app_data_update_intent_tests {
1190    use super::*;
1191
1192    #[test]
1193    fn round_trip_basic() {
1194        let intent = AppDataUpdateIntentData::new(0x800C, vec![0xAA, 0xBB, 0xCC]);
1195        let bytes: Vec<u8> = intent.clone().into();
1196        let restored = AppDataUpdateIntentData::try_from(bytes).unwrap();
1197        assert_eq!(restored, intent);
1198    }
1199
1200    #[test]
1201    fn empty_payload_round_trips() {
1202        // Empty payload bytes (a "no-op" intent shape) must round-trip
1203        // cleanly — useful for components that legitimately write
1204        // empty values (e.g., revoke clears EXTERNAL_COMMIT_POLICY's
1205        // symmetric_key to zero-length).
1206        let intent = AppDataUpdateIntentData::new(0x800C, vec![]);
1207        let bytes: Vec<u8> = intent.clone().into();
1208        let restored = AppDataUpdateIntentData::try_from(bytes).unwrap();
1209        assert_eq!(restored, intent);
1210    }
1211
1212    #[test]
1213    fn missing_version_variant_surfaces_error() {
1214        // Proto with no `version` oneof set decodes to `version: None`
1215        // on unknown-variant tolerance. Reject explicitly — an intent
1216        // payload without a recognised version variant is malformed.
1217        use prost::Message;
1218        use xmtp_proto::xmtp::mls::database::AppDataUpdateData;
1219        let bytes = AppDataUpdateData { version: None }.encode_to_vec();
1220        let err = AppDataUpdateIntentData::try_from(bytes).unwrap_err();
1221        match err {
1222            IntentError::Generic(msg) => assert!(msg.contains("missing version")),
1223            _ => panic!("expected Generic error, got {err:?}"),
1224        }
1225    }
1226
1227    #[test]
1228    fn component_id_overflow_surfaces_error() {
1229        // u16 component_id is widened to u32 on the wire. A payload that
1230        // somehow carries a value above u16::MAX (e.g., a malformed
1231        // intent or one written by a hypothetical future client widening
1232        // the id space) must be rejected — the dispatcher narrows to
1233        // u16 and we'd silently lose information without the explicit
1234        // check.
1235        use prost::Message;
1236        use xmtp_proto::xmtp::mls::database::{
1237            AppDataUpdateData, app_data_update_data::V1 as AppDataUpdateDataV1,
1238            app_data_update_data::Version as AppDataUpdateVersion,
1239        };
1240        let bytes = AppDataUpdateData {
1241            version: Some(AppDataUpdateVersion::V1(AppDataUpdateDataV1 {
1242                component_id: u16::MAX as u32 + 1,
1243                payload: vec![],
1244            })),
1245        }
1246        .encode_to_vec();
1247        let err = AppDataUpdateIntentData::try_from(bytes).unwrap_err();
1248        match err {
1249            IntentError::Generic(msg) => assert!(msg.contains("exceeds u16")),
1250            _ => panic!("expected Generic error, got {err:?}"),
1251        }
1252    }
1253
1254    /// A queued field write must reach the publisher exactly as encoded:
1255    /// its ID, the type it was encoded under (the publisher's stale-type
1256    /// check compares against it), and each operation kind.
1257    #[xmtp_common::test]
1258    fn field_writes_round_trip() {
1259        let write = |id, component_type, operation| FieldWrite {
1260            component_id: ComponentId::new(id),
1261            component_type,
1262            operation,
1263        };
1264        let intent = AppDataUpdateIntentData::Fields(vec![
1265            write(
1266                0x8001,
1267                ComponentType::String,
1268                WriteOperation::Update(b"a".to_vec()),
1269            ),
1270            write(0x8002, ComponentType::Bytes, WriteOperation::Remove),
1271            write(
1272                0x8003,
1273                ComponentType::TlsMapInboxIdString,
1274                WriteOperation::SetOwn(b"b".to_vec()),
1275            ),
1276            write(
1277                0x8004,
1278                ComponentType::TlsMapInboxIdBytes,
1279                WriteOperation::ClearOwn,
1280            ),
1281        ]);
1282        let bytes: Vec<u8> = intent.clone().into();
1283        assert_eq!(AppDataUpdateIntentData::try_from(bytes).unwrap(), intent);
1284    }
1285
1286    /// A field write whose encoding type or operation this build cannot
1287    /// read fails closed rather than publishing a guess.
1288    #[xmtp_common::test]
1289    fn unreadable_field_writes_fail_closed() {
1290        use xmtp_proto::xmtp::mls::database::{
1291            AppDataUpdateData,
1292            app_data_update_data::{
1293                FieldWrite as FieldWriteProto, V2, Version, field_write::Operation,
1294            },
1295        };
1296        let decode = |component_type, operation| {
1297            AppDataUpdateIntentData::try_from(
1298                AppDataUpdateData {
1299                    version: Some(Version::V2(V2 {
1300                        writes: vec![FieldWriteProto {
1301                            component_id: 0x8001,
1302                            component_type,
1303                            operation,
1304                        }],
1305                    })),
1306                }
1307                .encode_to_vec(),
1308            )
1309        };
1310        assert!(decode(ComponentType::Bytes.into(), Some(Operation::Remove(true))).is_ok());
1311        assert!(decode(99, Some(Operation::Remove(true))).is_err());
1312        assert!(decode(ComponentType::Bytes.into(), None).is_err());
1313    }
1314
1315    #[test]
1316    fn malformed_proto_bytes_surface_decode_error() {
1317        // Random bytes that aren't a valid proto must surface a
1318        // prost::DecodeError via the IntentError::Conversion path.
1319        let bytes = vec![0xFF; 16];
1320        assert!(AppDataUpdateIntentData::try_from(bytes).is_err());
1321    }
1322}
1323
1324#[cfg(test)]
1325pub(crate) mod tests {
1326    use crate::context::XmtpSharedContext;
1327    use crate::groups::send_message_opts::SendMessageOpts;
1328    use openmls::prelude::{ProcessedMessageContent, ProtocolMessage};
1329    use xmtp_cryptography::utils::generate_local_wallet;
1330    use xmtp_db::XmtpOpenMlsProviderRef;
1331
1332    use crate::{builder::ClientBuilder, utils::TestMlsGroup};
1333
1334    use super::*;
1335
1336    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1337    #[cfg_attr(not(target_arch = "wasm32"), test)]
1338    fn test_serialize_send_message() {
1339        let message = vec![1, 2, 3];
1340        let intent = SendMessageIntentData::new(message.clone());
1341        let as_bytes: Vec<u8> = intent.into();
1342        let restored_intent = SendMessageIntentData::from_bytes(as_bytes.as_slice()).unwrap();
1343
1344        assert_eq!(restored_intent.message, message);
1345    }
1346
1347    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1348    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
1349    async fn test_serialize_update_membership() {
1350        let mut membership_updates = HashMap::new();
1351        membership_updates.insert("foo".to_string(), 123);
1352
1353        let intent = UpdateGroupMembershipIntentData::new(
1354            membership_updates,
1355            vec!["bar".to_string()],
1356            vec![vec![1, 2, 3]],
1357        );
1358
1359        let as_bytes: Vec<u8> = intent.clone().into();
1360        let restored_intent: UpdateGroupMembershipIntentData = as_bytes.try_into().unwrap();
1361
1362        assert_eq!(
1363            intent.membership_updates,
1364            restored_intent.membership_updates
1365        );
1366
1367        assert_eq!(intent.removed_members, restored_intent.removed_members);
1368
1369        assert_eq!(
1370            intent.failed_installations,
1371            restored_intent.failed_installations
1372        );
1373    }
1374
1375    #[xmtp_common::test]
1376    async fn test_serialize_update_metadata() {
1377        let intent = UpdateMetadataIntentData::new_update_group_name("group name".to_string());
1378        let as_bytes: Vec<u8> = intent.clone().into();
1379        let restored_intent: UpdateMetadataIntentData =
1380            UpdateMetadataIntentData::try_from(as_bytes).unwrap();
1381
1382        assert_eq!(intent.field_value, restored_intent.field_value);
1383    }
1384
1385    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1386    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
1387    async fn test_serialize_readd_installations() {
1388        let readded_installations = vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];
1389
1390        let intent = ReaddInstallationsIntentData::new(readded_installations.clone());
1391
1392        let as_bytes: Vec<u8> = intent.into();
1393        let restored_intent: ReaddInstallationsIntentData = as_bytes.try_into().unwrap();
1394
1395        assert_eq!(readded_installations, restored_intent.readded_installations);
1396    }
1397
1398    #[xmtp_common::test]
1399    async fn test_key_rotation_before_first_message() {
1400        let client_a = ClientBuilder::new_test_client_vanilla(&generate_local_wallet()).await;
1401        let client_b = ClientBuilder::new_test_client_vanilla(&generate_local_wallet()).await;
1402
1403        // client A makes a group with client B, and then sends a message to client B.
1404        let group_a = client_a.create_group(None, None).expect("create group");
1405        group_a.add_members(&[client_b.inbox_id()]).await.unwrap();
1406        group_a
1407            .send_message(b"First message from A", SendMessageOpts::default())
1408            .await
1409            .unwrap();
1410
1411        // No key rotation is needed because A's commit to add B rotates the key.
1412        // The Add proposal, membership AppDataUpdate proposal, commit, and message are published.
1413        verify_num_payloads_in_group(&group_a, 4).await;
1414
1415        // Client B sends a message to Client A
1416        let groups_b = client_b.sync_welcomes().await.unwrap();
1417        assert_eq!(groups_b.len(), 1);
1418        let group_b = groups_b[0].clone();
1419        group_b
1420            .send_message(b"First message from B", SendMessageOpts::default())
1421            .await
1422            .expect("send message");
1423
1424        // B must perform a key rotation before sending their first message.
1425        // The initial four envelopes are followed by B's key rotation and message.
1426        let payloads_a = verify_num_payloads_in_group(&group_a, 6).await;
1427        let payloads_b = verify_num_payloads_in_group(&group_b, 6).await;
1428
1429        // Verify key rotation payload
1430        for i in 0..payloads_a.len() {
1431            assert_eq!(payloads_a[i].payload_hash, payloads_b[i].payload_hash);
1432        }
1433        verify_commit_updates_leaf_node(&group_a, &payloads_a[4]);
1434
1435        // Client B sends another message to Client A, and Client A sends another message to Client B.
1436        group_b
1437            .send_message(b"Second message from B", SendMessageOpts::default())
1438            .await
1439            .expect("send message");
1440        group_a
1441            .send_message(b"Second message from A", SendMessageOpts::default())
1442            .await
1443            .expect("send message");
1444
1445        // Group should only have two additional messages. No more key rotations are needed.
1446        verify_num_payloads_in_group(&group_a, 8).await;
1447        verify_num_payloads_in_group(&group_b, 8).await;
1448    }
1449
1450    async fn verify_num_payloads_in_group(
1451        group: &TestMlsGroup,
1452        num_messages: usize,
1453    ) -> Vec<xmtp_proto::types::GroupMessage> {
1454        let messages = group
1455            .context
1456            .api()
1457            .query_group_messages(group.group_id)
1458            .await
1459            .unwrap();
1460        assert_eq!(messages.len(), num_messages);
1461        messages
1462    }
1463
1464    fn verify_commit_updates_leaf_node(
1465        group: &TestMlsGroup,
1466        message: &xmtp_proto::types::GroupMessage,
1467    ) {
1468        let mls_message = message.message.clone();
1469        let mls_message = match mls_message {
1470            ProtocolMessage::PrivateMessage(mls_message) => mls_message,
1471            _ => panic!("error mls_message"),
1472        };
1473
1474        let storage = group.context.mls_storage();
1475        let decrypted_message = group
1476            .load_mls_group_with_lock(storage, |mut mls_group| {
1477                Ok(mls_group
1478                    .process_message(&XmtpOpenMlsProviderRef::new(storage), mls_message.clone())
1479                    .unwrap())
1480            })
1481            .unwrap();
1482
1483        let staged_commit = match decrypted_message.into_content() {
1484            ProcessedMessageContent::StagedCommitMessage(staged_commit) => *staged_commit,
1485            _ => panic!("error staged_commit"),
1486        };
1487
1488        // Check there is indeed some updated leaf node, which means the key update works.
1489        let path_update_leaf_node = staged_commit.update_path_leaf_node();
1490        assert!(path_update_leaf_node.is_some());
1491    }
1492}