use crate::payment::state_machine::{PaymentState, PaymentStateMachine};
use crate::rpc::payment::DEFAULT_SAFE_DEPTH;
use crate::rpc::rest::types::{
rest_error_failed, rest_error_invalid, ApiError, ApiResponse, ErrorDetails, ResponseMeta,
};
use blvm_protocol::payment::PaymentOutput;
use bytes::Bytes;
use http_body_util::Full;
use hyper::{Method, Response, StatusCode};
use serde_json::{json, Value};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::{debug, error};
use uuid::Uuid;
pub async fn handle_payment_request(
state_machine: Arc<PaymentStateMachine>,
method: &Method,
path: &str,
body: Option<Value>,
) -> Response<Full<Bytes>> {
let request_id = Uuid::new_v4().to_string();
match (method, path) {
(&Method::POST, "/api/v1/payments") => {
create_payment_request(state_machine, body, request_id).await
}
#[cfg(feature = "ctv")]
(method, path)
if method == Method::POST
&& path.starts_with("/api/v1/payments/")
&& path.ends_with("/covenant") =>
{
let payment_id = extract_payment_id(path, "/api/v1/payments/", "/covenant");
create_covenant_proof(state_machine, &payment_id, request_id).await
}
#[cfg(not(feature = "ctv"))]
(method, path)
if method == Method::POST
&& path.starts_with("/api/v1/payments/")
&& path.ends_with("/covenant") =>
{
error_response(
StatusCode::NOT_IMPLEMENTED,
"NOT_IMPLEMENTED",
"CTV feature required for covenant endpoint",
request_id,
)
}
(&Method::GET, path)
if path.starts_with("/api/v1/payments/") && !path.contains("/covenant") =>
{
let payment_id = extract_payment_id(path, "/api/v1/payments/", "");
get_payment_state(state_machine, &payment_id, request_id).await
}
(&Method::GET, "/api/v1/payments") => list_payments(state_machine, request_id).await,
_ => error_response(
StatusCode::NOT_FOUND,
"NOT_FOUND",
&format!("Payment endpoint not found: {} {}", method, path),
request_id,
),
}
}
fn extract_payment_id(path: &str, prefix: &str, suffix: &str) -> String {
path.strip_prefix(prefix)
.and_then(|s| s.strip_suffix(suffix))
.unwrap_or("")
.to_string()
}
async fn create_payment_request(
state_machine: Arc<PaymentStateMachine>,
body: Option<Value>,
request_id: String,
) -> Response<Full<Bytes>> {
debug!("REST: POST /api/v1/payments");
let body = match body {
Some(b) => b,
None => {
return error_response(
StatusCode::BAD_REQUEST,
"BAD_REQUEST",
"Request body required",
request_id,
);
}
};
let outputs: Vec<PaymentOutput> = match body.get("outputs") {
Some(outputs_value) => match serde_json::from_value(outputs_value.clone()) {
Ok(o) => o,
Err(e) => {
return error_response(
StatusCode::BAD_REQUEST,
"INVALID_OUTPUTS",
&rest_error_invalid("outputs format", e),
request_id,
);
}
},
None => {
return error_response(
StatusCode::BAD_REQUEST,
"MISSING_OUTPUTS",
"Missing 'outputs' field in request body",
request_id,
);
}
};
let merchant_data = body
.get("merchant_data")
.and_then(|v| v.as_str())
.and_then(|s| hex::decode(s).ok());
let create_covenant = body
.get("create_covenant")
.and_then(|v| v.as_bool())
.unwrap_or(false);
match state_machine
.create_payment_request(outputs, merchant_data, create_covenant)
.await
{
Ok((payment_id, covenant_proof)) => {
let mut response_data = json!({
"payment_id": payment_id,
});
#[cfg(feature = "ctv")]
{
if let Some(proof) = covenant_proof {
response_data["covenant_proof"] =
serde_json::to_value(&proof).unwrap_or_else(|_| json!(null));
}
}
success_response(response_data, request_id)
}
Err(e) => {
error!("Failed to create payment request: {}", e);
error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"PAYMENT_CREATION_FAILED",
&rest_error_failed("create payment request", e),
request_id,
)
}
}
}
#[cfg(feature = "ctv")]
async fn create_covenant_proof(
state_machine: Arc<PaymentStateMachine>,
payment_id: &str,
request_id: String,
) -> Response<Full<Bytes>> {
debug!("REST: POST /api/v1/payments/{}/covenant", payment_id);
if payment_id.is_empty() {
return error_response(
StatusCode::BAD_REQUEST,
"MISSING_PAYMENT_ID",
"Payment ID required in path",
request_id,
);
}
match state_machine.create_covenant_proof(payment_id).await {
Ok(covenant_proof) => {
let response_data =
serde_json::to_value(&covenant_proof).unwrap_or_else(|_| json!(null));
success_response(response_data, request_id)
}
Err(e) => {
error!("Failed to create covenant proof: {}", e);
error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"COVENANT_CREATION_FAILED",
&rest_error_failed("create covenant proof", e),
request_id,
)
}
}
}
async fn get_payment_state(
state_machine: Arc<PaymentStateMachine>,
payment_id: &str,
request_id: String,
) -> Response<Full<Bytes>> {
debug!("REST: GET /api/v1/payments/{}", payment_id);
if payment_id.is_empty() {
return error_response(
StatusCode::BAD_REQUEST,
"MISSING_PAYMENT_ID",
"Payment ID required in path",
request_id,
);
}
match state_machine.get_payment_state(payment_id).await {
Ok(state) => {
let state_json = payment_state_to_json(&state);
success_response(state_json, request_id)
}
Err(e) => {
error!("Failed to get payment state: {}", e);
error_response(
StatusCode::NOT_FOUND,
"PAYMENT_NOT_FOUND",
&format!("Payment not found: {}", e),
request_id,
)
}
}
}
async fn list_payments(
state_machine: Arc<PaymentStateMachine>,
request_id: String,
) -> Response<Full<Bytes>> {
debug!("REST: GET /api/v1/payments");
let states = state_machine.list_payment_states();
let payments: Vec<Value> = states
.iter()
.map(|(payment_id, state)| {
let state_str = match state {
PaymentState::RequestCreated { .. } => "request_created",
#[cfg(feature = "ctv")]
PaymentState::ProofCreated { .. } => "proof_created",
#[cfg(feature = "ctv")]
PaymentState::ProofBroadcast { .. } => "proof_broadcast",
#[cfg(not(feature = "ctv"))]
PaymentState::ProofCreated { .. } | PaymentState::ProofBroadcast { .. } => {
"proof_pending"
}
PaymentState::InMempool { .. } => "in_mempool",
PaymentState::Settled { .. } => "settled",
PaymentState::ReorgPending { .. } => "reorg_pending",
PaymentState::Failed { .. } => "failed",
};
json!({
"payment_id": payment_id,
"state": state_str,
})
})
.collect();
let response_data = json!({
"payments": payments,
"count": payments.len(),
});
success_response(response_data, request_id)
}
fn payment_state_to_json(state: &PaymentState) -> Value {
match state {
PaymentState::RequestCreated { request_id } => {
json!({
"state": "request_created",
"request_id": request_id,
})
}
#[cfg(feature = "ctv")]
PaymentState::ProofCreated {
request_id,
covenant_proof,
} => {
json!({
"state": "proof_created",
"request_id": request_id,
"covenant_proof": serde_json::to_value(covenant_proof)
.unwrap_or_else(|_| json!(null)),
})
}
#[cfg(feature = "ctv")]
PaymentState::ProofBroadcast {
request_id,
covenant_proof,
broadcast_peers,
} => {
json!({
"state": "proof_broadcast",
"request_id": request_id,
"covenant_proof": serde_json::to_value(covenant_proof)
.unwrap_or_else(|_| json!(null)),
"broadcast_peers": broadcast_peers.len(),
})
}
PaymentState::InMempool {
request_id,
tx_hash,
} => {
json!({
"state": "in_mempool",
"request_id": request_id,
"tx_hash": hex::encode(tx_hash),
})
}
PaymentState::Settled {
request_id,
tx_hash,
block_hash,
confirmation_count,
..
} => {
json!({
"state": "settled",
"request_id": request_id,
"tx_hash": hex::encode(tx_hash),
"block_hash": hex::encode(block_hash),
"confirmation_count": confirmation_count,
"safe_for_release": *confirmation_count >= DEFAULT_SAFE_DEPTH,
})
}
PaymentState::ReorgPending {
request_id,
tx_hash,
reason,
..
} => {
json!({
"state": "reorg_pending",
"request_id": request_id,
"tx_hash": hex::encode(tx_hash),
"reason": reason,
})
}
PaymentState::Failed { request_id, reason } => {
json!({
"state": "failed",
"request_id": request_id,
"reason": reason,
})
}
#[cfg(not(feature = "ctv"))]
PaymentState::ProofCreated { request_id, .. }
| PaymentState::ProofBroadcast { request_id, .. } => {
json!({ "state": "proof_pending", "request_id": request_id })
}
}
}
fn success_response(data: Value, request_id: String) -> Response<Full<Bytes>> {
let response = ApiResponse::success(data, Some(request_id));
let body = serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string());
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.header("Content-Length", body.len())
.body(Full::new(Bytes::from(body)))
.unwrap()
}
fn error_response(
status: StatusCode,
code: &str,
message: &str,
request_id: String,
) -> Response<Full<Bytes>> {
let error = ApiError::new(code, message, None, None, Some(request_id));
let body = serde_json::to_string(&error).unwrap_or_else(|_| "{}".to_string());
Response::builder()
.status(status)
.header("Content-Type", "application/json")
.header("Content-Length", body.len())
.body(Full::new(Bytes::from(body)))
.unwrap()
}