agentswap 0.11.2

AgentSwap CLI for requesting quotes, inspecting tokens and pools, managing intents, and registering an API key against the AgentSwap service.
// Batch-sell execution through the public CLI against isolated request, relay and RPC fixtures.
// Confirms the owner's discount wins and relay floor refusals do not stop other tokens.
use super::intent_tests::rpc;
use crate::{cli::Cli, signer::Signer, service::token};
use alloy::primitives::Address;
use clap::Parser;
use serde_json::{Value, json};
#[path = "../../client/test_server.rs"]
mod http;

#[test]
fn batch_sell_run_uses_confirmed_discount_and_continues_relay_422() {
    const NAME: &str = "service::sweep::batch_tests::batch_sell_run_uses_confirmed_discount_and_continues_relay_422";
    if let Ok(url) = std::env::var("BATCH_RUN_APP") {
        crate::routes::TEST_APP_ORIGIN.set(url).unwrap();
        let key = std::env::temp_dir().join(format!("batch-key-{}", std::process::id()));
        std::fs::write(&key, "01".repeat(32)).unwrap();
        let waiting = std::env::var("BATCH_WAIT").unwrap_or_else(|_| "0".into());
        let cli = Cli::try_parse_from(["agentswap", "--url", "http://127.0.0.1:1", "--allow-trade", "--json", "batch-sell", "run", "--request",
            "https://app.agentswap.co/grant/r/abcdefghijklmnopqrstuv", "--key-file", key.to_str().unwrap(), "--wait", &waiting]).unwrap();
        let result = tokio::runtime::Runtime::new().unwrap().block_on(crate::run_cli(cli));
        std::fs::remove_file(key).unwrap();
        result.unwrap(); return;
    }
    for code in ["floor_below_confirmed_discount", "unpriced_for_confirmed_discount", "filled"] {
        let receive = token::from_registry("WETH", 8453).unwrap();
        let signer = crate::signer::local::LocalKey::from_private_key(&"01".repeat(32)).unwrap();
        let request = json!({"id":"abcdefghijklmnopqrstuv","status":"confirmed",
            "confirmed":{"proxy":Address::repeat_byte(6),"generation":"1","maxLossBps":100},
            "request":{"v":1,"chainId":8453,"owner":Address::repeat_byte(4),"agent":signer.address(),"purpose":"batch-sell","maxLossBps":500,
            "tokens":[{"address":Address::repeat_byte(1),"cap":"1"},{"address":Address::repeat_byte(2),"cap":"1"},{"address":receive.address,"cap":"0"}]}});
        let prices = json!({"prices":{Address::repeat_byte(1).to_string():{"priceUsd":2,"source":"defillama"},
            Address::repeat_byte(2).to_string():{"priceUsd":1,"source":"defillama"},receive.address:{"priceUsd":4,"source":"defillama"}}});
        let app = http::Server::start(vec![(200,String::new(),request.to_string()), (200,String::new(),prices.to_string()),
            (if code == "filled" {200} else {422},String::new(),json!({"error":code}).to_string()), (200,String::new(),"{\"accepted\":true}".into())]);
        let rpc = rpc(5, if code == "filled" { "filled" } else { "open" });
        let output = std::process::Command::new(std::env::current_exe().unwrap()).args(["--exact",NAME,"--nocapture"])
            .env("BATCH_WAIT", if code == "filled" {"1"} else {"0"}).env("BATCH_RUN_APP", &app.url).env("AGENTSWAP_RPC_URL", &rpc.url).env("AGENTSWAP_RPC_URL_8453", &rpc.url).output().unwrap();
        let text = String::from_utf8_lossy(&output.stdout);
        assert!(output.status.success(), "{text} {}", String::from_utf8_lossy(&output.stderr));
        if code == "filled" { assert!(text.contains("sold") && text.contains("\"received_raw\": \"297000000000000000\""), "{text}"); }
        else { assert!(text.contains(code) && text.contains("placed"), "{text}"); }
        let requests = app.requests.lock().unwrap();
        let posts: Vec<_> = requests.iter().filter(|r| r.target == "/api/intents").collect();
        assert_eq!(posts.len(), 2);
        for (post, floor) in posts.iter().zip(["297000000000000000", "148500000000000000"]) {
            let body: Value = serde_json::from_str(&post.body).unwrap();
            assert_eq!(body["announce"]["order"]["endAmountOut"], floor);
        }
        assert_eq!(rpc.called("eth_sendRawTransaction"),0);
    }
}

#[test]
fn batch_sell_run_generation_and_request_cap_boundaries() {
    const NAME: &str = "service::sweep::batch_tests::batch_sell_run_generation_and_request_cap_boundaries";
    if let Ok(url) = std::env::var("BATCH_BOUNDARY_APP") {
        crate::routes::TEST_APP_ORIGIN.set(url).unwrap();
        let key = std::env::temp_dir().join(format!("batch-boundary-key-{}", std::process::id()));
        std::fs::write(&key, "01".repeat(32)).unwrap();
        let cli = Cli::try_parse_from(["agentswap", "--allow-trade", "--json", "batch-sell", "run", "--request",
            "abcdefghijklmnopqrstuv", "--key-file", key.to_str().unwrap()]).unwrap();
        let result = tokio::runtime::Runtime::new().unwrap().block_on(crate::run_cli(cli));
        std::fs::remove_file(key).unwrap();
        if std::env::var("BATCH_CHANGED").is_ok() {
            assert!(result.unwrap_err().to_string().contains("confirmed grant generation changed"));
        } else { result.unwrap(); }
        return;
    }
    for generation in ["2", "1"] {
        let receive = token::from_registry("WETH", 8453).unwrap();
        let signer = crate::signer::local::LocalKey::from_private_key(&"01".repeat(32)).unwrap();
        let record = json!({"id":"abcdefghijklmnopqrstuv","status":"confirmed",
            "confirmed":{"proxy":Address::repeat_byte(6),"generation":generation,"maxLossBps":100},
            "request":{"v":1,"chainId":8453,"owner":Address::repeat_byte(4),"agent":signer.address(),"purpose":"batch-sell","maxLossBps":500,
            "tokens":[{"address":Address::repeat_byte(1),"cap":"0.1"},{"address":receive.address,"cap":"0"}]}});
        let prices = json!({"prices":{Address::repeat_byte(1).to_string():{"priceUsd":2,"source":"defillama"},
            receive.address:{"priceUsd":4,"source":"defillama"}}});
        let app = http::Server::start(vec![(200,String::new(),record.to_string()),(200,String::new(),prices.to_string()),
            (200,String::new(),"{\"accepted\":true}".into())]);
        let rpc = rpc(5, "open");
        let mut command = std::process::Command::new(std::env::current_exe().unwrap());
        command.args(["--exact",NAME,"--nocapture"]).env("BATCH_BOUNDARY_APP", &app.url)
            .env("AGENTSWAP_RPC_URL", &rpc.url).env("AGENTSWAP_RPC_URL_8453", &rpc.url);
        if generation == "2" { command.env("BATCH_CHANGED", "1"); }
        let output = command.output().unwrap();
        let text = String::from_utf8_lossy(&output.stdout);
        assert!(output.status.success(), "{text} {}", String::from_utf8_lossy(&output.stderr));
        let requests = app.requests.lock().unwrap();
        let posts: Vec<_> = requests.iter().filter(|r| r.method == "POST" && r.target == "/api/intents").collect();
        assert_eq!(posts.len(), usize::from(generation == "1"));
        if generation == "1" {
            assert!(text.contains("\"amount_raw\": \"100000\""), "{text}");
            let body: Value = serde_json::from_str(&posts[0].body).unwrap();
            assert_eq!(body["announce"]["order"]["amountIn"], "100000");
        }
        assert_eq!(rpc.called("eth_sendRawTransaction"), 0);
    }
}