ostium-rust-sdk 0.1.0

Rust SDK for interacting with the Ostium trading platform on Arbitrum
Documentation
use alloy::network::Network;
use alloy::primitives::{Address, U256};
use alloy::providers::Provider;
use alloy::sol_types::{sol, SolCall};

use crate::error::{OstiumError, Result};
use crate::types::UnsignedTransaction;

// Define the USDC contract interface
sol! {
    #[sol(rpc)]
    contract USDC {
        function balanceOf(address account) external view returns (uint256);
        function allowance(address owner, address spender) external view returns (uint256);
        function approve(address spender, uint256 amount) external returns (bool);
        function transfer(address to, uint256 amount) external returns (bool);
        function transferFrom(address from, address to, uint256 amount) external returns (bool);
        function decimals() external view returns (uint8);
        function symbol() external view returns (string);
        function name() external view returns (string);
        function totalSupply() external view returns (uint256);
    }
}

/// USDC contract wrapper
pub struct UsdcContract<P: Provider<N>, N: Network = alloy::network::Ethereum> {
    contract: USDC::USDCInstance<P, N>,
}

impl<P: Provider<N>, N: Network> UsdcContract<P, N> {
    /// Create a new USDC contract instance
    pub fn new(address: Address, provider: P) -> Self {
        let contract = USDC::new(address, provider);
        Self { contract }
    }

    /// Get the balance of an account
    pub async fn balance_of(&self, account: Address) -> Result<U256> {
        let balance = self
            .contract
            .balanceOf(account)
            .call()
            .await
            .map_err(|e| OstiumError::contract(format!("Failed to get balance: {}", e)))?;
        Ok(balance)
    }

    /// Get the allowance for a spender
    pub async fn allowance(&self, owner: Address, spender: Address) -> Result<U256> {
        let allowance = self
            .contract
            .allowance(owner, spender)
            .call()
            .await
            .map_err(|e| OstiumError::contract(format!("Failed to get allowance: {}", e)))?;
        Ok(allowance)
    }

    /// Approve a spender to use tokens
    pub async fn approve(&self, spender: Address, amount: U256) -> Result<()> {
        let _receipt = self
            .contract
            .approve(spender, amount)
            .send()
            .await
            .map_err(|e| OstiumError::contract(format!("Failed to approve: {}", e)))?
            .get_receipt()
            .await
            .map_err(|e| OstiumError::contract(format!("Failed to get receipt: {}", e)))?;
        Ok(())
    }

    /// Transfer tokens to another address
    pub async fn transfer(&self, to: Address, amount: U256) -> Result<()> {
        let _receipt = self
            .contract
            .transfer(to, amount)
            .send()
            .await
            .map_err(|e| OstiumError::contract(format!("Failed to transfer: {}", e)))?
            .get_receipt()
            .await
            .map_err(|e| OstiumError::contract(format!("Failed to get receipt: {}", e)))?;
        Ok(())
    }

    /// Get token decimals
    pub async fn decimals(&self) -> Result<u8> {
        let decimals = self
            .contract
            .decimals()
            .call()
            .await
            .map_err(|e| OstiumError::contract(format!("Failed to get decimals: {}", e)))?;
        Ok(decimals)
    }

    /// Get token symbol
    pub async fn symbol(&self) -> Result<String> {
        let symbol = self
            .contract
            .symbol()
            .call()
            .await
            .map_err(|e| OstiumError::contract(format!("Failed to get symbol: {}", e)))?;
        Ok(symbol)
    }

    /// Get token name
    pub async fn name(&self) -> Result<String> {
        let name = self
            .contract
            .name()
            .call()
            .await
            .map_err(|e| OstiumError::contract(format!("Failed to get name: {}", e)))?;
        Ok(name)
    }

    /// Create an unsigned transaction for USDC approval
    pub async fn approve_unsigned(
        &self,
        spender: Address,
        amount: U256,
        tx_params: crate::UnsignedTransactionParams,
    ) -> Result<UnsignedTransaction> {
        // Create the approve call
        let approve_call = USDC::approveCall { spender, amount };

        self.build_unsigned_transaction_from_call(approve_call, tx_params)
            .await
    }

    /// Create an unsigned transaction for USDC transfer
    pub async fn transfer_unsigned(
        &self,
        to: Address,
        amount: U256,
        tx_params: crate::UnsignedTransactionParams,
    ) -> Result<UnsignedTransaction> {
        // Create the transfer call
        let transfer_call = USDC::transferCall { to, amount };

        self.build_unsigned_transaction_from_call(transfer_call, tx_params)
            .await
    }

    /// Generic method to build unsigned transactions from sol calls
    async fn build_unsigned_transaction_from_call<C>(
        &self,
        call: C,
        tx_params: crate::UnsignedTransactionParams,
    ) -> Result<UnsignedTransaction>
    where
        C: SolCall,
    {
        // Get the contract address
        let to = *self.contract.address();

        // Get the encoded call data
        let data = call.abi_encode();

        // Get chain ID from provider
        let chain_id = self
            .contract
            .provider()
            .get_chain_id()
            .await
            .map_err(|e| OstiumError::contract(format!("Failed to get chain ID: {}", e)))?;

        // Estimate gas if requested
        let gas_limit = if tx_params.include_gas_estimates {
            // Use conservative gas estimate for ERC20 operations
            Some(U256::from(65_000)) // Should cover approve and transfer
        } else {
            None
        };

        // Get gas price estimate if requested
        let gas_price = if tx_params.include_gas_estimates {
            match self.contract.provider().get_gas_price().await {
                Ok(price) => Some(U256::from(price)),
                Err(e) => {
                    tracing::warn!("Failed to get gas price: {}", e);
                    None
                }
            }
        } else {
            None
        };

        // Get nonce if requested
        let nonce = if tx_params.include_nonce {
            match self
                .contract
                .provider()
                .get_transaction_count(tx_params.from)
                .await
            {
                Ok(count) => Some(count),
                Err(e) => {
                    tracing::warn!("Failed to get nonce: {}", e);
                    None
                }
            }
        } else {
            None
        };

        Ok(UnsignedTransaction {
            to,
            data,
            value: U256::ZERO, // ERC20 operations don't send ETH
            gas_limit,
            gas_price,
            chain_id,
            nonce,
        })
    }
}