use std::future::{ready, Ready};
use std::panic::AssertUnwindSafe;
use std::rc::Rc;
use std::sync::Arc;
use actix_web::body::{EitherBody, MessageBody};
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::error::ResponseError;
use actix_web::http::header::{HeaderName, HeaderValue};
use actix_web::{Error, HttpMessage, HttpResponse};
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use futures_util::future::LocalBoxFuture;
use futures_util::FutureExt;
use tracing::{debug, error, warn};
use bsv::auth::utils::{create_nonce, verify_nonce};
use bsv::primitives::public_key::PublicKey;
use bsv::wallet::interfaces::{
InternalizeActionArgs, InternalizeOutput, InternalizeProtocol, Payment, WalletInterface,
};
use crate::config::PaymentMiddlewareConfig;
use crate::error::PaymentError;
use crate::extractor::AuthIdentity;
use crate::types::{BSVPayment, PaymentInfo, DEFAULT_SATOSHIS, PAYMENT_VERSION};
enum PanicOrError<E> {
Error(E),
Panic(String),
}
async fn catch_wallet_panic<F, T, E>(operation: &str, fut: F) -> Result<T, PanicOrError<E>>
where
F: std::future::Future<Output = Result<T, E>>,
{
match AssertUnwindSafe(fut).catch_unwind().await {
Ok(Ok(val)) => Ok(val),
Ok(Err(e)) => Err(PanicOrError::Error(e)),
Err(panic_payload) => {
let msg = panic_message(&panic_payload);
error!(operation, panic_message = %msg, "wallet call panicked");
Err(PanicOrError::Panic(msg))
}
}
}
fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
}
}
pub struct PaymentMiddlewareFactory<W: WalletInterface> {
config: Arc<PaymentMiddlewareConfig<W>>,
}
impl<W: WalletInterface> PaymentMiddlewareFactory<W> {
pub fn new(config: PaymentMiddlewareConfig<W>) -> Self {
Self {
config: Arc::new(config),
}
}
}
impl<S, B, W> Transform<S, ServiceRequest> for PaymentMiddlewareFactory<W>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: MessageBody + 'static,
W: WalletInterface + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Transform = PaymentMiddlewareService<S, W>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(PaymentMiddlewareService {
service: Rc::new(service),
config: Arc::clone(&self.config),
}))
}
}
pub struct PaymentMiddlewareService<S, W: WalletInterface> {
service: Rc<S>,
config: Arc<PaymentMiddlewareConfig<W>>,
}
impl<S, B, W> Service<ServiceRequest> for PaymentMiddlewareService<S, W>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: MessageBody + 'static,
W: WalletInterface + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let service = Rc::clone(&self.service);
let config = Arc::clone(&self.config);
async move {
let identity = {
let ext = req.extensions();
ext.get::<AuthIdentity>().cloned()
};
let identity = match identity {
Some(id) => id,
None => {
let err = PaymentError::ServerMisconfigured;
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
};
debug!(identity_key = %identity.identity_key, "payment middleware processing request");
let request_price: u64 = if let Some(ref callback) = config.calculate_request_price {
let cb = Arc::clone(callback);
let price_result = catch_wallet_panic("calculate_request_price", async {
(cb)(&req).await.map_err(|e| e.to_string())
})
.await;
match price_result {
Ok(price) => price,
Err(PanicOrError::Error(e)) => {
warn!(
identity_key = %identity.identity_key,
error = %e,
"calculate_request_price callback failed"
);
let err = PaymentError::PaymentInternal;
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
Err(PanicOrError::Panic(_)) => {
let err = PaymentError::PaymentInternal;
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
}
} else {
DEFAULT_SATOSHIS
};
debug!(identity_key = %identity.identity_key, satoshis = request_price, "price calculated");
if request_price == 0 {
debug!(identity_key = %identity.identity_key, "zero price, passing through");
req.extensions_mut().insert(PaymentInfo {
satoshis_paid: 0,
accepted: None,
tx: None,
});
let res = service.call(req).await?;
return Ok(res.map_into_left_body());
}
let payment_header = req
.headers()
.get("x-bsv-payment")
.and_then(|v| v.to_str().ok().map(|s| s.to_string()));
if payment_header.is_none() {
let nonce = match catch_wallet_panic(
"create_nonce",
create_nonce(&config.wallet),
)
.await
{
Ok(n) => n,
Err(PanicOrError::Error(e)) => {
error!(
identity_key = %identity.identity_key,
error = %e,
"create_nonce failed"
);
let err = PaymentError::PaymentInternal;
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
Err(PanicOrError::Panic(_)) => {
let err = PaymentError::PaymentInternal;
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
};
debug!(
identity_key = %identity.identity_key,
satoshis = request_price,
"402 payment required"
);
let body = serde_json::json!({
"status": "error",
"code": "ERR_PAYMENT_REQUIRED",
"satoshisRequired": request_price,
"description": "A BSV payment is required to complete this request. Provide the X-BSV-Payment header."
});
let response = HttpResponse::PaymentRequired()
.insert_header(("x-bsv-payment-version", PAYMENT_VERSION))
.insert_header((
"x-bsv-payment-satoshis-required",
request_price.to_string(),
))
.insert_header(("x-bsv-payment-derivation-prefix", nonce))
.json(body);
return Ok(req.into_response(response).map_into_right_body());
}
let payment_header_str = payment_header.unwrap();
let payment: BSVPayment = match serde_json::from_str(&payment_header_str) {
Ok(p) => p,
Err(_) => {
warn!(
identity_key = %identity.identity_key,
"malformed X-BSV-Payment header"
);
let err = PaymentError::MalformedPayment;
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
};
let nonce_valid = match catch_wallet_panic(
"verify_nonce",
verify_nonce(&config.wallet, &payment.derivation_prefix),
)
.await
{
Ok(valid) => valid,
Err(PanicOrError::Error(e)) => {
warn!(
identity_key = %identity.identity_key,
error = %e,
"nonce verification failed"
);
let err = PaymentError::InvalidDerivationPrefix;
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
Err(PanicOrError::Panic(_)) => {
warn!(
identity_key = %identity.identity_key,
"nonce verification failed"
);
let err = PaymentError::InvalidDerivationPrefix;
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
};
if !nonce_valid {
warn!(
identity_key = %identity.identity_key,
"nonce verification failed"
);
let err = PaymentError::InvalidDerivationPrefix;
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
debug!(identity_key = %identity.identity_key, "nonce verified successfully");
let tx_base64 = match payment.transaction.as_str() {
Some(s) => s,
None => {
warn!(
identity_key = %identity.identity_key,
"malformed X-BSV-Payment header"
);
let err = PaymentError::MalformedPayment;
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
};
let tx_bytes = match BASE64.decode(tx_base64) {
Ok(bytes) => bytes,
Err(_) => {
warn!(
identity_key = %identity.identity_key,
"malformed X-BSV-Payment header"
);
let err = PaymentError::MalformedPayment;
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
};
let sender_key = match PublicKey::from_string(&identity.identity_key) {
Ok(key) => key,
Err(_) => {
warn!(
identity_key = %identity.identity_key,
"invalid sender identity key"
);
let err =
PaymentError::PaymentFailed("Invalid sender identity key".into());
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
};
let args = InternalizeActionArgs {
tx: tx_bytes,
description: "Payment for request".to_string(),
labels: vec![],
seek_permission: None,
outputs: vec![InternalizeOutput {
output_index: 0,
protocol: InternalizeProtocol::WalletPayment,
payment_remittance: Some(Payment {
derivation_prefix: payment.derivation_prefix.into_bytes(),
derivation_suffix: payment.derivation_suffix.into_bytes(),
sender_identity_key: sender_key,
}),
insertion_remittance: None,
}],
};
let result = match catch_wallet_panic(
"internalize_action",
config.wallet.internalize_action(args, None),
)
.await
{
Ok(r) => r,
Err(PanicOrError::Error(e)) => {
let desc = {
let s = e.to_string();
if s.is_empty() {
"Payment failed.".to_string()
} else {
s
}
};
warn!(
identity_key = %identity.identity_key,
error = %desc,
"payment internalization failed"
);
let err = PaymentError::PaymentFailed(desc);
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
Err(PanicOrError::Panic(msg)) => {
error!(
identity_key = %identity.identity_key,
panic_message = %msg,
"wallet.internalize_action panicked"
);
let err = PaymentError::PaymentFailed("Payment failed.".into());
return Ok(req
.into_response(err.error_response())
.map_into_right_body());
}
};
debug!(
identity_key = %identity.identity_key,
satoshis = request_price,
accepted = result.accepted,
"payment internalized"
);
req.extensions_mut().insert(PaymentInfo {
satoshis_paid: request_price,
accepted: Some(result.accepted),
tx: Some(payment.transaction),
});
let mut res = service.call(req).await?;
res.headers_mut().insert(
HeaderName::from_static("x-bsv-payment-satoshis-paid"),
HeaderValue::from_str(&request_price.to_string()).unwrap(),
);
Ok(res.map_into_left_body())
}
.boxed_local()
}
}