From bb507e4f9360b3c24cb485295d8312f0f6bb7010 Mon Sep 17 00:00:00 2001 From: elnosh Date: Wed, 17 Jul 2024 11:01:33 -0500 Subject: [PATCH 1/2] option to include fees when sending --- cmd/nutw/nutw.go | 4 +- wallet/wallet.go | 135 ++++++++++++++++-------------- wallet/wallet_integration_test.go | 26 +++--- 3 files changed, 86 insertions(+), 79 deletions(-) diff --git a/cmd/nutw/nutw.go b/cmd/nutw/nutw.go index 72b3585..903c989 100644 --- a/cmd/nutw/nutw.go +++ b/cmd/nutw/nutw.go @@ -311,12 +311,12 @@ func send(ctx *cli.Context) error { printErr(err) } - token, err = nutw.SendToPubkey(sendAmount, selectedMint, pubkey) + token, err = nutw.SendToPubkey(sendAmount, selectedMint, pubkey, true) if err != nil { printErr(err) } } else { - token, err = nutw.Send(sendAmount, selectedMint) + token, err = nutw.Send(sendAmount, selectedMint, true) if err != nil { printErr(err) } diff --git a/wallet/wallet.go b/wallet/wallet.go index ad1dfc8..9f42c56 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -416,13 +416,13 @@ func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { } // Send will return a cashu token with proofs for the given amount -func (w *Wallet) Send(amount uint64, mintURL string) (*cashu.Token, error) { +func (w *Wallet) Send(amount uint64, mintURL string, includeFees bool) (*cashu.Token, error) { selectedMint, ok := w.mints[mintURL] if !ok { return nil, ErrMintNotExist } - proofsToSend, err := w.getProofsForAmount(amount, &selectedMint, nil) + proofsToSend, err := w.getProofsForAmount(amount, &selectedMint, nil, includeFees) if err != nil { return nil, err } @@ -437,6 +437,7 @@ func (w *Wallet) SendToPubkey( amount uint64, mintURL string, pubkey *btcec.PublicKey, + includeFees bool, ) (*cashu.Token, error) { selectedMint, ok := w.mints[mintURL] if !ok { @@ -453,7 +454,7 @@ func (w *Wallet) SendToPubkey( return nil, errors.New("mint does not support Pay to Public Key") } - lockedProofs, err := w.getProofsForAmount(amount, &selectedMint, pubkey) + lockedProofs, err := w.getProofsForAmount(amount, &selectedMint, pubkey, includeFees) if err != nil { return nil, err } @@ -692,7 +693,7 @@ func (w *Wallet) Melt(invoice string, mintURL string) (*nut05.PostMeltQuoteBolt1 } amountNeeded := meltQuoteResponse.Amount + meltQuoteResponse.FeeReserve - proofs, err := w.getProofsForAmount(amountNeeded, &selectedMint, nil) + proofs, err := w.getProofsForAmount(amountNeeded, &selectedMint, nil, true) if err != nil { return nil, err } @@ -774,9 +775,13 @@ func (w *Wallet) getActiveProofsByMint(mintURL string) cashu.Proofs { } // selectProofsToSend will try to select proofs for -// amount + fees (so that receiver does not pay for fees) -// TODO: add extra argument to specify whether or not to add fees for the receiver -func (w *Wallet) selectProofsToSend(proofs cashu.Proofs, amount uint64, mint *walletMint) (cashu.Proofs, error) { +// amount + fees (if includeFees is true) +func (w *Wallet) selectProofsToSend( + proofs cashu.Proofs, + amount uint64, + mint *walletMint, + includeFees bool, +) (cashu.Proofs, error) { proofsSum := proofs.Amount() if proofsSum < amount { return nil, ErrInsufficientMintBalance @@ -812,13 +817,17 @@ func (w *Wallet) selectProofsToSend(proofs cashu.Proofs, amount uint64, mint *wa selectedProofs = append(selectedProofs, selectedProof) selectedProofsSum += selectedProof.Amount - fees := w.fees(selectedProofs, mint) - if selectedProof.Amount >= remainingAmount+uint64(fees) { + var fees uint64 = 0 + if includeFees { + fees = uint64(w.fees(selectedProofs, mint)) + } + + if selectedProof.Amount >= remainingAmount+fees { break } - remainingAmount = amount + uint64(fees) - selectedProofsSum + remainingAmount = amount + fees - selectedProofsSum var tempSmaller cashu.Proofs for _, small := range smallerProofs { if small.Amount <= remainingAmount { @@ -831,70 +840,50 @@ func (w *Wallet) selectProofsToSend(proofs cashu.Proofs, amount uint64, mint *wa smallerProofs = tempSmaller } - if selectedProofsSum < amount+uint64(w.fees(selectedProofs, mint)) { + var fees uint64 = 0 + if includeFees { + fees = uint64(w.fees(selectedProofs, mint)) + } + + if selectedProofsSum < amount+fees { return nil, ErrInsufficientMintBalance } return selectedProofs, nil } -func (w *Wallet) swapToSend(amount uint64, mint *walletMint, pubkeyLock *btcec.PublicKey) (cashu.Proofs, error) { - proofs := w.getProofsFromMint(mint.mintURL) - proofsToSwap, err := w.selectProofsToSend(proofs, amount, mint) - if err != nil { - return nil, err - } - - // add inactive proofs in the swap to get rid of those - inactiveProofs := w.getInactiveProofsByMint(mint.mintURL) - inactiveProofs = slices.DeleteFunc(inactiveProofs, func(proof cashu.Proof) bool { - selected := false - for _, selectedProof := range proofsToSwap { - if selectedProof.Secret == proof.Secret { - selected = true - break - } - } - return selected - }) - proofsToSwap = append(proofsToSwap, inactiveProofs...) - - fees := w.fees(proofsToSwap, mint) - // if amount from selected proofs is the exact amount needed - // but pubkeyLock is present, need to add fees for the swap - // to create locked ecash - if proofsToSwap.Amount() == amount+uint64(fees) && pubkeyLock != nil { - proofs = slices.DeleteFunc(proofs, func(proof cashu.Proof) bool { - selected := false - for _, selectedProof := range proofsToSwap { - if selectedProof.Secret == proof.Secret { - selected = true - break - } - } - return selected - }) - - extraProofs, err := w.selectProofsToSend(proofs, uint64(fees), mint) - if err != nil { - return nil, err - } - proofsToSwap = append(proofsToSwap, extraProofs...) - } - +func (w *Wallet) swapToSend( + amount uint64, + mint *walletMint, + pubkeyLock *btcec.PublicKey, + includeFees bool, +) (cashu.Proofs, error) { var activeSatKeyset crypto.WalletKeyset for _, k := range mint.activeKeysets { activeSatKeyset = k break } + splitForSendAmount := cashu.AmountSplit(amount) + var feesToReceive uint = 0 + if includeFees { + feesToReceive = feesForCount(len(splitForSendAmount)+1, &activeSatKeyset) + amount += uint64(feesToReceive) + } + + proofs := w.getProofsFromMint(mint.mintURL) + proofsToSwap, err := w.selectProofsToSend(proofs, amount, mint, true) + if err != nil { + return nil, err + } + var send, change cashu.BlindedMessages var secrets, changeSecrets []string var rs, changeRs []*secp256k1.PrivateKey var counter, incrementCounterBy uint32 - amountNeededForSend := amount + uint64(fees) - split := cashu.AmountSplit(amountNeededForSend) + split := append(splitForSendAmount, cashu.AmountSplit(uint64(feesToReceive))...) + slices.Sort(split) if pubkeyLock == nil { counter = w.counterForKeyset(activeSatKeyset.Id) // blinded messages for send amount from counter @@ -914,10 +903,10 @@ func (w *Wallet) swapToSend(amount uint64, mint *walletMint, pubkeyLock *btcec.P } proofsAmount := proofsToSwap.Amount() - fees = w.fees(proofsToSwap, mint) + fees := w.fees(proofsToSwap, mint) // blinded messages for change amount - if proofsAmount-amountNeededForSend-uint64(fees) > 0 { - changeAmount := proofsAmount - amountNeededForSend - uint64(fees) + if proofsAmount-amount-uint64(fees) > 0 { + changeAmount := proofsAmount - amount - uint64(fees) changeSplit := w.splitWalletTarget(changeAmount, mint.mintURL) change, changeSecrets, changeRs, err = w.createBlindedMessages(changeSplit, activeSatKeyset.Id, &counter) if err != nil { @@ -976,15 +965,23 @@ func (w *Wallet) swapToSend(amount uint64, mint *walletMint, pubkeyLock *btcec.P // getProofsForAmount will return proofs from mint for the give amount. // if pubkeyLock is present it will generate proofs locked to the public key. // It returns error if wallet does not have enough proofs to fulfill amount -func (w *Wallet) getProofsForAmount(amount uint64, mint *walletMint, pubkeyLock *btcec.PublicKey) (cashu.Proofs, error) { +func (w *Wallet) getProofsForAmount( + amount uint64, + mint *walletMint, + pubkeyLock *btcec.PublicKey, + includeFees bool, +) (cashu.Proofs, error) { // TODO: need to check first if 'input_fee_ppk' for keyset has changed mintProofs := w.getProofsFromMint(mint.mintURL) - selectedProofs, err := w.selectProofsToSend(mintProofs, amount, mint) + selectedProofs, err := w.selectProofsToSend(mintProofs, amount, mint, includeFees) if err != nil { return nil, err } - fees := w.fees(selectedProofs, mint) + var fees uint64 = 0 + if includeFees { + fees = uint64(w.fees(selectedProofs, mint)) + } totalAmount := amount + uint64(fees) // only try selecting offline if lock is not specified @@ -1000,7 +997,9 @@ func (w *Wallet) getProofsForAmount(amount uint64, mint *walletMint, pubkeyLock } } - proofsToSend, err := w.swapToSend(amount, mint, pubkeyLock) + // if offline selection did not work or needed to do swap + // to lock the ecash, swap proofs to then send + proofsToSend, err := w.swapToSend(amount, mint, pubkeyLock, includeFees) if err != nil { return nil, err } @@ -1080,6 +1079,14 @@ func (w *Wallet) fees(proofs cashu.Proofs, mint *walletMint) uint { return (fees + 999) / 1000 } +func feesForCount(count int, keyset *crypto.WalletKeyset) uint { + var fees uint = 0 + for i := 0; i < count; i++ { + fees += keyset.InputFeePpk + } + return (fees + 999) / 1000 +} + // returns Blinded messages, secrets - [][]byte, and list of r // if counter is nil, it generates random secrets // if counter is non-nil, it will generate secrets deterministically diff --git a/wallet/wallet_integration_test.go b/wallet/wallet_integration_test.go index 7e1fc48..37081bd 100644 --- a/wallet/wallet_integration_test.go +++ b/wallet/wallet_integration_test.go @@ -165,7 +165,7 @@ func TestSend(t *testing.T) { } var sendAmount uint64 = 4200 - token, err := testWallet.Send(sendAmount, mintURL) + token, err := testWallet.Send(sendAmount, mintURL, true) if err != nil { t.Fatalf("got unexpected error: %v", err) } @@ -174,13 +174,13 @@ func TestSend(t *testing.T) { } // test with invalid mint - _, err = testWallet.Send(sendAmount, "http://nonexistent.mint") + _, err = testWallet.Send(sendAmount, "http://nonexistent.mint", true) if !errors.Is(err, wallet.ErrMintNotExist) { t.Fatalf("expected error '%v' but got error '%v'", wallet.ErrMintNotExist, err) } // insufficient balance in wallet - _, err = testWallet.Send(2000000, mintURL) + _, err = testWallet.Send(2000000, mintURL, true) if !errors.Is(err, wallet.ErrInsufficientMintBalance) { t.Fatalf("expected error '%v' but got error '%v'", wallet.ErrInsufficientMintBalance, err) } @@ -202,7 +202,7 @@ func TestSend(t *testing.T) { } sendAmount = 2000 - token, err = feesWallet.Send(sendAmount, mintWithFeesURL) + token, err = feesWallet.Send(sendAmount, mintWithFeesURL, true) if err != nil { t.Fatalf("got unexpected error: %v", err) } @@ -257,7 +257,7 @@ func TestReceive(t *testing.T) { t.Fatalf("error funding wallet: %v", err) } - token, err := testWallet2.Send(1500, mint2URL) + token, err := testWallet2.Send(1500, mint2URL, true) if err != nil { t.Fatalf("got unexpected error in send: %v", err) } @@ -277,7 +277,7 @@ func TestReceive(t *testing.T) { t.Fatalf("expected '%v' in list of trusted of trusted mints", defaultMint) } - token2, err := testWallet2.Send(1500, mint2URL) + token2, err := testWallet2.Send(1500, mint2URL, true) if err != nil { t.Fatalf("got unexpected error in send: %v", err) } @@ -325,7 +325,7 @@ func TestReceiveFees(t *testing.T) { }() var sendAmount uint64 = 2000 - token, err := testWallet.Send(sendAmount, mintURL) + token, err := testWallet.Send(sendAmount, mintURL, true) if err != nil { t.Fatalf("got unexpected error in send: %v", err) } @@ -466,7 +466,7 @@ func TestWalletBalance(t *testing.T) { balance := balanceTestWallet.GetBalance() // test balance after send var sendAmount uint64 = 1200 - _, err = balanceTestWallet.Send(sendAmount, mintURL) + _, err = balanceTestWallet.Send(sendAmount, mintURL, true) if err != nil { t.Fatalf("unexpected error in send: %v", err) } @@ -525,7 +525,7 @@ func TestWalletBalanceFees(t *testing.T) { for _, sendAmount := range sendAmounts { balance := balanceTestWallet.GetBalance() // test balance after send - token, err := balanceTestWallet.Send(sendAmount, mintURL) + token, err := balanceTestWallet.Send(sendAmount, mintURL, true) if err != nil { t.Fatalf("unexpected error in send: %v", err) } @@ -595,7 +595,7 @@ func TestSendToPubkey(t *testing.T) { receiverPubkey := testWallet2.GetReceivePubkey() - lockedEcash, err := testWallet.SendToPubkey(500, nutshellURL, receiverPubkey) + lockedEcash, err := testWallet.SendToPubkey(500, nutshellURL, receiverPubkey, true) if err != nil { t.Fatalf("unexpected error generating locked ecash: %v", err) } @@ -622,7 +622,7 @@ func TestSendToPubkey(t *testing.T) { t.Fatalf("expected balance of '%v' but got '%v' instead", amountReceived, balance) } - lockedEcash, err = testWallet.SendToPubkey(500, nutshellURL, receiverPubkey) + lockedEcash, err = testWallet.SendToPubkey(500, nutshellURL, receiverPubkey, true) if err != nil { t.Fatalf("unexpected error generating locked ecash: %v", err) } @@ -676,7 +676,7 @@ func TestWalletRestore(t *testing.T) { } var sendAmount1 uint64 = 5000 - token, err := testWallet.Send(sendAmount1, mintURL) + token, err := testWallet.Send(sendAmount1, mintURL, true) if err != nil { t.Fatalf("unexpected error in send: %v", err) } @@ -687,7 +687,7 @@ func TestWalletRestore(t *testing.T) { } var sendAmount2 uint64 = 1000 - token, err = testWallet.Send(sendAmount2, mintURL) + token, err = testWallet.Send(sendAmount2, mintURL, true) if err != nil { t.Fatalf("unexpected error in send: %v", err) } From 8e559a0bd7edb853f1efeba50be1894d97578475 Mon Sep 17 00:00:00 2001 From: elnosh Date: Wed, 17 Jul 2024 11:43:36 -0500 Subject: [PATCH 2/2] test include fees option in wallet --- wallet/wallet_integration_test.go | 39 +++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/wallet/wallet_integration_test.go b/wallet/wallet_integration_test.go index 37081bd..32776c0 100644 --- a/wallet/wallet_integration_test.go +++ b/wallet/wallet_integration_test.go @@ -214,6 +214,15 @@ func TestSend(t *testing.T) { if token.TotalAmount() != sendAmount+uint64(fees) { t.Fatalf("expected token amount of '%v' but got '%v' instead", sendAmount+uint64(fees), token.TotalAmount()) } + + // send without fees to receive + token, err = feesWallet.Send(sendAmount, mintWithFeesURL, false) + if err != nil { + t.Fatalf("got unexpected error: %v", err) + } + if token.TotalAmount() != sendAmount { + t.Fatalf("expected token amount of '%v' but got '%v' instead", sendAmount+uint64(fees), token.TotalAmount()) + } } func TestReceive(t *testing.T) { @@ -523,19 +532,33 @@ func TestWalletBalanceFees(t *testing.T) { sendAmounts := []uint64{1200, 2000, 5000} for _, sendAmount := range sendAmounts { - balance := balanceTestWallet.GetBalance() - // test balance after send token, err := balanceTestWallet.Send(sendAmount, mintURL, true) if err != nil { t.Fatalf("unexpected error in send: %v", err) } - fees, err := testutils.Fees(token.Token[0].Proofs, mintURL) + + // test balance in receiving wallet + balanceBeforeReceive := balanceTestWallet2.GetBalance() + _, err = balanceTestWallet2.Receive(*token, false) if err != nil { t.Fatalf("got unexpected error: %v", err) } - expectedBalance := balance - sendAmount - uint64(fees) - if balanceTestWallet.GetBalance() != expectedBalance { - t.Fatalf("expected balance of '%v' but got '%v' instead", expectedBalance, balanceTestWallet.GetBalance()) + expectedBalance := balanceBeforeReceive + sendAmount + if balanceTestWallet2.GetBalance() != expectedBalance { + t.Fatalf("expected balance of '%v' but got '%v' instead", expectedBalance, balanceTestWallet2.GetBalance()) + } + } + + // test without including fees in send + for _, sendAmount := range sendAmounts { + token, err := balanceTestWallet.Send(sendAmount, mintURL, false) + if err != nil { + t.Fatalf("unexpected error in send: %v", err) + } + + fees, err := testutils.Fees(token.Token[0].Proofs, mintURL) + if err != nil { + t.Fatalf("got unexpected error: %v", err) } // test balance in receiving wallet @@ -544,7 +567,9 @@ func TestWalletBalanceFees(t *testing.T) { if err != nil { t.Fatalf("got unexpected error: %v", err) } - expectedBalance = balanceBeforeReceive + token.TotalAmount() - uint64(fees) + // expected balance should be the sending amount minus fees + // since those were not included + expectedBalance := balanceBeforeReceive + sendAmount - uint64(fees) if balanceTestWallet2.GetBalance() != expectedBalance { t.Fatalf("expected balance of '%v' but got '%v' instead", expectedBalance, balanceTestWallet2.GetBalance()) }