use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use crate::error::{CoreError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IntentType {
Swap,
Limit,
Twap,
CrossChain,
Batch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IntentStatus {
Pending,
Executing,
Fulfilled,
Cancelled,
Expired,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Intent {
pub id: Uuid,
pub user_id: Uuid,
pub intent_type: IntentType,
pub sell_token_id: Uuid,
pub sell_amount: Decimal,
pub buy_token_id: Uuid,
pub min_buy_amount: Decimal,
pub max_slippage: Decimal,
pub expiration: DateTime<Utc>,
pub status: IntentStatus,
pub signature: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub filled_amount: Decimal,
pub solver_id: Option<Uuid>,
pub constraints: HashMap<String, String>,
}
impl Intent {
#[allow(clippy::too_many_arguments)]
pub fn new(
user_id: Uuid,
intent_type: IntentType,
sell_token_id: Uuid,
sell_amount: Decimal,
buy_token_id: Uuid,
min_buy_amount: Decimal,
max_slippage: Decimal,
expiration: DateTime<Utc>,
signature: String,
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
user_id,
intent_type,
sell_token_id,
sell_amount,
buy_token_id,
min_buy_amount,
max_slippage,
expiration,
status: IntentStatus::Pending,
signature,
created_at: now,
updated_at: now,
filled_amount: Decimal::ZERO,
solver_id: None,
constraints: HashMap::new(),
}
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.expiration
}
pub fn can_cancel(&self) -> bool {
matches!(self.status, IntentStatus::Pending | IntentStatus::Executing)
}
pub fn cancel(&mut self) -> Result<()> {
if !self.can_cancel() {
return Err(CoreError::Validation(
"Intent cannot be cancelled in current state".to_string(),
));
}
self.status = IntentStatus::Cancelled;
self.updated_at = Utc::now();
Ok(())
}
pub fn mark_executing(&mut self, solver_id: Uuid) -> Result<()> {
if self.status != IntentStatus::Pending {
return Err(CoreError::Validation(
"Intent must be pending to start execution".to_string(),
));
}
self.status = IntentStatus::Executing;
self.solver_id = Some(solver_id);
self.updated_at = Utc::now();
Ok(())
}
pub fn mark_fulfilled(&mut self, filled_amount: Decimal) -> Result<()> {
if self.status != IntentStatus::Executing {
return Err(CoreError::Validation(
"Intent must be executing to mark as fulfilled".to_string(),
));
}
self.status = IntentStatus::Fulfilled;
self.filled_amount = filled_amount;
self.updated_at = Utc::now();
Ok(())
}
pub fn verify_signature(&self) -> Result<bool> {
Ok(!self.signature.is_empty())
}
pub fn add_constraint(&mut self, key: String, value: String) {
self.constraints.insert(key, value);
self.updated_at = Utc::now();
}
pub fn unfilled_amount(&self) -> Decimal {
self.sell_amount - self.filled_amount
}
pub fn is_fully_filled(&self) -> bool {
self.filled_amount >= self.sell_amount
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Solver {
pub id: Uuid,
pub name: String,
pub total_solved: u64,
pub total_failed: u64,
pub avg_execution_time: Decimal,
pub reputation_score: Decimal,
pub total_rewards: Decimal,
pub is_active: bool,
pub registered_at: DateTime<Utc>,
pub last_active_at: DateTime<Utc>,
}
impl Solver {
pub fn new(name: String) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
name,
total_solved: 0,
total_failed: 0,
avg_execution_time: Decimal::ZERO,
reputation_score: Decimal::from(50), total_rewards: Decimal::ZERO,
is_active: true,
registered_at: now,
last_active_at: now,
}
}
pub fn success_rate(&self) -> Decimal {
let total = self.total_solved + self.total_failed;
if total == 0 {
return Decimal::ZERO;
}
Decimal::from(self.total_solved) / Decimal::from(total) * Decimal::from(100)
}
pub fn record_success(&mut self, execution_time: Decimal, reward: Decimal) {
self.total_solved += 1;
let total_time = self.avg_execution_time * Decimal::from(self.total_solved - 1);
self.avg_execution_time = (total_time + execution_time) / Decimal::from(self.total_solved);
self.reputation_score = (self.reputation_score + Decimal::ONE).min(Decimal::from(100));
self.total_rewards += reward;
self.last_active_at = Utc::now();
}
pub fn record_failure(&mut self) {
self.total_failed += 1;
self.reputation_score = (self.reputation_score - Decimal::from(2)).max(Decimal::ZERO);
self.last_active_at = Utc::now();
}
pub fn is_eligible(&self) -> bool {
self.is_active && self.reputation_score >= Decimal::from(20)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Solution {
pub id: Uuid,
pub intent_id: Uuid,
pub solver_id: Uuid,
pub execution_path: Vec<ExecutionStep>,
pub expected_output: Decimal,
pub estimated_gas: Decimal,
pub quality_score: Decimal,
pub proposed_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionStep {
pub venue: String,
pub input_token: Uuid,
pub input_amount: Decimal,
pub output_token: Uuid,
pub expected_output: Decimal,
pub step_index: u32,
}
impl Solution {
pub fn new(
intent_id: Uuid,
solver_id: Uuid,
execution_path: Vec<ExecutionStep>,
expected_output: Decimal,
estimated_gas: Decimal,
) -> Self {
let quality_score = expected_output - (estimated_gas * Decimal::from(100));
Self {
id: Uuid::new_v4(),
intent_id,
solver_id,
execution_path,
expected_output,
estimated_gas,
quality_score,
proposed_at: Utc::now(),
}
}
pub fn verify(&self, intent: &Intent) -> Result<bool> {
if self.expected_output < intent.min_buy_amount {
return Ok(false);
}
if self.execution_path.is_empty() {
return Ok(false);
}
for i in 0..self.execution_path.len() - 1 {
if self.execution_path[i].output_token != self.execution_path[i + 1].input_token {
return Ok(false);
}
}
if let Some(first_step) = self.execution_path.first() {
if first_step.input_token != intent.sell_token_id {
return Ok(false);
}
}
if let Some(last_step) = self.execution_path.last() {
if last_step.output_token != intent.buy_token_id {
return Ok(false);
}
}
Ok(true)
}
}
pub struct IntentMatcher {
pending_intents: Vec<Intent>,
solvers: HashMap<Uuid, Solver>,
solutions: HashMap<Uuid, Vec<Solution>>,
}
impl IntentMatcher {
pub fn new() -> Self {
Self {
pending_intents: Vec::new(),
solvers: HashMap::new(),
solutions: HashMap::new(),
}
}
pub fn submit_intent(&mut self, intent: Intent) -> Result<Uuid> {
if !intent.verify_signature()? {
return Err(CoreError::Validation(
"Invalid intent signature".to_string(),
));
}
if intent.is_expired() {
return Err(CoreError::Validation(
"Intent is already expired".to_string(),
));
}
let intent_id = intent.id;
self.pending_intents.push(intent);
Ok(intent_id)
}
pub fn register_solver(&mut self, solver: Solver) -> Result<Uuid> {
let solver_id = solver.id;
self.solvers.insert(solver_id, solver);
Ok(solver_id)
}
pub fn submit_solution(&mut self, solution: Solution) -> Result<Uuid> {
let intent = self
.pending_intents
.iter()
.find(|i| i.id == solution.intent_id)
.ok_or_else(|| CoreError::NotFound("Intent not found".to_string()))?
.clone();
if !solution.verify(&intent)? {
return Err(CoreError::Validation("Invalid solution".to_string()));
}
let solver = self
.solvers
.get(&solution.solver_id)
.ok_or_else(|| CoreError::NotFound("Solver not found".to_string()))?;
if !solver.is_eligible() {
return Err(CoreError::Validation("Solver is not eligible".to_string()));
}
let solution_id = solution.id;
self.solutions
.entry(solution.intent_id)
.or_default()
.push(solution);
Ok(solution_id)
}
pub fn select_best_solution(&self, intent_id: Uuid) -> Option<&Solution> {
self.solutions.get(&intent_id).and_then(|solutions| {
solutions
.iter()
.max_by(|a, b| a.quality_score.cmp(&b.quality_score))
})
}
pub fn execute_intent(&mut self, intent_id: Uuid) -> Result<Decimal> {
let intent_idx = self
.pending_intents
.iter()
.position(|i| i.id == intent_id)
.ok_or_else(|| CoreError::NotFound("Intent not found".to_string()))?;
let mut intent = self.pending_intents.remove(intent_idx);
let solution = self
.select_best_solution(intent_id)
.ok_or_else(|| CoreError::NotFound("No solutions found".to_string()))?
.clone();
intent.mark_executing(solution.solver_id)?;
let output_amount = solution.expected_output;
intent.mark_fulfilled(output_amount)?;
if let Some(solver) = self.solvers.get_mut(&solution.solver_id) {
let execution_time = Decimal::from(5); let reward = solution.estimated_gas * Decimal::from(2); solver.record_success(execution_time, reward);
}
self.solutions.remove(&intent_id);
Ok(output_amount)
}
pub fn cancel_intent(&mut self, intent_id: Uuid, user_id: Uuid) -> Result<()> {
let intent = self
.pending_intents
.iter_mut()
.find(|i| i.id == intent_id)
.ok_or_else(|| CoreError::NotFound("Intent not found".to_string()))?;
if intent.user_id != user_id {
return Err(CoreError::Unauthorized);
}
intent.cancel()?;
Ok(())
}
pub fn cleanup_expired(&mut self) -> usize {
let initial_count = self.pending_intents.len();
self.pending_intents.retain(|intent| !intent.is_expired());
let expired_ids: Vec<Uuid> = self
.pending_intents
.iter()
.filter(|i| i.is_expired())
.map(|i| i.id)
.collect();
for intent_id in expired_ids {
self.solutions.remove(&intent_id);
}
initial_count - self.pending_intents.len()
}
pub fn pending_count(&self) -> usize {
self.pending_intents.len()
}
pub fn get_solver(&self, solver_id: Uuid) -> Option<&Solver> {
self.solvers.get(&solver_id)
}
pub fn active_solvers(&self) -> Vec<&Solver> {
self.solvers
.values()
.filter(|s| s.is_active && s.is_eligible())
.collect()
}
}
impl Default for IntentMatcher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
use rust_decimal_macros::dec;
#[test]
fn test_intent_creation() {
let intent = Intent::new(
Uuid::new_v4(),
IntentType::Swap,
Uuid::new_v4(),
dec!(100),
Uuid::new_v4(),
dec!(95),
dec!(5),
Utc::now() + Duration::hours(1),
"signature".to_string(),
);
assert_eq!(intent.status, IntentStatus::Pending);
assert_eq!(intent.filled_amount, Decimal::ZERO);
assert!(!intent.is_expired());
}
#[test]
fn test_intent_cancel() {
let mut intent = Intent::new(
Uuid::new_v4(),
IntentType::Swap,
Uuid::new_v4(),
dec!(100),
Uuid::new_v4(),
dec!(95),
dec!(5),
Utc::now() + Duration::hours(1),
"signature".to_string(),
);
assert!(intent.cancel().is_ok());
assert_eq!(intent.status, IntentStatus::Cancelled);
assert!(intent.cancel().is_err());
}
#[test]
fn test_solver_reputation() {
let mut solver = Solver::new("TestSolver".to_string());
assert_eq!(solver.reputation_score, dec!(50));
assert!(solver.is_eligible());
solver.record_success(dec!(5), dec!(10));
assert_eq!(solver.total_solved, 1);
assert_eq!(solver.reputation_score, dec!(51));
solver.record_failure();
assert_eq!(solver.total_failed, 1);
assert_eq!(solver.reputation_score, dec!(49));
}
#[test]
fn test_solution_verification() {
let sell_token = Uuid::new_v4();
let buy_token = Uuid::new_v4();
let intent = Intent::new(
Uuid::new_v4(),
IntentType::Swap,
sell_token,
dec!(100),
buy_token,
dec!(95),
dec!(5),
Utc::now() + Duration::hours(1),
"signature".to_string(),
);
let execution_path = vec![ExecutionStep {
venue: "UniswapV3".to_string(),
input_token: sell_token,
input_amount: dec!(100),
output_token: buy_token,
expected_output: dec!(98),
step_index: 0,
}];
let solution = Solution::new(
intent.id,
Uuid::new_v4(),
execution_path,
dec!(98),
dec!(0.1),
);
assert!(solution.verify(&intent).unwrap());
}
#[test]
fn test_intent_matcher() {
let mut matcher = IntentMatcher::new();
let solver = Solver::new("TestSolver".to_string());
let solver_id = solver.id;
matcher.register_solver(solver).unwrap();
let sell_token = Uuid::new_v4();
let buy_token = Uuid::new_v4();
let intent = Intent::new(
Uuid::new_v4(),
IntentType::Swap,
sell_token,
dec!(100),
buy_token,
dec!(95),
dec!(5),
Utc::now() + Duration::hours(1),
"signature".to_string(),
);
let intent_id = intent.id;
matcher.submit_intent(intent).unwrap();
assert_eq!(matcher.pending_count(), 1);
let execution_path = vec![ExecutionStep {
venue: "UniswapV3".to_string(),
input_token: sell_token,
input_amount: dec!(100),
output_token: buy_token,
expected_output: dec!(98),
step_index: 0,
}];
let solution = Solution::new(intent_id, solver_id, execution_path, dec!(98), dec!(0.1));
matcher.submit_solution(solution).unwrap();
let output = matcher.execute_intent(intent_id).unwrap();
assert_eq!(output, dec!(98));
assert_eq!(matcher.pending_count(), 0);
}
#[test]
fn test_cleanup_expired() {
let mut matcher = IntentMatcher::new();
let intent = Intent::new(
Uuid::new_v4(),
IntentType::Swap,
Uuid::new_v4(),
dec!(100),
Uuid::new_v4(),
dec!(95),
dec!(5),
Utc::now() - Duration::hours(1), "signature".to_string(),
);
matcher.pending_intents.push(intent);
let cleaned = matcher.cleanup_expired();
assert_eq!(cleaned, 1);
assert_eq!(matcher.pending_count(), 0);
}
}