Add kind accessors to receive candidate errors - #1717
Conversation
CoinSelectionError and InputContributionError are opaque because their variants may change, but that leaves callers of try_preserving_privacy and contribute_inputs unable to distinguish failures that could succeed with a different candidate set from failures no candidate change can fix, short of matching display strings. A wallet driving selection with a retry loop burns attempts on the second class. Add kind() returning a non_exhaustive kind enum on both errors: callers get a stable, matchable classification while the error types stay free to evolve, since new variants only require a new kind category and consumers must already handle unknown kinds. Mirror the accessors in payjoin-ffi with an explicit Other catch-all, giving bindings the same signal; today these errors cross the FFI as display-only objects. Related to payjoin#1422.
Coverage Report for CI Build 29009356795Coverage increased (+0.03%) to 85.893%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
DanGould
left a comment
There was a problem hiding this comment.
My request is the same as your #1654. Please demonstrate the need in the downstream code, or at least a toy integration, to prove that the shape of the PR is appropriate for your feature request.
Second, why re-create the InternalCoinSelectionError rather than promote those variants to live inside CoinSelectionError as a transparent pub enum that just becomes #[non_exhaustive]. I'm not sure I see a reason that this one needs to remain an Intenal error with opaque variants because they're all rust-payjoin defined, unlike others which are kept internal to encapsulate error variants produced by dependencies.
|
The current downstream solution to the missing info is to assume every contribution failure is candidate-specific, drop the coin and retry with the rest (PayjoinReceiverInputSelector.cs:111-115): catch (PayjoinReceiverInputContributionException ex)
{
contributionFailures.Add($"candidate '{selectedCoinOutPoint}' rejected: {ex.Message}");
candidates.Remove(selected); // assume the failure was candidate-specific, retry with the rest
}That's the right move for The exception reaches us as a Display string only (PayjoinReceiverInputProposalOperations.cs:49-52), so the only alternative to guessing is matching on the message text. Selection has the same problem one layer up (PayjoinReceiverInputSelector.cs:81-85): we break identically on all three variants, but |
spacebear21
left a comment
There was a problem hiding this comment.
Concept NACK, the premise this PR rests on seems flawed.:
try_preserving_privacycan only ever returnEmpty.avoid_uihis the sole producer ofNotFound/UnsupportedOutputLength, and its error is unconditionally swallowed by the.or_else(select_first_candidate)fallback, so a non-empty candidate list always yieldsOkand an empty one always yieldsEmpty. The BTCPay implemenation already guardscandidates.Count == 0before its loop, which means thatCoinSelectionExceptioncatch is dead code there too.contribute_inputscan only raiseValueTooLowif the caller contributed additional outputs withreplace_receiver_outputs, which BTCPay (or any live implementation AFAIK) does not do.
UnsupportedOutputLength and DuplicateInput are the variants I least want to expose and freeze in the public API. Both of those are implementation details functions whose internals could change (e.g. avoid_uih could conceivably support >2 outputs, or contribute_inputs could use some kind of BTreeMap to bypass the duplicate inputs issue entirely). #[non_exhaustive] doesn't save us here, it protects additions but not removals.
So for BTCPay's actual flow, every reachable failure collapses to a single variant per method:
- Selection: only Empty, which they already guard before the loop, so the catch is dead.
- Contribution: only DuplicateInput, since ValueTooLow needs an output-value increase they don't do. And DuplicateInput is always the candidate-specific, remove-and-retry case, which their loop already handles correctly.
|
@spacebear21 That makes sense; I'd assumed some errors could be fatal beyond the currently selected inputs. Looking at the method calls, I think you're right. I've verified select_first_candidate makes Empty the only observable selection error, ValueTooLow needs replace_receiver_outputs, which we don't call, and our CoinSelectionException catch is now probably dead code behind the empty guard. I'll fix the BTCPay side instead of the API, and probably close this. |
Related to #1422.
CoinSelectionErrorandInputContributionErrorare opaque because their variants may still change, but that leaves callers oftry_preserving_privacy/contribute_inputsunable to distinguish failures that could succeed with a different candidate set from failures no candidate change can fix, short of matching display strings.Field report: the BTCPay Server payjoin plugin drives contribution with a candidate-snapshot loop, snapshot spendable coins, let selection pick, on failure remove the offending candidate and retry with the reduced set until success or exhaustion. Because the errors are opaque, the loop has to treat every failure as maybe-retryable, and burns attempts on failures like
ValueTooLowwhere removing candidates can never help.This adds
kind()returning a#[non_exhaustive]kind enum on both errors: callers get a stable, matchable classification while the error types stay free to evolve, a new variant only needs a kind category, and consumers must already handle unknown kinds conservatively. The accessors are mirrored in payjoin-ffi with an explicitOthercatch-all; today these errors cross the FFI as display-only objects.Disclosure: co-authored by Claude Code
Pull Request Checklist
Please confirm the following before requesting review:
AI
in the body of this PR.