From e449b2bb20bead93c9129ae4679df35c76bd213e Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Tue, 18 Jun 2024 10:24:52 -0700 Subject: [PATCH] Add account lot collect subcommand --- src/bin/sys-lend.rs | 2 +- src/db.rs | 47 +++++++++++++---------- src/main.rs | 94 +++++++++++++++++++++++++++++++++++++++++++-- src/token.rs | 4 ++ 4 files changed, 123 insertions(+), 24 deletions(-) diff --git a/src/bin/sys-lend.rs b/src/bin/sys-lend.rs index a3a937c..c09e1a8 100644 --- a/src/bin/sys-lend.rs +++ b/src/bin/sys-lend.rs @@ -656,7 +656,7 @@ async fn main() -> Result<(), Box> { .takes_value(true) .global(true) .validator(is_url) - .help("Helium JSON RPC URL to use only for the proprietary getPriorityFeeEstimate RPC method"), + .help("Helius JSON RPC URL to use only for the proprietary getPriorityFeeEstimate RPC method"), ) .arg( Arg::with_name("priority_fee_exact") diff --git a/src/db.rs b/src/db.rs index 08c8011..c7abc78 100644 --- a/src/db.rs +++ b/src/db.rs @@ -273,7 +273,7 @@ impl LotAcquistion { } } -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, EnumString, IntoStaticStr)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, EnumString, IntoStaticStr)] pub enum LotSelectionMethod { #[strum(serialize = "fifo")] FirstInFirstOut, @@ -331,6 +331,31 @@ impl Lot { } } +pub fn sort_lots_by_selection_method( + lots: &mut Vec, + lot_selection_method: LotSelectionMethod, +) { + match lot_selection_method { + LotSelectionMethod::FirstInFirstOut => { + lots.sort_by(|a, b| a.acquisition.when.cmp(&b.acquisition.when)); + if !lots.is_empty() { + // Assume the oldest lot is the rent-reserve. Extract it as the last resort + let first_lot = lots.remove(0); + lots.push(first_lot); + } + } + LotSelectionMethod::LastInFirstOut => { + lots.sort_by(|a, b| b.acquisition.when.cmp(&a.acquisition.when)) + } + LotSelectionMethod::LowestBasis => { + lots.sort_by(|a, b| a.acquisition.price().cmp(&b.acquisition.price())) + } + LotSelectionMethod::HighestBasis => { + lots.sort_by(|a, b| b.acquisition.price().cmp(&a.acquisition.price())) + } + } +} + #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub enum LotDisposalKind { Usd { @@ -455,25 +480,7 @@ fn split_lots( let mut extracted_lots = vec![]; let mut remaining_lots = vec![]; - match lot_selection_method { - LotSelectionMethod::FirstInFirstOut => { - lots.sort_by(|a, b| a.acquisition.when.cmp(&b.acquisition.when)); - if !lots.is_empty() { - // Assume the oldest lot is the rent-reserve. Extract it as the last resort - let first_lot = lots.remove(0); - lots.push(first_lot); - } - } - LotSelectionMethod::LastInFirstOut => { - lots.sort_by(|a, b| b.acquisition.when.cmp(&a.acquisition.when)) - } - LotSelectionMethod::LowestBasis => { - lots.sort_by(|a, b| a.acquisition.price().cmp(&b.acquisition.price())) - } - LotSelectionMethod::HighestBasis => { - lots.sort_by(|a, b| b.acquisition.price().cmp(&a.acquisition.price())) - } - } + sort_lots_by_selection_method(&mut lots, lot_selection_method); let mut amount_remaining = amount; for mut lot in lots { diff --git a/src/main.rs b/src/main.rs index 7847f8d..94a16be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4415,7 +4415,7 @@ async fn main() -> Result<(), Box> { .takes_value(true) .global(true) .validator(is_url) - .help("Helium JSON RPC URL to use only for the proprietary getPriorityFeeEstimate RPC method"), + .help("Helius JSON RPC URL to use only for the proprietary getPriorityFeeEstimate RPC method"), ) .arg( Arg::with_name("verbose") @@ -5129,7 +5129,7 @@ async fn main() -> Result<(), Box> { .setting(AppSettings::InferSubcommands) .subcommand( SubCommand::with_name("swap") - .about("Swap lots in the local database only") + .about("Swap lots") .arg( Arg::with_name("lot_number1") .value_name("LOT NUMBER") @@ -5147,6 +5147,27 @@ async fn main() -> Result<(), Box> { .help("Second lot number"), ) ) + .subcommand( + SubCommand::with_name("collect") + .about("Collect non-disposed lots of a desired type into an address") + .arg( + Arg::with_name("token") + .value_name("SOL or SPL Token") + .takes_value(true) + .required(true) + .validator(is_valid_token_or_sol) + .help("Token type"), + ) + .arg( + Arg::with_name("address") + .value_name("ADDRESS") + .takes_value(true) + .required(true) + .validator(is_valid_pubkey) + .help("Account address"), + ) + .arg(lot_selection_arg()) + ) .subcommand( SubCommand::with_name("delete") .about("Delete a lot from the local database only. \ @@ -5169,7 +5190,7 @@ async fn main() -> Result<(), Box> { ) .subcommand( SubCommand::with_name("move") - .about("Move a lot to a new addresses in the local database only. \ + .about("Move a lot to a new address. \ Useful if the on-chain state is out of sync with the database") .arg( Arg::with_name("lot_number") @@ -6145,6 +6166,73 @@ async fn main() -> Result<(), Box> { println!("Swapping lots {lot_number1} and {lot_number2}"); db.swap_lots(lot_number1, lot_number2)?; } + ("collect", Some(arg_matches)) => { + let address = pubkey_of(arg_matches, "address").unwrap(); + let token = MaybeToken::from(value_t!(arg_matches, "token", Token).ok()); + let lot_selection_method = + value_t_or_exit!(arg_matches, "lot_selection", LotSelectionMethod); + + println!( + "Collecting {lot_selection_method:?} lots for {address} ({})", + token.name() + ); + loop { + let mut current_lots = vec![]; + let mut candidate_lots = vec![]; + for account in db.get_accounts() { + if (account.token == token) + || (token.is_sol_or_wsol() && account.token.is_sol_or_wsol()) + { + if account.address == address && account.token == token { + assert!(current_lots.is_empty()); + current_lots = account.lots; + } else { + candidate_lots.extend(account.lots); + } + } + } + + sort_lots_by_selection_method(&mut current_lots, lot_selection_method); + + candidate_lots.extend(current_lots.clone()); + sort_lots_by_selection_method(&mut candidate_lots, lot_selection_method); + + while !current_lots.is_empty() { + match lot_selection_method { + LotSelectionMethod::LowestBasis + | LotSelectionMethod::HighestBasis => { + if current_lots[0].acquisition.price() + != candidate_lots[0].acquisition.price() + { + break; + } + } + LotSelectionMethod::FirstInFirstOut + | LotSelectionMethod::LastInFirstOut => { + if current_lots[0].acquisition.when + != candidate_lots[0].acquisition.when + { + break; + } + } + } + + current_lots.pop(); + candidate_lots.pop(); + } + + if current_lots.is_empty() { + println!("Done"); + break; + } + + println!( + "Swapping lots {} and {}", + current_lots[0].lot_number, candidate_lots[0].lot_number + ); + db.swap_lots(current_lots[0].lot_number, candidate_lots[0].lot_number)?; + } + } ("move", Some(arg_matches)) => { let lot_number = value_t_or_exit!(arg_matches, "lot_number", usize); let to_address = diff --git a/src/token.rs b/src/token.rs index d30e3e9..1fe701e 100644 --- a/src/token.rs +++ b/src/token.rs @@ -322,6 +322,10 @@ impl MaybeToken { !self.is_token() } + pub fn is_sol_or_wsol(&self) -> bool { + self.is_sol() || self.token() == Some(Token::wSOL) + } + pub fn ui_amount(&self, amount: u64) -> f64 { match self.0 { None => lamports_to_sol(amount),