Skip to main content

silent_payments_send/
error.rs

1//! Error types for Silent Payments sending operations.
2//!
3//! [`SendError`] covers all failure modes in the send flow:
4//! SIGHASH_ALL enforcement, input eligibility, cryptographic operations,
5//! and edge cases like keys summing to infinity.
6
7use silent_payments_core::error::{CryptoError, InputError};
8use thiserror::Error;
9
10/// Errors from constructing or executing a Silent Payment send operation.
11#[derive(Debug, Error)]
12pub enum SendError {
13    /// BIP 352 requires all inputs use SIGHASH_ALL. The transaction contained
14    /// an input with a different sighash type.
15    #[error("BIP 352 requires SIGHASH_ALL, got {actual}")]
16    SighashNotAll { actual: String },
17
18    /// Cannot create a Silent Payment with zero inputs.
19    #[error("cannot create SP payment with zero inputs")]
20    NoOutpoints,
21
22    /// No eligible inputs found after classification (SEC-03: explicit, never filtered).
23    #[error(transparent)]
24    NoEligibleInputs(#[from] InputError),
25
26    /// The sum of eligible input public keys equals the point at infinity.
27    /// This means the keys cancel each other out (e.g., BIP 352 test case 25).
28    #[error("eligible input public keys sum to the point at infinity")]
29    InputKeysSumToInfinity,
30
31    /// A cryptographic operation failed.
32    #[error(transparent)]
33    Crypto(#[from] CryptoError),
34}
35
36impl From<bdk_sp::send::error::SpSendError> for SendError {
37    fn from(err: bdk_sp::send::error::SpSendError) -> Self {
38        SendError::Crypto(CryptoError::Secp256k1(err.to_string()))
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn sighash_not_all_displays_meaningful_message() {
48        let err = SendError::SighashNotAll {
49            actual: "SIGHASH_NONE".to_string(),
50        };
51        let msg = err.to_string();
52        assert!(
53            msg.contains("SIGHASH_ALL"),
54            "should mention SIGHASH_ALL: {msg}"
55        );
56        assert!(
57            msg.contains("SIGHASH_NONE"),
58            "should mention actual type: {msg}"
59        );
60    }
61
62    #[test]
63    fn no_outpoints_displays_message() {
64        let err = SendError::NoOutpoints;
65        let msg = err.to_string();
66        assert!(
67            msg.contains("zero inputs"),
68            "should mention zero inputs: {msg}"
69        );
70    }
71
72    #[test]
73    fn no_eligible_inputs_wraps_input_error() {
74        let err = SendError::from(InputError::NoEligibleInputs);
75        let msg = err.to_string();
76        assert!(
77            msg.contains("no eligible"),
78            "should mention no eligible: {msg}"
79        );
80    }
81
82    #[test]
83    fn input_keys_sum_to_infinity_displays() {
84        let err = SendError::InputKeysSumToInfinity;
85        let msg = err.to_string();
86        assert!(
87            msg.contains("point at infinity"),
88            "should mention infinity: {msg}"
89        );
90    }
91
92    #[test]
93    fn crypto_error_wraps_transparently() {
94        let err = SendError::from(CryptoError::PointAtInfinity);
95        let msg = err.to_string();
96        assert!(
97            msg.contains("point at infinity"),
98            "should propagate message: {msg}"
99        );
100    }
101
102    #[test]
103    fn bdk_sp_error_converts_to_send_error() {
104        let bdk_err = bdk_sp::send::error::SpSendError::MissingInputsForSharedSecretDerivation;
105        let send_err = SendError::from(bdk_err);
106        assert!(
107            matches!(send_err, SendError::Crypto(_)),
108            "should map to Crypto variant"
109        );
110    }
111}