altius-tx-sdk 0.2.3

SDK for signing and sending Altius USD multi-token transactions
Documentation
//! RPC client for Altius JSON-RPC interactions.

use crate::Result;
use alloy_primitives::{Address, U256};
use serde::Deserialize;
use std::time::Duration;

/// JSON-RPC client for Ethereum-compatible chains
pub struct RpcClient {
    client: reqwest::Client,
    url: String,
}

impl RpcClient {
    /// Create a new RPC client
    pub fn new(url: &str) -> Self {
        Self {
            client: reqwest::Client::builder()
                .timeout(Duration::from_secs(30))
                .build()
                .expect("Failed to build HTTP client"),
            url: url.to_string(),
        }
    }

    /// Send a JSON-RPC request
    async fn request<T: for<'de> Deserialize<'de>>(&self, method: &str, params: serde_json::Value) -> Result<T> {
        let request = serde_json::json!({
            "jsonrpc": "2.0",
            "method": method,
            "params": params,
            "id": 1
        });

        let response = self.client
            .post(&self.url)
            .json(&request)
            .send()
            .await?
            .json::<RpcResponse<T>>()
            .await?;

        if let Some(error) = response.error {
            Err(crate::Error::Rpc(error.message))
        } else {
            response.result.ok_or_else(|| crate::Error::Rpc("null result".to_string()))
        }
    }

    /// Get chain ID
    pub async fn get_chain_id(&self) -> Result<u64> {
        let id: String = self.request("eth_chainId", serde_json::Value::Null).await?;
        Ok(u64::from_str_radix(&id[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
    }

    /// Get block number
    pub async fn get_block_number(&self) -> Result<u64> {
        let block: String = self.request("eth_blockNumber", serde_json::Value::Null).await?;
        Ok(u64::from_str_radix(&block[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
    }

    /// Get balance
    pub async fn get_balance(&self, address: Address) -> Result<U256> {
        let balance: String = self.request("eth_getBalance", serde_json::json!([address.to_string(), "latest"])).await?;
        Ok(U256::from_str_radix(&balance[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
    }

    /// Get ERC20 token balance (using hardcoded selector for simplicity)
    pub async fn get_erc20_balance(&self, token: Address, owner: Address) -> Result<U256> {
        // ERC20 balanceOf(address owner) - selector 0x70a08231
        let owner_hex = owner.to_string();
        let owner_bytes = &owner_hex[2..];
        let data = format!("0x70a082310000000000000000000000000000000000000000000000000000000000000000{}", owner_bytes);

        let result: String = self.request("eth_call", serde_json::json!([
            { "to": token.to_string(), "data": data },
            "latest"
        ])).await?;

        Ok(U256::from_str_radix(&result[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
    }

    /// Get transaction count (nonce) for an address
    pub async fn get_transaction_count(&self, address: Address, block: &str) -> Result<u64> {
        let nonce: String = self.request("eth_getTransactionCount", serde_json::json!([address.to_string(), block])).await?;
        Ok(u64::from_str_radix(&nonce[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
    }

    /// Send raw transaction
    pub async fn send_raw_transaction(&self, tx: String) -> Result<String> {
        self.request("eth_sendRawTransaction", serde_json::json!([tx])).await
    }

    /// Get transaction receipt
    pub async fn get_transaction_receipt(&self, tx_hash: String) -> Result<Option<TransactionReceipt>> {
        self.request("eth_getTransactionReceipt", serde_json::json!([tx_hash])).await
    }

    /// Get transaction by hash
    pub async fn get_transaction_by_hash(&self, tx_hash: String) -> Result<Option<Transaction>> {
        self.request("eth_getTransactionByHash", serde_json::json!([tx_hash])).await
    }

    /// Get code at address
    pub async fn get_code(&self, address: Address) -> Result<String> {
        self.request("eth_getCode", serde_json::json!([address.to_string(), "latest"])).await
    }

    /// Wait for transaction receipt
    pub async fn wait_for_receipt(&self, tx_hash: String, timeout_ms: u64) -> Result<TransactionReceipt> {
        let start = std::time::Instant::now();
        let poll_interval = std::time::Duration::from_millis(500);

        while start.elapsed().as_millis() < timeout_ms as u128 {
            if let Some(receipt) = self.get_transaction_receipt(tx_hash.clone()).await? {
                return Ok(receipt);
            }
            tokio::time::sleep(poll_interval).await;
        }

        Err(crate::Error::Rpc("Transaction receipt timeout".to_string()))
    }
}

/// RPC response wrapper
#[derive(Debug, Deserialize)]
struct RpcResponse<T> {
    #[serde(rename = "result")]
    result: Option<T>,
    #[serde(rename = "error")]
    error: Option<RpcError>,
}

/// RPC error
#[derive(Debug, Deserialize)]
struct RpcError {
    message: String,
}

/// Transaction receipt
#[derive(Debug, Deserialize, Clone)]
pub struct TransactionReceipt {
    pub transaction_hash: String,
    pub block_number: String,
    pub status: String,
    pub gas_used: String,
}

/// Transaction info
#[derive(Debug, Deserialize, Clone)]
pub struct Transaction {
    pub hash: String,
    pub from: String,
    pub to: Option<String>,
    pub value: String,
    pub input: String,
    pub gas: String,
    pub gas_price: Option<String>,
    pub nonce: String,
    #[serde(rename = "type")]
    pub tx_type: String,
}

/// Create a new RPC client (convenience function)
pub fn create_rpc_client(url: &str) -> RpcClient {
    RpcClient::new(url)
}