Skip to main content

rustauth_plugins/siwe/
types.rs

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}