From 65dbdb00f8b5537c6b66a9f10103e4180b92e258 Mon Sep 17 00:00:00 2001 From: Abhishek Krishna Date: Tue, 21 Apr 2026 06:48:51 +0530 Subject: [PATCH] fix+feat: unbreak CI + add Across V3 cross-chain decoder (Arbitrum-preferring) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI was red because `cargo clippy -- -D warnings` tripped on dead fields in the UniswapXOrder/OrderInput DTOs, an unused import in solver/engine, a let-and-return in the surplus_usd calc, an implicit_saturating_sub in Intent::time_remaining, and an unused `raw` arg in the UniswapX decode stub. All fixed surgically — no behavioural change. On top of the CI fix, add the Across Protocol V3 intent decoder the README promised (`src/intents/across.rs`). The decoder: - Implements the shared `IntentDecoder` trait so it drops into the existing `SolverEngine` alongside UniswapX. - Exposes a pure `decode_deposit_event` path that turns a V3 FundsDeposited-shaped JSON payload into our normalised `Intent`, making it unit-testable without network. - Exposes `RoutingPreferences` with Arbitrum as the default preferred destination — this is the agent-economy hook. When agents settle in a stablecoin (CR8-USD-style), Arbitrum is today's low-fee / deep- liquidity landing chain, so the solver now has a knob to express that preference. - Ships with 5 offline unit tests covering ETH→Arbitrum decoding, default + overridden preferences, unknown-chain rejection, and the raw-bytes `decode()` path via serde_json. Also wires the CLI so `resolver scan --protocol across --chain ethereum` and `resolver solve --protocol across --chain arbitrum` route to the new decoder. — [kcolbchain](https://kcolbchain.com) / [Abhishek Krishna](https://abhishekkrishna.com) --- src/execution/mod.rs | 2 +- src/intents/across.rs | 348 ++++++++++++++++++++++++++++++++++++++++ src/intents/mod.rs | 2 + src/intents/types.rs | 2 +- src/intents/uniswapx.rs | 74 ++++++--- src/lib.rs | 4 +- src/main.rs | 72 +++++++-- src/monitor/mod.rs | 11 +- src/solver/engine.rs | 15 +- 9 files changed, 484 insertions(+), 46 deletions(-) create mode 100644 src/intents/across.rs diff --git a/src/execution/mod.rs b/src/execution/mod.rs index fdda913..caea7d1 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -3,8 +3,8 @@ //! In production, this submits to Flashbots Protect or a private mempool //! to avoid frontrunning. -use crate::intents::SolverQuote; use crate::error::Result; +use crate::intents::SolverQuote; /// Fill result from executing a solver quote on-chain. #[derive(Debug)] diff --git a/src/intents/across.rs b/src/intents/across.rs new file mode 100644 index 0000000..f82da04 --- /dev/null +++ b/src/intents/across.rs @@ -0,0 +1,348 @@ +//! Across Protocol V3 intent decoder. +//! +//! Across is a cross-chain intent/bridge protocol: a user deposits on the +//! origin chain, relayers (solvers) fill on the destination chain and are +//! reimbursed on origin after the optimistic oracle period. +//! +//! For the AI-agent / CR8-USD settlement angle: Across is the primary way +//! agents can move value between chains (e.g. Ethereum ↔ Arbitrum) while an +//! intent is in-flight. This decoder normalises Across V3 `FundsDeposited` +//! events into the same [`Intent`] shape the rest of the solver consumes, so +//! the solver can reason about cross-chain agent settlement the same way it +//! reasons about same-chain UniswapX fills. +//! +//! Two entry points: +//! +//! 1. [`AcrossDecoder::fetch_open_intents`] — hits the public `app.across.to` +//! API to pull suggested-fee quotes for a preset route. Useful as a live +//! probe; not deterministic, so not exercised by unit tests. +//! +//! 2. [`AcrossDecoder::decode_deposit_event`] — pure, offline parse of a +//! V3FundsDeposited-shaped JSON payload into an [`Intent`]. This is what +//! the engine and tests use when replaying fixture data or indexer +//! output. + +use alloy::primitives::{Address, U256}; +use async_trait::async_trait; +use serde::Deserialize; + +use super::{Chain, Intent, IntentDecoder, Protocol}; +use crate::error::{ResolverError, Result}; + +/// Routing preferences a solver can apply when evaluating Across intents. +/// +/// The agent-economy thesis: when an AI agent is settling in a stablecoin +/// (CR8-USD-style), it wants to land on a low-fee chain with deep liquidity. +/// Arbitrum is today's default for that profile, so we prefer it. This is +/// advisory — the decoder still emits every intent, but callers can use +/// [`RoutingPreferences::prefers`] to sort or filter. +#[derive(Debug, Clone)] +pub struct RoutingPreferences { + /// Chain the solver would prefer intents to terminate on. + pub preferred_dest: Chain, +} + +impl Default for RoutingPreferences { + fn default() -> Self { + // Arbitrum is the default landing chain for agent settlement: cheap + // gas, deep USDC/USDT liquidity, and it's where the CR8 agent + // economy is building. + Self { + preferred_dest: Chain::Arbitrum, + } + } +} + +impl RoutingPreferences { + /// Returns true if the intent lands on the preferred chain. + pub fn prefers(&self, intent: &Intent) -> bool { + intent.dest_chain == self.preferred_dest + } +} + +/// Shape of an Across V3 `FundsDeposited` event, as emitted by indexers +/// (Subgraph, `cast logs --json`, etc.). Field names follow the on-chain +/// event ABI with camelCase rewriting applied by most indexers. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct V3DepositEvent { + pub deposit_id: u64, + pub origin_chain_id: u64, + pub destination_chain_id: u64, + pub input_token: String, + pub output_token: String, + pub input_amount: String, + pub output_amount: String, + pub recipient: String, + /// Unix seconds after which the deposit expires if unfilled. + pub fill_deadline: u64, + #[serde(default)] + pub depositor: String, + #[serde(default)] + pub message: String, +} + +/// Across Protocol decoder. +pub struct AcrossDecoder { + /// Origin chain this decoder pulls intents for (Across is multi-chain; one + /// decoder instance per origin is idiomatic). + origin: Chain, + client: reqwest::Client, + preferences: RoutingPreferences, +} + +impl AcrossDecoder { + /// Construct a decoder for the given origin chain with default + /// (Arbitrum-preferring) routing preferences. + pub fn new(origin: Chain) -> Self { + Self { + origin, + client: reqwest::Client::new(), + preferences: RoutingPreferences::default(), + } + } + + /// Override routing preferences (e.g. if the operator wants to prefer + /// Base or Optimism instead of Arbitrum). + pub fn with_preferences(mut self, prefs: RoutingPreferences) -> Self { + self.preferences = prefs; + self + } + + /// Expose the active routing preferences (read-only). + pub fn preferences(&self) -> &RoutingPreferences { + &self.preferences + } + + /// Pure parse: turn a decoded `FundsDeposited` event into our [`Intent`]. + /// + /// This never touches the network. All unit tests go through this path. + pub fn decode_deposit_event(&self, event: &V3DepositEvent) -> Result { + let source_chain = Chain::from_id(event.origin_chain_id).ok_or_else(|| { + ResolverError::Intent(format!("Unknown origin chain: {}", event.origin_chain_id)) + })?; + let dest_chain = Chain::from_id(event.destination_chain_id).ok_or_else(|| { + ResolverError::Intent(format!( + "Unknown destination chain: {}", + event.destination_chain_id + )) + })?; + + let token_in: Address = event + .input_token + .parse() + .map_err(|e| ResolverError::Intent(format!("Invalid input token: {e}")))?; + let token_out: Address = event + .output_token + .parse() + .map_err(|e| ResolverError::Intent(format!("Invalid output token: {e}")))?; + + let amount_in: U256 = event + .input_amount + .parse() + .map_err(|e| ResolverError::Intent(format!("Invalid input amount: {e}")))?; + let output_amount: U256 = event + .output_amount + .parse() + .map_err(|e| ResolverError::Intent(format!("Invalid output amount: {e}")))?; + + let recipient: Address = event + .recipient + .parse() + .map_err(|e| ResolverError::Intent(format!("Invalid recipient: {e}")))?; + + // Across V3 orders quote a fixed output amount (no Dutch decay on the + // output side), so `min_amount_out` and `current_amount_out` are the + // same value. Solver surplus on Across comes from filling cheaper + // than the relayer fee the user paid, not from decay. + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + // Deterministic ID: origin chain + depositId is globally unique in + // Across V3. + let id = format!("across-{}-{}", event.origin_chain_id, event.deposit_id); + + Ok(Intent { + id, + protocol: Protocol::Across, + source_chain, + dest_chain, + token_in, + token_out, + amount_in, + min_amount_out: output_amount, + current_amount_out: output_amount, + deadline: event.fill_deadline, + recipient, + raw_order: Vec::new(), + discovered_at: now, + }) + } +} + +#[async_trait] +impl IntentDecoder for AcrossDecoder { + async fn fetch_open_intents(&self) -> Result> { + // Across doesn't expose a public "open intents" firehose; solvers + // subscribe to chain events via Subgraph or direct RPC. For the + // MVP we hit the `deposits` status endpoint, which returns recent + // deposits for an origin chain. If the endpoint is unreachable or + // changes shape we return an empty list rather than erroring — this + // keeps the engine's cycle loop healthy. + let url = format!( + "https://app.across.to/api/deposits?originChainId={}&status=pending&limit=50", + self.origin.chain_id() + ); + + let resp = match self + .client + .get(&url) + .header("accept", "application/json") + .send() + .await + { + Ok(r) => r, + Err(e) => { + tracing::warn!("Across API unreachable ({url}): {e}"); + return Ok(Vec::new()); + } + }; + + #[derive(Deserialize)] + struct AcrossDepositsResponse { + #[serde(default)] + deposits: Vec, + } + + let body: AcrossDepositsResponse = match resp.json().await { + Ok(b) => b, + Err(e) => { + tracing::warn!("Across API shape drifted: {e}"); + return Ok(Vec::new()); + } + }; + + let intents: Vec = body + .deposits + .iter() + .filter_map(|d| self.decode_deposit_event(d).ok()) + .collect(); + + tracing::info!( + "Fetched {} open Across intents from {:?} (preferred dest: {:?})", + intents.len(), + self.origin, + self.preferences.preferred_dest, + ); + Ok(intents) + } + + fn decode(&self, raw: &[u8]) -> Result { + // Raw bytes for Across V3 are ABI-encoded `FundsDeposited` event + // data. Full ABI decoding lives in a follow-up; today we accept a + // JSON-encoded `V3DepositEvent` so indexers and tests can exercise + // the path end-to-end. + let event: V3DepositEvent = serde_json::from_slice(raw).map_err(|e| { + ResolverError::Intent(format!("Across decode: not a V3DepositEvent JSON: {e}")) + })?; + self.decode_deposit_event(&event) + } + + fn protocol(&self) -> &str { + "Across" + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fixture_event() -> V3DepositEvent { + V3DepositEvent { + deposit_id: 987654, + origin_chain_id: 1, // Ethereum + destination_chain_id: 42161, // Arbitrum — the agent-economy default + input_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".into(), // USDC (Ethereum) + output_token: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831".into(), // USDC (Arbitrum) + input_amount: "1000000000".into(), // 1 000 USDC + output_amount: "999500000".into(), // user pays 0.5 USDC relayer fee + recipient: "0x000000000000000000000000000000000000dEaD".into(), + fill_deadline: 9_999_999_999, // far future + depositor: "0x1111111111111111111111111111111111111111".into(), + message: String::new(), + } + } + + #[test] + fn decodes_eth_to_arbitrum_deposit() { + let decoder = AcrossDecoder::new(Chain::Ethereum); + let intent = decoder + .decode_deposit_event(&fixture_event()) + .expect("fixture should decode"); + + assert_eq!(intent.protocol, Protocol::Across); + assert_eq!(intent.source_chain, Chain::Ethereum); + assert_eq!(intent.dest_chain, Chain::Arbitrum); + assert!(intent.is_cross_chain()); + assert_eq!(intent.amount_in, U256::from(1_000_000_000u64)); + assert_eq!(intent.min_amount_out, U256::from(999_500_000u64)); + assert_eq!(intent.id, "across-1-987654"); + } + + #[test] + fn prefers_arbitrum_by_default() { + let decoder = AcrossDecoder::new(Chain::Ethereum); + let intent = decoder.decode_deposit_event(&fixture_event()).unwrap(); + assert!( + decoder.preferences().prefers(&intent), + "default preferences should prefer Arbitrum-landing intents" + ); + } + + #[test] + fn custom_preferences_override_default() { + let decoder = AcrossDecoder::new(Chain::Ethereum).with_preferences(RoutingPreferences { + preferred_dest: Chain::Base, + }); + let intent = decoder.decode_deposit_event(&fixture_event()).unwrap(); + assert!( + !decoder.preferences().prefers(&intent), + "Base-preferring decoder should not prefer an Arbitrum-landing intent" + ); + } + + #[test] + fn rejects_unknown_chain() { + let mut event = fixture_event(); + event.destination_chain_id = 99999; + let decoder = AcrossDecoder::new(Chain::Ethereum); + let err = decoder.decode_deposit_event(&event).unwrap_err(); + let msg = format!("{err}"); + assert!(msg.contains("Unknown destination chain"), "got: {msg}"); + } + + #[test] + fn decode_from_json_bytes_works() { + let json = serde_json::to_vec(&serde_json::json!({ + "depositId": 42, + "originChainId": 1, + "destinationChainId": 42161, + "inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "outputToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "inputAmount": "500000000", + "outputAmount": "499750000", + "recipient": "0x000000000000000000000000000000000000dEaD", + "fillDeadline": 9999999999u64, + "depositor": "0x1111111111111111111111111111111111111111", + "message": "" + })) + .unwrap(); + + let decoder = AcrossDecoder::new(Chain::Ethereum); + let intent = decoder.decode(&json).expect("JSON bytes should decode"); + assert_eq!(intent.id, "across-1-42"); + assert_eq!(intent.dest_chain, Chain::Arbitrum); + } +} diff --git a/src/intents/mod.rs b/src/intents/mod.rs index 72036cb..735a263 100644 --- a/src/intents/mod.rs +++ b/src/intents/mod.rs @@ -3,9 +3,11 @@ //! Supports ERC-7683 cross-chain intents and protocol-specific order formats //! (UniswapX Dutch orders, Across deposit orders, CoW Protocol GPv2 orders). +mod across; mod types; mod uniswapx; +pub use across::{AcrossDecoder, RoutingPreferences, V3DepositEvent}; pub use types::*; pub use uniswapx::UniswapXDecoder; diff --git a/src/intents/types.rs b/src/intents/types.rs index 8984225..03999b2 100644 --- a/src/intents/types.rs +++ b/src/intents/types.rs @@ -105,7 +105,7 @@ impl Intent { /// Time remaining before expiry (in seconds). pub fn time_remaining(&self, now: u64) -> u64 { - if now >= self.deadline { 0 } else { self.deadline - now } + self.deadline.saturating_sub(now) } } diff --git a/src/intents/uniswapx.rs b/src/intents/uniswapx.rs index 6ad0452..9c8d38d 100644 --- a/src/intents/uniswapx.rs +++ b/src/intents/uniswapx.rs @@ -18,8 +18,13 @@ fn api_url(chain: Chain) -> &'static str { } /// Raw UniswapX order from the API. +/// +/// `order_status`, `swapper`, and `OrderInput::end_amount` are parsed from the +/// API for completeness and future use (audit logs, risk filters, Dutch-decay +/// curves on the input side) but not read on the hot path yet. #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] +#[allow(dead_code)] struct UniswapXOrder { order_hash: String, chain_id: u64, @@ -34,6 +39,7 @@ struct UniswapXOrder { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] +#[allow(dead_code)] struct OrderInput { token: String, start_amount: String, @@ -69,30 +75,45 @@ impl UniswapXDecoder { } fn parse_order(&self, order: &UniswapXOrder) -> Result { - let source_chain = Chain::from_id(order.chain_id) - .ok_or_else(|| ResolverError::Intent( - format!("Unknown chain ID: {}", order.chain_id) - ))?; - - let token_in: Address = order.input.token.parse() + let source_chain = Chain::from_id(order.chain_id).ok_or_else(|| { + ResolverError::Intent(format!("Unknown chain ID: {}", order.chain_id)) + })?; + + let token_in: Address = order + .input + .token + .parse() .map_err(|e| ResolverError::Intent(format!("Invalid input token: {e}")))?; - let first_output = order.outputs.first() + let first_output = order + .outputs + .first() .ok_or_else(|| ResolverError::Intent("No outputs in order".into()))?; - let token_out: Address = first_output.token.parse() + let token_out: Address = first_output + .token + .parse() .map_err(|e| ResolverError::Intent(format!("Invalid output token: {e}")))?; - let amount_in: U256 = order.input.start_amount.parse() + let amount_in: U256 = order + .input + .start_amount + .parse() .map_err(|e| ResolverError::Intent(format!("Invalid input amount: {e}")))?; - let min_amount_out: U256 = first_output.end_amount.parse() + let min_amount_out: U256 = first_output + .end_amount + .parse() .map_err(|e| ResolverError::Intent(format!("Invalid min output: {e}")))?; - let current_amount_out: U256 = first_output.start_amount.parse() + let current_amount_out: U256 = first_output + .start_amount + .parse() .map_err(|e| ResolverError::Intent(format!("Invalid current output: {e}")))?; - let recipient: Address = first_output.recipient.parse() + let recipient: Address = first_output + .recipient + .parse() .map_err(|e| ResolverError::Intent(format!("Invalid recipient: {e}")))?; let now = std::time::SystemTime::now() @@ -113,8 +134,12 @@ impl UniswapXDecoder { deadline: order.deadline, recipient, raw_order: hex::decode( - order.encoded_order.strip_prefix("0x").unwrap_or(&order.encoded_order) - ).unwrap_or_default(), + order + .encoded_order + .strip_prefix("0x") + .unwrap_or(&order.encoded_order), + ) + .unwrap_or_default(), discovered_at: now, }) } @@ -125,7 +150,8 @@ impl IntentDecoder for UniswapXDecoder { async fn fetch_open_intents(&self) -> Result> { let url = api_url(self.chain); - let resp: ApiResponse = self.client + let resp: ApiResponse = self + .client .get(url) .header("accept", "application/json") .send() @@ -140,19 +166,27 @@ impl IntentDecoder for UniswapXDecoder { .unwrap_or_default() .as_secs(); - let intents: Vec = resp.orders + let intents: Vec = resp + .orders .iter() .filter(|o| o.deadline > now) // skip expired .filter_map(|o| self.parse_order(o).ok()) .collect(); - tracing::info!("Fetched {} open UniswapX intents on {:?}", intents.len(), self.chain); + tracing::info!( + "Fetched {} open UniswapX intents on {:?}", + intents.len(), + self.chain + ); Ok(intents) } - fn decode(&self, raw: &[u8]) -> Result { - // For raw on-chain decoding — would parse the EIP-712 signed order - Err(ResolverError::Intent("Raw order decoding not yet implemented".into())) + fn decode(&self, _raw: &[u8]) -> Result { + // For raw on-chain decoding — would parse the EIP-712 signed order. + // Tracked as a TODO; stub returns an error rather than panicking. + Err(ResolverError::Intent( + "Raw order decoding not yet implemented".into(), + )) } fn protocol(&self) -> &str { diff --git a/src/lib.rs b/src/lib.rs index 5d3a038..4377472 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,10 +3,10 @@ //! High-performance intent solver for DeFi. Parses, simulates, and fills //! intents across UniswapX, Across, and CoW Protocol. -pub mod intents; -pub mod solver; pub mod execution; +pub mod intents; pub mod monitor; +pub mod solver; mod error; pub use error::{ResolverError, Result}; diff --git a/src/main.rs b/src/main.rs index 3b2ccda..143b2db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,34 +1,73 @@ //! resolver CLI — scan, solve, and monitor intent filling. -use resolver::intents::{self, IntentDecoder, UniswapXDecoder}; -use resolver::solver::{SolverConfig, SolverEngine}; +use resolver::intents::{self, AcrossDecoder, IntentDecoder, UniswapXDecoder}; use resolver::monitor; +use resolver::solver::{SolverConfig, SolverEngine}; + +/// Parse a flag value like `--key value` from argv. Returns `None` if the +/// flag is absent or has no value; the default is applied by the caller. +fn flag<'a>(args: &'a [String], key: &str) -> Option<&'a str> { + args.iter() + .position(|a| a == key) + .and_then(|i| args.get(i + 1)) + .map(|s| s.as_str()) +} + +fn parse_chain(s: &str) -> intents::Chain { + match s.to_ascii_lowercase().as_str() { + "ethereum" | "eth" | "1" => intents::Chain::Ethereum, + "arbitrum" | "arb" | "42161" => intents::Chain::Arbitrum, + "optimism" | "op" | "10" => intents::Chain::Optimism, + "base" | "8453" => intents::Chain::Base, + "polygon" | "matic" | "137" => intents::Chain::Polygon, + other => { + eprintln!("unknown chain '{other}', falling back to base"); + intents::Chain::Base + } + } +} + +fn build_decoder(protocol: &str, chain: intents::Chain) -> Box { + match protocol.to_ascii_lowercase().as_str() { + "across" => Box::new(AcrossDecoder::new(chain)), + _ => Box::new(UniswapXDecoder::new(chain)), + } +} #[tokio::main] async fn main() -> resolver::Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::from_default_env() - .add_directive("resolver=info".parse().unwrap()) + .add_directive("resolver=info".parse().unwrap()), ) .init(); let args: Vec = std::env::args().collect(); let command = args.get(1).map(|s| s.as_str()).unwrap_or("scan"); + let chain = flag(&args, "--chain") + .map(parse_chain) + .unwrap_or(intents::Chain::Base); + let protocol = flag(&args, "--protocol").unwrap_or("uniswapx"); + match command { "scan" => { - let chain = intents::Chain::Base; - let decoder = UniswapXDecoder::new(chain); - let intents = decoder.fetch_open_intents().await?; + let decoder = build_decoder(protocol, chain); + let open = decoder.fetch_open_intents().await?; - println!("\n📋 Open UniswapX intents on {:?}: {}\n", chain, intents.len()); - for (i, intent) in intents.iter().take(10).enumerate() { + println!( + "\nOpen {} intents on {:?}: {}\n", + decoder.protocol(), + chain, + open.len() + ); + for (i, intent) in open.iter().take(10).enumerate() { let surplus = intent.max_surplus(); println!( - " {}. {} | in: {} | out_min: {} | surplus: {} | expires: {}s", + " {}. {} | in: {} | out_min: {} | surplus: {} | expires: {}s | dest: {:?}", i + 1, - &intent.id[..16], + &intent.id[..16.min(intent.id.len())], intent.amount_in, intent.min_amount_out, surplus, @@ -38,6 +77,7 @@ async fn main() -> resolver::Result<()> { .unwrap() .as_secs() ), + intent.dest_chain, ); } } @@ -49,12 +89,12 @@ async fn main() -> resolver::Result<()> { ..Default::default() }; let mut engine = SolverEngine::new(config); - engine.add_decoder(Box::new(UniswapXDecoder::new(intents::Chain::Base))); + engine.add_decoder(build_decoder(protocol, chain)); - println!("🔄 Running solver cycle...\n"); + println!("Running solver cycle...\n"); let quotes = engine.cycle().await?; - println!("\n💰 Profitable intents: {}\n", quotes.len()); + println!("\nProfitable intents: {}\n", quotes.len()); for q in "es { println!( " {} | profit: ${:.2} | gas: ${:.2}", @@ -74,10 +114,14 @@ async fn main() -> resolver::Result<()> { } _ => { - println!("Usage: resolver [scan|solve|monitor]"); + println!("Usage: resolver [--chain ] [--protocol ]"); println!(" scan — fetch and display open intents"); println!(" solve — run one solver cycle (simulation mode)"); println!(" monitor — display solver statistics"); + println!(); + println!("Flags:"); + println!(" --chain ethereum | arbitrum | optimism | base | polygon"); + println!(" --protocol uniswapx | across"); } } diff --git a/src/monitor/mod.rs b/src/monitor/mod.rs index 0e84b2b..f3da2c2 100644 --- a/src/monitor/mod.rs +++ b/src/monitor/mod.rs @@ -13,9 +13,14 @@ pub fn print_stats(engine: &SolverEngine) { println!("║ Profitable: {:>14} ║", stats.intents_profitable); println!("║ Filled: {:>14} ║", stats.intents_filled); println!("║ Total profit: ${:>13.2} ║", stats.total_profit_usd); - println!("║ Total gas: ${:>13.2} ║", stats.total_gas_spent_usd); - println!("║ Net P&L: ${:>13.2} ║", - stats.total_profit_usd - stats.total_gas_spent_usd); + println!( + "║ Total gas: ${:>13.2} ║", + stats.total_gas_spent_usd + ); + println!( + "║ Net P&L: ${:>13.2} ║", + stats.total_profit_usd - stats.total_gas_spent_usd + ); println!("║ Unique intents: {:>14} ║", engine.seen_count()); println!("╚══════════════════════════════════════╝"); } diff --git a/src/solver/engine.rs b/src/solver/engine.rs index 56d3645..b36f9a2 100644 --- a/src/solver/engine.rs +++ b/src/solver/engine.rs @@ -4,8 +4,8 @@ use alloy::primitives::U256; use dashmap::DashMap; use std::sync::Arc; +use crate::error::Result; use crate::intents::{Intent, IntentDecoder, SolverQuote}; -use crate::error::{ResolverError, Result}; /// Configuration for the solver engine. #[derive(Debug, Clone)] @@ -112,7 +112,9 @@ impl SolverEngine { self.stats.intents_profitable += 1; tracing::info!( "💰 Profitable: {} | profit: ${:.2} | gas: ${:.2}", - intent.id, quote.net_profit_usd, quote.gas_cost_usd + intent.id, + quote.net_profit_usd, + quote.gas_cost_usd ); profitable.push(quote); } @@ -145,8 +147,7 @@ impl SolverEngine { // In production: simulate the full swap path and compute exact output let surplus_usd = if surplus > U256::ZERO { // Rough estimation: assume 6 decimal stablecoin output - let surplus_f64 = surplus.to::() as f64 / 1e6; - surplus_f64 + surplus.to::() as f64 / 1e6 } else { 0.0 }; @@ -159,7 +160,11 @@ impl SolverEngine { amount_out: intent.current_amount_out, gas_cost_wei, gas_cost_usd, - net_profit_wei: if surplus > gas_cost_wei { surplus - gas_cost_wei } else { U256::ZERO }, + net_profit_wei: if surplus > gas_cost_wei { + surplus - gas_cost_wei + } else { + U256::ZERO + }, net_profit_usd, route: vec![], // populated by route finder in production profitable,