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

Prepared Send #596

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open

Prepared Send #596

wants to merge 41 commits into from

Conversation

davidcaseria
Copy link
Contributor

@davidcaseria davidcaseria commented Feb 11, 2025

Description

Prepare sends by selecting proofs and returning a fee amount before finalizing a send into a token.


Notes to the reviewers


Suggested CHANGELOG Updates

CHANGED

  • Unifies and optimizes the proof selection algorithm to use Wallet::select_proofs.
  • Wallet::send now requires a PreparedSend.
  • WalletDatabase proof state update functions have been consolidated into update_proofs_state.

ADDED

  • Sends should be initiated by calling Wallet::prepare_send.
  • A SendOptions struct controls optional functionality for sends.
  • Allow Amount splitting to target a fee rate amount.
  • Utility functions for Proofs.
  • Utility functions for SendKind.
  • Completed checked arithmetic operations for Amount (i.e., checked_mul and checked_div).

REMOVED

FIXED


Checklist

@davidcaseria davidcaseria marked this pull request as ready for review February 15, 2025 02:03
@thesimplekid thesimplekid self-requested a review February 18, 2025 14:34
@davidcaseria
Copy link
Contributor Author

@thesimplekid two areas I'm looking for feedback:

  1. How should we support CDK users who want to bring their own PreparedSend logic? I made PreparedSend a "private" structure so that users can't modify it, causing issues with our logic.
  2. How should we support memos? I have it in SendOptions but I think I should move it to be a parameter of the send function, possibly with a flag to include it in the Token or keep it internal with the eventual TransactionsDatabase.

Copy link
Collaborator

@thesimplekid thesimplekid left a comment

Choose a reason for hiding this comment

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

Great work on this PR.

How should we support CDK users who want to bring their own PreparedSend logic? I made PreparedSend a "private" structure so that users can't modify it, causing issues with our logic.

I think we can just added a PreparedSend::new that way they can at least create a PreparedSend to pass to send. But I think maybe better to not worry to much about this for now to keep it simpler and if that need arises we can revisit.

How should we support memos? I have it in SendOptions but I think I should move it to be a parameter of the send function, possibly with a flag to include it in the Token or keep it internal with the eventual TransactionsDatabase.
I agree I think we can move it to the send fn maybe we can have two and internal and an external (included in token) memo and when the former is not set use the external one.

Comment on lines +106 to +108
pub fn has_tolerance(&self) -> bool {
matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it be useful is this returned the tolerance? Option<Amount> instead of a bool?

Copy link
Contributor

Choose a reason for hiding this comment

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

Then has_tolerance could be merged with is_exact.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer to use bools instead of checking the state of Options when doing conditional logic, such as how these functions are used. I can add a tolerance function that returns an Option, but I don't see why we shouldn't provide all of these functions in the API.

Comment on lines 388 to 394
selected_proofs.extend(Wallet::select_proofs(
remaining_amount,
remaining_proofs,
active_keyset_id,
&HashMap::new(), // Fees are already calculated
false,
)?);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this handle the edge case where the proof added to pay the fee increased the fee?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe so. Do you have a specific edge case in mind that I can include as a test?

Comment on lines +55 to +70
.get_proofs(
Some(self.mint_url.clone()),
Some(self.unit.clone()),
Some(vec![State::Unspent]),
None,
)
.await?
.into_iter()
.filter_map(|p| {
if p.spending_condition.is_none() {
Some(p.proof)
} else {
None
}
})
.collect();
Copy link
Collaborator

Choose a reason for hiding this comment

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

This filter is required because of what you mentioned previously where the get_proofs will return proofs with conditions even if None is passed. I think we should update the get_proofs fn to handle this correctly to avoid the filter here.

I can do this as a small focused pr ahead of merging this. #611

amount: Amount,
proofs: Proofs,
active_keyset_id: Id,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this should be a Vec<Id> the nuts allow for mints to have multiple active keysets. Cdk mint limits this on the implementation level to only have one active keyset per unit, however on the wallet side I do not think we should assume this and should account for multiple as it is in spec.

/// Send kind
pub send_kind: SendKind,
/// Include fee
pub include_fee: bool,
Copy link
Collaborator

@thesimplekid thesimplekid Feb 21, 2025

Choose a reason for hiding this comment

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

I think the better ux is the default behavior is to include the fee always. Do we think that should be the default here in SendOptions or that should be done higher up the stack?

Copy link
Contributor

Choose a reason for hiding this comment

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

IMO it's better to set the SendOptions default here to true.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why? Typically, receivers (merchants) pay for fees.

Comment on lines +209 to +211
self.localstore
.update_proofs_state(ys, State::Pending)
.await?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should use Reserved here, and keep Pending as a state used by the mint ie in a melt operation where the ln payment is taking some time.

@thesimplekid
Copy link
Collaborator

Have you done any testing around does this change the proofs that would be selected from a set from previous cdk versions? Do you expect it to?

I would be interested to include some integrations tests on this so we don't change proofs selection accidentally.

Comment on lines +106 to +108
pub fn has_tolerance(&self) -> bool {
matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Then has_tolerance could be merged with is_exact.

async fn reserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err>;
/// Set proofs as unspent in storage. Proofs are identified by their Y
/// value.
async fn set_unspent_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe instead of removing the 3 methods, we can keep a default implementation in the trait?

    /// Set proofs as pending in storage. Proofs are identified by their Y
    /// value.
    async fn set_pending_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
        self.update_proofs_state(ys, State::Pending).await
    }
    /// Reserve proofs in storage. Proofs are identified by their Y value.
    async fn reserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
        self.update_proofs_state(ys, State::Reserved).await
    }
    /// Set proofs as unspent in storage. Proofs are identified by their Y
    /// value.
    async fn set_unspent_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
        self.update_proofs_state(ys, State::Unspent).await
    }

This would avoid the need for wallet DBs to re-implement them (as it's done in cdk-rexie now) and generally preserve the simpler caller semantics from before.


/// Total fee
pub fn fee(&self) -> Amount {
self.swap_fee + self.send_fee
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this check the options?

Suggested change
self.swap_fee + self.send_fee
match self.options.include_fee {
true => self.swap_fee + self.send_fee,
false => self.swap_fee,
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The send_fee should always be zero if include_fee == false. I think it's safer to keep as is.

/// Send kind
pub send_kind: SendKind,
/// Include fee
pub include_fee: bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO it's better to set the SendOptions default here to true.

@davidcaseria
Copy link
Contributor Author

Have you done any testing around does this change the proofs that would be selected from a set from previous cdk versions? Do you expect it to?

I would be interested to include some integrations tests on this so we don't change proofs selection accidentally.

Yes, it should change proof selections to be more efficient. I wrote some unit tests because I made the select_proofs function static.

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.

3 participants