mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-03-30 15:08:33 +00:00
Add fee estimation to wallet
This commit is contained in:
parent
48a142e12f
commit
78ca616b1f
@ -163,6 +163,8 @@ const (
|
||||
CmdGetMempoolEntriesByAddressesResponseMessage
|
||||
CmdGetCoinSupplyRequestMessage
|
||||
CmdGetCoinSupplyResponseMessage
|
||||
CmdGetFeeEstimateRequestMessage
|
||||
CmdGetFeeEstimateResponseMessage
|
||||
)
|
||||
|
||||
// ProtocolMessageCommandToString maps all MessageCommands to their string representation
|
||||
@ -300,6 +302,8 @@ var RPCMessageCommandToString = map[MessageCommand]string{
|
||||
CmdGetMempoolEntriesByAddressesResponseMessage: "GetMempoolEntriesByAddressesResponse",
|
||||
CmdGetCoinSupplyRequestMessage: "GetCoinSupplyRequest",
|
||||
CmdGetCoinSupplyResponseMessage: "GetCoinSupplyResponse",
|
||||
CmdGetFeeEstimateRequestMessage: "GetFeeEstimateRequest",
|
||||
CmdGetFeeEstimateResponseMessage: "GetFeeEstimateResponse",
|
||||
}
|
||||
|
||||
// Message is an interface that describes a kaspa message. A type that
|
||||
|
47
app/appmessage/rpc_fee_estimate.go
Normal file
47
app/appmessage/rpc_fee_estimate.go
Normal 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{}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/kaspanet/kaspad/infrastructure/config"
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
|
||||
"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)"`
|
||||
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)"`
|
||||
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"`
|
||||
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)"`
|
||||
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)"`
|
||||
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
|
||||
}
|
||||
|
||||
@ -316,6 +321,19 @@ func validateCreateUnsignedTransactionConf(conf *createUnsignedTransactionConfig
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -325,6 +343,19 @@ func validateSendConfig(conf *sendConfig) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
@ -26,12 +27,22 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
|
||||
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{
|
||||
From: conf.FromAddresses,
|
||||
Address: conf.ToAddress,
|
||||
Amount: sendAmountSompi,
|
||||
IsSendAll: conf.IsSendAll,
|
||||
UseExistingChangeAddress: conf.UseExistingChangeAddress,
|
||||
FeeRate: feeRate,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,22 +4,25 @@ option go_package = "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb";
|
||||
package kaspawalletd;
|
||||
|
||||
service kaspawalletd {
|
||||
rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse) {}
|
||||
rpc GetExternalSpendableUTXOs (GetExternalSpendableUTXOsRequest) returns (GetExternalSpendableUTXOsResponse) {}
|
||||
rpc CreateUnsignedTransactions (CreateUnsignedTransactionsRequest) returns (CreateUnsignedTransactionsResponse) {}
|
||||
rpc ShowAddresses (ShowAddressesRequest) returns (ShowAddressesResponse) {}
|
||||
rpc NewAddress (NewAddressRequest) returns (NewAddressResponse) {}
|
||||
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 GetBalance(GetBalanceRequest) returns (GetBalanceResponse) {}
|
||||
rpc GetExternalSpendableUTXOs(GetExternalSpendableUTXOsRequest)
|
||||
returns (GetExternalSpendableUTXOsResponse) {}
|
||||
rpc CreateUnsignedTransactions(CreateUnsignedTransactionsRequest)
|
||||
returns (CreateUnsignedTransactionsResponse) {}
|
||||
rpc ShowAddresses(ShowAddressesRequest) returns (ShowAddressesResponse) {}
|
||||
rpc NewAddress(NewAddressRequest) returns (NewAddressResponse) {}
|
||||
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) {}
|
||||
// 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 GetVersion(GetVersionRequest) returns (GetVersionResponse) {}
|
||||
}
|
||||
|
||||
message GetBalanceRequest {
|
||||
}
|
||||
message GetBalanceRequest {}
|
||||
|
||||
message GetBalanceResponse {
|
||||
uint64 available = 1;
|
||||
@ -33,46 +36,44 @@ message AddressBalances {
|
||||
uint64 pending = 3;
|
||||
}
|
||||
|
||||
message FeeRate {
|
||||
oneof feeRate {
|
||||
double max = 6;
|
||||
double exact = 7;
|
||||
}
|
||||
}
|
||||
|
||||
message CreateUnsignedTransactionsRequest {
|
||||
string address = 1;
|
||||
uint64 amount = 2;
|
||||
repeated string from = 3;
|
||||
bool useExistingChangeAddress = 4;
|
||||
bool isSendAll = 5;
|
||||
FeeRate feeRate = 6;
|
||||
}
|
||||
|
||||
message CreateUnsignedTransactionsResponse {
|
||||
repeated bytes unsignedTransactions = 1;
|
||||
}
|
||||
|
||||
message ShowAddressesRequest {
|
||||
}
|
||||
message ShowAddressesRequest {}
|
||||
|
||||
message ShowAddressesResponse {
|
||||
repeated string address = 1;
|
||||
}
|
||||
message ShowAddressesResponse { repeated string address = 1; }
|
||||
|
||||
message NewAddressRequest {
|
||||
}
|
||||
message NewAddressRequest {}
|
||||
|
||||
message NewAddressResponse {
|
||||
string address = 1;
|
||||
}
|
||||
message NewAddressResponse { string address = 1; }
|
||||
|
||||
message BroadcastRequest {
|
||||
bool isDomain = 1;
|
||||
repeated bytes transactions = 2;
|
||||
}
|
||||
|
||||
message BroadcastResponse {
|
||||
repeated string txIDs = 1;
|
||||
}
|
||||
message BroadcastResponse { repeated string txIDs = 1; }
|
||||
|
||||
message ShutdownRequest {
|
||||
}
|
||||
message ShutdownRequest {}
|
||||
|
||||
message ShutdownResponse {
|
||||
}
|
||||
message ShutdownResponse {}
|
||||
|
||||
message Outpoint {
|
||||
string transactionId = 1;
|
||||
@ -97,41 +98,37 @@ message UtxoEntry {
|
||||
bool isCoinbase = 4;
|
||||
}
|
||||
|
||||
message GetExternalSpendableUTXOsRequest{
|
||||
string address = 1;
|
||||
}
|
||||
message GetExternalSpendableUTXOsRequest { string address = 1; }
|
||||
|
||||
message GetExternalSpendableUTXOsResponse{
|
||||
message GetExternalSpendableUTXOsResponse {
|
||||
repeated UtxosByAddressesEntry Entries = 1;
|
||||
}
|
||||
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
message SendRequest{
|
||||
// Since SendRequest contains a password - this command should only be used on a
|
||||
// trusted or secure connection
|
||||
message SendRequest {
|
||||
string toAddress = 1;
|
||||
uint64 amount = 2;
|
||||
string password = 3;
|
||||
repeated string from = 4;
|
||||
bool useExistingChangeAddress = 5;
|
||||
bool isSendAll = 6;
|
||||
FeeRate feeRate = 7;
|
||||
}
|
||||
|
||||
message SendResponse{
|
||||
message SendResponse {
|
||||
repeated string txIDs = 1;
|
||||
repeated bytes signedTransactions = 2;
|
||||
}
|
||||
|
||||
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
message SignRequest{
|
||||
// Since SignRequest contains a password - this command should only be used on a
|
||||
// trusted or secure connection
|
||||
message SignRequest {
|
||||
repeated bytes unsignedTransactions = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message SignResponse{
|
||||
repeated bytes signedTransactions = 1;
|
||||
}
|
||||
message SignResponse { repeated bytes signedTransactions = 1; }
|
||||
|
||||
message GetVersionRequest{
|
||||
}
|
||||
message GetVersionRequest {}
|
||||
|
||||
message GetVersionResponse{
|
||||
string version = 1;
|
||||
}
|
||||
message GetVersionResponse { string version = 1; }
|
@ -29,9 +29,11 @@ type KaspawalletdClient interface {
|
||||
NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, error)
|
||||
Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, 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)
|
||||
// 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)
|
||||
GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error)
|
||||
}
|
||||
@ -145,9 +147,11 @@ type KaspawalletdServer interface {
|
||||
NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error)
|
||||
Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, 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)
|
||||
// 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)
|
||||
GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error)
|
||||
mustEmbedUnimplementedKaspawalletdServer()
|
||||
|
@ -3,17 +3,17 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"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/utxo"
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"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).
|
||||
// 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)
|
||||
@ -26,7 +26,7 @@ func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.Creat
|
||||
defer s.lock.Unlock()
|
||||
|
||||
unsignedTransactions, err := s.createUnsignedTransactions(request.Address, request.Amount, request.IsSendAll,
|
||||
request.From, request.UseExistingChangeAddress)
|
||||
request.From, request.UseExistingChangeAddress, request.FeeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -34,10 +34,23 @@ func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.Creat
|
||||
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() {
|
||||
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
|
||||
// potentially long UTXO refreshment operation
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
|
||||
changeAddress, changeWalletAddress, err := s.changeAddress(useExistingChangeAddress, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payments := []*libkaspawallet.Payment{{
|
||||
Address: toAddress,
|
||||
Amount: spendValue,
|
||||
@ -85,14 +98,14 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress)
|
||||
unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress, feeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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{}
|
||||
@ -103,6 +116,7 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
var fee uint64
|
||||
for _, utxo := range s.utxosSortedByAmount {
|
||||
if (fromAddresses != nil && !walletAddressesContain(fromAddresses, utxo.address)) ||
|
||||
!s.isUTXOSpendable(utxo, dagInfo.VirtualDAAScore) {
|
||||
@ -125,7 +139,11 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
|
||||
|
||||
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
|
||||
// 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
|
||||
@ -137,7 +155,6 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
|
||||
}
|
||||
}
|
||||
|
||||
fee := feePerInput * uint64(len(selectedUTXOs))
|
||||
var totalSpend uint64
|
||||
if isSendAll {
|
||||
totalSpend = totalValue
|
||||
@ -154,6 +171,80 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
|
||||
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 {
|
||||
for _, address := range addresses {
|
||||
if *address == *contain {
|
||||
|
@ -21,7 +21,15 @@ func (s *server) GetExternalSpendableUTXOs(_ context.Context, request *pb.GetExt
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -30,7 +38,7 @@ func (s *server) GetExternalSpendableUTXOs(_ context.Context, request *pb.GetExt
|
||||
}, 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()
|
||||
if err != nil {
|
||||
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
|
||||
var selectedExternalUtxos []*pb.UtxosByAddressesEntry
|
||||
|
||||
feePerInput, err := s.estimateFeePerInput(feeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range externalUTXOs.Entries {
|
||||
if !isExternalUTXOSpendable(entry, daaScore, maturity) {
|
||||
if !isExternalUTXOSpendable(entry, daaScore, maturity, feePerInput) {
|
||||
continue
|
||||
}
|
||||
selectedExternalUtxos = append(selectedExternalUtxos, libkaspawallet.AppMessageUTXOToKaspawalletdUTXO(entry))
|
||||
@ -52,7 +65,7 @@ func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOs
|
||||
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 {
|
||||
return true
|
||||
} else if entry.UTXOEntry.Amount <= feePerInput {
|
||||
|
@ -11,7 +11,7 @@ func (s *server) Send(_ context.Context, request *pb.SendRequest) (*pb.SendRespo
|
||||
defer s.lock.Unlock()
|
||||
|
||||
unsignedTransactions, err := s.createUnsignedTransactions(request.ToAddress, request.Amount, request.IsSendAll,
|
||||
request.From, request.UseExistingChangeAddress)
|
||||
request.From, request.UseExistingChangeAddress, request.FeeRate)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -20,14 +20,9 @@ import (
|
||||
// into a change address.
|
||||
// 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(transactionBytes []byte, toAddress util.Address,
|
||||
changeAddress util.Address, changeWalletAddress *walletAddress) ([][]byte, error) {
|
||||
transaction, err := serialization.DeserializePartiallySignedTransaction(transactionBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress)
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -47,6 +42,7 @@ func (s *server) mergeTransaction(
|
||||
toAddress util.Address,
|
||||
changeAddress util.Address,
|
||||
changeWalletAddress *walletAddress,
|
||||
feeRate float64,
|
||||
) (*serialization.PartiallySignedTransaction, error) {
|
||||
numOutputs := len(originalTransaction.Tx.Outputs)
|
||||
if numOutputs > 2 || numOutputs == 0 {
|
||||
@ -71,13 +67,18 @@ func (s *server) mergeTransaction(
|
||||
DerivationPath: s.walletAddressPath(changeWalletAddress),
|
||||
}
|
||||
totalValue += output.Value
|
||||
totalValue -= feePerInput
|
||||
}
|
||||
fee, err := s.estimateFee(utxos, feeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalValue -= fee
|
||||
|
||||
if totalValue < sentValue {
|
||||
// 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.
|
||||
additionalUTXOs, totalValueAdded, err := s.moreUTXOsForMergeTransaction(utxos, sentValue-totalValue)
|
||||
additionalUTXOs, totalValueAdded, err := s.moreUTXOsForMergeTransaction(utxos, sentValue-totalValue, feeRate)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serialization.DeserializePartiallySignedTransaction(mergeTransactionBytes)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
@ -117,7 +113,7 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -127,19 +123,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)
|
||||
splitTransactions[i], err = s.createSplitTransaction(transaction, changeAddress, startIndex, endIndex, feeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
splitMergeTransaction, err := s.maybeSplitAndMergeTransaction(mergeTransaction, toAddress, changeAddress, changeWalletAddress, feeRate)
|
||||
if err != nil {
|
||||
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.
|
||||
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,
|
||||
// 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,
|
||||
// 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 {
|
||||
return 0, 0, err
|
||||
}
|
||||
@ -190,7 +186,7 @@ func (s *server) splitAndInputPerSplitCounts(transaction *serialization.Partiall
|
||||
}
|
||||
|
||||
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)
|
||||
totalSompi := uint64(0)
|
||||
@ -206,19 +202,19 @@ func (s *server) createSplitTransaction(transaction *serialization.PartiallySign
|
||||
})
|
||||
|
||||
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,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: changeAddress,
|
||||
Amount: totalSompi,
|
||||
}}, selectedUTXOs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serialization.DeserializePartiallySignedTransaction(unsignedTransactionBytes)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
|
||||
@ -260,6 +256,11 @@ func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawa
|
||||
alreadySelectedUTXOsMap[*alreadySelectedUTXO.Outpoint] = struct{}{}
|
||||
}
|
||||
|
||||
feePerInput, err := s.estimateFeePerInput(feeRate)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range s.utxosSortedByAmount {
|
||||
if _, ok := alreadySelectedUTXOsMap[*utxo.Outpoint]; ok {
|
||||
continue
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
|
||||
func TestEstimateMassAfterSignatures(t *testing.T) {
|
||||
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)
|
||||
|
||||
serverInstance := &server{
|
||||
@ -33,16 +33,16 @@ func TestEstimateMassAfterSignatures(t *testing.T) {
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign: %+v", err)
|
||||
@ -68,7 +68,7 @@ func TestEstimateMassAfterSignatures(t *testing.T) {
|
||||
}
|
||||
|
||||
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
|
||||
params := &consensusConfig.Params
|
||||
|
@ -31,15 +31,10 @@ func CreateUnsignedTransaction(
|
||||
extendedPublicKeys []string,
|
||||
minimumSignatures uint32,
|
||||
payments []*Payment,
|
||||
selectedUTXOs []*UTXO) ([]byte, error) {
|
||||
selectedUTXOs []*UTXO) (*serialization.PartiallySignedTransaction, error) {
|
||||
|
||||
sortPublicKeys(extendedPublicKeys)
|
||||
unsignedTransaction, err := createUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serialization.SerializePartiallySignedTransaction(unsignedTransaction)
|
||||
return createUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
|
||||
}
|
||||
|
||||
func multiSigRedeemScript(extendedPublicKeys []string, minimumSignatures uint32, path string, ecdsa bool) ([]byte, error) {
|
||||
|
@ -2,6 +2,7 @@ package libkaspawallet_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"strings"
|
||||
"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) {
|
||||
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
||||
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{{
|
||||
Address: address,
|
||||
Amount: 10,
|
||||
@ -263,7 +278,7 @@ func TestP2PK(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
|
||||
unsignedTransaction, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: address,
|
||||
Amount: 10,
|
||||
@ -425,7 +440,7 @@ func TestMaxSompi(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
unsignedTxWithLargeInputAmount, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
|
||||
unsignedTxWithLargeInputAmount, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: address,
|
||||
Amount: 10,
|
||||
@ -476,7 +491,7 @@ func TestMaxSompi(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
unsignedTxWithLargeInputAndOutputAmount, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
|
||||
unsignedTxWithLargeInputAndOutputAmount, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: address,
|
||||
Amount: 22e6 * constants.SompiPerKaspa,
|
||||
|
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"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 :=
|
||||
daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{
|
||||
From: conf.FromAddresses,
|
||||
@ -49,6 +59,7 @@ func send(conf *sendConfig) error {
|
||||
Amount: sendAmountSompi,
|
||||
IsSendAll: conf.IsSendAll,
|
||||
UseExistingChangeAddress: conf.UseExistingChangeAddress,
|
||||
FeeRate: feeRate,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -139,6 +139,28 @@ message KaspadMessage {
|
||||
GetMempoolEntriesByAddressesResponseMessage getMempoolEntriesByAddressesResponse = 1085;
|
||||
GetCoinSupplyRequestMessage getCoinSupplyRequest = 1086;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.17.2
|
||||
// - protoc v3.12.3
|
||||
// source: messages.proto
|
||||
|
||||
package protowire
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.17.2
|
||||
// protoc-gen-go v1.27.1
|
||||
// protoc v3.12.3
|
||||
// source: p2p.proto
|
||||
|
||||
package protowire
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -721,3 +721,221 @@ message GetCoinSupplyResponseMessage{
|
||||
|
||||
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;
|
||||
}
|
@ -17,7 +17,7 @@ func (x *KaspadMessage_GetCurrentNetworkResponse) toAppMessage() (appmessage.Mes
|
||||
if x == 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 {
|
||||
|
@ -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
|
||||
}
|
@ -968,6 +968,13 @@ func toRPCPayload(message appmessage.Message) (isKaspadMessage_Payload, error) {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
case *appmessage.GetFeeEstimateRequestMessage:
|
||||
payload := new(KaspadMessage_GetFeeEstimateRequest)
|
||||
err := payload.fromAppMessage(message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
|
20
infrastructure/network/rpcclient/rpc_get_fee_estimate.go
Normal file
20
infrastructure/network/rpcclient/rpc_get_fee_estimate.go
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user