mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 13:44:55 +01:00
Prepared Send (#596)
Co-authored-by: thesimplekid <tsk@thesimplekid.com> Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
This commit is contained in:
@@ -49,6 +49,9 @@ impl Amount {
|
|||||||
/// Amount zero
|
/// Amount zero
|
||||||
pub const ZERO: Amount = Amount(0);
|
pub const ZERO: Amount = Amount(0);
|
||||||
|
|
||||||
|
// Amount one
|
||||||
|
pub const ONE: Amount = Amount(1);
|
||||||
|
|
||||||
/// Split into parts that are powers of two
|
/// Split into parts that are powers of two
|
||||||
pub fn split(&self) -> Vec<Self> {
|
pub fn split(&self) -> Vec<Self> {
|
||||||
let sats = self.0;
|
let sats = self.0;
|
||||||
@@ -119,6 +122,27 @@ impl Amount {
|
|||||||
Ok(parts)
|
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.
|
/// Checked addition for Amount. Returns None if overflow occurs.
|
||||||
pub fn checked_add(self, other: Amount) -> Option<Amount> {
|
pub fn checked_add(self, other: Amount) -> Option<Amount> {
|
||||||
self.0.checked_add(other.0).map(Amount)
|
self.0.checked_add(other.0).map(Amount)
|
||||||
@@ -129,6 +153,16 @@ impl Amount {
|
|||||||
self.0.checked_sub(other.0).map(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
|
/// Try sum to check for overflow
|
||||||
pub fn try_sum<I>(iter: I) -> Result<Self, Error>
|
pub fn try_sum<I>(iter: I) -> Result<Self, Error>
|
||||||
where
|
where
|
||||||
@@ -334,6 +368,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]
|
#[test]
|
||||||
fn test_split_values() {
|
fn test_split_values() {
|
||||||
let amount = Amount(10);
|
let amount = Amount(10);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
//! <https://github.com/cashubtc/nuts/blob/main/00.md>
|
//! <https://github.com/cashubtc/nuts/blob/main/00.md>
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -38,6 +39,12 @@ pub type Proofs = Vec<Proof>;
|
|||||||
|
|
||||||
/// Utility methods for [Proofs]
|
/// Utility methods for [Proofs]
|
||||||
pub trait ProofsMethods {
|
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
|
/// Try to sum up the amounts of all [Proof]s
|
||||||
fn total_amount(&self) -> Result<Amount, Error>;
|
fn total_amount(&self) -> Result<Amount, Error>;
|
||||||
|
|
||||||
@@ -46,17 +53,65 @@ pub trait ProofsMethods {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ProofsMethods for Proofs {
|
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> {
|
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> {
|
fn ys(&self) -> Result<Vec<PublicKey>, Error> {
|
||||||
self.iter()
|
ys(self.iter())
|
||||||
.map(|p| p.y())
|
|
||||||
.collect::<Result<Vec<PublicKey>, _>>()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
/// NUT00 Error
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@@ -272,6 +327,11 @@ impl Proof {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if proof is in active keyset `Id`s
|
||||||
|
pub fn is_active(&self, active_keyset_ids: &[Id]) -> bool {
|
||||||
|
active_keyset_ids.contains(&self.keyset_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get y from proof
|
/// Get y from proof
|
||||||
///
|
///
|
||||||
/// Where y is `hash_to_curve(secret)`
|
/// Where y is `hash_to_curve(secret)`
|
||||||
|
|||||||
@@ -12,13 +12,19 @@ use super::Error;
|
|||||||
use crate::SECP256K1;
|
use crate::SECP256K1;
|
||||||
|
|
||||||
/// PublicKey
|
/// 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))]
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
pub struct PublicKey {
|
pub struct PublicKey {
|
||||||
#[cfg_attr(feature = "swagger", schema(value_type = String))]
|
#[cfg_attr(feature = "swagger", schema(value_type = String))]
|
||||||
inner: secp256k1::PublicKey,
|
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 {
|
impl Deref for PublicKey {
|
||||||
type Target = secp256k1::PublicKey;
|
type Target = secp256k1::PublicKey;
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,12 @@ pub enum State {
|
|||||||
///
|
///
|
||||||
/// Currently being used in a transaction i.e. melt in progress
|
/// Currently being used in a transaction i.e. melt in progress
|
||||||
Pending,
|
Pending,
|
||||||
/// Proof is reserved
|
/// Reserved
|
||||||
///
|
///
|
||||||
/// i.e. used to create a token
|
/// Proof is reserved for future token creation
|
||||||
Reserved,
|
Reserved,
|
||||||
|
/// Pending spent (i.e., spent but not yet swapped by receiver)
|
||||||
|
PendingSpent,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for State {
|
impl fmt::Display for State {
|
||||||
@@ -44,6 +46,7 @@ impl fmt::Display for State {
|
|||||||
Self::Unspent => "UNSPENT",
|
Self::Unspent => "UNSPENT",
|
||||||
Self::Pending => "PENDING",
|
Self::Pending => "PENDING",
|
||||||
Self::Reserved => "RESERVED",
|
Self::Reserved => "RESERVED",
|
||||||
|
Self::PendingSpent => "PENDING_SPENT",
|
||||||
};
|
};
|
||||||
|
|
||||||
write!(f, "{}", s)
|
write!(f, "{}", s)
|
||||||
@@ -59,6 +62,7 @@ impl FromStr for State {
|
|||||||
"UNSPENT" => Ok(Self::Unspent),
|
"UNSPENT" => Ok(Self::Unspent),
|
||||||
"PENDING" => Ok(Self::Pending),
|
"PENDING" => Ok(Self::Pending),
|
||||||
"RESERVED" => Ok(Self::Reserved),
|
"RESERVED" => Ok(Self::Reserved),
|
||||||
|
"PENDING_SPENT" => Ok(Self::PendingSpent),
|
||||||
_ => Err(Error::UnknownState),
|
_ => Err(Error::UnknownState),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,3 +85,25 @@ pub enum SendKind {
|
|||||||
/// Wallet must remain offline but can over pay if below tolerance
|
/// Wallet must remain offline but can over pay if below tolerance
|
||||||
OfflineTolerance(Amount),
|
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(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use cdk::amount::SplitTarget;
|
|
||||||
use cdk::nuts::nut18::TransportType;
|
use cdk::nuts::nut18::TransportType;
|
||||||
use cdk::nuts::{PaymentRequest, PaymentRequestPayload};
|
use cdk::nuts::{PaymentRequest, PaymentRequestPayload};
|
||||||
use cdk::wallet::{MultiMintWallet, SendKind};
|
use cdk::wallet::{MultiMintWallet, SendOptions};
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use nostr_sdk::nips::nip19::Nip19Profile;
|
use nostr_sdk::nips::nip19::Nip19Profile;
|
||||||
use nostr_sdk::{Client as NostrClient, EventBuilder, FromBech32, Keys};
|
use nostr_sdk::{Client as NostrClient, EventBuilder, FromBech32, Keys};
|
||||||
@@ -81,17 +80,16 @@ pub async fn pay_request(
|
|||||||
})
|
})
|
||||||
.ok_or(anyhow!("No supported transport method found"))?;
|
.ok_or(anyhow!("No supported transport method found"))?;
|
||||||
|
|
||||||
let proofs = matching_wallet
|
let prepared_send = matching_wallet
|
||||||
.send(
|
.prepare_send(
|
||||||
amount,
|
amount,
|
||||||
None,
|
SendOptions {
|
||||||
None,
|
include_fee: true,
|
||||||
&SplitTarget::default(),
|
..Default::default()
|
||||||
&SendKind::default(),
|
},
|
||||||
true,
|
|
||||||
)
|
)
|
||||||
.await?
|
.await?;
|
||||||
.proofs();
|
let proofs = matching_wallet.send(prepared_send, None).await?.proofs();
|
||||||
|
|
||||||
let payload = PaymentRequestPayload {
|
let payload = PaymentRequestPayload {
|
||||||
id: payment_request.payment_id.clone(),
|
id: payment_request.payment_id.clone(),
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ use std::io::Write;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use cdk::amount::SplitTarget;
|
|
||||||
use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
|
use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
|
||||||
use cdk::wallet::types::{SendKind, WalletKey};
|
use cdk::wallet::types::{SendKind, WalletKey};
|
||||||
use cdk::wallet::MultiMintWallet;
|
use cdk::wallet::{MultiMintWallet, SendMemo, SendOptions};
|
||||||
use cdk::Amount;
|
use cdk::Amount;
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
@@ -170,16 +169,22 @@ pub async fn send(
|
|||||||
(false, None) => SendKind::OnlineExact,
|
(false, None) => SendKind::OnlineExact,
|
||||||
};
|
};
|
||||||
|
|
||||||
let token = wallet
|
let prepared_send = wallet
|
||||||
.send(
|
.prepare_send(
|
||||||
token_amount,
|
token_amount,
|
||||||
sub_command_args.memo.clone(),
|
SendOptions {
|
||||||
|
memo: sub_command_args.memo.clone().map(|memo| SendMemo {
|
||||||
|
memo,
|
||||||
|
include_memo: true,
|
||||||
|
}),
|
||||||
|
send_kind,
|
||||||
|
include_fee: sub_command_args.include_fee,
|
||||||
conditions,
|
conditions,
|
||||||
&SplitTarget::default(),
|
..Default::default()
|
||||||
&send_kind,
|
},
|
||||||
sub_command_args.include_fee,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
let token = wallet.send(prepared_send, None).await?;
|
||||||
|
|
||||||
match sub_command_args.v3 {
|
match sub_command_args.v3 {
|
||||||
true => {
|
true => {
|
||||||
|
|||||||
@@ -84,14 +84,6 @@ pub trait Database: Debug {
|
|||||||
added: Vec<ProofInfo>,
|
added: Vec<ProofInfo>,
|
||||||
removed_ys: Vec<PublicKey>,
|
removed_ys: Vec<PublicKey>,
|
||||||
) -> Result<(), Self::Err>;
|
) -> 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>;
|
|
||||||
/// Get proofs from storage
|
/// Get proofs from storage
|
||||||
async fn get_proofs(
|
async fn get_proofs(
|
||||||
&self,
|
&self,
|
||||||
@@ -100,6 +92,8 @@ pub trait Database: Debug {
|
|||||||
state: Option<Vec<State>>,
|
state: Option<Vec<State>>,
|
||||||
spending_conditions: Option<Vec<SpendingConditions>>,
|
spending_conditions: Option<Vec<SpendingConditions>>,
|
||||||
) -> Result<Vec<ProofInfo>, Self::Err>;
|
) -> 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
|
/// Increment Keyset counter
|
||||||
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
|
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ pub enum Error {
|
|||||||
/// Insufficient Funds
|
/// Insufficient Funds
|
||||||
#[error("Insufficient funds")]
|
#[error("Insufficient funds")]
|
||||||
InsufficientFunds,
|
InsufficientFunds,
|
||||||
|
/// Unexpected proof state
|
||||||
|
#[error("Unexpected proof state")]
|
||||||
|
UnexpectedProofState,
|
||||||
/// No active keyset
|
/// No active keyset
|
||||||
#[error("No active keyset")]
|
#[error("No active keyset")]
|
||||||
NoActiveKeyset,
|
NoActiveKeyset,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use std::assert_eq;
|
use std::assert_eq;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::hash::RandomState;
|
||||||
|
|
||||||
use cdk::amount::SplitTarget;
|
use cdk::amount::SplitTarget;
|
||||||
use cdk::nuts::nut00::ProofsMethods;
|
use cdk::nuts::nut00::ProofsMethods;
|
||||||
use cdk::wallet::SendKind;
|
use cdk::wallet::SendOptions;
|
||||||
use cdk::Amount;
|
use cdk::Amount;
|
||||||
use cdk_integration_tests::init_pure_tests::*;
|
use cdk_integration_tests::init_pure_tests::*;
|
||||||
|
|
||||||
@@ -18,18 +20,20 @@ async fn test_swap_to_send() -> anyhow::Result<()> {
|
|||||||
assert_eq!(Amount::from(64), balance_alice);
|
assert_eq!(Amount::from(64), balance_alice);
|
||||||
|
|
||||||
// Alice wants to send 40 sats, which internally swaps
|
// Alice wants to send 40 sats, which internally swaps
|
||||||
let token = wallet_alice
|
let prepared_send = wallet_alice
|
||||||
.send(
|
.prepare_send(Amount::from(40), SendOptions::default())
|
||||||
Amount::from(40),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
&SplitTarget::None,
|
|
||||||
&SendKind::OnlineExact,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
HashSet::<_, RandomState>::from_iter(prepared_send.proofs().ys()?),
|
||||||
|
HashSet::from_iter(wallet_alice.get_reserved_proofs().await?.ys()?)
|
||||||
|
);
|
||||||
|
let token = wallet_alice.send(prepared_send, None).await?;
|
||||||
assert_eq!(Amount::from(40), token.proofs().total_amount()?);
|
assert_eq!(Amount::from(40), token.proofs().total_amount()?);
|
||||||
assert_eq!(Amount::from(24), wallet_alice.total_balance().await?);
|
assert_eq!(Amount::from(24), wallet_alice.total_balance().await?);
|
||||||
|
assert_eq!(
|
||||||
|
HashSet::<_, RandomState>::from_iter(token.proofs().ys()?),
|
||||||
|
HashSet::from_iter(wallet_alice.get_pending_spent_proofs().await?.ys()?)
|
||||||
|
);
|
||||||
|
|
||||||
// Alice sends cashu, Carol receives
|
// Alice sends cashu, Carol receives
|
||||||
let wallet_carol = create_test_wallet_arc_for_mint(mint_bob.clone()).await?;
|
let wallet_carol = create_test_wallet_arc_for_mint(mint_bob.clone()).await?;
|
||||||
|
|||||||
@@ -145,46 +145,6 @@ impl WalletRedbDatabase {
|
|||||||
|
|
||||||
Ok(Self { db: Arc::new(db) })
|
Ok(Self { db: Arc::new(db) })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_proof_states(
|
|
||||||
&self,
|
|
||||||
ys: Vec<PublicKey>,
|
|
||||||
state: State,
|
|
||||||
) -> Result<(), database::Error> {
|
|
||||||
let read_txn = self.db.begin_read().map_err(Error::from)?;
|
|
||||||
let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
|
|
||||||
|
|
||||||
let write_txn = self.db.begin_write().map_err(Error::from)?;
|
|
||||||
|
|
||||||
for y in ys {
|
|
||||||
let y_slice = y.to_bytes();
|
|
||||||
let proof = table
|
|
||||||
.get(y_slice.as_slice())
|
|
||||||
.map_err(Error::from)?
|
|
||||||
.ok_or(Error::UnknownY)?;
|
|
||||||
|
|
||||||
let mut proof_info =
|
|
||||||
serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
|
|
||||||
|
|
||||||
proof_info.state = state;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
|
|
||||||
table
|
|
||||||
.insert(
|
|
||||||
y_slice.as_slice(),
|
|
||||||
serde_json::to_string(&proof_info)
|
|
||||||
.map_err(Error::from)?
|
|
||||||
.as_str(),
|
|
||||||
)
|
|
||||||
.map_err(Error::from)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
write_txn.commit().map_err(Error::from)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -611,21 +571,6 @@ impl WalletDatabase for WalletRedbDatabase {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, ys))]
|
|
||||||
async fn set_pending_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
|
|
||||||
self.update_proof_states(ys, State::Pending).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self, ys))]
|
|
||||||
async fn reserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
|
|
||||||
self.update_proof_states(ys, State::Reserved).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self, ys))]
|
|
||||||
async fn set_unspent_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
|
|
||||||
self.update_proof_states(ys, State::Unspent).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
async fn get_proofs(
|
async fn get_proofs(
|
||||||
&self,
|
&self,
|
||||||
@@ -659,6 +604,46 @@ impl WalletDatabase for WalletRedbDatabase {
|
|||||||
Ok(proofs)
|
Ok(proofs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_proofs_state(
|
||||||
|
&self,
|
||||||
|
ys: Vec<PublicKey>,
|
||||||
|
state: State,
|
||||||
|
) -> Result<(), database::Error> {
|
||||||
|
let read_txn = self.db.begin_read().map_err(Error::from)?;
|
||||||
|
let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
|
||||||
|
|
||||||
|
let write_txn = self.db.begin_write().map_err(Error::from)?;
|
||||||
|
|
||||||
|
for y in ys {
|
||||||
|
let y_slice = y.to_bytes();
|
||||||
|
let proof = table
|
||||||
|
.get(y_slice.as_slice())
|
||||||
|
.map_err(Error::from)?
|
||||||
|
.ok_or(Error::UnknownY)?;
|
||||||
|
|
||||||
|
let mut proof_info =
|
||||||
|
serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
|
||||||
|
|
||||||
|
proof_info.state = state;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
|
||||||
|
table
|
||||||
|
.insert(
|
||||||
|
y_slice.as_slice(),
|
||||||
|
serde_json::to_string(&proof_info)
|
||||||
|
.map_err(Error::from)?
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_txn.commit().map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self), fields(keyset_id = %keyset_id))]
|
#[instrument(skip(self), fields(keyset_id = %keyset_id))]
|
||||||
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
|
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
|
||||||
let write_txn = self.db.begin_write().map_err(Error::from)?;
|
let write_txn = self.db.begin_write().map_err(Error::from)?;
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-- Create a new table with the updated CHECK constraint
|
||||||
|
CREATE TABLE IF NOT EXISTS proof_new (
|
||||||
|
y BLOB PRIMARY KEY,
|
||||||
|
mint_url TEXT NOT NULL,
|
||||||
|
state TEXT CHECK ( state IN ('SPENT', 'UNSPENT', 'PENDING', 'RESERVED', 'PENDING_SPENT' ) ) NOT NULL,
|
||||||
|
spending_condition TEXT,
|
||||||
|
unit TEXT NOT NULL,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
keyset_id TEXT NOT NULL,
|
||||||
|
secret TEXT NOT NULL,
|
||||||
|
c BLOB NOT NULL,
|
||||||
|
witness TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS secret_index ON proof_new(secret);
|
||||||
|
CREATE INDEX IF NOT EXISTS state_index ON proof_new(state);
|
||||||
|
CREATE INDEX IF NOT EXISTS spending_condition_index ON proof_new(spending_condition);
|
||||||
|
CREATE INDEX IF NOT EXISTS unit_index ON proof_new(unit);
|
||||||
|
CREATE INDEX IF NOT EXISTS amount_index ON proof_new(amount);
|
||||||
|
CREATE INDEX IF NOT EXISTS mint_url_index ON proof_new(mint_url);
|
||||||
|
|
||||||
|
-- Copy data from old proof table to new proof table
|
||||||
|
INSERT INTO proof_new (y, mint_url, state, spending_condition, unit, amount, keyset_id, secret, c, witness)
|
||||||
|
SELECT y, mint_url, state, spending_condition, unit, amount, keyset_id, secret, c, witness
|
||||||
|
FROM proof;
|
||||||
|
|
||||||
|
-- Drop the old proof table
|
||||||
|
DROP TABLE proof;
|
||||||
|
|
||||||
|
-- Rename the new proof table to proof
|
||||||
|
ALTER TABLE proof_new RENAME TO proof;
|
||||||
@@ -59,23 +59,6 @@ impl WalletSqliteDatabase {
|
|||||||
.await
|
.await
|
||||||
.expect("Could not run migrations");
|
.expect("Could not run migrations");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), database::Error> {
|
|
||||||
sqlx::query(
|
|
||||||
r#"
|
|
||||||
UPDATE proof
|
|
||||||
SET state=?
|
|
||||||
WHERE y IS ?;
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(state.to_string())
|
|
||||||
.bind(y.to_bytes().to_vec())
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(Error::from)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -658,30 +641,6 @@ WHERE id=?
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_pending_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
|
|
||||||
for y in ys {
|
|
||||||
self.set_proof_state(y, State::Pending).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn reserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
|
|
||||||
for y in ys {
|
|
||||||
self.set_proof_state(y, State::Reserved).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_unspent_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
|
|
||||||
for y in ys {
|
|
||||||
self.set_proof_state(y, State::Unspent).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(self, state, spending_conditions))]
|
#[instrument(skip(self, state, spending_conditions))]
|
||||||
async fn get_proofs(
|
async fn get_proofs(
|
||||||
&self,
|
&self,
|
||||||
@@ -734,6 +693,31 @@ FROM proof;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_proofs_state(&self, ys: Vec<PublicKey>, state: State) -> Result<(), Self::Err> {
|
||||||
|
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
|
||||||
|
|
||||||
|
let update_sql = format!(
|
||||||
|
"UPDATE proof SET state = ? WHERE y IN ({})",
|
||||||
|
"?,".repeat(ys.len()).trim_end_matches(',')
|
||||||
|
);
|
||||||
|
|
||||||
|
ys.iter()
|
||||||
|
.fold(
|
||||||
|
sqlx::query(&update_sql).bind(state.to_string()),
|
||||||
|
|query, y| query.bind(y.to_bytes().to_vec()),
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!("SQLite could not update proof state: {err:?}");
|
||||||
|
Error::SQLX(err)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
transaction.commit().await.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self), fields(keyset_id = %keyset_id))]
|
#[instrument(skip(self), fields(keyset_id = %keyset_id))]
|
||||||
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
|
async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
|
||||||
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
|
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
|
||||||
|
|||||||
@@ -4,14 +4,23 @@ use cdk::amount::SplitTarget;
|
|||||||
use cdk::error::Error;
|
use cdk::error::Error;
|
||||||
use cdk::nuts::nut00::ProofsMethods;
|
use cdk::nuts::nut00::ProofsMethods;
|
||||||
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload};
|
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload};
|
||||||
use cdk::wallet::types::SendKind;
|
use cdk::wallet::{SendOptions, Wallet, WalletSubscription};
|
||||||
use cdk::wallet::{Wallet, WalletSubscription};
|
|
||||||
use cdk::Amount;
|
use cdk::Amount;
|
||||||
use cdk_sqlite::wallet::memory;
|
use cdk_sqlite::wallet::memory;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Error> {
|
async fn main() -> Result<(), Error> {
|
||||||
|
let default_filter = "debug";
|
||||||
|
|
||||||
|
let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn,rustls=warn";
|
||||||
|
|
||||||
|
let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter));
|
||||||
|
|
||||||
|
// Parse input
|
||||||
|
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||||
|
|
||||||
// Initialize the memory store for the wallet
|
// Initialize the memory store for the wallet
|
||||||
let localstore = memory::empty().await?;
|
let localstore = memory::empty().await?;
|
||||||
|
|
||||||
@@ -52,16 +61,8 @@ async fn main() -> Result<(), Error> {
|
|||||||
println!("Received {} from mint {}", receive_amount, mint_url);
|
println!("Received {} from mint {}", receive_amount, mint_url);
|
||||||
|
|
||||||
// Send a token with the specified amount
|
// Send a token with the specified amount
|
||||||
let token = wallet
|
let prepared_send = wallet.prepare_send(amount, SendOptions::default()).await?;
|
||||||
.send(
|
let token = wallet.send(prepared_send, None).await?;
|
||||||
amount,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
&SplitTarget::default(),
|
|
||||||
&SendKind::OnlineExact,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
println!("Token:");
|
println!("Token:");
|
||||||
println!("{}", token);
|
println!("{}", token);
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ use std::sync::Arc;
|
|||||||
use cdk::amount::SplitTarget;
|
use cdk::amount::SplitTarget;
|
||||||
use cdk::error::Error;
|
use cdk::error::Error;
|
||||||
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload, SecretKey, SpendingConditions};
|
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload, SecretKey, SpendingConditions};
|
||||||
use cdk::wallet::types::SendKind;
|
use cdk::wallet::{SendOptions, Wallet, WalletSubscription};
|
||||||
use cdk::wallet::{Wallet, WalletSubscription};
|
|
||||||
use cdk::Amount;
|
use cdk::Amount;
|
||||||
use cdk_sqlite::wallet::memory;
|
use cdk_sqlite::wallet::memory;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
@@ -30,10 +29,10 @@ async fn main() -> Result<(), Error> {
|
|||||||
// Define the mint URL and currency unit
|
// Define the mint URL and currency unit
|
||||||
let mint_url = "https://testnut.cashu.space";
|
let mint_url = "https://testnut.cashu.space";
|
||||||
let unit = CurrencyUnit::Sat;
|
let unit = CurrencyUnit::Sat;
|
||||||
let amount = Amount::from(50);
|
let amount = Amount::from(100);
|
||||||
|
|
||||||
// Create a new wallet
|
// Create a new wallet
|
||||||
let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?;
|
let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, Some(1))?;
|
||||||
|
|
||||||
// Request a mint quote from the wallet
|
// Request a mint quote from the wallet
|
||||||
let quote = wallet.mint_quote(amount, None).await?;
|
let quote = wallet.mint_quote(amount, None).await?;
|
||||||
@@ -57,7 +56,14 @@ async fn main() -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mint the received amount
|
// Mint the received amount
|
||||||
let _receive_amount = wallet.mint("e.id, SplitTarget::default(), None).await?;
|
let received_proofs = wallet.mint("e.id, SplitTarget::default(), None).await?;
|
||||||
|
println!(
|
||||||
|
"Minted nuts: {:?}",
|
||||||
|
received_proofs
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| p.amount)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
// Generate a secret key for spending conditions
|
// Generate a secret key for spending conditions
|
||||||
let secret = SecretKey::generate();
|
let secret = SecretKey::generate();
|
||||||
@@ -67,19 +73,21 @@ async fn main() -> Result<(), Error> {
|
|||||||
|
|
||||||
// Get the total balance of the wallet
|
// Get the total balance of the wallet
|
||||||
let bal = wallet.total_balance().await?;
|
let bal = wallet.total_balance().await?;
|
||||||
println!("{}", bal);
|
println!("Total balance: {}", bal);
|
||||||
|
|
||||||
// Send a token with the specified amount and spending conditions
|
// Send a token with the specified amount and spending conditions
|
||||||
let token = wallet
|
let prepared_send = wallet
|
||||||
.send(
|
.prepare_send(
|
||||||
10.into(),
|
10.into(),
|
||||||
None,
|
SendOptions {
|
||||||
Some(spending_conditions),
|
conditions: Some(spending_conditions),
|
||||||
&SplitTarget::default(),
|
include_fee: true,
|
||||||
&SendKind::default(),
|
..Default::default()
|
||||||
false,
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
println!("Fee: {}", prepared_send.fee());
|
||||||
|
let token = wallet.send(prepared_send, None).await?;
|
||||||
|
|
||||||
println!("Created token locked to pubkey: {}", secret.public_key());
|
println!("Created token locked to pubkey: {}", secret.public_key());
|
||||||
println!("{}", token);
|
println!("{}", token);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Wallet example with memory store
|
//! Wallet example with memory store
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use cdk::amount::SplitTarget;
|
use cdk::amount::SplitTarget;
|
||||||
@@ -59,9 +60,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let proofs = wallet.get_unspent_proofs().await?;
|
let proofs = wallet.get_unspent_proofs().await?;
|
||||||
|
|
||||||
// Select proofs to send
|
// Select proofs to send
|
||||||
let selected = wallet
|
let amount = Amount::from(64);
|
||||||
.select_proofs_to_send(Amount::from(64), proofs, false)
|
let active_keyset_ids = wallet
|
||||||
.await?;
|
.get_active_mint_keysets()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|keyset| keyset.id)
|
||||||
|
.collect();
|
||||||
|
let selected =
|
||||||
|
Wallet::select_proofs(amount, proofs, &active_keyset_ids, &HashMap::new(), false)?;
|
||||||
for (i, proof) in selected.iter().enumerate() {
|
for (i, proof) in selected.iter().enumerate() {
|
||||||
println!("{}: {}", i, proof.amount);
|
println!("{}: {}", i, proof.amount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ use std::time::Duration;
|
|||||||
use cdk::amount::SplitTarget;
|
use cdk::amount::SplitTarget;
|
||||||
use cdk::nuts::nut00::ProofsMethods;
|
use cdk::nuts::nut00::ProofsMethods;
|
||||||
use cdk::nuts::{CurrencyUnit, MintQuoteState};
|
use cdk::nuts::{CurrencyUnit, MintQuoteState};
|
||||||
use cdk::wallet::types::SendKind;
|
use cdk::wallet::{SendOptions, Wallet};
|
||||||
use cdk::wallet::Wallet;
|
|
||||||
use cdk::Amount;
|
use cdk::Amount;
|
||||||
use cdk_sqlite::wallet::memory;
|
use cdk_sqlite::wallet::memory;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
@@ -59,16 +58,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!("Minted {}", receive_amount);
|
println!("Minted {}", receive_amount);
|
||||||
|
|
||||||
// Send the token
|
// Send the token
|
||||||
let token = wallet
|
let prepared_send = wallet.prepare_send(amount, SendOptions::default()).await?;
|
||||||
.send(
|
let token = wallet.send(prepared_send, None).await?;
|
||||||
amount,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
&SplitTarget::None,
|
|
||||||
&SendKind::default(),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("{}", token);
|
println!("{}", token);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::nuts::{Id, KeySetInfo, Keys};
|
use crate::nuts::{Id, KeySetInfo, Keys};
|
||||||
@@ -99,4 +101,29 @@ impl Wallet {
|
|||||||
.ok_or(Error::NoActiveKeyset)?;
|
.ok_or(Error::NoActiveKeyset)?;
|
||||||
Ok(keyset_with_lowest_fee)
|
Ok(keyset_with_lowest_fee)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get keyset fees for mint
|
||||||
|
pub async fn get_keyset_fees(&self) -> Result<HashMap<Id, u64>, Error> {
|
||||||
|
let keysets = self
|
||||||
|
.localstore
|
||||||
|
.get_mint_keysets(self.mint_url.clone())
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::UnknownKeySet)?;
|
||||||
|
|
||||||
|
let mut fees = HashMap::new();
|
||||||
|
for keyset in keysets {
|
||||||
|
fees.insert(keyset.id, keyset.input_fee_ppk);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(fees)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get keyset fees for mint by keyset id
|
||||||
|
pub async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Error> {
|
||||||
|
self.get_keyset_fees()
|
||||||
|
.await?
|
||||||
|
.get(&keyset_id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or(Error::UnknownKeySet)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let ys = proofs.ys()?;
|
let ys = proofs.ys()?;
|
||||||
self.localstore.set_pending_proofs(ys).await?;
|
self.localstore
|
||||||
|
.update_proofs_state(ys, State::Pending)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
||||||
|
|
||||||
@@ -287,9 +289,20 @@ impl Wallet {
|
|||||||
|
|
||||||
let available_proofs = self.get_unspent_proofs().await?;
|
let available_proofs = self.get_unspent_proofs().await?;
|
||||||
|
|
||||||
let input_proofs = self
|
let active_keyset_ids = self
|
||||||
.select_proofs_to_swap(inputs_needed_amount, available_proofs)
|
.get_active_mint_keysets()
|
||||||
.await?;
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|k| k.id)
|
||||||
|
.collect();
|
||||||
|
let keyset_fees = self.get_keyset_fees().await?;
|
||||||
|
let input_proofs = Wallet::select_proofs(
|
||||||
|
inputs_needed_amount,
|
||||||
|
available_proofs,
|
||||||
|
&active_keyset_ids,
|
||||||
|
&keyset_fees,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
|
||||||
self.melt_proofs(quote_id, input_proofs).await
|
self.melt_proofs(quote_id, input_proofs).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ mod swap;
|
|||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
pub use cdk_common::wallet as types;
|
pub use cdk_common::wallet as types;
|
||||||
|
pub use send::{PreparedSend, SendMemo, SendOptions};
|
||||||
|
|
||||||
use crate::nuts::nut00::ProofsMethods;
|
use crate::nuts::nut00::ProofsMethods;
|
||||||
|
|
||||||
@@ -173,25 +174,24 @@ impl Wallet {
|
|||||||
/// Fee required for proof set
|
/// Fee required for proof set
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn get_proofs_fee(&self, proofs: &Proofs) -> Result<Amount, Error> {
|
pub async fn get_proofs_fee(&self, proofs: &Proofs) -> Result<Amount, Error> {
|
||||||
let mut proofs_per_keyset = HashMap::new();
|
let proofs_per_keyset = proofs.count_by_keyset();
|
||||||
let mut fee_per_keyset = HashMap::new();
|
self.get_proofs_fee_by_count(proofs_per_keyset).await
|
||||||
|
|
||||||
for proof in proofs {
|
|
||||||
if let std::collections::hash_map::Entry::Vacant(e) =
|
|
||||||
fee_per_keyset.entry(proof.keyset_id)
|
|
||||||
{
|
|
||||||
let mint_keyset_info = self
|
|
||||||
.localstore
|
|
||||||
.get_keyset_by_id(&proof.keyset_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(Error::UnknownKeySet)?;
|
|
||||||
e.insert(mint_keyset_info.input_fee_ppk);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
proofs_per_keyset
|
/// Fee required for proof set by count
|
||||||
.entry(proof.keyset_id)
|
pub async fn get_proofs_fee_by_count(
|
||||||
.and_modify(|count| *count += 1)
|
&self,
|
||||||
.or_insert(1);
|
proofs_per_keyset: HashMap<Id, u64>,
|
||||||
|
) -> Result<Amount, Error> {
|
||||||
|
let mut fee_per_keyset = HashMap::new();
|
||||||
|
|
||||||
|
for keyset_id in proofs_per_keyset.keys() {
|
||||||
|
let mint_keyset_info = self
|
||||||
|
.localstore
|
||||||
|
.get_keyset_by_id(keyset_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::UnknownKeySet)?;
|
||||||
|
fee_per_keyset.insert(*keyset_id, mint_keyset_info.input_fee_ppk);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fee = calculate_fee(&proofs_per_keyset, &fee_per_keyset)?;
|
let fee = calculate_fee(&proofs_per_keyset, &fee_per_keyset)?;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use cdk_common::wallet::WalletKey;
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use super::types::SendKind;
|
use super::send::{PreparedSend, SendMemo, SendOptions};
|
||||||
use super::Error;
|
use super::Error;
|
||||||
use crate::amount::SplitTarget;
|
use crate::amount::SplitTarget;
|
||||||
use crate::mint_url::MintUrl;
|
use crate::mint_url::MintUrl;
|
||||||
@@ -110,32 +110,36 @@ impl MultiMintWallet {
|
|||||||
Ok(mint_proofs)
|
Ok(mint_proofs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepare to send
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn prepare_send(
|
||||||
|
&self,
|
||||||
|
wallet_key: &WalletKey,
|
||||||
|
amount: Amount,
|
||||||
|
opts: SendOptions,
|
||||||
|
) -> Result<PreparedSend, Error> {
|
||||||
|
let wallet = self
|
||||||
|
.get_wallet(wallet_key)
|
||||||
|
.await
|
||||||
|
.ok_or(Error::UnknownWallet(wallet_key.clone()))?;
|
||||||
|
|
||||||
|
wallet.prepare_send(amount, opts).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Create cashu token
|
/// Create cashu token
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn send(
|
pub async fn send(
|
||||||
&self,
|
&self,
|
||||||
wallet_key: &WalletKey,
|
wallet_key: &WalletKey,
|
||||||
amount: Amount,
|
send: PreparedSend,
|
||||||
memo: Option<String>,
|
memo: Option<SendMemo>,
|
||||||
conditions: Option<SpendingConditions>,
|
|
||||||
send_kind: SendKind,
|
|
||||||
include_fees: bool,
|
|
||||||
) -> Result<Token, Error> {
|
) -> Result<Token, Error> {
|
||||||
let wallet = self
|
let wallet = self
|
||||||
.get_wallet(wallet_key)
|
.get_wallet(wallet_key)
|
||||||
.await
|
.await
|
||||||
.ok_or(Error::UnknownWallet(wallet_key.clone()))?;
|
.ok_or(Error::UnknownWallet(wallet_key.clone()))?;
|
||||||
|
|
||||||
wallet
|
wallet.send(send, memo).await
|
||||||
.send(
|
|
||||||
amount,
|
|
||||||
memo,
|
|
||||||
conditions,
|
|
||||||
&SplitTarget::default(),
|
|
||||||
&send_kind,
|
|
||||||
include_fees,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mint quote for wallet
|
/// Mint quote for wallet
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use cdk_common::Id;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::amount::SplitTarget;
|
use crate::amount::SplitTarget;
|
||||||
|
use crate::fees::calculate_fee;
|
||||||
use crate::nuts::nut00::ProofsMethods;
|
use crate::nuts::nut00::ProofsMethods;
|
||||||
use crate::nuts::{
|
use crate::nuts::{
|
||||||
CheckStateRequest, Proof, ProofState, Proofs, PublicKey, SpendingConditions, State,
|
CheckStateRequest, Proof, ProofState, Proofs, PublicKey, SpendingConditions, State,
|
||||||
@@ -30,6 +32,13 @@ impl Wallet {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get pending spent [`Proofs`]
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_pending_spent_proofs(&self) -> Result<Proofs, Error> {
|
||||||
|
self.get_proofs_with(Some(vec![State::PendingSpent]), None)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Get this wallet's [Proofs] that match the args
|
/// Get this wallet's [Proofs] that match the args
|
||||||
pub async fn get_proofs_with(
|
pub async fn get_proofs_with(
|
||||||
&self,
|
&self,
|
||||||
@@ -53,7 +62,10 @@ impl Wallet {
|
|||||||
/// Return proofs to unspent allowing them to be selected and spent
|
/// Return proofs to unspent allowing them to be selected and spent
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn unreserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Error> {
|
pub async fn unreserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Error> {
|
||||||
Ok(self.localstore.set_unspent_proofs(ys).await?)
|
Ok(self
|
||||||
|
.localstore
|
||||||
|
.update_proofs_state(ys, State::Unspent)
|
||||||
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reclaim unspent proofs
|
/// Reclaim unspent proofs
|
||||||
@@ -112,7 +124,7 @@ impl Wallet {
|
|||||||
.get_proofs(
|
.get_proofs(
|
||||||
Some(self.mint_url.clone()),
|
Some(self.mint_url.clone()),
|
||||||
Some(self.unit.clone()),
|
Some(self.unit.clone()),
|
||||||
Some(vec![State::Pending, State::Reserved]),
|
Some(vec![State::Pending, State::Reserved, State::PendingSpent]),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -153,123 +165,375 @@ impl Wallet {
|
|||||||
Ok(balance)
|
Ok(balance)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select proofs to send
|
/// Select proofs
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn select_proofs_to_send(
|
pub fn select_proofs(
|
||||||
&self,
|
|
||||||
amount: Amount,
|
amount: Amount,
|
||||||
proofs: Proofs,
|
proofs: Proofs,
|
||||||
|
active_keyset_ids: &Vec<Id>,
|
||||||
|
keyset_fees: &HashMap<Id, u64>,
|
||||||
include_fees: bool,
|
include_fees: bool,
|
||||||
) -> Result<Proofs, Error> {
|
) -> Result<Proofs, Error> {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Selecting proofs to send {} from {}",
|
"amount={}, proofs={:?}",
|
||||||
amount,
|
amount,
|
||||||
proofs.total_amount()?
|
proofs.iter().map(|p| p.amount.into()).collect::<Vec<u64>>()
|
||||||
);
|
);
|
||||||
|
if amount == Amount::ZERO {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
ensure_cdk!(proofs.total_amount()? >= amount, Error::InsufficientFunds);
|
ensure_cdk!(proofs.total_amount()? >= amount, Error::InsufficientFunds);
|
||||||
|
|
||||||
let (mut proofs_larger, mut proofs_smaller): (Proofs, Proofs) =
|
// Sort proofs in descending order
|
||||||
proofs.into_iter().partition(|p| p.amount > amount);
|
let mut proofs = proofs;
|
||||||
|
proofs.sort_by(|a, b| a.cmp(b).reverse());
|
||||||
|
|
||||||
let next_bigger_proof = proofs_larger.first().cloned();
|
// Split the amount into optimal amounts
|
||||||
|
let optimal_amounts = amount.split();
|
||||||
|
|
||||||
let mut selected_proofs: Proofs = Vec::new();
|
// Track selected proofs and remaining amounts (include all inactive proofs first)
|
||||||
let mut remaining_amount = amount;
|
let mut selected_proofs: HashSet<Proof> = proofs
|
||||||
|
.iter()
|
||||||
|
.filter(|p| !p.is_active(active_keyset_ids))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
if selected_proofs.total_amount()? >= amount {
|
||||||
|
tracing::debug!("All inactive proofs are sufficient");
|
||||||
|
return Ok(selected_proofs.into_iter().collect());
|
||||||
|
}
|
||||||
|
let mut remaining_amounts: Vec<Amount> = Vec::new();
|
||||||
|
|
||||||
while remaining_amount > Amount::ZERO {
|
// Select proof with the exact amount and not already selected
|
||||||
proofs_larger.sort();
|
let mut select_proof = |proofs: &Proofs, amount: Amount, exact: bool| -> bool {
|
||||||
// Sort smaller proofs in descending order
|
let mut last_proof = None;
|
||||||
proofs_smaller.sort_by(|a: &Proof, b: &Proof| b.cmp(a));
|
for proof in proofs.iter() {
|
||||||
|
if !selected_proofs.contains(proof) {
|
||||||
|
if proof.amount == amount {
|
||||||
|
selected_proofs.insert(proof.clone());
|
||||||
|
return true;
|
||||||
|
} else if !exact && proof.amount > amount {
|
||||||
|
last_proof = Some(proof.clone());
|
||||||
|
} else if proof.amount < amount {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(proof) = last_proof {
|
||||||
|
selected_proofs.insert(proof);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let selected_proof = if let Some(next_small) = proofs_smaller.clone().first() {
|
// Select proofs with the optimal amounts
|
||||||
next_small.clone()
|
for optimal_amount in optimal_amounts {
|
||||||
} else if let Some(next_bigger) = proofs_larger.first() {
|
if !select_proof(&proofs, optimal_amount, true) {
|
||||||
next_bigger.clone()
|
// Add the remaining amount to the remaining amounts because proof with the optimal amount was not found
|
||||||
|
remaining_amounts.push(optimal_amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all the optimal amounts are selected, return the selected proofs
|
||||||
|
if remaining_amounts.is_empty() {
|
||||||
|
tracing::debug!("All optimal amounts are selected");
|
||||||
|
if include_fees {
|
||||||
|
return Self::include_fees(
|
||||||
|
amount,
|
||||||
|
proofs,
|
||||||
|
selected_proofs.into_iter().collect(),
|
||||||
|
active_keyset_ids,
|
||||||
|
keyset_fees,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Ok(selected_proofs.into_iter().collect());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select proofs with the remaining amounts by checking for 2 of the half amount, 4 of the quarter amount, etc.
|
||||||
|
tracing::debug!("Selecting proofs with the remaining amounts");
|
||||||
|
for remaining_amount in remaining_amounts {
|
||||||
|
// Number of proofs to select
|
||||||
|
let mut n = 2;
|
||||||
|
|
||||||
|
let mut target_amount = remaining_amount;
|
||||||
|
let mut found = false;
|
||||||
|
while let Some(curr_amount) = target_amount.checked_div(Amount::from(2)) {
|
||||||
|
if curr_amount == Amount::ZERO {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select proofs with the current amount
|
||||||
|
let mut count = 0;
|
||||||
|
for _ in 0..n {
|
||||||
|
if select_proof(&proofs, curr_amount, true) {
|
||||||
|
count += 1;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
n -= count;
|
||||||
|
|
||||||
let proof_amount = selected_proof.amount;
|
// All proofs with the current amount are selected
|
||||||
|
if n == 0 {
|
||||||
selected_proofs.push(selected_proof);
|
found = true;
|
||||||
|
|
||||||
let fees = match include_fees {
|
|
||||||
true => self.get_proofs_fee(&selected_proofs).await?,
|
|
||||||
false => Amount::ZERO,
|
|
||||||
};
|
|
||||||
|
|
||||||
if proof_amount >= remaining_amount + fees {
|
|
||||||
remaining_amount = Amount::ZERO;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining_amount = amount.checked_add(fees).ok_or(Error::AmountOverflow)?
|
// Try to find double the number of the next amount
|
||||||
- selected_proofs.total_amount()?;
|
n *= 2;
|
||||||
(proofs_larger, proofs_smaller) = proofs_smaller
|
target_amount = curr_amount;
|
||||||
.into_iter()
|
|
||||||
.skip(1)
|
|
||||||
.partition(|p| p.amount > remaining_amount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if remaining_amount > Amount::ZERO {
|
// Find closest amount over the remaining amount
|
||||||
if let Some(next_bigger) = next_bigger_proof {
|
if !found {
|
||||||
return Ok(vec![next_bigger.clone()]);
|
select_proof(&proofs, remaining_amount, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(Error::InsufficientFunds);
|
// Check if the selected proofs total amount is equal to the amount else filter out unnecessary proofs
|
||||||
|
let mut selected_proofs = selected_proofs.into_iter().collect::<Vec<_>>();
|
||||||
|
let total_amount = selected_proofs.total_amount()?;
|
||||||
|
if total_amount != amount && selected_proofs.len() > 1 {
|
||||||
|
selected_proofs.sort_by(|a, b| a.cmp(b).reverse());
|
||||||
|
selected_proofs = Self::select_least_amount_over(selected_proofs, amount)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if include_fees {
|
||||||
|
return Self::include_fees(
|
||||||
|
amount,
|
||||||
|
proofs,
|
||||||
|
selected_proofs,
|
||||||
|
active_keyset_ids,
|
||||||
|
keyset_fees,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(selected_proofs)
|
Ok(selected_proofs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select proofs to send
|
fn select_least_amount_over(proofs: Proofs, amount: Amount) -> Result<Vec<Proof>, Error> {
|
||||||
#[instrument(skip_all)]
|
let total_amount = proofs.total_amount()?;
|
||||||
pub async fn select_proofs_to_swap(
|
if total_amount < amount {
|
||||||
&self,
|
return Err(Error::InsufficientFunds);
|
||||||
|
}
|
||||||
|
if proofs.len() == 1 {
|
||||||
|
return Ok(proofs);
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 1..proofs.len() {
|
||||||
|
let (left, right) = proofs.split_at(i);
|
||||||
|
let left = left.to_vec();
|
||||||
|
let right = right.to_vec();
|
||||||
|
let left_amount = left.total_amount()?;
|
||||||
|
let right_amount = right.total_amount()?;
|
||||||
|
|
||||||
|
if left_amount >= amount && right_amount >= amount {
|
||||||
|
match (
|
||||||
|
Self::select_least_amount_over(left, amount),
|
||||||
|
Self::select_least_amount_over(right, amount),
|
||||||
|
) {
|
||||||
|
(Ok(left_proofs), Ok(right_proofs)) => {
|
||||||
|
let left_total_amount = left_proofs.total_amount()?;
|
||||||
|
let right_total_amount = right_proofs.total_amount()?;
|
||||||
|
if left_total_amount < right_total_amount {
|
||||||
|
return Ok(left_proofs);
|
||||||
|
} else {
|
||||||
|
return Ok(right_proofs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Ok(left_proofs), Err(_)) => return Ok(left_proofs),
|
||||||
|
(Err(_), Ok(right_proofs)) => return Ok(right_proofs),
|
||||||
|
(Err(_), Err(_)) => return Err(Error::InsufficientFunds),
|
||||||
|
}
|
||||||
|
} else if left_amount >= amount {
|
||||||
|
return Self::select_least_amount_over(left, amount);
|
||||||
|
} else if right_amount >= amount {
|
||||||
|
return Self::select_least_amount_over(right, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(proofs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn include_fees(
|
||||||
amount: Amount,
|
amount: Amount,
|
||||||
proofs: Proofs,
|
proofs: Proofs,
|
||||||
|
mut selected_proofs: Proofs,
|
||||||
|
active_keyset_ids: &Vec<Id>,
|
||||||
|
keyset_fees: &HashMap<Id, u64>,
|
||||||
) -> Result<Proofs, Error> {
|
) -> Result<Proofs, Error> {
|
||||||
|
tracing::debug!("Including fees");
|
||||||
|
let fee =
|
||||||
|
calculate_fee(&selected_proofs.count_by_keyset(), keyset_fees).unwrap_or_default();
|
||||||
|
let net_amount = selected_proofs.total_amount()? - fee;
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Selecting proofs to swap {} from {}",
|
"Net amount={}, fee={}, total amount={}",
|
||||||
amount,
|
net_amount,
|
||||||
proofs.total_amount()?
|
fee,
|
||||||
);
|
|
||||||
let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
|
||||||
|
|
||||||
let (mut active_proofs, mut inactive_proofs): (Proofs, Proofs) = proofs
|
|
||||||
.into_iter()
|
|
||||||
.partition(|p| p.keyset_id == active_keyset_id);
|
|
||||||
|
|
||||||
let mut selected_proofs: Proofs = Vec::new();
|
|
||||||
inactive_proofs.sort_by(|a: &Proof, b: &Proof| b.cmp(a));
|
|
||||||
|
|
||||||
for inactive_proof in inactive_proofs {
|
|
||||||
selected_proofs.push(inactive_proof);
|
|
||||||
let selected_total = selected_proofs.total_amount()?;
|
|
||||||
let fees = self.get_proofs_fee(&selected_proofs).await?;
|
|
||||||
|
|
||||||
if selected_total >= amount + fees {
|
|
||||||
return Ok(selected_proofs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
active_proofs.sort_by(|a: &Proof, b: &Proof| b.cmp(a));
|
|
||||||
|
|
||||||
for active_proof in active_proofs {
|
|
||||||
selected_proofs.push(active_proof);
|
|
||||||
let selected_total = selected_proofs.total_amount()?;
|
|
||||||
let fees = self.get_proofs_fee(&selected_proofs).await?;
|
|
||||||
|
|
||||||
if selected_total >= amount + fees {
|
|
||||||
return Ok(selected_proofs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!(
|
|
||||||
"Could not select proofs to swap: total selected: {}",
|
|
||||||
selected_proofs.total_amount()?
|
selected_proofs.total_amount()?
|
||||||
);
|
);
|
||||||
|
if net_amount >= amount {
|
||||||
|
tracing::debug!(
|
||||||
|
"Selected proofs: {:?}",
|
||||||
|
selected_proofs
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.amount.into())
|
||||||
|
.collect::<Vec<u64>>(),
|
||||||
|
);
|
||||||
|
return Ok(selected_proofs);
|
||||||
|
}
|
||||||
|
|
||||||
Err(Error::InsufficientFunds)
|
tracing::debug!("Net amount is less than the required amount");
|
||||||
|
let remaining_amount = amount - net_amount;
|
||||||
|
let remaining_proofs = proofs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|p| !selected_proofs.contains(p))
|
||||||
|
.collect::<Proofs>();
|
||||||
|
selected_proofs.extend(Wallet::select_proofs(
|
||||||
|
remaining_amount,
|
||||||
|
remaining_proofs,
|
||||||
|
active_keyset_ids,
|
||||||
|
&HashMap::new(), // Fees are already calculated
|
||||||
|
false,
|
||||||
|
)?);
|
||||||
|
tracing::debug!(
|
||||||
|
"Selected proofs: {:?}",
|
||||||
|
selected_proofs
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.amount.into())
|
||||||
|
.collect::<Vec<u64>>(),
|
||||||
|
);
|
||||||
|
Ok(selected_proofs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use cdk_common::secret::Secret;
|
||||||
|
use cdk_common::{Amount, Id, Proof, PublicKey};
|
||||||
|
|
||||||
|
use crate::Wallet;
|
||||||
|
|
||||||
|
fn id() -> Id {
|
||||||
|
Id::from_bytes(&[0; 8]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proof(amount: u64) -> Proof {
|
||||||
|
Proof::new(
|
||||||
|
Amount::from(amount),
|
||||||
|
id(),
|
||||||
|
Secret::generate(),
|
||||||
|
PublicKey::from_hex(
|
||||||
|
"03deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_proofs_empty() {
|
||||||
|
let proofs = vec![];
|
||||||
|
let selected_proofs =
|
||||||
|
Wallet::select_proofs(0.into(), proofs, &vec![id()], &HashMap::new(), false).unwrap();
|
||||||
|
assert_eq!(selected_proofs.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_proofs_insufficient() {
|
||||||
|
let proofs = vec![proof(1), proof(2), proof(4)];
|
||||||
|
let selected_proofs =
|
||||||
|
Wallet::select_proofs(8.into(), proofs, &vec![id()], &HashMap::new(), false);
|
||||||
|
assert!(selected_proofs.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_proofs_exact() {
|
||||||
|
let proofs = vec![
|
||||||
|
proof(1),
|
||||||
|
proof(2),
|
||||||
|
proof(4),
|
||||||
|
proof(8),
|
||||||
|
proof(16),
|
||||||
|
proof(32),
|
||||||
|
proof(64),
|
||||||
|
];
|
||||||
|
let mut selected_proofs =
|
||||||
|
Wallet::select_proofs(77.into(), proofs, &vec![id()], &HashMap::new(), false).unwrap();
|
||||||
|
selected_proofs.sort();
|
||||||
|
assert_eq!(selected_proofs.len(), 4);
|
||||||
|
assert_eq!(selected_proofs[0].amount, 1.into());
|
||||||
|
assert_eq!(selected_proofs[1].amount, 4.into());
|
||||||
|
assert_eq!(selected_proofs[2].amount, 8.into());
|
||||||
|
assert_eq!(selected_proofs[3].amount, 64.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_proofs_over() {
|
||||||
|
let proofs = vec![proof(1), proof(2), proof(4), proof(8), proof(32), proof(64)];
|
||||||
|
let selected_proofs =
|
||||||
|
Wallet::select_proofs(31.into(), proofs, &vec![id()], &HashMap::new(), false).unwrap();
|
||||||
|
assert_eq!(selected_proofs.len(), 1);
|
||||||
|
assert_eq!(selected_proofs[0].amount, 32.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_proofs_smaller_over() {
|
||||||
|
let proofs = vec![proof(8), proof(16), proof(32)];
|
||||||
|
let selected_proofs =
|
||||||
|
Wallet::select_proofs(23.into(), proofs, &vec![id()], &HashMap::new(), false).unwrap();
|
||||||
|
assert_eq!(selected_proofs.len(), 2);
|
||||||
|
assert_eq!(selected_proofs[0].amount, 16.into());
|
||||||
|
assert_eq!(selected_proofs[1].amount, 8.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_proofs_many_ones() {
|
||||||
|
let proofs = (0..1024).into_iter().map(|_| proof(1)).collect::<Vec<_>>();
|
||||||
|
let selected_proofs =
|
||||||
|
Wallet::select_proofs(1024.into(), proofs, &vec![id()], &HashMap::new(), false)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(selected_proofs.len(), 1024);
|
||||||
|
for i in 0..1024 {
|
||||||
|
assert_eq!(selected_proofs[i].amount, 1.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_proofs_huge_proofs() {
|
||||||
|
let proofs = (0..32)
|
||||||
|
.flat_map(|i| {
|
||||||
|
(0..5)
|
||||||
|
.into_iter()
|
||||||
|
.map(|_| proof(1 << i))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut selected_proofs = Wallet::select_proofs(
|
||||||
|
((1u64 << 32) - 1).into(),
|
||||||
|
proofs,
|
||||||
|
&vec![id()],
|
||||||
|
&HashMap::new(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
selected_proofs.sort();
|
||||||
|
assert_eq!(selected_proofs.len(), 32);
|
||||||
|
for i in 0..32 {
|
||||||
|
assert_eq!(selected_proofs[i].amount, (1 << i).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_proofs_with_fees() {
|
||||||
|
let proofs = vec![proof(64), proof(4), proof(32)];
|
||||||
|
let mut keyset_fees = HashMap::new();
|
||||||
|
keyset_fees.insert(id(), 100);
|
||||||
|
let selected_proofs =
|
||||||
|
Wallet::select_proofs(10.into(), proofs, &vec![id()], &keyset_fees, false).unwrap();
|
||||||
|
assert_eq!(selected_proofs.len(), 1);
|
||||||
|
assert_eq!(selected_proofs[0].amount, 32.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use super::SendKind;
|
use super::SendKind;
|
||||||
@@ -7,36 +9,26 @@ use crate::nuts::{Proofs, SpendingConditions, State, Token};
|
|||||||
use crate::{Amount, Error, Wallet};
|
use crate::{Amount, Error, Wallet};
|
||||||
|
|
||||||
impl Wallet {
|
impl Wallet {
|
||||||
/// Send specific proofs
|
/// Prepare A Send Transaction
|
||||||
#[instrument(skip(self))]
|
///
|
||||||
pub async fn send_proofs(&self, memo: Option<String>, proofs: Proofs) -> Result<Token, Error> {
|
/// This function prepares a send transaction by selecting proofs to send and proofs to swap.
|
||||||
let ys = proofs.ys()?;
|
/// By doing so, it ensures that the wallet user is able to view the fees associated with the send transaction.
|
||||||
self.localstore.reserve_proofs(ys).await?;
|
///
|
||||||
|
/// ```no_compile
|
||||||
Ok(Token::new(
|
/// let send = wallet.prepare_send(Amount::from(10), SendOptions::default()).await?;
|
||||||
self.mint_url.clone(),
|
/// assert!(send.fee() <= Amount::from(1));
|
||||||
proofs,
|
/// let token = wallet.send(send, None).await?;
|
||||||
memo,
|
/// ```
|
||||||
self.unit.clone(),
|
#[instrument(skip(self), err)]
|
||||||
))
|
pub async fn prepare_send(
|
||||||
}
|
|
||||||
|
|
||||||
/// Send
|
|
||||||
#[instrument(skip(self))]
|
|
||||||
pub async fn send(
|
|
||||||
&self,
|
&self,
|
||||||
amount: Amount,
|
amount: Amount,
|
||||||
memo: Option<String>,
|
opts: SendOptions,
|
||||||
conditions: Option<SpendingConditions>,
|
) -> Result<PreparedSend, Error> {
|
||||||
amount_split_target: &SplitTarget,
|
tracing::info!("Preparing send");
|
||||||
send_kind: &SendKind,
|
|
||||||
include_fees: bool,
|
|
||||||
) -> Result<Token, Error> {
|
|
||||||
// If online send check mint for current keysets fees
|
// If online send check mint for current keysets fees
|
||||||
if matches!(
|
if opts.send_kind.is_online() {
|
||||||
send_kind,
|
|
||||||
SendKind::OnlineExact | SendKind::OnlineTolerance(_)
|
|
||||||
) {
|
|
||||||
if let Err(e) = self.get_active_mint_keyset().await {
|
if let Err(e) = self.get_active_mint_keyset().await {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"Error fetching active mint keyset: {:?}. Using stored keysets",
|
"Error fetching active mint keyset: {:?}. Using stored keysets",
|
||||||
@@ -45,135 +37,377 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let available_proofs = self
|
// Get keyset fees from localstore
|
||||||
|
let keyset_fees = self.get_keyset_fees().await?;
|
||||||
|
|
||||||
|
// Get available proofs matching conditions
|
||||||
|
let mut available_proofs = self
|
||||||
.get_proofs_with(
|
.get_proofs_with(
|
||||||
Some(vec![State::Unspent]),
|
Some(vec![State::Unspent]),
|
||||||
conditions.clone().map(|c| vec![c]),
|
opts.conditions.clone().map(|c| vec![c]),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let proofs_sum = available_proofs.total_amount()?;
|
// Check if sufficient proofs are available
|
||||||
|
let mut force_swap = false;
|
||||||
let available_proofs = if proofs_sum < amount {
|
let available_sum = available_proofs.total_amount()?;
|
||||||
match &conditions {
|
if available_sum < amount {
|
||||||
Some(conditions) => {
|
if opts.conditions.is_none() || opts.send_kind.is_offline() {
|
||||||
tracing::debug!("Insufficient prrofs matching conditions attempting swap");
|
return Err(Error::InsufficientFunds);
|
||||||
let unspent_proofs = self.get_unspent_proofs().await?;
|
|
||||||
let proofs_to_swap = self.select_proofs_to_swap(amount, unspent_proofs).await?;
|
|
||||||
|
|
||||||
let proofs_with_conditions = self
|
|
||||||
.swap(
|
|
||||||
Some(amount),
|
|
||||||
SplitTarget::default(),
|
|
||||||
proofs_to_swap,
|
|
||||||
Some(conditions.clone()),
|
|
||||||
include_fees,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
proofs_with_conditions.ok_or(Error::InsufficientFunds)
|
|
||||||
}
|
|
||||||
None => Err(Error::InsufficientFunds),
|
|
||||||
}?
|
|
||||||
} else {
|
} else {
|
||||||
available_proofs
|
// Swap is required for send
|
||||||
};
|
tracing::debug!("Insufficient proofs matching conditions");
|
||||||
|
force_swap = true;
|
||||||
|
available_proofs = self
|
||||||
|
.localstore
|
||||||
|
.get_proofs(
|
||||||
|
Some(self.mint_url.clone()),
|
||||||
|
Some(self.unit.clone()),
|
||||||
|
Some(vec![State::Unspent]),
|
||||||
|
Some(vec![]),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| p.proof)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let selected = self
|
// Select proofs
|
||||||
.select_proofs_to_send(amount, available_proofs, include_fees)
|
let active_keyset_ids = self
|
||||||
|
.get_active_mint_keysets()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|k| k.id)
|
||||||
|
.collect();
|
||||||
|
let selected_proofs = Wallet::select_proofs(
|
||||||
|
amount,
|
||||||
|
available_proofs,
|
||||||
|
&active_keyset_ids,
|
||||||
|
&keyset_fees,
|
||||||
|
opts.include_fee,
|
||||||
|
)?;
|
||||||
|
let selected_total = selected_proofs.total_amount()?;
|
||||||
|
|
||||||
|
// Check if selected proofs are exact
|
||||||
|
let send_fee = if opts.include_fee {
|
||||||
|
self.get_proofs_fee(&selected_proofs).await?
|
||||||
|
} else {
|
||||||
|
Amount::ZERO
|
||||||
|
};
|
||||||
|
if selected_total == amount + send_fee {
|
||||||
|
return self
|
||||||
|
.internal_prepare_send(amount, opts, selected_proofs, force_swap)
|
||||||
.await;
|
.await;
|
||||||
|
} else if opts.send_kind == SendKind::OfflineExact {
|
||||||
|
return Err(Error::InsufficientFunds);
|
||||||
|
}
|
||||||
|
|
||||||
let send_proofs: Proofs = match (send_kind, selected, conditions.clone()) {
|
// Check if selected proofs are sufficient for tolerance
|
||||||
// Handle exact matches offline
|
let tolerance = match opts.send_kind {
|
||||||
(SendKind::OfflineExact, Ok(selected_proofs), _) => {
|
SendKind::OfflineTolerance(tolerance) => Some(tolerance),
|
||||||
let selected_proofs_amount = selected_proofs.total_amount()?;
|
SendKind::OnlineTolerance(tolerance) => Some(tolerance),
|
||||||
|
_ => None,
|
||||||
let amount_to_send = match include_fees {
|
|
||||||
true => amount + self.get_proofs_fee(&selected_proofs).await?,
|
|
||||||
false => amount,
|
|
||||||
};
|
};
|
||||||
|
if let Some(tolerance) = tolerance {
|
||||||
if selected_proofs_amount == amount_to_send {
|
if selected_total - amount > tolerance && opts.send_kind.is_offline() {
|
||||||
selected_proofs
|
|
||||||
} else {
|
|
||||||
return Err(Error::InsufficientFunds);
|
return Err(Error::InsufficientFunds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle exact matches
|
self.internal_prepare_send(amount, opts, selected_proofs, force_swap)
|
||||||
(SendKind::OnlineExact, Ok(selected_proofs), _) => {
|
.await
|
||||||
let selected_proofs_amount = selected_proofs.total_amount()?;
|
}
|
||||||
|
|
||||||
let amount_to_send = match include_fees {
|
async fn internal_prepare_send(
|
||||||
true => amount + self.get_proofs_fee(&selected_proofs).await?,
|
&self,
|
||||||
false => amount,
|
amount: Amount,
|
||||||
};
|
opts: SendOptions,
|
||||||
|
proofs: Proofs,
|
||||||
if selected_proofs_amount == amount_to_send {
|
force_swap: bool,
|
||||||
selected_proofs
|
) -> Result<PreparedSend, Error> {
|
||||||
|
// Split amount with fee if necessary
|
||||||
|
let (send_amounts, send_fee) = if opts.include_fee {
|
||||||
|
let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
||||||
|
let keyset_fee_ppk = self.get_keyset_fees_by_id(active_keyset_id).await?;
|
||||||
|
tracing::debug!("Keyset fee per proof: {:?}", keyset_fee_ppk);
|
||||||
|
let send_split = amount.split_with_fee(keyset_fee_ppk)?;
|
||||||
|
let send_fee = self
|
||||||
|
.get_proofs_fee_by_count(
|
||||||
|
vec![(active_keyset_id, send_split.len() as u64)]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
(send_split, send_fee)
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("Could not select proofs exact while offline.");
|
let send_split = amount.split();
|
||||||
tracing::info!("Attempting to select proofs and swapping");
|
let send_fee = Amount::ZERO;
|
||||||
|
(send_split, send_fee)
|
||||||
|
};
|
||||||
|
tracing::debug!("Send amounts: {:?}", send_amounts);
|
||||||
|
tracing::debug!("Send fee: {:?}", send_fee);
|
||||||
|
|
||||||
self.swap_from_unspent(amount, conditions, include_fees)
|
// Reserve proofs
|
||||||
|
self.localstore
|
||||||
|
.update_proofs_state(proofs.ys()?, State::Reserved)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Check if proofs are exact send amount
|
||||||
|
let proofs_exact_amount = proofs.total_amount()? == amount + send_fee;
|
||||||
|
|
||||||
|
// Split proofs to swap and send
|
||||||
|
let mut proofs_to_swap = Proofs::new();
|
||||||
|
let mut proofs_to_send = Proofs::new();
|
||||||
|
if force_swap {
|
||||||
|
proofs_to_swap = proofs;
|
||||||
|
} else if proofs_exact_amount
|
||||||
|
|| opts.send_kind.is_offline()
|
||||||
|
|| opts.send_kind.has_tolerance()
|
||||||
|
{
|
||||||
|
proofs_to_send = proofs;
|
||||||
|
} else {
|
||||||
|
let mut remaining_send_amounts = send_amounts.clone();
|
||||||
|
for proof in proofs {
|
||||||
|
if let Some(idx) = remaining_send_amounts
|
||||||
|
.iter()
|
||||||
|
.position(|a| a == &proof.amount)
|
||||||
|
{
|
||||||
|
proofs_to_send.push(proof);
|
||||||
|
remaining_send_amounts.remove(idx);
|
||||||
|
} else {
|
||||||
|
proofs_to_swap.push(proof);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate swap fee
|
||||||
|
let swap_fee = self.get_proofs_fee(&proofs_to_swap).await?;
|
||||||
|
|
||||||
|
// Return prepared send
|
||||||
|
Ok(PreparedSend {
|
||||||
|
amount,
|
||||||
|
options: opts,
|
||||||
|
proofs_to_swap,
|
||||||
|
swap_fee,
|
||||||
|
proofs_to_send,
|
||||||
|
send_fee,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finalize A Send Transaction
|
||||||
|
///
|
||||||
|
/// This function finalizes a send transaction by constructing a token the [`PreparedSend`].
|
||||||
|
/// See [`Wallet::prepare_send`] for more information.
|
||||||
|
#[instrument(skip(self), err)]
|
||||||
|
pub async fn send(&self, send: PreparedSend, memo: Option<SendMemo>) -> Result<Token, Error> {
|
||||||
|
tracing::info!("Sending prepared send");
|
||||||
|
let mut proofs_to_send = send.proofs_to_send;
|
||||||
|
|
||||||
|
// Get active keyset ID
|
||||||
|
let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
||||||
|
tracing::debug!("Active keyset ID: {:?}", active_keyset_id);
|
||||||
|
|
||||||
|
// Get keyset fees
|
||||||
|
let keyset_fee_ppk = self.get_keyset_fees_by_id(active_keyset_id).await?;
|
||||||
|
tracing::debug!("Keyset fees: {:?}", keyset_fee_ppk);
|
||||||
|
|
||||||
|
// Calculate total send amount
|
||||||
|
let total_send_amount = send.amount + send.send_fee;
|
||||||
|
tracing::debug!("Total send amount: {}", total_send_amount);
|
||||||
|
|
||||||
|
// Swap proofs if necessary
|
||||||
|
if !send.proofs_to_swap.is_empty() {
|
||||||
|
let swap_amount = total_send_amount - proofs_to_send.total_amount()?;
|
||||||
|
tracing::debug!("Swapping proofs; swap_amount={:?}", swap_amount);
|
||||||
|
if let Some(proofs) = self
|
||||||
|
.swap(
|
||||||
|
Some(swap_amount),
|
||||||
|
SplitTarget::None,
|
||||||
|
send.proofs_to_swap,
|
||||||
|
send.options.conditions.clone(),
|
||||||
|
false, // already included in swap_amount
|
||||||
|
)
|
||||||
.await?
|
.await?
|
||||||
|
{
|
||||||
|
proofs_to_send.extend(proofs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tracing::debug!(
|
||||||
|
"Proofs to send: {:?}",
|
||||||
|
proofs_to_send.iter().map(|p| p.amount).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
// Handle offline tolerance
|
// Check if sufficient proofs are available
|
||||||
(SendKind::OfflineTolerance(tolerance), Ok(selected_proofs), _) => {
|
if send.amount > proofs_to_send.total_amount()? {
|
||||||
let selected_proofs_amount = selected_proofs.total_amount()?;
|
|
||||||
|
|
||||||
let amount_to_send = match include_fees {
|
|
||||||
true => amount + self.get_proofs_fee(&selected_proofs).await?,
|
|
||||||
false => amount,
|
|
||||||
};
|
|
||||||
if selected_proofs_amount - amount_to_send <= *tolerance {
|
|
||||||
selected_proofs
|
|
||||||
} else {
|
|
||||||
tracing::info!("Selected proofs greater than tolerance. Must swap online");
|
|
||||||
return Err(Error::InsufficientFunds);
|
return Err(Error::InsufficientFunds);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Handle online tolerance when selection fails and conditions are present
|
// Check if proofs are reserved or unspent
|
||||||
(SendKind::OnlineTolerance(_), Err(_), Some(_)) => {
|
let sendable_proof_ys = self
|
||||||
tracing::info!("Could not select proofs with conditions while offline.");
|
.get_proofs_with(
|
||||||
tracing::info!("Attempting to select proofs without conditions and swapping");
|
Some(vec![State::Reserved, State::Unspent]),
|
||||||
|
send.options.conditions.clone().map(|c| vec![c]),
|
||||||
self.swap_from_unspent(amount, conditions, include_fees)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
.ys()?;
|
||||||
|
if proofs_to_send
|
||||||
|
.ys()?
|
||||||
|
.iter()
|
||||||
|
.any(|y| !sendable_proof_ys.contains(y))
|
||||||
|
{
|
||||||
|
tracing::warn!("Proofs to send are not reserved or unspent");
|
||||||
|
return Err(Error::UnexpectedProofState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle online tolerance with successful selection
|
// Update proofs state to pending spent
|
||||||
(SendKind::OnlineTolerance(tolerance), Ok(selected_proofs), _) => {
|
tracing::debug!(
|
||||||
let selected_proofs_amount = selected_proofs.total_amount()?;
|
"Updating proofs state to pending spent: {:?}",
|
||||||
let amount_to_send = match include_fees {
|
proofs_to_send.ys()?
|
||||||
true => amount + self.get_proofs_fee(&selected_proofs).await?,
|
);
|
||||||
false => amount,
|
self.localstore
|
||||||
};
|
.update_proofs_state(proofs_to_send.ys()?, State::PendingSpent)
|
||||||
if selected_proofs_amount - amount_to_send <= *tolerance {
|
.await?;
|
||||||
selected_proofs
|
|
||||||
} else {
|
// Include token memo
|
||||||
tracing::info!("Could not select proofs while offline. Attempting swap");
|
let send_memo = send.options.memo.or(memo);
|
||||||
self.swap_from_unspent(amount, conditions, include_fees)
|
let memo = send_memo.and_then(|m| if m.include_memo { Some(m.memo) } else { None });
|
||||||
.await?
|
|
||||||
}
|
// Create and return token
|
||||||
|
Ok(Token::new(
|
||||||
|
self.mint_url.clone(),
|
||||||
|
proofs_to_send,
|
||||||
|
memo,
|
||||||
|
self.unit.clone(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle all other cases where selection fails
|
/// Cancel prepared send
|
||||||
(
|
pub async fn cancel_send(&self, send: PreparedSend) -> Result<(), Error> {
|
||||||
SendKind::OfflineExact
|
tracing::info!("Cancelling prepared send");
|
||||||
| SendKind::OnlineExact
|
|
||||||
| SendKind::OfflineTolerance(_)
|
|
||||||
| SendKind::OnlineTolerance(_),
|
|
||||||
Err(_),
|
|
||||||
_,
|
|
||||||
) => {
|
|
||||||
tracing::debug!("Could not select proofs");
|
|
||||||
return Err(Error::InsufficientFunds);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.send_proofs(memo, send_proofs).await
|
// Double-check proofs state
|
||||||
|
let reserved_proofs = self.get_reserved_proofs().await?.ys()?;
|
||||||
|
if !send
|
||||||
|
.proofs()
|
||||||
|
.ys()?
|
||||||
|
.iter()
|
||||||
|
.all(|y| reserved_proofs.contains(y))
|
||||||
|
{
|
||||||
|
return Err(Error::UnexpectedProofState);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.localstore
|
||||||
|
.update_proofs_state(send.proofs().ys()?, State::Unspent)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepared send
|
||||||
|
pub struct PreparedSend {
|
||||||
|
amount: Amount,
|
||||||
|
options: SendOptions,
|
||||||
|
proofs_to_swap: Proofs,
|
||||||
|
swap_fee: Amount,
|
||||||
|
proofs_to_send: Proofs,
|
||||||
|
send_fee: Amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreparedSend {
|
||||||
|
/// Amount
|
||||||
|
pub fn amount(&self) -> Amount {
|
||||||
|
self.amount
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send options
|
||||||
|
pub fn options(&self) -> &SendOptions {
|
||||||
|
&self.options
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proofs to swap (i.e., proofs that need to be swapped before constructing the token)
|
||||||
|
pub fn proofs_to_swap(&self) -> &Proofs {
|
||||||
|
&self.proofs_to_swap
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swap fee
|
||||||
|
pub fn swap_fee(&self) -> Amount {
|
||||||
|
self.swap_fee
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proofs to send (i.e., proofs that will be included in the token)
|
||||||
|
pub fn proofs_to_send(&self) -> &Proofs {
|
||||||
|
&self.proofs_to_send
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send fee
|
||||||
|
pub fn send_fee(&self) -> Amount {
|
||||||
|
self.send_fee
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All proofs
|
||||||
|
pub fn proofs(&self) -> Proofs {
|
||||||
|
let mut proofs = self.proofs_to_swap.clone();
|
||||||
|
proofs.extend(self.proofs_to_send.clone());
|
||||||
|
proofs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total fee
|
||||||
|
pub fn fee(&self) -> Amount {
|
||||||
|
self.swap_fee + self.send_fee
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for PreparedSend {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("PreparedSend")
|
||||||
|
.field("amount", &self.amount)
|
||||||
|
.field("options", &self.options)
|
||||||
|
.field(
|
||||||
|
"proofs_to_swap",
|
||||||
|
&self
|
||||||
|
.proofs_to_swap
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.amount)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.field("swap_fee", &self.swap_fee)
|
||||||
|
.field(
|
||||||
|
"proofs_to_send",
|
||||||
|
&self
|
||||||
|
.proofs_to_send
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.amount)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.field("send_fee", &self.send_fee)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send options
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct SendOptions {
|
||||||
|
/// Memo
|
||||||
|
pub memo: Option<SendMemo>,
|
||||||
|
/// Spending conditions
|
||||||
|
pub conditions: Option<SpendingConditions>,
|
||||||
|
/// Amount split target
|
||||||
|
pub amount_split_target: SplitTarget,
|
||||||
|
/// Send kind
|
||||||
|
pub send_kind: SendKind,
|
||||||
|
/// Include fee
|
||||||
|
///
|
||||||
|
/// When this is true the token created will include the amount of fees needed to redeem the token (amount + fee_to_redeem)
|
||||||
|
pub include_fee: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send memo
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SendMemo {
|
||||||
|
/// Memo
|
||||||
|
pub memo: String,
|
||||||
|
/// Include memo in token
|
||||||
|
pub include_memo: bool,
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ impl Wallet {
|
|||||||
spending_conditions: Option<SpendingConditions>,
|
spending_conditions: Option<SpendingConditions>,
|
||||||
include_fees: bool,
|
include_fees: bool,
|
||||||
) -> Result<Option<Proofs>, Error> {
|
) -> Result<Option<Proofs>, Error> {
|
||||||
|
tracing::info!("Swapping");
|
||||||
let mint_url = &self.mint_url;
|
let mint_url = &self.mint_url;
|
||||||
let unit = &self.unit;
|
let unit = &self.unit;
|
||||||
|
|
||||||
let pre_swap = self
|
let pre_swap = self
|
||||||
.create_swap(
|
.create_swap(
|
||||||
amount,
|
amount,
|
||||||
amount_split_target,
|
amount_split_target.clone(),
|
||||||
input_proofs.clone(),
|
input_proofs.clone(),
|
||||||
spending_conditions.clone(),
|
spending_conditions.clone(),
|
||||||
include_fees,
|
include_fees,
|
||||||
@@ -72,13 +73,15 @@ impl Wallet {
|
|||||||
let mut all_proofs = proofs_without_condition;
|
let mut all_proofs = proofs_without_condition;
|
||||||
all_proofs.reverse();
|
all_proofs.reverse();
|
||||||
|
|
||||||
let mut proofs_to_send: Proofs = Vec::new();
|
let mut proofs_to_send = Proofs::new();
|
||||||
let mut proofs_to_keep = Vec::new();
|
let mut proofs_to_keep = Proofs::new();
|
||||||
|
let mut amount_split = amount.split_targeted(&amount_split_target)?;
|
||||||
|
|
||||||
for proof in all_proofs {
|
for proof in all_proofs {
|
||||||
let proofs_to_send_amount = proofs_to_send.total_amount()?;
|
if let Some(idx) = amount_split.iter().position(|&a| a == proof.amount)
|
||||||
if proof.amount + proofs_to_send_amount <= amount + pre_swap.fee {
|
{
|
||||||
proofs_to_send.push(proof);
|
proofs_to_send.push(proof);
|
||||||
|
amount_split.remove(idx);
|
||||||
} else {
|
} else {
|
||||||
proofs_to_keep.push(proof);
|
proofs_to_keep.push(proof);
|
||||||
}
|
}
|
||||||
@@ -163,7 +166,20 @@ impl Wallet {
|
|||||||
|
|
||||||
ensure_cdk!(proofs_sum >= amount, Error::InsufficientFunds);
|
ensure_cdk!(proofs_sum >= amount, Error::InsufficientFunds);
|
||||||
|
|
||||||
let proofs = self.select_proofs_to_swap(amount, available_proofs).await?;
|
let active_keyset_ids = self
|
||||||
|
.get_active_mint_keysets()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|k| k.id)
|
||||||
|
.collect();
|
||||||
|
let keyset_fees = self.get_keyset_fees().await?;
|
||||||
|
let proofs = Wallet::select_proofs(
|
||||||
|
amount,
|
||||||
|
available_proofs,
|
||||||
|
&active_keyset_ids,
|
||||||
|
&keyset_fees,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
|
||||||
self.swap(
|
self.swap(
|
||||||
Some(amount),
|
Some(amount),
|
||||||
@@ -186,13 +202,16 @@ impl Wallet {
|
|||||||
spending_conditions: Option<SpendingConditions>,
|
spending_conditions: Option<SpendingConditions>,
|
||||||
include_fees: bool,
|
include_fees: bool,
|
||||||
) -> Result<PreSwap, Error> {
|
) -> Result<PreSwap, Error> {
|
||||||
|
tracing::info!("Creating swap");
|
||||||
let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
let active_keyset_id = self.get_active_mint_keyset().await?.id;
|
||||||
|
|
||||||
// Desired amount is either amount passed or value of all proof
|
// Desired amount is either amount passed or value of all proof
|
||||||
let proofs_total = proofs.total_amount()?;
|
let proofs_total = proofs.total_amount()?;
|
||||||
|
|
||||||
let ys: Vec<PublicKey> = proofs.ys()?;
|
let ys: Vec<PublicKey> = proofs.ys()?;
|
||||||
self.localstore.set_pending_proofs(ys).await?;
|
self.localstore
|
||||||
|
.update_proofs_state(ys, State::Reserved)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let fee = self.get_proofs_fee(&proofs).await?;
|
let fee = self.get_proofs_fee(&proofs).await?;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user