use super::*;
use alloy::primitives::{Address, B256, Signature};
use async_trait::async_trait;
use std::sync::atomic::{AtomicUsize, Ordering};
#[tokio::test]
async fn ethereum_v7_trade_and_policy_refuse_before_transport_or_signing() {
let signer = Arc::new(CountingSigner { calls: AtomicUsize::new(0) });
let server = AgentSwapMcp::new(Config {
client: Client::new("http://127.0.0.1:1", None),
intent_client: Client::new("http://127.0.0.1:1", None),
signer: Some(signer.clone()), allow_trade: true, trade_max_amount: None,
});
for chain in ["1", "ethereum", "mainnet"] {
for dry_run in [false, true] {
let input = serde_json::from_value(serde_json::json!({
"chain_id": chain, "from": "USDC", "to": "WETH", "amount": "1",
"min_out": "1", "proxy": format!("{:?}", Address::ZERO),
"dry_run": dry_run, "self_submit": true
})).unwrap();
let error = server.trade(Parameters(input)).await.err().expect("Ethereum agents unavailable");
assert!(error.contains("agent delegation is unavailable on Ethereum (1)"), "{error}");
}
let error = server.policy(Parameters(intent::PolicyInput {
chain_id: chain.into(), owner: format!("{:?}", Address::ZERO),
agent: format!("{:?}", Address::ZERO), lookback_blocks: None, tokens: vec![],
})).await.err().expect("Ethereum policy unavailable");
assert!(error.contains("agent delegation is unavailable on Ethereum (1)"), "{error}");
}
assert_eq!(signer.calls.load(Ordering::SeqCst), 0);
}
#[test]
fn trade_mcp_schema_is_unchanged_by_internal_verification() {
let client = Client::new("http://127.0.0.1:1", None);
let server = AgentSwapMcp::new(Config { client: client.clone(), intent_client: client,
signer: None, allow_trade: false, trade_max_amount: None });
let tools = server.tool_router.list_all();
let trade = tools.iter().find(|tool| tool.name == "trade").unwrap();
let schema = serde_json::to_value(&trade.input_schema).unwrap();
assert_eq!(alloy::primitives::keccak256(serde_json::to_vec(&schema).unwrap()).to_string(),
"0xe4a1bde6e857a3a5680755e09da592d10c0bb12b9dd2ce83c8c42ab68cfc1fff");
}
#[test]
fn arc_is_in_the_mcp_v6_chain_description() {
let server = AgentSwapMcp::new(Config {
client: Client::new("http://127.0.0.1:1", None),
intent_client: Client::new("http://127.0.0.1:1", None),
signer: None, allow_trade: false, trade_max_amount: None,
});
let tools = server.tool_router.list_all();
for name in ["portfolio", "grant_link"] {
let tool = tools.iter().find(|tool| tool.name == name).unwrap();
assert!(tool.description.as_deref().unwrap().contains(crate::tokens::HOLDINGS_CHAINS_NOTE));
}
for name in ["trade", "intent_place", "intent_list", "intent_status", "policy"] {
let tool = tools.iter().find(|tool| tool.name == name).unwrap();
let description = tool.description.as_deref().unwrap();
assert!(description.contains(crate::tokens::PROTOCOL_CHAINS_NOTE), "{name}");
assert!(description.contains("Arc (5042)"), "{name}");
}
}
#[test]
fn arc_testnet_is_in_the_mcp_v6_chain_description() {
let server = AgentSwapMcp::new(Config {
client: Client::new("http://127.0.0.1:1", None),
intent_client: Client::new("http://127.0.0.1:1", None),
signer: None, allow_trade: false, trade_max_amount: None,
});
let tools = server.tool_router.list_all();
for name in ["trade", "intent_place", "intent_list", "intent_status", "policy"] {
let tool = tools.iter().find(|tool| tool.name == name).unwrap();
let description = tool.description.as_deref().unwrap();
assert!(description.contains(crate::tokens::PROTOCOL_CHAINS_NOTE), "{name}");
assert!(description.contains("Arc Testnet (5042002)"), "{name}");
}
}
struct CountingSigner {
calls: AtomicUsize,
}
#[async_trait]
impl Signer for CountingSigner {
fn address(&self) -> Address { Address::ZERO }
async fn sign_message(&self, _: &[u8]) -> Result<Signature> {
Err(eyre::eyre!("unexpected signature"))
}
async fn sign_hash(&self, _: B256) -> Result<Signature> {
self.calls.fetch_add(1, Ordering::SeqCst);
Err(eyre::eyre!("unexpected signature"))
}
}
#[tokio::test]
async fn malformed_mcp_trade_cap_rejects_before_signing() {
for cap in ["not-an-amount", "12abc", "", " ", "1_000"] {
let signer = Arc::new(CountingSigner { calls: AtomicUsize::new(0) });
let server = AgentSwapMcp::new(Config {
client: Client::new("http://127.0.0.1:1", None),
intent_client: Client::new("http://127.0.0.1:1", None),
signer: Some(signer.clone()),
allow_trade: true,
trade_max_amount: Some(cap.to_string()),
});
let result = server.trade(Parameters(trade::TradeInput {
chain_id: "base".to_string(), from: "USDC".to_string(), to: "WETH".to_string(),
amount: "1".to_string(), slippage: None, min_out: Some("1".to_string()),
max_amount: None, mode: "agent-order".to_string(),
proxy: "0x2222222222222222222222222222222222222222".to_string(),
nonce: Some("1".to_string()), deadline_secs: Some(120), dry_run: true,
self_submit: false, verify_quote: true,
})).await;
let error = match result {
Ok(_) => panic!("malformed operator cap {cap:?} must fail"),
Err(error) => error,
};
assert!(error.contains("invalid MCP trade max-amount"), "cap {cap:?}: {error}");
assert_eq!(signer.calls.load(Ordering::SeqCst), 0, "cap {cap:?} signed");
}
}
#[test]
fn mcp_json_keeps_large_raw_amount_as_a_string() {
let amount = "900719925474099300000000000000000000";
let output = quote::QuoteOutput {
request: quote::QuoteContext {
chain_id: 8453,
token_in: "0x1".to_string(),
token_in_symbol: "USDC".to_string(),
token_in_decimals: 6,
token_out: "0x2".to_string(),
token_out_symbol: "WETH".to_string(),
token_out_decimals: 18,
amount_in: amount.to_string(),
},
response: serde_json::json!({}),
};
let json = serde_json::to_value(output).expect("MCP JSON");
assert_eq!(json["request"]["amount_in"], amount);
}
#[test]
fn a_sent_transaction_that_did_not_confirm_is_a_tool_error_carrying_the_outcome() {
use crate::service::submit::TxStatus;
let outcome = serde_json::json!({"txHash": "0xab", "txStatus": "reverted"});
let failure = NotConfirmed::check(Some("0xab"), Some(TxStatus::Reverted), None);
let error = confirmed(outcome.clone(), failure).err().expect("a revert is not success");
assert!(error.starts_with("transaction 0xab was mined and reverted\n"), "{error}");
assert!(error.contains(r#""txStatus":"reverted""#), "{error}");
let Json(value) = confirmed(outcome.clone(), None).expect("confirmed or not sent");
assert_eq!(value, outcome);
}
#[test]
fn tool_errors_keep_the_rpc_host_and_drop_its_keyed_path() {
let error = tool_error(eyre::eyre!("error sending request for url (https://rpc.example/v2/KEY)"));
assert_eq!(error, "error sending request for url (https://rpc.example/[redacted])");
}
#[test]
fn batch_sell_mcp_schemas_replace_sweep() {
let client = Client::new("http://127.0.0.1:1", None);
let server = AgentSwapMcp::new(Config { client:client.clone(), intent_client:client, signer:None, allow_trade:false, trade_max_amount:None });
let tools = server.tool_router.list_all();
assert!(!tools.iter().any(|t| t.name == "sweep"));
for name in ["batch_sell_plan", "batch_sell_run", "batch_sell_report"] {
let tool = tools.iter().find(|t| t.name == name).unwrap();
let schema = serde_json::to_value(&tool.input_schema).unwrap();
if name.ends_with("report") {
assert_eq!(schema["required"], serde_json::json!(["request"]));
assert!(tool.description.as_deref().unwrap_or_default().contains("after every batch_sell_run"));
} else if name.ends_with("plan") {
for field in ["chain_id","owner","agent","receive","max_loss_bps"] { assert!(schema["required"].as_array().unwrap().contains(&serde_json::json!(field))); }
for field in ["min_usd","max_usd","tokens","exclude"] { assert!(!schema["required"].as_array().unwrap().contains(&serde_json::json!(field))); }
assert_eq!(schema["properties"]["max_loss_bps"]["maximum"], 5000);
} else {
assert_eq!(schema["properties"]["via"]["default"], "intent");
assert!(schema["properties"].get("max_loss_bps").is_none());
assert!(schema["properties"].get("wait").is_some());
assert!(tool.description.as_deref().unwrap_or_default().contains("batch_sell_report"));
}
}
}