1use std::future::Future;
8use std::sync::Arc;
9
10use axum_core::body::Body;
11use http::Request;
12use r402_extensions::siwx::{
13 EvmVerifier, PaidAddressStore, SIWX_KEY, SiwxChain, SiwxError, SiwxExtension, SiwxOrigin,
14 SiwxProof, SiwxProofError,
15};
16use r402_protocol::payment::ExtensionEntry;
17use time::OffsetDateTime;
18
19use super::hooks::{GateHooks, ProtectedRequestOutcome};
20use crate::headers::SIGN_IN_WITH_X;
21
22pub struct SiwxGate {
24 extension: SiwxExtension,
25 store: Arc<dyn PaidAddressStore>,
26 auth_only: bool,
27 evm: EvmVerifier,
28}
29
30impl Clone for SiwxGate {
31 #[cfg_attr(
32 not(feature = "siwx-eip1271"),
33 allow(
34 clippy::clone_on_copy,
35 reason = "EvmVerifier is Copy only without eip1271"
36 )
37 )]
38 fn clone(&self) -> Self {
39 Self {
40 extension: self.extension.clone(),
41 store: Arc::clone(&self.store),
42 auth_only: self.auth_only,
43 evm: self.evm.clone(),
44 }
45 }
46}
47
48impl std::fmt::Debug for SiwxGate {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("SiwxGate")
51 .field("origin", self.extension.origin())
52 .field("auth_only", &self.auth_only)
53 .finish_non_exhaustive()
54 }
55}
56
57impl SiwxGate {
58 #[must_use]
62 pub fn new(origin: SiwxOrigin, store: impl PaidAddressStore + 'static) -> Self {
63 Self {
64 extension: SiwxExtension::new(origin),
65 store: Arc::new(store),
66 auth_only: false,
67 evm: EvmVerifier::new(),
68 }
69 }
70
71 #[must_use]
73 pub fn with_chain(mut self, chain: SiwxChain) -> Self {
74 self.extension = self.extension.with_chain(chain);
75 self
76 }
77
78 #[must_use]
80 pub fn with_statement(mut self, statement: impl Into<compact_str::CompactString>) -> Self {
81 self.extension = self.extension.with_statement(statement);
82 self
83 }
84
85 #[must_use]
87 pub const fn with_auth_only(mut self) -> Self {
88 self.auth_only = true;
89 self
90 }
91
92 #[cfg(feature = "siwx-eip1271")]
97 #[must_use]
98 pub fn with_evm_rpc_map(
99 mut self,
100 map: impl IntoIterator<Item = (u64, impl Into<String>)>,
101 timeout: Option<std::time::Duration>,
102 ) -> Self {
103 let mut evm = EvmVerifier::with_rpc_map(map);
104 if let Some(timeout) = timeout {
105 evm = evm.with_rpc_timeout(timeout);
106 }
107 self.evm = evm;
108 self
109 }
110
111 #[must_use]
113 pub const fn is_auth_only(&self) -> bool {
114 self.auth_only
115 }
116
117 #[must_use]
119 pub const fn origin(&self) -> &SiwxOrigin {
120 self.extension.origin()
121 }
122
123 #[must_use]
125 pub fn store(&self) -> &dyn PaidAddressStore {
126 self.store.as_ref()
127 }
128
129 pub fn challenge_entry(&self, path: &str) -> Result<ExtensionEntry, SiwxError> {
135 self.extension.challenge_now(path)
136 }
137
138 pub fn record_success(&self, path: &str, address: &str) {
140 let key = self.extension.origin().store_key(path);
141 self.store.record_success(&key, address);
142 }
143
144 #[must_use]
146 pub const fn key() -> &'static str {
147 SIWX_KEY
148 }
149
150 pub(crate) async fn try_grant(&self, header: &str, path: &str) -> bool {
151 let proof = match SiwxProof::parse_header(header) {
152 Ok(proof) => proof,
153 Err(err) => return log_parse_denied(err),
154 };
155 if let Err(err) = proof
156 .verify_at(
157 self.extension.origin(),
158 path,
159 OffsetDateTime::now_utc(),
160 &self.evm,
161 )
162 .await
163 {
164 return log_denied(err);
165 }
166 let key = self.extension.origin().store_key(path);
167 if !(self.auth_only || self.store.contains(&key, &proof.address)) {
168 return log_valid_unpaid();
169 }
170 if !self.store.consume_nonce(&proof.nonce) {
173 return log_nonce_replay();
174 }
175 log_granted()
176 }
177}
178
179fn log_parse_denied(err: SiwxProofError) -> bool {
180 tracing::debug!(error = ?err, "siwx parse denied");
181 false
182}
183
184fn log_denied(err: SiwxError) -> bool {
185 tracing::debug!(code = err.as_str(), "siwx denied");
186 false
187}
188
189fn log_valid_unpaid() -> bool {
190 tracing::debug!("siwx valid unpaid");
191 false
192}
193
194fn log_nonce_replay() -> bool {
195 tracing::debug!("siwx nonce replay");
196 false
197}
198
199fn log_granted() -> bool {
200 tracing::debug!("siwx granted");
201 true
202}
203
204impl GateHooks for SiwxGate {
205 fn on_protected_request<'a>(
206 &'a self,
207 req: &'a Request<Body>,
208 ) -> impl Future<Output = ProtectedRequestOutcome> + Send + 'a {
209 let header = req
210 .headers()
211 .get(SIGN_IN_WITH_X)
212 .and_then(|v| v.to_str().ok())
213 .map(str::to_owned);
214 let path = req.uri().path().to_owned();
215 async move {
216 let Some(header) = header else {
217 return ProtectedRequestOutcome::Continue;
218 };
219 if self.try_grant(&header, &path).await {
220 ProtectedRequestOutcome::GrantAccess
221 } else {
222 ProtectedRequestOutcome::Continue
223 }
224 }
225 }
226}