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 #[cfg(target_arch = "wasm32")]
143 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_dedicated_worker);
144
145 use super::*;
146
147 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
148 #[cfg_attr(not(target_arch = "wasm32"), test)]
149 fn create_signatures() {
150 let account_identifier =
151 Identifier::eth("0x1234567890abcdef1234567890abcdef12345678").unwrap();
152
153 let client_timestamp_ns: u64 = 12;
154 let new_member_address = "0x4567890abcdef1234567890abcdef12345678123".to_string();
155 let new_recovery_identifier =
156 Identifier::eth("0x7890abcdef1234567890abcdef12345678123456").unwrap();
157 let new_installation_id = vec![1, 2, 3];
158 let create_inbox = UnsignedCreateInbox {
159 nonce: 0,
160 account_identifier: account_identifier.clone(),
161 };
162 let inbox_id = account_identifier.inbox_id(create_inbox.nonce).unwrap();
163
164 let add_address = UnsignedAddAssociation {
165 new_member_identifier: MemberIdentifier::eth(&new_member_address).unwrap(),
166 };
167
168 let add_installation = UnsignedAddAssociation {
169 new_member_identifier: MemberIdentifier::installation(new_installation_id.clone()),
170 };
171
172 let revoke_address = UnsignedRevokeAssociation {
173 revoked_member: MemberIdentifier::eth(new_member_address).unwrap(),
174 };
175
176 let revoke_installation = UnsignedRevokeAssociation {
177 revoked_member: MemberIdentifier::installation(new_installation_id.clone()),
178 };
179
180 let change_recovery_address = UnsignedChangeRecoveryAddress {
181 new_recovery_identifier: new_recovery_identifier.clone(),
182 };
183
184 let identity_update = UnsignedIdentityUpdate {
185 inbox_id: inbox_id.clone(),
186 client_timestamp_ns,
187 actions: vec![
188 UnsignedAction::CreateInbox(create_inbox.clone()),
189 UnsignedAction::AddAssociation(add_address.clone()),
190 UnsignedAction::AddAssociation(add_installation.clone()),
191 UnsignedAction::RevokeAssociation(revoke_address.clone()),
192 UnsignedAction::RevokeAssociation(revoke_installation.clone()),
193 UnsignedAction::ChangeRecoveryAddress(change_recovery_address.clone()),
194 ],
195 };
196 let signature_text = identity_update.signature_text();
197 let expected_text = "XMTP : Authenticate to inbox
198
199Inbox ID: fcd18d86276d7a99fe522dba9660c420f03c8648785ada7c5daae232a3df77a9
200Current time: 1970-01-01T00:00:00Z
201
202- Create inbox
203 (Owner: 0x1234567890abcdef1234567890abcdef12345678)
204- Link address to inbox
205 (Address: 0x4567890abcdef1234567890abcdef12345678123)
206- Grant messaging access to app
207 (ID: 010203)
208- Unlink address from inbox
209 (Address: 0x4567890abcdef1234567890abcdef12345678123)
210- Revoke messaging access from app
211 (ID: 010203)
212- Change inbox recovery address
213 (Address: 0x7890abcdef1234567890abcdef12345678123456)
214
215For more info: https://xmtp.org/signatures";
216 assert_eq!(signature_text, expected_text)
217 }
218}