Skip to main content

r402_server/
settle.rs

1//! Settle-payment orchestration.
2
3use r402_facilitator::{DynFacilitator, FailureRecovery};
4use r402_protocol::error::{FacilitatorError, VerificationError};
5use r402_protocol::extension::{ExtensionRegistry, SettleContext as ExtensionSettleContext};
6use r402_protocol::payment::{
7    Extensions, PaymentPayload, PaymentRequirements, SettleRequest, SettleResponse,
8    SettlementOverrides, TypedVerifyRequest, V2, asset_decimals_from_extra,
9    resolve_settlement_override_amount,
10};
11use serde_json::Value;
12
13use crate::hooks::{
14    BeforeOpDecision, PaymentHookContext, SettleContext, SettleResultContext, WirePaymentPayload,
15    assert_additive_payload_enrichment,
16};
17use crate::payment_flow::{PaymentFlowName, SettlePhase};
18use crate::resource::ResourceServer;
19use crate::verify::to_verify_request;
20
21/// Successful settle captured for a later phase (echo / cancel).
22#[derive(Debug, Clone)]
23#[non_exhaustive]
24pub struct CompletedSettlement {
25    /// Settle invocation that produced `result`.
26    pub phase: SettlePhase,
27    /// Payment flow in effect when this settle ran.
28    pub flow: PaymentFlowName,
29    /// Facilitator settle response.
30    pub result: SettleResponse,
31    /// Requirements used for this settle.
32    pub requirements: PaymentRequirements,
33}
34
35impl CompletedSettlement {
36    /// Constructs a completed-settlement record.
37    #[must_use]
38    pub const fn new(
39        phase: SettlePhase,
40        flow: PaymentFlowName,
41        result: SettleResponse,
42        requirements: PaymentRequirements,
43    ) -> Self {
44        Self {
45            phase,
46            flow,
47            result,
48            requirements,
49        }
50    }
51}
52
53impl ResourceServer {
54    /// Optional amount overrides → before hooks → facilitator → after hooks.
55    ///
56    /// Facilitator settle is attempted twice only when the first `Ok` result is
57    /// `settlement_pending` with a non-empty `transaction`; no sleep.
58    ///
59    /// # Errors
60    ///
61    /// Propagates facilitator / abort / invalid-override errors unless a
62    /// failure hook recovers.
63    pub async fn settle_payment(
64        &self,
65        payload: &WirePaymentPayload,
66        requirements: &PaymentRequirements,
67        overrides: Option<&SettlementOverrides>,
68        phase: SettlePhase,
69        resource_url: Option<&str>,
70        advertised: Option<&Extensions>,
71    ) -> Result<SettleResponse, FacilitatorError> {
72        let effective = apply_settlement_overrides(requirements, overrides)?;
73        let payload = self
74            .apply_scheme_settlement_payload_enrich(payload, &effective, phase)
75            .await?;
76        let settle_ctx = SettleContext {
77            payment: PaymentHookContext {
78                payload,
79                requirements: effective,
80            },
81            declared_extensions: advertised.cloned().unwrap_or_default(),
82            phase,
83            resource_url: resource_url.map(compact_str::CompactString::from),
84        };
85
86        let mut skipped: Option<SettleResponse> = None;
87        for hook in &self.hooks {
88            match hook.before_settle(&settle_ctx).await {
89                BeforeOpDecision::Continue => {}
90                BeforeOpDecision::Abort { reason, message } => {
91                    return Err(FacilitatorError::Aborted { reason, message });
92                }
93                BeforeOpDecision::Skip { result } => {
94                    skipped = Some(result);
95                    break;
96                }
97            }
98        }
99
100        let mut response = if let Some(local) = skipped {
101            local
102        } else {
103            self.call_settle_with_failure_hooks(&settle_ctx).await?
104        };
105        attach_settle_extensions(&self.extensions, &settle_ctx, &mut response, resource_url)
106            .await?;
107
108        let result_ctx = SettleResultContext {
109            settle: settle_ctx,
110            result: response.clone(),
111        };
112        for hook in &self.hooks {
113            hook.after_settle(&result_ctx).await;
114        }
115
116        Ok(response)
117    }
118
119    async fn call_settle_with_failure_hooks(
120        &self,
121        settle: &SettleContext,
122    ) -> Result<SettleResponse, FacilitatorError> {
123        let request = build_settle_request(&settle.payment.payload, &settle.payment.requirements)?;
124        #[cfg(feature = "metrics")]
125        let started = std::time::Instant::now();
126        let result = match self.settle_with_pending_retry(request).await {
127            Ok(r) => Ok(r),
128            Err(error) => self.recover_settle(settle, error).await,
129        };
130        #[cfg(feature = "metrics")]
131        record_settle_metrics(&result, started.elapsed());
132        result
133    }
134
135    /// Calls facilitator settle once, then retries identically iff the first
136    /// result is `Ok(SettleResponse::Failure)` with `settlement_pending` and a
137    /// non-empty transaction hash. `Err` is never retried.
138    async fn settle_with_pending_retry(
139        &self,
140        request: SettleRequest,
141    ) -> Result<SettleResponse, FacilitatorError> {
142        let first = DynFacilitator::settle(self.facilitator.as_ref(), request.clone()).await;
143        match &first {
144            Ok(response) if response.is_retryable_settlement_pending() => {
145                DynFacilitator::settle(self.facilitator.as_ref(), request).await
146            }
147            _ => first,
148        }
149    }
150
151    async fn apply_scheme_settlement_payload_enrich(
152        &self,
153        payload: &WirePaymentPayload,
154        requirements: &PaymentRequirements,
155        phase: SettlePhase,
156    ) -> Result<WirePaymentPayload, FacilitatorError> {
157        let Some(scheme) =
158            self.registered_scheme(requirements.scheme.as_str(), &requirements.network)
159        else {
160            return Ok(payload.clone());
161        };
162        let ctx = SettleContext {
163            payment: PaymentHookContext {
164                payload: payload.clone(),
165                requirements: requirements.clone(),
166            },
167            declared_extensions: Extensions::new(),
168            phase,
169            resource_url: None,
170        };
171        let Some(enrichment) = scheme.enrich_settlement_payload(&ctx).await? else {
172            return Ok(payload.clone());
173        };
174        let mut enriched = payload.clone();
175        let Value::Object(payload_map) = &mut enriched.payload else {
176            return Err(FacilitatorError::Verification(
177                VerificationError::InvalidFormat("settlement payload is not a JSON object".into()),
178            ));
179        };
180        assert_additive_payload_enrichment(payload_map, &enrichment, scheme.scheme())
181            .map_err(FacilitatorError::internal)?;
182        for (key, value) in enrichment {
183            payload_map.insert(key, value);
184        }
185        Ok(enriched)
186    }
187
188    async fn recover_settle(
189        &self,
190        settle: &SettleContext,
191        error: FacilitatorError,
192    ) -> Result<SettleResponse, FacilitatorError> {
193        for hook in &self.hooks {
194            if let FailureRecovery::Recovered(r) = hook.on_settle_failure(settle, &error).await {
195                return Ok(r);
196            }
197        }
198        Err(error)
199    }
200}
201
202async fn attach_settle_extensions(
203    registry: &ExtensionRegistry,
204    settle: &SettleContext,
205    response: &mut SettleResponse,
206    resource_url: Option<&str>,
207) -> Result<(), FacilitatorError> {
208    if registry.is_empty() {
209        return Ok(());
210    }
211    let json_payload = json_payment_payload(&settle.payment.payload)?;
212    let extra = {
213        let mut ctx =
214            ExtensionSettleContext::new(&json_payload, &settle.payment.requirements, response);
215        if let Some(url) = resource_url.or(settle.resource_url.as_deref()) {
216            ctx = ctx.with_resource_url(url);
217        }
218        if !settle.declared_extensions.is_empty() {
219            ctx = ctx.with_advertised(&settle.declared_extensions);
220        }
221        registry.collect_settle(&ctx).await
222    };
223    response.extensions_mut().extend(extra);
224    Ok(())
225}
226
227fn json_payment_payload(
228    payload: &WirePaymentPayload,
229) -> Result<PaymentPayload<Value, Value>, FacilitatorError> {
230    let accepted = serde_json::to_value(&payload.accepted).map_err(FacilitatorError::internal)?;
231    Ok(PaymentPayload::new(accepted, payload.payload.clone())
232        .with_optional_resource(payload.resource.clone())
233        .with_extensions(payload.extensions.clone()))
234}
235
236fn build_settle_request(
237    payload: &WirePaymentPayload,
238    requirements: &PaymentRequirements,
239) -> Result<SettleRequest, FacilitatorError> {
240    let typed = TypedVerifyRequest {
241        x402_version: V2,
242        payment_payload: payload.clone(),
243        payment_requirements: requirements.clone(),
244    };
245    let verify = to_verify_request(&typed)?;
246    Ok(SettleRequest::from(verify.into_json()))
247}
248
249/// Applies settlement overrides onto a clone of `requirements`.
250fn apply_settlement_overrides(
251    requirements: &PaymentRequirements,
252    overrides: Option<&SettlementOverrides>,
253) -> Result<PaymentRequirements, FacilitatorError> {
254    let Some(overrides) = overrides else {
255        return Ok(requirements.clone());
256    };
257    let Some(raw) = overrides
258        .amount
259        .as_ref()
260        .map(|s| s.trim())
261        .filter(|s| !s.is_empty())
262    else {
263        return Ok(requirements.clone());
264    };
265    let decimals = asset_decimals_from_extra(requirements.extra.as_ref());
266    let resolved = resolve_settlement_override_amount(raw, requirements.amount.as_str(), decimals)
267        .map_err(|e| {
268            FacilitatorError::Verification(VerificationError::InvalidFormat(format!(
269                "invalid settlement override: {e}"
270            )))
271        })?;
272    let mut effective = requirements.clone();
273    effective.amount = resolved.into();
274    Ok(effective)
275}
276
277#[cfg(feature = "metrics")]
278fn record_settle_metrics(
279    result: &Result<SettleResponse, FacilitatorError>,
280    elapsed: std::time::Duration,
281) {
282    let label = match result {
283        Ok(r) if r.is_success() => "success",
284        Ok(_) => "failure",
285        Err(_) => "error",
286    };
287    ::metrics::counter!(
288        r402_protocol::metrics::FACILITATOR_SETTLE_TOTAL,
289        "result" => label,
290    )
291    .increment(1);
292    ::metrics::histogram!(r402_protocol::metrics::FACILITATOR_SETTLE_DURATION_SECONDS)
293        .record(elapsed.as_secs_f64());
294}