use r402_facilitator::{DynFacilitator, FailureRecovery};
use r402_protocol::error::{FacilitatorError, VerificationError};
use r402_protocol::extension::{ExtensionRegistry, SettleContext as ExtensionSettleContext};
use r402_protocol::payment::{
Extensions, PaymentPayload, PaymentRequirements, SettleRequest, SettleResponse,
SettlementOverrides, TypedVerifyRequest, V2, asset_decimals_from_extra,
resolve_settlement_override_amount,
};
use serde_json::Value;
use crate::hooks::{
BeforeOpDecision, PaymentHookContext, SettleContext, SettleResultContext, WirePaymentPayload,
assert_additive_payload_enrichment,
};
use crate::payment_flow::{PaymentFlowName, SettlePhase};
use crate::resource::ResourceServer;
use crate::verify::to_verify_request;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CompletedSettlement {
pub phase: SettlePhase,
pub flow: PaymentFlowName,
pub result: SettleResponse,
pub requirements: PaymentRequirements,
}
impl CompletedSettlement {
#[must_use]
pub const fn new(
phase: SettlePhase,
flow: PaymentFlowName,
result: SettleResponse,
requirements: PaymentRequirements,
) -> Self {
Self {
phase,
flow,
result,
requirements,
}
}
}
impl ResourceServer {
pub async fn settle_payment(
&self,
payload: &WirePaymentPayload,
requirements: &PaymentRequirements,
overrides: Option<&SettlementOverrides>,
phase: SettlePhase,
resource_url: Option<&str>,
advertised: Option<&Extensions>,
) -> Result<SettleResponse, FacilitatorError> {
let effective = apply_settlement_overrides(requirements, overrides)?;
let payload = self
.apply_scheme_settlement_payload_enrich(payload, &effective, phase)
.await?;
let settle_ctx = SettleContext {
payment: PaymentHookContext {
payload,
requirements: effective,
},
declared_extensions: advertised.cloned().unwrap_or_default(),
phase,
resource_url: resource_url.map(compact_str::CompactString::from),
};
let mut skipped: Option<SettleResponse> = None;
for hook in &self.hooks {
match hook.before_settle(&settle_ctx).await {
BeforeOpDecision::Continue => {}
BeforeOpDecision::Abort { reason, message } => {
return Err(FacilitatorError::Aborted { reason, message });
}
BeforeOpDecision::Skip { result } => {
skipped = Some(result);
break;
}
}
}
let mut response = if let Some(local) = skipped {
local
} else {
self.call_settle_with_failure_hooks(&settle_ctx).await?
};
attach_settle_extensions(&self.extensions, &settle_ctx, &mut response, resource_url)
.await?;
let result_ctx = SettleResultContext {
settle: settle_ctx,
result: response.clone(),
};
for hook in &self.hooks {
hook.after_settle(&result_ctx).await;
}
Ok(response)
}
async fn call_settle_with_failure_hooks(
&self,
settle: &SettleContext,
) -> Result<SettleResponse, FacilitatorError> {
let request = build_settle_request(&settle.payment.payload, &settle.payment.requirements)?;
#[cfg(feature = "metrics")]
let started = std::time::Instant::now();
let result = match self.settle_with_pending_retry(request).await {
Ok(r) => Ok(r),
Err(error) => self.recover_settle(settle, error).await,
};
#[cfg(feature = "metrics")]
record_settle_metrics(&result, started.elapsed());
result
}
async fn settle_with_pending_retry(
&self,
request: SettleRequest,
) -> Result<SettleResponse, FacilitatorError> {
let first = DynFacilitator::settle(self.facilitator.as_ref(), request.clone()).await;
match &first {
Ok(response) if response.is_retryable_settlement_pending() => {
DynFacilitator::settle(self.facilitator.as_ref(), request).await
}
_ => first,
}
}
async fn apply_scheme_settlement_payload_enrich(
&self,
payload: &WirePaymentPayload,
requirements: &PaymentRequirements,
phase: SettlePhase,
) -> Result<WirePaymentPayload, FacilitatorError> {
let Some(scheme) =
self.registered_scheme(requirements.scheme.as_str(), &requirements.network)
else {
return Ok(payload.clone());
};
let ctx = SettleContext {
payment: PaymentHookContext {
payload: payload.clone(),
requirements: requirements.clone(),
},
declared_extensions: Extensions::new(),
phase,
resource_url: None,
};
let Some(enrichment) = scheme.enrich_settlement_payload(&ctx).await? else {
return Ok(payload.clone());
};
let mut enriched = payload.clone();
let Value::Object(payload_map) = &mut enriched.payload else {
return Err(FacilitatorError::Verification(
VerificationError::InvalidFormat("settlement payload is not a JSON object".into()),
));
};
assert_additive_payload_enrichment(payload_map, &enrichment, scheme.scheme())
.map_err(FacilitatorError::internal)?;
for (key, value) in enrichment {
payload_map.insert(key, value);
}
Ok(enriched)
}
async fn recover_settle(
&self,
settle: &SettleContext,
error: FacilitatorError,
) -> Result<SettleResponse, FacilitatorError> {
for hook in &self.hooks {
if let FailureRecovery::Recovered(r) = hook.on_settle_failure(settle, &error).await {
return Ok(r);
}
}
Err(error)
}
}
async fn attach_settle_extensions(
registry: &ExtensionRegistry,
settle: &SettleContext,
response: &mut SettleResponse,
resource_url: Option<&str>,
) -> Result<(), FacilitatorError> {
if registry.is_empty() {
return Ok(());
}
let json_payload = json_payment_payload(&settle.payment.payload)?;
let extra = {
let mut ctx =
ExtensionSettleContext::new(&json_payload, &settle.payment.requirements, response);
if let Some(url) = resource_url.or(settle.resource_url.as_deref()) {
ctx = ctx.with_resource_url(url);
}
if !settle.declared_extensions.is_empty() {
ctx = ctx.with_advertised(&settle.declared_extensions);
}
registry.collect_settle(&ctx).await
};
response.extensions_mut().extend(extra);
Ok(())
}
fn json_payment_payload(
payload: &WirePaymentPayload,
) -> Result<PaymentPayload<Value, Value>, FacilitatorError> {
let accepted = serde_json::to_value(&payload.accepted).map_err(FacilitatorError::internal)?;
Ok(PaymentPayload::new(accepted, payload.payload.clone())
.with_optional_resource(payload.resource.clone())
.with_extensions(payload.extensions.clone()))
}
fn build_settle_request(
payload: &WirePaymentPayload,
requirements: &PaymentRequirements,
) -> Result<SettleRequest, FacilitatorError> {
let typed = TypedVerifyRequest {
x402_version: V2,
payment_payload: payload.clone(),
payment_requirements: requirements.clone(),
};
let verify = to_verify_request(&typed)?;
Ok(SettleRequest::from(verify.into_json()))
}
fn apply_settlement_overrides(
requirements: &PaymentRequirements,
overrides: Option<&SettlementOverrides>,
) -> Result<PaymentRequirements, FacilitatorError> {
let Some(overrides) = overrides else {
return Ok(requirements.clone());
};
let Some(raw) = overrides
.amount
.as_ref()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
else {
return Ok(requirements.clone());
};
let decimals = asset_decimals_from_extra(requirements.extra.as_ref());
let resolved = resolve_settlement_override_amount(raw, requirements.amount.as_str(), decimals)
.map_err(|e| {
FacilitatorError::Verification(VerificationError::InvalidFormat(format!(
"invalid settlement override: {e}"
)))
})?;
let mut effective = requirements.clone();
effective.amount = resolved.into();
Ok(effective)
}
#[cfg(feature = "metrics")]
fn record_settle_metrics(
result: &Result<SettleResponse, FacilitatorError>,
elapsed: std::time::Duration,
) {
let label = match result {
Ok(r) if r.is_success() => "success",
Ok(_) => "failure",
Err(_) => "error",
};
::metrics::counter!(
r402_protocol::metrics::FACILITATOR_SETTLE_TOTAL,
"result" => label,
)
.increment(1);
::metrics::histogram!(r402_protocol::metrics::FACILITATOR_SETTLE_DURATION_SECONDS)
.record(elapsed.as_secs_f64());
}