chainstream_sdk/
wallet_auth.rs1use async_trait::async_trait;
9use rand::Rng;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12const SIGNATURE_PREFIX: &str = "chainstream";
13
14#[async_trait]
16pub trait WalletSigner: Send + Sync {
17 fn chain(&self) -> &str;
19 fn address(&self) -> &str;
21 async fn sign_message(
23 &self,
24 message: &str,
25 ) -> Result<String, Box<dyn std::error::Error + Send + Sync>>;
26}
27
28pub fn build_sign_message(chain: &str, address: &str, timestamp: &str, nonce: &str) -> String {
30 format!("{SIGNATURE_PREFIX}:{chain}:{address}:{timestamp}:{nonce}")
31}
32
33fn generate_nonce() -> String {
34 let bytes: [u8; 16] = rand::thread_rng().gen();
35 hex::encode(bytes)
36}
37
38pub struct WalletAuthHeaders {
40 pub address: String,
41 pub chain: String,
42 pub signature: String,
43 pub timestamp: String,
44 pub nonce: String,
45}
46
47pub async fn create_wallet_auth_headers(
49 signer: &dyn WalletSigner,
50) -> Result<WalletAuthHeaders, Box<dyn std::error::Error + Send + Sync>> {
51 let timestamp = SystemTime::now()
52 .duration_since(UNIX_EPOCH)?
53 .as_secs()
54 .to_string();
55 let nonce = generate_nonce();
56 let message = build_sign_message(signer.chain(), signer.address(), ×tamp, &nonce);
57 let signature = signer.sign_message(&message).await?;
58
59 Ok(WalletAuthHeaders {
60 address: signer.address().to_string(),
61 chain: signer.chain().to_string(),
62 signature,
63 timestamp,
64 nonce,
65 })
66}
67
68pub fn build_siwx_message(
70 address: &str,
71 chain: &str,
72 nonce: &str,
73 issued_at: &str,
74 expires_at: &str,
75) -> String {
76 let chain_label = if chain == "solana" {
77 "Solana"
78 } else {
79 "Ethereum"
80 };
81 let chain_id = if chain == "solana" { "mainnet" } else { "8453" };
82
83 format!(
84 "{domain} wants you to sign in with your {chain_label} account:\n\
85 {address}\n\n\
86 Sign in to ChainStream API\n\n\
87 URI: https://{domain}\n\
88 Version: 1\n\
89 Chain ID: {chain_id}\n\
90 Nonce: {nonce}\n\
91 Issued At: {issued_at}\n\
92 Expiration Time: {expires_at}",
93 domain = "api.chainstream.io",
94 chain_label = chain_label,
95 address = address,
96 chain_id = chain_id,
97 nonce = nonce,
98 issued_at = issued_at,
99 expires_at = expires_at,
100 )
101}
102
103pub struct SiwxAuthMiddleware {
106 signer: Box<dyn WalletSigner>,
107}
108
109impl SiwxAuthMiddleware {
110 pub fn new(signer: Box<dyn WalletSigner>) -> Self {
111 Self { signer }
112 }
113}
114
115#[async_trait]
116impl reqwest_middleware::Middleware for SiwxAuthMiddleware {
117 async fn handle(
118 &self,
119 mut req: reqwest::Request,
120 extensions: &mut http::Extensions,
121 next: reqwest_middleware::Next<'_>,
122 ) -> reqwest_middleware::Result<reqwest::Response> {
123 let nonce = generate_nonce();
124 let now = chrono::Utc::now();
125 let expires_at = now + chrono::Duration::hours(1);
126 let issued_at_str = now.to_rfc3339();
127 let expires_at_str = expires_at.to_rfc3339();
128
129 let message = build_siwx_message(
130 self.signer.address(),
131 self.signer.chain(),
132 &nonce,
133 &issued_at_str,
134 &expires_at_str,
135 );
136
137 let signature = self
138 .signer
139 .sign_message(&message)
140 .await
141 .map_err(|e| reqwest_middleware::Error::Middleware(anyhow::anyhow!("{e}")))?;
142
143 use base64::Engine;
144 let message_b64 = base64::engine::general_purpose::STANDARD.encode(message.as_bytes());
145 let token = format!("{}.{}", message_b64, signature);
146
147 req.headers_mut()
148 .insert("Authorization", format!("SIWX {}", token).parse().unwrap());
149
150 next.run(req, extensions).await
151 }
152}
153
154pub struct WalletAuthMiddleware {
156 signer: Box<dyn WalletSigner>,
157}
158
159impl WalletAuthMiddleware {
160 pub fn new(signer: Box<dyn WalletSigner>) -> Self {
161 Self { signer }
162 }
163}
164
165#[async_trait]
166impl reqwest_middleware::Middleware for WalletAuthMiddleware {
167 async fn handle(
168 &self,
169 mut req: reqwest::Request,
170 extensions: &mut http::Extensions,
171 next: reqwest_middleware::Next<'_>,
172 ) -> reqwest_middleware::Result<reqwest::Response> {
173 let headers = create_wallet_auth_headers(self.signer.as_ref())
174 .await
175 .map_err(|e| reqwest_middleware::Error::Middleware(anyhow::anyhow!("{e}")))?;
176
177 let h = req.headers_mut();
178 h.insert("X-Wallet-Address", headers.address.parse().unwrap());
179 h.insert("X-Wallet-Chain", headers.chain.parse().unwrap());
180 h.insert("X-Wallet-Signature", headers.signature.parse().unwrap());
181 h.insert("X-Wallet-Timestamp", headers.timestamp.parse().unwrap());
182 h.insert("X-Wallet-Nonce", headers.nonce.parse().unwrap());
183
184 next.run(req, extensions).await
185 }
186}
187
188pub struct ApiKeyAuthMiddleware {
190 api_key: String,
191}
192
193impl ApiKeyAuthMiddleware {
194 pub fn new(api_key: impl Into<String>) -> Self {
195 Self {
196 api_key: api_key.into(),
197 }
198 }
199}
200
201#[async_trait]
202impl reqwest_middleware::Middleware for ApiKeyAuthMiddleware {
203 async fn handle(
204 &self,
205 mut req: reqwest::Request,
206 extensions: &mut http::Extensions,
207 next: reqwest_middleware::Next<'_>,
208 ) -> reqwest_middleware::Result<reqwest::Response> {
209 req.headers_mut()
210 .insert("X-API-KEY", self.api_key.parse().unwrap());
211 next.run(req, extensions).await
212 }
213}