xmtp_id/associations/
unsigned_actions.rs1use super::{MemberIdentifier, member::Identifier};
2use crate::associations::{MemberKind, member::HasMemberKind};
3use chrono::DateTime;
4
5const HEADER: &str = "XMTP : Authenticate to inbox";
6const FOOTER: &str = "For more info: https://xmtp.org/signatures";
7
8pub trait SignatureTextCreator {
9 fn signature_text(&self) -> String;
10}
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct UnsignedCreateInbox {
14 pub nonce: u64,
15 pub account_identifier: Identifier,
16}
17
18impl SignatureTextCreator for UnsignedCreateInbox {
19 fn signature_text(&self) -> String {
20 format!("- Create inbox\n (Owner: {})", self.account_identifier)
21 }
22}
23
24#[derive(Clone, Debug, PartialEq)]
25pub struct UnsignedAddAssociation {
26 pub new_member_identifier: MemberIdentifier,
27}
28
29impl SignatureTextCreator for UnsignedAddAssociation {
30 fn signature_text(&self) -> String {
31 let member_kind = self.new_member_identifier.kind();
32 let id_kind = get_identifier_text(&member_kind);
33 let prefix = match member_kind {
34 MemberKind::Installation => "Grant messaging access to app",
35 MemberKind::Ethereum => "Link address to inbox",
36 MemberKind::Passkey => "Link passkey to inbox",
37 };
38 format!("- {prefix}\n ({id_kind}: {})", self.new_member_identifier)
39 }
40}
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct UnsignedRevokeAssociation {
44 pub revoked_member: MemberIdentifier,
45}
46
47impl SignatureTextCreator for UnsignedRevokeAssociation {
48 fn signature_text(&self) -> String {
49 let member_kind = self.revoked_member.kind();
50 let id_kind = get_identifier_text(&member_kind);
51 let prefix = match self.revoked_member.kind() {
52 MemberKind::Installation => "Revoke messaging access from app",
53 MemberKind::Ethereum => "Unlink address from inbox",
54 MemberKind::Passkey => "Unlink passkey from inbox",
55 };
56 format!("- {prefix}\n ({id_kind}: {})", self.revoked_member)
57 }
58}
59
60#[derive(Clone, Debug, PartialEq)]
61pub struct UnsignedChangeRecoveryAddress {
62 pub new_recovery_identifier: Identifier,
63}
64
65impl SignatureTextCreator for UnsignedChangeRecoveryAddress {
66 fn signature_text(&self) -> String {
67 format!(
68 "- Change inbox recovery address\n (Address: {})",
69 self.new_recovery_identifier
70 )
71 }
72}
73
74#[allow(dead_code)]
75#[derive(Clone, Debug, PartialEq)]
76pub enum UnsignedAction {
77 CreateInbox(UnsignedCreateInbox),
78 AddAssociation(UnsignedAddAssociation),
79 RevokeAssociation(UnsignedRevokeAssociation),
80 ChangeRecoveryAddress(UnsignedChangeRecoveryAddress),
81}
82
83impl SignatureTextCreator for UnsignedAction {
84 fn signature_text(&self) -> String {
85 match self {
86 UnsignedAction::CreateInbox(action) => action.signature_text(),
87 UnsignedAction::AddAssociation(action) => action.signature_text(),
88 UnsignedAction::RevokeAssociation(action) => action.signature_text(),
89 UnsignedAction::ChangeRecoveryAddress(action) => action.signature_text(),
90 }
91 }
92}
93
94#[derive(Clone, Debug, PartialEq)]
95pub struct UnsignedIdentityUpdate {
96 pub inbox_id: String,
97 pub client_timestamp_ns: u64,
98 pub actions: Vec<UnsignedAction>,
99}
100
101impl UnsignedIdentityUpdate {
102 pub fn new(actions: Vec<UnsignedAction>, inbox_id: String, client_timestamp_ns: u64) -> Self {
103 UnsignedIdentityUpdate {
104 inbox_id,
105 client_timestamp_ns,
106 actions,
107 }
108 }
109}
110
111impl SignatureTextCreator for UnsignedIdentityUpdate {
112 fn signature_text(&self) -> String {
113 let all_signatures = self
114 .actions
115 .iter()
116 .map(|action| action.signature_text())
117 .collect::<Vec<String>>();
118 format!(
119 "{HEADER}\n\nInbox ID: {}\nCurrent time: {}\n\n{}\n\n{FOOTER}",
120 self.inbox_id,
121 pretty_timestamp(self.client_timestamp_ns),
122 all_signatures.join("\n"),
123 )
124 }
125}
126
127fn get_identifier_text(kind: &MemberKind) -> String {
128 match kind {
129 MemberKind::Ethereum => "Address".to_string(),
130 MemberKind::Installation => "ID".to_string(),
131 MemberKind::Passkey => "Passkey".to_string(),
132 }
133}
134
135fn pretty_timestamp(ns_date: u64) -> String {
136 let date = DateTime::from_timestamp_nanos(ns_date as i64);
137 date.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
138}
139
140#[cfg(test)]
141pub(crate) mod tests {
142 use super::*;
143
144 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
145 #[cfg_attr(not(target_arch = "wasm32"), test)]
146 fn create_signatures() {
147 let account_identifier =
148 Identifier::eth("0x1234567890abcdef1234567890abcdef12345678").unwrap();
149
150 let client_timestamp_ns: u64 = 12;
151 let new_member_address = "0x4567890abcdef1234567890abcdef12345678123".to_string();
152 let new_recovery_identifier =
153 Identifier::eth("0x7890abcdef1234567890abcdef12345678123456").unwrap();
154 let new_installation_id = vec![1, 2, 3];
155 let create_inbox = UnsignedCreateInbox {
156 nonce: 0,
157 account_identifier: account_identifier.clone(),
158 };
159 let inbox_id = account_identifier.inbox_id(create_inbox.nonce).unwrap();
160
161 let add_address = UnsignedAddAssociation {
162 new_member_identifier: MemberIdentifier::eth(&new_member_address).unwrap(),
163 };
164
165 let add_installation = UnsignedAddAssociation {
166 new_member_identifier: MemberIdentifier::installation(new_installation_id.clone()),
167 };
168
169 let revoke_address = UnsignedRevokeAssociation {
170 revoked_member: MemberIdentifier::eth(new_member_address).unwrap(),
171 };
172
173 let revoke_installation = UnsignedRevokeAssociation {
174 revoked_member: MemberIdentifier::installation(new_installation_id.clone()),
175 };
176
177 let change_recovery_address = UnsignedChangeRecoveryAddress {
178 new_recovery_identifier: new_recovery_identifier.clone(),
179 };
180
181 let identity_update = UnsignedIdentityUpdate {
182 inbox_id: inbox_id.clone(),
183 client_timestamp_ns,
184 actions: vec![
185 UnsignedAction::CreateInbox(create_inbox.clone()),
186 UnsignedAction::AddAssociation(add_address.clone()),
187 UnsignedAction::AddAssociation(add_installation.clone()),
188 UnsignedAction::RevokeAssociation(revoke_address.clone()),
189 UnsignedAction::RevokeAssociation(revoke_installation.clone()),
190 UnsignedAction::ChangeRecoveryAddress(change_recovery_address.clone()),
191 ],
192 };
193 let signature_text = identity_update.signature_text();
194 let expected_text = "XMTP : Authenticate to inbox
195
196Inbox ID: fcd18d86276d7a99fe522dba9660c420f03c8648785ada7c5daae232a3df77a9
197Current time: 1970-01-01T00:00:00Z
198
199- Create inbox
200 (Owner: 0x1234567890abcdef1234567890abcdef12345678)
201- Link address to inbox
202 (Address: 0x4567890abcdef1234567890abcdef12345678123)
203- Grant messaging access to app
204 (ID: 010203)
205- Unlink address from inbox
206 (Address: 0x4567890abcdef1234567890abcdef12345678123)
207- Revoke messaging access from app
208 (ID: 010203)
209- Change inbox recovery address
210 (Address: 0x7890abcdef1234567890abcdef12345678123456)
211
212For more info: https://xmtp.org/signatures";
213 assert_eq!(signature_text, expected_text)
214 }
215}