use crate::signer::Signer;
use alloy::consensus::TxEnvelope;
use alloy::network::{EthereumWallet, TxSigner};
use alloy::primitives::{Address, Signature};
use alloy::rpc::types::TransactionRequest;
use alloy::providers::{DynProvider, Provider, ProviderBuilder};
use alloy::signers::Error as SignerError;
use eyre::{eyre, Result};
use std::sync::Arc;
mod events;
pub use events::query as event_query;
#[derive(Debug, Clone, Copy)]
pub struct ChainConfig {
pub id: u64,
pub rpc: &'static str,
pub factory: Address,
pub settler: Address,
pub generation: &'static str,
pub lens: Address,
}
const EVENT_LOOKBACK_BLOCKS: u64 = 200_000;
const PUBLIC_DEFAULT_EVENT_LOOKBACK_BLOCKS: u64 = 9_000;
pub const EVENT_CHUNK_SIZE: u64 = 5_000;
pub fn chain_config(chain_id: &str) -> Result<ChainConfig> {
let id = crate::tokens::chain_name_to_id(chain_id)
.ok_or_else(|| eyre!("{}", crate::tokens::unknown_chain_id(chain_id)))?;
let rpc = match id {
1 => "https://ethereum-rpc.publicnode.com",
8453 => "https://mainnet.base.org",
42161 => "https://arb1.arbitrum.io/rpc",
56 => "https://bsc-rpc.publicnode.com",
4663 => "https://rpc.mainnet.chain.robinhood.com",
5042 => "https://rpc.mainnet.arc.io",
5042002 => "https://rpc.testnet.arc.io",
_ => return Err(eyre!("intent protocol is unavailable on chain id {id}")),
};
let (generation, factory, settler, lens) = match id {
1 => ("v7",
alloy::primitives::address!("0x121e3622C1a8D26DDa7E84c812607802BfFD23d1"),
alloy::primitives::address!("0xB522a73B93D0F567bBCa1b53351e38358C648bC6"),
alloy::primitives::address!("0x120b028Eeb7c8FB5fed2519cEBFa7e8F5A526FB4")),
8453 | 42161 | 56 | 4663 | 5042 | 5042002 => ("v6",
alloy::primitives::address!("0xc1660e4BbC825f8367dA92b60dccc17E4E10bc26"),
alloy::primitives::address!("0x2dd81c4fD1FC38b009Ab10D5C9b1f01Ca51cE462"),
alloy::primitives::address!("0x3AFfAafAF3Ec0A8A535723CBf0891482680F30C3")),
_ => return Err(eyre!("intent protocol is unavailable on chain id {id}")),
};
Ok(ChainConfig { id, rpc, factory, settler, generation, lens })
}
pub fn agent_chain_config(chain_id: &str) -> Result<ChainConfig> {
let config = chain_config(chain_id)?;
if config.id == 1 {
return Err(eyre!("agent delegation is unavailable on Ethereum (1); trade and policy refuse chain 1; use intents for swaps"));
}
Ok(config)
}
pub fn wrapped_native(chain: u64) -> Option<Address> {
if chain == 56 {
return Some(alloy::primitives::address!("0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"));
}
crate::tokens::resolve_token("WETH", chain).and_then(|(address, _, _)| address.parse().ok())
}
pub fn rpc_url(config: ChainConfig) -> String {
let key = format!("AGENTSWAP_RPC_URL_{}", config.id);
std::env::var(key)
.or_else(|_| std::env::var("AGENTSWAP_RPC_URL"))
.unwrap_or_else(|_| config.rpc.to_string())
}
pub fn read_provider(url: &str) -> Result<DynProvider> {
let url = url.parse().map_err(|e| eyre!("invalid RPC URL: {e}"))?;
Ok(ProviderBuilder::new().connect_http(url).erased())
}
pub async fn read_provider_checked(url: &str, expected_chain_id: u64) -> Result<DynProvider> {
let provider = read_provider(url)?;
let actual = provider.get_chain_id().await
.map_err(|error| eyre!("failed to read RPC chain id: {error}"))?;
if actual != expected_chain_id {
return Err(eyre!("RPC chain id {actual} does not match requested chain id {expected_chain_id}"));
}
Ok(provider)
}
pub fn event_lookback_blocks(config: ChainConfig, override_blocks: Option<u64>) -> u64 {
if let Some(blocks) = override_blocks {
return blocks;
}
if matches!(config.id, 1 | 56) && !has_rpc_override(config.id) {
return PUBLIC_DEFAULT_EVENT_LOOKBACK_BLOCKS;
}
EVENT_LOOKBACK_BLOCKS
}
pub async fn event_start_block(provider: &DynProvider, lookback_blocks: u64) -> Result<u64> {
let latest = provider.get_block_number().await?;
Ok(latest.saturating_sub(lookback_blocks))
}
pub fn event_query_error(
config: ChainConfig,
from_block: u64,
to_block: u64,
error: impl std::fmt::Display,
) -> eyre::Report {
let message = error.to_string();
let lower = message.to_ascii_lowercase();
if lower.contains("limit exceeded") || lower.contains("archive") {
return eyre!(
"eth_getLogs refused by the RPC backend on {} while reading blocks {}..={}; access or quota restrictions may apply; set AGENTSWAP_RPC_URL_{} (or AGENTSWAP_RPC_URL) to a keyed RPC endpoint",
crate::tokens::chain_id_to_name(config.id),
from_block,
to_block,
config.id
);
}
eyre!("{message}")
}
fn has_rpc_override(chain_id: u64) -> bool {
let chain_key = format!("AGENTSWAP_RPC_URL_{chain_id}");
std::env::var_os(chain_key).is_some() || std::env::var_os("AGENTSWAP_RPC_URL").is_some()
}
const GAS_HEADROOM_PERCENT: u64 = 25;
pub async fn sign_transaction(
url: &str,
signer: Arc<dyn Signer>,
mut request: TransactionRequest,
) -> Result<(DynProvider, TxEnvelope)> {
let url = url.parse().map_err(|e| eyre!("invalid RPC URL: {e}"))?;
let wallet = EthereumWallet::new(WalletSigner(signer));
let provider = ProviderBuilder::new().wallet(wallet).connect_http(url);
if request.gas.is_none() {
let estimate = provider.estimate_gas(request.clone()).await?;
request.gas = Some(estimate.saturating_add(estimate / 100 * GAS_HEADROOM_PERCENT));
}
let envelope = provider
.fill(request)
.await?
.try_into_envelope()
.map_err(|_| eyre!("the transaction was filled but not signed"))?;
Ok((provider.erased(), envelope))
}
#[derive(Clone)]
struct WalletSigner(Arc<dyn Signer>);
impl std::fmt::Debug for WalletSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("WalletSigner").field(&self.0.address()).finish()
}
}
#[async_trait::async_trait]
impl TxSigner<Signature> for WalletSigner {
fn address(&self) -> Address {
self.0.address()
}
async fn sign_transaction(
&self,
tx: &mut dyn alloy::consensus::SignableTransaction<Signature>,
) -> alloy::signers::Result<Signature> {
self.0
.sign_hash(tx.signature_hash())
.await
.map_err(SignerError::other)
}
}
#[cfg(test)]
pub const GRANT_FIXTURES: [&str; 2] = [
"https://app.agentswap.co/grant?v=1&chain=8453&agent=0x2000000000000000000000000000000000000002&label=dust+sweep&t=0x1000000000000000000000000000000000000001%3A1.2345&t=0x1000000000000000000000000000000000000002%3A0.000000000000000005&t=0x3000000000000000000000000000000000000003%3A0&epoch=1w&expiry=2026-10-10T12%3A00%3A00Z&actions=market%2Cintent&owner=0x4000000000000000000000000000000000000004",
"https://app.agentswap.co/grant?v=1&chain=42161&agent=0x2000000000000000000000000000000000000002&t=0x1000000000000000000000000000000000000001%3A250&t=0x1000000000000000000000000000000000000002%3A0.5&t=0x3000000000000000000000000000000000000003%3A0&epoch=1w&expiry=30d&actions=market%2Cintent",
];
#[cfg(test)]
pub const GRANT_TOKENS: [&str; 3] = [
"0x1000000000000000000000000000000000000001",
"0x1000000000000000000000000000000000000002",
"0x3000000000000000000000000000000000000003",
];
#[cfg(test)]
pub const GRANT_AGENT: &str = "0x2000000000000000000000000000000000000002";
#[cfg(test)]
pub const GRANT_OWNER: &str = "0x4000000000000000000000000000000000000004";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ethereum_rpc_overrides_and_public_lookback_are_respected() {
if let Ok(case) = std::env::var("ETHEREUM_RPC_TEST_CASE") {
let config = chain_config("1").unwrap();
let (url, blocks) = match case.as_str() {
"chain" => ("http://127.0.0.1:1", 200_000),
"global" => ("http://127.0.0.1:2", 200_000),
_ => ("https://ethereum-rpc.publicnode.com", 9_000),
};
assert_eq!(rpc_url(config), url);
assert_eq!(event_lookback_blocks(config, None), blocks);
assert_eq!(event_lookback_blocks(config, Some(42)), 42);
assert_eq!(agent_chain_config("8453").unwrap().generation, "v6");
assert!(agent_chain_config("1").is_err());
return;
}
for case in ["public", "global", "chain"] {
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
command.args(["ethereum_rpc_overrides_and_public_lookback_are_respected", "--nocapture"])
.env("ETHEREUM_RPC_TEST_CASE", case)
.env_remove("AGENTSWAP_RPC_URL_1").env_remove("AGENTSWAP_RPC_URL");
if case != "public" { command.env("AGENTSWAP_RPC_URL", "http://127.0.0.1:2"); }
if case == "chain" { command.env("AGENTSWAP_RPC_URL_1", "http://127.0.0.1:1"); }
let output = command.output().unwrap();
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stdout));
}
}
#[test]
fn ethereum_v7_contracts_and_v6_chains_are_pinned() {
for alias in ["1", "ethereum", "eth", "mainnet"] {
let config = chain_config(alias).expect("Ethereum V7 config");
assert_eq!(config.id, 1);
assert_eq!(config.generation, "v7");
assert_eq!(config.factory, alloy::primitives::address!("0x121e3622C1a8D26DDa7E84c812607802BfFD23d1"));
assert_eq!(config.settler, alloy::primitives::address!("0xB522a73B93D0F567bBCa1b53351e38358C648bC6"));
assert_eq!(config.lens, alloy::primitives::address!("0x120b028Eeb7c8FB5fed2519cEBFa7e8F5A526FB4"));
}
for id in ["8453", "42161", "56", "4663", "5042", "5042002"] {
let config = chain_config(id).unwrap();
assert_eq!(config.generation, "v6");
assert_eq!(config.factory, alloy::primitives::address!("0xc1660e4BbC825f8367dA92b60dccc17E4E10bc26"));
assert_eq!(config.settler, alloy::primitives::address!("0x2dd81c4fD1FC38b009Ab10D5C9b1f01Ca51cE462"));
assert_eq!(config.lens, alloy::primitives::address!("0x3AFfAafAF3Ec0A8A535723CBf0891482680F30C3"));
}
}
#[test]
fn arc_rpc_uses_the_shared_v6_contracts() {
for alias in ["arc", "arc-mainnet", "5042"] {
let arc = chain_config(alias).expect("Arc config");
assert_eq!(arc.id, 5042);
assert_eq!(arc.rpc, "https://rpc.mainnet.arc.io");
for chain in ["base", "arbitrum", "bsc", "robinhood", "arc-testnet"] {
let existing = chain_config(chain).unwrap();
assert_eq!(arc.factory, existing.factory);
assert_eq!(arc.settler, existing.settler);
assert_eq!(arc.lens, existing.lens);
assert_eq!(arc.generation, existing.generation);
}
assert_eq!(event_lookback_blocks(arc, None), 200_000);
assert_eq!(event_lookback_blocks(arc, Some(300_000)), 300_000);
}
}
#[test]
fn arc_testnet_rpc_uses_the_shared_v6_contracts() {
for alias in ["arc-testnet", "arct", "5042002"] {
let arc = chain_config(alias).expect("Arc Testnet config");
assert_eq!(arc.id, 5042002);
assert_eq!(arc.rpc, "https://rpc.testnet.arc.io");
for chain in ["base", "arbitrum", "bsc", "robinhood"] {
let existing = chain_config(chain).unwrap();
assert_eq!(arc.factory, existing.factory);
assert_eq!(arc.settler, existing.settler);
assert_eq!(arc.lens, existing.lens);
assert_eq!(arc.generation, existing.generation);
}
assert_eq!(event_lookback_blocks(arc, None), 200_000);
assert_eq!(event_lookback_blocks(arc, Some(300_000)), 300_000);
}
}
#[test]
fn resolved_chain_without_v6_deployment_reports_unavailability() {
let error = chain_config("10").expect_err("Optimism resolves but has no deployment");
assert_eq!(format!("{error}"), "intent protocol is unavailable on chain id 10");
}
}