xmtp_id/associations/
unverified.rs

1#![allow(dead_code)]
2use crate::scw_verifier::SmartContractSignatureVerifier;
3
4use super::{
5    AccountId, Action, AddAssociation, CreateInbox, IdentityUpdate, RevokeAssociation,
6    SignatureError,
7    unsigned_actions::{
8        SignatureTextCreator, UnsignedAction, UnsignedAddAssociation,
9        UnsignedChangeRecoveryAddress, UnsignedCreateInbox, UnsignedIdentityUpdate,
10        UnsignedRevokeAssociation,
11    },
12    verified_signature::VerifiedSignature,
13};
14use crate::associations::AssociationError;
15use futures::future::try_join_all;
16use xmtp_db::identity_update::StoredIdentityUpdate;
17use xmtp_proto::xmtp::message_contents::SignedPublicKey as LegacySignedPublicKeyProto;
18
19impl TryFrom<StoredIdentityUpdate> for UnverifiedIdentityUpdate {
20    type Error = AssociationError;
21
22    fn try_from(update: StoredIdentityUpdate) -> Result<Self, Self::Error> {
23        Ok(UnverifiedIdentityUpdate::try_from(update.payload)?)
24    }
25}
26
27#[derive(Debug, Clone, PartialEq)]
28pub struct UnverifiedIdentityUpdate {
29    pub inbox_id: String,
30    pub client_timestamp_ns: u64,
31    pub actions: Vec<UnverifiedAction>,
32}
33
34impl UnverifiedIdentityUpdate {
35    pub fn new(inbox_id: String, client_timestamp_ns: u64, actions: Vec<UnverifiedAction>) -> Self {
36        Self {
37            inbox_id,
38            client_timestamp_ns,
39            actions,
40        }
41    }
42
43    fn signature_text(&self) -> String {
44        let unsigned_actions = self
45            .actions
46            .iter()
47            .map(|action| action.unsigned_action())
48            .collect();
49        let unsigned_identity_update = UnsignedIdentityUpdate::new(
50            unsigned_actions,
51            self.inbox_id.clone(),
52            self.client_timestamp_ns,
53        );
54
55        unsigned_identity_update.signature_text()
56    }
57
58    fn signatures(&self) -> Vec<UnverifiedSignature> {
59        self.actions
60            .iter()
61            .flat_map(|action| action.signatures())
62            .collect()
63    }
64
65    pub async fn to_verified(
66        &self,
67        scw_verifier: impl SmartContractSignatureVerifier,
68    ) -> Result<IdentityUpdate, SignatureError> {
69        let signature_text = self.signature_text();
70
71        let actions: Vec<Action> = try_join_all(
72            self.actions
73                .iter()
74                .map(|action| action.to_verified(&signature_text, &scw_verifier)),
75        )
76        .await?;
77
78        Ok(IdentityUpdate::new(
79            actions,
80            self.inbox_id.clone(),
81            self.client_timestamp_ns,
82        ))
83    }
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub enum UnverifiedAction {
88    CreateInbox(UnverifiedCreateInbox),
89    AddAssociation(UnverifiedAddAssociation),
90    RevokeAssociation(UnverifiedRevokeAssociation),
91    ChangeRecoveryAddress(UnverifiedChangeRecoveryAddress),
92}
93
94impl UnverifiedAction {
95    fn unsigned_action(&self) -> UnsignedAction {
96        match self {
97            UnverifiedAction::CreateInbox(action) => {
98                UnsignedAction::CreateInbox(action.unsigned_action.clone())
99            }
100            UnverifiedAction::AddAssociation(action) => {
101                UnsignedAction::AddAssociation(action.unsigned_action.clone())
102            }
103            UnverifiedAction::RevokeAssociation(action) => {
104                UnsignedAction::RevokeAssociation(action.unsigned_action.clone())
105            }
106            UnverifiedAction::ChangeRecoveryAddress(action) => {
107                UnsignedAction::ChangeRecoveryAddress(action.unsigned_action.clone())
108            }
109        }
110    }
111
112    fn signatures(&self) -> Vec<UnverifiedSignature> {
113        match self {
114            UnverifiedAction::CreateInbox(action) => {
115                vec![action.initial_identifier_signature.clone()]
116            }
117            UnverifiedAction::AddAssociation(action) => vec![
118                action.existing_member_signature.clone(),
119                action.new_member_signature.clone(),
120            ],
121            UnverifiedAction::RevokeAssociation(action) => {
122                vec![action.recovery_identifier_signature.clone()]
123            }
124            UnverifiedAction::ChangeRecoveryAddress(action) => {
125                vec![action.recovery_identifier_signature.clone()]
126            }
127        }
128    }
129
130    pub async fn to_verified<Text: AsRef<str>>(
131        &self,
132        signature_text: Text,
133        scw_verifier: impl SmartContractSignatureVerifier,
134    ) -> Result<Action, SignatureError> {
135        let action = match self {
136            UnverifiedAction::CreateInbox(action) => Action::CreateInbox(CreateInbox {
137                nonce: action.unsigned_action.nonce,
138                account_identifier: action.unsigned_action.account_identifier.clone(),
139                initial_identifier_signature: action
140                    .initial_identifier_signature
141                    .to_verified(signature_text.as_ref(), &scw_verifier)
142                    .await?,
143            }),
144            UnverifiedAction::AddAssociation(action) => Action::AddAssociation(AddAssociation {
145                new_member_signature: action
146                    .new_member_signature
147                    .to_verified(signature_text.as_ref(), &scw_verifier)
148                    .await?,
149                new_member_identifier: action.unsigned_action.new_member_identifier.clone(),
150                existing_member_signature: action
151                    .existing_member_signature
152                    .to_verified(signature_text.as_ref(), &scw_verifier)
153                    .await?,
154            }),
155            UnverifiedAction::RevokeAssociation(action) => {
156                Action::RevokeAssociation(RevokeAssociation {
157                    recovery_identifier_signature: action
158                        .recovery_identifier_signature
159                        .to_verified(signature_text.as_ref(), &scw_verifier)
160                        .await?,
161                    revoked_member: action.unsigned_action.revoked_member.clone(),
162                })
163            }
164            UnverifiedAction::ChangeRecoveryAddress(action) => {
165                Action::ChangeRecoveryIdentity(super::ChangeRecoveryIdentity {
166                    recovery_identifier_signature: action
167                        .recovery_identifier_signature
168                        .to_verified(signature_text.as_ref(), &scw_verifier)
169                        .await?,
170                    new_recovery_identifier: action.unsigned_action.new_recovery_identifier.clone(),
171                })
172            }
173        };
174
175        Ok(action)
176    }
177}
178
179#[derive(Debug, Clone, PartialEq)]
180pub struct UnverifiedCreateInbox {
181    pub(crate) unsigned_action: UnsignedCreateInbox,
182    pub(crate) initial_identifier_signature: UnverifiedSignature,
183}
184
185impl UnverifiedCreateInbox {
186    pub fn new(
187        unsigned_action: UnsignedCreateInbox,
188        initial_identifier_signature: UnverifiedSignature,
189    ) -> Self {
190        Self {
191            unsigned_action,
192            initial_identifier_signature,
193        }
194    }
195}
196
197#[derive(Debug, Clone, PartialEq)]
198pub struct UnverifiedAddAssociation {
199    pub(crate) unsigned_action: UnsignedAddAssociation,
200    pub(crate) new_member_signature: UnverifiedSignature,
201    pub(crate) existing_member_signature: UnverifiedSignature,
202}
203
204impl UnverifiedAddAssociation {
205    pub fn new(
206        unsigned_action: UnsignedAddAssociation,
207        new_member_signature: UnverifiedSignature,
208        existing_member_signature: UnverifiedSignature,
209    ) -> Self {
210        Self {
211            unsigned_action,
212            new_member_signature,
213            existing_member_signature,
214        }
215    }
216}
217#[derive(Debug, Clone, PartialEq)]
218pub struct UnverifiedRevokeAssociation {
219    pub(crate) recovery_identifier_signature: UnverifiedSignature,
220    pub(crate) unsigned_action: UnsignedRevokeAssociation,
221}
222
223impl UnverifiedRevokeAssociation {
224    pub fn new(
225        unsigned_action: UnsignedRevokeAssociation,
226        recovery_identifier_signature: UnverifiedSignature,
227    ) -> Self {
228        Self {
229            unsigned_action,
230            recovery_identifier_signature,
231        }
232    }
233}
234
235#[derive(Debug, Clone, PartialEq)]
236pub struct UnverifiedChangeRecoveryAddress {
237    pub(crate) recovery_identifier_signature: UnverifiedSignature,
238    pub(crate) unsigned_action: UnsignedChangeRecoveryAddress,
239}
240
241impl UnverifiedChangeRecoveryAddress {
242    pub fn new(
243        unsigned_action: UnsignedChangeRecoveryAddress,
244        recovery_identifier_signature: UnverifiedSignature,
245    ) -> Self {
246        Self {
247            unsigned_action,
248            recovery_identifier_signature,
249        }
250    }
251}
252
253#[derive(Debug, Clone, PartialEq)]
254pub enum UnverifiedSignature {
255    InstallationKey(UnverifiedInstallationKeySignature),
256    RecoverableEcdsa(UnverifiedRecoverableEcdsaSignature),
257    SmartContractWallet(UnverifiedSmartContractWalletSignature),
258    LegacyDelegated(UnverifiedLegacyDelegatedSignature),
259    Passkey(UnverifiedPasskeySignature),
260}
261
262impl UnverifiedSignature {
263    pub async fn to_verified<Text: AsRef<str>>(
264        &self,
265        signature_text: Text,
266        scw_verifier: impl SmartContractSignatureVerifier,
267    ) -> Result<VerifiedSignature, SignatureError> {
268        match self {
269            UnverifiedSignature::InstallationKey(sig) => VerifiedSignature::from_installation_key(
270                signature_text,
271                &sig.signature_bytes,
272                *sig.verifying_key(),
273            ),
274            UnverifiedSignature::RecoverableEcdsa(sig) => {
275                VerifiedSignature::from_recoverable_ecdsa(signature_text, &sig.signature_bytes)
276            }
277            UnverifiedSignature::SmartContractWallet(sig) => {
278                VerifiedSignature::from_smart_contract_wallet(
279                    signature_text,
280                    scw_verifier,
281                    &sig.signature_bytes,
282                    sig.account_id.clone(),
283                    &mut Some(sig.block_number),
284                )
285                .await
286            }
287            UnverifiedSignature::LegacyDelegated(sig) => VerifiedSignature::from_legacy_delegated(
288                signature_text,
289                &sig.legacy_key_signature.signature_bytes,
290                sig.signed_public_key_proto.clone(),
291            ),
292            UnverifiedSignature::Passkey(sig) => VerifiedSignature::from_passkey(
293                signature_text,
294                &sig.public_key,
295                &sig.signature,
296                &sig.authenticator_data,
297                &sig.client_data_json,
298            ),
299        }
300    }
301
302    pub fn new_recoverable_ecdsa(signature: Vec<u8>) -> Self {
303        Self::RecoverableEcdsa(UnverifiedRecoverableEcdsaSignature::new(signature))
304    }
305
306    pub fn new_installation_key(
307        signature: Vec<u8>,
308        verifying_key: ed25519_dalek::VerifyingKey,
309    ) -> Self {
310        Self::InstallationKey(UnverifiedInstallationKeySignature::new(
311            signature,
312            verifying_key,
313        ))
314    }
315
316    pub fn new_passkey(
317        public_key: Vec<u8>,
318        signature: Vec<u8>,
319        authenticator_data: Vec<u8>,
320        client_data_json: Vec<u8>,
321    ) -> Self {
322        Self::Passkey(UnverifiedPasskeySignature {
323            client_data_json,
324            authenticator_data,
325            signature,
326            public_key,
327        })
328    }
329
330    pub fn new_smart_contract_wallet(
331        signature: Vec<u8>,
332        account_id: AccountId,
333        block_number: u64,
334    ) -> Self {
335        Self::SmartContractWallet(UnverifiedSmartContractWalletSignature::new(
336            signature,
337            account_id,
338            block_number,
339        ))
340    }
341
342    pub fn new_legacy_delegated(
343        signature: Vec<u8>,
344        signed_public_key_proto: LegacySignedPublicKeyProto,
345    ) -> Self {
346        Self::LegacyDelegated(UnverifiedLegacyDelegatedSignature::new(
347            UnverifiedRecoverableEcdsaSignature::new(signature),
348            signed_public_key_proto,
349        ))
350    }
351}
352
353#[derive(Debug, Clone, PartialEq)]
354pub struct UnverifiedInstallationKeySignature {
355    pub(crate) signature_bytes: Vec<u8>,
356    /// The Signature Verifying Key
357    // boxing avoids large enum variants if an enum contains multiple signature
358    pub(crate) verifying_key: Box<ed25519_dalek::VerifyingKey>,
359}
360
361impl UnverifiedInstallationKeySignature {
362    pub fn new(signature_bytes: Vec<u8>, verifying_key: ed25519_dalek::VerifyingKey) -> Self {
363        Self {
364            signature_bytes,
365            verifying_key: Box::new(verifying_key),
366        }
367    }
368
369    pub fn verifying_key(&self) -> &ed25519_dalek::VerifyingKey {
370        self.verifying_key.as_ref()
371    }
372
373    pub fn verifying_key_bytes(&self) -> Vec<u8> {
374        self.verifying_key.as_ref().as_ref().to_vec()
375    }
376}
377
378#[derive(Debug, Clone, PartialEq)]
379pub struct UnverifiedPasskeySignature {
380    // This json string contains the challenge we sent out
381    pub public_key: Vec<u8>,
382    pub signature: Vec<u8>,
383    pub authenticator_data: Vec<u8>,
384    pub client_data_json: Vec<u8>,
385}
386
387#[derive(Debug, Clone, PartialEq)]
388pub struct UnverifiedRecoverableEcdsaSignature {
389    pub(crate) signature_bytes: Vec<u8>,
390}
391
392impl UnverifiedRecoverableEcdsaSignature {
393    pub fn new(signature_bytes: Vec<u8>) -> Self {
394        Self { signature_bytes }
395    }
396    pub fn signature_bytes(&self) -> &[u8] {
397        &self.signature_bytes
398    }
399}
400
401#[derive(Debug, Clone, PartialEq)]
402pub struct UnverifiedSmartContractWalletSignature {
403    pub(crate) signature_bytes: Vec<u8>,
404    pub(crate) account_id: AccountId,
405    pub(crate) block_number: u64,
406}
407
408#[derive(Debug, Clone, PartialEq)]
409pub struct NewUnverifiedSmartContractWalletSignature {
410    pub(crate) signature_bytes: Vec<u8>,
411    pub(crate) account_id: AccountId,
412    pub(crate) block_number: Option<u64>,
413}
414
415impl NewUnverifiedSmartContractWalletSignature {
416    pub fn new(signature_bytes: Vec<u8>, account_id: AccountId, block_number: Option<u64>) -> Self {
417        Self {
418            account_id,
419            block_number,
420            signature_bytes,
421        }
422    }
423}
424
425impl UnverifiedSmartContractWalletSignature {
426    pub fn new(signature_bytes: Vec<u8>, account_id: AccountId, block_number: u64) -> Self {
427        Self {
428            signature_bytes,
429            account_id,
430            block_number,
431        }
432    }
433}
434
435#[derive(Debug, Clone, PartialEq)]
436pub struct UnverifiedLegacyDelegatedSignature {
437    pub(crate) legacy_key_signature: UnverifiedRecoverableEcdsaSignature,
438    pub(crate) signed_public_key_proto: LegacySignedPublicKeyProto,
439}
440
441impl UnverifiedLegacyDelegatedSignature {
442    pub fn new(
443        legacy_key_signature: UnverifiedRecoverableEcdsaSignature,
444        signed_public_key_proto: LegacySignedPublicKeyProto,
445    ) -> Self {
446        Self {
447            legacy_key_signature,
448            signed_public_key_proto,
449        }
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    #[cfg(target_arch = "wasm32")]
456    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker);
457
458    use crate::associations::{member::Identifier, unsigned_actions::UnsignedCreateInbox};
459
460    use super::{
461        UnverifiedAction, UnverifiedCreateInbox, UnverifiedIdentityUpdate,
462        UnverifiedRecoverableEcdsaSignature, UnverifiedSignature,
463    };
464
465    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
466    #[cfg_attr(not(target_arch = "wasm32"), test)]
467    fn create_identity_update() {
468        let account_identifier = Identifier::rand_ethereum();
469        let nonce = 1;
470        let update = UnverifiedIdentityUpdate {
471            inbox_id: account_identifier.inbox_id(nonce).unwrap(),
472            client_timestamp_ns: 10,
473            actions: vec![UnverifiedAction::CreateInbox(UnverifiedCreateInbox {
474                unsigned_action: UnsignedCreateInbox {
475                    account_identifier: account_identifier.clone(),
476                    nonce,
477                },
478                initial_identifier_signature: UnverifiedSignature::RecoverableEcdsa(
479                    UnverifiedRecoverableEcdsaSignature {
480                        signature_bytes: vec![1, 2, 3],
481                    },
482                ),
483            })],
484        };
485        assert!(
486            update
487                .signature_text()
488                .contains(format!("(Owner: {})", account_identifier).as_str()),
489            "could not find account address in signature text: {}",
490            update.signature_text()
491        );
492    }
493}