Skip to content

Commit de5c122

Browse files
committed
Expose PSBT input error index in FFI
Add an `index()` getter to `PsbtInputsError` and an `input_index()` method on the core `BuildSenderError` so the FFI layer can extract the zero-based position of the offending PSBT input. The FFI `BuildSenderError` now carries an `input_index` field (exposed via uniffi) that is populated from the core error during conversion. This lets Swift, Kotlin, Python, and other FFI consumers surface precise diagnostics about which input failed validation. Six new tests verify propagation at the core psbt, core send, and FFI layers. Closes payjoin#1276
1 parent 3647419 commit de5c122

File tree

3 files changed

+101
-3
lines changed

3 files changed

+101
-3
lines changed

payjoin-ffi/src/send/error.rs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,36 @@ use crate::error::{FfiValidationError, ImplementationError};
88
/// Error building a Sender from a SenderBuilder.
99
///
1010
/// This error is unrecoverable.
11+
///
12+
/// When the error is caused by an invalid original PSBT input, the
13+
/// [`input_index`](BuildSenderError::input_index) method returns the
14+
/// zero-based position of the offending input so that FFI consumers can
15+
/// surface precise diagnostics.
1116
#[derive(Debug, PartialEq, Eq, thiserror::Error, uniffi::Object)]
1217
#[error("Error initializing the sender: {msg}")]
1318
pub struct BuildSenderError {
1419
msg: String,
20+
input_index: Option<u64>,
21+
}
22+
23+
#[uniffi::export]
24+
impl BuildSenderError {
25+
/// Returns the zero-based index of the PSBT input that failed
26+
/// validation, if this error was caused by an invalid original input.
27+
pub fn input_index(&self) -> Option<u64> { self.input_index }
1528
}
1629

1730
impl From<PsbtParseError> for BuildSenderError {
18-
fn from(value: PsbtParseError) -> Self { BuildSenderError { msg: value.to_string() } }
31+
fn from(value: PsbtParseError) -> Self {
32+
BuildSenderError { msg: value.to_string(), input_index: None }
33+
}
1934
}
2035

2136
impl From<send::BuildSenderError> for BuildSenderError {
22-
fn from(value: send::BuildSenderError) -> Self { BuildSenderError { msg: value.to_string() } }
37+
fn from(value: send::BuildSenderError) -> Self {
38+
let input_index = value.input_index().map(|i| i as u64);
39+
BuildSenderError { msg: value.to_string(), input_index }
40+
}
2341
}
2442

2543
/// FFI-visible PSBT parsing error surfaced at the sender boundary.
@@ -195,3 +213,31 @@ where
195213
SenderPersistedError::Unexpected
196214
}
197215
}
216+
217+
#[cfg(test)]
218+
mod tests {
219+
use super::*;
220+
221+
#[test]
222+
fn ffi_build_sender_error_with_input_index() {
223+
let err = BuildSenderError { msg: "invalid PSBT input #5".into(), input_index: Some(5) };
224+
assert_eq!(err.input_index(), Some(5));
225+
assert!(err.to_string().contains("#5"));
226+
}
227+
228+
#[test]
229+
fn ffi_build_sender_error_without_input_index() {
230+
let err = BuildSenderError {
231+
msg: "the original transaction has no inputs".into(),
232+
input_index: None,
233+
};
234+
assert_eq!(err.input_index(), None);
235+
}
236+
237+
#[test]
238+
fn ffi_build_sender_error_from_psbt_parse_error() {
239+
let parse_err = PsbtParseError::InvalidPsbt("bad psbt".into());
240+
let err = BuildSenderError::from(parse_err);
241+
assert_eq!(err.input_index(), None);
242+
}
243+
}

payjoin/src/core/psbt/mod.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,16 @@ impl fmt::Display for PsbtInputsError {
353353
}
354354
}
355355

356+
impl PsbtInputsError {
357+
/// Returns the index of the PSBT input that failed validation.
358+
pub fn index(&self) -> usize { self.index }
359+
360+
#[cfg(test)]
361+
pub(crate) fn new_test(index: usize, error: InternalPsbtInputError) -> Self {
362+
Self { index, error }
363+
}
364+
}
365+
356366
impl std::error::Error for PsbtInputsError {
357367
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.error) }
358368
}
@@ -430,7 +440,10 @@ mod test {
430440
use bitcoin::{Psbt, ScriptBuf, Transaction};
431441
use payjoin_test_utils::PARSED_ORIGINAL_PSBT;
432442

433-
use crate::psbt::{InputWeightError, InternalInputPair, InternalPsbtInputError, PsbtExt};
443+
use crate::psbt::{
444+
InputWeightError, InternalInputPair, InternalPsbtInputError, PrevTxOutError, PsbtExt,
445+
PsbtInputsError,
446+
};
434447

435448
#[test]
436449
fn validate_input_utxos() {
@@ -541,4 +554,14 @@ mod test {
541554
let weight = pair.expected_input_weight();
542555
assert_eq!(weight.unwrap_err(), InputWeightError::NoRedeemScript)
543556
}
557+
558+
#[test]
559+
fn psbt_inputs_error_exposes_index() {
560+
let error = PsbtInputsError {
561+
index: 7,
562+
error: InternalPsbtInputError::PrevTxOut(PrevTxOutError::MissingUtxoInformation),
563+
};
564+
assert_eq!(error.index(), 7);
565+
assert!(error.to_string().contains("#7"));
566+
}
544567
}

payjoin/src/core/send/error.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ pub(crate) enum InternalBuildSenderError {
3030
AddressType(crate::psbt::AddressTypeError),
3131
}
3232

33+
impl BuildSenderError {
34+
/// Returns the index of the invalid PSBT input, if this error was
35+
/// caused by an invalid original input.
36+
pub fn input_index(&self) -> Option<usize> {
37+
match &self.0 {
38+
InternalBuildSenderError::InvalidOriginalInput(e) => Some(e.index()),
39+
_ => None,
40+
}
41+
}
42+
}
43+
3344
impl From<InternalBuildSenderError> for BuildSenderError {
3445
fn from(value: InternalBuildSenderError) -> Self { BuildSenderError(value) }
3546
}
@@ -435,4 +446,22 @@ mod tests {
435446
ResponseError::Validation(_)
436447
));
437448
}
449+
450+
#[test]
451+
fn build_sender_error_exposes_input_index() {
452+
use crate::psbt::{InternalPsbtInputError, PrevTxOutError, PsbtInputsError};
453+
454+
let psbt_err = PsbtInputsError::new_test(
455+
3,
456+
InternalPsbtInputError::PrevTxOut(PrevTxOutError::MissingUtxoInformation),
457+
);
458+
let err = BuildSenderError::from(InternalBuildSenderError::InvalidOriginalInput(psbt_err));
459+
assert_eq!(err.input_index(), Some(3));
460+
}
461+
462+
#[test]
463+
fn build_sender_error_no_index_for_other_variants() {
464+
let err = BuildSenderError::from(InternalBuildSenderError::NoInputs);
465+
assert_eq!(err.input_index(), None);
466+
}
438467
}

0 commit comments

Comments
 (0)