Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions payjoin-ffi/dart/test/test_payjoin_unit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -268,5 +268,35 @@ void main() {
throwsA(isA<payjoin.SenderInputException>()),
);
});

test("Validation sender builder exposes invalid original input index", () {
final receiverPersister = InMemoryReceiverPersister("1");
final receiver = payjoin.ReceiverBuilder(
address: "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK",
directory: "https://example.com",
ohttpKeys: payjoin.OhttpKeys.decode(
bytes: Uint8List.fromList(
hex.decode(
"01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003",
),
),
),
).build().save(persister: receiverPersister);
final uri = receiver.pjUri();

try {
payjoin.SenderBuilder(
psbt: payjoin.invalidOriginalInputPsbt(),
uri: uri,
).buildNonIncentivizing(minFeeRate: 1000);
fail("expected sender build error");
} on payjoin.BuildSenderInputException catch (e) {
expect(e.v0.invalidOriginalInputIndex(), 0);
expect(
e.v0.invalidOriginalInputMessage(),
"invalid previous transaction output",
);
}
});
});
}
29 changes: 29 additions & 0 deletions payjoin-ffi/python/test/test_payjoin_unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,35 @@ def test_sender_builder_rejects_bad_psbt(self):
with self.assertRaises(payjoin.SenderInputError):
payjoin.SenderBuilder("not-a-psbt", uri)

def test_sender_builder_exposes_invalid_original_input_index(self):
receiver_persister = InMemoryReceiverPersister(1)
receiver = (
payjoin.ReceiverBuilder(
"2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK",
"https://example.com",
payjoin.OhttpKeys.decode(
bytes.fromhex(
"01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003"
)
),
)
.build()
.save(receiver_persister)
)
uri = receiver.pj_uri()

with self.assertRaises(payjoin.SenderInputError.Build) as ctx:
payjoin.SenderBuilder(
payjoin.invalid_original_input_psbt(), uri
).build_non_incentivizing(1000)

error = ctx.exception[0]
self.assertEqual(error.invalid_original_input_index(), 0)
self.assertEqual(
error.invalid_original_input_message(),
"invalid previous transaction output",
)


if __name__ == "__main__":
unittest.main()
82 changes: 80 additions & 2 deletions payjoin-ffi/src/send/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,41 @@ use crate::error::{FfiValidationError, ImplementationError};
#[error("Error initializing the sender: {msg}")]
pub struct BuildSenderError {
msg: String,
invalid_original_input_index: Option<u64>,
invalid_original_input_message: Option<String>,
}

impl From<PsbtParseError> for BuildSenderError {
fn from(value: PsbtParseError) -> Self { BuildSenderError { msg: value.to_string() } }
fn from(value: PsbtParseError) -> Self {
BuildSenderError {
msg: value.to_string(),
invalid_original_input_index: None,
invalid_original_input_message: None,
}
}
}

impl From<send::BuildSenderError> for BuildSenderError {
fn from(value: send::BuildSenderError) -> Self { BuildSenderError { msg: value.to_string() } }
fn from(value: send::BuildSenderError) -> Self {
BuildSenderError {
msg: value.to_string(),
invalid_original_input_index: value
.invalid_original_input_index()
.map(|index| index as u64),
invalid_original_input_message: value.invalid_original_input_message(),
}
}
}

#[uniffi::export]
impl BuildSenderError {
pub fn message(&self) -> String { self.msg.clone() }

pub fn invalid_original_input_index(&self) -> Option<u64> { self.invalid_original_input_index }

pub fn invalid_original_input_message(&self) -> Option<String> {
self.invalid_original_input_message.clone()
}
}

/// FFI-visible PSBT parsing error surfaced at the sender boundary.
Expand Down Expand Up @@ -195,3 +222,54 @@ where
SenderPersistedError::Unexpected
}
}

#[cfg(all(test, feature = "_test-utils"))]
mod tests {
use std::str::FromStr;
use std::sync::Arc;

use payjoin::bitcoin::hex::FromHex;

use crate::send::{SenderBuilder, SenderInputError};
use crate::test_utils::invalid_original_input_psbt;
use crate::uri::PjUri;

#[test]
fn test_build_sender_error_exposes_invalid_input_index() {
let address =
payjoin::bitcoin::Address::from_str("tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4")
.expect("address should parse")
.assume_checked();
let ohttp_keys = payjoin::OhttpKeys::decode(
&<Vec<u8> as FromHex>::from_hex(
"01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003",
)
.expect("hex fixture should decode"),
)
.expect("OHTTP keys should decode");
let receiver = payjoin::receive::v2::ReceiverBuilder::new(
address,
"https://example.com".to_string(),
ohttp_keys,
)
.expect("receiver builder should succeed")
.build()
.save(&payjoin::persist::NoopSessionPersister::default())
.expect("no-op persister should not fail");
let uri = Arc::new(PjUri::from(receiver.pj_uri()));

let error = SenderBuilder::new(invalid_original_input_psbt(), uri)
.expect("PSBT should parse")
.build_non_incentivizing(1000);

let Err(SenderInputError::Build(error)) = error else {
panic!("expected sender build error");
};

assert_eq!(error.invalid_original_input_index(), Some(0));
assert_eq!(
error.invalid_original_input_message(),
Some("invalid previous transaction output".to_string())
);
}
}
10 changes: 10 additions & 0 deletions payjoin-ffi/src/test_utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::io;
use std::str::FromStr;
use std::sync::Arc;

use lazy_static::lazy_static;
use payjoin::bitcoin::Psbt;
use payjoin_test_utils::corepc_node::AddressType;
use payjoin_test_utils::{
corepc_node, EXAMPLE_URL, INVALID_PSBT, ORIGINAL_PSBT, PAYJOIN_PROPOSAL,
Expand Down Expand Up @@ -204,6 +206,14 @@ pub fn original_psbt() -> String { ORIGINAL_PSBT.to_string() }
#[uniffi::export]
pub fn invalid_psbt() -> String { INVALID_PSBT.to_string() }

#[uniffi::export]
pub fn invalid_original_input_psbt() -> String {
let mut psbt = Psbt::from_str(ORIGINAL_PSBT).expect("original PSBT fixture should parse");
psbt.inputs[0].witness_utxo = None;
psbt.inputs[0].non_witness_utxo = None;
psbt.to_string()
}

#[uniffi::export]
pub fn payjoin_proposal() -> String { PAYJOIN_PROPOSAL.to_string() }

Expand Down
6 changes: 6 additions & 0 deletions payjoin/src/core/psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,12 @@ pub struct PsbtInputsError {
error: InternalPsbtInputError,
}

impl PsbtInputsError {
pub(crate) fn index(&self) -> usize { self.index }

pub(crate) fn error_message(&self) -> String { self.error.to_string() }
}

impl fmt::Display for PsbtInputsError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "invalid PSBT input #{}", self.index)
Expand Down
39 changes: 39 additions & 0 deletions payjoin/src/core/send/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ impl From<crate::psbt::AddressTypeError> for BuildSenderError {
}
}

impl BuildSenderError {
/// Returns the original PSBT input index when sender construction failed because one input
/// was malformed.
pub fn invalid_original_input_index(&self) -> Option<usize> {
match &self.0 {
InternalBuildSenderError::InvalidOriginalInput(error) => Some(error.index()),
_ => None,
}
}

/// Returns the nested invalid-input message when sender construction failed because one input
/// was malformed.
pub fn invalid_original_input_message(&self) -> Option<String> {
match &self.0 {
InternalBuildSenderError::InvalidOriginalInput(error) => Some(error.error_message()),
_ => None,
}
}
}

impl fmt::Display for BuildSenderError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use InternalBuildSenderError::*;
Expand Down Expand Up @@ -403,9 +423,28 @@ impl WellKnownError {

#[cfg(test)]
mod tests {
use payjoin_test_utils::PARSED_ORIGINAL_PSBT;
use serde_json::json;

use super::*;
use crate::psbt::PsbtExt;

#[test]
fn test_build_sender_error_invalid_original_input_accessors() {
let mut psbt = PARSED_ORIGINAL_PSBT.clone();
psbt.inputs[0].witness_utxo = None;
psbt.inputs[0].non_witness_utxo = None;

let invalid_input = psbt.validate_input_utxos().expect_err("PSBT should be invalid");
let error =
BuildSenderError::from(InternalBuildSenderError::InvalidOriginalInput(invalid_input));

assert_eq!(error.invalid_original_input_index(), Some(0));
assert_eq!(
error.invalid_original_input_message(),
Some("invalid previous transaction output".to_string())
);
}

#[test]
fn test_parse_json() {
Expand Down
Loading