apply max fee constrains

This commit is contained in:
Michael Sutton 2024-09-15 12:15:22 +03:00
parent 0beb9edf12
commit c0415eaaec
3 changed files with 48 additions and 31 deletions

View File

@ -61,7 +61,7 @@ func (s *server) BumpFee(_ context.Context, request *pb.BumpFeeRequest) (*pb.Bum
mass := s.txMassCalculator.CalculateTransactionOverallMass(domainTx) mass := s.txMassCalculator.CalculateTransactionOverallMass(domainTx)
feeRate := float64(entry.Entry.Fee) / float64(mass) feeRate := float64(entry.Entry.Fee) / float64(mass)
newFeeRate, err := s.calculateFeeRate(request.FeePolicy) newFeeRate, maxFee, err := s.calculateFeeLimits(request.FeePolicy)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -87,7 +87,7 @@ func (s *server) BumpFee(_ context.Context, request *pb.BumpFeeRequest) (*pb.Bum
for outpoint := range outpointsToInputs { for outpoint := range outpointsToInputs {
allowUsed[outpoint] = struct{}{} 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 { if err != nil {
return nil, err return nil, err
} }
@ -128,7 +128,7 @@ func (s *server) BumpFee(_ context.Context, request *pb.BumpFeeRequest) (*pb.Bum
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -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) // 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 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) ( func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.CreateUnsignedTransactionsRequest) (
*pb.CreateUnsignedTransactionsResponse, error, *pb.CreateUnsignedTransactionsResponse, error,
) { ) {
@ -34,23 +37,36 @@ func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.Creat
return &pb.CreateUnsignedTransactionsResponse{UnsignedTransactions: unsignedTransactions}, nil return &pb.CreateUnsignedTransactionsResponse{UnsignedTransactions: unsignedTransactions}, nil
} }
func (s *server) calculateFeeRate(requestFeePolicy *pb.FeePolicy) (float64, error) { func (s *server) calculateFeeLimits(requestFeePolicy *pb.FeePolicy) (feeRate float64, maxFee uint64, err error) {
var feeRate float64 feeRate = minFeeRate
maxFee = math.MaxUint64
switch requestFeePolicy := requestFeePolicy.FeePolicy.(type) { switch requestFeePolicy := requestFeePolicy.FeePolicy.(type) {
case *pb.FeePolicy_ExactFeeRate: case *pb.FeePolicy_ExactFeeRate:
feeRate = requestFeePolicy.ExactFeeRate feeRate = math.Max(requestFeePolicy.ExactFeeRate, minFeeRate)
case *pb.FeePolicy_MaxFeeRate: case *pb.FeePolicy_MaxFeeRate:
estimate, err := s.rpcClient.GetFeeEstimate() estimate, err := s.rpcClient.GetFeeEstimate()
if err != nil { if err != nil {
return 0, err return 0, 0, err
} }
feeRate = math.Min(estimate.Estimate.NormalBuckets[0].Feerate, requestFeePolicy.MaxFeeRate) feeRate = math.Min(estimate.Estimate.NormalBuckets[0].Feerate, requestFeePolicy.MaxFeeRate)
case *pb.FeePolicy_MaxFee: case *pb.FeePolicy_MaxFee:
// TODO estimate, err := s.rpcClient.GetFeeEstimate()
panic("todo") 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) { 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()) 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 { if err != nil {
return nil, err return nil, err
} }
@ -84,7 +100,7 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -110,19 +126,19 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
return unsignedTransactions, nil 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) { 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) { selectedUTXOs []*libkaspawallet.UTXO, totalReceived uint64, changeSompi uint64, err error) {
preSelectedSet := make(map[externalapi.DomainOutpoint]struct{}) preSelectedSet := make(map[externalapi.DomainOutpoint]struct{})
@ -168,7 +184,7 @@ func (s *server) selectUTXOsWithPreselected(preSelectedUTXOs []*walletUTXO, allo
totalValue += utxo.UTXOEntry.Amount() totalValue += utxo.UTXOEntry.Amount()
// We're overestimating a bit by assuming that any transaction will have a change output // 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 { if err != nil {
return false, err return false, err
} }
@ -227,7 +243,7 @@ func (s *server) selectUTXOsWithPreselected(preSelectedUTXOs []*walletUTXO, allo
return selectedUTXOs, totalReceived, totalValue - totalSpend, nil 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{} fakePubKey := [util.PublicKeySize]byte{}
fakeAddr, err := util.NewAddressPublicKey(fakePubKey[:], s.params.Prefix) fakeAddr, err := util.NewAddressPublicKey(fakePubKey[:], s.params.Prefix)
if err != nil { if err != nil {
@ -273,7 +289,7 @@ func (s *server) estimateFee(selectedUTXOs []*libkaspawallet.UTXO, feeRate float
return 0, err 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) { func (s *server) estimateFeePerInput(feeRate float64) (uint64, error) {

View File

@ -22,8 +22,8 @@ import (
// An additional `mergeTransaction` is generated - which merges the outputs of the above splits into a single output // An additional `mergeTransaction` is generated - which merges the outputs of the above splits into a single output
// paying to the original transaction's payee. // paying to the original transaction's payee.
func (s *server) maybeAutoCompoundTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address, func (s *server) maybeAutoCompoundTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address,
changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64) ([][]byte, error) { changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64, maxFee uint64) ([][]byte, error) {
splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress, feeRate) splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -44,6 +44,7 @@ func (s *server) mergeTransaction(
changeAddress util.Address, changeAddress util.Address,
changeWalletAddress *walletAddress, changeWalletAddress *walletAddress,
feeRate float64, feeRate float64,
maxFee uint64,
) (*serialization.PartiallySignedTransaction, error) { ) (*serialization.PartiallySignedTransaction, error) {
numOutputs := len(originalTransaction.Tx.Outputs) numOutputs := len(originalTransaction.Tx.Outputs)
if numOutputs > 2 || numOutputs == 0 { if numOutputs > 2 || numOutputs == 0 {
@ -70,7 +71,7 @@ func (s *server) mergeTransaction(
totalValue += output.Value totalValue += output.Value
} }
// We're overestimating a bit by assuming that any transaction will have a change output // 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 { if err != nil {
return nil, err return nil, err
} }
@ -104,7 +105,7 @@ func (s *server) mergeTransaction(
} }
func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address, 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) transactionMass, err := s.estimateComputeMassAfterSignatures(transaction)
if err != nil { if err != nil {
@ -115,7 +116,7 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia
return []*serialization.PartiallySignedTransaction{transaction}, nil 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 { if err != nil {
return nil, err return nil, err
} }
@ -125,19 +126,19 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia
startIndex := i * inputCountPerSplit startIndex := i * inputCountPerSplit
endIndex := startIndex + inputCountPerSplit endIndex := startIndex + inputCountPerSplit
var err error 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 { if err != nil {
return nil, err return nil, err
} }
} }
if len(splitTransactions) > 1 { 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 { if err != nil {
return nil, err return nil, err
} }
// Recursion will be 2-3 iterations deep even in the rarest` cases, so considered safe.. // 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 { if err != nil {
return nil, err 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. // 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, 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, // 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 // 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, // 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 // 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 { if err != nil {
return 0, 0, err return 0, 0, err
} }
@ -188,7 +189,7 @@ func (s *server) splitAndInputPerSplitCounts(transaction *serialization.Partiall
} }
func (s *server) createSplitTransaction(transaction *serialization.PartiallySignedTransaction, 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) selectedUTXOs := make([]*libkaspawallet.UTXO, 0, endIndex-startIndex)
totalSompi := uint64(0) totalSompi := uint64(0)
@ -206,7 +207,7 @@ func (s *server) createSplitTransaction(transaction *serialization.PartiallySign
totalSompi += selectedUTXOs[i-startIndex].UTXOEntry.Amount() totalSompi += selectedUTXOs[i-startIndex].UTXOEntry.Amount()
} }
if len(selectedUTXOs) != 0 { if len(selectedUTXOs) != 0 {
fee, err := s.estimateFee(selectedUTXOs, feeRate, false) fee, err := s.estimateFee(selectedUTXOs, feeRate, maxFee, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }