Skip to main content

chainstream_sdk/
wallet_auth.rs

1//! Authentication middleware for ChainStream API.
2//!
3//! Supports three modes:
4//! - API Key: `X-API-KEY` header (via `ApiKeyAuthMiddleware`)
5//! - Wallet signature: `X-Wallet-*` headers for x402 (via `WalletAuthMiddleware`)
6//! - Bearer token: `Authorization: Bearer` header (via `BearerAuthMiddleware` in tests)
7
8use async_trait::async_trait;
9use rand::Rng;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12const SIGNATURE_PREFIX: &str = "chainstream";
13
14/// Trait for wallet signers (EVM or Solana).
15#[async_trait]
16pub trait WalletSigner: Send + Sync {
17    /// Returns "evm" or "solana".
18    fn chain(&self) -> &str;
19    /// Returns the wallet address (hex for EVM, Base58 for Solana).
20    fn address(&self) -> &str;
21    /// Sign a message and return the signature string.
22    async fn sign_message(&self, message: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>>;
23}
24
25/// Build the message that must be signed for wallet auth.
26pub 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
35/// Wallet auth headers for HTTP requests.
36pub struct WalletAuthHeaders {
37    pub address: String,
38    pub chain: String,
39    pub signature: String,
40    pub timestamp: String,
41    pub nonce: String,
42}
43
44/// Generate X-Wallet-* headers for a request.
45pub 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(), &timestamp, &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
65/// Middleware for reqwest that adds X-Wallet-* headers.
66pub struct WalletAuthMiddleware {
67    signer: Box<dyn WalletSigner>,
68}
69
70impl WalletAuthMiddleware {
71    pub fn new(signer: Box<dyn WalletSigner>) -> Self {
72        Self { signer }
73    }
74}
75
76#[async_trait]
77impl reqwest_middleware::Middleware for WalletAuthMiddleware {
78    async fn handle(
79        &self,
80        mut req: reqwest::Request,
81        extensions: &mut http::Extensions,
82        next: reqwest_middleware::Next<'_>,
83    ) -> reqwest_middleware::Result<reqwest::Response> {
84        let headers = create_wallet_auth_headers(self.signer.as_ref())
85            .await
86            .map_err(|e| reqwest_middleware::Error::Middleware(anyhow::anyhow!("{e}")))?;
87
88        let h = req.headers_mut();
89        h.insert("X-Wallet-Address", headers.address.parse().unwrap());
90        h.insert("X-Wallet-Chain", headers.chain.parse().unwrap());
91        h.insert("X-Wallet-Signature", headers.signature.parse().unwrap());
92        h.insert("X-Wallet-Timestamp", headers.timestamp.parse().unwrap());
93        h.insert("X-Wallet-Nonce", headers.nonce.parse().unwrap());
94
95        next.run(req, extensions).await
96    }
97}
98
99/// Middleware for reqwest that adds X-API-KEY header.
100pub struct ApiKeyAuthMiddleware {
101    api_key: String,
102}
103
104impl ApiKeyAuthMiddleware {
105    pub fn new(api_key: impl Into<String>) -> Self {
106        Self {
107            api_key: api_key.into(),
108        }
109    }
110}
111
112#[async_trait]
113impl reqwest_middleware::Middleware for ApiKeyAuthMiddleware {
114    async fn handle(
115        &self,
116        mut req: reqwest::Request,
117        extensions: &mut http::Extensions,
118        next: reqwest_middleware::Next<'_>,
119    ) -> reqwest_middleware::Result<reqwest::Response> {
120        req.headers_mut()
121            .insert("X-API-KEY", self.api_key.parse().unwrap());
122        next.run(req, extensions).await
123    }
124}