1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use rustauth_core::error::RustAuthError;
6use serde::{Deserialize, Serialize};
7use time::OffsetDateTime;
8
9use super::schema::SiweSchemaOptions;
10
11type BoxFuture<T> = Pin<Box<dyn Future<Output = Result<T, RustAuthError>> + Send>>;
12
13pub type GetNonce = Arc<dyn Fn() -> BoxFuture<String> + Send + Sync>;
14pub type VerifyMessage = Arc<dyn Fn(SiweVerifyMessageArgs) -> BoxFuture<bool> + Send + Sync>;
15pub type EnsLookup = Arc<dyn Fn(EnsLookupArgs) -> BoxFuture<Option<EnsLookupResult>> + Send + Sync>;
16
17#[derive(Clone)]
18pub struct SiweOptions {
19 pub(crate) domain: String,
20 pub(crate) email_domain_name: Option<String>,
21 pub(crate) anonymous: bool,
22 pub(crate) get_nonce: GetNonce,
23 pub(crate) verify_message: VerifyMessage,
24 pub(crate) ens_lookup: Option<EnsLookup>,
25 pub(crate) schema: SiweSchemaOptions,
26}
27
28impl SiweOptions {
29 pub fn new<G, GFut, V, VFut>(domain: impl Into<String>, get_nonce: G, verify_message: V) -> Self
30 where
31 G: Fn() -> GFut + Send + Sync + 'static,
32 GFut: Future<Output = Result<String, RustAuthError>> + Send + 'static,
33 V: Fn(SiweVerifyMessageArgs) -> VFut + Send + Sync + 'static,
34 VFut: Future<Output = Result<bool, RustAuthError>> + Send + 'static,
35 {
36 Self {
37 domain: domain.into(),
38 email_domain_name: None,
39 anonymous: true,
40 get_nonce: Arc::new(move || Box::pin(get_nonce())),
41 verify_message: Arc::new(move |args| Box::pin(verify_message(args))),
42 ens_lookup: None,
43 schema: SiweSchemaOptions::new(),
44 }
45 }
46
47 #[must_use]
48 pub fn builder() -> SiweOptionsBuilder {
49 SiweOptionsBuilder::default()
50 }
51
52 #[must_use]
53 pub fn email_domain_name(mut self, domain: impl Into<String>) -> Self {
54 self.email_domain_name = Some(domain.into());
55 self
56 }
57
58 #[must_use]
59 pub fn anonymous(mut self, anonymous: bool) -> Self {
60 self.anonymous = anonymous;
61 self
62 }
63
64 #[must_use]
65 pub fn ens_lookup<E, EFut>(mut self, ens_lookup: E) -> Self
66 where
67 E: Fn(EnsLookupArgs) -> EFut + Send + Sync + 'static,
68 EFut: Future<Output = Result<Option<EnsLookupResult>, RustAuthError>> + Send + 'static,
69 {
70 self.ens_lookup = Some(Arc::new(move |args| Box::pin(ens_lookup(args))));
71 self
72 }
73
74 #[must_use]
75 pub fn schema(mut self, schema: SiweSchemaOptions) -> Self {
76 self.schema = schema;
77 self
78 }
79
80 pub(crate) fn schema_options(&self) -> &SiweSchemaOptions {
81 &self.schema
82 }
83
84 pub(crate) fn validate(&self) -> Result<(), RustAuthError> {
85 if self.domain.trim().is_empty() {
86 return Err(RustAuthError::InvalidConfig(
87 "siwe domain cannot be empty".to_owned(),
88 ));
89 }
90 Ok(())
91 }
92
93 pub(crate) fn metadata(&self) -> serde_json::Value {
94 let mut metadata = serde_json::json!({
95 "domain": self.domain,
96 "anonymous": self.anonymous,
97 "schema": self.schema.metadata(),
98 });
99 if let Some(email_domain_name) = &self.email_domain_name {
100 metadata["emailDomainName"] = serde_json::Value::String(email_domain_name.clone());
101 }
102 metadata
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct WalletAddress {
108 pub id: String,
109 pub user_id: String,
110 pub address: String,
111 pub chain_id: i64,
112 pub is_primary: bool,
113 pub created_at: OffsetDateTime,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
117pub struct SiweVerifyMessageArgs {
118 pub message: String,
119 pub signature: String,
120 pub address: String,
121 pub chain_id: i64,
122 pub cacao: Cacao,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
126pub struct Cacao {
127 pub h: CacaoHeader,
128 pub p: CacaoPayload,
129 pub s: CacaoSignature,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
133pub struct CacaoHeader {
134 pub t: String,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
138pub struct CacaoPayload {
139 pub domain: String,
140 pub aud: String,
141 pub nonce: String,
142 pub iss: String,
143 pub version: Option<String>,
144 pub iat: Option<String>,
145 pub nbf: Option<String>,
146 pub exp: Option<String>,
147 pub statement: Option<String>,
148 pub request_id: Option<String>,
149 pub resources: Option<Vec<String>>,
150 pub r#type: Option<String>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
154pub struct CacaoSignature {
155 pub t: String,
156 pub s: String,
157 pub m: Option<String>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct EnsLookupArgs {
162 pub wallet_address: String,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct EnsLookupResult {
167 pub name: String,
168 pub avatar: String,
169}
170
171#[derive(Debug, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub(crate) struct NonceRequest {
174 pub wallet_address: String,
175 #[serde(default = "default_chain_id")]
176 pub chain_id: i64,
177}
178
179#[derive(Debug, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub(crate) struct VerifyRequest {
182 pub message: String,
183 pub signature: String,
184 pub wallet_address: String,
185 #[serde(default = "default_chain_id")]
186 pub chain_id: i64,
187 pub email: Option<String>,
188}
189
190fn default_chain_id() -> i64 {
191 1
192}
193
194#[derive(Clone, Default)]
195pub struct SiweOptionsBuilder {
196 domain: Option<String>,
197 email_domain_name: Option<Option<String>>,
198 anonymous: Option<bool>,
199 get_nonce: Option<GetNonce>,
200 verify_message: Option<VerifyMessage>,
201 ens_lookup: Option<EnsLookup>,
202 schema: Option<SiweSchemaOptions>,
203}
204
205impl SiweOptionsBuilder {
206 #[must_use]
207 pub fn domain(mut self, domain: impl Into<String>) -> Self {
208 self.domain = Some(domain.into());
209 self
210 }
211
212 #[must_use]
213 pub fn email_domain_name(mut self, domain: impl Into<String>) -> Self {
214 self.email_domain_name = Some(Some(domain.into()));
215 self
216 }
217
218 #[must_use]
219 pub fn anonymous(mut self, anonymous: bool) -> Self {
220 self.anonymous = Some(anonymous);
221 self
222 }
223
224 #[must_use]
225 pub fn get_nonce(mut self, get_nonce: GetNonce) -> Self {
226 self.get_nonce = Some(get_nonce);
227 self
228 }
229
230 #[must_use]
231 pub fn verify_message(mut self, verify_message: VerifyMessage) -> Self {
232 self.verify_message = Some(verify_message);
233 self
234 }
235
236 #[must_use]
237 pub fn ens_lookup(mut self, ens_lookup: EnsLookup) -> Self {
238 self.ens_lookup = Some(ens_lookup);
239 self
240 }
241
242 #[must_use]
243 pub fn schema(mut self, schema: SiweSchemaOptions) -> Self {
244 self.schema = Some(schema);
245 self
246 }
247
248 pub fn build(self) -> Result<SiweOptions, RustAuthError> {
249 let domain = self
250 .domain
251 .ok_or_else(|| RustAuthError::InvalidConfig("siwe domain is required".to_owned()))?;
252 let get_nonce = self.get_nonce.ok_or_else(|| {
253 RustAuthError::InvalidConfig("siwe get_nonce callback is required".to_owned())
254 })?;
255 let verify_message = self.verify_message.ok_or_else(|| {
256 RustAuthError::InvalidConfig("siwe verify_message callback is required".to_owned())
257 })?;
258 let options = SiweOptions {
259 domain,
260 email_domain_name: self.email_domain_name.unwrap_or(None),
261 anonymous: self.anonymous.unwrap_or(true),
262 get_nonce,
263 verify_message,
264 ens_lookup: self.ens_lookup,
265 schema: self.schema.unwrap_or_default(),
266 };
267 options.validate()?;
268 Ok(options)
269 }
270}