use alloy_primitives::Address;
use async_trait::async_trait;
use crate::error::{Error, Result};
use super::signer::TradingSigner;
use super::types::Eip712Payload;
#[derive(Debug, Clone)]
pub struct PrivyConfig {
pub app_id: String,
pub app_secret: String,
pub authorization_key: String,
}
impl PrivyConfig {
pub fn from_env() -> Result<Self> {
Ok(Self {
app_id: std::env::var("PRIVY_APP_ID")
.map_err(|_| Error::Trading("Missing PRIVY_APP_ID".into()))?,
app_secret: std::env::var("PRIVY_APP_SECRET")
.map_err(|_| Error::Trading("Missing PRIVY_APP_SECRET".into()))?,
authorization_key: std::env::var("PRIVY_AUTHORIZATION_KEY")
.map_err(|_| Error::Trading("Missing PRIVY_AUTHORIZATION_KEY".into()))?,
})
}
}
pub struct PrivySigner {
client: reqwest::Client,
config: PrivyConfig,
wallet_id: String,
wallet_address: Address,
}
impl PrivySigner {
pub fn new(config: PrivyConfig, wallet_id: String, wallet_address: Address) -> Self {
Self {
client: reqwest::Client::new(),
config,
wallet_id,
wallet_address,
}
}
pub fn from_env(wallet_id: String, wallet_address: Address) -> Result<Self> {
Ok(Self::new(PrivyConfig::from_env()?, wallet_id, wallet_address))
}
fn auth_headers(&self) -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
let basic = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
format!("{}:{}", self.config.app_id, self.config.app_secret),
);
headers.insert("Authorization", format!("Basic {}", basic).parse().unwrap());
headers.insert("privy-app-id", self.config.app_id.parse().unwrap());
headers.insert("Content-Type", "application/json".parse().unwrap());
headers
}
}
#[async_trait]
impl TradingSigner for PrivySigner {
fn address(&self) -> Address {
self.wallet_address
}
async fn sign_typed_data(&self, payload: &Eip712Payload) -> Result<Vec<u8>> {
let types = payload.types.clone();
let body = serde_json::json!({
"address": format!("{}", self.wallet_address),
"method": "eth_signTypedData_v4",
"params": {
"typed_data": {
"domain": payload.domain,
"types": types,
"primary_type": payload.primary_type,
"message": payload.message,
}
}
});
let resp = self.client
.post(format!(
"https://api.privy.io/v1/wallets/{}/rpc",
self.wallet_id
))
.headers(self.auth_headers())
.json(&body)
.send()
.await?;
if !resp.status().is_success() {
let err = resp.text().await.unwrap_or_default();
return Err(Error::Signing(format!("Privy signTypedData failed: {}", err)));
}
let data: serde_json::Value = resp.json().await?;
extract_privy_signature(&data)
}
async fn sign_message(&self, message: &[u8]) -> Result<Vec<u8>> {
let body = serde_json::json!({
"address": format!("{}", self.wallet_address),
"method": "personal_sign",
"params": {
"message": format!("0x{}", hex::encode(message)),
"encoding": "hex",
}
});
let resp = self.client
.post(format!(
"https://api.privy.io/v1/wallets/{}/rpc",
self.wallet_id
))
.headers(self.auth_headers())
.json(&body)
.send()
.await?;
if !resp.status().is_success() {
let err = resp.text().await.unwrap_or_default();
return Err(Error::Signing(format!("Privy signMessage failed: {}", err)));
}
let data: serde_json::Value = resp.json().await?;
extract_privy_signature(&data)
}
}
fn extract_privy_signature(data: &serde_json::Value) -> Result<Vec<u8>> {
let sig_hex = data.get("data")
.and_then(|d| {
d.as_str().map(|s| s.to_string())
.or_else(|| d.get("signature").and_then(|v| v.as_str()).map(|s| s.to_string()))
})
.or_else(|| data.get("signature").and_then(|v| v.as_str()).map(|s| s.to_string()))
.ok_or_else(|| Error::Signing(format!("No signature in Privy response: {}", data)))?;
let mut sig_bytes = hex::decode(sig_hex.strip_prefix("0x").unwrap_or(&sig_hex))
.map_err(|e| Error::Signing(format!("Invalid signature hex: {}", e)))?;
if sig_bytes.len() == 65 && sig_bytes[64] >= 27 {
sig_bytes[64] -= 27;
}
Ok(sig_bytes)
}