Skip to content
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

add ProvisionalProposal struct #90

Merged

Conversation

jbesraa
Copy link
Collaborator

@jbesraa jbesraa commented Jul 25, 2023

resolves #73

@jbesraa jbesraa changed the title add ProvisionalProposal struct [Draft] add ProvisionalProposal struct Jul 25, 2023
@jbesraa jbesraa marked this pull request as draft July 25, 2023 12:58
@jbesraa
Copy link
Collaborator Author

jbesraa commented Jul 25, 2023

@DanGould
so I did just the first step which is creating a ProvisionalProposal without doing any changes to functionality but just signature changes.
I am planning to combine apply_fees and prepare_psbt as the next step, but when I looked in the CLI implementation I see that after apply_fee is executed, the psbt is finalized with the bitcoind.wallet but we dont have access to the wallet in payjoin receive module. should I add that as an input to "finalize_proposal" ?

actually maybe it would be good to think about the signature of finalize_proposal.
what do u think about this?

pub fn finalize_proposal(&mut self, w: impl wallet trait, min_feerate_sat_per_vb: Option<u64>)

@DanGould
Copy link
Contributor

🏋️‍♂️🤙

It might make more sense to pass a closure as a parameter than a trait we'd have to define and implement downstream. This is how check methods are done earlier in the module. Take a look at those and let me know which parts you need more clarity on

payjoin/Cargo.toml Outdated Show resolved Hide resolved
@jbesraa jbesraa force-pushed the Feature/Add-ProvisionalPayjoinProposal-Struct branch 2 times, most recently from fb02857 to 579bbda Compare July 31, 2023 19:57
@jbesraa jbesraa changed the title [Draft] add ProvisionalProposal struct add ProvisionalProposal struct Jul 31, 2023
@jbesraa jbesraa marked this pull request as ready for review July 31, 2023 20:22
Copy link
Contributor

@DanGould DanGould left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is starting to look beautiful. The attention to detail and effort is paying off

payjoin/src/receive/mod.rs Outdated Show resolved Hide resolved
payjoin/src/receive/mod.rs Outdated Show resolved Hide resolved
payjoin-cli/src/app.rs Outdated Show resolved Hide resolved
Ok(false)
}
})?;
let mut provisional_payjoin: ProvisionalProposal =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this explicit type required? Seems redundant since you're using such care with naming.

a nit would be to make check4's result proposal or provisional_payjoin instead of becoming a payjoin and turning into a provisional_payjoin

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really, removed.

created a new issue for check4 change here #96, will handle that in a new pr as i dont wanna add more changes to this pr

Copy link
Contributor

@DanGould DanGould Aug 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused why you chose to make this a new issue, it seems this PR introduces the issue, no? The second part of the comment was just about naming.

@jbesraa jbesraa force-pushed the Feature/Add-ProvisionalPayjoinProposal-Struct branch 2 times, most recently from b00f73d to 956025e Compare August 6, 2023 07:31
@jbesraa
Copy link
Collaborator Author

jbesraa commented Aug 6, 2023

@DanGould thanks so much for the input. I made the required changes, feel free to review

Copy link
Contributor

@DanGould DanGould left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this is looking like a much less leaky way to handle typestate. I'm impressed.

I left a few more questions about the interface design

Comment on lines 815 to 816
wallet_process_psbt: impl Fn(&Psbt) -> Result<std::string::String, Error>,
min_feerate_sat_per_vb: Option<u64>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might we want to return Result<Psbt, Error> to validate the result and min_feerate: Option<FeeRate> for parameters?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we currently validate the result with Psbt::from_str, if we change the signature we basically leave the validation(or the conversion part from str to psbt) to the user.
when u make a call to finalize_psbt it will look something similar to this:

            .finalize_proposal(
                |psbt: &Psbt| {
                    Ok(receiver
                        .wallet_process_psbt(
                            &bitcoin::base64::encode(psbt.serialize()),
                            None,
                            None,
                            Some(false),
                        )
                        .map(|res: WalletProcessPsbtResult| {
                            let psbt = Psbt::from_str(&res.psbt).unwrap(); //convert from str to psbt
                            return psbt;
                        })
                        .unwrap())
                },
                Some(bitcoin::FeeRate::MIN),
            )

and finalize_proposal would look like this

    pub fn finalize_proposal(
        mut self,
        wallet_process_psbt: impl Fn(&Psbt) -> Result<Psbt, Error>,
        min_feerate_sat_per_vb: Option<FeeRate>,
    ) -> Result<PayjoinProposal, RequestError> {
        let psbt = self.apply_fee(min_feerate_sat_per_vb)?;
        let psbt = wallet_process_psbt(psbt).map_err(|_| {
            log::error!("wallet_process_psbt error");
            InternalRequestError::PsbtFinalizingError
        })?;
        let payjoin_proposal = self.prepare_psbt(psbt).map_err(RequestError::from)?;
        Ok(payjoin_proposal)
    }

I like it. its nicer and makes a bit more sense because the function implementation is the user responsibility so it makes sense that they send the EXACT expected type. we can also do some checks after we get the psbt and do a set of validations from pj perspective (maybe differentiating between psbt version(s) or so..)

Base64(bitcoin::base64::DecodeError),
PsbtFinalizingError(&'static str),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This always returns the same kind of string, so it doesn't need to take a value at all

InternalRequestError::PsbtFinalize would be fine

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed!

Comment on lines 819 to 826
let payjoin_proposal_psbt = match wallet_process_psbt(payjoin_proposal_psbt) {
Ok(p) => p,
Err(_) => {
return Err(RequestError::from(InternalRequestError::PsbtFinalizingError(
"wallet_process_psbt failed",
)));
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let payjoin_proposal_psbt = match wallet_process_psbt(payjoin_proposal_psbt) {
Ok(p) => p,
Err(_) => {
return Err(RequestError::from(InternalRequestError::PsbtFinalizingError(
"wallet_process_psbt failed",
)));
}
};
let psbt = wallet_process_psbt(payjoin_proposal_psbt)
.map_err(|_| InternalRequestError::PsbtFinalizingError.into())?;

I think it's fine to call it plain psbt in this context, we know it's a payjoin_proposal method function.

.map(|res| res.psbt)
.map_err(|e| Error::Server(e.into()))
},
Some(1),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

magic numbers usually mean something has gone wrong somewhere. I think we should accept a FeeRate here instead of u64

Some(1),
)
.unwrap();
let psbt = payjoin_proposal.get_payjoin_psbt();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a good reason to return a PayjoinProposal to hang onto instead of just the psbt straight away? there might be.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea we might need to return other stuff in the future so In that case it wouldnt be a breaking change

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. It might make more sense to name this psbt() since we know it's in the PayjoinProposal and get_ is associated with returning from an index in rust by convention https://rust-lang.github.io/api-guidelines/naming.html#getter-names-follow-rust-convention-c-getter

@@ -550,8 +551,8 @@ impl OutputsUnknown {
}

/// A mutable checked proposal that the receiver may contribute inputs to to make a payjoin.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stray line

@@ -28,4 +28,4 @@ env_logger = "0.9.0"
bitcoind = { version = "0.31.1", features = ["0_21_2"] }

[package.metadata.docs.rs]
features = ["send", "receive"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stray change here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so weird, cant find this change locally

@jbesraa jbesraa force-pushed the Feature/Add-ProvisionalPayjoinProposal-Struct branch 2 times, most recently from 25eac79 to e26ad17 Compare August 9, 2023 15:13
@jbesraa
Copy link
Collaborator Author

jbesraa commented Aug 9, 2023

hey @DanGould thanks for the review.
I made some small changes and wanna allocate a bigger time window for this to get to a better shape. I am trying to run the integration tests locally without success, any tips about that?
I tried with cargo test --verbose --all-features --lib and cargo test tests/integration.rs --verbose --all-features --lib without success.

@DanGould
Copy link
Contributor

DanGould commented Aug 11, 2023

RUST_LOG=debug cargo test --package payjoin --test integration --features send --features receive -- integration::integration_test --exact --nocapture works for me, try that

None,
Some(false),
)
.map(|res| Psbt::from_str(&res.psbt).unwrap())
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DanGould been trying to tackle this unwrap without success. how would you approach it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This article gives a few different options for returning errors from iterators: https://doc.rust-lang.org/rust-by-example/error/iter_result.html

I think the main issue is that it it doesn't make sense for finalize_proposal to return a RequestError, because the request has already been checked by this point. The error is more of a receiver::Error::Server and could be returned inside that Box.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah changing that to Error made things so easier! thanks

@jbesraa jbesraa force-pushed the Feature/Add-ProvisionalPayjoinProposal-Struct branch from d025713 to ac4f10a Compare August 13, 2023 14:19
@jbesraa
Copy link
Collaborator Author

jbesraa commented Aug 13, 2023

hey @DanGould I made few changes and resolved #96
we need to add a bit of docs to reflect this change, any other place than the code that I need to take into account?

min_feerate_sat_per_vb: Option<FeeRate>,
) -> Result<PayjoinProposal, RequestError> {
let psbt = self.apply_fee(min_feerate_sat_per_vb)?;
let psbt = wallet_process_psbt(psbt).map_err(|_| {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to handle the specific error instead of discarding it by returning Error from finalize_proposal instead of RequestError. Then we don't have to convert after prepare_psbt either

Comment on lines 294 to 308
let payjoin = proposal.check_no_inputs_seen_before(|input| {
Ok(!self.insert_input_seen_before(*input).map_err(|e| Error::Server(e.into()))?)
})?;
let mut provisional_payjoin = proposal.check_no_inputs_seen_before(
|input| {
Ok(!self.insert_input_seen_before(*input).map_err(|e| Error::Server(e.into()))?)
},
|output_script| {
if let Ok(address) = bitcoin::Address::from_script(output_script, network) {
self.bitcoind
.get_address_info(&address)
.map(|info| info.is_mine.unwrap_or(false))
.map_err(|e| Error::Server(e.into()))
} else {
Ok(false)
}
},
)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

identify_receiver_outputs does not seem directly related to the check, so I think we lose some semantics by merging them into one step. Either way it's unrelated to the ProvisionalProposal struct and would be better suited to its own PR

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea I see what u mean
maybe we can change the parent function name

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it should be renamed rather not combined. It's unrelated to this PR.

@@ -491,7 +491,8 @@ impl MaybeInputsSeen {
pub fn check_no_inputs_seen_before(
self,
is_known: impl Fn(&OutPoint) -> Result<bool, Error>,
) -> Result<OutputsUnknown, Error> {
is_receiver_output: impl Fn(&Script) -> Result<bool, Error>,
) -> Result<ProvisionalProposal, Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see why these might be merged, but perhaps the checks should return RequestError (since if they fail, the request is bad) while the closure returns Error, since that's a problem with the server implementation. Either way this is separate from the ProvisionalProposal change

@jbesraa jbesraa force-pushed the Feature/Add-ProvisionalPayjoinProposal-Struct branch 2 times, most recently from aaf5b99 to 84e8ac9 Compare August 18, 2023 05:07
@DanGould
Copy link
Contributor

I'm not sure my comments re: return type errors and the merging of 2 functions into one have been addressed. Addressing them both will help this API deliver on its purpose.

I think #[derive(Default)] on an enum breaks our MSRV requirement in CI, too.

And for the most detail oriented changes, the commit messages would benefit from these semantics so we have a neat history to read later down the line https://cbea.ms/git-commit/

@jbesraa jbesraa force-pushed the Feature/Add-ProvisionalPayjoinProposal-Struct branch from 84e8ac9 to 88d37e7 Compare August 20, 2023 15:11
@jbesraa jbesraa force-pushed the Feature/Add-ProvisionalPayjoinProposal-Struct branch 2 times, most recently from 64602c3 to 209c153 Compare August 20, 2023 15:29
CI is failing because flate2 released new version 1.0.27
that is not passing rustc 1.57.
Reference: rust-lang/flate2-rs#370
Motivaiton is to create a struct that is used
until we finalize the proposal and return PayjoinProposal so
we dont leak data out of PayjoinProposal.

Changed apply_fee and prepare_psbt to private functions
and they are now invoked from finalize_proposal
@DanGould DanGould force-pushed the Feature/Add-ProvisionalPayjoinProposal-Struct branch from 17da43c to be353ab Compare August 23, 2023 17:00
@DanGould
Copy link
Contributor

I've re-ordered the commits so that each one passes tests and changed the apply_fee param to min_fee_rate since it's using the FeeRate type and doesn't need to specify sats_per_byte.

@jbesraa you've made this receiver interface 10x cleaner at least ⭐️

@DanGould DanGould merged commit 0c6a2e5 into payjoin:master Aug 23, 2023
7 checks passed
@DanGould DanGould mentioned this pull request Sep 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PayjoinProposal typestate is leaky
2 participants