Supply & parse receiver response errors#120
Conversation
|
since rust-bitcoin is our dependency and has decided to keep serde, shall we persist with it (perhaps without derive) rather than the unmaintained tinyjson? #110 (comment) see rust-bitcoin/rust-bitcoin#2093 on excluding |
|
Yea, that makes sense. Will see what they did and try to follow that. I started actually be reverting the last commit and then started to write test first. |
210ca40 to
59121d5
Compare
|
Generally the functionality seems to be working. |
59121d5 to
693ab86
Compare
DanGould
left a comment
There was a problem hiding this comment.
Comprehensive looking 😎 Any reason this is review requested but still draft? Because it is, I haven't checked it out yet.
| bip21 = "0.3.1" | ||
| log = { version = "0.4.14"} | ||
| rand = { version = "0.8.4", optional = true } | ||
| serde = { version = "1.0.107", features = ["derive"] } |
There was a problem hiding this comment.
if we're using serde I think we want to use it without "derive," That's where the static binary comes in
| Self::Server(_) => write!( | ||
| f, | ||
| r#"{{ "errorCode": "unavailable", "message": "The payjoin endpoint is not available for now." }}"# | ||
| ), |
There was a problem hiding this comment.
Display is for human readable errors, not really json formamtted ones. This would be wherever the error is printed internally. We probably do not want to drop e for this purpose. Json response should be a separate method, this may have been an oversight in my original pr.
| let receiver_server = MockServer::start(); | ||
| { | ||
| receiver_server.mock(|when, then| { | ||
| // let err = Error::BadRequest | ||
| let error_response = Error::Server("".to_string().into()); | ||
| // dbg!(&error_response); | ||
| when.method(httpmock::Method::POST).path("/payjoin"); | ||
| then.status(200) | ||
| .header("content-type", "text/plain") | ||
| .body(error_response.to_string()); | ||
| }); | ||
| let mut response = isahc::post(receiver_server.url("/payjoin"), "").unwrap(); | ||
| let client_response = ctx.clone().process_response(response.body_mut()); | ||
| dbg!(&client_response); | ||
| } |
There was a problem hiding this comment.
Is there any reason we couldn't check against a real integration to avoid mocking?
| env_logger = "0.9.0" | ||
| bitcoind = { version = "0.31.1", features = ["0_21_2"] } | ||
| httpmock = "0.7.0-rc.1" | ||
| isahc = "1.7.2" |
There was a problem hiding this comment.
We're using ureq as msrv complianat http elsewhere in the codebase, and it's a bitcoind dependency, so it might make more sense than isahc
DanGould
left a comment
There was a problem hiding this comment.
Hate to burst your bubble because I see you've taken a valiant effort to dive into the more difficult error handling territory. But I think there's some confusion between the receiver's bip21 parameters and sender's optional parameters in this last commit.
The version unsupported error is actually already produced from receive.
This PR should attempt to parse and categorize the response from an independent send module just from the json it receives inside process_response
Where Psbt::from_str is called should try to parse the response as a json error if Psbt parsing fails and then return some error, perhaps a receiver::Error with ResponseError parsed from receiver json or a ValidationError variants. The ResponseError could contain both known and unknown variants, but only display messages from known errors, as per spec. I believe this was already done in #110 via WellKnownError::from_str inside ResponseError::from_json.
| disable_output_substitution: false, | ||
| additional_fee_contribution: None, | ||
| min_feerate: FeeRate::ZERO, | ||
| version: 1, |
There was a problem hiding this comment.
version: 1 is default and does not need to be explicitly specified when sending v1 payjoin requests
If not specified, the receiver will assume the sender is v=1.
| pub fn from_query_pairs<K, V, I>(pairs: I) -> Result<Self, Error> | ||
| pub fn from_query_pairs<K, V, I>(pairs: I) -> Result<Self, ResponseError> |
There was a problem hiding this comment.
from_query_pairs probably should not return a ResponseError, as that type encapsulates the error presented in the response. Error on the other hand can contain richness only relevant to the internal receiver state
| pub struct DeserializationState { | ||
| pj: Option<Url>, | ||
| pjos: Option<bool>, | ||
| v: Option<usize>, |
There was a problem hiding this comment.
"&v=" is a sender optional parameter, not a bip21 uri parameter.
bip21 receiver parameters are specified by the receiver and unrecognized by v1 senders.
sender optional parameters are attached to the http request that sends a payjoin.
| use crate::send::error::WellKnownError; | ||
| use crate::send::ResponseError; |
There was a problem hiding this comment.
optional_parameters mod lives inside receive and should not depend on anything that's not common to both send and receive or explicitly inside receive. receive depending on send should be a red flag that the design might be going down the wrong path
| type MaxAdditionalFeeContribution = bitcoin::Amount; | ||
| type AdditionalFeeOutputIndex = usize; |
There was a problem hiding this comment.
How does this newtype help someone reading the error signature better understand what they're looking at?
3f138a3 to
c76ad71
Compare
|
thanks for the input @DanGould pushed some changes to revert some of the mistakes introduced previously. |
| let fallback_tx = psbt.clone().extract_tx(); | ||
| let (req, ctx) = payjoin::send::RequestBuilder::from_psbt_and_uri(psbt, uri) | ||
| let version = 1; | ||
| let (req, ctx) = payjoin::send::RequestBuilder::from_psbt_and_uri(psbt, uri, version) |
There was a problem hiding this comment.
motivation > we need a way to provide the sender to specify the version that will be appended in the url
1f9fc3d to
d0575aa
Compare
|
Thanks for the input! TBH I was sure we dont want i agree, we shouldnt expose in general I think all is implemented for the We have the integration tests and cli that are not using |
|
I think we should at least have the errors created here handled in integration tests. It would be nice to have them done properly in the app too with proper println!(errorCode) println!(message) for WellKnownErrors and println!(generic error message about unknown error) for UnknownErrors + debug logs including the code+messages for both |
| let known_str_error = "{\"errorCode\":\"version-unsupported\", | ||
| \"message\":\"This version of payjoin is not supported.\"}"; |
There was a problem hiding this comment.
This is a good opportunity to take advantage of string literals
| let known_str_error = "{\"errorCode\":\"version-unsupported\", | |
| \"message\":\"This version of payjoin is not supported.\"}"; | |
| let known_str_error = r#"{"errorCode":"version-unsupported", "message":"This version of payjoin is not supported."}"#; | |
|
|
||
| const ORIGINAL_PSBT: &str = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA="; | ||
|
|
||
| const ORIGINAL_PROPOSAL: &str = "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcBBBYAFMeKRXJTVYKNVlgHTdUmDV/LaYUwIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUIgICygvBWB5prpfx61y1HDAwo37kYP3YRJBvAjtunBAur3wYSFzWUDEAAIABAACAAAAAgAEAAAABAAAAAAA="; |
There was a problem hiding this comment.
This should be called PAYJOIN_PROPOSAL in BIP 78 parlance
cae632b to
874b874
Compare
32f3f3c to
16907ad
Compare
16907ad to
e989bd4
Compare
There was a problem hiding this comment.
I see that WellKnown now ignores the message. It should still hang on to it for debug, but only return the template for display. So close!
I also now see what you were saying about additional json fields. In spec "version-unsupported" error contains a supported-versions array for example. We're not using it yet, so I don't think it's awful to ignore for the first round, but we must consider that we may support additional json fields in our design (saying we're OK with breaking changes is one way to handle that) in the future as you mentioned over voice
https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#optional-parameters
I also pinned "which" in order to pass msrv
| fn from_str(s: &str) -> Result<Self, ()> { | ||
| match s { | ||
| "unavailable" => Ok(WellKnownError::Unavailable), | ||
| "not-enough-money" => Ok(WellKnownError::NotEnoughMoney), | ||
| "version-unsupported" => Ok(WellKnownError::VersionUnsupported), | ||
| "original-psbt-rejected" => Ok(WellKnownError::OriginalPsbtRejected), | ||
| _ => Err(()), | ||
| } |
There was a problem hiding this comment.
Yes, we want wellknown error to only match on errorCode, but it should still contain a message which may be different than the template. However, only template message will be displayed to the user and the actual message printed to debug log
|
|
||
| #[test] | ||
| fn test_parse_json() { | ||
| let known_str_error = r#"{"errorCode":"version-unsupported", "message":"This version of payjoin is not supported."}"#; |
There was a problem hiding this comment.
This test could make the message distinct from the template, and check that it prints the message to debug and displays the canned "This version of payjoin is not supported"
| r#"Well known error: {{ "errorCode": "{}", | ||
| "message": "{}" }}"#, | ||
| e.error_code(), | ||
| e.to_string(), |
There was a problem hiding this comment.
we should print the saved json error message here and use to_string just for display
e989bd4 to
08533a3
Compare
1ac76e1 to
24e09b0
Compare
|
fixed up code to add message and actually separate "message" and "meaning" ugh |
DanGould
left a comment
There was a problem hiding this comment.
I made a small nit to WellKnownError getter return ownership and applied this change to work in the payjoin-cli If that floats your boat. I say we merge should you approve my changes on top.
| assert_eq!( | ||
| format!("{:?}", response_error), | ||
| "Well known error: { \"errorCode\": \"version-unsupported\",\n \"message\": \"Debug Message\" }" | ||
| ); | ||
| match response_error { | ||
| payjoin::send::ResponseError::WellKnown(e) => { | ||
| assert_eq!(e.clone().error_code(), "version-unsupported"); | ||
| assert_eq!(e.message(), "Debug Message"); | ||
| } | ||
| _ => panic!("Unexpected error type"), | ||
| }; |
There was a problem hiding this comment.
I definitely prefer the match asserts to the assert_eq. Seems like that assert_eq could start failing while the response is still spec valid.
| let unrecognized_error = r#"{"errorCode":"not-recognized", "message":"o"}"#; | ||
| let response_error = ctx.clone().process_response(&mut unrecognized_error.as_bytes()); | ||
| assert_eq!( | ||
| response_error.unwrap_err().to_string(), | ||
| "The receiver sent an unrecognized error." | ||
| ); |
There was a problem hiding this comment.
This seems to be more of a unit test than related to this particular integration since we're mocking the unrecognized error
b083792 to
b867f69
Compare
| } | ||
|
|
||
| impl WellKnownError { | ||
| fn from_error_code(s: &str, message: String) -> Result<Self, ()> { |
There was a problem hiding this comment.
Seems like this could shortcut return directly to an unrecognized ResponseError insted of Err(()). Just a note in case this gets refactored in the future.
| assert_eq!( | ||
| ctx.process_response(&mut invalid_json_error.as_bytes()).unwrap_err().to_string(), | ||
| "The receiver sent an invalid response: couldn't decode as PSBT or JSON" | ||
| ) |
There was a problem hiding this comment.
I might check Eq between error types instead of the magic string representation next time
|
A nit in the commit log would be that there's a |
|
Excellent review volley here. Huge congrats that this is merged 🎉 |
supersedes #110
resolves #53