altius-tx-sdk 0.2.5

SDK for signing and sending Altius USD multi-token transactions
Documentation
//! High-level client for sending Altius transactions.

use crate::constants::{BASE_FEE_ATTO, DEFAULT_GAS_LIMIT, USDA_ADDRESS};
use crate::nonce::NonceManager;
use crate::rpc::RpcClient;
use crate::signer::EcdsaSigner;
use crate::transaction::{Signer as SignerTrait, SignedTx, TxBuilder};
use crate::Result;
use alloy_primitives::{Address, U256};
use std::sync::Arc;

/// TxClient - High-level client for sending Altius transactions
pub struct TxClient {
    signer: EcdsaSigner,
    rpc: Arc<RpcClient>,
    nonce_manager: NonceManager,
    fee_token: Address,
    base_fee: u64,
    gas_limit: u64,
    chain_id: u64,
}

impl TxClient {
    /// Create a new transaction client
    pub fn new(private_key: &str, rpc_url: &str, fee_token: &str) -> Result<Self> {
        let signer = EcdsaSigner::from_private_key(private_key)?;
        let rpc = Arc::new(RpcClient::new(rpc_url));
        let fee_token: Address = fee_token.parse().unwrap();
        let nonce_manager = NonceManager::new(Arc::clone(&rpc), signer.address());

        Ok(Self {
            signer,
            rpc,
            nonce_manager,
            fee_token,
            base_fee: BASE_FEE_ATTO,
            gas_limit: DEFAULT_GAS_LIMIT,
            chain_id: 0,
        })
    }

    /// Get the wallet address
    pub fn address(&self) -> Address {
        self.signer.address()
    }

    /// Get the chain ID
    pub async fn chain_id(&mut self) -> Result<u64> {
        if self.chain_id == 0 {
            self.chain_id = self.rpc.get_chain_id().await?;
        }
        Ok(self.chain_id)
    }

    /// Get current nonce
    pub async fn get_nonce(&self) -> Result<u64> {
        self.nonce_manager.get_nonce().await
    }

    /// Build an ERC20 transfer transaction
    pub async fn build_erc20_transfer(&mut self, token: Address, recipient: Address, amount: U256) -> Result<TxBuilder> {
        let chain_id = self.chain_id().await?;
        let nonce = self.nonce_manager.get_and_increment_nonce().await?;

        Ok(TxBuilder::new()
            .chain_id(chain_id)
            .nonce(nonce)
            .gas_limit(self.gas_limit)
            .erc20_transfer(token, recipient, amount)
            .fee_token(self.fee_token)
            .max_fee_per_gas_usd((self.base_fee as u128) * 2))
    }

    /// Sign a transaction using the signer
    pub async fn sign(&mut self, tx: TxBuilder) -> Result<SignedTx> {
        // Ensure chain_id is loaded
        let _ = self.chain_id().await?;

        // Sign the transaction
        let signed = tx.sign(&self.signer)
            .map_err(|e| crate::Error::SigningFailed(e.to_string()))?;

        Ok(signed)
    }

    /// Send a signed transaction to the network
    pub async fn send(&mut self, signed: SignedTx) -> Result<String> {
        // Send the raw transaction
        let tx_hash = self.rpc.send_raw_transaction(signed.raw_transaction).await?;
        Ok(tx_hash)
    }

    /// Send an ERC20 transfer transaction
    pub async fn send_erc20_transfer(&mut self, token: Address, recipient: Address, amount: U256) -> Result<String> {
        let tx = self.build_erc20_transfer(token, recipient, amount).await?;
        let signed = self.sign(tx).await?;
        self.send(signed).await
    }

    /// Send a transaction and wait for receipt
    pub async fn send_and_wait(&mut self, signed: SignedTx, timeout_ms: u64) -> Result<crate::rpc::TransactionReceipt> {
        let tx_hash = self.send(signed).await?;
        self.rpc.wait_for_receipt(tx_hash, timeout_ms).await
    }

    /// Transfer USDA (fee token)
    pub async fn transfer_usda(&mut self, recipient: Address, amount_micro: u64) -> Result<String> {
        // amount_micro is in microdollars, convert to wei (18 decimals)
        let amount_wei = U256::from(amount_micro) * U256::from(1_000_000_000_000_u64);
        self.send_erc20_transfer(self.fee_token, recipient, amount_wei).await
    }

    /// Get fee token balance
    pub async fn fee_token_balance(&self, address: Address) -> Result<U256> {
        self.rpc.get_erc20_balance(self.fee_token, address).await
    }

    /// Get native balance
    pub async fn native_balance(&self, address: Address) -> Result<U256> {
        self.rpc.get_balance(address).await
    }

    /// Reset the nonce cache
    pub fn reset_nonce(&self) {
        self.nonce_manager.reset_nonce();
    }
}

/// TxClientBuilder - Builder for TxClient
pub struct TxClientBuilder {
    private_key: String,
    rpc_url: String,
    fee_token: String,
    base_fee: u64,
    gas_limit: u64,
}

impl TxClientBuilder {
    pub fn new(private_key: &str, rpc_url: &str) -> Self {
        Self {
            private_key: private_key.to_string(),
            rpc_url: rpc_url.to_string(),
            fee_token: USDA_ADDRESS.to_string(),
            base_fee: BASE_FEE_ATTO,
            gas_limit: DEFAULT_GAS_LIMIT,
        }
    }

    pub fn fee_token(mut self, fee_token: &str) -> Self {
        self.fee_token = fee_token.to_string();
        self
    }

    pub fn base_fee(mut self, base_fee: u64) -> Self {
        self.base_fee = base_fee;
        self
    }

    pub fn gas_limit(mut self, gas_limit: u64) -> Self {
        self.gas_limit = gas_limit;
        self
    }

    pub fn build(&self) -> Result<TxClient> {
        let mut client = TxClient::new(&self.private_key, &self.rpc_url, &self.fee_token)?;
        client.base_fee = self.base_fee;
        client.gas_limit = self.gas_limit;
        Ok(client)
    }
}

/// Create a new TxClient (convenience function)
pub fn create_tx_client(private_key: &str, rpc_url: &str, fee_token: &str) -> Result<TxClient> {
    TxClient::new(private_key, rpc_url, fee_token)
}