agentswap 0.12.0

AgentSwap CLI for requesting quotes, inspecting tokens and pools, managing intents, and registering an API key against the AgentSwap service.
// AgentSwap CLI entrypoint for the standalone published crate.
// Exports: binary command parsing, dispatch, and the exit status of a failed run.
// Deps: clap, tokio, crate::commands, crate::{client, credentials, redact, service::submit}

mod commands;
mod client;
mod cli;
mod credentials;
mod display;
mod evm;
mod mcp;
mod order_types;
mod redact;
mod routes;
mod service;
mod signer;
mod tokens;
mod x402;

use clap::Parser;
use cli::{Cli, Commands, IntentCommands};
use eyre::Result;
mod startup;
use startup::{exit_code, signer_from_file};

#[tokio::main]
async fn main() {
    let _ = client::Client::status;

    let cli = Cli::parse();
    let result = run_cli(cli).await;

    if let Err(e) = result {
        let msg = redact::urls(&format!("{e}"));
        eprintln!("Error: {msg}");
        if msg.contains("HTTP 401") {
            eprintln!();
            eprintln!("This endpoint needs a valid API key. To get one:");
            eprintln!("  agentswap register --address <OWNER_WALLET> --key-file <KEY_FILE>");
            eprintln!("  (or set SR_API_KEY if you already have a key)");
        }
        std::process::exit(exit_code(&e));
    }
}

async fn run_cli(cli: Cli) -> Result<()> {
    match &cli.command {
        Commands::BatchSell { command } => return commands::batch_sell::dispatch(&cli, command).await,
        Commands::Portfolio(input) => return commands::holdings::portfolio(&cli, input.clone()).await,
        Commands::GrantLink(input) => return commands::holdings::grant_link(&cli, input.clone()).await,
        _ => {}
    }
    let _ = (routes::X402_VERIFY, routes::X402_SETTLE, routes::X402_DISCOVERY);
    if let Some(max_amount) = &cli.trade_max_amount {
        order_types::parse_raw_amount("trade max-amount", max_amount)?;
    }
    order_types::parse_raw_amount("x402 payment cap", &cli.x402_max_amount)?;
    let api_key = cli
        .api_key
        .clone()
        .filter(|k| !k.is_empty())
        .or_else(credentials::load_api_key);
    let signer = signer_from_file(cli.key_file.as_deref())?;
    let x402_signer = signer_from_file(cli.x402_key_file.as_deref().or(cli.key_file.as_deref()))?;
    let client = client::Client::new(&cli.url, api_key.clone()).with_x402(
        x402::Config {
            enabled: cli.x402,
            prefer_x402: cli.prefer_x402,
            chain_id: cli.x402_chain_id,
            max_amount: cli.x402_max_amount.clone(),
            asset: cli.x402_asset.clone(),
        },
        x402_signer.clone(),
    );
    let intent_relay_client = client::Client::new(crate::routes::app_origin(), api_key).with_x402(
        x402::Config {
            enabled: cli.x402,
            prefer_x402: cli.prefer_x402,
            chain_id: cli.x402_chain_id,
            max_amount: cli.x402_max_amount.clone(),
            asset: cli.x402_asset.clone(),
        },
        x402_signer,
    );

    match cli.command {
        Commands::BatchSell { .. } | Commands::Portfolio(_) | Commands::GrantLink(_) => unreachable!("read-only commands dispatched above"),
        Commands::BatchQuote {
            chain_id,
            pairs,
            taker,
            amount,
        } => {
            commands::batch_quote::run(
                &client,
                commands::batch_quote::Args {
                    chain_id,
                    pairs,
                    taker,
                    amount,
                    json: cli.json,
                },
            )
            .await
        }
        Commands::Chains => commands::chains::run(&client, cli.json).await,
        Commands::BuyQuota {
            chain_id,
            token,
            amount,
        } => {
            commands::buy_quota::run(
                &client,
                commands::buy_quota::Args {
                    chain_id,
                    token,
                    amount,
                    json: cli.json,
                },
            )
            .await
        }
        Commands::Quote {
            chain_id,
            from,
            to,
            amount,
            slippage,
            verify,
            taker,
        } => {
            commands::quote::run(
                &client,
                commands::quote::Args {
                    chain_id,
                    from,
                    to,
                    amount,
                    slippage,
                    verify,
                    taker,
                    json: cli.json,
                },
            )
            .await
        }
        Commands::Health => commands::health::run(&client, cli.json).await,
        Commands::KeyInfo => commands::key_info::run(&client, cli.json).await,
        Commands::Tokens { chain_id } => {
            commands::tokens::run(&client, cli.json, chain_id.as_deref()).await
        }
        Commands::Pools { chain_id, address } => {
            commands::pools::run(
                &client,
                commands::pools::Args {
                    chain_id,
                    address,
                    json: cli.json,
                },
            )
            .await
        }
        Commands::Register {
            address,
            private_key,
            key_file,
        } => {
            commands::register::run(
                &client,
                commands::register::Args {
                    address,
                    private_key,
                    key_file,
                    json: cli.json,
                },
            )
            .await
        }
        Commands::Pricing => commands::pricing::run(&client, cli.json).await,
        Commands::QuotaClaim { chain_id, tx_hash } => {
            commands::quota_claim::run(
                &client,
                commands::quota_claim::Args {
                    chain_id,
                    tx_hash,
                    json: cli.json,
                },
            )
            .await
        }
        Commands::RouteExplain { hash } => {
            commands::route_explain::run(
                &client,
                commands::route_explain::Args {
                    hash,
                    json: cli.json,
                },
            )
            .await
        }
        Commands::Mcp => {
            mcp::serve_stdio(mcp::Config {
                client,
                intent_client: intent_relay_client,
                signer,
                allow_trade: cli.allow_trade,
                trade_max_amount: cli.trade_max_amount.clone(),
            })
            .await
        }
        Commands::Intent { command } => match command {
            IntentCommands::Place {
                chain_id, proxy_owner, from, to, amount, start_out, end_out,
                decay_secs, duration_secs, deadline_secs, relay, self_submit, broadcast, dry_run,
            } => {
                let signer = signer.ok_or_else(|| eyre::eyre!("intent place requires --key-file or AGENTSWAP_KEY_FILE"))?;
                commands::intent::run_place(
                    &intent_relay_client,
                    service::intent::PlaceInput {
                        chain_id, proxy_owner, from, to, amount, start_out, end_out,
                        decay_secs, duration_secs, deadline_secs, relay, self_submit, broadcast, dry_run,
                        max_amount: cli.trade_max_amount.clone(),
                    }, signer, cli.allow_trade, cli.json,
                ).await
            }
            IntentCommands::List { chain_id, owner, agent, lookback_blocks } => {
                commands::intent::run_list(service::intent::ListInput { chain_id, owner, agent, lookback_blocks }, cli.json).await
            }
            IntentCommands::Status { chain_id, id, lookback_blocks } => {
                commands::intent::run_status(service::intent::StatusInput { chain_id, id, lookback_blocks }, cli.json).await
            }
        },
        Commands::Policy { chain_id, owner, agent, lookback_blocks, tokens } => {
            commands::intent::run_policy(service::intent::PolicyInput { chain_id, owner, agent, lookback_blocks, tokens }, cli.json).await
        }
        Commands::Trade {
            chain_id,
            from,
            to,
            amount,
            slippage,
            min_out,
            mode,
            proxy,
            nonce,
            deadline_secs,
            dry_run,
            self_submit,
            key_file,
        } => {
            evm::agent_chain_config(&chain_id)?;
            let signer = signer_from_file(key_file.as_deref())?
                .or(signer)
                .ok_or_else(|| eyre::eyre!("trade requires --key-file or AGENTSWAP_KEY_FILE"))?;
            commands::trade::run(
                &client,
                signer,
                commands::trade::Args {
                    chain_id,
                    from,
                    to,
                    amount,
                    slippage,
                    min_out,
                    mode,
                    proxy,
                    nonce,
                    deadline_secs: Some(deadline_secs),
                    dry_run: dry_run || !cli.allow_trade,
                    self_submit,
                    json: cli.json,
                    max_amount: cli.trade_max_amount.clone(),
                },
                cli.allow_trade,
            )
            .await
        }
    }
}

#[cfg(test)]
#[path = "main_tests.rs"]
mod tests;
#[cfg(test)]
mod holdings_tests;

#[cfg(test)]
mod fix_tests;
#[cfg(test)]
mod batch_sell_tests;

#[cfg(test)]
mod batch_sell_fix_tests;

#[cfg(test)]
mod batch_sell_fork_tests;