agentswap 0.13.0

AgentSwap CLI for requesting quotes, inspecting tokens and pools, managing intents, and registering an API key against the AgentSwap service.
// Submission-mode and broadcast wire tests for both protocol generations.
// Deps: parent submission module, intent test fixtures and local HTTP fixture.

use super::*;
use crate::service::test_rpc::TestRpc;

#[test]
fn public_broadcast_locator_names_the_owner_history_lookup() {
    let config = evm::chain_config("1").unwrap();
    let order = super::super::tests::build_order_for_record();
    let body = broadcast_body(config, &order, &Bytes::from(vec![1, 2, 3]));
    let identity = publication_identity(BROADCAST_URL, &body).unwrap();
    let displayed = crate::redact::urls(&identity);
    assert!(displayed.contains("https://broadcast.intentscan.net"));
    assert!(!displayed.contains("[redacted]"));
    for text in ["GET /v1/intents", "owner", "chain_ids", "order_hash", "before_ms", "next_before_ms"] {
        assert!(displayed.contains(text), "{displayed}");
    }
    assert!(displayed.contains(&format!("{:?}", order.owner)));
    let private = publication_identity("https://rpc.example/SECRET?token=HIDDEN", &body).unwrap();
    assert!(!private.contains("SECRET") && !private.contains("HIDDEN"));
}

#[test]
fn audit_fix_broadcast_identity_is_printed_before_publication() {
    let config = evm::chain_config("1").unwrap();
    let order = super::super::tests::build_order_for_record();
    let body = broadcast_body(config, &order, &Bytes::from(vec![1, 2, 3]));
    if let Ok(url) = std::env::var("BROADCAST_IDENTITY_CHILD") {
        let result = tokio::runtime::Runtime::new().unwrap().block_on(publish(&url, &body)).unwrap();
        assert_eq!(result["identity_printed_before_request"], true);
        return;
    }
    let path = std::env::temp_dir().join(format!("broadcast-identity-{}.log", std::process::id()));
    let capture = std::fs::File::create(&path).unwrap();
    let observed = path.clone();
    let rpc = TestRpc::start(move |_| {
        let text = std::fs::read_to_string(&observed).unwrap();
        let printed = text.contains(&format!("{:?}", order_types::order_id(&order)))
            && text.contains("chain 1") && text.contains("agentswap-v6")
            && text.contains(&format!("{:?}", config.settler));
        Some(serde_json::json!({"identity_printed_before_request": printed}))
    });
    let output = std::process::Command::new(std::env::current_exe().unwrap())
        .args(["audit_fix_broadcast_identity_is_printed_before_publication", "--nocapture"])
        .env("BROADCAST_IDENTITY_CHILD", &rpc.url).stderr(capture).output().unwrap();
    let stderr = std::fs::read_to_string(&path).unwrap();
    std::fs::remove_file(path).unwrap();
    assert!(output.status.success(), "{}: {stderr}", String::from_utf8_lossy(&output.stdout));
    assert!(stderr.contains(&format!("{:?}", order_types::order_id(&super::super::tests::build_order_for_record()))), "{stderr}");
}

#[tokio::test]
async fn audit_fix_broadcast_error_preserves_safe_bounded_response_body() {
    use std::io::{Read, Write};
    use std::net::TcpListener;
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let url = format!("http://{}", listener.local_addr().unwrap());
    let worker = std::thread::spawn(move || {
        let (mut stream, _) = listener.accept().unwrap();
        let mut request = [0; 4096];
        stream.read(&mut request).unwrap();
        let body = serde_json::json!({"error": "unsupported chain; https://rpc.example/key/SECRET?token=HIDDEN",
            "api_key": "NEVER_PRINT", "auth": "SIGNED_AUTH", "detail": "x".repeat(2048)}).to_string();
        write!(stream, "HTTP/1.1 422 Unprocessable Entity\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).unwrap();
    });
    let error = publish(&url, &serde_json::json!({})).await.unwrap_err().to_string();
    worker.join().unwrap();
    assert!(error.contains("422") && error.contains("unsupported chain"), "{error}");
    assert!(error.contains("[truncated]") && error.len() < 1400, "diagnostic must be bounded");
    for secret in ["SECRET", "HIDDEN", "NEVER_PRINT", "SIGNED_AUTH"] { assert!(!error.contains(secret)); }
}

#[test]
fn submission_modes_are_exclusive_and_ethereum_never_uses_sponsored_relay() {
    for id in ["1", "8453"] {
        let config = evm::chain_config(id).unwrap();
        for bits in 0u8..8 {
            let mut input = super::super::tests::input("USDC", "1");
            input.relay = bits & 1 != 0;
            input.self_submit = bits & 2 != 0;
            input.broadcast = bits & 4 != 0;
            for dry_run in [false, true] {
                input.dry_run = dry_run;
                let valid = bits.count_ones() <= 1 && (dry_run || bits != 0)
                    && !(id == "1" && input.relay);
                assert_eq!(validate(config, &input, true).is_ok(), valid, "{id}/{bits}/{dry_run}");
            }
        }
    }
}

#[tokio::test]
async fn a_transport_failure_retains_the_intent_locator_without_the_envelope() {
    let config = evm::chain_config("1").unwrap();
    let order = super::super::tests::build_order_for_record();
    let body = broadcast_body(config, &order, &Bytes::from(vec![7; 65]));
    let error = publish("http://127.0.0.1:9/SECRET?token=HIDDEN", &body).await.unwrap_err().to_string();
    assert!(error.contains(&format!("{:?}", order_types::order_id(&order))));
    assert!(error.contains("chain 1") && error.contains("agentswap-v6"));
    assert!(error.contains(&format!("{:?}", config.settler)));
    assert!(error.contains("admission is unknown") && error.contains("re-running after an error signs a new intent"));
    for secret in ["SECRET", "HIDDEN", body["auth"].as_str().unwrap()] { assert!(!error.contains(secret)); }
}

#[tokio::test]
async fn broadcast_posts_v6_wire_order_with_the_chain_settler() {
    for id in ["1", "8453"] {
        let config = evm::chain_config(id).unwrap();
        let order = super::super::tests::build_order_for_record();
        let envelope = Bytes::from(vec![1, 2, 3]);
        let body = broadcast_body(config, &order, &envelope);
        assert_eq!(body["chainId"], config.id);
        assert_eq!(body["standard"], "agentswap-v6");
        assert_eq!(body["settler"], format!("{:?}", config.settler));
        assert_eq!(body["auth"], "0x010203");
        let encoded = hex::decode(body["order"].as_str().unwrap().trim_start_matches("0x")).unwrap();
        let decoded = Order::abi_decode(&encoded).unwrap();
        assert_eq!(order_types::order_id(&decoded), order_types::order_id(&order));
        let expected = body.clone();
        let rpc = TestRpc::start(move |received| {
            assert_eq!(received, &expected);
            Some(serde_json::json!({"accepted": true}))
        });
        assert_eq!(publish(&rpc.url, &body).await.unwrap()["accepted"], true);
        assert_eq!(rpc.called(""), 1);
    }
}

#[tokio::test]
async fn broadcast_rejects_redirects_and_accepts_empty_admission_responses() {
    use std::io::{Read, Write};
    use std::net::TcpListener;
    for status in ["201 Created", "302 Found", "422 Unprocessable Entity"] {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let url = format!("http://{}", listener.local_addr().unwrap());
        let worker = std::thread::spawn(move || {
            let (mut stream, _) = listener.accept().unwrap();
            stream.set_read_timeout(Some(std::time::Duration::from_secs(5))).unwrap();
            let mut data = vec![0; 4096];
            let count = stream.read(&mut data).unwrap();
            let request = String::from_utf8_lossy(&data[..count]).to_ascii_lowercase();
            assert!(!request.contains("x-api-key:") && !request.contains("x-payment:"));
            write!(stream, "HTTP/1.1 {status}\r\nContent-Length: 0\r\nLocation: http://127.0.0.1:1/\r\nConnection: close\r\n\r\n").unwrap();
        });
        let result = publish(&url, &serde_json::json!({})).await;
        if status.starts_with("201") {
            assert_eq!(result.unwrap()["accepted"], true);
        } else {
            assert!(result.unwrap_err().to_string().contains(&format!("broadcast returned HTTP {}", &status[..3])));
        }
        worker.join().unwrap();
    }
}