Skip to content

Commit

Permalink
Merge pull request #38 from elnosh/nut02-fees
Browse files Browse the repository at this point in the history
wallet: option to include fees
  • Loading branch information
elnosh authored Jul 17, 2024
2 parents 406686d + 8e559a0 commit 8e0c3dd
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 86 deletions.
4 changes: 2 additions & 2 deletions cmd/nutw/nutw.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
135 changes: 71 additions & 64 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 8e0c3dd

Please sign in to comment.