polynode 0.13.4

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, FEE_ESCROW_ADDRESS_V2};
use super::signer::TradingSigner;
use super::types::{Eip712Payload, ExchangeVersion, FeeAuthRequest};
use alloy_primitives::U256;

const ZERO_ADDRESS: &str = "0x0000000000000000000000000000000000000000";

/// Return the FeeEscrow contract address for a given exchange version.
/// V1 = USDC.e collateral; V2 = pUSD collateral.
pub fn fee_escrow_address_for(version: ExchangeVersion) -> &'static str {
    match version {
        ExchangeVersion::V2 => FEE_ESCROW_ADDRESS_V2,
        _ => FEE_ESCROW_ADDRESS,
    }
}

/// Return the EIP-712 FeeAuth domain name for a given exchange version.
fn fee_auth_domain_name(version: ExchangeVersion) -> &'static str {
    match version {
        ExchangeVersion::V2 => "PolyNodeFeeEscrowV2",
        _ => "PolyNodeFeeEscrow",
    }
}

/// Return the EIP-712 FeeAuth domain version for a given exchange version.
fn fee_auth_domain_version(version: ExchangeVersion) -> &'static str {
    match version {
        ExchangeVersion::V2 => "2",
        _ => "1",
    }
}

// ── 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 a FeeEscrow contract via eth_call.
///
/// Pass `fee_escrow_address_for(exchange_version)` for the `escrow_address` when the
/// caller's exchange version is known — V1 and V2 nonces are tracked independently on
/// separate contracts.
pub async fn fetch_escrow_nonce(
    rpc_url: &str,
    signer_address: &str,
    escrow_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": 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.
///
/// `exchange_version` selects the FeeEscrow contract + EIP-712 domain:
///   V1: FeeEscrow (USDC.e collateral), domain ("PolyNodeFeeEscrow", "1")
///   V2: FeeEscrowV2 (pUSD collateral), domain ("PolyNodeFeeEscrowV2", "2")
/// On V2 the returned `FeeAuthRequest` carries `escrow_contract = Some(V2 addr)`,
/// which the polynode cosigner uses to route `pullFee` to the V2 operator.
#[allow(clippy::too_many_arguments)]
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>,
    exchange_version: ExchangeVersion,
) -> Result<FeeAuthRequest> {
    let signer_address = format!("{}", signer.address());
    let escrow_addr = fee_escrow_address_for(exchange_version);

    let payload = Eip712Payload {
        domain: serde_json::json!({
            "name": fee_auth_domain_name(exchange_version),
            "version": fee_auth_domain_version(exchange_version),
            "chainId": CHAIN_ID,
            "verifyingContract": escrow_addr
        }),
        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));

    let escrow_contract = match exchange_version {
        ExchangeVersion::V2 => Some(FEE_ESCROW_ADDRESS_V2.to_string()),
        _ => None,
    };

    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),
        escrow_contract,
    })
}