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