Skip to content

Commit 3c24a85

Browse files
authored
Solution verification (#18)
* added json logging for auctions and solutions * fixed json logging for auctions and solutions * split into auctions and solutions file * added liquidity json logging * added competition info fetching from CoW API * added liquidity-enhanced solutions logging * added solution verification through quoting Balancer V2 Vault and Balancer V3 Batch Router * solution verification now based on enhanced solutions * fixed version detection * removed duplicate code * contract calldata logging & issues remediation * fixed rate provider propagation * fixed rate propagation * added fixes to balancer solver * adjusted rate handling * attempt to fix rate provider handling * adjusted v3 poolId usage * attempt to fix v3 quoting * added debugging for v3 batch router call * attempt to fix static call
1 parent 0af1ae7 commit 3c24a85

File tree

97 files changed

+1536
-126
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+1536
-126
lines changed

crates/balancer-solver/config/example.baseline.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ native-token-price-estimation-amount = "100000000000000000"
1313
# driver-url = "http://localhost:8080"
1414
# timeout-ms = 5000
1515
# protocols = ["balancer_v2", "uniswap_v2"]
16+
17+
# Optional: Directory to save auction and solution JSON files for debugging
18+
# auction-save-directory = "/tmp/balancer-auctions"

crates/balancer-solver/config/example.legacy.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ chain-id = "1"
22
solver-name = "CoW Solver"
33
endpoint = "https://solver.cow.fi"
44
gzip-requests = false
5+
6+
# Optional: Directory to save auction and solution JSON files for debugging
7+
# auction-save-directory = "/tmp/balancer-auctions"

crates/balancer-solver/openapi.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,6 @@ components:
518518
- kind
519519
- tokens
520520
- fee
521-
- balancer_pool_id
522521
properties:
523522
kind:
524523
type: string
@@ -557,7 +556,6 @@ components:
557556
- tokens
558557
- amplificationParameter
559558
- fee
560-
- balancer_pool_id
561559
properties:
562560
kind:
563561
type: string

crates/balancer-solver/src/api/routes/solve/dto/auction.rs

Lines changed: 166 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,23 @@ fn extract_token_pairs_from_auction(
7979
/// Converts a data transfer object into its domain object representation.
8080
/// If liquidity_client is provided and auction has empty liquidity, fetches
8181
/// independently.
82+
/// Returns the auction and optionally the fetched liquidity response.
8283
pub async fn into_domain(
8384
auction: Auction,
8485
liquidity_client: Option<&LiquidityClient>,
8586
base_tokens: Option<&[eth::H160]>,
8687
protocols: Option<&[String]>,
87-
) -> Result<auction::Auction, Error> {
88-
Ok(auction::Auction {
88+
save_directory: Option<&std::path::Path>,
89+
) -> Result<
90+
(
91+
auction::Auction,
92+
Option<crate::infra::liquidity_client::LiquidityResponse>,
93+
),
94+
Error,
95+
> {
96+
let mut fetched_liquidity_response = None;
97+
98+
let auction_domain = auction::Auction {
8999
id: match auction.id {
90100
Some(id) => auction::Id::Solve(id),
91101
None => auction::Id::Quote,
@@ -188,12 +198,29 @@ pub async fn into_domain(
188198
"Successfully fetched liquidity from API"
189199
);
190200

201+
// Save liquidity to JSON if save_directory is provided
202+
if let Some(save_dir) = save_directory {
203+
let liquidity_json = serde_json::to_value(&response).ok();
204+
let save_dir = save_dir.to_path_buf();
205+
let auction_id = auction.id;
206+
tokio::spawn(async move {
207+
if let Some(liquidity) = liquidity_json {
208+
save_liquidity_json(liquidity, auction_id, &save_dir).await;
209+
}
210+
});
211+
}
212+
191213
// Process the fetched liquidity
192-
response
214+
let domain_liquidity = response
193215
.liquidity
194216
.iter()
195217
.map(|liquidity| convert_dto_liquidity_to_domain(liquidity))
196-
.try_collect()?
218+
.try_collect()?;
219+
220+
// Store the response for enhanced solutions
221+
fetched_liquidity_response = Some(response);
222+
223+
domain_liquidity
197224
}
198225
Err(e) => {
199226
tracing::warn!(
@@ -215,7 +242,9 @@ pub async fn into_domain(
215242
},
216243
gas_price: auction::GasPrice(eth::Ether(auction.effective_gas_price)),
217244
deadline: auction::Deadline(auction.deadline),
218-
})
245+
};
246+
247+
Ok((auction_domain, fetched_liquidity_response))
219248
}
220249

221250
/// Helper function to convert DTO liquidity to domain liquidity
@@ -238,6 +267,73 @@ fn convert_dto_liquidity_to_domain(liquidity: &Liquidity) -> Result<liquidity::L
238267
}
239268
}
240269

270+
/// Saves fetched liquidity data to a JSON file in the configured directory.
271+
/// This function runs in a background task and logs errors without failing the
272+
/// request.
273+
async fn save_liquidity_json(
274+
liquidity: serde_json::Value,
275+
auction_id: Option<i64>,
276+
save_dir: &std::path::Path,
277+
) {
278+
use tokio::fs;
279+
280+
// Determine filename based on auction ID
281+
let base_filename = match auction_id {
282+
Some(id) => id.to_string(),
283+
None => {
284+
// Use timestamp for quote auctions
285+
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S_%3f");
286+
format!("quote_{}", timestamp)
287+
}
288+
};
289+
290+
let liquidity_file_path = save_dir.join(format!("{}_liquidity.json", base_filename));
291+
292+
// Create directory if it doesn't exist
293+
if let Err(err) = fs::create_dir_all(save_dir).await {
294+
tracing::warn!(
295+
?err,
296+
directory = ?save_dir,
297+
"Failed to create liquidity save directory"
298+
);
299+
return;
300+
}
301+
302+
// Serialize liquidity to pretty JSON
303+
let liquidity_json = match serde_json::to_string_pretty(&liquidity) {
304+
Ok(content) => content,
305+
Err(err) => {
306+
tracing::warn!(?err, "Failed to serialize liquidity to JSON");
307+
return;
308+
}
309+
};
310+
311+
let liquidity_count = liquidity
312+
.get("liquidity")
313+
.and_then(|l| l.as_array())
314+
.map(|a| a.len())
315+
.unwrap_or(0);
316+
317+
// Write liquidity file
318+
match fs::write(&liquidity_file_path, liquidity_json).await {
319+
Ok(_) => {
320+
tracing::info!(
321+
liquidity_file = ?liquidity_file_path,
322+
auction_id = ?auction_id,
323+
liquidity_count,
324+
"💾 Saved fetched liquidity to JSON file"
325+
);
326+
}
327+
Err(err) => {
328+
tracing::warn!(
329+
?err,
330+
file_path = ?liquidity_file_path,
331+
"Failed to write liquidity JSON file"
332+
);
333+
}
334+
}
335+
}
336+
241337
mod erc4626 {
242338
use super::*;
243339
pub fn to_domain(edge: &Erc4626Edge) -> Result<liquidity::Liquidity, Error> {
@@ -301,6 +397,7 @@ mod weighted_product_pool {
301397
scale: conv::decimal_to_rational(&token.scaling_factor)
302398
.and_then(liquidity::ScalingFactor::new)
303399
.ok_or("invalid token scaling factor")?,
400+
rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?,
304401
})
305402
})
306403
.collect::<Result<Vec<_>, Error>>()?;
@@ -340,6 +437,7 @@ mod stable_pool {
340437
scale: conv::decimal_to_rational(&token.scaling_factor)
341438
.and_then(liquidity::ScalingFactor::new)
342439
.ok_or("invalid token scaling factor")?,
440+
rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?,
343441
})
344442
})
345443
.collect::<Result<Vec<_>, Error>>()?;
@@ -439,6 +537,7 @@ mod gyro_e_pool {
439537
scale: conv::decimal_to_rational(&token.scaling_factor)
440538
.and_then(liquidity::ScalingFactor::new)
441539
.ok_or("invalid token scaling factor")?,
540+
rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?,
442541
})
443542
})
444543
.collect::<Result<Vec<_>, Error>>()?;
@@ -502,6 +601,7 @@ mod gyro_2clp_pool {
502601
scale: conv::decimal_to_rational(&token.scaling_factor)
503602
.and_then(liquidity::ScalingFactor::new)
504603
.ok_or("invalid token scaling factor")?,
604+
rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?,
505605
})
506606
})
507607
.collect::<Result<Vec<_>, Error>>()?;
@@ -547,6 +647,7 @@ mod gyro_3clp_pool {
547647
.ok_or("invalid scaling factor")?,
548648
)
549649
.ok_or("invalid scaling factor")?,
650+
rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?,
550651
})
551652
})
552653
.collect::<Result<Vec<_>, Error>>()?;
@@ -585,6 +686,7 @@ mod reclamm_pool {
585686
scale: conv::decimal_to_rational(&token.scaling_factor)
586687
.and_then(liquidity::ScalingFactor::new)
587688
.ok_or("invalid token scaling factor")?,
689+
rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?,
588690
})
589691
})
590692
.collect::<Result<Vec<_>, Error>>()?;
@@ -644,6 +746,7 @@ mod stable_surge_pool {
644746
scale: conv::decimal_to_rational(&token.scaling_factor)
645747
.and_then(liquidity::ScalingFactor::new)
646748
.ok_or("invalid token scaling factor")?,
749+
rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?,
647750
})
648751
})
649752
.collect::<Result<Vec<_>, Error>>()?;
@@ -685,6 +788,7 @@ mod quant_amm_pool {
685788
scale: conv::decimal_to_rational(&token.scaling_factor)
686789
.and_then(liquidity::ScalingFactor::new)
687790
.ok_or("invalid token scaling factor")?,
791+
rate: conv::decimal_to_rational(&token.rate).ok_or("invalid token rate")?,
688792
})
689793
})
690794
.collect::<Result<Vec<_>, Error>>()?;
@@ -725,3 +829,60 @@ mod quant_amm_pool {
725829
})
726830
}
727831
}
832+
833+
/// Creates an enhanced solutions JSON with full liquidity details embedded
834+
pub fn create_enhanced_solutions(
835+
solutions: &solvers_dto::solution::Solutions,
836+
liquidity_response: &crate::infra::liquidity_client::LiquidityResponse,
837+
) -> serde_json::Value {
838+
// Convert to JSON value
839+
let mut solutions_json = serde_json::to_value(solutions).unwrap();
840+
841+
// Build a map of liquidity ID -> full liquidity details
842+
let mut liquidity_map: std::collections::HashMap<String, &solvers_dto::auction::Liquidity> =
843+
std::collections::HashMap::new();
844+
845+
for liq in &liquidity_response.liquidity {
846+
let id = extract_liquidity_id(liq);
847+
liquidity_map.insert(id, liq);
848+
}
849+
850+
// Enhance each solution's interactions
851+
if let Some(solutions_array) = solutions_json["solutions"].as_array_mut() {
852+
for solution in solutions_array {
853+
if let Some(interactions) = solution["interactions"].as_array_mut() {
854+
for interaction in interactions {
855+
if interaction["kind"] == "liquidity" {
856+
if let Some(id) = interaction["id"].as_str() {
857+
if let Some(liquidity_details) = liquidity_map.get(id) {
858+
// Embed full liquidity details
859+
interaction["liquidityDetails"] =
860+
serde_json::to_value(liquidity_details).unwrap();
861+
}
862+
}
863+
}
864+
}
865+
}
866+
}
867+
}
868+
869+
solutions_json
870+
}
871+
872+
fn extract_liquidity_id(liq: &solvers_dto::auction::Liquidity) -> String {
873+
// Extract ID from each liquidity variant
874+
match liq {
875+
solvers_dto::auction::Liquidity::ConstantProduct(p) => p.id.clone(),
876+
solvers_dto::auction::Liquidity::WeightedProduct(p) => p.id.clone(),
877+
solvers_dto::auction::Liquidity::Stable(p) => p.id.clone(),
878+
solvers_dto::auction::Liquidity::ConcentratedLiquidity(p) => p.id.clone(),
879+
solvers_dto::auction::Liquidity::GyroE(p) => p.id.clone(),
880+
solvers_dto::auction::Liquidity::Gyro2CLP(p) => p.id.clone(),
881+
solvers_dto::auction::Liquidity::Gyro3CLP(p) => p.id.clone(),
882+
solvers_dto::auction::Liquidity::LimitOrder(p) => p.id.clone(),
883+
solvers_dto::auction::Liquidity::Erc4626(p) => p.id.clone(),
884+
solvers_dto::auction::Liquidity::ReClamm(p) => p.id.clone(),
885+
solvers_dto::auction::Liquidity::QuantAmm(p) => p.id.clone(),
886+
solvers_dto::auction::Liquidity::StableSurge(p) => p.id.clone(),
887+
}
888+
}

0 commit comments

Comments
 (0)