diff --git a/cmd/kaspawallet/daemon/server/bump_fee.go b/cmd/kaspawallet/daemon/server/bump_fee.go index 85227bb05..7e39257af 100644 --- a/cmd/kaspawallet/daemon/server/bump_fee.go +++ b/cmd/kaspawallet/daemon/server/bump_fee.go @@ -61,7 +61,7 @@ func (s *server) BumpFee(_ context.Context, request *pb.BumpFeeRequest) (*pb.Bum mass := s.txMassCalculator.CalculateTransactionOverallMass(domainTx) feeRate := float64(entry.Entry.Fee) / float64(mass) - newFeeRate, err := s.calculateFeeRate(request.FeePolicy) + newFeeRate, maxFee, err := s.calculateFeeLimits(request.FeePolicy) if err != nil { return nil, err } @@ -87,7 +87,7 @@ func (s *server) BumpFee(_ context.Context, request *pb.BumpFeeRequest) (*pb.Bum for outpoint := range outpointsToInputs { allowUsed[outpoint] = struct{}{} } - selectedUTXOs, spendValue, changeSompi, err := s.selectUTXOsWithPreselected([]*walletUTXO{maxUTXO}, allowUsed, domainTx.Outputs[0].Value, false, newFeeRate, fromAddresses) + selectedUTXOs, spendValue, changeSompi, err := s.selectUTXOsWithPreselected([]*walletUTXO{maxUTXO}, allowUsed, domainTx.Outputs[0].Value, false, newFeeRate, maxFee, fromAddresses) if err != nil { return nil, err } @@ -128,7 +128,7 @@ func (s *server) BumpFee(_ context.Context, request *pb.BumpFeeRequest) (*pb.Bum return nil, err } - unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress, newFeeRate) + unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress, newFeeRate, maxFee) if err != nil { return nil, err } diff --git a/cmd/kaspawallet/daemon/server/create_unsigned_transaction.go b/cmd/kaspawallet/daemon/server/create_unsigned_transaction.go index e7dea3b69..b6a4b08cd 100644 --- a/cmd/kaspawallet/daemon/server/create_unsigned_transaction.go +++ b/cmd/kaspawallet/daemon/server/create_unsigned_transaction.go @@ -19,6 +19,9 @@ import ( // should succeed (at most 50K storage mass for each output, thus overall lower than standard mass upper bound which is 100K gram) const minChangeTarget = constants.SompiPerKaspa / 5 +// The current minimal fee rate according to mempool standards +const minFeeRate = 1.0 + func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.CreateUnsignedTransactionsRequest) ( *pb.CreateUnsignedTransactionsResponse, error, ) { @@ -34,23 +37,36 @@ func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.Creat return &pb.CreateUnsignedTransactionsResponse{UnsignedTransactions: unsignedTransactions}, nil } -func (s *server) calculateFeeRate(requestFeePolicy *pb.FeePolicy) (float64, error) { - var feeRate float64 +func (s *server) calculateFeeLimits(requestFeePolicy *pb.FeePolicy) (feeRate float64, maxFee uint64, err error) { + feeRate = minFeeRate + maxFee = math.MaxUint64 switch requestFeePolicy := requestFeePolicy.FeePolicy.(type) { case *pb.FeePolicy_ExactFeeRate: - feeRate = requestFeePolicy.ExactFeeRate + feeRate = math.Max(requestFeePolicy.ExactFeeRate, minFeeRate) case *pb.FeePolicy_MaxFeeRate: estimate, err := s.rpcClient.GetFeeEstimate() if err != nil { - return 0, err + return 0, 0, err } feeRate = math.Min(estimate.Estimate.NormalBuckets[0].Feerate, requestFeePolicy.MaxFeeRate) case *pb.FeePolicy_MaxFee: - // TODO - panic("todo") + estimate, err := s.rpcClient.GetFeeEstimate() + if err != nil { + return 0, 0, err + } + feeRate = estimate.Estimate.NormalBuckets[0].Feerate + maxFee = requestFeePolicy.MaxFee + case nil: + estimate, err := s.rpcClient.GetFeeEstimate() + if err != nil { + return 0, 0, err + } + feeRate = estimate.Estimate.NormalBuckets[0].Feerate + // Default to a bound of max 1 KAS as fee + maxFee = constants.SompiPerKaspa } - return feeRate, nil + return feeRate, maxFee, nil } func (s *server) createUnsignedTransactions(address string, amount uint64, isSendAll bool, fromAddressesString []string, useExistingChangeAddress bool, requestFeePolicy *pb.FeePolicy) ([][]byte, error) { @@ -58,7 +74,7 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen return nil, errors.Errorf("wallet daemon is not synced yet, %s", s.formatSyncStateReport()) } - feeRate, err := s.calculateFeeRate(requestFeePolicy) + feeRate, maxFee, err := s.calculateFeeLimits(requestFeePolicy) if err != nil { return nil, err } @@ -84,7 +100,7 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen return nil, err } - selectedUTXOs, spendValue, changeSompi, err := s.selectUTXOs(amount, isSendAll, feeRate, fromAddresses) + selectedUTXOs, spendValue, changeSompi, err := s.selectUTXOs(amount, isSendAll, feeRate, maxFee, fromAddresses) if err != nil { return nil, err } @@ -110,19 +126,19 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen return nil, err } - unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress, feeRate) + unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee) if err != nil { return nil, err } return unsignedTransactions, nil } -func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feeRate float64, fromAddresses []*walletAddress) ( +func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feeRate float64, maxFee uint64, fromAddresses []*walletAddress) ( selectedUTXOs []*libkaspawallet.UTXO, totalReceived uint64, changeSompi uint64, err error) { - return s.selectUTXOsWithPreselected(nil, map[externalapi.DomainOutpoint]struct{}{}, spendAmount, isSendAll, feeRate, fromAddresses) + return s.selectUTXOsWithPreselected(nil, map[externalapi.DomainOutpoint]struct{}{}, spendAmount, isSendAll, feeRate, maxFee, fromAddresses) } -func (s *server) selectUTXOsWithPreselected(preSelectedUTXOs []*walletUTXO, allowUsed map[externalapi.DomainOutpoint]struct{}, spendAmount uint64, isSendAll bool, feeRate float64, fromAddresses []*walletAddress) ( +func (s *server) selectUTXOsWithPreselected(preSelectedUTXOs []*walletUTXO, allowUsed map[externalapi.DomainOutpoint]struct{}, spendAmount uint64, isSendAll bool, feeRate float64, maxFee uint64, fromAddresses []*walletAddress) ( selectedUTXOs []*libkaspawallet.UTXO, totalReceived uint64, changeSompi uint64, err error) { preSelectedSet := make(map[externalapi.DomainOutpoint]struct{}) @@ -168,7 +184,7 @@ func (s *server) selectUTXOsWithPreselected(preSelectedUTXOs []*walletUTXO, allo totalValue += utxo.UTXOEntry.Amount() // We're overestimating a bit by assuming that any transaction will have a change output - fee, err = s.estimateFee(selectedUTXOs, feeRate, true) + fee, err = s.estimateFee(selectedUTXOs, feeRate, maxFee, true) if err != nil { return false, err } @@ -227,7 +243,7 @@ func (s *server) selectUTXOsWithPreselected(preSelectedUTXOs []*walletUTXO, allo return selectedUTXOs, totalReceived, totalValue - totalSpend, nil } -func (s *server) estimateFee(selectedUTXOs []*libkaspawallet.UTXO, feeRate float64, assumeChange bool) (uint64, error) { +func (s *server) estimateFee(selectedUTXOs []*libkaspawallet.UTXO, feeRate float64, maxFee uint64, assumeChange bool) (uint64, error) { fakePubKey := [util.PublicKeySize]byte{} fakeAddr, err := util.NewAddressPublicKey(fakePubKey[:], s.params.Prefix) if err != nil { @@ -273,7 +289,7 @@ func (s *server) estimateFee(selectedUTXOs []*libkaspawallet.UTXO, feeRate float return 0, err } - return uint64(float64(mass) * feeRate), nil + return min(uint64(math.Ceil(float64(mass)*feeRate)), maxFee), nil } func (s *server) estimateFeePerInput(feeRate float64) (uint64, error) { diff --git a/cmd/kaspawallet/daemon/server/split_transaction.go b/cmd/kaspawallet/daemon/server/split_transaction.go index 330eaf875..3907cc82d 100644 --- a/cmd/kaspawallet/daemon/server/split_transaction.go +++ b/cmd/kaspawallet/daemon/server/split_transaction.go @@ -22,8 +22,8 @@ import ( // An additional `mergeTransaction` is generated - which merges the outputs of the above splits into a single output // paying to the original transaction's payee. func (s *server) maybeAutoCompoundTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address, - changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64) ([][]byte, error) { - splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress, feeRate) + changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64, maxFee uint64) ([][]byte, error) { + splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee) if err != nil { return nil, err } @@ -44,6 +44,7 @@ func (s *server) mergeTransaction( changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64, + maxFee uint64, ) (*serialization.PartiallySignedTransaction, error) { numOutputs := len(originalTransaction.Tx.Outputs) if numOutputs > 2 || numOutputs == 0 { @@ -70,7 +71,7 @@ func (s *server) mergeTransaction( totalValue += output.Value } // We're overestimating a bit by assuming that any transaction will have a change output - fee, err := s.estimateFee(utxos, feeRate, true) + fee, err := s.estimateFee(utxos, feeRate, maxFee, true) if err != nil { return nil, err } @@ -104,7 +105,7 @@ func (s *server) mergeTransaction( } func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address, - changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64) ([]*serialization.PartiallySignedTransaction, error) { + changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64, maxFee uint64) ([]*serialization.PartiallySignedTransaction, error) { transactionMass, err := s.estimateComputeMassAfterSignatures(transaction) if err != nil { @@ -115,7 +116,7 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia return []*serialization.PartiallySignedTransaction{transaction}, nil } - splitCount, inputCountPerSplit, err := s.splitAndInputPerSplitCounts(transaction, transactionMass, changeAddress, feeRate) + splitCount, inputCountPerSplit, err := s.splitAndInputPerSplitCounts(transaction, transactionMass, changeAddress, feeRate, maxFee) if err != nil { return nil, err } @@ -125,19 +126,19 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia startIndex := i * inputCountPerSplit endIndex := startIndex + inputCountPerSplit var err error - splitTransactions[i], err = s.createSplitTransaction(transaction, changeAddress, startIndex, endIndex, feeRate) + splitTransactions[i], err = s.createSplitTransaction(transaction, changeAddress, startIndex, endIndex, feeRate, maxFee) if err != nil { return nil, err } } if len(splitTransactions) > 1 { - mergeTransaction, err := s.mergeTransaction(splitTransactions, transaction, toAddress, changeAddress, changeWalletAddress, feeRate) + mergeTransaction, err := s.mergeTransaction(splitTransactions, transaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee) if err != nil { return nil, err } // Recursion will be 2-3 iterations deep even in the rarest` cases, so considered safe.. - splitMergeTransaction, err := s.maybeSplitAndMergeTransaction(mergeTransaction, toAddress, changeAddress, changeWalletAddress, feeRate) + splitMergeTransaction, err := s.maybeSplitAndMergeTransaction(mergeTransaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee) if err != nil { return nil, err } @@ -150,7 +151,7 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia // splitAndInputPerSplitCounts calculates the number of splits to create, and the number of inputs to assign per split. func (s *server) splitAndInputPerSplitCounts(transaction *serialization.PartiallySignedTransaction, transactionMass uint64, - changeAddress util.Address, feeRate float64) (splitCount, inputsPerSplitCount int, err error) { + changeAddress util.Address, feeRate float64, maxFee uint64) (splitCount, inputsPerSplitCount int, err error) { // Create a dummy transaction which is a clone of the original transaction, but without inputs, // to calculate how much mass do all the inputs have @@ -170,7 +171,7 @@ func (s *server) splitAndInputPerSplitCounts(transaction *serialization.Partiall // Create another dummy transaction, this time one similar to the split transactions we wish to generate, // but with 0 inputs, to calculate how much mass for inputs do we have available in the split transactions - splitTransactionWithoutInputs, err := s.createSplitTransaction(transaction, changeAddress, 0, 0, feeRate) + splitTransactionWithoutInputs, err := s.createSplitTransaction(transaction, changeAddress, 0, 0, feeRate, maxFee) if err != nil { return 0, 0, err } @@ -188,7 +189,7 @@ func (s *server) splitAndInputPerSplitCounts(transaction *serialization.Partiall } func (s *server) createSplitTransaction(transaction *serialization.PartiallySignedTransaction, - changeAddress util.Address, startIndex int, endIndex int, feeRate float64) (*serialization.PartiallySignedTransaction, error) { + changeAddress util.Address, startIndex int, endIndex int, feeRate float64, maxFee uint64) (*serialization.PartiallySignedTransaction, error) { selectedUTXOs := make([]*libkaspawallet.UTXO, 0, endIndex-startIndex) totalSompi := uint64(0) @@ -206,7 +207,7 @@ func (s *server) createSplitTransaction(transaction *serialization.PartiallySign totalSompi += selectedUTXOs[i-startIndex].UTXOEntry.Amount() } if len(selectedUTXOs) != 0 { - fee, err := s.estimateFee(selectedUTXOs, feeRate, false) + fee, err := s.estimateFee(selectedUTXOs, feeRate, maxFee, false) if err != nil { return nil, err }