altius-tx-sdk 0.1.22

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::{fee_payer_signature_hash, Signer as SignerTrait, TxBuilder};
use crate::Result;
use alloy_primitives::{Address, Bytes, 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
    pub async fn sign(&mut self, tx: TxBuilder) -> Result<SignedTransaction> {
        let fields = tx.build();
        let _chain_id = self.chain_id().await?;

        // Compute fee payer signature
        let fee_payer_sig = fee_payer_signature_hash(
            fields.chain_id,
            fields.nonce,
            fields.gas_limit,
            fields.fee_token,
            fields.fee_payer,
            fields.max_fee_per_gas_usd,
            self.signer.address(),
        );

        let signature = self.signer.sign_hash(&fee_payer_sig)
            .map_err(|e: Box<dyn std::error::Error>| crate::Error::SigningFailed(e.to_string()))?;

        Ok(SignedTransaction {
            tx,
            signature: signature.to_bytes(),
            sender: self.signer.address(),
        })
    }

    /// Send a pre-built transaction
    pub async fn send(&mut self, _signed: SignedTransaction) -> Result<String> {
        // For now, return placeholder
        // Full RLP encoding would be needed for actual submission
        Ok("0x".to_string())
    }

    /// 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: SignedTransaction, 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)
    }
}

/// Signed transaction
pub struct SignedTransaction {
    pub tx: TxBuilder,
    pub signature: Bytes,
    pub sender: Address,
}

impl SignedTransaction {
    /// Get raw transaction bytes (RLP encoded)
    pub fn raw_transaction(&self) -> String {
        // Simplified: return signature as placeholder
        format!("0x7a{}", hex::encode(self.signature.as_ref()))
    }
}

/// 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)
}