use super::*;
use crate::service::test_rpc::{TestRpc, ok};
use serde_json::json;
#[tokio::test]
async fn archive_http_403_refusals_retry_each_chunk_with_strict_limits() {
let phrase = "Archive requests require a personal token";
for (chain, http, code, message, failures, attempts) in [
(1, 403, -32602, phrase, 3, 4),
(1, 403, -32602, phrase, usize::MAX, 4),
(8453, 403, -32602, phrase, usize::MAX, 1),
(1, 400, -32602, phrase, usize::MAX, 1),
(1, 403, -32000, phrase, usize::MAX, 1),
(1, 403, -32602, "invalid filter", usize::MAX, 1),
] {
let mut counts = std::collections::BTreeMap::new();
let rpc = TestRpc::with_status(move |body| {
if body["method"] == "eth_chainId" {
return Some((200, ok(body, json!(format!("0x{chain:x}")))));
}
let count = counts.entry(body["params"].to_string()).or_insert(0);
*count += 1;
Some(if *count <= failures {
(http, json!({"jsonrpc":"2.0", "id":body["id"], "error":{"code":code,"message":message}}))
} else { (200, ok(body, json!([]))) })
});
let config = evm::chain_config(&chain.to_string()).unwrap();
let provider = evm::read_provider_checked(&rpc.url, chain).await.unwrap();
let settler = IntentSettlerV3::new(config.settler, provider);
for block in [1, 5001] {
let mut filter = settler.IntentAnnounced_filter();
filter.filter = filter.filter.from_block(block).to_block(block + 4999);
let before = rpc.called("eth_getLogs");
let result = evm::event_query(config, block, block + 4999, || filter.query()).await;
assert_eq!(result.is_ok(), failures == 3, "{chain}/{http}/{code}: {result:?}");
assert_eq!(rpc.called("eth_getLogs") - before, attempts);
}
}
}
#[test]
fn audit_fix_wrong_chain_rpc_fails_before_contract_reads() {
use crate::service::test_rpc::{ok, TestRpc};
use serde_json::json;
if std::env::var_os("WRONG_CHAIN_CHILD").is_some() {
tokio::runtime::Runtime::new().unwrap().block_on(async {
let error = list(ListInput {
chain_id: "1".into(), owner: None, agent: None, lookback_blocks: Some(1),
}).await.expect_err("wrong-chain RPC must fail");
assert!(error.to_string().contains("RPC chain id 8453 does not match requested chain id 1"), "{error}");
});
return;
}
let rpc = TestRpc::start(|body| Some(ok(body, match body["method"].as_str().unwrap() {
"eth_chainId" => json!("0x2105"),
"eth_call" => json!(format!("0x{:064x}", 3)),
"eth_blockNumber" => json!("0x1"),
_ => json!([]),
})));
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args(["audit_fix_wrong_chain_rpc_fails_before_contract_reads", "--nocapture"])
.env("WRONG_CHAIN_CHILD", "1").env("AGENTSWAP_RPC_URL_1", &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_chainId"), 1);
assert_eq!(rpc.called("eth_call"), 0);
}
#[test]
fn audit_fix_archive_refusals_retry_each_chunk_and_preserve_search_scope() {
use crate::service::test_rpc::{ok, TestRpc};
use serde_json::json;
if let Ok(case) = std::env::var("ARCHIVE_RETRY_CHILD") {
tokio::runtime::Runtime::new().unwrap().block_on(check_archive_result(&case));
return;
}
for recover in [true, false] {
let mut attempts = std::collections::BTreeMap::new();
let rpc = TestRpc::start(move |body| {
let result = match body["method"].as_str().unwrap() {
"eth_chainId" => json!("0x1"),
"eth_call" => {
use alloy::sol_types::SolCall;
let tx = &body["params"][0];
let data = tx.get("input").or_else(|| tx.get("data")).unwrap().as_str().unwrap();
let settled = [IntentSettlerV3::filledCall::SELECTOR, IntentSettlerV3::cancelledCall::SELECTOR]
.iter().any(|s| data.starts_with(&format!("0x{}", hex::encode(s))));
json!(format!("0x{:064x}", if settled { 0 } else { 3 }))
}
"eth_blockNumber" => json!("0x4e20"),
"eth_getLogs" => {
let count = attempts.entry(body["params"].to_string()).or_insert(0);
*count += 1;
if !recover || *count <= 3 {
return Some(json!({"jsonrpc":"2.0", "id":body["id"], "error": {
"code": -32602, "message": "Archive requests require a personal token"}}));
}
json!([])
}
method => panic!("unexpected {method}"),
};
Some(ok(body, result))
});
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args(["audit_fix_archive_refusals_retry_each_chunk_and_preserve_search_scope", "--nocapture"])
.env("ARCHIVE_RETRY_CHILD", if recover { "recover" } else { "refuse" })
.env("AGENTSWAP_RPC_URL_1", &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_getLogs"), if recover { 8 } else { 4 });
assert_eq!(rpc.called("eth_chainId"), 1);
}
}
#[test]
fn ethereum_reads_use_v7_lens_and_settler_with_bounded_log_queries() {
use alloy::sol_types::{SolCall, SolValue};
use crate::service::test_rpc::{ok, TestRpc};
use serde_json::json;
if std::env::var_os("ETHEREUM_READ_TEST_CHILD").is_some() {
tokio::runtime::Runtime::new().unwrap().block_on(async {
let config = evm::chain_config("ethereum").unwrap();
let provider = evm::read_provider_checked(&evm::rpc_url(config), config.id).await.unwrap();
let down = Origins { stream: "http://127.0.0.1:1", data: "http://127.0.0.1:1" };
let records = list_from(&down, &provider, config, &ListInput {
chain_id: "ethereum".into(), owner: Some(format!("{:?}", Address::ZERO)),
agent: None, lookback_blocks: Some(9_000),
}).await.unwrap();
assert!(records.is_empty());
let error = status_from(&down, &provider, config, &StatusInput {
chain_id: "1".into(), id: format!("{:?}", B256::ZERO), lookback_blocks: Some(9_000),
}).await.unwrap_err();
assert!(error.to_string().contains("neither filled nor cancelled"), "{error}");
});
return;
}
let config = evm::chain_config("1").unwrap();
let rpc = TestRpc::start(move |body| {
let result = match body["method"].as_str().unwrap() {
"eth_chainId" => json!("0x1"),
"eth_call" => {
let tx = &body["params"][0];
let data = tx.get("input").or_else(|| tx.get("data")).unwrap().as_str().unwrap().to_string();
if tx["to"] == format!("{:?}", config.settler) {
let selectors = [IntentSettlerV3::filledCall::SELECTOR, IntentSettlerV3::cancelledCall::SELECTOR];
assert!(selectors.iter().any(|s| data.starts_with(&format!("0x{}", hex::encode(s)))), "{data}");
json!(format!("0x{}", hex::encode(false.abi_encode())))
} else {
assert_eq!(tx["to"], format!("{:?}", config.lens));
assert_eq!(data, format!("0x{}", hex::encode(IntentLensV3::PREVIEW_LAYOUTCall::SELECTOR)));
json!(format!("0x{}", hex::encode(U256::from(3).abi_encode())))
}
}
"eth_blockNumber" => json!("0x4e20"),
"eth_getLogs" => {
let filter = &body["params"][0];
assert_eq!(filter["address"], format!("{:?}", config.settler));
let start = u64::from_str_radix(filter["fromBlock"].as_str().unwrap().trim_start_matches("0x"), 16).unwrap();
let end = u64::from_str_radix(filter["toBlock"].as_str().unwrap().trim_start_matches("0x"), 16).unwrap();
assert!(start >= 11_000 && end <= 20_000 && end - start < 5_000);
json!([])
}
method => panic!("unexpected RPC: {method}"),
};
Some(ok(body, result))
});
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args(["ethereum_reads_use_v7_lens_and_settler_with_bounded_log_queries", "--nocapture"])
.env("ETHEREUM_READ_TEST_CHILD", "1").env("AGENTSWAP_RPC_URL_1", &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_getLogs"), 4);
}
#[test]
fn status_word_explains_v3_exclusive_window() {
let view = IntentLensV3::IntentView {
id: B256::ZERO,
cancelled: false,
filled: false,
nonceSpent: false,
killedByOwner: false,
proxyDeployed: true,
inWindow: true,
exclusiveWindow: true,
decayComplete: false,
floorNow: U256::from(3_000_000_000u64),
feeNow: U256::from(900_000u64),
requiredNow: U256::from(3_000_900_000u64),
floorForOutsider: U256::from(3_007_500_000u64),
requiredForOutsider: U256::from(3_008_402_250u64),
ownerBalance: U256::ZERO,
ownerProxyAllowance: U256::ZERO,
proxy: Address::ZERO,
observedAt: U256::ZERO,
observedBlock: U256::ZERO,
};
assert_eq!(
status_word(&view, false),
("open".to_string(), "exclusive window: only the system solver fills at the floor; an outsider pays the floor plus the exclusivity override".to_string())
);
}
async fn check_archive_result(case: &str) {
let config = evm::chain_config("1").unwrap();
let provider = evm::read_provider_checked(&evm::rpc_url(config), config.id).await.unwrap();
let down = Origins { stream: "http://127.0.0.1:1", data: "http://127.0.0.1:1" };
let result = status_from(&down, &provider, config, &StatusInput {
chain_id: "1".into(), id: format!("{:?}", B256::ZERO), lookback_blocks: Some(9_000),
}).await;
let error = result.expect_err("empty or unavailable logs").to_string();
if case == "recover" {
assert!(error.contains("neither filled nor cancelled"), "{error}");
} else {
assert!(error.contains("RPC backend") && error.contains("AGENTSWAP_RPC_URL_1"), "{error}");
assert!(!error.contains("for block span"), "{error}");
}
}
#[tokio::test]
async fn other_chains_and_other_rpc_errors_are_not_retried() {
for (chain, code, message) in [
(8453, -32602, "Archive requests require a personal token"),
(1, -32000, "Archive requests require a personal token"),
(1, -32602, "invalid filter"),
] {
let rpc = TestRpc::start(move |body| {
Some(if body["method"] == "eth_chainId" {
ok(body, json!(format!("0x{chain:x}")))
} else {
json!({"jsonrpc":"2.0", "id":body["id"], "error":{"code":code,"message":message}})
})
});
let config = evm::chain_config(&chain.to_string()).unwrap();
let provider = evm::read_provider_checked(&rpc.url, chain).await.unwrap();
let settler = IntentSettlerV3::new(config.settler, provider);
let filter = settler.IntentAnnounced_filter();
assert!(evm::event_query(config, 1, 2, || filter.query()).await.is_err());
assert_eq!(rpc.called("eth_getLogs"), 1);
assert_eq!(rpc.called("eth_chainId"), 1);
}
}
#[tokio::test]
async fn unreadable_chain_id_fails_closed() {
let rpc =
TestRpc::start(|body| Some(crate::service::test_rpc::failure(body, "unavailable")));
let error = evm::read_provider_checked(&rpc.url, 1).await.unwrap_err();
assert!(error.to_string().contains("failed to read RPC chain id"));
assert_eq!(rpc.called("eth_chainId"), 1);
assert_eq!(rpc.called("eth_call"), 0);
}