bsv-payment-actix-middleware 0.1.0

BSV payment middleware for Actix-web, wire-compatible with the TypeScript payment-express-middleware
Documentation
//! Wire-compatible payment types matching the TypeScript payment-express-middleware.

use serde::{Deserialize, Serialize};

/// Payment protocol version.
pub const PAYMENT_VERSION: &str = "1.0";

/// Default price in satoshis when no callback is provided.
pub const DEFAULT_SATOSHIS: u64 = 100;

/// Payment data from the `X-BSV-Payment` request header.
///
/// Wire format uses camelCase to match the TypeScript middleware.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BSVPayment {
    /// Key derivation prefix used for the payment output script.
    pub derivation_prefix: String,
    /// Key derivation suffix used for the payment output script.
    pub derivation_suffix: String,
    /// The raw BSV transaction (opaque JSON value to match TS `unknown`).
    pub transaction: serde_json::Value,
}

/// Result of payment processing, inserted into request extensions by the middleware.
#[derive(Clone, Debug)]
pub struct PaymentInfo {
    /// Amount paid in satoshis.
    pub satoshis_paid: u64,
    /// Whether the payment was accepted by the wallet.
    pub accepted: Option<bool>,
    /// The raw transaction data after internalization.
    pub tx: Option<serde_json::Value>,
}

/// Type alias for the async price calculation callback.
///
/// Receives a reference to the incoming request and returns the price in satoshis.
/// Used by the middleware to determine how much a request costs.
pub type CalculateRequestPrice = Box<
    dyn Fn(
            &actix_web::dev::ServiceRequest,
        ) -> futures_util::future::BoxFuture<
            'static,
            Result<u64, Box<dyn std::error::Error + Send + Sync>>,
        > + Send
        + Sync,
>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_bsv_payment_deserialize_camel_case() {
        let json = r#"{"derivationPrefix":"abc","derivationSuffix":"def","transaction":"tx"}"#;
        let payment: BSVPayment = serde_json::from_str(json).unwrap();
        assert_eq!(payment.derivation_prefix, "abc");
        assert_eq!(payment.derivation_suffix, "def");
        assert_eq!(payment.transaction, serde_json::json!("tx"));
    }

    #[test]
    fn test_bsv_payment_serialize_camel_case() {
        let payment = BSVPayment {
            derivation_prefix: "abc".to_string(),
            derivation_suffix: "def".to_string(),
            transaction: serde_json::json!("tx"),
        };
        let json = serde_json::to_string(&payment).unwrap();
        assert!(json.contains("derivationPrefix"));
        assert!(json.contains("derivationSuffix"));
        assert!(json.contains("transaction"));
        assert!(!json.contains("derivation_prefix"));
        assert!(!json.contains("derivation_suffix"));
    }

    #[test]
    fn test_bsv_payment_round_trip() {
        let original = BSVPayment {
            derivation_prefix: "prefix123".to_string(),
            derivation_suffix: "suffix456".to_string(),
            transaction: serde_json::json!({"hex": "deadbeef"}),
        };
        let json = serde_json::to_string(&original).unwrap();
        let deserialized: BSVPayment = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.derivation_prefix, original.derivation_prefix);
        assert_eq!(deserialized.derivation_suffix, original.derivation_suffix);
        assert_eq!(deserialized.transaction, original.transaction);
    }

    #[test]
    fn test_payment_version_constant() {
        assert_eq!(PAYMENT_VERSION, "1.0");
    }

    #[test]
    fn test_default_satoshis_constant() {
        assert_eq!(DEFAULT_SATOSHIS, 100);
    }

    #[test]
    fn test_payment_info_struct() {
        let info = PaymentInfo {
            satoshis_paid: 200,
            accepted: Some(true),
            tx: Some(serde_json::json!({"txid": "abc"})),
        };
        assert_eq!(info.satoshis_paid, 200);
        assert_eq!(info.accepted, Some(true));
        assert!(info.tx.is_some());
    }

    #[test]
    fn test_payment_info_clone() {
        let info = PaymentInfo {
            satoshis_paid: 100,
            accepted: None,
            tx: None,
        };
        let cloned = info.clone();
        assert_eq!(cloned.satoshis_paid, 100);
        assert_eq!(cloned.accepted, None);
        assert!(cloned.tx.is_none());
    }
}