Skip to main content

altius_tx_sdk/
rpc.rs

1//! RPC client for Altius JSON-RPC interactions.
2
3use crate::Result;
4use alloy_primitives::{Address, U256};
5use serde::Deserialize;
6use std::time::Duration;
7
8/// JSON-RPC client for Ethereum-compatible chains
9pub struct RpcClient {
10    client: reqwest::Client,
11    url: String,
12}
13
14impl RpcClient {
15    /// Create a new RPC client
16    pub fn new(url: &str) -> Self {
17        Self {
18            client: reqwest::Client::builder()
19                .timeout(Duration::from_secs(30))
20                .build()
21                .expect("Failed to build HTTP client"),
22            url: url.to_string(),
23        }
24    }
25
26    /// Send a JSON-RPC request
27    async fn request<T: for<'de> Deserialize<'de>>(&self, method: &str, params: serde_json::Value) -> Result<T> {
28        let request = serde_json::json!({
29            "jsonrpc": "2.0",
30            "method": method,
31            "params": params,
32            "id": 1
33        });
34
35        let response = self.client
36            .post(&self.url)
37            .json(&request)
38            .send()
39            .await?
40            .json::<RpcResponse<T>>()
41            .await?;
42
43        if let Some(error) = response.error {
44            Err(crate::Error::Rpc(error.message))
45        } else {
46            response.result.ok_or_else(|| crate::Error::Rpc("null result".to_string()))
47        }
48    }
49
50    /// Get chain ID
51    pub async fn get_chain_id(&self) -> Result<u64> {
52        let id: String = self.request("eth_chainId", serde_json::Value::Null).await?;
53        Ok(u64::from_str_radix(&id[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
54    }
55
56    /// Get block number
57    pub async fn get_block_number(&self) -> Result<u64> {
58        let block: String = self.request("eth_blockNumber", serde_json::Value::Null).await?;
59        Ok(u64::from_str_radix(&block[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
60    }
61
62    /// Get balance
63    pub async fn get_balance(&self, address: Address) -> Result<U256> {
64        let balance: String = self.request("eth_getBalance", serde_json::json!([address.to_string(), "latest"])).await?;
65        Ok(U256::from_str_radix(&balance[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
66    }
67
68    /// Get ERC20 token balance (using hardcoded selector for simplicity)
69    pub async fn get_erc20_balance(&self, token: Address, owner: Address) -> Result<U256> {
70        // ERC20 balanceOf(address owner) - selector 0x70a08231
71        let owner_hex = owner.to_string();
72        let owner_bytes = &owner_hex[2..];
73        let data = format!("0x70a082310000000000000000000000000000000000000000000000000000000000000000{}", owner_bytes);
74
75        let result: String = self.request("eth_call", serde_json::json!([
76            { "to": token.to_string(), "data": data },
77            "latest"
78        ])).await?;
79
80        Ok(U256::from_str_radix(&result[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
81    }
82
83    /// Get transaction count (nonce) for an address
84    pub async fn get_transaction_count(&self, address: Address, block: &str) -> Result<u64> {
85        let nonce: String = self.request("eth_getTransactionCount", serde_json::json!([address.to_string(), block])).await?;
86        Ok(u64::from_str_radix(&nonce[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
87    }
88
89    /// Send raw transaction
90    pub async fn send_raw_transaction(&self, tx: String) -> Result<String> {
91        self.request("eth_sendRawTransaction", serde_json::json!([tx])).await
92    }
93
94    /// Get transaction receipt
95    pub async fn get_transaction_receipt(&self, tx_hash: String) -> Result<Option<TransactionReceipt>> {
96        self.request("eth_getTransactionReceipt", serde_json::json!([tx_hash])).await
97    }
98
99    /// Get transaction by hash
100    pub async fn get_transaction_by_hash(&self, tx_hash: String) -> Result<Option<Transaction>> {
101        self.request("eth_getTransactionByHash", serde_json::json!([tx_hash])).await
102    }
103
104    /// Get code at address
105    pub async fn get_code(&self, address: Address) -> Result<String> {
106        self.request("eth_getCode", serde_json::json!([address.to_string(), "latest"])).await
107    }
108
109    /// Wait for transaction receipt
110    pub async fn wait_for_receipt(&self, tx_hash: String, timeout_ms: u64) -> Result<TransactionReceipt> {
111        let start = std::time::Instant::now();
112        let poll_interval = std::time::Duration::from_millis(500);
113
114        while start.elapsed().as_millis() < timeout_ms as u128 {
115            if let Some(receipt) = self.get_transaction_receipt(tx_hash.clone()).await? {
116                return Ok(receipt);
117            }
118            tokio::time::sleep(poll_interval).await;
119        }
120
121        Err(crate::Error::Rpc("Transaction receipt timeout".to_string()))
122    }
123}
124
125/// RPC response wrapper
126#[derive(Debug, Deserialize)]
127struct RpcResponse<T> {
128    #[serde(rename = "result")]
129    result: Option<T>,
130    #[serde(rename = "error")]
131    error: Option<RpcError>,
132}
133
134/// RPC error
135#[derive(Debug, Deserialize)]
136struct RpcError {
137    message: String,
138}
139
140/// Transaction receipt
141#[derive(Debug, Deserialize, Clone)]
142pub struct TransactionReceipt {
143    pub transaction_hash: String,
144    pub block_number: String,
145    pub status: String,
146    pub gas_used: String,
147}
148
149/// Transaction info
150#[derive(Debug, Deserialize, Clone)]
151pub struct Transaction {
152    pub hash: String,
153    pub from: String,
154    pub to: Option<String>,
155    pub value: String,
156    pub input: String,
157    pub gas: String,
158    pub gas_price: Option<String>,
159    pub nonce: String,
160    #[serde(rename = "type")]
161    pub tx_type: String,
162}
163
164/// Create a new RPC client (convenience function)
165pub fn create_rpc_client(url: &str) -> RpcClient {
166    RpcClient::new(url)
167}