use super::*;
use crate::{order_types::{IntentSettlerV3, UserProxyFactoryV6}, service::test_rpc::{TestRpc, ok}};
use alloy::{primitives::U256, sol_types::{SolCall, SolValue, SolEvent}};
use serde_json::{Value, json};
#[path = "../../client/test_server.rs"]
mod relay_server;
fn input(via: Option<&str>, dry: bool) -> Input {
let mut value = json!({"chain_id":"8453", "proxy":Address::repeat_byte(6), "receive":"WETH",
"tokens":[Address::repeat_byte(1), Address::repeat_byte(2)], "max_usd":"5", "max_loss_bps":100,
"dry_run":dry, "wait":0});
if let Some(via) = via { value["via"] = json!(via); }
serde_json::from_value(value).unwrap()
}
pub(super) fn rpc(mask: u8, status: &'static str) -> TestRpc {
let mut orders = Vec::<order_types::Order>::new();
TestRpc::start(move |body| {
let result = match body["method"].as_str().unwrap() {
"eth_getBlockByNumber" => {
let mut block = serde_json::to_value(alloy::rpc::types::Block::<alloy::rpc::types::Transaction>::default()).unwrap();
block["timestamp"] = json!("0x64"); block
}
"eth_gasPrice" => match status {
"gas_missing" => return Some(crate::service::test_rpc::failure(body, "unavailable")),
"gas_high" => json!("0x3b9aca00"),
"gas_border" => json!("0xe4e1c0"),
_ => json!("0x1"),
},
"eth_blockNumber" => json!("0x30d40"),
"eth_getLogs" => {
if status == "bounded" && body["params"][0]["fromBlock"] != "0x30d40" {
return Some(crate::service::test_rpc::failure(body, "scan predates placement"));
}
let id = body["params"][0]["topics"][1].as_str().unwrap();
let order = orders.iter().find(|o| format!("{:?}", order_types::order_id(o)) == id).unwrap();
if body["params"][0]["topics"][0] == json!(IntentSettlerV3::IntentFilled::SIGNATURE_HASH) {
let event = IntentSettlerV3::IntentFilled { id:order_types::order_id(order),owner:order.owner,solver:Address::repeat_byte(5),
recipient:order.owner,caller:Address::repeat_byte(5),amountIn:order.amountIn,requiredOut:order.endAmountOut + U256::from(10),
fee:U256::from(10),receivedOut:order.endAmountOut + U256::from(10),aboveFloor:U256::ZERO }.encode_log_data();
return Some(ok(body,json!([{"address":evm::chain_config("8453").unwrap().settler,"topics":event.topics(),"data":event.data,
"blockNumber":"0x30d40","logIndex":"0x0","transactionIndex":"0x0"}])));
}
let event = IntentSettlerV3::IntentAnnounced { id: order_types::order_id(order), owner: order.owner,
appData: order.appData, order: order.abi_encode().into(), ownerSig: (U256::ZERO, alloy::primitives::Bytes::new()).abi_encode().into() };
let log = event.encode_log_data();
if status == "missing" { json!([]) } else { json!([{"address":evm::chain_config("8453").unwrap().settler,
"topics":log.topics(), "data":log.data, "blockNumber":"0x1", "logIndex":"0x0", "transactionIndex":"0x0"}]) }
}
"eth_call" => {
let tx = &body["params"][0];
let data = hex::decode(tx["input"].as_str().or(tx["data"].as_str()).unwrap().trim_start_matches("0x")).unwrap();
if data.starts_with(&IntentSettlerV3::orderHashCall::SELECTOR) {
orders.push(IntentSettlerV3::orderHashCall::abi_decode(&data).unwrap().o);
}
if data.starts_with(&order_types::IntentLensV3::PREVIEW_LAYOUTCall::SELECTOR) { return Some(ok(body, json!(format!("0x{}", hex::encode(U256::from(3).abi_encode()))))); }
if data.starts_with(&order_types::IntentLensV3::previewCall::SELECTOR) {
let mut words = [U256::ZERO; 19];
words[1] = U256::from(u8::from(status == "cancelled")); words[2] = U256::from(u8::from(status == "filled"));
words[5] = U256::from(1); words[6] = U256::from(u8::from(status != "expired"));
return Some(ok(body, json!(format!("0x{}", hex::encode(words.abi_encode())))));
}
json!(format!("0x{}", hex::encode(call(&data, mask, tx["to"].as_str().unwrap().parse().unwrap()))))
}
method => panic!("unexpected RPC {method}"),
};
Some(ok(body, result))
})
}
fn call(data: &[u8], mask: u8, proxy: Address) -> Vec<u8> {
let s = &data[..4];
if s == UserProxyV6::ownerCall::SELECTOR { return Address::repeat_byte(4).abi_encode(); }
if s == UserProxyFactoryV6::proxyOfCall::SELECTOR { return Address::repeat_byte(6).abi_encode(); }
if s == UserProxyV6::policyOfCall::SELECTOR { return (U256::from(u64::MAX), U256::from(60), U256::from(mask), U256::from(1)).abi_encode(); }
if s == UserProxyV6::agentTokenInfoCall::SELECTOR { return (true, U256::from(900_000), U256::from(200_000), U256::from(90)).abi_encode(); }
if s == discovery::BalanceReader::balanceOfCall::SELECTOR { return U256::from(1_000_000).abi_encode(); }
if s == sale::AllowanceReader::allowanceCall::SELECTOR { return U256::from(600_000).abi_encode(); }
if s == order_types::Erc20Metadata::decimalsCall::SELECTOR { return U256::from(6).abi_encode(); }
if s == order_types::Erc20Metadata::symbolCall::SELECTOR { return "TEST".to_string().abi_encode(); }
if s == UserProxyV6::isAgentNonceUsedCall::SELECTOR { return false.abi_encode(); }
if s == UserProxyV6::isIntentAuthorizedCall::SELECTOR { return true.abi_encode(); }
if s == IntentSettlerV3::orderHashCall::SELECTOR {
return order_types::order_id(&IntentSettlerV3::orderHashCall::abi_decode(data).unwrap().o).abi_encode();
}
if s == UserProxyV6::hashIntentAuthorizationCall::SELECTOR {
let auth = UserProxyV6::hashIntentAuthorizationCall::abi_decode(data).unwrap().auth;
return order_types::signing_hash(&auth, &order_types::proxy_domain(8453, proxy)).abi_encode();
}
panic!("unexpected selector {}", hex::encode(s));
}
#[test]
fn gasless_sweep_relay_amounts_previews_skips_and_failures() {
const NAME: &str = "service::sweep::intent_tests::gasless_sweep_relay_amounts_previews_skips_and_failures";
if let Ok(url) = std::env::var("SWEEP_INTENT_RPC") {
tokio::runtime::Runtime::new().unwrap().block_on(flows(&url)); return;
}
let rpc = rpc(5, "open");
let output = std::process::Command::new(std::env::current_exe().unwrap()).args(["--exact", NAME, "--nocapture"])
.env("SWEEP_INTENT_RPC", &rpc.url).env("AGENTSWAP_RPC_URL_8453", &rpc.url).output().unwrap();
assert!(output.status.success(), "{}\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
assert_eq!(rpc.called("eth_sendRawTransaction"), 0);
}
async fn flows(url: &str) {
let receive = token::from_registry("WETH", 8453).unwrap();
let price_body = json!({"prices":{
Address::repeat_byte(1).to_string():{"priceUsd":2,"source":"defillama"},
Address::repeat_byte(2).to_string():{"priceUsd":1,"source":"defillama"},
receive.address.clone():{"priceUsd":4,"source":"defillama"}
}}).to_string();
for (dry, relay_status, max, cap, skip) in [(false, 200, "5", None, None), (true, 200, "5", None, None),
(false, 500, "5", None, None), (false, 200, "0.1", None, Some("over_max_usd")),
(false, 200, "5", Some(U256::from(1)), None), (false, 200, "5", None, Some("below_floor")),
(false, 200, "5", None, Some("unpriced")), (false, 200, "5", None, Some("price_not_independent"))] {
let mut body: Value = serde_json::from_str(&price_body).unwrap();
let receive_price = &mut body["prices"][&receive.address];
match skip {
Some("below_floor") => receive_price["priceUsd"] = serde_json::from_str("100000000000000000000").unwrap(),
Some("unpriced") => *receive_price = Value::Null,
Some("price_not_independent") => receive_price["source"] = json!("oracle"),
_ => {}
}
let app = relay_server::Server::start(vec![(200, String::new(), body.to_string())]);
let relay = relay_server::Server::start(vec![(relay_status, String::new(), json!({"accepted":true}).to_string())]);
let signer = Arc::new(crate::signer::local::LocalKey::from_private_key(&"01".repeat(32)).unwrap());
let mut request = input(None, dry); request.max_usd = max.into();
let output = read(&Client::new(&relay.url, None), signer, request, evm::read_provider(url).unwrap(), &app.url, cap, Wait::MCP).await.unwrap();
assert_rows(&output, dry, relay_status == 500 || cap.is_some(), skip);
let requests = relay.requests.lock().unwrap();
assert_eq!(requests.len(), if dry || skip.is_some() || cap.is_some() { 0 } else { 2 });
assert_requests(&requests, &output);
}
}
fn assert_rows(output: &Output, dry: bool, failed: bool, skip: Option<&str>) {
let value = serde_json::to_value(&output).unwrap();
let rows: Vec<_> = value["tokens"].as_array().unwrap().iter().filter(|r| r["reason"] != "receive_token").collect();
assert_eq!(rows.len(), 2);
for (i, row) in rows.iter().enumerate() {
assert!(row.get("intent_id").is_some(), "intent result fields missing: {row}");
if let Some(skip) = skip {
assert_eq!(row["reason"], skip);
if skip == "below_floor" { assert_eq!(row["floor_raw"], "0"); }
continue;
}
assert_eq!(row["amount_raw"], "600000");
assert_eq!(row["start_out_raw"], if i == 0 { "303000000000000000" } else { "151500000000000000" });
assert_eq!(row["floor_raw"], if i == 0 { "297000000000000000" } else { "148500000000000000" });
if failed { assert_eq!(row["outcome"], "failed"); }
else {
assert_eq!(row["announce_status"], if dry { "dry_run" } else { "accepted" });
assert_eq!(row["intent_id"].as_str().unwrap().len(), 66);
}
}
assert_eq!(output.check().is_err(), failed);
}
fn assert_requests(requests: &[relay_server::Request], output: &Output) {
let value = serde_json::to_value(output).unwrap();
let rows: Vec<_> = value["tokens"].as_array().unwrap().iter().filter(|r| r["reason"] != "receive_token").collect();
for (i, request) in requests.iter().enumerate() {
assert_eq!(request.target, crate::routes::INTENT_ANNOUNCE, "never quote in intent mode");
assert!(!request.paid);
let body: Value = serde_json::from_str(&request.body).unwrap();
assert_eq!(body["chainId"], 8453); assert_eq!(body["generation"], "v6");
let order = &body["announce"]["order"];
assert_eq!(order["amountIn"], "600000");
assert_eq!(order["startAmountOut"], rows[i]["start_out_raw"]);
assert_eq!(order["endAmountOut"], rows[i]["floor_raw"]);
assert_eq!(order["owner"], format!("{:?}", Address::repeat_byte(4)));
assert_eq!(order["recipient"], order["owner"]);
assert_eq!(order["endTime"].as_str().unwrap().parse::<u64>().unwrap() - order["startTime"].as_str().unwrap().parse::<u64>().unwrap(), 600);
assert_eq!(order["endTime"], order["decayEndTime"]);
assert_ne!(body["announce"]["auth"], "0x");
}
}
#[tokio::test]
async fn gasless_sweep_action_bits_refused_before_prices() {
for (via, mask, reason) in [(None, 1, Some("intent action")), (Some("intent"), 1, Some("intent action")),
(Some("market"), 4, Some("market action")), (None, 4, None), (Some("market"), 1, None)] {
let rpc = rpc(mask, "open");
let app = relay_server::Server::start(vec![(200, String::new(), json!({"prices":{}}).to_string())]);
let signer = Arc::new(crate::signer::local::LocalKey::from_private_key(&"01".repeat(32)).unwrap());
let result = read(&Client::new(&app.url, None), signer, input(via, true), evm::read_provider(&rpc.url).unwrap(), &app.url, None, Wait::MCP).await;
if let Some(reason) = reason { assert!(result.unwrap_err().to_string().contains(reason)); assert_eq!(app.requests.lock().unwrap().len(), 0); }
else { assert!(result.unwrap().check().is_ok()); }
}
}
#[test]
fn gasless_sweep_wait_reports_terminal_and_timeout_states() {
const NAME: &str = "service::sweep::intent_tests::gasless_sweep_wait_reports_terminal_and_timeout_states";
if let Ok(url) = std::env::var("SWEEP_INTENT_RPC") {
tokio::runtime::Runtime::new().unwrap().block_on(async {
let relay = relay_server::Server::start(vec![(200, String::new(), json!({"accepted":true}).to_string())]);
let client = Client::new(&relay.url, None);
let value = json!({"chain_id":"8453", "proxy":Address::repeat_byte(6), "receive":"WETH",
"tokens":[Address::repeat_byte(1)], "max_usd":"5", "max_loss_bps":100, "dry_run":false,"wait":1});
let input = serde_json::from_value(value).unwrap();
let receive = token::from_registry("WETH", 8453).unwrap();
let mut policy = tests::policy(); policy.tokens.push(intent::TokenPolicy {
token: Address::repeat_byte(1).to_string(), allowed:true, cap:"900000".into(), used:"200000".into(), epoch_start:"90".into() });
policy.tokens[0].token = receive.address.clone(); policy.action_mask = "5".into();
let prices = BTreeMap::from([(Address::repeat_byte(1), tests::price("1", true)), (receive.address.parse().unwrap(), tests::price("1", true))]);
let signer = Arc::new(crate::signer::local::LocalKey::from_private_key(&"01".repeat(32)).unwrap());
let output = run(Context { client:&client, signer, input, provider:evm::read_provider(&url).unwrap(), owner:Address::repeat_byte(4),
receive, prices, policy, max:amount::fixed("5", false).unwrap(), server_cap:None, wait:Wait::MCP }).await.unwrap();
output.check().unwrap();
let rows = serde_json::to_value(output).unwrap();
let row = &rows["tokens"][1];
let status = std::env::var("SWEEP_WAIT_STATUS").unwrap();
assert_eq!(row["intent_status"], if status == "missing" { "unknown" } else { &status });
assert_eq!(row["wait_timed_out"], status == "open" || status == "missing");
assert_eq!(relay.requests.lock().unwrap().len(), 1);
}); return;
}
for status in ["filled", "expired", "cancelled", "open", "missing"] {
let rpc = rpc(5, status);
let output = std::process::Command::new(std::env::current_exe().unwrap()).args(["--exact", NAME, "--nocapture"])
.env("SWEEP_INTENT_RPC", &rpc.url).env("AGENTSWAP_RPC_URL_8453", &rpc.url).env("SWEEP_WAIT_STATUS", status).output().unwrap();
assert!(output.status.success(), "{status}: {}\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
assert_eq!(rpc.called("eth_sendRawTransaction"), 0);
}
}
#[path = "gas_floor/flow_tests.rs"]
mod fix_tests;
#[path = "premium_tests.rs"]
mod premium_tests;