|
@@ -3,6 +3,7 @@
|
|
|
//! Is any unit and will be treated as the unit of the wallet
|
|
|
|
|
|
use std::cmp::Ordering;
|
|
|
+use std::collections::HashMap;
|
|
|
use std::fmt;
|
|
|
use std::str::FromStr;
|
|
|
|
|
@@ -11,6 +12,7 @@ use serde::{Deserialize, Serialize};
|
|
|
use thiserror::Error;
|
|
|
|
|
|
use crate::nuts::CurrencyUnit;
|
|
|
+use crate::Id;
|
|
|
|
|
|
/// Amount Error
|
|
|
#[derive(Debug, Error)]
|
|
@@ -41,6 +43,40 @@ pub enum Error {
|
|
|
#[serde(transparent)]
|
|
|
pub struct Amount(u64);
|
|
|
|
|
|
+/// Fees and and amount type, it can be casted just as a reference to the inner amounts, or a single
|
|
|
+/// u64 which is the fee
|
|
|
+#[derive(Debug, Clone)]
|
|
|
+pub struct FeeAndAmounts {
|
|
|
+ fee: u64,
|
|
|
+ amounts: Vec<u64>,
|
|
|
+}
|
|
|
+
|
|
|
+impl From<(u64, Vec<u64>)> for FeeAndAmounts {
|
|
|
+ fn from(value: (u64, Vec<u64>)) -> Self {
|
|
|
+ Self {
|
|
|
+ fee: value.0,
|
|
|
+ amounts: value.1,
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl FeeAndAmounts {
|
|
|
+ /// Fees
|
|
|
+ #[inline(always)]
|
|
|
+ pub fn fee(&self) -> u64 {
|
|
|
+ self.fee
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Amounts
|
|
|
+ #[inline(always)]
|
|
|
+ pub fn amounts(&self) -> &[u64] {
|
|
|
+ &self.amounts
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Fees and Amounts for each Keyset
|
|
|
+pub type KeysetFeeAndAmounts = HashMap<Id, FeeAndAmounts>;
|
|
|
+
|
|
|
impl FromStr for Amount {
|
|
|
type Err = Error;
|
|
|
|
|
@@ -60,8 +96,9 @@ impl Amount {
|
|
|
pub const ONE: Amount = Amount(1);
|
|
|
|
|
|
/// Split into parts that are powers of two
|
|
|
- pub fn split(&self, amounts_ppk: &[u64]) -> Vec<Self> {
|
|
|
- amounts_ppk
|
|
|
+ pub fn split(&self, fee_and_amounts: &FeeAndAmounts) -> Vec<Self> {
|
|
|
+ fee_and_amounts
|
|
|
+ .amounts
|
|
|
.iter()
|
|
|
.rev()
|
|
|
.fold((Vec::new(), self.0), |(mut acc, total), &amount| {
|
|
@@ -77,20 +114,20 @@ impl Amount {
|
|
|
pub fn split_targeted(
|
|
|
&self,
|
|
|
target: &SplitTarget,
|
|
|
- amounts_ppk: &[u64],
|
|
|
+ fee_and_amounts: &FeeAndAmounts,
|
|
|
) -> Result<Vec<Self>, Error> {
|
|
|
let mut parts = match target {
|
|
|
- SplitTarget::None => self.split(amounts_ppk),
|
|
|
+ SplitTarget::None => self.split(fee_and_amounts),
|
|
|
SplitTarget::Value(amount) => {
|
|
|
if self.le(amount) {
|
|
|
- return Ok(self.split(amounts_ppk));
|
|
|
+ return Ok(self.split(fee_and_amounts));
|
|
|
}
|
|
|
|
|
|
let mut parts_total = Amount::ZERO;
|
|
|
let mut parts = Vec::new();
|
|
|
|
|
|
// The powers of two that are need to create target value
|
|
|
- let parts_of_value = amount.split(amounts_ppk);
|
|
|
+ let parts_of_value = amount.split(fee_and_amounts);
|
|
|
|
|
|
while parts_total.lt(self) {
|
|
|
for part in parts_of_value.iter().copied() {
|
|
@@ -98,7 +135,7 @@ impl Amount {
|
|
|
parts.push(part);
|
|
|
} else {
|
|
|
let amount_left = *self - parts_total;
|
|
|
- parts.extend(amount_left.split(amounts_ppk));
|
|
|
+ parts.extend(amount_left.split(fee_and_amounts));
|
|
|
}
|
|
|
|
|
|
parts_total = Amount::try_sum(parts.clone().iter().copied())?;
|
|
@@ -121,7 +158,7 @@ impl Amount {
|
|
|
}
|
|
|
Ordering::Greater => {
|
|
|
let extra = *self - values_total;
|
|
|
- let mut extra_amount = extra.split(amounts_ppk);
|
|
|
+ let mut extra_amount = extra.split(fee_and_amounts);
|
|
|
let mut values = values.clone();
|
|
|
|
|
|
values.append(&mut extra_amount);
|
|
@@ -136,17 +173,18 @@ impl Amount {
|
|
|
}
|
|
|
|
|
|
/// Splits amount into powers of two while accounting for the swap fee
|
|
|
- pub fn split_with_fee(&self, fee_ppk: u64, amounts_ppk: &[u64]) -> Result<Vec<Self>, Error> {
|
|
|
- let without_fee_amounts = self.split(amounts_ppk);
|
|
|
- let total_fee_ppk = fee_ppk
|
|
|
+ pub fn split_with_fee(&self, fee_and_amounts: &FeeAndAmounts) -> Result<Vec<Self>, Error> {
|
|
|
+ let without_fee_amounts = self.split(fee_and_amounts);
|
|
|
+ let total_fee_ppk = fee_and_amounts
|
|
|
+ .fee
|
|
|
.checked_mul(without_fee_amounts.len() as u64)
|
|
|
.ok_or(Error::AmountOverflow)?;
|
|
|
let fee = Amount::from(total_fee_ppk.div_ceil(1000));
|
|
|
let new_amount = self.checked_add(fee).ok_or(Error::AmountOverflow)?;
|
|
|
|
|
|
- let split = new_amount.split(amounts_ppk);
|
|
|
+ let split = new_amount.split(fee_and_amounts);
|
|
|
let split_fee_ppk = (split.len() as u64)
|
|
|
- .checked_mul(fee_ppk)
|
|
|
+ .checked_mul(fee_and_amounts.fee)
|
|
|
.ok_or(Error::AmountOverflow)?;
|
|
|
let split_fee = Amount::from(split_fee_ppk.div_ceil(1000));
|
|
|
|
|
@@ -157,7 +195,7 @@ impl Amount {
|
|
|
}
|
|
|
self.checked_add(Amount::ONE)
|
|
|
.ok_or(Error::AmountOverflow)?
|
|
|
- .split_with_fee(fee_ppk, amounts_ppk)
|
|
|
+ .split_with_fee(fee_and_amounts)
|
|
|
}
|
|
|
|
|
|
/// Checked addition for Amount. Returns None if overflow occurs.
|
|
@@ -387,37 +425,43 @@ mod tests {
|
|
|
|
|
|
#[test]
|
|
|
fn test_split_amount() {
|
|
|
- let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
|
|
|
+ let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
|
|
|
- assert_eq!(Amount::from(1).split(&amounts_ppk), vec![Amount::from(1)]);
|
|
|
- assert_eq!(Amount::from(2).split(&amounts_ppk), vec![Amount::from(2)]);
|
|
|
assert_eq!(
|
|
|
- Amount::from(3).split(&amounts_ppk),
|
|
|
+ Amount::from(1).split(&fee_and_amounts),
|
|
|
+ vec![Amount::from(1)]
|
|
|
+ );
|
|
|
+ assert_eq!(
|
|
|
+ Amount::from(2).split(&fee_and_amounts),
|
|
|
+ vec![Amount::from(2)]
|
|
|
+ );
|
|
|
+ assert_eq!(
|
|
|
+ Amount::from(3).split(&fee_and_amounts),
|
|
|
vec![Amount::from(2), Amount::from(1)]
|
|
|
);
|
|
|
let amounts: Vec<Amount> = [8, 2, 1].iter().map(|a| Amount::from(*a)).collect();
|
|
|
- assert_eq!(Amount::from(11).split(&amounts_ppk), amounts);
|
|
|
+ assert_eq!(Amount::from(11).split(&fee_and_amounts), amounts);
|
|
|
let amounts: Vec<Amount> = [128, 64, 32, 16, 8, 4, 2, 1]
|
|
|
.iter()
|
|
|
.map(|a| Amount::from(*a))
|
|
|
.collect();
|
|
|
- assert_eq!(Amount::from(255).split(&amounts_ppk), amounts);
|
|
|
+ assert_eq!(Amount::from(255).split(&fee_and_amounts), amounts);
|
|
|
}
|
|
|
|
|
|
#[test]
|
|
|
fn test_split_target_amount() {
|
|
|
- let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
|
|
|
+ let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
let amount = Amount(65);
|
|
|
|
|
|
let split = amount
|
|
|
- .split_targeted(&SplitTarget::Value(Amount(32)), &amounts_ppk)
|
|
|
+ .split_targeted(&SplitTarget::Value(Amount(32)), &fee_and_amounts)
|
|
|
.unwrap();
|
|
|
assert_eq!(vec![Amount(1), Amount(32), Amount(32)], split);
|
|
|
|
|
|
let amount = Amount(150);
|
|
|
|
|
|
let split = amount
|
|
|
- .split_targeted(&SplitTarget::Value(Amount::from(50)), &amounts_ppk)
|
|
|
+ .split_targeted(&SplitTarget::Value(Amount::from(50)), &fee_and_amounts)
|
|
|
.unwrap();
|
|
|
assert_eq!(
|
|
|
vec![
|
|
@@ -437,7 +481,7 @@ mod tests {
|
|
|
let amount = Amount::from(63);
|
|
|
|
|
|
let split = amount
|
|
|
- .split_targeted(&SplitTarget::Value(Amount::from(32)), &amounts_ppk)
|
|
|
+ .split_targeted(&SplitTarget::Value(Amount::from(32)), &fee_and_amounts)
|
|
|
.unwrap();
|
|
|
assert_eq!(
|
|
|
vec![
|
|
@@ -454,23 +498,21 @@ mod tests {
|
|
|
|
|
|
#[test]
|
|
|
fn test_split_with_fee() {
|
|
|
- let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
|
|
|
+ let fee_and_amounts = (1, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
let amount = Amount(2);
|
|
|
- let fee_ppk = 1;
|
|
|
|
|
|
- let split = amount.split_with_fee(fee_ppk, &amounts_ppk).unwrap();
|
|
|
+ let split = amount.split_with_fee(&fee_and_amounts).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, &amounts_ppk).unwrap();
|
|
|
+ let split = amount.split_with_fee(&fee_and_amounts).unwrap();
|
|
|
assert_eq!(split, vec![Amount(4)]);
|
|
|
|
|
|
let amount = Amount(3);
|
|
|
- let fee_ppk = 1000;
|
|
|
+ let fee_and_amounts = (1000, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
|
|
|
- let split = amount.split_with_fee(fee_ppk, &amounts_ppk).unwrap();
|
|
|
+ let split = amount.split_with_fee(&fee_and_amounts).unwrap();
|
|
|
// With fee_ppk=1000 (100%), amount 3 requires proofs totaling at least 5
|
|
|
// to cover both the amount (3) and fees (~2 for 2 proofs)
|
|
|
assert_eq!(split, vec![Amount(4), Amount(1)]);
|
|
@@ -478,15 +520,14 @@ mod tests {
|
|
|
|
|
|
#[test]
|
|
|
fn test_split_with_fee_reported_issue() {
|
|
|
- let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
|
|
|
+ let fee_and_amounts = (100, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
// Test the reported issue: mint 600, send 300 with fee_ppk=100
|
|
|
let amount = Amount(300);
|
|
|
- let fee_ppk = 100;
|
|
|
|
|
|
- let split = amount.split_with_fee(fee_ppk, &amounts_ppk).unwrap();
|
|
|
+ let split = amount.split_with_fee(&fee_and_amounts).unwrap();
|
|
|
|
|
|
// Calculate the total fee for the split
|
|
|
- let total_fee_ppk = (split.len() as u64) * fee_ppk;
|
|
|
+ let total_fee_ppk = (split.len() as u64) * fee_and_amounts.fee;
|
|
|
let total_fee = Amount::from(total_fee_ppk.div_ceil(1000));
|
|
|
|
|
|
// The split should cover the amount plus fees
|
|
@@ -502,7 +543,6 @@ mod tests {
|
|
|
|
|
|
#[test]
|
|
|
fn test_split_with_fee_edge_cases() {
|
|
|
- let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
|
|
|
// Test various amounts with fee_ppk=100
|
|
|
let test_cases = vec![
|
|
|
(Amount(1), 100),
|
|
@@ -519,7 +559,9 @@ mod tests {
|
|
|
];
|
|
|
|
|
|
for (amount, fee_ppk) in test_cases {
|
|
|
- let result = amount.split_with_fee(fee_ppk, &amounts_ppk);
|
|
|
+ let fee_and_amounts =
|
|
|
+ (fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
+ let result = amount.split_with_fee(&fee_and_amounts);
|
|
|
assert!(
|
|
|
result.is_ok(),
|
|
|
"split_with_fee failed for amount {} with fee_ppk {}: {:?}",
|
|
@@ -556,7 +598,6 @@ mod tests {
|
|
|
|
|
|
#[test]
|
|
|
fn test_split_with_fee_high_fees() {
|
|
|
- let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
|
|
|
// Test with very high fees
|
|
|
let test_cases = vec![
|
|
|
(Amount(10), 500), // 50% fee
|
|
@@ -568,7 +609,9 @@ mod tests {
|
|
|
];
|
|
|
|
|
|
for (amount, fee_ppk) in test_cases {
|
|
|
- let result = amount.split_with_fee(fee_ppk, &amounts_ppk);
|
|
|
+ let fee_and_amounts =
|
|
|
+ (fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
+ let result = amount.split_with_fee(&fee_and_amounts);
|
|
|
assert!(
|
|
|
result.is_ok(),
|
|
|
"split_with_fee failed for amount {} with fee_ppk {}: {:?}",
|
|
@@ -593,13 +636,13 @@ mod tests {
|
|
|
|
|
|
#[test]
|
|
|
fn test_split_with_fee_recursion_limit() {
|
|
|
- let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
|
|
|
// Test that the recursion doesn't go infinite
|
|
|
// This tests the edge case where the method keeps adding Amount::ONE
|
|
|
let amount = Amount(1);
|
|
|
- let fee_ppk = 10000; // Very high fee that might cause recursion
|
|
|
+ let fee_ppk = 10000;
|
|
|
+ let fee_and_amounts = (fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
|
|
|
- let result = amount.split_with_fee(fee_ppk, &amounts_ppk);
|
|
|
+ let result = amount.split_with_fee(&fee_and_amounts);
|
|
|
assert!(
|
|
|
result.is_ok(),
|
|
|
"split_with_fee should handle extreme fees without infinite recursion"
|
|
@@ -608,14 +651,16 @@ mod tests {
|
|
|
|
|
|
#[test]
|
|
|
fn test_split_values() {
|
|
|
- let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
|
|
|
+ let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
|
|
|
let amount = Amount(10);
|
|
|
|
|
|
let target = vec![Amount(2), Amount(4), Amount(4)];
|
|
|
|
|
|
let split_target = SplitTarget::Values(target.clone());
|
|
|
|
|
|
- let values = amount.split_targeted(&split_target, &amounts_ppk).unwrap();
|
|
|
+ let values = amount
|
|
|
+ .split_targeted(&split_target, &fee_and_amounts)
|
|
|
+ .unwrap();
|
|
|
|
|
|
assert_eq!(target, values);
|
|
|
|
|
@@ -623,13 +668,15 @@ mod tests {
|
|
|
|
|
|
let split_target = SplitTarget::Values(vec![Amount(2), Amount(4)]);
|
|
|
|
|
|
- let values = amount.split_targeted(&split_target, &amounts_ppk).unwrap();
|
|
|
+ let values = amount
|
|
|
+ .split_targeted(&split_target, &fee_and_amounts)
|
|
|
+ .unwrap();
|
|
|
|
|
|
assert_eq!(target, values);
|
|
|
|
|
|
let split_target = SplitTarget::Values(vec![Amount(2), Amount(10)]);
|
|
|
|
|
|
- let values = amount.split_targeted(&split_target, &amounts_ppk);
|
|
|
+ let values = amount.split_targeted(&split_target, &fee_and_amounts);
|
|
|
|
|
|
assert!(values.is_err())
|
|
|
}
|