Add fee estimation to wallet

This commit is contained in:
Ori Newman 2024-09-04 14:50:16 +03:00
parent 48a142e12f
commit 78ca616b1f
25 changed files with 5352 additions and 1304 deletions

View File

@ -163,6 +163,8 @@ const (
CmdGetMempoolEntriesByAddressesResponseMessage CmdGetMempoolEntriesByAddressesResponseMessage
CmdGetCoinSupplyRequestMessage CmdGetCoinSupplyRequestMessage
CmdGetCoinSupplyResponseMessage CmdGetCoinSupplyResponseMessage
CmdGetFeeEstimateRequestMessage
CmdGetFeeEstimateResponseMessage
) )
// ProtocolMessageCommandToString maps all MessageCommands to their string representation // ProtocolMessageCommandToString maps all MessageCommands to their string representation
@ -300,6 +302,8 @@ var RPCMessageCommandToString = map[MessageCommand]string{
CmdGetMempoolEntriesByAddressesResponseMessage: "GetMempoolEntriesByAddressesResponse", CmdGetMempoolEntriesByAddressesResponseMessage: "GetMempoolEntriesByAddressesResponse",
CmdGetCoinSupplyRequestMessage: "GetCoinSupplyRequest", CmdGetCoinSupplyRequestMessage: "GetCoinSupplyRequest",
CmdGetCoinSupplyResponseMessage: "GetCoinSupplyResponse", CmdGetCoinSupplyResponseMessage: "GetCoinSupplyResponse",
CmdGetFeeEstimateRequestMessage: "GetFeeEstimateRequest",
CmdGetFeeEstimateResponseMessage: "GetFeeEstimateResponse",
} }
// Message is an interface that describes a kaspa message. A type that // Message is an interface that describes a kaspa message. A type that

View File

@ -0,0 +1,47 @@
package appmessage
// GetFeeEstimateRequestMessage is an appmessage corresponding to
// its respective RPC message
type GetFeeEstimateRequestMessage struct {
baseMessage
}
// Command returns the protocol command string for the message
func (msg *GetFeeEstimateRequestMessage) Command() MessageCommand {
return CmdGetFeeEstimateRequestMessage
}
// NewGetFeeEstimateRequestMessage returns a instance of the message
func NewGetFeeEstimateRequestMessage() *GetFeeEstimateRequestMessage {
return &GetFeeEstimateRequestMessage{}
}
type RPCFeeRateBucket struct {
Feerate float64
EstimatedSeconds float64
}
type RPCFeeEstimate struct {
PriorityBucket RPCFeeRateBucket
NormalBuckets []RPCFeeRateBucket
LowBuckets []RPCFeeRateBucket
}
// GetCoinSupplyResponseMessage is an appmessage corresponding to
// its respective RPC message
type GetFeeEstimateResponseMessage struct {
baseMessage
Estimate RPCFeeEstimate
Error *RPCError
}
// Command returns the protocol command string for the message
func (msg *GetFeeEstimateResponseMessage) Command() MessageCommand {
return CmdGetFeeEstimateResponseMessage
}
// NewGetFeeEstimateResponseMessage returns a instance of the message
func NewGetFeeEstimateResponseMessage() *GetFeeEstimateResponseMessage {
return &GetFeeEstimateResponseMessage{}
}

View File

@ -1,9 +1,10 @@
package main package main
import ( import (
"os"
"github.com/kaspanet/kaspad/infrastructure/config" "github.com/kaspanet/kaspad/infrastructure/config"
"github.com/pkg/errors" "github.com/pkg/errors"
"os"
"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
) )
@ -62,6 +63,8 @@ type sendConfig struct {
SendAmount string `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"` SendAmount string `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"`
IsSendAll bool `long:"send-all" description:"Send all the Kaspa in the wallet (mutually exclusive with --send-amount). If --from-address was used, will send all only from the specified addresses."` IsSendAll bool `long:"send-all" description:"Send all the Kaspa in the wallet (mutually exclusive with --send-amount). If --from-address was used, will send all only from the specified addresses."`
UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"` UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"`
MaxFeeRate float64 `long:"max-fee-rate" short:"m" description:"Maximum fee rate in Sompi/gram to use for the transaction. The wallet will take the maximum between the fee estimate from the connected node and this value."`
FeeRate float64 `long:"fee-rate" short:"r" description:"Fee rate in Sompi/gram to use for the transaction. This option will override any fee estimate from the connected node."`
Verbose bool `long:"show-serialized" short:"s" description:"Show a list of hex encoded sent transactions"` Verbose bool `long:"show-serialized" short:"s" description:"Show a list of hex encoded sent transactions"`
config.NetworkFlags config.NetworkFlags
} }
@ -79,6 +82,8 @@ type createUnsignedTransactionConfig struct {
SendAmount string `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"` SendAmount string `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"`
IsSendAll bool `long:"send-all" description:"Send all the Kaspa in the wallet (mutually exclusive with --send-amount)"` IsSendAll bool `long:"send-all" description:"Send all the Kaspa in the wallet (mutually exclusive with --send-amount)"`
UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"` UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"`
MaxFeeRate float64 `long:"max-fee-rate" short:"m" description:"Maximum fee rate in Sompi/gram to use for the transaction. The wallet will take the maximum between the fee estimate from the connected node and this value."`
FeeRate float64 `long:"fee-rate" short:"r" description:"Fee rate in Sompi/gram to use for the transaction. This option will override any fee estimate from the connected node."`
config.NetworkFlags config.NetworkFlags
} }
@ -316,6 +321,19 @@ func validateCreateUnsignedTransactionConf(conf *createUnsignedTransactionConfig
return errors.New("exactly one of '--send-amount' or '--all' must be specified") return errors.New("exactly one of '--send-amount' or '--all' must be specified")
} }
if conf.MaxFeeRate < 0 {
return errors.New("--max-fee-rate must be a positive number")
}
if conf.FeeRate < 0 {
return errors.New("--fee-rate must be a positive number")
}
if conf.MaxFeeRate > 0 && conf.FeeRate > 0 {
return errors.New("at most one of '--max-fee-rate' or '--fee-rate' can be specified")
}
return nil return nil
} }
@ -325,6 +343,19 @@ func validateSendConfig(conf *sendConfig) error {
return errors.New("exactly one of '--send-amount' or '--all' must be specified") return errors.New("exactly one of '--send-amount' or '--all' must be specified")
} }
if conf.MaxFeeRate < 0 {
return errors.New("--max-fee-rate must be a positive number")
}
if conf.FeeRate < 0 {
return errors.New("--fee-rate must be a positive number")
}
if conf.MaxFeeRate > 0 && conf.FeeRate > 0 {
return errors.New("at most one of '--max-fee-rate' or '--fee-rate' can be specified")
}
return nil return nil
} }

View File

@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"os" "os"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
@ -26,12 +27,22 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
return err return err
} }
feeRate := &pb.FeeRate{
FeeRate: &pb.FeeRate_Max{Max: math.MaxFloat64},
}
if conf.FeeRate > 0 {
feeRate.FeeRate = &pb.FeeRate_Exact{Exact: conf.FeeRate}
} else if conf.MaxFeeRate > 0 {
feeRate.FeeRate = &pb.FeeRate_Max{Max: conf.MaxFeeRate}
}
response, err := daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{ response, err := daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{
From: conf.FromAddresses, From: conf.FromAddresses,
Address: conf.ToAddress, Address: conf.ToAddress,
Amount: sendAmountSompi, Amount: sendAmountSompi,
IsSendAll: conf.IsSendAll, IsSendAll: conf.IsSendAll,
UseExistingChangeAddress: conf.UseExistingChangeAddress, UseExistingChangeAddress: conf.UseExistingChangeAddress,
FeeRate: feeRate,
}) })
if err != nil { if err != nil {
return err return err

File diff suppressed because it is too large Load Diff

View File

@ -4,22 +4,25 @@ option go_package = "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb";
package kaspawalletd; package kaspawalletd;
service kaspawalletd { service kaspawalletd {
rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse) {} rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse) {}
rpc GetExternalSpendableUTXOs (GetExternalSpendableUTXOsRequest) returns (GetExternalSpendableUTXOsResponse) {} rpc GetExternalSpendableUTXOs(GetExternalSpendableUTXOsRequest)
rpc CreateUnsignedTransactions (CreateUnsignedTransactionsRequest) returns (CreateUnsignedTransactionsResponse) {} returns (GetExternalSpendableUTXOsResponse) {}
rpc ShowAddresses (ShowAddressesRequest) returns (ShowAddressesResponse) {} rpc CreateUnsignedTransactions(CreateUnsignedTransactionsRequest)
rpc NewAddress (NewAddressRequest) returns (NewAddressResponse) {} returns (CreateUnsignedTransactionsResponse) {}
rpc Shutdown (ShutdownRequest) returns (ShutdownResponse) {} rpc ShowAddresses(ShowAddressesRequest) returns (ShowAddressesResponse) {}
rpc Broadcast (BroadcastRequest) returns (BroadcastResponse) {} rpc NewAddress(NewAddressRequest) returns (NewAddressResponse) {}
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection rpc Shutdown(ShutdownRequest) returns (ShutdownResponse) {}
rpc Broadcast(BroadcastRequest) returns (BroadcastResponse) {}
// Since SendRequest contains a password - this command should only be used on
// a trusted or secure connection
rpc Send(SendRequest) returns (SendResponse) {} rpc Send(SendRequest) returns (SendResponse) {}
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection // Since SignRequest contains a password - this command should only be used on
// a trusted or secure connection
rpc Sign(SignRequest) returns (SignResponse) {} rpc Sign(SignRequest) returns (SignResponse) {}
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {} rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {}
} }
message GetBalanceRequest { message GetBalanceRequest {}
}
message GetBalanceResponse { message GetBalanceResponse {
uint64 available = 1; uint64 available = 1;
@ -33,46 +36,44 @@ message AddressBalances {
uint64 pending = 3; uint64 pending = 3;
} }
message FeeRate {
oneof feeRate {
double max = 6;
double exact = 7;
}
}
message CreateUnsignedTransactionsRequest { message CreateUnsignedTransactionsRequest {
string address = 1; string address = 1;
uint64 amount = 2; uint64 amount = 2;
repeated string from = 3; repeated string from = 3;
bool useExistingChangeAddress = 4; bool useExistingChangeAddress = 4;
bool isSendAll = 5; bool isSendAll = 5;
FeeRate feeRate = 6;
} }
message CreateUnsignedTransactionsResponse { message CreateUnsignedTransactionsResponse {
repeated bytes unsignedTransactions = 1; repeated bytes unsignedTransactions = 1;
} }
message ShowAddressesRequest { message ShowAddressesRequest {}
}
message ShowAddressesResponse { message ShowAddressesResponse { repeated string address = 1; }
repeated string address = 1;
}
message NewAddressRequest { message NewAddressRequest {}
}
message NewAddressResponse { message NewAddressResponse { string address = 1; }
string address = 1;
}
message BroadcastRequest { message BroadcastRequest {
bool isDomain = 1; bool isDomain = 1;
repeated bytes transactions = 2; repeated bytes transactions = 2;
} }
message BroadcastResponse { message BroadcastResponse { repeated string txIDs = 1; }
repeated string txIDs = 1;
}
message ShutdownRequest { message ShutdownRequest {}
}
message ShutdownResponse { message ShutdownResponse {}
}
message Outpoint { message Outpoint {
string transactionId = 1; string transactionId = 1;
@ -97,41 +98,37 @@ message UtxoEntry {
bool isCoinbase = 4; bool isCoinbase = 4;
} }
message GetExternalSpendableUTXOsRequest{ message GetExternalSpendableUTXOsRequest { string address = 1; }
string address = 1;
}
message GetExternalSpendableUTXOsResponse{ message GetExternalSpendableUTXOsResponse {
repeated UtxosByAddressesEntry Entries = 1; repeated UtxosByAddressesEntry Entries = 1;
} }
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection // Since SendRequest contains a password - this command should only be used on a
message SendRequest{ // trusted or secure connection
message SendRequest {
string toAddress = 1; string toAddress = 1;
uint64 amount = 2; uint64 amount = 2;
string password = 3; string password = 3;
repeated string from = 4; repeated string from = 4;
bool useExistingChangeAddress = 5; bool useExistingChangeAddress = 5;
bool isSendAll = 6; bool isSendAll = 6;
FeeRate feeRate = 7;
} }
message SendResponse{ message SendResponse {
repeated string txIDs = 1; repeated string txIDs = 1;
repeated bytes signedTransactions = 2; repeated bytes signedTransactions = 2;
} }
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection // Since SignRequest contains a password - this command should only be used on a
message SignRequest{ // trusted or secure connection
message SignRequest {
repeated bytes unsignedTransactions = 1; repeated bytes unsignedTransactions = 1;
string password = 2; string password = 2;
} }
message SignResponse{ message SignResponse { repeated bytes signedTransactions = 1; }
repeated bytes signedTransactions = 1;
}
message GetVersionRequest{ message GetVersionRequest {}
}
message GetVersionResponse{ message GetVersionResponse { string version = 1; }
string version = 1;
}

View File

@ -29,9 +29,11 @@ type KaspawalletdClient interface {
NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, error) NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, error)
Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error) Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error)
Broadcast(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error) Broadcast(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error)
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection // Since SendRequest contains a password - this command should only be used on
// a trusted or secure connection
Send(ctx context.Context, in *SendRequest, opts ...grpc.CallOption) (*SendResponse, error) Send(ctx context.Context, in *SendRequest, opts ...grpc.CallOption) (*SendResponse, error)
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection // Since SignRequest contains a password - this command should only be used on
// a trusted or secure connection
Sign(ctx context.Context, in *SignRequest, opts ...grpc.CallOption) (*SignResponse, error) Sign(ctx context.Context, in *SignRequest, opts ...grpc.CallOption) (*SignResponse, error)
GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error)
} }
@ -145,9 +147,11 @@ type KaspawalletdServer interface {
NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error) NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error)
Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error) Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error)
Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error) Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error)
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection // Since SendRequest contains a password - this command should only be used on
// a trusted or secure connection
Send(context.Context, *SendRequest) (*SendResponse, error) Send(context.Context, *SendRequest) (*SendResponse, error)
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection // Since SignRequest contains a password - this command should only be used on
// a trusted or secure connection
Sign(context.Context, *SignRequest) (*SignResponse, error) Sign(context.Context, *SignRequest) (*SignResponse, error)
GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error)
mustEmbedUnimplementedKaspawalletdServer() mustEmbedUnimplementedKaspawalletdServer()

View File

@ -3,17 +3,17 @@ package server
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet" "github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants" "github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/util" "github.com/kaspanet/kaspad/util"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// TODO: Implement a better fee estimation mechanism
const feePerInput = 10000
// The minimal change amount to target in order to avoid large storage mass (see KIP9 for more details). // The minimal change amount to target in order to avoid large storage mass (see KIP9 for more details).
// By having at least 0.2KAS in the change output we make sure that every transaction with send value >= 0.2KAS // By having at least 0.2KAS in the change output we make sure that every transaction with send value >= 0.2KAS
// 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)
@ -26,7 +26,7 @@ func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.Creat
defer s.lock.Unlock() defer s.lock.Unlock()
unsignedTransactions, err := s.createUnsignedTransactions(request.Address, request.Amount, request.IsSendAll, unsignedTransactions, err := s.createUnsignedTransactions(request.Address, request.Amount, request.IsSendAll,
request.From, request.UseExistingChangeAddress) request.From, request.UseExistingChangeAddress, request.FeeRate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -34,10 +34,23 @@ func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.Creat
return &pb.CreateUnsignedTransactionsResponse{UnsignedTransactions: unsignedTransactions}, nil return &pb.CreateUnsignedTransactionsResponse{UnsignedTransactions: unsignedTransactions}, nil
} }
func (s *server) createUnsignedTransactions(address string, amount uint64, isSendAll bool, fromAddressesString []string, useExistingChangeAddress bool) ([][]byte, error) { func (s *server) createUnsignedTransactions(address string, amount uint64, isSendAll bool, fromAddressesString []string, useExistingChangeAddress bool, feeRateOneOf *pb.FeeRate) ([][]byte, error) {
if !s.isSynced() { if !s.isSynced() {
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())
} }
var feeRate float64
switch requestFeeRate := feeRateOneOf.FeeRate.(type) {
case *pb.FeeRate_Exact:
feeRate = requestFeeRate.Exact
case *pb.FeeRate_Max:
estimate, err := s.rpcClient.GetFeeEstimate()
if err != nil {
return nil, err
}
feeRate = math.Min(estimate.Estimate.NormalBuckets[0].Feerate, requestFeeRate.Max)
}
// make sure address string is correct before proceeding to a // make sure address string is correct before proceeding to a
// potentially long UTXO refreshment operation // potentially long UTXO refreshment operation
toAddress, err := util.DecodeAddress(address, s.params.Prefix) toAddress, err := util.DecodeAddress(address, s.params.Prefix)
@ -54,7 +67,12 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen
fromAddresses = append(fromAddresses, fromAddress) fromAddresses = append(fromAddresses, fromAddress)
} }
selectedUTXOs, spendValue, changeSompi, err := s.selectUTXOs(amount, isSendAll, feePerInput, fromAddresses) changeAddress, changeWalletAddress, err := s.changeAddress(useExistingChangeAddress, fromAddresses)
if err != nil {
return nil, err
}
selectedUTXOs, spendValue, changeSompi, err := s.selectUTXOs(amount, isSendAll, feeRate, fromAddresses)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -63,11 +81,6 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen
return nil, errors.Errorf("couldn't find funds to spend") return nil, errors.Errorf("couldn't find funds to spend")
} }
changeAddress, changeWalletAddress, err := s.changeAddress(useExistingChangeAddress, fromAddresses)
if err != nil {
return nil, err
}
payments := []*libkaspawallet.Payment{{ payments := []*libkaspawallet.Payment{{
Address: toAddress, Address: toAddress,
Amount: spendValue, Amount: spendValue,
@ -85,14 +98,14 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen
return nil, err return nil, err
} }
unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress) unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress, feeRate)
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, feePerInput uint64, fromAddresses []*walletAddress) ( func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feeRate float64, fromAddresses []*walletAddress) (
selectedUTXOs []*libkaspawallet.UTXO, totalReceived uint64, changeSompi uint64, err error) { selectedUTXOs []*libkaspawallet.UTXO, totalReceived uint64, changeSompi uint64, err error) {
selectedUTXOs = []*libkaspawallet.UTXO{} selectedUTXOs = []*libkaspawallet.UTXO{}
@ -103,6 +116,7 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
return nil, 0, 0, err return nil, 0, 0, err
} }
var fee uint64
for _, utxo := range s.utxosSortedByAmount { for _, utxo := range s.utxosSortedByAmount {
if (fromAddresses != nil && !walletAddressesContain(fromAddresses, utxo.address)) || if (fromAddresses != nil && !walletAddressesContain(fromAddresses, utxo.address)) ||
!s.isUTXOSpendable(utxo, dagInfo.VirtualDAAScore) { !s.isUTXOSpendable(utxo, dagInfo.VirtualDAAScore) {
@ -125,7 +139,11 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
totalValue += utxo.UTXOEntry.Amount() totalValue += utxo.UTXOEntry.Amount()
fee := feePerInput * uint64(len(selectedUTXOs)) fee, err = s.estimateFee(selectedUTXOs, feeRate)
if err != nil {
return nil, 0, 0, err
}
totalSpend := spendAmount + fee totalSpend := spendAmount + fee
// Two break cases (if not send all): // Two break cases (if not send all):
// 1. totalValue == totalSpend, so there's no change needed -> number of outputs = 1, so a single input is sufficient // 1. totalValue == totalSpend, so there's no change needed -> number of outputs = 1, so a single input is sufficient
@ -137,7 +155,6 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
} }
} }
fee := feePerInput * uint64(len(selectedUTXOs))
var totalSpend uint64 var totalSpend uint64
if isSendAll { if isSendAll {
totalSpend = totalValue totalSpend = totalValue
@ -154,6 +171,80 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
return selectedUTXOs, totalReceived, totalValue - totalSpend, nil return selectedUTXOs, totalReceived, totalValue - totalSpend, nil
} }
func (s *server) estimateFee(selectedUTXOs []*libkaspawallet.UTXO, feeRate float64) (uint64, error) {
fakePubKey := [util.PublicKeySize]byte{}
fakeAddr, err := util.NewAddressPublicKey(fakePubKey[:], s.params.Prefix)
if err != nil {
return 0, err
}
mockPayments := []*libkaspawallet.Payment{
{
Address: fakeAddr,
Amount: 1,
},
{ // We're overestimating a bit by assuming that any transaction will have a change output
Address: fakeAddr,
Amount: 1,
},
}
mockTx, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
s.keysFile.MinimumSignatures,
mockPayments, selectedUTXOs)
if err != nil {
return 0, err
}
mass, err := s.estimateMassAfterSignatures(mockTx)
if err != nil {
return 0, err
}
return uint64(float64(mass) * feeRate), nil
}
func (s *server) estimateFeePerInput(feeRate float64) (uint64, error) {
mockUTXO := &libkaspawallet.UTXO{
Outpoint: &externalapi.DomainOutpoint{
TransactionID: externalapi.DomainTransactionID{},
Index: 0,
},
UTXOEntry: utxo.NewUTXOEntry(1, &externalapi.ScriptPublicKey{
Script: nil,
Version: 0,
}, false, 0),
DerivationPath: "m",
}
mockTx, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
s.keysFile.MinimumSignatures,
nil, []*libkaspawallet.UTXO{mockUTXO})
if err != nil {
return 0, err
}
mass, err := s.estimateMassAfterSignatures(mockTx)
if err != nil {
return 0, err
}
mockTxWithoutUTXO, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
s.keysFile.MinimumSignatures,
nil, nil)
if err != nil {
return 0, err
}
massWithoutUTXO, err := s.estimateMassAfterSignatures(mockTxWithoutUTXO)
if err != nil {
return 0, err
}
inputMass := mass - massWithoutUTXO
return uint64(float64(inputMass) * feeRate), nil
}
func walletAddressesContain(addresses []*walletAddress, contain *walletAddress) bool { func walletAddressesContain(addresses []*walletAddress, contain *walletAddress) bool {
for _, address := range addresses { for _, address := range addresses {
if *address == *contain { if *address == *contain {

View File

@ -21,7 +21,15 @@ func (s *server) GetExternalSpendableUTXOs(_ context.Context, request *pb.GetExt
if err != nil { if err != nil {
return nil, err return nil, err
} }
selectedUTXOs, err := s.selectExternalSpendableUTXOs(externalUTXOs, request.Address)
estimate, err := s.rpcClient.GetFeeEstimate()
if err != nil {
return nil, err
}
feeRate := estimate.Estimate.NormalBuckets[0].Feerate
selectedUTXOs, err := s.selectExternalSpendableUTXOs(externalUTXOs, feeRate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -30,7 +38,7 @@ func (s *server) GetExternalSpendableUTXOs(_ context.Context, request *pb.GetExt
}, nil }, nil
} }
func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOsByAddressesResponseMessage, address string) ([]*pb.UtxosByAddressesEntry, error) { func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOsByAddressesResponseMessage, feeRate float64) ([]*pb.UtxosByAddressesEntry, error) {
dagInfo, err := s.rpcClient.GetBlockDAGInfo() dagInfo, err := s.rpcClient.GetBlockDAGInfo()
if err != nil { if err != nil {
return nil, err return nil, err
@ -42,8 +50,13 @@ func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOs
//we do not make because we do not know size, because of unspendable utxos //we do not make because we do not know size, because of unspendable utxos
var selectedExternalUtxos []*pb.UtxosByAddressesEntry var selectedExternalUtxos []*pb.UtxosByAddressesEntry
feePerInput, err := s.estimateFeePerInput(feeRate)
if err != nil {
return nil, err
}
for _, entry := range externalUTXOs.Entries { for _, entry := range externalUTXOs.Entries {
if !isExternalUTXOSpendable(entry, daaScore, maturity) { if !isExternalUTXOSpendable(entry, daaScore, maturity, feePerInput) {
continue continue
} }
selectedExternalUtxos = append(selectedExternalUtxos, libkaspawallet.AppMessageUTXOToKaspawalletdUTXO(entry)) selectedExternalUtxos = append(selectedExternalUtxos, libkaspawallet.AppMessageUTXOToKaspawalletdUTXO(entry))
@ -52,7 +65,7 @@ func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOs
return selectedExternalUtxos, nil return selectedExternalUtxos, nil
} }
func isExternalUTXOSpendable(entry *appmessage.UTXOsByAddressesEntry, virtualDAAScore uint64, coinbaseMaturity uint64) bool { func isExternalUTXOSpendable(entry *appmessage.UTXOsByAddressesEntry, virtualDAAScore uint64, coinbaseMaturity uint64, feePerInput uint64) bool {
if !entry.UTXOEntry.IsCoinbase { if !entry.UTXOEntry.IsCoinbase {
return true return true
} else if entry.UTXOEntry.Amount <= feePerInput { } else if entry.UTXOEntry.Amount <= feePerInput {

View File

@ -11,7 +11,7 @@ func (s *server) Send(_ context.Context, request *pb.SendRequest) (*pb.SendRespo
defer s.lock.Unlock() defer s.lock.Unlock()
unsignedTransactions, err := s.createUnsignedTransactions(request.ToAddress, request.Amount, request.IsSendAll, unsignedTransactions, err := s.createUnsignedTransactions(request.ToAddress, request.Amount, request.IsSendAll,
request.From, request.UseExistingChangeAddress) request.From, request.UseExistingChangeAddress, request.FeeRate)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -20,14 +20,9 @@ import (
// into a change address. // into a change address.
// 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(transactionBytes []byte, toAddress util.Address, func (s *server) maybeAutoCompoundTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address,
changeAddress util.Address, changeWalletAddress *walletAddress) ([][]byte, error) { changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64) ([][]byte, error) {
transaction, err := serialization.DeserializePartiallySignedTransaction(transactionBytes) splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress, feeRate)
if err != nil {
return nil, err
}
splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -47,6 +42,7 @@ func (s *server) mergeTransaction(
toAddress util.Address, toAddress util.Address,
changeAddress util.Address, changeAddress util.Address,
changeWalletAddress *walletAddress, changeWalletAddress *walletAddress,
feeRate float64,
) (*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 {
@ -71,13 +67,18 @@ func (s *server) mergeTransaction(
DerivationPath: s.walletAddressPath(changeWalletAddress), DerivationPath: s.walletAddressPath(changeWalletAddress),
} }
totalValue += output.Value totalValue += output.Value
totalValue -= feePerInput
} }
fee, err := s.estimateFee(utxos, feeRate)
if err != nil {
return nil, err
}
totalValue -= fee
if totalValue < sentValue { if totalValue < sentValue {
// sometimes the fees from compound transactions make the total output higher than what's available from selected // sometimes the fees from compound transactions make the total output higher than what's available from selected
// utxos, in such cases - find one more UTXO and use it. // utxos, in such cases - find one more UTXO and use it.
additionalUTXOs, totalValueAdded, err := s.moreUTXOsForMergeTransaction(utxos, sentValue-totalValue) additionalUTXOs, totalValueAdded, err := s.moreUTXOsForMergeTransaction(utxos, sentValue-totalValue, feeRate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -96,17 +97,12 @@ func (s *server) mergeTransaction(
}) })
} }
mergeTransactionBytes, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys, return libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
s.keysFile.MinimumSignatures, payments, utxos) s.keysFile.MinimumSignatures, payments, utxos)
if err != nil {
return nil, err
}
return serialization.DeserializePartiallySignedTransaction(mergeTransactionBytes)
} }
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) ([]*serialization.PartiallySignedTransaction, error) { changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64) ([]*serialization.PartiallySignedTransaction, error) {
transactionMass, err := s.estimateMassAfterSignatures(transaction) transactionMass, err := s.estimateMassAfterSignatures(transaction)
if err != nil { if err != nil {
@ -117,7 +113,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) splitCount, inputCountPerSplit, err := s.splitAndInputPerSplitCounts(transaction, transactionMass, changeAddress, feeRate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -127,19 +123,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) splitTransactions[i], err = s.createSplitTransaction(transaction, changeAddress, startIndex, endIndex, feeRate)
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) mergeTransaction, err := s.mergeTransaction(splitTransactions, transaction, toAddress, changeAddress, changeWalletAddress, feeRate)
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) splitMergeTransaction, err := s.maybeSplitAndMergeTransaction(mergeTransaction, toAddress, changeAddress, changeWalletAddress, feeRate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -152,7 +148,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) (splitCount, inputsPerSplitCount int, err error) { changeAddress util.Address, feeRate float64) (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
@ -172,7 +168,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) splitTransactionWithoutInputs, err := s.createSplitTransaction(transaction, changeAddress, 0, 0, feeRate)
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
} }
@ -190,7 +186,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) (*serialization.PartiallySignedTransaction, error) { changeAddress util.Address, startIndex int, endIndex int, feeRate float64) (*serialization.PartiallySignedTransaction, error) {
selectedUTXOs := make([]*libkaspawallet.UTXO, 0, endIndex-startIndex) selectedUTXOs := make([]*libkaspawallet.UTXO, 0, endIndex-startIndex)
totalSompi := uint64(0) totalSompi := uint64(0)
@ -206,19 +202,19 @@ func (s *server) createSplitTransaction(transaction *serialization.PartiallySign
}) })
totalSompi += selectedUTXOs[i-startIndex].UTXOEntry.Amount() totalSompi += selectedUTXOs[i-startIndex].UTXOEntry.Amount()
totalSompi -= feePerInput
} }
unsignedTransactionBytes, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys, fee, err := s.estimateFee(selectedUTXOs, feeRate)
if err != nil {
return nil, err
}
totalSompi -= fee
return libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
s.keysFile.MinimumSignatures, s.keysFile.MinimumSignatures,
[]*libkaspawallet.Payment{{ []*libkaspawallet.Payment{{
Address: changeAddress, Address: changeAddress,
Amount: totalSompi, Amount: totalSompi,
}}, selectedUTXOs) }}, selectedUTXOs)
if err != nil {
return nil, err
}
return serialization.DeserializePartiallySignedTransaction(unsignedTransactionBytes)
} }
func (s *server) estimateMassAfterSignatures(transaction *serialization.PartiallySignedTransaction) (uint64, error) { func (s *server) estimateMassAfterSignatures(transaction *serialization.PartiallySignedTransaction) (uint64, error) {
@ -248,7 +244,7 @@ func (s *server) estimateMassAfterSignatures(transaction *serialization.Partiall
return s.txMassCalculator.CalculateTransactionMass(transactionWithSignatures), nil return s.txMassCalculator.CalculateTransactionMass(transactionWithSignatures), nil
} }
func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawallet.UTXO, requiredAmount uint64) ( func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawallet.UTXO, requiredAmount uint64, feeRate float64) (
additionalUTXOs []*libkaspawallet.UTXO, totalValueAdded uint64, err error) { additionalUTXOs []*libkaspawallet.UTXO, totalValueAdded uint64, err error) {
dagInfo, err := s.rpcClient.GetBlockDAGInfo() dagInfo, err := s.rpcClient.GetBlockDAGInfo()
@ -260,6 +256,11 @@ func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawa
alreadySelectedUTXOsMap[*alreadySelectedUTXO.Outpoint] = struct{}{} alreadySelectedUTXOsMap[*alreadySelectedUTXO.Outpoint] = struct{}{}
} }
feePerInput, err := s.estimateFeePerInput(feeRate)
if err != nil {
return nil, 0, err
}
for _, utxo := range s.utxosSortedByAmount { for _, utxo := range s.utxosSortedByAmount {
if _, ok := alreadySelectedUTXOsMap[*utxo.Outpoint]; ok { if _, ok := alreadySelectedUTXOsMap[*utxo.Outpoint]; ok {
continue continue

View File

@ -22,7 +22,7 @@ import (
func TestEstimateMassAfterSignatures(t *testing.T) { func TestEstimateMassAfterSignatures(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) { testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
unsignedTransactionBytes, mnemonics, params, teardown := testEstimateMassIncreaseForSignaturesSetUp(t, consensusConfig) unsignedTransaction, mnemonics, params, teardown := testEstimateMassIncreaseForSignaturesSetUp(t, consensusConfig)
defer teardown(false) defer teardown(false)
serverInstance := &server{ serverInstance := &server{
@ -33,16 +33,16 @@ func TestEstimateMassAfterSignatures(t *testing.T) {
txMassCalculator: txmass.NewCalculator(params.MassPerTxByte, params.MassPerScriptPubKeyByte, params.MassPerSigOp), txMassCalculator: txmass.NewCalculator(params.MassPerTxByte, params.MassPerScriptPubKeyByte, params.MassPerSigOp),
} }
unsignedTransaction, err := serialization.DeserializePartiallySignedTransaction(unsignedTransactionBytes)
if err != nil {
t.Fatalf("Error deserializing unsignedTransaction: %s", err)
}
estimatedMassAfterSignatures, err := serverInstance.estimateMassAfterSignatures(unsignedTransaction) estimatedMassAfterSignatures, err := serverInstance.estimateMassAfterSignatures(unsignedTransaction)
if err != nil { if err != nil {
t.Fatalf("Error from estimateMassAfterSignatures: %s", err) t.Fatalf("Error from estimateMassAfterSignatures: %s", err)
} }
unsignedTransactionBytes, err := serialization.SerializePartiallySignedTransaction(unsignedTransaction)
if err != nil {
t.Fatalf("Error deserializing unsignedTransaction: %s", err)
}
signedTxStep1Bytes, err := libkaspawallet.Sign(params, mnemonics[:1], unsignedTransactionBytes, false) signedTxStep1Bytes, err := libkaspawallet.Sign(params, mnemonics[:1], unsignedTransactionBytes, false)
if err != nil { if err != nil {
t.Fatalf("Sign: %+v", err) t.Fatalf("Sign: %+v", err)
@ -68,7 +68,7 @@ func TestEstimateMassAfterSignatures(t *testing.T) {
} }
func testEstimateMassIncreaseForSignaturesSetUp(t *testing.T, consensusConfig *consensus.Config) ( func testEstimateMassIncreaseForSignaturesSetUp(t *testing.T, consensusConfig *consensus.Config) (
[]byte, []string, *dagconfig.Params, func(keepDataDir bool)) { *serialization.PartiallySignedTransaction, []string, *dagconfig.Params, func(keepDataDir bool)) {
consensusConfig.BlockCoinbaseMaturity = 0 consensusConfig.BlockCoinbaseMaturity = 0
params := &consensusConfig.Params params := &consensusConfig.Params

View File

@ -31,15 +31,10 @@ func CreateUnsignedTransaction(
extendedPublicKeys []string, extendedPublicKeys []string,
minimumSignatures uint32, minimumSignatures uint32,
payments []*Payment, payments []*Payment,
selectedUTXOs []*UTXO) ([]byte, error) { selectedUTXOs []*UTXO) (*serialization.PartiallySignedTransaction, error) {
sortPublicKeys(extendedPublicKeys) sortPublicKeys(extendedPublicKeys)
unsignedTransaction, err := createUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs) return createUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
if err != nil {
return nil, err
}
return serialization.SerializePartiallySignedTransaction(unsignedTransaction)
} }
func multiSigRedeemScript(extendedPublicKeys []string, minimumSignatures uint32, path string, ecdsa bool) ([]byte, error) { func multiSigRedeemScript(extendedPublicKeys []string, minimumSignatures uint32, path string, ecdsa bool) ([]byte, error) {

View File

@ -2,6 +2,7 @@ package libkaspawallet_test
import ( import (
"fmt" "fmt"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants" "github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"strings" "strings"
"testing" "testing"
@ -26,6 +27,20 @@ func forSchnorrAndECDSA(t *testing.T, testFunc func(t *testing.T, ecdsa bool)) {
}) })
} }
func createUnsignedTransactionSerialized(
extendedPublicKeys []string,
minimumSignatures uint32,
payments []*libkaspawallet.Payment,
selectedUTXOs []*libkaspawallet.UTXO) ([]byte, error) {
tx, err := libkaspawallet.CreateUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
if err != nil {
return nil, err
}
return serialization.SerializePartiallySignedTransaction(tx)
}
func TestMultisig(t *testing.T) { func TestMultisig(t *testing.T) {
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) { testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
params := &consensusConfig.Params params := &consensusConfig.Params
@ -102,7 +117,7 @@ func TestMultisig(t *testing.T) {
}, },
} }
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures, unsignedTransaction, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
[]*libkaspawallet.Payment{{ []*libkaspawallet.Payment{{
Address: address, Address: address,
Amount: 10, Amount: 10,
@ -263,7 +278,7 @@ func TestP2PK(t *testing.T) {
}, },
} }
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures, unsignedTransaction, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
[]*libkaspawallet.Payment{{ []*libkaspawallet.Payment{{
Address: address, Address: address,
Amount: 10, Amount: 10,
@ -425,7 +440,7 @@ func TestMaxSompi(t *testing.T) {
}, },
} }
unsignedTxWithLargeInputAmount, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures, unsignedTxWithLargeInputAmount, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
[]*libkaspawallet.Payment{{ []*libkaspawallet.Payment{{
Address: address, Address: address,
Amount: 10, Amount: 10,
@ -476,7 +491,7 @@ func TestMaxSompi(t *testing.T) {
}, },
} }
unsignedTxWithLargeInputAndOutputAmount, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures, unsignedTxWithLargeInputAndOutputAmount, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
[]*libkaspawallet.Payment{{ []*libkaspawallet.Payment{{
Address: address, Address: address,
Amount: 22e6 * constants.SompiPerKaspa, Amount: 22e6 * constants.SompiPerKaspa,

View File

@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"os" "os"
"strings" "strings"
@ -42,6 +43,15 @@ func send(conf *sendConfig) error {
} }
} }
feeRate := &pb.FeeRate{
FeeRate: &pb.FeeRate_Max{Max: math.MaxFloat64},
}
if conf.FeeRate > 0 {
feeRate.FeeRate = &pb.FeeRate_Exact{Exact: conf.FeeRate}
} else if conf.MaxFeeRate > 0 {
feeRate.FeeRate = &pb.FeeRate_Max{Max: conf.MaxFeeRate}
}
createUnsignedTransactionsResponse, err := createUnsignedTransactionsResponse, err :=
daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{ daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{
From: conf.FromAddresses, From: conf.FromAddresses,
@ -49,6 +59,7 @@ func send(conf *sendConfig) error {
Amount: sendAmountSompi, Amount: sendAmountSompi,
IsSendAll: conf.IsSendAll, IsSendAll: conf.IsSendAll,
UseExistingChangeAddress: conf.UseExistingChangeAddress, UseExistingChangeAddress: conf.UseExistingChangeAddress,
FeeRate: feeRate,
}) })
if err != nil { if err != nil {
return err return err

View File

@ -139,6 +139,28 @@ message KaspadMessage {
GetMempoolEntriesByAddressesResponseMessage getMempoolEntriesByAddressesResponse = 1085; GetMempoolEntriesByAddressesResponseMessage getMempoolEntriesByAddressesResponse = 1085;
GetCoinSupplyRequestMessage getCoinSupplyRequest = 1086; GetCoinSupplyRequestMessage getCoinSupplyRequest = 1086;
GetCoinSupplyResponseMessage getCoinSupplyResponse= 1087; GetCoinSupplyResponseMessage getCoinSupplyResponse= 1087;
PingRequestMessage pingRequest = 1088;
GetMetricsRequestMessage getMetricsRequest = 1090;
GetServerInfoRequestMessage getServerInfoRequest = 1092;
GetSyncStatusRequestMessage getSyncStatusRequest = 1094;
GetDaaScoreTimestampEstimateRequestMessage getDaaScoreTimestampEstimateRequest = 1096;
SubmitTransactionReplacementRequestMessage submitTransactionReplacementRequest = 1100;
GetConnectionsRequestMessage getConnectionsRequest = 1102;
GetSystemInfoRequestMessage getSystemInfoRequest = 1104;
GetFeeEstimateRequestMessage getFeeEstimateRequest = 1106;
GetFeeEstimateExperimentalRequestMessage getFeeEstimateExperimentalRequest = 1108;
GetCurrentBlockColorRequestMessage getCurrentBlockColorRequest = 1110;
PingResponseMessage pingResponse= 1089;
GetMetricsResponseMessage getMetricsResponse= 1091;
GetServerInfoResponseMessage getServerInfoResponse = 1093;
GetSyncStatusResponseMessage getSyncStatusResponse = 1095;
GetDaaScoreTimestampEstimateResponseMessage getDaaScoreTimestampEstimateResponse = 1097;
SubmitTransactionReplacementResponseMessage submitTransactionReplacementResponse = 1101;
GetConnectionsResponseMessage getConnectionsResponse= 1103;
GetSystemInfoResponseMessage getSystemInfoResponse= 1105;
GetFeeEstimateResponseMessage getFeeEstimateResponse = 1107;
GetFeeEstimateExperimentalResponseMessage getFeeEstimateExperimentalResponse = 1109;
GetCurrentBlockColorResponseMessage getCurrentBlockColorResponse = 1111;
} }
} }

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.2.0 // - protoc-gen-go-grpc v1.2.0
// - protoc v3.17.2 // - protoc v3.12.3
// source: messages.proto // source: messages.proto
package protowire package protowire

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.26.0 // protoc-gen-go v1.27.1
// protoc v3.17.2 // protoc v3.12.3
// source: p2p.proto // source: p2p.proto
package protowire package protowire

View File

@ -721,3 +721,221 @@ message GetCoinSupplyResponseMessage{
RPCError error = 1000; RPCError error = 1000;
} }
message PingRequestMessage{
}
message PingResponseMessage { RPCError error = 1000; }
message ProcessMetrics {
uint64 residentSetSize = 1;
uint64 virtualMemorySize = 2;
uint32 coreNum = 3;
float cpuUsage = 4;
uint32 fdNum = 5;
uint64 diskIoReadBytes = 6;
uint64 diskIoWriteBytes = 7;
float diskIoReadPerSec = 8;
float diskIoWritePerSec = 9;
}
message ConnectionMetrics {
uint32 borshLiveConnections = 31;
uint64 borshConnectionAttempts = 32;
uint64 borshHandshakeFailures = 33;
uint32 jsonLiveConnections = 41;
uint64 jsonConnectionAttempts = 42;
uint64 jsonHandshakeFailures = 43;
uint32 activePeers = 51;
}
message BandwidthMetrics {
uint64 borshBytesTx = 61;
uint64 borshBytesRx = 62;
uint64 jsonBytesTx = 63;
uint64 jsonBytesRx = 64;
uint64 grpcP2pBytesTx = 65;
uint64 grpcP2pBytesRx = 66;
uint64 grpcUserBytesTx = 67;
uint64 grpcUserBytesRx = 68;
}
message ConsensusMetrics {
uint64 blocksSubmitted = 1;
uint64 headerCounts = 2;
uint64 depCounts = 3;
uint64 bodyCounts = 4;
uint64 txsCounts = 5;
uint64 chainBlockCounts = 6;
uint64 massCounts = 7;
uint64 blockCount = 11;
uint64 headerCount = 12;
uint64 mempoolSize = 13;
uint32 tipHashesCount = 14;
double difficulty = 15;
uint64 pastMedianTime = 16;
uint32 virtualParentHashesCount = 17;
uint64 virtualDaaScore = 18;
}
message StorageMetrics { uint64 storageSizeBytes = 1; }
message GetConnectionsRequestMessage { bool includeProfileData = 1; }
message ConnectionsProfileData {
double cpuUsage = 1;
uint64 memoryUsage = 2;
}
message GetConnectionsResponseMessage {
uint32 clients = 1;
uint32 peers = 2;
ConnectionsProfileData profileData = 3;
RPCError error = 1000;
}
message GetSystemInfoRequestMessage {}
message GetSystemInfoResponseMessage {
string version = 1;
string systemId = 2;
string gitHash = 3;
uint32 coreNum = 4;
uint64 totalMemory = 5;
uint32 fdLimit = 6;
RPCError error = 1000;
}
message GetMetricsRequestMessage {
bool processMetrics = 1;
bool connectionMetrics = 2;
bool bandwidthMetrics = 3;
bool consensusMetrics = 4;
bool storageMetrics = 5;
bool customMetrics = 6;
}
message GetMetricsResponseMessage {
uint64 serverTime = 1;
ProcessMetrics processMetrics = 11;
ConnectionMetrics connectionMetrics = 12;
BandwidthMetrics bandwidthMetrics = 13;
ConsensusMetrics consensusMetrics = 14;
StorageMetrics storageMetrics = 15;
RPCError error = 1000;
}
message GetServerInfoRequestMessage {}
message GetServerInfoResponseMessage {
uint32 rpcApiVersion = 1;
uint32 rpcApiRevision = 2;
string serverVersion = 3;
string networkId = 4;
bool hasUtxoIndex = 5;
bool isSynced = 6;
uint64 virtualDaaScore = 7;
RPCError error = 1000;
}
message GetSyncStatusRequestMessage {}
message GetSyncStatusResponseMessage {
bool isSynced = 1;
RPCError error = 1000;
}
message GetDaaScoreTimestampEstimateRequestMessage {
repeated uint64 daa_scores = 1;
}
message GetDaaScoreTimestampEstimateResponseMessage {
repeated uint64 timestamps = 1;
RPCError error = 1000;
}
message RpcFeerateBucket {
// Fee/mass of a transaction in `sompi/gram` units
double feerate = 1;
double estimated_seconds = 2;
}
// Data required for making fee estimates.
//
// Feerate values represent fee/mass of a transaction in `sompi/gram` units.
// Given a feerate value recommendation, calculate the required fee by
// taking the transaction mass and multiplying it by feerate: `fee = feerate *
// mass(tx)`
message RpcFeeEstimate {
// Top-priority feerate bucket. Provides an estimation of the feerate required
// for sub-second DAG inclusion.
RpcFeerateBucket priority_bucket = 1;
// A vector of *normal* priority feerate values. The first value of this
// vector is guaranteed to exist and provide an estimation for sub-*minute*
// DAG inclusion. All other values will have shorter estimation times than all
// `low_bucket` values. Therefor by chaining `[priority] | normal | low` and
// interpolating between them, one can compose a complete feerate function on
// the client side. The API makes an effort to sample enough "interesting"
// points on the feerate-to-time curve, so that the interpolation is
// meaningful.
repeated RpcFeerateBucket normal_buckets = 2;
// A vector of *low* priority feerate values. The first value of this vector
// is guaranteed to exist and provide an estimation for sub-*hour* DAG
// inclusion.
repeated RpcFeerateBucket low_buckets = 3;
}
message RpcFeeEstimateVerboseExperimentalData {
uint64 mempool_ready_transactions_count = 1;
uint64 mempool_ready_transactions_total_mass = 2;
uint64 network_mass_per_second = 3;
double next_block_template_feerate_min = 11;
double next_block_template_feerate_median = 12;
double next_block_template_feerate_max = 13;
}
message GetFeeEstimateRequestMessage {}
message GetFeeEstimateResponseMessage {
RpcFeeEstimate estimate = 1;
RPCError error = 1000;
}
message GetFeeEstimateExperimentalRequestMessage { bool verbose = 1; }
message GetFeeEstimateExperimentalResponseMessage {
RpcFeeEstimate estimate = 1;
RpcFeeEstimateVerboseExperimentalData verbose = 2;
RPCError error = 1000;
}
message GetCurrentBlockColorRequestMessage { string hash = 1; }
message GetCurrentBlockColorResponseMessage {
bool blue = 1;
RPCError error = 1000;
}
// SubmitTransactionReplacementRequestMessage submits a transaction to the
// mempool, applying a mandatory Replace by Fee policy
message SubmitTransactionReplacementRequestMessage {
RpcTransaction transaction = 1;
}
message SubmitTransactionReplacementResponseMessage {
// The transaction ID of the submitted transaction
string transactionId = 1;
// The previous transaction replaced in the mempool by the newly submitted one
RpcTransaction replacedTransaction = 2;
RPCError error = 1000;
}

View File

@ -17,7 +17,7 @@ func (x *KaspadMessage_GetCurrentNetworkResponse) toAppMessage() (appmessage.Mes
if x == nil { if x == nil {
return nil, errors.Wrapf(errorNil, "KaspadMessage_GetCurrentNetworkResponse is nil") return nil, errors.Wrapf(errorNil, "KaspadMessage_GetCurrentNetworkResponse is nil")
} }
return x.toAppMessage() return x.GetCurrentNetworkResponse.toAppMessage()
} }
func (x *KaspadMessage_GetCurrentNetworkResponse) fromAppMessage(message *appmessage.GetCurrentNetworkResponseMessage) error { func (x *KaspadMessage_GetCurrentNetworkResponse) fromAppMessage(message *appmessage.GetCurrentNetworkResponseMessage) error {

View File

@ -0,0 +1,67 @@
package protowire
import (
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/pkg/errors"
)
func (x *KaspadMessage_GetFeeEstimateRequest) toAppMessage() (appmessage.Message, error) {
return &appmessage.GetFeeEstimateRequestMessage{}, nil
}
func (x *KaspadMessage_GetFeeEstimateRequest) fromAppMessage(_ *appmessage.GetFeeEstimateRequestMessage) error {
return nil
}
func (x *KaspadMessage_GetFeeEstimateResponse) toAppMessage() (appmessage.Message, error) {
if x == nil {
return nil, errors.Wrapf(errorNil, "KaspadMessage_GetFeeEstimateResponse is nil")
}
return x.GetFeeEstimateResponse.toAppMessage()
}
func (x *GetFeeEstimateResponseMessage) toAppMessage() (appmessage.Message, error) {
if x == nil {
return nil, errors.Wrapf(errorNil, "GetFeeEstimateResponseMessage is nil")
}
rpcErr, err := x.Error.toAppMessage()
// Error is an optional field
if err != nil && !errors.Is(err, errorNil) {
return nil, err
}
estimate, err := x.Estimate.toAppMessage()
if err != nil {
return nil, err
}
return &appmessage.GetFeeEstimateResponseMessage{
Error: rpcErr,
Estimate: estimate,
}, nil
}
func (x *RpcFeeEstimate) toAppMessage() (appmessage.RPCFeeEstimate, error) {
if x == nil {
return appmessage.RPCFeeEstimate{}, errors.Wrapf(errorNil, "RpcFeeEstimate is nil")
}
return appmessage.RPCFeeEstimate{
PriorityBucket: appmessage.RPCFeeRateBucket{
Feerate: x.PriorityBucket.Feerate,
EstimatedSeconds: x.PriorityBucket.EstimatedSeconds,
},
NormalBuckets: feeRateBucketsToAppMessage(x.NormalBuckets),
LowBuckets: feeRateBucketsToAppMessage(x.LowBuckets),
}, nil
}
func feeRateBucketsToAppMessage(protoBuckets []*RpcFeerateBucket) []appmessage.RPCFeeRateBucket {
appMsgBuckets := make([]appmessage.RPCFeeRateBucket, len(protoBuckets))
for i, bucket := range protoBuckets {
appMsgBuckets[i] = appmessage.RPCFeeRateBucket{
Feerate: bucket.Feerate,
EstimatedSeconds: bucket.EstimatedSeconds,
}
}
return appMsgBuckets
}

View File

@ -968,6 +968,13 @@ func toRPCPayload(message appmessage.Message) (isKaspadMessage_Payload, error) {
return nil, err return nil, err
} }
return payload, nil return payload, nil
case *appmessage.GetFeeEstimateRequestMessage:
payload := new(KaspadMessage_GetFeeEstimateRequest)
err := payload.fromAppMessage(message)
if err != nil {
return nil, err
}
return payload, nil
default: default:
return nil, nil return nil, nil
} }

View File

@ -0,0 +1,20 @@
package rpcclient
import "github.com/kaspanet/kaspad/app/appmessage"
// GetFeeEstimate sends an RPC request respective to the function's name and returns the RPC server's response
func (c *RPCClient) GetFeeEstimate() (*appmessage.GetFeeEstimateResponseMessage, error) {
err := c.rpcRouter.outgoingRoute().Enqueue(appmessage.NewGetFeeEstimateRequestMessage())
if err != nil {
return nil, err
}
response, err := c.route(appmessage.CmdGetFeeEstimateResponseMessage).DequeueWithTimeout(c.timeout)
if err != nil {
return nil, err
}
resp := response.(*appmessage.GetFeeEstimateResponseMessage)
if resp.Error != nil {
return nil, c.convertRPCError(resp.Error)
}
return resp, nil
}