-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comprehensive looking 😎 Any reason this is review requested but still draft? Because it is, I haven't checked it out yet.
payjoin/Cargo.toml
Outdated
@@ -24,11 +24,15 @@ bitcoin = { version = "0.30.0", features = ["base64"] } | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we're using serde I think we want to use it without "derive," That's where the static binary comes in
payjoin/src/receive/error.rs
Outdated
Self::Server(_) => write!( | ||
f, | ||
r#"{{ "errorCode": "unavailable", "message": "The payjoin endpoint is not available for now." }}"# | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
payjoin/tests/integration.rs
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason we couldn't check against a real integration to avoid mocking?
payjoin/Cargo.toml
Outdated
url = "2.2.2" | ||
|
||
[dev-dependencies] | ||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
.
@@ -22,17 +46,19 @@ impl Default for Params { | |||
disable_output_substitution: false, | |||
additional_fee_contribution: None, | |||
min_feerate: FeeRate::ZERO, | |||
version: 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
payjoin/src/uri.rs
Outdated
@@ -110,6 +111,7 @@ impl<'a> bip21::de::DeserializeParams<'a> for Payjoin { | |||
pub struct DeserializationState { | |||
pj: Option<Url>, | |||
pjos: Option<bool>, | |||
v: Option<usize>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"&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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
payjoin-cli/src/app.rs
Outdated
@@ -91,7 +91,8 @@ impl App { | |||
let psbt = Psbt::from_str(&psbt).with_context(|| "Failed to load PSBT from base64")?; | |||
log::debug!("Original psbt: {:#?}", psbt); | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
payjoin/src/send/error.rs
Outdated
let known_str_error = "{\"errorCode\":\"version-unsupported\", | ||
\"message\":\"This version of payjoin is not supported.\"}"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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."}"#; | |
payjoin/src/send/mod.rs
Outdated
|
||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
payjoin/src/send/error.rs
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
payjoin/src/send/error.rs
Outdated
|
||
#[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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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"
payjoin/src/send/error.rs
Outdated
r#"Well known error: {{ "errorCode": "{}", | ||
"message": "{}" }}"#, | ||
e.error_code(), | ||
e.to_string(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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