zk402-facilitator 0.1.1

Facilitator implementation for verifying and settling zk402 payments
Documentation
//! On-chain settlement via ethers

use ethers::{
    prelude::*,
    types::{Address as EthAddress, Bytes, U256},
};
use std::sync::Arc;
use zk402_core::{
    PaymentPayload, PublicInputs as CorePublicInputs, SchemeError,
};

abigen!(
    ZkSettleVerifier,
    "$CARGO_MANIFEST_DIR/ZkSettleVerifier.abi.json"
);

/// Configuration for on-chain settlement
#[derive(Clone)]
pub struct SettlementConfig {
    pub rpc_url: String,
    pub verifier_address: [u8; 20],
    pub private_key: Option<String>,
}

impl SettlementConfig {
    pub fn new(rpc_url: String, verifier_address: [u8; 20]) -> Self {
        Self {
            rpc_url,
            verifier_address,
            private_key: None,
        }
    }

    pub fn with_private_key(mut self, key: String) -> Self {
        self.private_key = Some(key);
        self
    }
}

/// Submit a settlement transaction to the on-chain verifier contract
pub async fn submit_settlement(
    config: &SettlementConfig,
    payload: &PaymentPayload,
) -> Result<String, SchemeError> {
    let provider = Provider::<Http>::try_from(&config.rpc_url)
        .map_err(|e| SchemeError::ChainError(format!("invalid RPC URL: {}", e)))?;

    let client: Arc<SignerMiddleware<Provider<Http>, LocalWallet>> = if let Some(pk) = &config.private_key {
        let wallet: LocalWallet = pk
            .parse()
            .map_err(|e| SchemeError::ChainError(format!("invalid private key: {}", e)))?;
        let chain_id = provider
            .get_chainid()
            .await
            .map_err(|e| SchemeError::ChainError(format!("failed to get chain ID: {}", e)))?;
        Arc::new(SignerMiddleware::new(provider, wallet.with_chain_id(chain_id.as_u64())))
    } else {
        return Err(SchemeError::Internal("no signer configured".to_string()));
    };

    let verifier_address = EthAddress::from_slice(&config.verifier_address);
    let contract = ZkSettleVerifier::new(verifier_address, client);

    // Convert PublicInputs to Solidity tuple format
    let inputs_tuple = convert_public_inputs(&payload.public_inputs)?;
    let proof = Bytes::from(payload.proof.clone());

    // Submit transaction and await receipt
    let receipt = contract
        .settle(proof, inputs_tuple)
        .send()
        .await
        .map_err(|e| SchemeError::ChainError(format!("transaction failed: {}", e)))?
        .await
        .map_err(|e| SchemeError::ChainError(format!("failed to get receipt: {}", e)))?
        .ok_or_else(|| SchemeError::ChainError("transaction receipt not found".to_string()))?;

    Ok(format!("0x{}", hex::encode(receipt.transaction_hash.as_bytes())))
}

fn convert_public_inputs(
    inputs: &CorePublicInputs,
) -> Result<PublicInputs, SchemeError> {
    let amount = inputs
        .amount
        .parse::<U256>()
        .map_err(|e| SchemeError::Internal(format!("invalid amount: {}", e)))?;

    Ok(PublicInputs {
        amount,
        asset: EthAddress::from_slice(&inputs.asset),
        pay_to: EthAddress::from_slice(&inputs.pay_to),
        from: EthAddress::from_slice(&inputs.from),
        network: inputs.network.clone(),
        nonce: inputs.nonce,
        valid_after: inputs.valid_after,
        valid_before: inputs.valid_before,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn converts_public_inputs_correctly() {
        let inputs = CorePublicInputs {
            amount: "100".to_string(),
            asset: [1u8; 20],
            pay_to: [2u8; 20],
            from: [3u8; 20],
            network: "eip155:31337".to_string(),
            nonce: [4u8; 32],
            valid_after: 1000,
            valid_before: 2000,
            public_key: vec![5u8; 32],
        };

        let result = convert_public_inputs(&inputs).unwrap();
        assert_eq!(result.amount, U256::from(100));
        assert_eq!(result.network, "eip155:31337");
        assert_eq!(result.valid_after, 1000);
        assert_eq!(result.valid_before, 2000);
    }
}