Skip to main content

chainstream_sdk/
wallet_auth.rs

1//! Wallet signature authentication for x402 payment access.
2//!
3//! Agents sign each API request with their wallet (EVM or Solana).
4//! The server verifies the signature and checks the payment record.
5
6use async_trait::async_trait;
7use rand::Rng;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10const SIGNATURE_PREFIX: &str = "chainstream";
11
12/// Trait for wallet signers (EVM or Solana).
13#[async_trait]
14pub trait WalletSigner: Send + Sync {
15    /// Returns "evm" or "solana".
16    fn chain(&self) -> &str;
17    /// Returns the wallet address (hex for EVM, Base58 for Solana).
18    fn address(&self) -> &str;
19    /// Sign a message and return the signature string.
20    async fn sign_message(&self, message: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>>;
21}
22
23/// Build the message that must be signed for wallet auth.
24pub fn build_sign_message(chain: &str, address: &str, timestamp: &str, nonce: &str) -> String {
25    format!("{SIGNATURE_PREFIX}:{chain}:{address}:{timestamp}:{nonce}")
26}
27
28fn generate_nonce() -> String {
29    let bytes: [u8; 16] = rand::thread_rng().gen();
30    hex::encode(bytes)
31}
32
33/// Wallet auth headers for HTTP requests.
34pub struct WalletAuthHeaders {
35    pub address: String,
36    pub chain: String,
37    pub signature: String,
38    pub timestamp: String,
39    pub nonce: String,
40}
41
42/// Generate X-Wallet-* headers for a request.
43pub async fn create_wallet_auth_headers(
44    signer: &dyn WalletSigner,
45) -> Result<WalletAuthHeaders, Box<dyn std::error::Error + Send + Sync>> {
46    let timestamp = SystemTime::now()
47        .duration_since(UNIX_EPOCH)?
48        .as_secs()
49        .to_string();
50    let nonce = generate_nonce();
51    let message = build_sign_message(signer.chain(), signer.address(), &timestamp, &nonce);
52    let signature = signer.sign_message(&message).await?;
53
54    Ok(WalletAuthHeaders {
55        address: signer.address().to_string(),
56        chain: signer.chain().to_string(),
57        signature,
58        timestamp,
59        nonce,
60    })
61}
62
63/// Middleware for reqwest that adds X-Wallet-* headers.
64pub struct WalletAuthMiddleware {
65    signer: Box<dyn WalletSigner>,
66}
67
68impl WalletAuthMiddleware {
69    pub fn new(signer: Box<dyn WalletSigner>) -> Self {
70        Self { signer }
71    }
72}
73
74#[async_trait]
75impl reqwest_middleware::Middleware for WalletAuthMiddleware {
76    async fn handle(
77        &self,
78        mut req: reqwest::Request,
79        extensions: &mut http::Extensions,
80        next: reqwest_middleware::Next<'_>,
81    ) -> reqwest_middleware::Result<reqwest::Response> {
82        let headers = create_wallet_auth_headers(self.signer.as_ref())
83            .await
84            .map_err(|e| reqwest_middleware::Error::Middleware(anyhow::anyhow!("{e}")))?;
85
86        let h = req.headers_mut();
87        h.insert("X-Wallet-Address", headers.address.parse().unwrap());
88        h.insert("X-Wallet-Chain", headers.chain.parse().unwrap());
89        h.insert("X-Wallet-Signature", headers.signature.parse().unwrap());
90        h.insert("X-Wallet-Timestamp", headers.timestamp.parse().unwrap());
91        h.insert("X-Wallet-Nonce", headers.nonce.parse().unwrap());
92
93        next.run(req, extensions).await
94    }
95}