Skip to main content

zk402_wasm/
lib.rs

1//! WASM bindings for zk402 client-side proof generation
2//!
3//! Exposes a single function: `build_payload(requirements_json, witness_json) -> payload_json`
4
5use wasm_bindgen::prelude::*;
6use zk402_prover::{build_payload as prover_build_payload, ClientWitness, ProverConfig};
7
8/// Set up panic hook for better error messages in browser console
9#[wasm_bindgen(start)]
10pub fn init() {
11    console_error_panic_hook::set_once();
12}
13
14/// Input structure for WASM build_payload function
15#[derive(serde::Deserialize)]
16#[serde(rename_all = "camelCase")]
17struct WasmWitnessInput {
18    secret_key: String,           // hex-encoded
19    from: String,                 // 0x-prefixed hex address
20    payment_nonce: String,        // 0x-prefixed hex (32 bytes)
21    proving_key: String,          // hex-encoded or base64
22    valid_after: u64,
23    valid_before: u64,
24}
25
26/// Build a payment payload from requirements and witness data
27///
28/// # Arguments
29/// * `requirements_json` - JSON string of PaymentRequirements (camelCase)
30/// * `witness_json` - JSON string containing witness data and config
31///
32/// # Returns
33/// JSON string of PaymentPayload (camelCase), or error message
34///
35/// # Example (JavaScript)
36/// ```js
37/// const requirements = {
38///   scheme: "zk-settle",
39///   network: "eip155:8453",
40///   amount: "100",
41///   asset: "0x...",
42///   payTo: "0x...",
43///   maxTimeoutSeconds: 3600,
44///   extra: {
45///     proofSystem: "groth16",
46///     circuitId: "zk-settle-transfer-v1",
47///     verifierAddress: "0x..."
48///   }
49/// };
50///
51/// const witness = {
52///   secretKey: "0x...",
53///   from: "0x...",
54///   paymentNonce: "0x...",
55///   provingKey: "0x...",
56///   validAfter: 1700000000,
57///   validBefore: 1700003600
58/// };
59///
60/// const payload = build_payload(
61///   JSON.stringify(requirements),
62///   JSON.stringify(witness)
63/// );
64/// console.log(JSON.parse(payload));
65/// ```
66#[wasm_bindgen]
67pub fn build_payload(requirements_json: &str, witness_json: &str) -> Result<String, JsValue> {
68    // Parse requirements
69    let requirements: zk402_core::PaymentRequirements = serde_json::from_str(requirements_json)
70        .map_err(|e| JsValue::from_str(&format!("failed to parse requirements: {}", e)))?;
71
72    // Parse witness input
73    let witness_input: WasmWitnessInput = serde_json::from_str(witness_json)
74        .map_err(|e| JsValue::from_str(&format!("failed to parse witness: {}", e)))?;
75
76    // Decode hex fields
77    let secret_key = decode_hex(&witness_input.secret_key)?;
78    let from = decode_address(&witness_input.from)?;
79    let payment_nonce = decode_nonce(&witness_input.payment_nonce)?;
80    let proving_key = decode_hex(&witness_input.proving_key)?;
81
82    // Build witness
83    let witness = ClientWitness {
84        secret_key,
85        from,
86        payment_nonce,
87    };
88
89    // Build config
90    let config = ProverConfig {
91        proving_key,
92        valid_after: witness_input.valid_after,
93        valid_before: witness_input.valid_before,
94    };
95
96    // Generate payload
97    let payload = prover_build_payload(&requirements, &witness, &config)
98        .map_err(|e| JsValue::from_str(&format!("proof generation failed: {}", e)))?;
99
100    // Serialize to JSON
101    serde_json::to_string(&payload)
102        .map_err(|e| JsValue::from_str(&format!("failed to serialize payload: {}", e)))
103}
104
105/// Decode hex string (with or without 0x prefix)
106fn decode_hex(s: &str) -> Result<Vec<u8>, JsValue> {
107    let s = s.strip_prefix("0x").unwrap_or(s);
108    hex::decode(s).map_err(|e| JsValue::from_str(&format!("invalid hex: {}", e)))
109}
110
111/// Decode 20-byte address
112fn decode_address(s: &str) -> Result<[u8; 20], JsValue> {
113    let bytes = decode_hex(s)?;
114    bytes
115        .try_into()
116        .map_err(|_| JsValue::from_str("address must be 20 bytes"))
117}
118
119/// Decode 32-byte nonce
120fn decode_nonce(s: &str) -> Result<[u8; 32], JsValue> {
121    let bytes = decode_hex(s)?;
122    bytes
123        .try_into()
124        .map_err(|_| JsValue::from_str("nonce must be 32 bytes"))
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn decodes_hex_with_prefix() {
133        let result = decode_hex("0x1234");
134        assert!(result.is_ok());
135        assert_eq!(result.unwrap(), vec![0x12, 0x34]);
136    }
137
138    #[test]
139    fn decodes_hex_without_prefix() {
140        let result = decode_hex("1234");
141        assert!(result.is_ok());
142        assert_eq!(result.unwrap(), vec![0x12, 0x34]);
143    }
144
145    #[test]
146    fn decodes_address() {
147        let addr = "0x0000000000000000000000000000000000000001";
148        let result = decode_address(addr);
149        assert!(result.is_ok());
150        assert_eq!(result.unwrap()[19], 1);
151    }
152
153    #[test]
154    fn rejects_invalid_address_length() {
155        let addr = "0x1234"; // Too short
156        let result = decode_address(addr);
157        assert!(result.is_err());
158    }
159}