Skip to content

Commit 455909e

Browse files
committed
Add custom error types for output and input contributions
Partially addresses payjoin#312
1 parent 2e65772 commit 455909e

File tree

6 files changed

+117
-32
lines changed

6 files changed

+117
-32
lines changed

payjoin-cli/src/app/v1.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,8 @@ impl App {
348348
.require_network(network)
349349
.map_err(|e| Error::Server(e.into()))?
350350
.script_pubkey(),
351-
)?
351+
)
352+
.map_err(|e| Error::Server(e.into()))?
352353
.commit_outputs();
353354

354355
let provisional_payjoin = try_contributing_inputs(payjoin.clone(), &bitcoind)
@@ -397,6 +398,7 @@ fn try_contributing_inputs(
397398

398399
Ok(payjoin
399400
.contribute_witness_inputs(vec![(selected_outpoint, txo_to_contribute)])
401+
.expect("This shouldn't happen. Failed to contribute inputs.")
400402
.commit_inputs())
401403
}
402404

payjoin-cli/src/app/v2.rs

+1
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ fn try_contributing_inputs(
377377

378378
Ok(payjoin
379379
.contribute_witness_inputs(vec![(selected_outpoint, txo_to_contribute)])
380+
.expect("This shouldn't happen. Failed to contribute inputs.")
380381
.commit_inputs())
381382
}
382383

payjoin/src/receive/error.rs

+71
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,51 @@ impl std::error::Error for RequestError {
196196
}
197197
}
198198

199+
/// Error that may occur when output substitution fails.
200+
///
201+
/// This is currently opaque type because we aren't sure which variants will stay.
202+
/// You can only display it.
203+
#[derive(Debug)]
204+
pub struct OutputSubstitutionError(InternalOutputSubstitutionError);
205+
206+
#[derive(Debug)]
207+
pub(crate) enum InternalOutputSubstitutionError {
208+
/// Output substitution is disabled
209+
OutputSubstitutionDisabled(&'static str),
210+
/// Current output substitution implementation doesn't support reducing the number of outputs
211+
NotEnoughOutputs,
212+
/// The provided drain script could not be identified in the provided replacement outputs
213+
InvalidDrainScript,
214+
}
215+
216+
impl fmt::Display for OutputSubstitutionError {
217+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
218+
match &self.0 {
219+
InternalOutputSubstitutionError::OutputSubstitutionDisabled(reason) => write!(f, "{}", &format!("Output substitution is disabled: {}", reason)),
220+
InternalOutputSubstitutionError::NotEnoughOutputs => write!(
221+
f,
222+
"Current output substitution implementation doesn't support reducing the number of outputs"
223+
),
224+
InternalOutputSubstitutionError::InvalidDrainScript =>
225+
write!(f, "The provided drain script could not be identified in the provided replacement outputs"),
226+
}
227+
}
228+
}
229+
230+
impl From<InternalOutputSubstitutionError> for OutputSubstitutionError {
231+
fn from(value: InternalOutputSubstitutionError) -> Self { OutputSubstitutionError(value) }
232+
}
233+
234+
impl std::error::Error for OutputSubstitutionError {
235+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
236+
match &self.0 {
237+
InternalOutputSubstitutionError::OutputSubstitutionDisabled(_) => None,
238+
InternalOutputSubstitutionError::NotEnoughOutputs => None,
239+
InternalOutputSubstitutionError::InvalidDrainScript => None,
240+
}
241+
}
242+
}
243+
199244
/// Error that may occur when coin selection fails.
200245
///
201246
/// This is currently opaque type because we aren't sure which variants will stay.
@@ -230,3 +275,29 @@ impl fmt::Display for SelectionError {
230275
impl From<InternalSelectionError> for SelectionError {
231276
fn from(value: InternalSelectionError) -> Self { SelectionError(value) }
232277
}
278+
279+
/// Error that may occur when input contribution fails.
280+
///
281+
/// This is currently opaque type because we aren't sure which variants will stay.
282+
/// You can only display it.
283+
#[derive(Debug)]
284+
pub struct InputContributionError(InternalInputContributionError);
285+
286+
#[derive(Debug)]
287+
pub(crate) enum InternalInputContributionError {
288+
/// Total input value is not enough to cover additional output value
289+
ValueTooLow,
290+
}
291+
292+
impl fmt::Display for InputContributionError {
293+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
294+
match &self.0 {
295+
InternalInputContributionError::ValueTooLow =>
296+
write!(f, "Total input value is not enough to cover additional output value"),
297+
}
298+
}
299+
}
300+
301+
impl From<InternalInputContributionError> for InputContributionError {
302+
fn from(value: InternalInputContributionError) -> Self { InputContributionError(value) }
303+
}

payjoin/src/receive/mod.rs

+28-24
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ mod optional_parameters;
3838
pub mod v2;
3939

4040
use bitcoin::secp256k1::rand::{self, Rng};
41-
pub use error::{Error, RequestError, SelectionError};
42-
use error::{InternalRequestError, InternalSelectionError};
41+
pub use error::{Error, OutputSubstitutionError, RequestError, SelectionError};
42+
use error::{
43+
InputContributionError, InternalInputContributionError, InternalOutputSubstitutionError,
44+
InternalRequestError, InternalSelectionError,
45+
};
4346
use optional_parameters::Params;
4447

4548
use crate::input_type::InputType;
@@ -359,7 +362,10 @@ impl WantsOutputs {
359362
}
360363

361364
/// Substitute the receiver output script with the provided script.
362-
pub fn substitute_receiver_script(self, output_script: &Script) -> Result<WantsOutputs, Error> {
365+
pub fn substitute_receiver_script(
366+
self,
367+
output_script: &Script,
368+
) -> Result<WantsOutputs, OutputSubstitutionError> {
363369
let output_value = self.original_psbt.unsigned_tx.output[self.change_vout].value;
364370
let outputs = vec![TxOut { value: output_value, script_pubkey: output_script.into() }];
365371
self.replace_receiver_outputs(outputs, output_script)
@@ -374,7 +380,7 @@ impl WantsOutputs {
374380
self,
375381
replacement_outputs: Vec<TxOut>,
376382
drain_script: &Script,
377-
) -> Result<WantsOutputs, Error> {
383+
) -> Result<WantsOutputs, OutputSubstitutionError> {
378384
let mut payjoin_psbt = self.original_psbt.clone();
379385
let mut change_vout: Option<usize> = None;
380386
if self.params.disable_output_substitution {
@@ -387,14 +393,16 @@ impl WantsOutputs {
387393
.find(|txo| txo.script_pubkey == original_output.script_pubkey)
388394
{
389395
Some(txo) if txo.value < original_output.value => {
390-
return Err(Error::Server(
391-
"Decreasing the receiver output value is not allowed".into(),
392-
));
396+
return Err(InternalOutputSubstitutionError::OutputSubstitutionDisabled(
397+
"Decreasing the receiver output value is not allowed",
398+
)
399+
.into());
393400
}
394401
None =>
395-
return Err(Error::Server(
396-
"Changing the receiver output script pubkey is not allowed".into(),
397-
)),
402+
return Err(InternalOutputSubstitutionError::OutputSubstitutionDisabled(
403+
"Changing the receiver output script pubkey is not allowed",
404+
)
405+
.into()),
398406
_ => log::info!("Receiver is augmenting outputs"),
399407
}
400408
}
@@ -406,7 +414,7 @@ impl WantsOutputs {
406414
if self.owned_vouts.contains(&i) {
407415
// Receiver output: substitute with a provided output picked randomly
408416
if replacement_outputs.is_empty() {
409-
return Err(Error::Server("Not enough outputs".into()));
417+
return Err(InternalOutputSubstitutionError::NotEnoughOutputs.into());
410418
}
411419
let index = rng.gen_range(0..replacement_outputs.len());
412420
let txo = replacement_outputs.swap_remove(index);
@@ -435,7 +443,7 @@ impl WantsOutputs {
435443
original_psbt: self.original_psbt,
436444
payjoin_psbt,
437445
params: self.params,
438-
change_vout: change_vout.ok_or(Error::Server("Invalid drain script".into()))?,
446+
change_vout: change_vout.ok_or(InternalOutputSubstitutionError::InvalidDrainScript)?,
439447
owned_vouts: self.owned_vouts,
440448
})
441449
}
@@ -475,13 +483,13 @@ impl WantsInputs {
475483
candidate_inputs: HashMap<Amount, OutPoint>,
476484
) -> Result<OutPoint, SelectionError> {
477485
if candidate_inputs.is_empty() {
478-
return Err(SelectionError::from(InternalSelectionError::Empty));
486+
return Err(InternalSelectionError::Empty.into());
479487
}
480488

481489
if self.payjoin_psbt.outputs.len() > 2 {
482490
// This UIH avoidance function supports only
483491
// many-input, n-output transactions such that n <= 2 for now
484-
return Err(SelectionError::from(InternalSelectionError::TooManyOutputs));
492+
return Err(InternalSelectionError::TooManyOutputs.into());
485493
}
486494

487495
if self.payjoin_psbt.outputs.len() == 2 {
@@ -531,26 +539,22 @@ impl WantsInputs {
531539
}
532540

533541
// No suitable privacy preserving selection found
534-
Err(SelectionError::from(InternalSelectionError::NotFound))
542+
Err(InternalSelectionError::NotFound.into())
535543
}
536544

537545
fn select_first_candidate(
538546
&self,
539547
candidate_inputs: HashMap<Amount, OutPoint>,
540548
) -> Result<OutPoint, SelectionError> {
541-
candidate_inputs
542-
.values()
543-
.next()
544-
.cloned()
545-
.ok_or(SelectionError::from(InternalSelectionError::NotFound))
549+
candidate_inputs.values().next().cloned().ok_or(InternalSelectionError::NotFound.into())
546550
}
547551

548552
/// Add the provided list of inputs to the transaction.
549553
/// Any excess input amount is added to the change_vout output indicated previously.
550554
pub fn contribute_witness_inputs(
551555
self,
552556
inputs: impl IntoIterator<Item = (OutPoint, TxOut)>,
553-
) -> WantsInputs {
557+
) -> Result<WantsInputs, InputContributionError> {
554558
let mut payjoin_psbt = self.payjoin_psbt.clone();
555559
// The payjoin proposal must not introduce mixed input sequence numbers
556560
let original_sequence = self
@@ -587,15 +591,15 @@ impl WantsInputs {
587591
let change_amount = receiver_input_amount - receiver_min_input_amount;
588592
payjoin_psbt.unsigned_tx.output[self.change_vout].value += change_amount;
589593
} else {
590-
todo!("Input amount is not enough to cover additional output value");
594+
return Err(InternalInputContributionError::ValueTooLow.into());
591595
}
592596

593-
WantsInputs {
597+
Ok(WantsInputs {
594598
original_psbt: self.original_psbt,
595599
payjoin_psbt,
596600
params: self.params,
597601
change_vout: self.change_vout,
598-
}
602+
})
599603
}
600604

601605
// Compute the minimum amount that the receiver must contribute to the transaction as input

payjoin/src/receive/v2/mod.rs

+12-6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ use serde::{Deserialize, Serialize, Serializer};
1515
use url::Url;
1616

1717
use super::v2::error::{InternalSessionError, SessionError};
18-
use super::{Error, InternalRequestError, RequestError, SelectionError};
18+
use super::{
19+
Error, InputContributionError, InternalRequestError, OutputSubstitutionError, RequestError,
20+
SelectionError,
21+
};
1922
use crate::psbt::PsbtExt;
2023
use crate::receive::optional_parameters::Params;
2124
use crate::v2::OhttpEncapsulationError;
@@ -402,7 +405,10 @@ impl WantsOutputs {
402405
}
403406

404407
/// Substitute the receiver output script with the provided script.
405-
pub fn substitute_receiver_script(self, output_script: &Script) -> Result<WantsOutputs, Error> {
408+
pub fn substitute_receiver_script(
409+
self,
410+
output_script: &Script,
411+
) -> Result<WantsOutputs, OutputSubstitutionError> {
406412
let inner = self.inner.substitute_receiver_script(output_script)?;
407413
Ok(WantsOutputs { inner, context: self.context })
408414
}
@@ -416,7 +422,7 @@ impl WantsOutputs {
416422
self,
417423
replacement_outputs: Vec<TxOut>,
418424
drain_script: &Script,
419-
) -> Result<WantsOutputs, Error> {
425+
) -> Result<WantsOutputs, OutputSubstitutionError> {
420426
let inner = self.inner.replace_receiver_outputs(replacement_outputs, drain_script)?;
421427
Ok(WantsOutputs { inner, context: self.context })
422428
}
@@ -460,9 +466,9 @@ impl WantsInputs {
460466
pub fn contribute_witness_inputs(
461467
self,
462468
inputs: impl IntoIterator<Item = (OutPoint, TxOut)>,
463-
) -> WantsInputs {
464-
let inner = self.inner.contribute_witness_inputs(inputs);
465-
WantsInputs { inner, context: self.context }
469+
) -> Result<WantsInputs, InputContributionError> {
470+
let inner = self.inner.contribute_witness_inputs(inputs)?;
471+
Ok(WantsInputs { inner, context: self.context })
466472
}
467473

468474
/// Proceed to the proposal finalization step.

payjoin/tests/integration.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,7 @@ mod integration {
644644

645645
let payjoin = payjoin
646646
.contribute_witness_inputs(vec![(selected_outpoint, txo_to_contribute)])
647+
.unwrap()
647648
.commit_inputs();
648649

649650
// Sign and finalize the proposal PSBT
@@ -1103,7 +1104,7 @@ mod integration {
11031104
}
11041105
};
11051106

1106-
let payjoin = payjoin.contribute_witness_inputs(inputs).commit_inputs();
1107+
let payjoin = payjoin.contribute_witness_inputs(inputs).unwrap().commit_inputs();
11071108

11081109
let payjoin_proposal = payjoin
11091110
.finalize_proposal(

0 commit comments

Comments
 (0)