Skip to main content

altius_tx_sdk/
client.rs

1//! High-level client for sending Altius transactions.
2
3use crate::constants::{BASE_FEE_ATTO, DEFAULT_GAS_LIMIT, USDA_ADDRESS};
4use crate::nonce::NonceManager;
5use crate::rpc::RpcClient;
6use crate::signer::EcdsaSigner;
7use crate::transaction::{Signer as SignerTrait, SignedTx, TxBuilder};
8use crate::Result;
9use alloy_primitives::{Address, U256};
10use std::sync::Arc;
11
12/// TxClient - High-level client for sending Altius transactions
13pub struct TxClient {
14    signer: EcdsaSigner,
15    rpc: Arc<RpcClient>,
16    nonce_manager: NonceManager,
17    fee_token: Address,
18    base_fee: u64,
19    gas_limit: u64,
20    chain_id: u64,
21}
22
23impl TxClient {
24    /// Create a new transaction client
25    pub fn new(private_key: &str, rpc_url: &str, fee_token: &str) -> Result<Self> {
26        let signer = EcdsaSigner::from_private_key(private_key)?;
27        let rpc = Arc::new(RpcClient::new(rpc_url));
28        let fee_token: Address = fee_token.parse().unwrap();
29        let nonce_manager = NonceManager::new(Arc::clone(&rpc), signer.address());
30
31        Ok(Self {
32            signer,
33            rpc,
34            nonce_manager,
35            fee_token,
36            base_fee: BASE_FEE_ATTO,
37            gas_limit: DEFAULT_GAS_LIMIT,
38            chain_id: 0,
39        })
40    }
41
42    /// Get the wallet address
43    pub fn address(&self) -> Address {
44        self.signer.address()
45    }
46
47    /// Get the chain ID
48    pub async fn chain_id(&mut self) -> Result<u64> {
49        if self.chain_id == 0 {
50            self.chain_id = self.rpc.get_chain_id().await?;
51        }
52        Ok(self.chain_id)
53    }
54
55    /// Get current nonce
56    pub async fn get_nonce(&self) -> Result<u64> {
57        self.nonce_manager.get_nonce().await
58    }
59
60    /// Build an ERC20 transfer transaction
61    pub async fn build_erc20_transfer(&mut self, token: Address, recipient: Address, amount: U256) -> Result<TxBuilder> {
62        let chain_id = self.chain_id().await?;
63        let nonce = self.nonce_manager.get_and_increment_nonce().await?;
64
65        Ok(TxBuilder::new()
66            .chain_id(chain_id)
67            .nonce(nonce)
68            .gas_limit(self.gas_limit)
69            .erc20_transfer(token, recipient, amount)
70            .fee_token(self.fee_token)
71            .max_fee_per_gas_usd((self.base_fee as u128) * 2))
72    }
73
74    /// Sign a transaction using the signer
75    pub async fn sign(&mut self, tx: TxBuilder) -> Result<SignedTx> {
76        // Ensure chain_id is loaded
77        let _ = self.chain_id().await?;
78
79        // Sign the transaction
80        let signed = tx.sign(&self.signer)
81            .map_err(|e| crate::Error::SigningFailed(e.to_string()))?;
82
83        Ok(signed)
84    }
85
86    /// Send a signed transaction to the network
87    pub async fn send(&mut self, signed: SignedTx) -> Result<String> {
88        // Send the raw transaction
89        let tx_hash = self.rpc.send_raw_transaction(signed.raw_transaction).await?;
90        Ok(tx_hash)
91    }
92
93    /// Send an ERC20 transfer transaction
94    pub async fn send_erc20_transfer(&mut self, token: Address, recipient: Address, amount: U256) -> Result<String> {
95        let tx = self.build_erc20_transfer(token, recipient, amount).await?;
96        let signed = self.sign(tx).await?;
97        self.send(signed).await
98    }
99
100    /// Send a transaction and wait for receipt
101    pub async fn send_and_wait(&mut self, signed: SignedTx, timeout_ms: u64) -> Result<crate::rpc::TransactionReceipt> {
102        let tx_hash = self.send(signed).await?;
103        self.rpc.wait_for_receipt(tx_hash, timeout_ms).await
104    }
105
106    /// Transfer USDA (fee token)
107    pub async fn transfer_usda(&mut self, recipient: Address, amount_micro: u64) -> Result<String> {
108        // amount_micro is in microdollars, convert to wei (18 decimals)
109        let amount_wei = U256::from(amount_micro) * U256::from(1_000_000_000_000_u64);
110        self.send_erc20_transfer(self.fee_token, recipient, amount_wei).await
111    }
112
113    /// Get fee token balance
114    pub async fn fee_token_balance(&self, address: Address) -> Result<U256> {
115        self.rpc.get_erc20_balance(self.fee_token, address).await
116    }
117
118    /// Get native balance
119    pub async fn native_balance(&self, address: Address) -> Result<U256> {
120        self.rpc.get_balance(address).await
121    }
122
123    /// Reset the nonce cache
124    pub fn reset_nonce(&self) {
125        self.nonce_manager.reset_nonce();
126    }
127}
128
129/// TxClientBuilder - Builder for TxClient
130pub struct TxClientBuilder {
131    private_key: String,
132    rpc_url: String,
133    fee_token: String,
134    base_fee: u64,
135    gas_limit: u64,
136}
137
138impl TxClientBuilder {
139    pub fn new(private_key: &str, rpc_url: &str) -> Self {
140        Self {
141            private_key: private_key.to_string(),
142            rpc_url: rpc_url.to_string(),
143            fee_token: USDA_ADDRESS.to_string(),
144            base_fee: BASE_FEE_ATTO,
145            gas_limit: DEFAULT_GAS_LIMIT,
146        }
147    }
148
149    pub fn fee_token(mut self, fee_token: &str) -> Self {
150        self.fee_token = fee_token.to_string();
151        self
152    }
153
154    pub fn base_fee(mut self, base_fee: u64) -> Self {
155        self.base_fee = base_fee;
156        self
157    }
158
159    pub fn gas_limit(mut self, gas_limit: u64) -> Self {
160        self.gas_limit = gas_limit;
161        self
162    }
163
164    pub fn build(&self) -> Result<TxClient> {
165        let mut client = TxClient::new(&self.private_key, &self.rpc_url, &self.fee_token)?;
166        client.base_fee = self.base_fee;
167        client.gas_limit = self.gas_limit;
168        Ok(client)
169    }
170}
171
172/// Create a new TxClient (convenience function)
173pub fn create_tx_client(private_key: &str, rpc_url: &str, fee_token: &str) -> Result<TxClient> {
174    TxClient::new(private_key, rpc_url, fee_token)
175}