Skip to main content

zk402_facilitator_http/
lib.rs

1//! HTTP facilitator service exposing verify and settle endpoints
2//!
3//! Implements the x402 facilitator HTTP contract with POST /verify and POST /settle.
4
5use axum::{
6    extract::State,
7    http::StatusCode,
8    response::{IntoResponse, Response},
9    routing::post,
10    Json, Router,
11};
12use serde::Deserialize;
13use std::sync::Arc;
14use zk402_core::*;
15use zk402_facilitator::ZkSettleFacilitator;
16
17/// HTTP request for POST /verify
18#[derive(Debug, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct VerifyRequest {
21    pub requirements: PaymentRequirements,
22    pub payload: PaymentPayload,
23}
24
25/// HTTP request for POST /settle
26#[derive(Debug, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct SettleRequest {
29    pub requirements: PaymentRequirements,
30    pub payload: PaymentPayload,
31}
32
33/// Application state holding the facilitator instance
34pub struct AppState<PS: ProofSystem, VKR: VerifyingKeyRegistry> {
35    pub facilitator: Arc<ZkSettleFacilitator<PS, VKR>>,
36}
37
38/// Build the HTTP router with verify and settle endpoints
39pub fn app_router<PS, VKR>(facilitator: Arc<ZkSettleFacilitator<PS, VKR>>) -> Router
40where
41    PS: ProofSystem + Send + Sync + 'static,
42    VKR: VerifyingKeyRegistry<VerifyingKeyHandle = PS::VerifyingKey> + Send + Sync + 'static,
43{
44    let state = Arc::new(AppState { facilitator });
45
46    Router::new()
47        .route("/verify", post(verify_handler::<PS, VKR>))
48        .route("/settle", post(settle_handler::<PS, VKR>))
49        .with_state(state)
50}
51
52/// POST /verify handler
53async fn verify_handler<PS, VKR>(
54    State(state): State<Arc<AppState<PS, VKR>>>,
55    Json(request): Json<VerifyRequest>,
56) -> Result<Json<VerifyResponse>, AppError>
57where
58    PS: ProofSystem + Send + Sync,
59    VKR: VerifyingKeyRegistry<VerifyingKeyHandle = PS::VerifyingKey> + Send + Sync,
60{
61    let response = state
62        .facilitator
63        .verify(&request.requirements, &request.payload)
64        .await
65        .map_err(AppError::from)?;
66
67    Ok(Json(response))
68}
69
70/// POST /settle handler
71async fn settle_handler<PS, VKR>(
72    State(state): State<Arc<AppState<PS, VKR>>>,
73    Json(request): Json<SettleRequest>,
74) -> Result<Json<SettleResponse>, AppError>
75where
76    PS: ProofSystem + Send + Sync,
77    VKR: VerifyingKeyRegistry<VerifyingKeyHandle = PS::VerifyingKey> + Send + Sync,
78{
79    let response = state
80        .facilitator
81        .settle(&request.requirements, &request.payload)
82        .await
83        .map_err(AppError::from)?;
84
85    Ok(Json(response))
86}
87
88/// Application error wrapper
89#[derive(Debug)]
90pub struct AppError(SchemeError);
91
92impl From<SchemeError> for AppError {
93    fn from(err: SchemeError) -> Self {
94        Self(err)
95    }
96}
97
98impl IntoResponse for AppError {
99    fn into_response(self) -> Response {
100        let (status, message) = match self.0 {
101            SchemeError::StructuralMismatch(msg) => (StatusCode::BAD_REQUEST, msg),
102            SchemeError::UnsupportedCircuit => {
103                (StatusCode::BAD_REQUEST, "unsupported circuit".to_string())
104            }
105            SchemeError::CryptographicVerificationFailed => {
106                (StatusCode::BAD_REQUEST, "proof verification failed".to_string())
107            }
108            SchemeError::Expired => {
109                (StatusCode::BAD_REQUEST, "payment window expired".to_string())
110            }
111            SchemeError::ChainError(msg) => (StatusCode::BAD_GATEWAY, msg),
112            SchemeError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
113        };
114
115        let body = serde_json::json!({
116            "error": message
117        });
118
119        (status, Json(body)).into_response()
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use axum::http::Request;
127    use tower::ServiceExt;
128    use zk402_facilitator::StaticRegistry;
129
130    struct MockProofSystem;
131
132    impl ProofSystem for MockProofSystem {
133        type ProvingKey = Vec<u8>;
134        type VerifyingKey = Vec<u8>;
135
136        fn prove(
137            _pk: &Self::ProvingKey,
138            _witness: &Witness,
139            _public_inputs: &PublicInputs,
140        ) -> Result<Vec<u8>, ProofSystemError> {
141            Ok(vec![0x42])
142        }
143
144        fn verify(
145            _vk: &Self::VerifyingKey,
146            proof: &[u8],
147            _public_inputs: &PublicInputs,
148        ) -> Result<bool, ProofSystemError> {
149            Ok(proof == &[0x42])
150        }
151    }
152
153    fn test_facilitator() -> Arc<ZkSettleFacilitator<MockProofSystem, StaticRegistry<Vec<u8>>>> {
154        let mut registry = StaticRegistry::new();
155        registry.register(
156            "groth16".to_string(),
157            "zk-settle-transfer-v1".to_string(),
158            vec![0u8],
159        );
160        Arc::new(ZkSettleFacilitator::new(registry))
161    }
162
163    fn test_payload() -> PaymentPayload {
164        PaymentPayload {
165            x402_version: 1,
166            accepted: PaymentRequirements {
167                scheme: "zk-settle".to_string(),
168                network: "eip155:8453".to_string(),
169                amount: "100".to_string(),
170                asset: [1u8; 20],
171                pay_to: [2u8; 20],
172                max_timeout_seconds: 3600,
173                extra: ZkExtra {
174                    proof_system: "groth16".to_string(),
175                    circuit_id: "zk-settle-transfer-v1".to_string(),
176                    verifier_address: [3u8; 20],
177                },
178            },
179            proof: vec![0x42],
180            public_inputs: PublicInputs {
181                amount: "100".to_string(),
182                asset: [1u8; 20],
183                pay_to: [2u8; 20],
184                from: [4u8; 20],
185                network: "eip155:8453".to_string(),
186                nonce: [5u8; 32],
187                valid_after: 1000000000,
188                valid_before: 9999999999,
189                public_key: vec![6u8; 32],
190            },
191            proof_system_extra: None,
192        }
193    }
194
195    #[tokio::test]
196    async fn verify_endpoint_returns_valid_response() {
197        let app = app_router(test_facilitator());
198        let payload = test_payload();
199
200        let request = Request::builder()
201            .uri("/verify")
202            .method("POST")
203            .header("content-type", "application/json")
204            .body(
205                serde_json::to_string(&VerifyRequest {
206                    requirements: payload.accepted.clone(),
207                    payload: payload.clone(),
208                })
209                .unwrap(),
210            )
211            .unwrap();
212
213        let response = app.oneshot(request).await.unwrap();
214        assert_eq!(response.status(), StatusCode::OK);
215    }
216
217    #[tokio::test]
218    async fn verify_endpoint_rejects_invalid_proof() {
219        let app = app_router(test_facilitator());
220        let mut payload = test_payload();
221        payload.proof = vec![0x99]; // Invalid proof
222
223        let request = Request::builder()
224            .uri("/verify")
225            .method("POST")
226            .header("content-type", "application/json")
227            .body(
228                serde_json::to_string(&VerifyRequest {
229                    requirements: payload.accepted.clone(),
230                    payload: payload.clone(),
231                })
232                .unwrap(),
233            )
234            .unwrap();
235
236        let response = app.oneshot(request).await.unwrap();
237        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
238    }
239}