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 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
39c40bd
select_proofs_v2
davidcaseria Jan 18, 2025
7668b25
select_proofs_v2 fix ups
davidcaseria Jan 19, 2025
d9365f3
select_proofs_v2 edge case tests
davidcaseria Jan 19, 2025
10a150f
select_proofs_v2 fix over test
davidcaseria Jan 19, 2025
1fa02aa
Merge remote-tracking branch 'origin/proof-select-v2' into prepared-send
davidcaseria Feb 10, 2025
68caa7c
Prepared send
davidcaseria Feb 11, 2025
d26c7b6
Fix swap amount
davidcaseria Feb 11, 2025
e1f5cdb
WIP
davidcaseria Feb 11, 2025
9b26914
Try fix
davidcaseria Feb 11, 2025
7c75483
Fix loop range modification bug
davidcaseria Feb 11, 2025
7a50e25
Maybe this works
davidcaseria Feb 12, 2025
0d2925f
Fix split_with_fee
davidcaseria Feb 12, 2025
e6820b8
Refactor to pre-compute send_fee
davidcaseria Feb 13, 2025
6d82e22
Fix clippy errors
davidcaseria Feb 13, 2025
f9d18d1
Add more comments
davidcaseria Feb 13, 2025
878e9a9
Use new proof selection algorithm
davidcaseria Feb 13, 2025
a999c5b
Remove empty file
davidcaseria Feb 13, 2025
d99e140
Export PreparedSend struct
davidcaseria Feb 13, 2025
ab29654
Include total fee in prepared send
davidcaseria Feb 13, 2025
333741f
Fix swap amount
davidcaseria Feb 13, 2025
f51eabd
Fix swap amount
davidcaseria Feb 13, 2025
6bcb512
erge branch 'main' into prepared-send
davidcaseria Feb 13, 2025
fba73c3
Fix swap for including fees
davidcaseria Feb 13, 2025
a538e45
Finally fix fees
davidcaseria Feb 14, 2025
5dc9e31
Fix returned swap amount
davidcaseria Feb 14, 2025
781797e
Actually fix fees
davidcaseria Feb 14, 2025
3fbc522
Handle exact amounts
davidcaseria Feb 14, 2025
7b2bdd0
DRY ProofsMethods
davidcaseria Feb 14, 2025
ff0778b
Ensure proofs are sorted before selecting least amount over
davidcaseria Feb 17, 2025
09e0af0
Allow memo to be set a time of send
davidcaseria Feb 19, 2025
92d3b58
Create SendMemo to use in SendOptions
davidcaseria Feb 19, 2025
67e33e7
Fix clippy
davidcaseria Feb 19, 2025
0037705
Fix fmt
davidcaseria Feb 20, 2025
b5514ea
Update crates/cdk/src/wallet/send.rs
davidcaseria Feb 26, 2025
2ef0155
Update crates/cdk/src/wallet/send.rs
davidcaseria Feb 26, 2025
5eb67a6
Update crates/cdk/src/wallet/keysets.rs
davidcaseria Feb 26, 2025
abdee5d
Address PR feedback
davidcaseria Mar 3, 2025
a26ba5c
Fix clippy
davidcaseria Mar 3, 2025
fc559a8
Merge remote-tracking branch 'upstream/main' into prepared-send
davidcaseria Mar 3, 2025
9cb7ac7
Fix doc to use no_compile
davidcaseria Mar 3, 2025
a19626c
Merge remote-tracking branch 'upstream/main' into prepared-send
davidcaseria Mar 10, 2025
6f4d6e3
Merge remote-tracking branch 'upstream/main' into prepared-send
davidcaseria Mar 10, 2025
f1ccbe9
Add sqlite migration for new proof state
davidcaseria Mar 10, 2025
2fca71a
Change initial migration
davidcaseria Mar 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions crates/cashu/src/amount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ impl Amount {
/// Amount zero
pub const ZERO: Amount = Amount(0);

// Amount one
pub const ONE: Amount = Amount(1);

/// Split into parts that are powers of two
pub fn split(&self) -> Vec<Self> {
let sats = self.0;
Expand Down Expand Up @@ -105,6 +108,27 @@ impl Amount {
Ok(parts)
}

/// Splits amount into powers of two while accounting for the swap fee
pub fn split_with_fee(&self, fee_ppk: u64) -> Result<Vec<Self>, Error> {
let without_fee_amounts = self.split();
let fee_ppk = fee_ppk * without_fee_amounts.len() as u64;
let fee = Amount::from((fee_ppk + 999) / 1000);
let new_amount = self.checked_add(fee).ok_or(Error::AmountOverflow)?;

let split = new_amount.split();
let split_fee_ppk = split.len() as u64 * fee_ppk;
let split_fee = Amount::from((split_fee_ppk + 999) / 1000);

if let Some(net_amount) = new_amount.checked_sub(split_fee) {
if net_amount >= *self {
return Ok(split);
}
}
self.checked_add(Amount::ONE)
.ok_or(Error::AmountOverflow)?
.split_with_fee(fee_ppk)
}

/// Checked addition for Amount. Returns None if overflow occurs.
pub fn checked_add(self, other: Amount) -> Option<Amount> {
self.0.checked_add(other.0).map(Amount)
Expand All @@ -115,6 +139,16 @@ impl Amount {
self.0.checked_sub(other.0).map(Amount)
}

/// Checked multiplication for Amount. Returns None if overflow occurs.
pub fn checked_mul(self, other: Amount) -> Option<Amount> {
self.0.checked_mul(other.0).map(Amount)
}

/// Checked division for Amount. Returns None if overflow occurs.
pub fn checked_div(self, other: Amount) -> Option<Amount> {
self.0.checked_div(other.0).map(Amount)
}

/// Try sum to check for overflow
pub fn try_sum<I>(iter: I) -> Result<Self, Error>
where
Expand Down Expand Up @@ -368,6 +402,27 @@ mod tests {
);
}

#[test]
fn test_split_with_fee() {
let amount = Amount(2);
let fee_ppk = 1;

let split = amount.split_with_fee(fee_ppk).unwrap();
assert_eq!(split, vec![Amount(2), Amount(1)]);

let amount = Amount(3);
let fee_ppk = 1;

let split = amount.split_with_fee(fee_ppk).unwrap();
assert_eq!(split, vec![Amount(4)]);

let amount = Amount(3);
let fee_ppk = 1000;

let split = amount.split_with_fee(fee_ppk).unwrap();
assert_eq!(split, vec![Amount(32)]);
}

#[test]
fn test_split_values() {
let amount = Amount(10);
Expand Down
63 changes: 59 additions & 4 deletions crates/cashu/src/nuts/nut00/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! <https://github.com/cashubtc/nuts/blob/main/00.md>

use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
Expand Down Expand Up @@ -38,6 +39,12 @@ pub type Proofs = Vec<Proof>;

/// Utility methods for [Proofs]
pub trait ProofsMethods {
/// Count proofs by keyset
fn count_by_keyset(&self) -> HashMap<Id, u64>;

/// Sum proofs by keyset
fn sum_by_keyset(&self) -> HashMap<Id, Amount>;

/// Try to sum up the amounts of all [Proof]s
fn total_amount(&self) -> Result<Amount, Error>;

Expand All @@ -46,15 +53,63 @@ pub trait ProofsMethods {
}

impl ProofsMethods for Proofs {
fn count_by_keyset(&self) -> HashMap<Id, u64> {
count_by_keyset(self.iter())
}

fn sum_by_keyset(&self) -> HashMap<Id, Amount> {
sum_by_keyset(self.iter())
}

fn total_amount(&self) -> Result<Amount, Error> {
Amount::try_sum(self.iter().map(|p| p.amount)).map_err(Into::into)
total_amount(self.iter())
}

fn ys(&self) -> Result<Vec<PublicKey>, Error> {
self.iter()
.map(|p| p.y())
.collect::<Result<Vec<PublicKey>, _>>()
ys(self.iter())
}
}

impl ProofsMethods for HashSet<Proof> {
fn count_by_keyset(&self) -> HashMap<Id, u64> {
count_by_keyset(self.iter())
}

fn sum_by_keyset(&self) -> HashMap<Id, Amount> {
sum_by_keyset(self.iter())
}

fn total_amount(&self) -> Result<Amount, Error> {
total_amount(self.iter())
}

fn ys(&self) -> Result<Vec<PublicKey>, Error> {
ys(self.iter())
}
}

fn count_by_keyset<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> HashMap<Id, u64> {
let mut counts = HashMap::new();
for proof in proofs {
*counts.entry(proof.keyset_id).or_insert(0) += 1;
}
counts
}

fn sum_by_keyset<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> HashMap<Id, Amount> {
let mut sums = HashMap::new();
for proof in proofs {
*sums.entry(proof.keyset_id).or_insert(Amount::ZERO) += proof.amount;
}
sums
}

fn total_amount<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> Result<Amount, Error> {
Amount::try_sum(proofs.map(|p| p.amount)).map_err(Into::into)
}

fn ys<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> Result<Vec<PublicKey>, Error> {
proofs.map(|p| p.y()).collect::<Result<Vec<PublicKey>, _>>()
}

/// NUT00 Error
Expand Down
8 changes: 7 additions & 1 deletion crates/cashu/src/nuts/nut01/public_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@ use super::Error;
use crate::SECP256K1;

/// PublicKey
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct PublicKey {
#[cfg_attr(feature = "swagger", schema(value_type = String))]
inner: secp256k1::PublicKey,
}

impl fmt::Debug for PublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PublicKey({})", self.to_hex())
}
}

impl Deref for PublicKey {
type Target = secp256k1::PublicKey;

Expand Down
3 changes: 3 additions & 0 deletions crates/cashu/src/nuts/nut07.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub enum State {
///
/// i.e. used to create a token
Reserved,
/// Pending spent (i.e., spent but not yet swapped by receiver)
PendingSpent,
}

impl fmt::Display for State {
Expand All @@ -44,6 +46,7 @@ impl fmt::Display for State {
Self::Unspent => "UNSPENT",
Self::Pending => "PENDING",
Self::Reserved => "RESERVED",
Self::PendingSpent => "PENDING_SPENT",
};

write!(f, "{}", s)
Expand Down
22 changes: 22 additions & 0 deletions crates/cashu/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,25 @@ pub enum SendKind {
/// Wallet must remain offline but can over pay if below tolerance
OfflineTolerance(Amount),
}

impl SendKind {
/// Check if send kind is online
pub fn is_online(&self) -> bool {
matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
}

/// Check if send kind is offline
pub fn is_offline(&self) -> bool {
matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
}

/// Check if send kind is exact
pub fn is_exact(&self) -> bool {
matches!(self, Self::OnlineExact | Self::OfflineExact)
}

/// Check if send kind has tolerance
pub fn has_tolerance(&self) -> bool {
matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
}
Comment on lines +106 to +108
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.

}
20 changes: 9 additions & 11 deletions crates/cdk-cli/src/sub_commands/pay_request.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use std::io::{self, Write};

use anyhow::{anyhow, Result};
use cdk::amount::SplitTarget;
use cdk::nuts::nut18::TransportType;
use cdk::nuts::{PaymentRequest, PaymentRequestPayload};
use cdk::wallet::{MultiMintWallet, SendKind};
use cdk::wallet::{MultiMintWallet, SendOptions};
use clap::Args;
use nostr_sdk::nips::nip19::Nip19Profile;
use nostr_sdk::{Client as NostrClient, EventBuilder, FromBech32, Keys};
Expand Down Expand Up @@ -81,17 +80,16 @@ pub async fn pay_request(
})
.ok_or(anyhow!("No supported transport method found"))?;

let proofs = matching_wallet
.send(
let prepared_send = matching_wallet
.prepare_send(
amount,
None,
None,
&SplitTarget::default(),
&SendKind::default(),
true,
SendOptions {
include_fee: true,
..Default::default()
},
)
.await?
.proofs();
.await?;
let proofs = matching_wallet.send(prepared_send, None).await?.proofs();

let payload = PaymentRequestPayload {
id: payment_request.payment_id.clone(),
Expand Down
23 changes: 14 additions & 9 deletions crates/cdk-cli/src/sub_commands/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ use std::io::Write;
use std::str::FromStr;

use anyhow::{bail, Result};
use cdk::amount::SplitTarget;
use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
use cdk::wallet::types::{SendKind, WalletKey};
use cdk::wallet::MultiMintWallet;
use cdk::wallet::{MultiMintWallet, SendMemo, SendOptions};
use cdk::Amount;
use clap::Args;

Expand Down Expand Up @@ -170,16 +169,22 @@ pub async fn send(
(false, None) => SendKind::OnlineExact,
};

let token = wallet
.send(
let prepared_send = wallet
.prepare_send(
token_amount,
sub_command_args.memo.clone(),
conditions,
&SplitTarget::default(),
&send_kind,
sub_command_args.include_fee,
SendOptions {
memo: sub_command_args.memo.clone().map(|memo| SendMemo {
memo,
include_memo: true,
}),
send_kind,
include_fee: sub_command_args.include_fee,
conditions,
..Default::default()
},
)
.await?;
let token = wallet.send(prepared_send, None).await?;

match sub_command_args.v3 {
true => {
Expand Down
10 changes: 2 additions & 8 deletions crates/cdk-common/src/database/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,6 @@ pub trait Database: Debug {
added: Vec<ProofInfo>,
removed_ys: Vec<PublicKey>,
) -> Result<(), Self::Err>;
/// 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>;
/// Reserve proofs in storage. Proofs are identified by their Y value.
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.

/// Get proofs from storage
async fn get_proofs(
&self,
Expand All @@ -100,6 +92,8 @@ pub trait Database: Debug {
state: Option<Vec<State>>,
spending_conditions: Option<Vec<SpendingConditions>>,
) -> Result<Vec<ProofInfo>, Self::Err>;
/// Update proofs state in storage
async fn update_proofs_state(&self, ys: Vec<PublicKey>, state: State) -> Result<(), Self::Err>;

/// Increment Keyset counter
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
Expand Down
14 changes: 4 additions & 10 deletions crates/cdk-integration-tests/tests/integration_tests_pure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::assert_eq;

use cdk::amount::SplitTarget;
use cdk::nuts::nut00::ProofsMethods;
use cdk::wallet::SendKind;
use cdk::wallet::SendOptions;
use cdk::Amount;
use cdk_integration_tests::init_pure_tests::{
create_and_start_test_mint, create_test_wallet_for_mint, fund_wallet,
Expand All @@ -19,16 +19,10 @@ async fn test_swap_to_send() -> anyhow::Result<()> {
assert_eq!(Amount::from(64), balance_alice);

// Alice wants to send 40 sats, which internally swaps
let token = wallet_alice
.send(
Amount::from(40),
None,
None,
&SplitTarget::None,
&SendKind::OnlineExact,
false,
)
let prepared_send = wallet_alice
.prepare_send(Amount::from(40), SendOptions::default())
.await?;
let token = wallet_alice.send(prepared_send, None).await?;
assert_eq!(Amount::from(40), token.proofs().total_amount()?);
assert_eq!(Amount::from(24), wallet_alice.total_balance().await?);

Expand Down
Loading
Loading