agentswap 0.11.2

AgentSwap CLI for requesting quotes, inspecting tokens and pools, managing intents, and registering an API key against the AgentSwap service.
// Observed owner proceeds from confirmed execution events.
// Intent gross output includes a fee; only net proceeds are reported as received.
use crate::{evm, order_types::{self, IntentSettlerV3}, service::{sweep::{Output, Row}, submit::TxStatus}};
use alloy::{primitives::{Address, B256}, providers::{DynProvider, Provider}, sol_types::SolEvent};
use eyre::{Result, eyre};

alloy::sol! {
    event ExecutedAsAgent(address indexed agent, address indexed router, address indexed relayer,
        address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut);
}

pub(super) async fn report(chain: &str, proxy: &str, receive: &str, output: &mut Output) -> Result<()> {
    let config = evm::chain_config(chain)?;
    let provider = evm::read_provider(&evm::rpc_url(config))?;
    for row in &mut output.tokens {
        let received = if row.intent_status.as_deref() == Some("filled") {
            row.outcome = "sold".into();
            intent_received(&provider, config, row).await
        } else if row.tx_status == Some(TxStatus::Confirmed) {
            market_received(&provider, proxy.parse()?, receive.parse()?, row).await
        } else {
            if matches!(row.intent_status.as_deref(), Some("expired" | "cancelled" | "dead")) {
                row.outcome = "skipped".into(); row.reason = row.intent_status.clone();
            }
            continue;
        };
        match received {
            Ok(raw) => row.received_raw = Some(raw),
            Err(error) => row.warnings.push(format!("received amount unavailable: {}", crate::redact::urls(&error.to_string()))),
        }
    }
    Ok(())
}

async fn intent_received(provider: &DynProvider, config: evm::ChainConfig, row: &Row) -> Result<String> {
    let id: B256 = row.intent_id.as_deref().ok_or_else(|| eyre!("missing intent id"))?.parse()?;
    let first = row.placement_block.ok_or_else(|| eyre!("missing placement block"))?;
    let latest = provider.get_block_number().await?;
    let contract = IntentSettlerV3::new(config.settler, provider.clone());
    let mut start = first;
    while start <= latest {
        let end = start.saturating_add(evm::EVENT_CHUNK_SIZE - 1).min(latest);
        let mut filter = contract.IntentFilled_filter();
        filter.filter = filter.filter.from_block(start).to_block(end).topic1(id);
        if let Some((event, _)) = filter.query().await?.into_iter().next() {
            return Ok(event.receivedOut.checked_sub(event.fee).ok_or_else(|| eyre!("fill fee exceeds gross received"))?.to_string());
        }
        if end == latest { break; }
        start = end + 1;
    }
    Err(eyre!("fill event unavailable"))
}

async fn market_received(provider: &DynProvider, proxy: Address, receive: Address, row: &Row) -> Result<String> {
    let hash = row.tx_hash.as_deref().ok_or_else(|| eyre!("missing transaction hash"))?.parse()?;
    let receipt = provider.get_transaction_receipt(hash).await?.ok_or_else(|| eyre!("receipt unavailable"))?;
    for log in receipt.inner.logs() {
        if log.address() != proxy { continue; }
        if let Ok(event) = ExecutedAsAgent::decode_log(&log.inner) {
            if event.tokenIn == order_types::parse_address(&row.token)? && event.tokenOut == receive
                && event.amountIn == order_types::parse_u256(&row.amount_raw)? {
                return Ok(event.amountOut.to_string());
            }
        }
    }
    Err(eyre!("execution event unavailable"))
}

#[cfg(test)]
#[path = "received_tests.rs"]
mod tests;