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