1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use openauth_core::error::OpenAuthError;
6use serde::{Deserialize, Serialize};
7use time::OffsetDateTime;
8
9use super::schema::SiweSchemaOptions;
10
11type BoxFuture<T> = Pin<Box<dyn Future<Output = Result<T, OpenAuthError>> + 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, OpenAuthError>> + Send + 'static,
33 V: Fn(SiweVerifyMessageArgs) -> VFut + Send + Sync + 'static,
34 VFut: Future<Output = Result<bool, OpenAuthError>> + 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 email_domain_name(mut self, domain: impl Into<String>) -> Self {
49 self.email_domain_name = Some(domain.into());
50 self
51 }
52
53 #[must_use]
54 pub fn anonymous(mut self, anonymous: bool) -> Self {
55 self.anonymous = anonymous;
56 self
57 }
58
59 #[must_use]
60 pub fn ens_lookup<E, EFut>(mut self, ens_lookup: E) -> Self
61 where
62 E: Fn(EnsLookupArgs) -> EFut + Send + Sync + 'static,
63 EFut: Future<Output = Result<Option<EnsLookupResult>, OpenAuthError>> + Send + 'static,
64 {
65 self.ens_lookup = Some(Arc::new(move |args| Box::pin(ens_lookup(args))));
66 self
67 }
68
69 #[must_use]
70 pub fn schema(mut self, schema: SiweSchemaOptions) -> Self {
71 self.schema = schema;
72 self
73 }
74
75 pub(crate) fn schema_options(&self) -> &SiweSchemaOptions {
76 &self.schema
77 }
78
79 pub(crate) fn validate(&self) -> Result<(), OpenAuthError> {
80 if self.domain.trim().is_empty() {
81 return Err(OpenAuthError::InvalidConfig(
82 "siwe domain cannot be empty".to_owned(),
83 ));
84 }
85 Ok(())
86 }
87
88 pub(crate) fn metadata(&self) -> serde_json::Value {
89 let mut metadata = serde_json::json!({
90 "domain": self.domain,
91 "anonymous": self.anonymous,
92 "schema": self.schema.metadata(),
93 });
94 if let Some(email_domain_name) = &self.email_domain_name {
95 metadata["emailDomainName"] = serde_json::Value::String(email_domain_name.clone());
96 }
97 metadata
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct WalletAddress {
103 pub id: String,
104 pub user_id: String,
105 pub address: String,
106 pub chain_id: i64,
107 pub is_primary: bool,
108 pub created_at: OffsetDateTime,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
112pub struct SiweVerifyMessageArgs {
113 pub message: String,
114 pub signature: String,
115 pub address: String,
116 pub chain_id: i64,
117 pub cacao: Cacao,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
121pub struct Cacao {
122 pub h: CacaoHeader,
123 pub p: CacaoPayload,
124 pub s: CacaoSignature,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
128pub struct CacaoHeader {
129 pub t: String,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
133pub struct CacaoPayload {
134 pub domain: String,
135 pub aud: String,
136 pub nonce: String,
137 pub iss: String,
138 pub version: Option<String>,
139 pub iat: Option<String>,
140 pub nbf: Option<String>,
141 pub exp: Option<String>,
142 pub statement: Option<String>,
143 pub request_id: Option<String>,
144 pub resources: Option<Vec<String>>,
145 pub r#type: Option<String>,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
149pub struct CacaoSignature {
150 pub t: String,
151 pub s: String,
152 pub m: Option<String>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct EnsLookupArgs {
157 pub wallet_address: String,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct EnsLookupResult {
162 pub name: String,
163 pub avatar: String,
164}
165
166#[derive(Debug, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub(crate) struct NonceRequest {
169 pub wallet_address: String,
170 #[serde(default = "default_chain_id")]
171 pub chain_id: i64,
172}
173
174#[derive(Debug, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub(crate) struct VerifyRequest {
177 pub message: String,
178 pub signature: String,
179 pub wallet_address: String,
180 #[serde(default = "default_chain_id")]
181 pub chain_id: i64,
182 pub email: Option<String>,
183}
184
185fn default_chain_id() -> i64 {
186 1
187}