polynode 0.8.1

Rust SDK for the PolyNode API — real-time Polymarket data
Documentation
//! Fee Escrow — EIP-712 signing, fee calculation, and nonce fetching
//! for the on-chain FeeEscrow contract.
//!
//! The escrow is completely optional. When fee_bps=0, none of this code runs.

use crate::error::{Error, Result};
use super::constants::{CHAIN_ID, FEE_ESCROW_ADDRESS};
use super::signer::TradingSigner;
use super::types::{Eip712Payload, FeeAuthRequest};
use alloy_primitives::U256;

const ZERO_ADDRESS: &str = "0x0000000000000000000000000000000000000000";

// ── Fee Calculation ──

/// Calculate fee amount in raw USDC (6 decimals).
///
/// price: Order price (e.g. 0.55)
/// size: Order size in tokens (e.g. 100)
/// fee_bps: Fee in basis points (e.g. 50 = 0.5%)
pub fn calculate_fee(price: f64, size: f64, fee_bps: u16) -> u64 {
    let notional_raw = (price * size * 1e6).floor() as i64;
    let fee = (notional_raw as f64 * fee_bps as f64 / 10000.0).floor() as i64;
    if fee > 0 { fee as u64 } else { 0 }
}

// ── Order ID Generation ──

/// Generate a random bytes32 escrow order ID (0x-prefixed hex).
pub fn generate_escrow_order_id() -> String {
    use rand::Rng;
    let mut rng = rand::thread_rng();
    let bytes: [u8; 32] = rng.gen();
    format!("0x{}", hex::encode(bytes))
}

// ── Nonce Fetching ──

/// Fetch the current nonce for a signer from the FeeEscrow contract via eth_call.
pub async fn fetch_escrow_nonce(rpc_url: &str, signer_address: &str) -> Result<u64> {
    let client = reqwest::Client::new();
    // getNonce(address) selector = 0x2d0335ab
    let addr = signer_address.to_lowercase().replace("0x", "");
    let data = format!("0x2d0335ab{:0>64}", addr);

    let resp = client
        .post(rpc_url)
        .json(&serde_json::json!({
            "jsonrpc": "2.0",
            "id": 1,
            "method": "eth_call",
            "params": [{"to": FEE_ESCROW_ADDRESS, "data": data}, "latest"]
        }))
        .send()
        .await
        .map_err(|e| Error::Trading(format!("Escrow nonce fetch failed: {}", e)))?;

    let json: serde_json::Value = resp.json().await
        .map_err(|e| Error::Trading(format!("Escrow nonce parse failed: {}", e)))?;

    if let Some(err) = json.get("error") {
        return Err(Error::Trading(format!("Escrow nonce RPC error: {}", err)));
    }

    let hex = json.get("result").and_then(|v| v.as_str()).unwrap_or("0x0");
    let nonce = u64::from_str_radix(hex.trim_start_matches("0x"), 16)
        .map_err(|e| Error::Trading(format!("Escrow nonce parse int failed: {}", e)))?;

    Ok(nonce)
}

// ── EIP-712 Signing ──

/// Sign a FeeAuth EIP-712 message using the trading signer.
pub async fn sign_fee_auth(
    signer: &dyn TradingSigner,
    escrow_order_id: &str,
    payer: &str,
    fee_amount: u64,
    deadline: u64,
    nonce: u64,
    affiliate: Option<&str>,
    affiliate_share_bps: Option<u16>,
) -> Result<FeeAuthRequest> {
    let signer_address = format!("{}", signer.address());

    let payload = Eip712Payload {
        domain: serde_json::json!({
            "name": "PolyNodeFeeEscrow",
            "version": "1",
            "chainId": CHAIN_ID,
            "verifyingContract": FEE_ESCROW_ADDRESS
        }),
        types: serde_json::json!({
            "FeeAuth": [
                {"name": "orderId", "type": "bytes32"},
                {"name": "payer", "type": "address"},
                {"name": "signer", "type": "address"},
                {"name": "feeAmount", "type": "uint256"},
                {"name": "deadline", "type": "uint256"},
                {"name": "nonce", "type": "uint256"}
            ]
        }),
        primary_type: "FeeAuth".into(),
        message: serde_json::json!({
            "orderId": escrow_order_id,
            "payer": payer,
            "signer": signer_address,
            "feeAmount": fee_amount.to_string(),
            "deadline": deadline.to_string(),
            "nonce": nonce.to_string()
        }),
    };

    let sig_bytes = signer.sign_typed_data(&payload).await?;
    let signature = format!("0x{}", hex::encode(&sig_bytes));

    Ok(FeeAuthRequest {
        escrow_order_id: escrow_order_id.to_string(),
        payer: payer.to_string(),
        signer: signer_address,
        fee_amount: fee_amount.to_string(),
        deadline,
        nonce,
        signature,
        affiliate: affiliate.unwrap_or(ZERO_ADDRESS).to_string(),
        affiliate_share_bps: affiliate_share_bps.unwrap_or(10000),
    })
}