Skip to content

Commit 52e00ea

Browse files
evanlinjinclaude
andcommitted
refactor!: rename value-only predicates to is_funded/is_fundable
`Target` now carries two constraints of opposite monotonicity — the value target (a lower bound, monotone) and `max_weight` (an upper bound, anti-monotone). Rename the value-only predicates so their names say which one they check: - `is_target_met` -> `is_funded` - `is_target_met_with_drain` -> `is_funded_with_drain` - `is_selection_possible` -> `is_fundable` "funded"/"fundable" is money-centric and so naturally excludes weight; the `-ed`/`-able` pair distinguishes "this selection covers the value" from "these candidates *can* cover it". Pure rename, no behaviour change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fbfa8f0 commit 52e00ea

6 files changed

Lines changed: 30 additions & 30 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ let candidates = vec![
5757
let mut coin_selector = CoinSelector::new(&candidates);
5858
coin_selector.select(0);
5959

60-
assert!(!coin_selector.is_target_met(target), "we didn't select enough");
60+
assert!(!coin_selector.is_funded(target), "we didn't select enough");
6161
println!("we didn't select enough yet we're missing: {}", coin_selector.missing(target));
6262
coin_selector.select(1);
63-
assert!(coin_selector.is_target_met(target), "we should have enough now");
63+
assert!(coin_selector.is_funded(target), "we should have enough now");
6464

6565
// Now we need to know if we need a change output to drain the excess if we overshot too much
6666
//

src/coin_selector.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ impl<'a> CoinSelector<'a> {
112112
}
113113

114114
/// Whether the candidates can cover this `target`'s **value** (net of input fees) — i.e. whether
115-
/// enough value is reachable for [`is_target_met`] to hold. Respects [`ban`]ned candidates.
115+
/// enough value is reachable for [`is_funded`] to hold. Respects [`ban`]ned candidates.
116116
///
117117
/// Selecting *all* effective inputs maximizes the value available, so if that can't meet the
118118
/// target value, nothing can. Monotone, hence exact.
@@ -122,12 +122,12 @@ impl<'a> CoinSelector<'a> {
122122
/// which enforce the cap) to actually build a selection.
123123
///
124124
/// [`ban`]: Self::ban
125-
/// [`is_target_met`]: Self::is_target_met
125+
/// [`is_funded`]: Self::is_funded
126126
/// [`select_until_target_met`]: Self::select_until_target_met
127-
pub fn is_selection_possible(&self, target: Target) -> bool {
127+
pub fn is_fundable(&self, target: Target) -> bool {
128128
let mut test = self.clone();
129129
test.select_all_effective(target.fee.rate);
130-
test.is_target_met(target)
130+
test.is_funded(target)
131131
}
132132

133133
/// Returns true if no candidates have been selected.
@@ -439,7 +439,7 @@ impl<'a> CoinSelector<'a> {
439439
///
440440
/// Always `true` when `max_weight` is `None`. Note this is the *anti-monotone* half of
441441
/// feasibility (adding inputs adds weight), so it is kept separate from the monotone
442-
/// value-only [`is_target_met`](Self::is_target_met).
442+
/// value-only [`is_funded`](Self::is_funded).
443443
pub fn is_within_max_weight(&self, target: Target, drain: Drain) -> bool {
444444
match target.max_weight {
445445
Some(max_weight) => self.weight(target.outputs, drain.weights) <= max_weight,
@@ -452,21 +452,21 @@ impl<'a> CoinSelector<'a> {
452452
///
453453
/// This is **monotone**: selecting more never un-meets it. It deliberately does *not* include
454454
/// the weight cap — see [`is_within_max_weight`](Self::is_within_max_weight).
455-
pub fn is_target_met_with_drain(&self, target: Target, drain: Drain) -> bool {
455+
pub fn is_funded_with_drain(&self, target: Target, drain: Drain) -> bool {
456456
self.excess(target, drain) >= 0
457457
}
458458

459459
/// Whether the selection covers the target **value** (net of input fees), i.e. [`excess`] is
460460
/// non-negative. **Monotone** (selecting more never un-meets it), and it deliberately does
461461
/// *not* check [`Target::max_weight`] — that is the separate, anti-monotone
462-
/// [`is_within_max_weight`]. See [`is_target_met_with_drain`] for the version that
462+
/// [`is_within_max_weight`]. See [`is_funded_with_drain`] for the version that
463463
/// accounts for a specific `drain`.
464464
///
465465
/// [`excess`]: Self::excess
466466
/// [`is_within_max_weight`]: Self::is_within_max_weight
467-
/// [`is_target_met_with_drain`]: Self::is_target_met_with_drain
468-
pub fn is_target_met(&self, target: Target) -> bool {
469-
self.is_target_met_with_drain(target, Drain::NONE)
467+
/// [`is_funded_with_drain`]: Self::is_funded_with_drain
468+
pub fn is_funded(&self, target: Target) -> bool {
469+
self.is_funded_with_drain(target, Drain::NONE)
470470
}
471471

472472
/// Select all unselected candidates
@@ -492,8 +492,8 @@ impl<'a> CoinSelector<'a> {
492492
);
493493
if excess > change_policy.min_value as i64 {
494494
debug_assert_eq!(
495-
self.is_target_met(target),
496-
self.is_target_met_with_drain(
495+
self.is_funded(target),
496+
self.is_funded_with_drain(
497497
target,
498498
Drain {
499499
weights: change_policy.drain_weights,
@@ -512,12 +512,12 @@ impl<'a> CoinSelector<'a> {
512512
/// `change_policy`. If it should not, then it will return [`Drain::NONE`]. The value of the
513513
/// `Drain` will be the same as [`drain_value`].
514514
///
515-
/// If [`is_target_met`] returns true for this selection then [`is_target_met_with_drain`] will
515+
/// If [`is_funded`] returns true for this selection then [`is_funded_with_drain`] will
516516
/// also be true if you pass in the drain returned from this method.
517517
///
518518
/// [`drain_value`]: Self::drain_value
519-
/// [`is_target_met_with_drain`]: Self::is_target_met_with_drain
520-
/// [`is_target_met`]: Self::is_target_met
519+
/// [`is_funded_with_drain`]: Self::is_funded_with_drain
520+
/// [`is_funded`]: Self::is_funded
521521
#[must_use]
522522
pub fn drain(&self, target: Target, change_policy: ChangePolicy) -> Drain {
523523
match self.drain_value(target, change_policy) {
@@ -554,7 +554,7 @@ impl<'a> CoinSelector<'a> {
554554
/// [`Target::max_weight`]. Note this only reflects *this* in-order greedy selection; a
555555
/// different selection might still fit the cap (use branch and bound to search for one).
556556
pub fn select_until_target_met(&mut self, target: Target) -> Result<(), SelectError> {
557-
self.select_until(|cs| cs.is_target_met(target))
557+
self.select_until(|cs| cs.is_funded(target))
558558
.ok_or_else(|| {
559559
SelectError::InsufficientFunds(InsufficientFunds {
560560
missing: self.excess(target, Drain::NONE).unsigned_abs(),

src/metrics/lowest_fee.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ impl LowestFee {
7373
/// inadmissible; [`score`](BnbMetric::score) reuses the returned drain for its cap check so the
7474
/// drain is only decided once.
7575
fn fee_score(&self, cs: &CoinSelector<'_>, target: Target) -> Option<(Ordf32, Drain)> {
76-
if !cs.is_target_met(target) {
76+
if !cs.is_funded(target) {
7777
return None;
7878
}
7979
let drain = self
@@ -125,7 +125,7 @@ impl BnbMetric for LowestFee {
125125
return None;
126126
}
127127

128-
if cs.is_target_met(target) {
128+
if cs.is_funded(target) {
129129
let current_score = self.fee_score(cs, target).unwrap().0;
130130

131131
// `current_score` is already a valid lower bound for a selection that has change: a
@@ -172,7 +172,7 @@ impl BnbMetric for LowestFee {
172172
let (mut cs, resize_index, to_resize) = cs
173173
.clone()
174174
.select_iter()
175-
.find(|(cs, _, _)| cs.is_target_met(target))?;
175+
.find(|(cs, _, _)| cs.is_funded(target))?;
176176

177177
// If this selection is already perfect, return its score directly.
178178
if cs.excess(target, Drain::NONE) == 0 {
@@ -243,7 +243,7 @@ impl BnbMetric for LowestFee {
243243
}
244244
}
245245

246-
// `scale` could be 0 even if `is_target_met` is `false` due to the latter being based on
246+
// `scale` could be 0 even if `is_funded` is `false` due to the latter being based on
247247
// rounded-up vbytes.
248248
let ideal_fee = scale.0 * to_resize.value as f32 + cs.selected_value() as f32
249249
- target.value() as f32;

tests/bnb.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ proptest! {
162162

163163
match solutions.enumerate().filter_map(|(i, sol)| Some((i, sol?))).last() {
164164
Some((_i, (sol, _score))) => assert!(sol.selected_value() >= target_value),
165-
_ => prop_assert!(!cs.is_selection_possible(target)),
165+
_ => prop_assert!(!cs.is_fundable(target)),
166166
}
167167
}
168168

tests/common.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ where
190190
cs,
191191
parent_has_change,
192192
lb_score,
193-
cs.is_target_met(target),
193+
cs.is_funded(target),
194194
descendant_cs,
195195
descendant_has_change,
196196
descendant_score,
@@ -385,12 +385,12 @@ where
385385
/// current selection) meet `target`, i.e. cover the value **and** stay within `max_weight`?
386386
///
387387
/// Enumerates every subset via [`ExhaustiveIter`] and reuses the real
388-
/// [`CoinSelector::is_target_met`] + [`CoinSelector::is_within_max_weight`], so it inherits the
388+
/// [`CoinSelector::is_funded`] + [`CoinSelector::is_within_max_weight`], so it inherits the
389389
/// exact weight model and is independent of the BnB weight prune it audits. Exponential — small `n`
390390
/// only.
391391
pub fn exact_selection_possible(cs: &CoinSelector, target: Target) -> bool {
392392
let feasible =
393-
|s: &CoinSelector| s.is_target_met(target) && s.is_within_max_weight(target, Drain::NONE);
393+
|s: &CoinSelector| s.is_funded(target) && s.is_within_max_weight(target, Drain::NONE);
394394
// the current selection itself (no additions) is a valid subset and isn't yielded by the iter
395395
feasible(cs)
396396
|| ExhaustiveIter::new(cs)
@@ -523,7 +523,7 @@ pub fn compare_against_benchmarks<M: BnbMetric + Clone>(
523523
}
524524
}
525525
None => {
526-
// Full feasibility (value *and* max_weight) is needed here; `is_selection_possible`
526+
// Full feasibility (value *and* max_weight) is needed here; `is_fundable`
527527
// only covers value, so use the exact exhaustive oracle to assert impossibility.
528528
prop_assert!(!exact_selection_possible(&cs, target));
529529
}
@@ -545,7 +545,7 @@ fn randomly_satisfy_target<'a, R: rand::Rng>(
545545
let mut last_score: Option<Ordf32> = None;
546546
while let Some(next) = cs.unselected_indices().choose(rng) {
547547
cs.select(next);
548-
if cs.is_target_met(target) {
548+
if cs.is_funded(target) {
549549
let curr_score = metric.score(&cs, target);
550550
if let Some(last_score) = last_score {
551551
if curr_score.is_none() || curr_score.unwrap() > last_score {

tests/lowest_fee.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ proptest! {
7272
drain_dust in 100..=1000_u64, // drain dust (sats)
7373
n_drain_outputs in 1usize..150, // the number of drain outputs
7474
// No `max_weight` here: `n` is too large for the exhaustive oracle, and this test's
75-
// impossibility check relies on the (value-only) `is_selection_possible`, so a weight cap
75+
// impossibility check relies on the (value-only) `is_fundable`, so a weight cap
7676
// (which BnB could fail on while value is reachable) would break it. Cap handling is
7777
// covered by `bnb_respects_max_weight` and `can_eventually_find_best_solution`.
7878
) {
@@ -94,7 +94,7 @@ proptest! {
9494
let mut cs = CoinSelector::new(&candidates);
9595

9696
let metric = params.lowest_fee_metric();
97-
let is_impossible = !cs.is_selection_possible(params.target());
97+
let is_impossible = !cs.is_fundable(params.target());
9898
match common::bnb_search(&mut cs, params.target(), metric, params.n_candidates * 10) {
9999
Ok((score, rounds)) => {
100100
// the +1 is because the iterator will always try selecting nothing as a solution so we have

0 commit comments

Comments
 (0)