Skip to main content

r402_http/server/
verify.rs

1//! Steps 3–5: extract `Payment-Signature`, match, verify, skip-handler, settle-before.
2
3use http::HeaderMap;
4use r402_extensions::{
5    EIP2612_GAS_SPONSORING_KEY, ERC20_APPROVAL_GAS_SPONSORING_KEY, Eip2612GasSponsoringExtension,
6    Erc20ApprovalGasSponsoringExtension,
7};
8use r402_facilitator::DynFacilitator;
9use r402_protocol::error::FacilitatorError;
10use r402_protocol::extension::{AdvertiseContext, Extension};
11use r402_protocol::payment::{
12    Base64Bytes, Extensions, PaymentRequired, PaymentRequirements, SettleResponse,
13    SettlementOverrides, SupportedResponse, VerifyResponse,
14};
15use r402_server::{
16    CancelReason, CompletedSettlement, PaymentFlowName, PaymentFlowPhases,
17    PaymentRequiredBuildContext, ResourceServer, SettlePhase, SkipHandlerDirective,
18    WirePaymentPayload, resolve_payment_flow_phases, validate_accepts_against_supported,
19};
20
21use super::fail::GateError;
22use super::gate::Gate;
23use crate::headers::PAYMENT_SIGNATURE;
24
25/// Verified payment ready for settle. Consumed by after-handler settle.
26#[derive(Debug, Clone)]
27pub struct VerifiedPayment {
28    pub(crate) payload: WirePaymentPayload,
29    pub(crate) requirements: PaymentRequirements,
30    pub(crate) server: ResourceServer,
31    pub(crate) skip_handler: Option<SkipHandlerDirective>,
32    pub(crate) resource_url: compact_str::CompactString,
33    pub(crate) advertised: Extensions,
34}
35
36impl VerifiedPayment {
37    /// One-shot cancel dispatcher.
38    #[must_use]
39    pub fn cancellation_guard(&self) -> r402_server::CancellationGuard {
40        self.server
41            .cancellation_guard(self.payload.clone(), self.requirements.clone())
42    }
43
44    /// Matched requirements (override resolution).
45    #[must_use]
46    pub const fn requirements(&self) -> &PaymentRequirements {
47        &self.requirements
48    }
49
50    pub(crate) async fn settle_phase(
51        &self,
52        phase: SettlePhase,
53        overrides: Option<&SettlementOverrides>,
54    ) -> Result<SettleResponse, FacilitatorError> {
55        self.server
56            .settle_payment(
57                &self.payload,
58                &self.requirements,
59                overrides,
60                phase,
61                Some(self.resource_url.as_str()),
62                Some(&self.advertised),
63            )
64            .await
65    }
66}
67
68impl Gate {
69    /// Fetches `/supported` and builds the shared 402 body.
70    ///
71    /// # Errors
72    ///
73    /// [`GateError::Transport`] when `/supported` fails.
74    /// [`GateError::FacilitatorSupport`] when this accept list has no matching
75    /// kind or advertised extra is unusable.
76    /// [`GateError::PaymentRequiredBuild`] when 402 construction fails.
77    pub async fn build_payment_required(&mut self) -> Result<(), GateError> {
78        let facilitator = self.server.facilitator();
79        let supported = DynFacilitator::supported(facilitator.as_ref())
80            .await
81            .map_err(GateError::from_verify_facilitator)?;
82        let reqs: Vec<_> = self
83            .accepts
84            .iter()
85            .map(|pt| pt.requirements.clone())
86            .collect();
87        validate_accepts_against_supported(&self.server, &reqs, &supported)
88            .map_err(GateError::FacilitatorSupport)?;
89        let mut built = self
90            .server
91            .create_payment_required_response(
92                reqs,
93                PaymentRequiredBuildContext {
94                    resource: self.resource.clone(),
95                    error: None,
96                    extensions: Extensions::new(),
97                    supported: supported.clone(),
98                    payment_payload: None,
99                },
100            )
101            .await
102            .map_err(|err| GateError::PaymentRequiredBuild(err.to_string()))?;
103        advertise_permit2_gas_sponsoring(&mut built, &supported, &self.server);
104        self.payment_required = Some(built);
105        Ok(())
106    }
107
108    /// Verifies `Payment-Signature` against the built 402 accepts.
109    ///
110    /// # Errors
111    ///
112    /// Missing/malformed header → 402. Transport → 502. `isValid: false` → 402/412.
113    pub async fn verify_only(&self, headers: &HeaderMap) -> Result<VerifiedPayment, GateError> {
114        let header_bytes = headers
115            .get(PAYMENT_SIGNATURE)
116            .map(http::HeaderValue::as_bytes)
117            .ok_or(GateError::PaymentHeaderMissing)?;
118
119        let payload =
120            decode_payment_payload(header_bytes).ok_or(GateError::InvalidPaymentHeader)?;
121
122        let payment_required: &PaymentRequired =
123            self.payment_required.as_ref().ok_or_else(|| {
124                GateError::PaymentRequiredBuild(
125                    "payment-required response has not been built".into(),
126                )
127            })?;
128        let requirements = self
129            .server
130            .find_matching_requirements(&payment_required.accepts, &payload)
131            .cloned()
132            .ok_or(GateError::NoPaymentMatching)?;
133        let outcome = self
134            .server
135            .verify_payment(&payload, &requirements, Some(&payment_required.extensions))
136            .await
137            .map_err(GateError::from_verify_facilitator)?;
138
139        if let VerifyResponse::Invalid {
140            reason, message, ..
141        } = &outcome.response
142        {
143            return Err(GateError::from_invalid_verify(
144                reason.clone(),
145                message.as_deref(),
146            ));
147        }
148
149        Ok(VerifiedPayment {
150            payload,
151            requirements,
152            server: self.server.clone(),
153            skip_handler: outcome.skip_handler,
154            resource_url: payment_required.resource.url.clone(),
155            advertised: payment_required.extensions.clone(),
156        })
157    }
158}
159
160pub(crate) fn decode_payment_payload(header_bytes: &[u8]) -> Option<WirePaymentPayload> {
161    let decoded = Base64Bytes::from(header_bytes).decode().ok()?;
162    serde_json::from_slice(decoded.as_ref()).ok()
163}
164
165pub(crate) fn payment_flow_of(
166    server: &ResourceServer,
167    requirements: &PaymentRequirements,
168) -> Result<PaymentFlowName, GateError> {
169    server
170        .get_payment_flow(requirements)
171        .map_err(|err| match err {
172            r402_server::PaymentFlowError::UnregisteredScheme { scheme, .. } => {
173                GateError::MissingScheme {
174                    scheme: scheme.into(),
175                    network: requirements.network.clone(),
176                }
177            }
178            other => GateError::PaymentRequiredBuild(other.to_string()),
179        })
180}
181
182pub(crate) fn cancellation_guard(
183    verified: &VerifiedPayment,
184    before_handler: Option<&CompletedSettlement>,
185) -> r402_server::CancellationGuard {
186    let guard = verified.cancellation_guard();
187    match before_handler {
188        Some(completed) => guard
189            .with_settled_phases([SettlePhase::BeforeHandler])
190            .with_before_handler(completed.clone()),
191        None => guard,
192    }
193}
194
195pub(crate) async fn settle_before_handler_if_needed(
196    verified: &VerifiedPayment,
197    flow: PaymentFlowName,
198    phases: PaymentFlowPhases,
199) -> Result<Option<CompletedSettlement>, GateError> {
200    if !phases.settle_before_handler {
201        return Ok(None);
202    }
203    let result = map_settle(
204        verified
205            .settle_phase(SettlePhase::BeforeHandler, None)
206            .await,
207    )?;
208    Ok(Some(CompletedSettlement::new(
209        SettlePhase::BeforeHandler,
210        flow,
211        result,
212        verified.requirements().clone(),
213    )))
214}
215
216pub(crate) fn map_settle(
217    result: Result<SettleResponse, FacilitatorError>,
218) -> Result<SettleResponse, GateError> {
219    let settlement = result.map_err(GateError::from_settle_facilitator)?;
220    if matches!(settlement, SettleResponse::Failure { .. }) {
221        return Err(GateError::Settlement(Box::new(settlement)));
222    }
223    Ok(settlement)
224}
225
226pub(crate) fn phases_of(
227    verified: &VerifiedPayment,
228) -> Result<(PaymentFlowName, PaymentFlowPhases), GateError> {
229    let flow = payment_flow_of(&verified.server, verified.requirements())?;
230    Ok((flow, resolve_payment_flow_phases(flow)))
231}
232
233/// Reason string for handler-failed cancel.
234pub(crate) const HANDLER_FAILED: &str = "handler returned error status";
235
236pub(crate) const fn handler_failed_reason() -> CancelReason {
237    CancelReason::HandlerFailed
238}
239
240fn advertise_permit2_gas_sponsoring(
241    body: &mut PaymentRequired,
242    supported: &SupportedResponse,
243    server: &ResourceServer,
244) {
245    if !body.accepts.iter().any(|req| is_evm_permit2(req, server)) {
246        return;
247    }
248    if body.extensions.get(EIP2612_GAS_SPONSORING_KEY).is_none() {
249        let entry = Eip2612GasSponsoringExtension::new().advertise(
250            &AdvertiseContext::for_payment_required(&body.resource, &body.accepts, None),
251        );
252        if let Some(entry) = entry {
253            body.extensions.insert(EIP2612_GAS_SPONSORING_KEY, entry);
254        }
255    }
256    let erc20_listed = supported
257        .extensions
258        .iter()
259        .any(|e| e.as_str() == ERC20_APPROVAL_GAS_SPONSORING_KEY);
260    if erc20_listed
261        && body
262            .extensions
263            .get(ERC20_APPROVAL_GAS_SPONSORING_KEY)
264            .is_none()
265    {
266        let entry = Erc20ApprovalGasSponsoringExtension::new().advertise(
267            &AdvertiseContext::for_payment_required(&body.resource, &body.accepts, None),
268        );
269        if let Some(entry) = entry {
270            body.extensions
271                .insert(ERC20_APPROVAL_GAS_SPONSORING_KEY, entry);
272        }
273    }
274}
275
276fn is_evm_permit2(req: &PaymentRequirements, server: &ResourceServer) -> bool {
277    if extra_atm(req) == Some("permit2") {
278        return true;
279    }
280    server
281        .registered_scheme(req.scheme.as_str(), &req.network)
282        .is_some_and(|s| s.default_asset_transfer_method() == "permit2")
283}
284
285fn extra_atm(req: &PaymentRequirements) -> Option<&str> {
286    req.extra
287        .as_ref()
288        .and_then(|extra| extra.get("assetTransferMethod"))
289        .and_then(serde_json::Value::as_str)
290}