aimo-cli 0.4.0

AiMo Network client CLI
use std::path::PathBuf;

use aimo_client::types::transactions::{TopUpTxRequest, TransactionResponse};
use aimo_core::token_map::token_map;
use base64::Engine;
use bincode::config;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::{message::Message, signature::Keypair, signer::Signer, transaction::Transaction};
use url::Url;

pub async fn top_up_escrow(
    amount: f64,
    id: Option<PathBuf>,
    rpc_url: String,
    router_url: String,
    devnet_tokens: bool,
    token: Option<String>,
) -> anyhow::Result<()> {
    // Use devnet tokens for CLI operations (this could be made configurable)
    let token_map = token_map(devnet_tokens);

    let token_name = token.unwrap_or("USDC".to_string());
    let token = token_map
        .find(&token_name)
        .ok_or(anyhow::anyhow!("Token {token_name} isn't supported"))?;

    // Convert amount to token's smallest unit (considering decimals)
    let token_amount = (amount * 10_f64.powi(token.decimals as i32)) as u64;

    if token_amount == 0 {
        return Err(anyhow::anyhow!("Amount must be greater than 0"));
    }

    // Calculate the 3% fee for display purposes (same as the program does)
    let fee_amount = token_amount
        .checked_mul(3)
        .ok_or_else(|| anyhow::anyhow!("Math overflow calculating fee"))?
        .checked_div(100)
        .ok_or_else(|| anyhow::anyhow!("Math overflow calculating fee"))?;

    let total_amount = token_amount
        .checked_add(fee_amount)
        .ok_or_else(|| anyhow::anyhow!("Math overflow calculating total"))?;

    // Load user keypair
    let user_keypair = aimo_core::utils::id::create_keypair_from_file(id)?;

    do_top_up(&user_keypair, token_name, total_amount, router_url, rpc_url).await?;

    Ok(())
}

async fn do_top_up(
    user: &Keypair,
    token: String,
    amount: u64,
    router_url: String,
    rpc_url: String,
) -> anyhow::Result<()> {
    let client = RpcClient::new(rpc_url);
    let block_hash = client.get_latest_blockhash().await?;

    // Build transaction
    let tx = reqwest::Client::new()
        .post(Url::parse(&format!(
            "{router_url}/api/v1/users/topup/message"
        ))?)
        .json(&TopUpTxRequest {
            user_address: user.pubkey().to_string(),
            token,
            amount,
        })
        .send()
        .await
        .inspect(|r| tracing::debug!("Received response {:?}", r))?
        .json::<TransactionResponse>()
        .await
        .map(|tx| tx.tx)
        .map_err(anyhow::Error::from)
        // Decode the base64 transaction
        .and_then(|tx| {
            base64::engine::general_purpose::STANDARD
                .decode(tx)
                .map_err(anyhow::Error::from)
        })
        // Deserialize the transaction into VersionedTransaction
        .and_then(|tx| {
            bincode::serde::decode_from_slice::<Message, _>(&tx[..], config::standard())
                .map(|(tx, _)| tx)
                .map_err(anyhow::Error::from)
        })
        // Build transaction with message and sign it
        .map(|msg| {
            let mut transaction = Transaction::new_unsigned(msg);
            transaction.sign(&[user], block_hash);
            transaction
        })?;

    let tx = client
        .send_and_confirm_transaction_with_spinner(&tx)
        .await?;

    println!("Top-up successful! Transaction signature: {}", tx);

    Ok(())
}