rrelayer_core 0.7.0

Core types and functionality for rrelayer - a powerful blockchain transaction relay service
Documentation
use crate::common_types::EvmAddress;
use crate::network::ChainId;
use crate::wallet::{WalletError, WalletManagerTrait};
use alloy::consensus::{TxEnvelope, TypedTransaction};
use alloy::dyn_abi::TypedData;
use alloy::primitives::Signature;
use alloy_rlp::Decodable;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str::FromStr;
use tokio::sync::Mutex;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrivyWallet {
    pub id: String,
    pub address: EvmAddress,
    pub chain_type: String,
    pub created_at: u64,
}

#[derive(Debug)]
pub struct PrivyWalletManager {
    pub app_id: String,
    pub app_secret: String,
    pub wallets: Mutex<HashMap<u32, PrivyWallet>>,
    pub client: reqwest::Client,
}

impl PrivyWalletManager {
    pub async fn new(app_id: String, app_secret: String) -> Result<Self, WalletError> {
        let client = reqwest::Client::new();
        let manager = Self {
            app_id: app_id.clone(),
            app_secret,
            wallets: Mutex::new(HashMap::new()),
            client,
        };

        manager.load_wallets().await?;
        Ok(manager)
    }

    async fn load_wallets(&self) -> Result<(), WalletError> {
        let mut all_wallets = Vec::new();
        let mut cursor: Option<String> = None;

        loop {
            let mut url = "https://api.privy.io/v1/wallets".to_string();
            if let Some(ref cursor_value) = cursor {
                url.push_str(&format!("?cursor={}", cursor_value));
            }

            let response = self
                .client
                .get(&url)
                .basic_auth(&self.app_id, Some(&self.app_secret))
                .header("privy-app-id", &self.app_id)
                .header("Content-Type", "application/json")
                .send()
                .await?;

            let response_data: serde_json::Value = response.json().await?;

            let wallets_in_page: Vec<PrivyWallet> =
                serde_json::from_value(response_data["data"].clone())?;

            all_wallets.extend(wallets_in_page);

            cursor = response_data["next_cursor"].as_str().map(|s| s.to_string());

            if cursor.is_none() {
                break;
            }
        }

        let mut wallets = self.wallets.lock().await;
        for (index, wallet) in all_wallets.into_iter().enumerate() {
            wallets.insert(index as u32, wallet);
        }

        Ok(())
    }
}

#[async_trait]
impl WalletManagerTrait for PrivyWalletManager {
    async fn create_wallet(
        &self,
        wallet_index: u32,
        _chain_id: &ChainId,
    ) -> Result<EvmAddress, WalletError> {
        self.load_wallets().await?;

        {
            let wallets = self.wallets.lock().await;
            if let Some(wallet) = wallets.get(&wallet_index) {
                return Ok(wallet.address);
            }
        }

        let response = self
            .client
            .post("https://api.privy.io/v1/wallets")
            .basic_auth(&self.app_id, Some(&self.app_secret))
            .header("privy-app-id", &self.app_id)
            .header("Content-Type", "application/json")
            .json(&serde_json::json!({
                "chain_type": "ethereum"
            }))
            .send()
            .await?;

        if !response.status().is_success() {
            let error_text = response.text().await?;
            return Err(WalletError::ApiError {
                message: format!("Failed to create wallet: {}", error_text),
            });
        }

        let new_wallet: PrivyWallet = response.json().await?;
        let address = new_wallet.address;

        {
            let mut wallets = self.wallets.lock().await;
            wallets.entry(wallet_index).or_insert(new_wallet);
        }

        Ok(address)
    }

    async fn get_address(
        &self,
        wallet_index: u32,
        _chain_id: &ChainId,
    ) -> Result<EvmAddress, WalletError> {
        let wallets = self.wallets.lock().await;
        let wallet = wallets
            .get(&wallet_index)
            .ok_or(WalletError::WalletNotFound { index: wallet_index })?;
        Ok(wallet.address)
    }

    async fn sign_transaction(
        &self,
        wallet_index: u32,
        transaction: &TypedTransaction,
        _chain_id: &ChainId,
    ) -> Result<Signature, WalletError> {
        let wallets = self.wallets.lock().await;
        let wallet = wallets
            .get(&wallet_index)
            .ok_or(WalletError::WalletNotFound { index: wallet_index })?;

        let privy_transaction = match transaction {
            TypedTransaction::Legacy(tx) => {
                let to_addr = match &tx.to {
                    alloy::primitives::TxKind::Call(addr) => Some(format!("0x{:x}", addr)),
                    alloy::primitives::TxKind::Create => None,
                };
                serde_json::json!({
                    "type": 0,
                    "to": to_addr,
                    "value": format!("0x{:x}", tx.value),
                    "data": format!("0x{}", hex::encode(&tx.input)),
                    "gas_limit": tx.gas_limit,
                    "gas_price": format!("0x{:x}", tx.gas_price),
                    "nonce": tx.nonce
                })
            }
            TypedTransaction::Eip2930(tx) => {
                let to_addr = match &tx.to {
                    alloy::primitives::TxKind::Call(addr) => Some(format!("0x{:x}", addr)),
                    alloy::primitives::TxKind::Create => None,
                };
                serde_json::json!({
                    "type": 1,
                    "to": to_addr,
                    "value": format!("0x{:x}", tx.value),
                    "data": format!("0x{}", hex::encode(&tx.input)),
                    "gas_limit": tx.gas_limit,
                    "gas_price": format!("0x{:x}", tx.gas_price),
                    "nonce": tx.nonce,
                    "chain_id": tx.chain_id
                })
            }
            TypedTransaction::Eip1559(tx) => {
                let to_addr = match &tx.to {
                    alloy::primitives::TxKind::Call(addr) => Some(format!("0x{:x}", addr)),
                    alloy::primitives::TxKind::Create => None,
                };
                serde_json::json!({
                    "type": 2,
                    "to": to_addr,
                    "value": format!("0x{:x}", tx.value),
                    "data": format!("0x{}", hex::encode(&tx.input)),
                    "gas_limit": tx.gas_limit,
                    "max_fee_per_gas": tx.max_fee_per_gas,
                    "max_priority_fee_per_gas": format!("0x{:x}", tx.max_priority_fee_per_gas),
                    "nonce": tx.nonce,
                    "chain_id": tx.chain_id
                })
            }
            TypedTransaction::Eip4844(_) => {
                return Err(WalletError::UnsupportedTransactionType {
                    tx_type: "EIP-4844 blob transactions are not supported by Privy wallet API"
                        .to_string(),
                })
            }
            _ => {
                return Err(WalletError::UnsupportedTransactionType {
                    tx_type: format!("Unsupported transaction type for Privy: {:?}", transaction),
                })
            }
        };

        let response = self
            .client
            .post(format!("https://api.privy.io/v1/wallets/{}/rpc", wallet.id))
            .basic_auth(&self.app_id, Some(&self.app_secret))
            .header("privy-app-id", &self.app_id)
            .header("Content-Type", "application/json")
            .json(&serde_json::json!({
                "method": "eth_signTransaction",
                "params": {
                    "transaction": privy_transaction
                }
            }))
            .send()
            .await?;

        if !response.status().is_success() {
            let error_text = response.text().await?;
            return Err(WalletError::ApiError {
                message: format!("Privy API error: {}", error_text),
            });
        }

        let result: serde_json::Value = response.json().await?;

        if let Some(error) = result.get("error") {
            return Err(WalletError::ApiError {
                message: format!("Privy signing error: {}", error),
            });
        }

        let signed_transaction_hex =
            result["data"]["signed_transaction"].as_str().ok_or(WalletError::ApiError {
                message: "No signed transaction in response data".to_string(),
            })?;

        let tx_bytes = hex::decode(signed_transaction_hex.trim_start_matches("0x"))?;
        let tx_envelope = TxEnvelope::decode(&mut tx_bytes.as_slice())?;

        let signature = match tx_envelope {
            TxEnvelope::Eip1559(signed_tx) => *signed_tx.signature(),
            TxEnvelope::Legacy(signed_tx) => *signed_tx.signature(),
            TxEnvelope::Eip2930(signed_tx) => *signed_tx.signature(),
            _ => {
                return Err(WalletError::UnsupportedTransactionType {
                    tx_type: "Unknown transaction envelope type".to_string(),
                })
            }
        };

        Ok(signature)
    }

    async fn sign_text(
        &self,
        wallet_index: u32,
        text: &str,
        _chain_id: &ChainId,
    ) -> Result<Signature, WalletError> {
        let wallets = self.wallets.lock().await;
        let wallet = wallets
            .get(&wallet_index)
            .ok_or(WalletError::WalletNotFound { index: wallet_index })?;

        let response = self
            .client
            .post(format!("https://api.privy.io/v1/wallets/{}/rpc", wallet.id))
            .basic_auth(&self.app_id, Some(&self.app_secret))
            .header("privy-app-id", &self.app_id)
            .header("Content-Type", "application/json")
            .json(&serde_json::json!({
                "method": "personal_sign",
                "params": {
                    "message": text,
                    "encoding": "utf-8"
                }
            }))
            .send()
            .await?;

        if !response.status().is_success() {
            let error_text = response.text().await?;
            return Err(WalletError::ApiError {
                message: format!("Privy API error: {}", error_text),
            });
        }

        let result: serde_json::Value = response.json().await?;

        if let Some(error) = result.get("error") {
            return Err(WalletError::ApiError {
                message: format!("Privy signing error: {}", error),
            });
        }

        let signature_hex = result["data"]["signature"].as_str().ok_or(WalletError::ApiError {
            message: "No signature in response data".to_string(),
        })?;

        let signature = Signature::from_str(signature_hex)?;
        Ok(signature)
    }

    async fn sign_typed_data(
        &self,
        wallet_index: u32,
        typed_data: &TypedData,
        _chain_id: &ChainId,
    ) -> Result<Signature, WalletError> {
        let wallets = self.wallets.lock().await;
        let wallet = wallets
            .get(&wallet_index)
            .ok_or(WalletError::WalletNotFound { index: wallet_index })?;

        let privy_typed_data = serde_json::json!({
            "types": typed_data.resolver,
            "message": typed_data.message,
            "primary_type": typed_data.primary_type,
            "domain": typed_data.domain,
        });

        let response = self
            .client
            .post(format!("https://api.privy.io/v1/wallets/{}/rpc", wallet.id))
            .basic_auth(&self.app_id, Some(&self.app_secret))
            .header("privy-app-id", &self.app_id)
            .header("Content-Type", "application/json")
            .json(&serde_json::json!({
                "method": "eth_signTypedData_v4",
                "params": {
                    "typed_data": privy_typed_data
                }
            }))
            .send()
            .await?;

        if !response.status().is_success() {
            let error_text = response.text().await?;
            return Err(WalletError::ApiError {
                message: format!("Privy API error: {}", error_text),
            });
        }

        let result: serde_json::Value = response.json().await?;

        if let Some(error) = result.get("error") {
            return Err(WalletError::ApiError {
                message: format!("Privy signing error: {}", error),
            });
        }

        let signature_hex = result["data"]["signature"].as_str().ok_or(WalletError::ApiError {
            message: "No signature in response data".to_string(),
        })?;

        let signature = Signature::from_str(signature_hex)?;
        Ok(signature)
    }

    fn supports_blobs(&self) -> bool {
        false
    }
}