1use simulator_api::{AgentParams, AgentStatsReport};
2use solana_address::Address;
3use spl_associated_token_account_interface::program::id as ata_program_id;
4use spl_token_2022_interface::inline_spl_token::id as token_program_id;
5use tracing::warn;
6
7pub async fn report_agent_pnl(
11 rpc_url: &str,
12 agents_params: &[AgentParams],
13 agent_stats: Option<&[AgentStatsReport]>,
14) {
15 if rpc_url.is_empty() {
16 return;
17 }
18 let http = reqwest::Client::new();
19
20 println!();
21 println!("=== Agent PnL Summary ===");
22
23 for (i, params) in agents_params.iter().enumerate() {
24 let Some(ref wallet_str) = params.wallet else {
25 continue;
26 };
27 let Ok(wallet) = wallet_str.parse::<Address>() else {
28 continue;
29 };
30
31 let seed_lamports = params.seed_sol_lamports.unwrap_or(1_000_000_000);
32 let final_sol = rpc_get_balance(&http, rpc_url, &wallet).await;
33 let sol_pnl = final_sol as i64 - seed_lamports as i64;
34
35 println!("Agent {i} ({wallet_str}):");
36 println!(
37 " SOL PnL: {:+.9} ({:+} lamports)",
38 sol_pnl as f64 / 1e9,
39 sol_pnl
40 );
41
42 for (mint_str, seeded_amount) in ¶ms.seed_token_accounts {
43 let Ok(mint) = mint_str.parse::<Address>() else {
44 continue;
45 };
46 let (ata, _) = Address::find_program_address(
47 &[wallet.as_ref(), token_program_id().as_ref(), mint.as_ref()],
48 &ata_program_id(),
49 );
50 let final_balance = rpc_get_token_balance(&http, rpc_url, &ata).await;
51 let pnl = final_balance as i64 - *seeded_amount as i64;
52 let short_mint = &mint_str[..8.min(mint_str.len())];
53 println!(" {short_mint}.. PnL: {pnl:+} base units");
54 }
55
56 if let Some(stats) = agent_stats.and_then(|s| s.get(i)) {
57 println!(
58 " Opportunities: {} found, {} skipped, {} no routes",
59 stats.opportunities_found, stats.opportunities_skipped, stats.no_routes,
60 );
61 println!(" Txs produced: {}", stats.txs_produced);
62
63 let total_txs = stats.txs_submitted + stats.txs_failed;
64 if total_txs > 0 {
65 let tx_success_rate = stats.txs_submitted as f64 / total_txs as f64 * 100.0;
66 println!(
67 " Tx execution: {} submitted, {} failed ({:.1}% success rate)",
68 stats.txs_submitted, stats.txs_failed, tx_success_rate,
69 );
70 }
71
72 if stats.txs_simulation_rejected > 0 || stats.txs_simulation_failed > 0 {
73 println!(
74 " Preflight: {} rejected (unprofitable), {} sim errors",
75 stats.txs_simulation_rejected, stats.txs_simulation_failed,
76 );
77 }
78
79 if !stats.expected_gain_by_mint.is_empty() {
80 println!(" Expected PnL (from quotes):");
81 for (mint, gain) in &stats.expected_gain_by_mint {
82 let short_mint = &mint[..8.min(mint.len())];
83 println!(" {short_mint}..: {gain:+} base units");
84 }
85 }
86 }
87 }
88
89 println!("=========================");
90}
91
92async fn rpc_get_balance(http: &reqwest::Client, rpc_url: &str, address: &Address) -> u64 {
93 let Ok(resp) = http
94 .post(rpc_url)
95 .json(&serde_json::json!({
96 "jsonrpc": "2.0", "id": 1,
97 "method": "getBalance",
98 "params": [address.to_string()]
99 }))
100 .send()
101 .await
102 else {
103 warn!("getBalance request failed for {address}");
104 return 0;
105 };
106 let Ok(json) = resp.json::<serde_json::Value>().await else {
107 warn!("getBalance response not JSON for {address}");
108 return 0;
109 };
110 json["result"]["value"].as_u64().unwrap_or(0)
111}
112
113async fn rpc_get_token_balance(http: &reqwest::Client, rpc_url: &str, ata: &Address) -> u64 {
114 let Ok(resp) = http
115 .post(rpc_url)
116 .json(&serde_json::json!({
117 "jsonrpc": "2.0", "id": 1,
118 "method": "getTokenAccountBalance",
119 "params": [ata.to_string()]
120 }))
121 .send()
122 .await
123 else {
124 warn!("getTokenAccountBalance request failed for {ata}");
125 return 0;
126 };
127 let Ok(json) = resp.json::<serde_json::Value>().await else {
128 warn!("getTokenAccountBalance response not JSON for {ata}");
129 return 0;
130 };
131 json["result"]["value"]["amount"]
132 .as_str()
133 .and_then(|s| s.parse().ok())
134 .unwrap_or(0)
135}