1use std::collections::HashMap;
6
7use super::member::{HasMemberKind, Identifier};
8use crate::scw_verifier::SmartContractSignatureVerifier;
9use thiserror::Error;
10use xmtp_common::time::now_ns;
11
12use super::{
13 MemberIdentifier, MemberKind, SignatureError,
14 unsigned_actions::{
15 SignatureTextCreator, UnsignedAction, UnsignedAddAssociation,
16 UnsignedChangeRecoveryAddress, UnsignedCreateInbox, UnsignedIdentityUpdate,
17 UnsignedRevokeAssociation,
18 },
19 unverified::{
20 NewUnverifiedSmartContractWalletSignature, UnverifiedAction, UnverifiedAddAssociation,
21 UnverifiedChangeRecoveryAddress, UnverifiedCreateInbox, UnverifiedIdentityUpdate,
22 UnverifiedRevokeAssociation, UnverifiedSignature, UnverifiedSmartContractWalletSignature,
23 },
24 verified_signature::VerifiedSignature,
25};
26
27#[derive(Clone, PartialEq, Hash, Eq, Debug)]
30enum SignatureField {
31 InitialAddress,
32 ExistingMember,
33 NewMember,
34 RecoveryAddress,
35}
36
37#[derive(Clone, Debug)]
38pub struct PendingIdentityAction {
39 unsigned_action: UnsignedAction,
40 pending_signatures: HashMap<SignatureField, MemberIdentifier>,
41}
42
43pub struct SignatureRequestBuilder {
48 inbox_id: String,
49 client_timestamp_ns: u64,
50 actions: Vec<PendingIdentityAction>,
51}
52
53impl SignatureRequestBuilder {
54 pub fn new<S: AsRef<str>>(inbox_id: S) -> Self {
56 Self {
57 inbox_id: inbox_id.as_ref().to_string(),
58 client_timestamp_ns: now_ns() as u64,
59 actions: vec![],
60 }
61 }
62
63 pub fn create_inbox(mut self, signer_identity: Identifier, nonce: u64) -> Self {
65 let pending_action = PendingIdentityAction {
66 unsigned_action: UnsignedAction::CreateInbox(UnsignedCreateInbox {
67 account_identifier: signer_identity.clone(),
68 nonce,
69 }),
70 pending_signatures: HashMap::from([(
71 SignatureField::InitialAddress,
72 signer_identity.into(),
73 )]),
74 };
75 self.actions.push(pending_action);
77
78 self
79 }
80
81 pub fn add_association(
83 mut self,
84 new_member_identifier: MemberIdentifier,
85 existing_member_identifier: MemberIdentifier,
86 ) -> Self {
87 self.actions.push(PendingIdentityAction {
88 unsigned_action: UnsignedAction::AddAssociation(UnsignedAddAssociation {
89 new_member_identifier: new_member_identifier.clone(),
90 }),
91 pending_signatures: HashMap::from([
92 (SignatureField::ExistingMember, existing_member_identifier),
93 (SignatureField::NewMember, new_member_identifier),
94 ]),
95 });
96
97 self
98 }
99
100 pub fn revoke_association(
101 mut self,
102 recovery_address_signer: MemberIdentifier,
103 revoked_member: MemberIdentifier,
104 ) -> Self {
105 self.actions.push(PendingIdentityAction {
106 pending_signatures: HashMap::from([(
107 SignatureField::RecoveryAddress,
108 recovery_address_signer,
109 )]),
110 unsigned_action: UnsignedAction::RevokeAssociation(UnsignedRevokeAssociation {
111 revoked_member,
112 }),
113 });
114
115 self
116 }
117
118 pub fn change_recovery_address(
119 mut self,
120 recovery_address_signer: MemberIdentifier,
121 new_recovery_identifier: Identifier,
122 ) -> Self {
123 self.actions.push(PendingIdentityAction {
124 pending_signatures: HashMap::from([(
125 SignatureField::RecoveryAddress,
126 recovery_address_signer,
127 )]),
128 unsigned_action: UnsignedAction::ChangeRecoveryAddress(UnsignedChangeRecoveryAddress {
129 new_recovery_identifier,
130 }),
131 });
132
133 self
134 }
135
136 pub fn build(self) -> SignatureRequest {
137 let unsigned_actions: Vec<UnsignedAction> = self
138 .actions
139 .iter()
140 .map(|pending_action| pending_action.unsigned_action.clone())
141 .collect();
142
143 let signature_text = get_signature_text(
144 unsigned_actions,
145 self.inbox_id.clone(),
146 self.client_timestamp_ns,
147 );
148
149 SignatureRequest::new(
150 self.actions,
151 signature_text,
152 self.inbox_id,
153 self.client_timestamp_ns,
154 )
155 }
156}
157
158#[derive(Debug, Error)]
159pub enum SignatureRequestError {
160 #[error("Unknown signer")]
161 UnknownSigner,
162 #[error("Required signature was not provided")]
163 MissingSigner,
164 #[error("Signature error {0}")]
165 Signature(#[from] SignatureError),
166 #[error("Unable to get block number")]
167 BlockNumber,
168}
169
170#[derive(Clone, Debug)]
175pub struct SignatureRequest {
176 pending_actions: Vec<PendingIdentityAction>,
177 signature_text: String,
178 signatures: HashMap<MemberIdentifier, UnverifiedSignature>,
179 client_timestamp_ns: u64,
180 inbox_id: String,
181}
182
183impl SignatureRequest {
184 pub fn new(
185 pending_actions: Vec<PendingIdentityAction>,
186 signature_text: String,
187 inbox_id: String,
188 client_timestamp_ns: u64,
189 ) -> Self {
190 Self {
191 inbox_id,
192 pending_actions,
193 signature_text,
194 signatures: HashMap::new(),
195 client_timestamp_ns,
196 }
197 }
198
199 pub fn missing_signatures(&self) -> Vec<&MemberIdentifier> {
200 self.pending_actions
201 .iter()
202 .flat_map(|pending_action| pending_action.pending_signatures.values())
203 .filter(|ident| !self.signatures.contains_key(ident))
204 .collect()
205 }
206
207 pub fn missing_address_signatures(&self) -> Vec<&MemberIdentifier> {
208 self.missing_signatures()
209 .into_iter()
210 .filter(|member| matches!(member.kind(), MemberKind::Ethereum | MemberKind::Passkey))
211 .collect()
212 }
213
214 pub async fn add_new_unverified_smart_contract_signature(
218 &mut self,
219 mut signature: NewUnverifiedSmartContractWalletSignature,
220 scw_verifier: impl SmartContractSignatureVerifier,
221 ) -> Result<(), SignatureRequestError> {
222 let verified_signature = VerifiedSignature::from_smart_contract_wallet(
223 &self.signature_text,
224 scw_verifier,
225 &signature.signature_bytes,
226 signature.account_id.clone(),
227 &mut signature.block_number,
228 )
229 .await?;
230
231 let Some(block_number) = signature.block_number else {
232 return Err(SignatureRequestError::BlockNumber);
233 };
234
235 self.add_verified_signature(
236 UnverifiedSignature::SmartContractWallet(UnverifiedSmartContractWalletSignature {
237 account_id: signature.account_id,
238 block_number,
239 signature_bytes: signature.signature_bytes,
240 }),
241 verified_signature,
242 )
243 }
244
245 pub async fn add_signature(
246 &mut self,
247 signature: UnverifiedSignature,
248 scw_verifier: impl SmartContractSignatureVerifier,
249 ) -> Result<(), SignatureRequestError> {
250 let verified_signature = signature
251 .to_verified(self.signature_text.clone(), scw_verifier)
252 .await?;
253
254 self.add_verified_signature(signature, verified_signature)
255 }
256
257 fn add_verified_signature(
258 &mut self,
259 signature: UnverifiedSignature,
260 verified_signature: VerifiedSignature,
261 ) -> Result<(), SignatureRequestError> {
262 let signer_identity = &verified_signature.signer;
263
264 let missing_signatures = self.missing_signatures();
265 tracing::info!(
266 signer = %signer_identity,
267 missing_signatures=?missing_signatures,
268 "adding verified signature");
269
270 if !missing_signatures.contains(&signer_identity) {
272 return Err(SignatureRequestError::UnknownSigner);
273 }
274
275 self.signatures.insert(verified_signature.signer, signature);
276
277 Ok(())
278 }
279
280 pub fn is_ready(&self) -> bool {
281 self.missing_signatures().is_empty()
282 }
283
284 pub fn signature_text(&self) -> String {
285 self.signature_text.clone()
286 }
287
288 pub fn build_identity_update(self) -> Result<UnverifiedIdentityUpdate, SignatureRequestError> {
289 if !self.is_ready() {
290 return Err(SignatureRequestError::MissingSigner);
291 }
292
293 let actions = self
294 .pending_actions
295 .clone()
296 .into_iter()
297 .map(|pending_action| build_action(pending_action, &self.signatures))
298 .collect::<Result<Vec<UnverifiedAction>, SignatureRequestError>>()?;
299
300 Ok(UnverifiedIdentityUpdate::new(
301 self.inbox_id,
302 self.client_timestamp_ns,
303 actions,
304 ))
305 }
306
307 pub fn inbox_id(&self) -> crate::InboxIdRef<'_> {
308 &self.inbox_id
309 }
310}
311
312fn build_action(
313 pending_action: PendingIdentityAction,
314 signatures: &HashMap<MemberIdentifier, UnverifiedSignature>,
315) -> Result<UnverifiedAction, SignatureRequestError> {
316 match pending_action.unsigned_action {
317 UnsignedAction::CreateInbox(unsigned_action) => {
318 let signer_identity = pending_action
319 .pending_signatures
320 .get(&SignatureField::InitialAddress)
321 .ok_or(SignatureRequestError::MissingSigner)?;
322 let initial_identifier_signature = signatures
323 .get(signer_identity)
324 .cloned()
325 .ok_or(SignatureRequestError::MissingSigner)?;
326
327 Ok(UnverifiedAction::CreateInbox(UnverifiedCreateInbox {
328 unsigned_action,
329 initial_identifier_signature,
330 }))
331 }
332 UnsignedAction::AddAssociation(unsigned_action) => {
333 let existing_member_signer_identity = pending_action
334 .pending_signatures
335 .get(&SignatureField::ExistingMember)
336 .ok_or(SignatureRequestError::MissingSigner)?;
337 let new_member_signer_identity = pending_action
338 .pending_signatures
339 .get(&SignatureField::NewMember)
340 .ok_or(SignatureRequestError::MissingSigner)?;
341
342 let existing_member_signature = signatures
343 .get(existing_member_signer_identity)
344 .cloned()
345 .ok_or(SignatureRequestError::MissingSigner)?;
346
347 let new_member_signature = signatures
348 .get(new_member_signer_identity)
349 .cloned()
350 .ok_or(SignatureRequestError::MissingSigner)?;
351
352 Ok(UnverifiedAction::AddAssociation(UnverifiedAddAssociation {
353 unsigned_action,
354 existing_member_signature,
355 new_member_signature,
356 }))
357 }
358 UnsignedAction::RevokeAssociation(unsigned_action) => {
359 let signer_identity = pending_action
360 .pending_signatures
361 .get(&SignatureField::RecoveryAddress)
362 .ok_or(SignatureRequestError::MissingSigner)?;
363 let recovery_address_signature = signatures
364 .get(signer_identity)
365 .cloned()
366 .ok_or(SignatureRequestError::MissingSigner)?;
367
368 Ok(UnverifiedAction::RevokeAssociation(
369 UnverifiedRevokeAssociation {
370 recovery_identifier_signature: recovery_address_signature,
371 unsigned_action,
372 },
373 ))
374 }
375 UnsignedAction::ChangeRecoveryAddress(unsigned_action) => {
376 let signer_identity = pending_action
377 .pending_signatures
378 .get(&SignatureField::RecoveryAddress)
379 .ok_or(SignatureRequestError::MissingSigner)?;
380
381 let recovery_identifier_signature = signatures
382 .get(signer_identity)
383 .cloned()
384 .ok_or(SignatureRequestError::MissingSigner)?;
385
386 Ok(UnverifiedAction::ChangeRecoveryAddress(
387 UnverifiedChangeRecoveryAddress {
388 recovery_identifier_signature,
389 unsigned_action,
390 },
391 ))
392 }
393 }
394}
395
396fn get_signature_text(
397 actions: Vec<UnsignedAction>,
398 inbox_id: String,
399 client_timestamp_ns: u64,
400) -> String {
401 let identity_update = UnsignedIdentityUpdate {
402 client_timestamp_ns,
403 actions,
404 inbox_id,
405 };
406
407 identity_update.signature_text()
408}
409
410#[cfg(test)]
411pub(crate) mod tests {
412 #[cfg(target_arch = "wasm32")]
413 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker);
414 use alloy::signers::{Signer, local::PrivateKeySigner};
415 use xmtp_cryptography::XmtpInstallationCredential;
416
417 use crate::{
418 InboxOwner,
419 associations::{
420 IdentityUpdate, get_state,
421 test_utils::{
422 MockSmartContractSignatureVerifier, WalletTestExt, add_installation_key_signature,
423 add_wallet_signature,
424 },
425 unverified::UnverifiedRecoverableEcdsaSignature,
426 },
427 };
428
429 use super::*;
430
431 async fn convert_to_verified(identity_update: &UnverifiedIdentityUpdate) -> IdentityUpdate {
432 let scw_verifier = MockSmartContractSignatureVerifier::new(false);
433 identity_update
434 .to_verified(&scw_verifier)
435 .await
436 .expect("should be valid")
437 }
438
439 #[xmtp_common::test]
440 async fn create_inbox() {
441 let wallet = PrivateKeySigner::random();
442 let account_ident = wallet.get_identifier().unwrap();
443 let nonce = 0;
444 let inbox_id = wallet.get_inbox_id(nonce);
445
446 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
447 .create_inbox(account_ident, nonce)
448 .build();
449
450 add_wallet_signature(&mut signature_request, &wallet).await;
451
452 let identity_update = signature_request
453 .build_identity_update()
454 .expect("should be valid");
455
456 get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
457 }
458
459 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
460 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
461 async fn create_and_add_identity() {
462 let wallet = PrivateKeySigner::random();
463 let installation_key = XmtpInstallationCredential::new();
464 let account_address = wallet.get_identifier().unwrap();
465 let nonce = 0;
466 let inbox_id = wallet.get_inbox_id(nonce);
467 let ident = Identifier::eth(&account_address).unwrap();
468 let new_member_identifier =
469 MemberIdentifier::installation(installation_key.public_bytes().to_vec());
470
471 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
472 .create_inbox(ident.clone(), nonce)
473 .add_association(new_member_identifier, ident.into())
474 .build();
475
476 add_wallet_signature(&mut signature_request, &wallet).await;
477 add_installation_key_signature(&mut signature_request, &installation_key).await;
478
479 let identity_update = signature_request
480 .build_identity_update()
481 .expect("should be valid");
482
483 let state =
484 get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
485 assert_eq!(state.members().len(), 2);
486 }
487
488 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
489 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
490 async fn create_and_revoke() {
491 let wallet = PrivateKeySigner::random();
492 let nonce = 0;
493 let inbox_id = wallet.get_inbox_id(nonce);
494 let existing_member_identifier = wallet.identifier();
495
496 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
497 .create_inbox(existing_member_identifier.clone(), nonce)
498 .revoke_association(
499 existing_member_identifier.clone().into(),
500 existing_member_identifier.clone().into(),
501 )
502 .build();
503
504 add_wallet_signature(&mut signature_request, &wallet).await;
505
506 let identity_update = signature_request
507 .build_identity_update()
508 .expect("should be valid");
509
510 let state =
511 get_state(vec![convert_to_verified(&identity_update).await]).expect("should be valid");
512
513 assert_eq!(state.members().len(), 0);
514 }
515
516 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
517 #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
518 async fn attempt_adding_unknown_signer() {
519 let account_address = "0x1234567890abcdef1234567890abcdef12345678".to_string();
520 let nonce = 0;
521 let ident = Identifier::eth(&account_address).unwrap();
522 let inbox_id = ident.inbox_id(nonce).unwrap();
523
524 let mut signature_request = SignatureRequestBuilder::new(inbox_id)
525 .create_inbox(ident, nonce)
526 .build();
527
528 let rand_wallet = PrivateKeySigner::random();
529
530 let signature_text = signature_request.signature_text();
531 let sig = rand_wallet
532 .sign_message(signature_text.as_bytes())
533 .await
534 .unwrap();
535 let unverified_sig = UnverifiedSignature::RecoverableEcdsa(
536 UnverifiedRecoverableEcdsaSignature::new(sig.into()),
537 );
538 let scw_verifier = MockSmartContractSignatureVerifier::new(false);
539
540 let attempt_to_add_random_member = signature_request
541 .add_signature(unverified_sig, &scw_verifier)
542 .await;
543
544 assert!(matches!(
545 attempt_to_add_random_member,
546 Err(SignatureRequestError::UnknownSigner)
547 ));
548 }
549}