@@ -775,21 +775,22 @@ pub(crate) mod test {
775775 use std:: str:: FromStr ;
776776
777777 use bitcoin:: { Address , Network } ;
778- use payjoin_test_utils:: { BoxError , ORIGINAL_PSBT , QUERY_PARAMS } ;
778+ use payjoin_test_utils:: { PARSED_ORIGINAL_PSBT , QUERY_PARAMS , RECEIVER_INPUT_CONTRIBUTION } ;
779779 use rand:: rngs:: StdRng ;
780780 use rand:: SeedableRng ;
781781
782782 use super :: * ;
783- pub ( crate ) fn proposal_from_test_vector ( ) -> Result < UncheckedProposal , BoxError > {
783+ use crate :: receive:: PayloadError ;
784+
785+ pub ( crate ) fn unchecked_proposal_from_test_vector ( ) -> UncheckedProposal {
784786 let pairs = url:: form_urlencoded:: parse ( QUERY_PARAMS . as_bytes ( ) ) ;
785- let params = Params :: from_query_pairs ( pairs, & [ 1 ] ) ?;
786- Ok ( UncheckedProposal { psbt : bitcoin:: Psbt :: from_str ( ORIGINAL_PSBT ) ?, params } )
787+ let params =
788+ Params :: from_query_pairs ( pairs, & [ 1 ] ) . expect ( "Could not parse params from query pairs" ) ;
789+ UncheckedProposal { psbt : PARSED_ORIGINAL_PSBT . clone ( ) , params }
787790 }
788791
789- fn wants_outputs_from_test_vector (
790- proposal : UncheckedProposal ,
791- ) -> Result < WantsOutputs , BoxError > {
792- Ok ( proposal
792+ fn wants_outputs_from_test_vector ( proposal : UncheckedProposal ) -> WantsOutputs {
793+ proposal
793794 . assume_interactive_receiver ( )
794795 . check_inputs_not_owned ( |_| Ok ( false ) )
795796 . expect ( "No inputs should be owned" )
@@ -802,36 +803,46 @@ pub(crate) mod test {
802803 . unwrap ( )
803804 . require_network ( network)
804805 . unwrap ( ) )
805- } ) ?)
806+ } )
807+ . expect ( "Receiver output should be identified" )
808+ }
809+
810+ fn provisional_proposal_from_test_vector ( proposal : UncheckedProposal ) -> ProvisionalProposal {
811+ wants_outputs_from_test_vector ( proposal) . commit_outputs ( ) . commit_inputs ( )
806812 }
807813
808814 #[ test]
809- fn can_get_proposal_from_request ( ) {
810- let proposal = proposal_from_test_vector ( ) ;
811- assert ! ( proposal. is_ok( ) , "OriginalPSBT should be a valid request" ) ;
815+ fn is_output_substitution_disabled ( ) {
816+ let mut proposal = unchecked_proposal_from_test_vector ( ) ;
817+ let payjoin = wants_outputs_from_test_vector ( proposal. clone ( ) ) ;
818+ assert_eq ! ( payjoin. output_substitution( ) , OutputSubstitution :: Enabled ) ;
819+
820+ proposal. params . output_substitution = OutputSubstitution :: Disabled ;
821+ let payjoin = wants_outputs_from_test_vector ( proposal) ;
822+ assert_eq ! ( payjoin. output_substitution( ) , OutputSubstitution :: Disabled ) ;
823+ }
824+
825+ #[ test]
826+ fn unchecked_proposal_below_min_fee ( ) {
827+ let proposal = unchecked_proposal_from_test_vector ( ) ;
828+ let min_fee_rate = FeeRate :: MAX ;
829+ match proposal. clone ( ) . check_broadcast_suitability ( Some ( min_fee_rate) , |_| Ok ( true ) ) {
830+ Err ( ReplyableError :: Payload ( PayloadError ( InternalPayloadError :: PsbtBelowFeeRate (
831+ proposal_rate,
832+ min_rate,
833+ ) ) ) ) => {
834+ assert_eq ! ( proposal_rate, proposal. clone( ) . psbt_fee_rate( ) . unwrap( ) ) ;
835+ assert_eq ! ( min_rate, min_fee_rate) ;
836+ } ,
837+ _ => panic ! ( "Broadcast suitability check should fail due to being below the min fee rate or unexpected error type" ) ,
838+ } ;
812839 }
813840
814841 #[ test]
815842 fn unchecked_proposal_unlocks_after_checks ( ) {
816- let proposal = proposal_from_test_vector ( ) . unwrap ( ) ;
843+ let proposal = unchecked_proposal_from_test_vector ( ) ;
817844 assert_eq ! ( proposal. psbt_fee_rate( ) . unwrap( ) . to_sat_per_vb_floor( ) , 2 ) ;
818- let payjoin = proposal
819- . assume_interactive_receiver ( )
820- . check_inputs_not_owned ( |_| Ok ( false ) )
821- . expect ( "No inputs should be owned" )
822- . check_no_inputs_seen_before ( |_| Ok ( false ) )
823- . expect ( "No inputs should be seen before" )
824- . identify_receiver_outputs ( |script| {
825- let network = Network :: Bitcoin ;
826- Ok ( Address :: from_script ( script, network) . unwrap ( )
827- == Address :: from_str ( "3CZZi7aWFugaCdUCS15dgrUUViupmB8bVM" )
828- . unwrap ( )
829- . require_network ( network)
830- . unwrap ( ) )
831- } )
832- . expect ( "Receiver output should be identified" )
833- . commit_outputs ( )
834- . commit_inputs ( ) ;
845+ let payjoin = provisional_proposal_from_test_vector ( proposal) ;
835846
836847 {
837848 let mut payjoin = payjoin. clone ( ) ;
@@ -847,7 +858,7 @@ pub(crate) mod test {
847858
848859 #[ test]
849860 fn empty_candidates_inputs ( ) {
850- let proposal = proposal_from_test_vector ( ) . unwrap ( ) ;
861+ let proposal = unchecked_proposal_from_test_vector ( ) ;
851862 let wants_inputs = proposal
852863 . assume_interactive_receiver ( )
853864 . check_inputs_not_owned ( |_| Ok ( false ) )
@@ -869,55 +880,47 @@ pub(crate) mod test {
869880 . commit_outputs ( ) ;
870881 let empty_candidate_inputs: Vec < InputPair > = vec ! [ ] ;
871882 let result = wants_inputs. try_preserving_privacy ( empty_candidate_inputs) ;
872- match result {
873- Err ( err) => {
874- let debug_str = format ! ( "{:?}" , err) ;
875- assert ! (
876- debug_str. contains( "Empty" ) ,
877- "Error should indicate 'Empty' but was: {}" ,
878- debug_str
879- ) ;
880- }
881- Ok ( _) => panic ! ( "try_preserving_privacy should fail with empty candidate inputs" ) ,
882- }
883+ assert_eq ! (
884+ result. unwrap_err( ) ,
885+ SelectionError :: from( InternalSelectionError :: Empty ) ,
886+ "try_preserving_privacy should fail with empty candidate inputs"
887+ ) ;
883888 }
884889
885890 #[ test]
886891 fn sender_specifies_excessive_fee_rate ( ) {
887- let mut proposal = proposal_from_test_vector ( ) . unwrap ( ) ;
892+ let mut proposal = unchecked_proposal_from_test_vector ( ) ;
888893 assert_eq ! ( proposal. psbt_fee_rate( ) . unwrap( ) . to_sat_per_vb_floor( ) , 2 ) ;
889894 // Specify excessive fee rate in sender params
890895 proposal. params . min_fee_rate = FeeRate :: from_sat_per_vb_unchecked ( 1000 ) ;
891- // Input contribution for the receiver, from the BIP78 test vector
892- let proposal_psbt = Psbt :: from_str ( "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQAAAA==" ) . unwrap ( ) ;
896+ let proposal_psbt = Psbt :: from_str ( RECEIVER_INPUT_CONTRIBUTION ) . unwrap ( ) ;
893897 let input = InputPair {
894898 txin : proposal_psbt. unsigned_tx . input [ 1 ] . clone ( ) ,
895899 psbtin : proposal_psbt. inputs [ 1 ] . clone ( ) ,
896900 } ;
897- let mut payjoin = proposal
898- . assume_interactive_receiver ( )
899- . check_inputs_not_owned ( |_| Ok ( false ) )
900- . expect ( "No inputs should be owned" )
901- . check_no_inputs_seen_before ( |_| Ok ( false ) )
902- . expect ( "No inputs should be seen before" )
903- . identify_receiver_outputs ( |script| {
904- let network = Network :: Bitcoin ;
905- Ok ( Address :: from_script ( script, network) . unwrap ( )
906- == Address :: from_str ( "3CZZi7aWFugaCdUCS15dgrUUViupmB8bVM" )
907- . unwrap ( )
908- . require_network ( network)
909- . unwrap ( ) )
910- } )
911- . expect ( "Receiver output should be identified" )
901+ let mut payjoin = wants_outputs_from_test_vector ( proposal)
912902 . commit_outputs ( )
913903 . contribute_inputs ( vec ! [ input] )
914904 . expect ( "Failed to contribute inputs" )
915905 . commit_inputs ( ) ;
906+ let additional_output = TxOut {
907+ value : Amount :: ZERO ,
908+ script_pubkey : payjoin. original_psbt . unsigned_tx . output [ 0 ] . script_pubkey . clone ( ) ,
909+ } ;
910+ payjoin. payjoin_psbt . unsigned_tx . output . push ( additional_output) ;
916911 let mut payjoin_clone = payjoin. clone ( ) ;
917912 let psbt = payjoin. apply_fee ( None , Some ( FeeRate :: from_sat_per_vb_unchecked ( 1000 ) ) ) ;
918913 assert ! ( psbt. is_ok( ) , "Payjoin should be a valid PSBT" ) ;
919914 let psbt = payjoin_clone. apply_fee ( None , Some ( FeeRate :: from_sat_per_vb_unchecked ( 995 ) ) ) ;
920- assert ! ( psbt. is_err( ) , "Payjoin exceeds receiver fee preference and should error" ) ;
915+ match psbt {
916+ Err ( InternalPayloadError :: FeeTooHigh ( proposed, max) ) => {
917+ assert_eq ! ( FeeRate :: from_str( "249630" ) . unwrap( ) , proposed) ;
918+ assert_eq ! ( FeeRate :: from_sat_per_vb_unchecked( 995 ) , max) ;
919+ }
920+ _ => panic ! (
921+ "Payjoin exceeds receiver fee preference and should error or unexpected error type"
922+ ) ,
923+ }
921924 }
922925
923926 #[ test]
@@ -977,70 +980,71 @@ pub(crate) mod test {
977980
978981 #[ test]
979982 fn test_pjos_disabled ( ) {
980- let mut proposal = proposal_from_test_vector ( ) . unwrap ( ) ;
983+ let mut proposal = unchecked_proposal_from_test_vector ( ) ;
981984 proposal. params . output_substitution = OutputSubstitution :: Disabled ;
982- let wants_outputs = wants_outputs_from_test_vector ( proposal) . unwrap ( ) ;
985+ let wants_outputs = wants_outputs_from_test_vector ( proposal) ;
986+ let script_pubkey = & wants_outputs. original_psbt . unsigned_tx . output
987+ [ wants_outputs. change_vout ]
988+ . script_pubkey ;
983989
984990 let output_value =
985991 wants_outputs. original_psbt . unsigned_tx . output [ wants_outputs. change_vout ] . value
986992 + Amount :: ONE_SAT ;
987- let outputs = vec ! [ TxOut {
988- value: output_value,
989- script_pubkey: wants_outputs. original_psbt. unsigned_tx. output
990- [ wants_outputs. change_vout]
991- . script_pubkey
992- . clone( ) ,
993- } ] ;
994- let increased_amount = wants_outputs. clone ( ) . replace_receiver_outputs (
995- outputs,
996- wants_outputs. original_psbt . unsigned_tx . output [ wants_outputs. change_vout ]
997- . script_pubkey
998- . as_script ( ) ,
993+ let outputs = vec ! [ TxOut { value: output_value, script_pubkey: script_pubkey. clone( ) } ] ;
994+ let increased_amount =
995+ wants_outputs. clone ( ) . replace_receiver_outputs ( outputs, script_pubkey. as_script ( ) ) ;
996+ assert ! (
997+ increased_amount. is_ok( ) ,
998+ "Increasing the receiver output amount should always be allowed"
999999 ) ;
1000- assert ! ( increased_amount. is_ok( ) , "Replacement Outputs should be a valid WantsOutput" ) ;
10011000 assert_ne ! ( wants_outputs. payjoin_psbt, increased_amount. unwrap( ) . payjoin_psbt) ;
10021001
10031002 let output_value =
10041003 wants_outputs. original_psbt . unsigned_tx . output [ wants_outputs. change_vout ] . value
10051004 - Amount :: ONE_SAT ;
1006- let outputs = vec ! [ TxOut {
1007- value: output_value,
1008- script_pubkey: wants_outputs. original_psbt. unsigned_tx. output
1009- [ wants_outputs. change_vout]
1010- . script_pubkey
1011- . clone( ) ,
1012- } ] ;
1013- let decreased_amount = wants_outputs. clone ( ) . replace_receiver_outputs (
1014- outputs,
1015- wants_outputs. original_psbt . unsigned_tx . output [ wants_outputs. change_vout ]
1016- . script_pubkey
1017- . as_script ( ) ,
1005+ let outputs = vec ! [ TxOut { value: output_value, script_pubkey: script_pubkey. clone( ) } ] ;
1006+ let decreased_amount =
1007+ wants_outputs. clone ( ) . replace_receiver_outputs ( outputs, script_pubkey. as_script ( ) ) ;
1008+ assert_eq ! (
1009+ decreased_amount. unwrap_err( ) ,
1010+ OutputSubstitutionError :: from(
1011+ InternalOutputSubstitutionError :: DecreasedValueWhenDisabled
1012+ ) ,
1013+ "Payjoin receiver amount has been decreased and should error"
10181014 ) ;
1019- match decreased_amount {
1020- Ok ( _) => panic ! ( "Expected error, got success" ) ,
1021- Err ( error) => {
1022- assert_eq ! (
1023- error,
1024- OutputSubstitutionError :: from(
1025- InternalOutputSubstitutionError :: DecreasedValueWhenDisabled
1026- )
1027- ) ;
1028- }
1029- } ;
10301015
10311016 let script = Script :: new ( ) ;
10321017 let replace_receiver_script_pubkey = wants_outputs. substitute_receiver_script ( script) ;
1033- match replace_receiver_script_pubkey {
1034- Ok ( _) => panic ! ( "Expected error, got success" ) ,
1035- Err ( error) => {
1036- assert_eq ! (
1037- error,
1038- OutputSubstitutionError :: from(
1039- InternalOutputSubstitutionError :: ScriptPubKeyChangedWhenDisabled
1040- )
1041- ) ;
1042- }
1018+ assert_eq ! (
1019+ replace_receiver_script_pubkey. unwrap_err( ) ,
1020+ OutputSubstitutionError :: from(
1021+ InternalOutputSubstitutionError :: ScriptPubKeyChangedWhenDisabled
1022+ ) ,
1023+ "Payjoin receiver script pubkey has been modified and should error"
1024+ ) ;
1025+ }
1026+
1027+ #[ test]
1028+ fn test_avoid_uih_one_output ( ) {
1029+ let proposal = unchecked_proposal_from_test_vector ( ) ;
1030+ let proposal_psbt = Psbt :: from_str ( RECEIVER_INPUT_CONTRIBUTION ) . unwrap ( ) ;
1031+ let input = InputPair {
1032+ txin : proposal_psbt. unsigned_tx . input [ 1 ] . clone ( ) ,
1033+ psbtin : proposal_psbt. inputs [ 1 ] . clone ( ) ,
10431034 } ;
1035+ let input_iter = [ input] . into_iter ( ) ;
1036+ let mut payjoin = wants_outputs_from_test_vector ( proposal)
1037+ . commit_outputs ( )
1038+ . contribute_inputs ( input_iter. clone ( ) )
1039+ . expect ( "Failed to contribute inputs" ) ;
1040+
1041+ payjoin. payjoin_psbt . outputs . pop ( ) ;
1042+ let avoid_uih = payjoin. avoid_uih ( input_iter) ;
1043+ assert_eq ! (
1044+ avoid_uih. unwrap_err( ) ,
1045+ SelectionError :: from( InternalSelectionError :: UnsupportedOutputLength ) ,
1046+ "Payjoin below minimum allowed outputs for avoid uih and should error"
1047+ ) ;
10441048 }
10451049
10461050 #[ test]
0 commit comments