Prepared Send #596
No reviewers
Labels
No labels
DB & Storage
Deployment
Error Handling & Logging
Maintenance
Payment Backend
backport
backport v0.13.x
backport v0.14.x
backport v0.15.x
bindings
blocked
bug
cdk-sql
ci
cli
deps
documentation
duplicate
enhancement
good first issue
help wanted
invalid
keep-open
ldk-node-ui
migrations
mint
mutation-testing
needs rebase
needs review
new nut
nut change
question
ready
rust-version
rustfmt
stacked hold
stale
testing
wallet
weekly-report
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
cashubtc/cdk!596
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "prepared-send"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
Wallet::select_proofs.Wallet::sendnow requires aPreparedSend.WalletDatabaseproof state update functions have been consolidated intoupdate_proofs_state.ADDED
Wallet::prepare_send.SendOptionsstruct controls optional functionality for sends.Amountsplitting to target a fee rate amount.Proofs.SendKind.Amount(i.e.,checked_mulandchecked_div).REMOVED
FIXED
Checklist
just final-checkbefore committing@thesimplekid two areas I'm looking for feedback:
PreparedSendlogic? I madePreparedSenda "private" structure so that users can't modify it, causing issues with our logic.SendOptionsbut I think I should move it to be a parameter of thesendfunction, possibly with a flag to include it in theTokenor keep it internal with the eventualTransactionsDatabase.Great work on this PR.
I think we can just added a
PreparedSend::newthat way they can at least create aPreparedSendto pass tosend. 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.@ -33,3 +33,3 @@Pending,/// Proof is reserved/// Reserved///This is not longer used when a proof is included in a token as that is
PendingSpent@ -88,0 +105,4 @@/// Check if send kind has tolerancepub fn has_tolerance(&self) -> bool {matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))}Would it be useful is this returned the tolerance?
Option<Amount>instead of a bool?Does this handle the edge case where the proof added to pay the fee increased the fee?
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.@ -229,0 +351,4 @@} else if right_amount >= amount {return Self::select_least_amount_over(right, amount);}}Does this logic only work for powers of two? This is more of a general question and not blocking as the currently cdk mostly assumes power of 2 keysets however this isn't actually required by the nuts so we should move away from it.
@ -35,0 +26,4 @@opts: SendOptions,) -> Result<PreparedSend, Error> {tracing::info!("Preparing send");This filter is required because of what you mentioned previously where the
get_proofswill return proofs with conditions even ifNoneis passed. I think we should update theget_proofsfn to handle this correctly to avoid the filter here.I can do this as a small focused pr ahead of merging this. https://github.com/cashubtc/cdk/issues/611
We can just check the error case as if its okay we can allow it to continue to the
internal_prepare_sendbelowI may have missed it but was the proof set to
Reservedinprepare_send. Am I correct that the expectation is the once a proof is selected as part of a prepared send it is reserved and cannot be used in other operations unless explicitly unreserved? This wayprepare_sendcan be called multiple times without conflicting and we do not have to block other operations.Can we add a doc comment to the send/prepare_send fn explaining this expectation.
@ -178,0 +212,4 @@let keyset_fee_ppk = self.get_keyset_fees_by_id(active_keyset_id).await?;tracing::debug!("Keyset fees: {:?}", keyset_fee_ppk);// Calculate total send amountWhat is the intention of
force_swapis it just for the case where a wallet may want to rotate proofs for example from an inactive to active keyset?Can we just add a few more lines explaining how the
proofs_to_swapvsproofs_to_sendshould be uses and what they are?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
SendOptionsor that should be done higher up the stack?I think we should use
Reservedhere, and keepPendingas a state used by the mint ie in a melt operation where the ln payment is taking some time.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.
@ -88,0 +105,4 @@/// Check if send kind has tolerancepub fn has_tolerance(&self) -> bool {matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))}Then
has_tolerancecould be merged withis_exact.@ -100,6 +92,8 @@ pub trait Database: Debug {state: Option<Vec<State>>,spending_conditions: Option<Vec<SpendingConditions>>,) -> Result<Vec<ProofInfo>, Self::Err>;Maybe instead of removing the 3 methods, we can keep a default implementation in the trait?
This would avoid the need for wallet DBs to re-implement them (as it's done in
cdk-rexienow) and generally preserve the simpler caller semantics from before.This can re-use the
get_keyset_feesmethod above.Shouldn't this check the options?
IMO it's better to set the
SendOptionsdefault here totrue.Yes, it should change proof selections to be more efficient. I wrote some unit tests because I made the
select_proofsfunction static.@ -88,0 +105,4 @@/// Check if send kind has tolerancepub fn has_tolerance(&self) -> bool {matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))}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.
I believe so. Do you have a specific edge case in mind that I can include as a test?
@ -178,0 +212,4 @@let keyset_fee_ppk = self.get_keyset_fees_by_id(active_keyset_id).await?;tracing::debug!("Keyset fees: {:?}", keyset_fee_ppk);// Calculate total send amountThis is for when we need to send proofs with conditions. Any selected proofs must be swapped.
@ -229,0 +351,4 @@} else if right_amount >= amount {return Self::select_least_amount_over(right, amount);}}The logic should work for any keyset because of the fallback in finding the smallest amount over the target amount. However, it’s optimized for powers of two.
Why? Typically, receivers (merchants) pay for fees.
The send_fee should always be zero if include_fee == false. I think it's safer to keep as is.
@ -35,0 +26,4 @@opts: SendOptions,) -> Result<PreparedSend, Error> {tracing::info!("Preparing send");Sorry forgot I said I would look at this and got lost in the review.
This pr has a fix for matching the conditions using the current api which admittedly as a bit confusing and I'm open to suggestions on. But passing in
Nonemeans that we do not care about that condition and want all proofs no matter what that field. For exampleget_proofs(None, None, None, None)would return ALL proofs in the db.get_proofs(Some("exmaple.xyz"), None, None, Nonewould return all proofs of that mint. So the way that spending conditions is isNonewould but proofs of any or no spending condition. So to get proofs that do not have a spending condition it would beSome(vec![]). Firstly, Does how I've explained how this currently works make sense? Secondly, does this api make any sense or do you have a suggestion to improve it?https://github.com/cashubtc/cdk/pull/654
This is true but in ecash i think this is going to lead to bad ux where someone says send me 25 sats you create a token for 25 sats without including fees and then when they receive they say why did you only send me 23 sats.
But as i alluded to above maybe this shouldn't be pushed on the lib level and the default of false makes more sense and wallet devs can choose the ux they want.
We need to create a new migration file. We can't just modify the init as anyone with an existing wallet will get errors.
I'm happy to push a commit fixing this if thats easier.
@ -734,6 +693,31 @@ FROM proof;}I know this is just following the bad example I set, but can we remove the for loop and do an in clause like Ive updated the mint to use.
github.com/cashubtc/cdk@72dff95322/crates/cdk-sqlite/src/mint/mod.rs (L1152-L1173)Not sure whats going on with the CI here it does not seem related to any of the code changes in this PR
I forgot I did this, and I couldn't figure out how to update this check in a new migration. ChatGPT had me create a new table, transfer the data, delete the old table, and rename the new table. That didn't seem right so I just did this to get it to work. Do you know how to change the check?
Yeah for sqlite that is what you have to do. The ALTER TABLE in sqlite is pretty limited if it was psotgress then we could just modify the existing table.
https://github.com/cashubtc/cdk/blob/main/crates/cdk-sqlite/src/mint/migrations/20240718203721_allow_unspent.sql
@ -35,0 +26,4 @@opts: SendOptions,) -> Result<PreparedSend, Error> {tracing::info!("Preparing send");My loosely held opinion is to keep the
get_proofsAPI as is and add another function to return all proofs.@ -45,135 +37,377 @@ impl Wallet {}Should we check the proof state here to make sure they are Reserved or Unspent and have not already been used in another tx
@ -178,0 +183,4 @@// Calculate swap feelet swap_fee = self.get_proofs_fee(&proofs_to_swap).await?;To get proofs with no conditions it should be.
@ -178,0 +215,4 @@// Calculate total send amountlet total_send_amount = send.amount + send.send_fee;tracing::debug!("Total send amount: {}", total_send_amount);I see there is a
cancel_sendthat sets the prepared proofs back to unspent but I do not see where the proofs are reserved, maybe I'm just missing it?My understanding is that
UNSPENTproofs should be selected and set toRESERVEDin theprepare_sendand thenPreparedSendeither needs to be used andproofsare setPENDINGSPENTorPreparedSendneeds to be canceled and proofs are moved back toUNSPENT.If we do not reserve the proofs in the prepare then if prepare is called again these proofs maybe selected again.
@ -178,0 +215,4 @@// Calculate total send amountlet total_send_amount = send.amount + send.send_fee;tracing::debug!("Total send amount: {}", total_send_amount);Somehow reserving the proofs got deleted. Adding a test now.
@ -45,135 +37,377 @@ impl Wallet {}What error should be returned?
@ -45,135 +37,377 @@ impl Wallet {}This will set all the proofs in the PreparedSend to Unspent. But we probably dont want to set proofs that are PendingSpent or Spent back. If the api is uses correctlly this wont be an issue but i think its best we check it.
Maybe some new error ProofStateUnexpected and just add a log message.
@ -178,0 +215,4 @@// Calculate total send amountlet total_send_amount = send.amount + send.send_fee;tracing::debug!("Total send amount: {}", total_send_amount);What happens if a caller forgets to call
cancel_send? (or the app stops / crashes just before calling it)Will those proofs remain
RESERVEDindefinitely?@ -45,135 +37,377 @@ impl Wallet {}Is this in case a bug is introduced? I can add the check for safety but I'm not sure how that would happen.
@ -178,0 +215,4 @@// Calculate total send amountlet total_send_amount = send.amount + send.send_fee;tracing::debug!("Total send amount: {}", total_send_amount);Yes, until
Wallet::unreserve_proofsis called.@ -45,135 +37,377 @@ impl Wallet {}My thinking is to protect the user from the flow of prepared send -> send but then calling cancel on the prepared send. In this case where they've already created the token with send they should have to receive the token to reclaim it.
@ -45,135 +37,377 @@ impl Wallet {}This shouldn't be an issue because
PreparedSendis notClone, and bothsendandcancel_sendmove the struct, but I will add an additional check just in case.@ -45,135 +37,377 @@ impl Wallet {}Ah good point
ACK
8c7506f43c