Skip to content

Commit bca0c99

Browse files
committed
Add account lot collect subcommand
1 parent bd6ac2f commit bca0c99

File tree

4 files changed

+104
-24
lines changed

4 files changed

+104
-24
lines changed

src/bin/sys-lend.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
656656
.takes_value(true)
657657
.global(true)
658658
.validator(is_url)
659-
.help("Helium JSON RPC URL to use only for the proprietary getPriorityFeeEstimate RPC method"),
659+
.help("Helius JSON RPC URL to use only for the proprietary getPriorityFeeEstimate RPC method"),
660660
)
661661
.arg(
662662
Arg::with_name("priority_fee_exact")

src/db.rs

+25-20
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ impl LotAcquistion {
273273
}
274274
}
275275

276-
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, EnumString, IntoStaticStr)]
276+
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, EnumString, IntoStaticStr)]
277277
pub enum LotSelectionMethod {
278278
#[strum(serialize = "fifo")]
279279
FirstInFirstOut,
@@ -285,6 +285,17 @@ pub enum LotSelectionMethod {
285285
HighestBasis,
286286
}
287287

288+
impl LotSelectionMethod {
289+
pub fn cmp_lots(&self, a: &Lot, b: &Lot) -> std::cmp::Ordering {
290+
match self {
291+
LotSelectionMethod::FirstInFirstOut => a.acquisition.when.cmp(&b.acquisition.when),
292+
LotSelectionMethod::LastInFirstOut => b.acquisition.when.cmp(&a.acquisition.when),
293+
LotSelectionMethod::LowestBasis => a.acquisition.price().cmp(&b.acquisition.price()),
294+
LotSelectionMethod::HighestBasis => b.acquisition.price().cmp(&a.acquisition.price()),
295+
}
296+
}
297+
}
298+
288299
pub const POSSIBLE_LOT_SELECTION_METHOD_VALUES: &[&str] =
289300
&["fifo", "lifo", "lowest-basis", "highest-basis"];
290301

@@ -331,6 +342,18 @@ impl Lot {
331342
}
332343
}
333344

345+
pub fn sort_lots_by_selection_method(
346+
lots: &mut Vec<Lot>,
347+
lot_selection_method: LotSelectionMethod,
348+
) {
349+
lots.sort_by(|a, b| lot_selection_method.cmp_lots(a, b));
350+
if lot_selection_method == LotSelectionMethod::FirstInFirstOut && !lots.is_empty() {
351+
// Assume the oldest lot is the rent-reserve. Extract it as the last resort
352+
let first_lot = lots.remove(0);
353+
lots.push(first_lot);
354+
}
355+
}
356+
334357
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
335358
pub enum LotDisposalKind {
336359
Usd {
@@ -455,25 +478,7 @@ fn split_lots(
455478
let mut extracted_lots = vec![];
456479
let mut remaining_lots = vec![];
457480

458-
match lot_selection_method {
459-
LotSelectionMethod::FirstInFirstOut => {
460-
lots.sort_by(|a, b| a.acquisition.when.cmp(&b.acquisition.when));
461-
if !lots.is_empty() {
462-
// Assume the oldest lot is the rent-reserve. Extract it as the last resort
463-
let first_lot = lots.remove(0);
464-
lots.push(first_lot);
465-
}
466-
}
467-
LotSelectionMethod::LastInFirstOut => {
468-
lots.sort_by(|a, b| b.acquisition.when.cmp(&a.acquisition.when))
469-
}
470-
LotSelectionMethod::LowestBasis => {
471-
lots.sort_by(|a, b| a.acquisition.price().cmp(&b.acquisition.price()))
472-
}
473-
LotSelectionMethod::HighestBasis => {
474-
lots.sort_by(|a, b| b.acquisition.price().cmp(&a.acquisition.price()))
475-
}
476-
}
481+
sort_lots_by_selection_method(&mut lots, lot_selection_method);
477482

478483
let mut amount_remaining = amount;
479484
for mut lot in lots {

src/main.rs

+74-3
Original file line numberDiff line numberDiff line change
@@ -4415,7 +4415,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
44154415
.takes_value(true)
44164416
.global(true)
44174417
.validator(is_url)
4418-
.help("Helium JSON RPC URL to use only for the proprietary getPriorityFeeEstimate RPC method"),
4418+
.help("Helius JSON RPC URL to use only for the proprietary getPriorityFeeEstimate RPC method"),
44194419
)
44204420
.arg(
44214421
Arg::with_name("verbose")
@@ -5129,7 +5129,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
51295129
.setting(AppSettings::InferSubcommands)
51305130
.subcommand(
51315131
SubCommand::with_name("swap")
5132-
.about("Swap lots in the local database only")
5132+
.about("Swap lots")
51335133
.arg(
51345134
Arg::with_name("lot_number1")
51355135
.value_name("LOT NUMBER")
@@ -5147,6 +5147,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
51475147
.help("Second lot number"),
51485148
)
51495149
)
5150+
.subcommand(
5151+
SubCommand::with_name("collect")
5152+
.about("Collect non-disposed lots of a desired type into an address")
5153+
.arg(
5154+
Arg::with_name("token")
5155+
.value_name("SOL or SPL Token")
5156+
.takes_value(true)
5157+
.required(true)
5158+
.validator(is_valid_token_or_sol)
5159+
.help("Token type"),
5160+
)
5161+
.arg(
5162+
Arg::with_name("address")
5163+
.value_name("ADDRESS")
5164+
.takes_value(true)
5165+
.required(true)
5166+
.validator(is_valid_pubkey)
5167+
.help("Account address"),
5168+
)
5169+
.arg(lot_selection_arg())
5170+
)
51505171
.subcommand(
51515172
SubCommand::with_name("delete")
51525173
.about("Delete a lot from the local database only. \
@@ -5169,7 +5190,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
51695190
)
51705191
.subcommand(
51715192
SubCommand::with_name("move")
5172-
.about("Move a lot to a new addresses in the local database only. \
5193+
.about("Move a lot to a new address. \
51735194
Useful if the on-chain state is out of sync with the database")
51745195
.arg(
51755196
Arg::with_name("lot_number")
@@ -6145,6 +6166,56 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
61456166
println!("Swapping lots {lot_number1} and {lot_number2}");
61466167
db.swap_lots(lot_number1, lot_number2)?;
61476168
}
6169+
("collect", Some(arg_matches)) => {
6170+
let address = pubkey_of(arg_matches, "address").unwrap();
6171+
let token = MaybeToken::from(value_t!(arg_matches, "token", Token).ok());
6172+
let lot_selection_method =
6173+
value_t_or_exit!(arg_matches, "lot_selection", LotSelectionMethod);
6174+
6175+
println!(
6176+
"Collecting {lot_selection_method:?} lots for {address} ({})",
6177+
token.name()
6178+
);
6179+
loop {
6180+
let mut current_lots = vec![];
6181+
let mut candidate_lots = vec![];
6182+
for account in db.get_accounts() {
6183+
if (account.token == token)
6184+
|| (token.is_sol_or_wsol() && account.token.is_sol_or_wsol())
6185+
{
6186+
if account.address == address && account.token == token {
6187+
assert!(current_lots.is_empty());
6188+
current_lots = account.lots;
6189+
} else {
6190+
candidate_lots.extend(account.lots);
6191+
}
6192+
}
6193+
}
6194+
6195+
sort_lots_by_selection_method(&mut current_lots, lot_selection_method);
6196+
sort_lots_by_selection_method(&mut candidate_lots, lot_selection_method);
6197+
6198+
while !current_lots.is_empty() && !candidate_lots.is_empty() {
6199+
if lot_selection_method.cmp_lots(&current_lots[0], &candidate_lots[0])
6200+
== std::cmp::Ordering::Greater
6201+
{
6202+
break;
6203+
}
6204+
current_lots.remove(0);
6205+
}
6206+
6207+
if current_lots.is_empty() || candidate_lots.is_empty() {
6208+
println!("Done");
6209+
break;
6210+
}
6211+
6212+
println!(
6213+
"Swapping lots {} and {}",
6214+
current_lots[0].lot_number, candidate_lots[0].lot_number
6215+
);
6216+
db.swap_lots(current_lots[0].lot_number, candidate_lots[0].lot_number)?;
6217+
}
6218+
}
61486219
("move", Some(arg_matches)) => {
61496220
let lot_number = value_t_or_exit!(arg_matches, "lot_number", usize);
61506221
let to_address =

src/token.rs

+4
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ impl MaybeToken {
322322
!self.is_token()
323323
}
324324

325+
pub fn is_sol_or_wsol(&self) -> bool {
326+
self.is_sol() || self.token() == Some(Token::wSOL)
327+
}
328+
325329
pub fn ui_amount(&self, amount: u64) -> f64 {
326330
match self.0 {
327331
None => lamports_to_sol(amount),

0 commit comments

Comments
 (0)