Skip to content

Commit 756b13a

Browse files
authored
feat: cli improvements (#167)
**Problem** - When running `create-merkle-tree-collection` or `create-meta-merkle-tree`, the save_path is passed into read_stake_meta_collection and read_merkle_tree_collection, respectively, with a full path. This causes the read functions to look for a file that doesn't exist, as the expected filename gets concatenated onto the save_path. - As operators, we would like improved visibility into distribution account stats for a given epoch **Solution** - Pass in an unmodified save_path to read functions - Add get-tip-distribution-stats for printing a table of distribution account stats for an epoch
1 parent e2d5497 commit 756b13a

File tree

4 files changed

+261
-12
lines changed

4 files changed

+261
-12
lines changed

tip-router-operator-cli/src/cli.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,14 @@ pub enum Commands {
265265
#[arg(long, env, default_value = "true")]
266266
save: bool,
267267
},
268+
GetTipDistributionStats {
269+
#[arg(long, env)]
270+
tip_distribution_program_id: Pubkey,
271+
272+
#[arg(long, env)]
273+
priority_fee_distribution_program_id: Pubkey,
274+
275+
#[arg(long, env)]
276+
epoch: Option<u64>,
277+
},
268278
}

tip-router-operator-cli/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod reclaim;
1515
pub mod rpc_utils;
1616
pub mod solana_cli;
1717
pub mod submit;
18+
pub mod tip_distribution_stats;
1819
pub mod tx_utils;
1920

2021
use std::fs;

tip-router-operator-cli/src/main.rs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ use ::{
1414
cli::{Cli, Commands, SnapshotPaths},
1515
create_merkle_tree_collection, create_meta_merkle_tree, create_stake_meta,
1616
ledger_utils::get_bank_from_snapshot_at_slot,
17-
load_bank_from_snapshot, merkle_tree_collection_file_name, meta_merkle_tree_path,
18-
process_epoch, read_merkle_tree_collection, read_stake_meta_collection, reclaim,
19-
stake_meta_file_name,
17+
load_bank_from_snapshot, meta_merkle_tree_path, process_epoch, read_merkle_tree_collection,
18+
read_stake_meta_collection, reclaim,
2019
submit::{submit_recent_epochs_to_ncn, submit_to_ncn},
20+
tip_distribution_stats::get_tip_distribution_stats,
2121
tip_router::get_ncn_config,
2222
Version,
2323
},
@@ -511,10 +511,7 @@ async fn main() -> Result<()> {
511511
save,
512512
} => {
513513
// Load the stake_meta_collection from disk
514-
let stake_meta_collection = read_stake_meta_collection(
515-
epoch,
516-
&cli.get_save_path().join(stake_meta_file_name(epoch)),
517-
);
514+
let stake_meta_collection = read_stake_meta_collection(epoch, &cli.get_save_path());
518515
let config = get_ncn_config(&rpc_client, &tip_router_program_id, &ncn_address).await?;
519516
// Tip Router looks backwards in time (typically current_epoch - 1) to calculated
520517
// distributions. Meanwhile the NCN's Ballot is for the current_epoch. So we
@@ -539,11 +536,7 @@ async fn main() -> Result<()> {
539536
}
540537
Commands::CreateMetaMerkleTree { epoch, save } => {
541538
// Load the stake_meta_collection from disk
542-
let merkle_tree_collection = read_merkle_tree_collection(
543-
epoch,
544-
&cli.get_save_path()
545-
.join(merkle_tree_collection_file_name(epoch)),
546-
);
539+
let merkle_tree_collection = read_merkle_tree_collection(epoch, &cli.get_save_path());
547540

548541
create_meta_merkle_tree(
549542
cli.operator_address,
@@ -554,6 +547,28 @@ async fn main() -> Result<()> {
554547
&cli.cluster,
555548
);
556549
}
550+
Commands::GetTipDistributionStats {
551+
tip_distribution_program_id,
552+
priority_fee_distribution_program_id,
553+
epoch,
554+
} => {
555+
let stats_epoch = if let Some(epoch) = epoch {
556+
epoch
557+
} else {
558+
rpc_client.get_epoch_info().await?.epoch
559+
};
560+
info!(
561+
"Getting tip distribution stats for epoch {}...",
562+
stats_epoch
563+
);
564+
get_tip_distribution_stats(
565+
&rpc_client,
566+
&tip_distribution_program_id,
567+
&priority_fee_distribution_program_id,
568+
stats_epoch,
569+
)
570+
.await?;
571+
}
557572
}
558573
Ok(())
559574
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
use anchor_lang::AccountDeserialize;
2+
use anyhow::Result;
3+
use jito_priority_fee_distribution_sdk::PriorityFeeDistributionAccount;
4+
use jito_tip_distribution_sdk::TipDistributionAccount;
5+
use log::info;
6+
use solana_client::{
7+
nonblocking::rpc_client::RpcClient,
8+
rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
9+
};
10+
use solana_sdk::pubkey::Pubkey;
11+
12+
pub struct TipDistributionStats {
13+
pub account_pubkey: Pubkey,
14+
pub validator_vote_account: Pubkey,
15+
pub total_lamports: u64,
16+
pub is_priority_fee: bool,
17+
pub validator_commission_bps: u16,
18+
}
19+
20+
pub async fn get_tip_distribution_stats(
21+
rpc_client: &RpcClient,
22+
tip_distribution_program_id: &Pubkey,
23+
priority_fee_distribution_program_id: &Pubkey,
24+
epoch: u64,
25+
) -> Result<()> {
26+
info!("Fetching tip distribution accounts for epoch {}...", epoch);
27+
28+
let rpc_client_with_timeout =
29+
RpcClient::new_with_timeout(rpc_client.url(), std::time::Duration::from_secs(1800));
30+
31+
let tip_distribution_accounts = get_tip_distribution_accounts_for_epoch(
32+
&rpc_client_with_timeout,
33+
tip_distribution_program_id,
34+
epoch,
35+
)
36+
.await?;
37+
38+
let priority_fee_distribution_accounts = get_priority_fee_distribution_accounts_for_epoch(
39+
&rpc_client_with_timeout,
40+
priority_fee_distribution_program_id,
41+
epoch,
42+
)
43+
.await?;
44+
45+
info!(
46+
"Found {} tip distribution accounts and {} priority fee distribution accounts",
47+
tip_distribution_accounts.len(),
48+
priority_fee_distribution_accounts.len()
49+
);
50+
51+
let mut all_stats = Vec::new();
52+
53+
for (pubkey, account) in tip_distribution_accounts {
54+
let stats =
55+
process_tip_distribution_account(&rpc_client_with_timeout, &pubkey, &account, false)
56+
.await?;
57+
all_stats.push(stats);
58+
}
59+
60+
for (pubkey, account) in priority_fee_distribution_accounts {
61+
let stats = process_priority_fee_distribution_account(
62+
&rpc_client_with_timeout,
63+
&pubkey,
64+
&account,
65+
true,
66+
)
67+
.await?;
68+
all_stats.push(stats);
69+
}
70+
71+
print_tip_distribution_summary(&all_stats, epoch);
72+
73+
Ok(())
74+
}
75+
76+
async fn get_tip_distribution_accounts_for_epoch(
77+
rpc_client: &RpcClient,
78+
tip_distribution_program_id: &Pubkey,
79+
epoch: u64,
80+
) -> Result<Vec<(Pubkey, TipDistributionAccount)>> {
81+
let accounts = rpc_client
82+
.get_program_accounts_with_config(
83+
tip_distribution_program_id,
84+
RpcProgramAccountsConfig {
85+
filters: None,
86+
account_config: RpcAccountInfoConfig {
87+
encoding: Some(solana_account_decoder::UiAccountEncoding::Base64),
88+
..RpcAccountInfoConfig::default()
89+
},
90+
..RpcProgramAccountsConfig::default()
91+
},
92+
)
93+
.await?;
94+
95+
let mut result = Vec::new();
96+
for (pubkey, account) in accounts {
97+
if let Ok(tip_distribution_account) =
98+
TipDistributionAccount::try_deserialize(&mut account.data.as_slice())
99+
{
100+
if tip_distribution_account.epoch_created_at == epoch {
101+
result.push((pubkey, tip_distribution_account));
102+
}
103+
}
104+
}
105+
106+
Ok(result)
107+
}
108+
109+
async fn get_priority_fee_distribution_accounts_for_epoch(
110+
rpc_client: &RpcClient,
111+
priority_fee_distribution_program_id: &Pubkey,
112+
epoch: u64,
113+
) -> Result<Vec<(Pubkey, PriorityFeeDistributionAccount)>> {
114+
let accounts = rpc_client
115+
.get_program_accounts_with_config(
116+
priority_fee_distribution_program_id,
117+
RpcProgramAccountsConfig {
118+
filters: None,
119+
account_config: RpcAccountInfoConfig {
120+
encoding: Some(solana_account_decoder::UiAccountEncoding::Base64),
121+
..RpcAccountInfoConfig::default()
122+
},
123+
..RpcProgramAccountsConfig::default()
124+
},
125+
)
126+
.await?;
127+
128+
let mut result = Vec::new();
129+
for (pubkey, account) in accounts {
130+
if let Ok(priority_fee_distribution_account) =
131+
PriorityFeeDistributionAccount::try_deserialize(&mut account.data.as_slice())
132+
{
133+
if priority_fee_distribution_account.epoch_created_at == epoch {
134+
result.push((pubkey, priority_fee_distribution_account));
135+
}
136+
}
137+
}
138+
139+
Ok(result)
140+
}
141+
142+
async fn process_tip_distribution_account(
143+
rpc_client: &RpcClient,
144+
account_pubkey: &Pubkey,
145+
tip_distribution_account: &TipDistributionAccount,
146+
is_priority_fee: bool,
147+
) -> Result<TipDistributionStats> {
148+
// Get the account data to calculate total lamports
149+
let account_info = rpc_client.get_account(account_pubkey).await?;
150+
let rent_exempt_amount = rpc_client
151+
.get_minimum_balance_for_rent_exemption(account_info.data.len())
152+
.await?;
153+
let total_lamports = account_info.lamports.saturating_sub(rent_exempt_amount);
154+
155+
Ok(TipDistributionStats {
156+
account_pubkey: *account_pubkey,
157+
validator_vote_account: tip_distribution_account.validator_vote_account,
158+
total_lamports,
159+
is_priority_fee,
160+
validator_commission_bps: tip_distribution_account.validator_commission_bps,
161+
})
162+
}
163+
164+
async fn process_priority_fee_distribution_account(
165+
rpc_client: &RpcClient,
166+
account_pubkey: &Pubkey,
167+
priority_fee_distribution_account: &PriorityFeeDistributionAccount,
168+
is_priority_fee: bool,
169+
) -> Result<TipDistributionStats> {
170+
// Get the account data to calculate total lamports
171+
let account_info = rpc_client.get_account(account_pubkey).await?;
172+
let rent_exempt_amount = rpc_client
173+
.get_minimum_balance_for_rent_exemption(account_info.data.len())
174+
.await?;
175+
let total_lamports = account_info.lamports.saturating_sub(rent_exempt_amount);
176+
177+
Ok(TipDistributionStats {
178+
account_pubkey: *account_pubkey,
179+
validator_vote_account: priority_fee_distribution_account.validator_vote_account,
180+
total_lamports,
181+
is_priority_fee,
182+
validator_commission_bps: priority_fee_distribution_account.validator_commission_bps,
183+
})
184+
}
185+
186+
fn print_tip_distribution_summary(stats: &[TipDistributionStats], epoch: u64) {
187+
info!("\n=== Epoch {} Tip Distribution Statistics ===", epoch);
188+
info!(
189+
"{:<50} {:<15} {:<10} {:<10}",
190+
"Account", "Total (SOL)", "Type", "Commission"
191+
);
192+
info!("{:-<85}", "");
193+
194+
let mut total_total = 0u64;
195+
196+
for stat in stats {
197+
let total_sol = stat.total_lamports as f64 / 1_000_000_000.0;
198+
let account_type = if stat.is_priority_fee {
199+
"Priority"
200+
} else {
201+
"Tip"
202+
};
203+
let commission_pct = stat.validator_commission_bps as f64 / 100.0;
204+
205+
info!(
206+
"{:<50} {:<15.6} {:<10} {:<10.2}",
207+
format!("{}", stat.account_pubkey),
208+
total_sol,
209+
account_type,
210+
commission_pct
211+
);
212+
213+
total_total += stat.total_lamports;
214+
}
215+
216+
info!("{:-<85}", "");
217+
let total_total_sol = total_total as f64 / 1_000_000_000.0;
218+
219+
info!(
220+
"{:<50} {:<15.6} {:<10} {:<10}",
221+
"TOTAL", total_total_sol, "ALL", ""
222+
);
223+
}

0 commit comments

Comments
 (0)