use simulator_api::{AgentParams, AgentStatsReport};
use solana_address::Address;
use spl_associated_token_account_interface::program::id as ata_program_id;
use spl_token_2022_interface::inline_spl_token::id as token_program_id;
use tracing::warn;
pub async fn report_agent_pnl(
rpc_url: &str,
agents_params: &[AgentParams],
agent_stats: Option<&[AgentStatsReport]>,
) {
if rpc_url.is_empty() {
return;
}
let http = reqwest::Client::new();
println!();
println!("=== Agent PnL Summary ===");
for (i, params) in agents_params.iter().enumerate() {
let Some(ref wallet_str) = params.wallet else {
continue;
};
let Ok(wallet) = wallet_str.parse::<Address>() else {
continue;
};
let seed_lamports = params.seed_sol_lamports.unwrap_or(1_000_000_000);
let final_sol = rpc_get_balance(&http, rpc_url, &wallet).await;
let sol_pnl = final_sol as i64 - seed_lamports as i64;
println!("Agent {i} ({wallet_str}):");
println!(
" SOL PnL: {:+.9} ({:+} lamports)",
sol_pnl as f64 / 1e9,
sol_pnl
);
for (mint_str, seeded_amount) in ¶ms.seed_token_accounts {
let Ok(mint) = mint_str.parse::<Address>() else {
continue;
};
let (ata, _) = Address::find_program_address(
&[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()],
&ata_program_id(),
);
let final_balance = rpc_get_token_balance(&http, rpc_url, &ata).await;
let pnl = final_balance as i64 - *seeded_amount as i64;
let short_mint = &mint_str[..8.min(mint_str.len())];
println!(" {short_mint}.. PnL: {pnl:+} base units");
}
if let Some(stats) = agent_stats.and_then(|s| s.get(i)) {
println!(
" Opportunities: {} found, {} skipped, {} no routes",
stats.opportunities_found, stats.opportunities_skipped, stats.no_routes,
);
println!(" Txs produced: {}", stats.txs_produced);
let total_txs = stats.txs_submitted + stats.txs_failed;
if total_txs > 0 {
let tx_success_rate = stats.txs_submitted as f64 / total_txs as f64 * 100.0;
println!(
" Tx execution: {} submitted, {} failed ({:.1}% success rate)",
stats.txs_submitted, stats.txs_failed, tx_success_rate,
);
}
if stats.txs_simulation_rejected > 0 || stats.txs_simulation_failed > 0 {
println!(
" Preflight: {} rejected (unprofitable), {} sim errors",
stats.txs_simulation_rejected, stats.txs_simulation_failed,
);
}
if !stats.expected_gain_by_mint.is_empty() {
println!(" Expected PnL (from quotes):");
for (mint, gain) in &stats.expected_gain_by_mint {
let short_mint = &mint[..8.min(mint.len())];
println!(" {short_mint}..: {gain:+} base units");
}
}
}
}
println!("=========================");
}
async fn rpc_get_balance(http: &reqwest::Client, rpc_url: &str, address: &Address) -> u64 {
let Ok(resp) = http
.post(rpc_url)
.json(&serde_json::json!({
"jsonrpc": "2.0", "id": 1,
"method": "getBalance",
"params": [address.to_string()]
}))
.send()
.await
else {
warn!("getBalance request failed for {address}");
return 0;
};
let Ok(json) = resp.json::<serde_json::Value>().await else {
warn!("getBalance response not JSON for {address}");
return 0;
};
json["result"]["value"].as_u64().unwrap_or(0)
}
async fn rpc_get_token_balance(http: &reqwest::Client, rpc_url: &str, ata: &Address) -> u64 {
let Ok(resp) = http
.post(rpc_url)
.json(&serde_json::json!({
"jsonrpc": "2.0", "id": 1,
"method": "getTokenAccountBalance",
"params": [ata.to_string()]
}))
.send()
.await
else {
warn!("getTokenAccountBalance request failed for {ata}");
return 0;
};
let Ok(json) = resp.json::<serde_json::Value>().await else {
warn!("getTokenAccountBalance response not JSON for {ata}");
return 0;
};
json["result"]["value"]["amount"]
.as_str()
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}