mirror of
https://github.com/kaspanet/kaspad.git
synced 2026-02-27 05:33:18 +00:00
Compare commits
5 Commits
v0.12.16-r
...
min-fee-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01c81143ab | ||
|
|
86b89065cf | ||
|
|
f41dc7fa0b | ||
|
|
6b38bf7069 | ||
|
|
d2453f8e7b |
@@ -28,7 +28,8 @@ func HandleSubmitTransaction(context *rpccontext.Context, _ *router.Router, requ
|
||||
}
|
||||
|
||||
log.Debugf("Rejected transaction %s: %s", transactionID, err)
|
||||
errorMessage := &appmessage.SubmitTransactionResponseMessage{}
|
||||
// Return the ID also in the case of error, so that clients can match the response to the correct transaction submit request
|
||||
errorMessage := appmessage.NewSubmitTransactionResponseMessage(transactionID.String())
|
||||
errorMessage.Error = appmessage.RPCErrorf("Rejected transaction %s: %s", transactionID, err)
|
||||
return errorMessage, nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
Kaspad v0.12.17 - 2024-02-19
|
||||
===========================
|
||||
|
||||
* Wallet-related improvements and fixes (#2253, #2257, #2258, #2262)
|
||||
|
||||
Kaspad v0.12.16 - 2023-12-25
|
||||
===========================
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/kaspanet/kaspad/infrastructure/config"
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
)
|
||||
@@ -58,9 +59,9 @@ type sendConfig struct {
|
||||
Password string `long:"password" short:"p" description:"Wallet password"`
|
||||
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
|
||||
ToAddress string `long:"to-address" short:"t" description:"The public address to send Kaspa to" required:"true"`
|
||||
FromAddresses []string `long:"from-address" short:"a" description:"Specific public address to send Kaspa from. Use multiple times to accept several addresses" required:"false"`
|
||||
FromAddresses []string `long:"from-address" short:"a" description:"Specific public address to send Kaspa from. Repeat multiple times (adding -a before each) to accept several addresses" required:"false"`
|
||||
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). 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)"`
|
||||
Verbose bool `long:"show-serialized" short:"s" description:"Show a list of hex encoded sent transactions"`
|
||||
config.NetworkFlags
|
||||
@@ -115,12 +116,13 @@ type newAddressConfig struct {
|
||||
}
|
||||
|
||||
type startDaemonConfig struct {
|
||||
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
|
||||
Password string `long:"password" short:"p" description:"Wallet password"`
|
||||
RPCServer string `long:"rpcserver" short:"s" description:"RPC server to connect to"`
|
||||
Listen string `long:"listen" short:"l" description:"Address to listen on (default: 0.0.0.0:8082)"`
|
||||
Timeout uint32 `long:"wait-timeout" short:"w" description:"Waiting timeout for RPC calls, seconds (default: 30 s)"`
|
||||
Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"`
|
||||
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
|
||||
Password string `long:"password" short:"p" description:"Wallet password"`
|
||||
RPCServer string `long:"rpcserver" short:"s" description:"RPC server to connect to"`
|
||||
Listen string `long:"listen" short:"l" description:"Address to listen on (default: 0.0.0.0:8082)"`
|
||||
Timeout uint32 `long:"wait-timeout" short:"w" description:"Waiting timeout for RPC calls, seconds (default: 30 s)"`
|
||||
Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"`
|
||||
MinFeePerTx uint64 `long:"min-fee-per-tx" description:"Minimum fee per transaction (in sompis) (default: 0)"`
|
||||
config.NetworkFlags
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
@@ -14,13 +15,15 @@ func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.Get
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
if !s.isSynced() {
|
||||
return nil, errors.Errorf("wallet daemon is not synced yet, %s", s.formatSyncStateReport())
|
||||
}
|
||||
|
||||
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
daaScore := dagInfo.VirtualDAAScore
|
||||
maturity := s.params.BlockCoinbaseMaturity
|
||||
|
||||
balancesMap := make(balancesMapType, 0)
|
||||
for _, entry := range s.utxosSortedByAmount {
|
||||
amount := entry.UTXOEntry.Amount()
|
||||
@@ -30,7 +33,7 @@ func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.Get
|
||||
balances = new(balancesType)
|
||||
balancesMap[address] = balances
|
||||
}
|
||||
if isUTXOSpendable(entry, daaScore, maturity) {
|
||||
if s.isUTXOSpendable(entry, daaScore) {
|
||||
balances.available += amount
|
||||
} else {
|
||||
balances.pending += amount
|
||||
@@ -64,9 +67,9 @@ func (s *server) GetBalance(_ context.Context, _ *pb.GetBalanceRequest) (*pb.Get
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isUTXOSpendable(entry *walletUTXO, virtualDAAScore uint64, coinbaseMaturity uint64) bool {
|
||||
func (s *server) isUTXOSpendable(entry *walletUTXO, virtualDAAScore uint64) bool {
|
||||
if !entry.UTXOEntry.IsCoinbase() {
|
||||
return true
|
||||
}
|
||||
return entry.UTXOEntry.BlockDAAScore()+coinbaseMaturity < virtualDAAScore
|
||||
return entry.UTXOEntry.BlockDAAScore()+s.coinbaseMaturity < virtualDAAScore
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
||||
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *server) Broadcast(_ context.Context, request *pb.BroadcastRequest) (*pb.BroadcastResponse, error) {
|
||||
@@ -54,16 +56,12 @@ func (s *server) broadcast(transactions [][]byte, isDomain bool) ([]string, erro
|
||||
}
|
||||
}
|
||||
|
||||
err = s.refreshUTXOs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.forceSync()
|
||||
return txIDs, nil
|
||||
}
|
||||
|
||||
func sendTransaction(client *rpcclient.RPCClient, tx *externalapi.DomainTransaction) (string, error) {
|
||||
submitTransactionResponse, err := client.SubmitTransaction(appmessage.DomainTransactionToRPCTransaction(tx), false)
|
||||
submitTransactionResponse, err := client.SubmitTransaction(appmessage.DomainTransactionToRPCTransaction(tx), consensushashing.TransactionID(tx).String(), false)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error submitting transaction")
|
||||
}
|
||||
|
||||
@@ -3,19 +3,22 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// 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)
|
||||
const minChangeTarget = constants.SompiPerKaspa / 5
|
||||
|
||||
func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.CreateUnsignedTransactionsRequest) (
|
||||
*pb.CreateUnsignedTransactionsResponse, error,
|
||||
) {
|
||||
@@ -35,7 +38,6 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen
|
||||
if !s.isSynced() {
|
||||
return nil, errors.Errorf("wallet daemon is not synced yet, %s", s.formatSyncStateReport())
|
||||
}
|
||||
|
||||
// make sure address string is correct before proceeding to a
|
||||
// potentially long UTXO refreshment operation
|
||||
toAddress, err := util.DecodeAddress(address, s.params.Prefix)
|
||||
@@ -43,16 +45,11 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.refreshUTXOs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var fromAddresses []*walletAddress
|
||||
for _, from := range fromAddressesString {
|
||||
fromAddress, exists := s.addressSet[from]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("Specified from address %s does not exists", from)
|
||||
return nil, fmt.Errorf("specified from address %s does not exists", from)
|
||||
}
|
||||
fromAddresses = append(fromAddresses, fromAddress)
|
||||
}
|
||||
@@ -106,19 +103,14 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
coinbaseMaturity := s.params.BlockCoinbaseMaturity
|
||||
if dagInfo.NetworkName == "kaspa-testnet-11" {
|
||||
coinbaseMaturity = 1000
|
||||
}
|
||||
|
||||
for _, utxo := range s.utxosSortedByAmount {
|
||||
if (fromAddresses != nil && !slices.Contains(fromAddresses, utxo.address)) ||
|
||||
!isUTXOSpendable(utxo, dagInfo.VirtualDAAScore, coinbaseMaturity) {
|
||||
if (fromAddresses != nil && !walletAddressesContain(fromAddresses, utxo.address)) ||
|
||||
!s.isUTXOSpendable(utxo, dagInfo.VirtualDAAScore) {
|
||||
continue
|
||||
}
|
||||
|
||||
if broadcastTime, ok := s.usedOutpoints[*utxo.Outpoint]; ok {
|
||||
if time.Since(broadcastTime) > time.Minute {
|
||||
if s.usedOutpointHasExpired(broadcastTime) {
|
||||
delete(s.usedOutpoints, *utxo.Outpoint)
|
||||
} else {
|
||||
continue
|
||||
@@ -134,17 +126,26 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
|
||||
totalValue += utxo.UTXOEntry.Amount()
|
||||
|
||||
fee := feePerInput * uint64(len(selectedUTXOs))
|
||||
if fee < s.minFeePerTx {
|
||||
fee = s.minFeePerTx
|
||||
}
|
||||
|
||||
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
|
||||
// 2. totalValue > totalSpend, so there will be change and 2 outputs, therefor in order to not struggle with new dust
|
||||
// rules we try and find at least 2 inputs (even though the next one is not necessary in terms of spend value)
|
||||
if !isSendAll && (totalValue == totalSpend || (totalValue > totalSpend && len(selectedUTXOs) > 1)) {
|
||||
// 2. totalValue > totalSpend, so there will be change and 2 outputs, therefor in order to not struggle with --
|
||||
// 2.1 go-nodes dust patch we try and find at least 2 inputs (even though the next one is not necessary in terms of spend value)
|
||||
// 2.2 KIP9 we try and make sure that the change amount is not too small
|
||||
if !isSendAll && (totalValue == totalSpend || (totalValue >= totalSpend+minChangeTarget && len(selectedUTXOs) > 1)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fee := feePerInput * uint64(len(selectedUTXOs))
|
||||
if fee < s.minFeePerTx {
|
||||
fee = s.minFeePerTx
|
||||
}
|
||||
|
||||
var totalSpend uint64
|
||||
if isSendAll {
|
||||
totalSpend = totalValue
|
||||
@@ -160,3 +161,13 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
|
||||
|
||||
return selectedUTXOs, totalReceived, totalValue - totalSpend, nil
|
||||
}
|
||||
|
||||
func walletAddressesContain(addresses []*walletAddress, contain *walletAddress) bool {
|
||||
for _, address := range addresses {
|
||||
if *address == *contain {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/version"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/version"
|
||||
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
|
||||
"github.com/kaspanet/kaspad/util/txmass"
|
||||
@@ -28,17 +30,23 @@ import (
|
||||
type server struct {
|
||||
pb.UnimplementedKaspawalletdServer
|
||||
|
||||
rpcClient *rpcclient.RPCClient
|
||||
params *dagconfig.Params
|
||||
rpcClient *rpcclient.RPCClient // RPC client for ongoing user requests
|
||||
backgroundRPCClient *rpcclient.RPCClient // RPC client dedicated for address and UTXO background fetching
|
||||
params *dagconfig.Params
|
||||
coinbaseMaturity uint64 // Is different from default if we use testnet-11
|
||||
minFeePerTx uint64
|
||||
|
||||
lock sync.RWMutex
|
||||
utxosSortedByAmount []*walletUTXO
|
||||
nextSyncStartIndex uint32
|
||||
keysFile *keys.File
|
||||
shutdown chan struct{}
|
||||
addressSet walletAddressSet
|
||||
txMassCalculator *txmass.Calculator
|
||||
usedOutpoints map[externalapi.DomainOutpoint]time.Time
|
||||
lock sync.RWMutex
|
||||
utxosSortedByAmount []*walletUTXO
|
||||
nextSyncStartIndex uint32
|
||||
keysFile *keys.File
|
||||
shutdown chan struct{}
|
||||
forceSyncChan chan struct{}
|
||||
startTimeOfLastCompletedRefresh time.Time
|
||||
addressSet walletAddressSet
|
||||
txMassCalculator *txmass.Calculator
|
||||
usedOutpoints map[externalapi.DomainOutpoint]time.Time
|
||||
firstSyncDone atomic.Bool
|
||||
|
||||
isLogFinalProgressLineShown bool
|
||||
maxUsedAddressesForLog uint32
|
||||
@@ -50,7 +58,7 @@ type server struct {
|
||||
const MaxDaemonSendMsgSize = 100_000_000
|
||||
|
||||
// Start starts the kaspawalletd server
|
||||
func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath string, profile string, timeout uint32) error {
|
||||
func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath string, profile string, timeout uint32, minFeePerTx uint64) error {
|
||||
initLog(defaultLogFile, defaultErrLogFile)
|
||||
|
||||
defer panics.HandlePanic(log, "MAIN", nil)
|
||||
@@ -72,6 +80,10 @@ func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath stri
|
||||
if err != nil {
|
||||
return (errors.Wrapf(err, "Error connecting to RPC server %s", rpcServer))
|
||||
}
|
||||
backgroundRPCClient, err := connectToRPC(params, rpcServer, timeout)
|
||||
if err != nil {
|
||||
return (errors.Wrapf(err, "Error making a second connection to RPC server %s", rpcServer))
|
||||
}
|
||||
|
||||
log.Infof("Connected, reading keys file %s...", keysFilePath)
|
||||
keysFile, err := keys.ReadKeysFile(params, keysFilePath)
|
||||
@@ -84,13 +96,27 @@ func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath stri
|
||||
return err
|
||||
}
|
||||
|
||||
dagInfo, err := rpcClient.GetBlockDAGInfo()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
coinbaseMaturity := params.BlockCoinbaseMaturity
|
||||
if dagInfo.NetworkName == "kaspa-testnet-11" {
|
||||
coinbaseMaturity = 1000
|
||||
}
|
||||
|
||||
serverInstance := &server{
|
||||
rpcClient: rpcClient,
|
||||
backgroundRPCClient: backgroundRPCClient,
|
||||
params: params,
|
||||
coinbaseMaturity: coinbaseMaturity,
|
||||
minFeePerTx: minFeePerTx,
|
||||
utxosSortedByAmount: []*walletUTXO{},
|
||||
nextSyncStartIndex: 0,
|
||||
keysFile: keysFile,
|
||||
shutdown: make(chan struct{}),
|
||||
forceSyncChan: make(chan struct{}),
|
||||
addressSet: make(walletAddressSet),
|
||||
txMassCalculator: txmass.NewCalculator(params.MassPerTxByte, params.MassPerScriptPubKeyByte, params.MassPerSigOp),
|
||||
usedOutpoints: map[externalapi.DomainOutpoint]time.Time{},
|
||||
@@ -100,8 +126,8 @@ func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath stri
|
||||
}
|
||||
|
||||
log.Infof("Read, syncing the wallet...")
|
||||
spawn("serverInstance.sync", func() {
|
||||
err := serverInstance.sync()
|
||||
spawn("serverInstance.syncLoop", func() {
|
||||
err := serverInstance.syncLoop()
|
||||
if err != nil {
|
||||
printErrorAndExit(errors.Wrap(err, "error syncing the wallet"))
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawa
|
||||
if _, ok := alreadySelectedUTXOsMap[*utxo.Outpoint]; ok {
|
||||
continue
|
||||
}
|
||||
if !isUTXOSpendable(utxo, dagInfo.VirtualDAAScore, s.params.BlockCoinbaseMaturity) {
|
||||
if !s.isUTXOSpendable(utxo, dagInfo.VirtualDAAScore) {
|
||||
continue
|
||||
}
|
||||
additionalUTXOs = append(additionalUTXOs, &libkaspawallet.UTXO{
|
||||
|
||||
@@ -23,7 +23,7 @@ func (was walletAddressSet) strings() []string {
|
||||
return addresses
|
||||
}
|
||||
|
||||
func (s *server) sync() error {
|
||||
func (s *server) syncLoop() error {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -32,29 +32,39 @@ func (s *server) sync() error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.refreshExistingUTXOsWithLock()
|
||||
err = s.refreshUTXOs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for range ticker.C {
|
||||
err = s.collectFarAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
s.firstSyncDone.Store(true)
|
||||
log.Infof("Wallet is synced and ready for operation")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-s.forceSyncChan:
|
||||
}
|
||||
|
||||
err = s.collectRecentAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.refreshExistingUTXOsWithLock()
|
||||
err := s.sync()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
func (s *server) sync() error {
|
||||
err := s.collectFarAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.collectRecentAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.refreshUTXOs()
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -158,7 +168,7 @@ func (s *server) collectAddresses(start, end uint32) error {
|
||||
return err
|
||||
}
|
||||
|
||||
getBalancesByAddressesResponse, err := s.rpcClient.GetBalancesByAddresses(addressSet.strings())
|
||||
getBalancesByAddressesResponse, err := s.backgroundRPCClient.GetBalancesByAddresses(addressSet.strings())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -208,15 +218,17 @@ func (s *server) updateAddressesAndLastUsedIndexes(requestedAddressSet walletAdd
|
||||
return s.keysFile.SetLastUsedInternalIndex(lastUsedInternalIndex)
|
||||
}
|
||||
|
||||
func (s *server) refreshExistingUTXOsWithLock() error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
return s.refreshUTXOs()
|
||||
func (s *server) usedOutpointHasExpired(outpointBroadcastTime time.Time) bool {
|
||||
// If the node returns a UTXO we previously attempted to spend and enough time has passed, we assume
|
||||
// that the network rejected or lost the previous transaction and allow a reuse. We set this time
|
||||
// interval to a minute.
|
||||
// We also verify that a full refresh UTXO operation started after this time point and has already
|
||||
// completed, in order to make sure that indeed this state reflects a state obtained following the required wait time.
|
||||
return s.startTimeOfLastCompletedRefresh.After(outpointBroadcastTime.Add(time.Minute))
|
||||
}
|
||||
|
||||
// updateUTXOSet clears the current UTXO set, and re-fills it with the given entries
|
||||
func (s *server) updateUTXOSet(entries []*appmessage.UTXOsByAddressesEntry, mempoolEntries []*appmessage.MempoolEntryByAddress) error {
|
||||
func (s *server) updateUTXOSet(entries []*appmessage.UTXOsByAddressesEntry, mempoolEntries []*appmessage.MempoolEntryByAddress, refreshStart time.Time) error {
|
||||
utxos := make([]*walletUTXO, 0, len(entries))
|
||||
|
||||
exclude := make(map[appmessage.RPCOutpoint]struct{})
|
||||
@@ -243,6 +255,7 @@ func (s *server) updateUTXOSet(entries []*appmessage.UTXOsByAddressesEntry, memp
|
||||
return err
|
||||
}
|
||||
|
||||
// No need to lock for reading since the only writer of this set is on `syncLoop` on the same goroutine.
|
||||
address, ok := s.addressSet[entry.Address]
|
||||
if !ok {
|
||||
return errors.Errorf("Got result from address %s even though it wasn't requested", entry.Address)
|
||||
@@ -256,32 +269,56 @@ func (s *server) updateUTXOSet(entries []*appmessage.UTXOsByAddressesEntry, memp
|
||||
|
||||
sort.Slice(utxos, func(i, j int) bool { return utxos[i].UTXOEntry.Amount() > utxos[j].UTXOEntry.Amount() })
|
||||
|
||||
s.lock.Lock()
|
||||
s.startTimeOfLastCompletedRefresh = refreshStart
|
||||
s.utxosSortedByAmount = utxos
|
||||
|
||||
// Cleanup expired used outpoints to avoid a memory leak
|
||||
for outpoint, broadcastTime := range s.usedOutpoints {
|
||||
if s.usedOutpointHasExpired(broadcastTime) {
|
||||
delete(s.usedOutpoints, outpoint)
|
||||
}
|
||||
}
|
||||
s.lock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) refreshUTXOs() error {
|
||||
refreshStart := time.Now()
|
||||
|
||||
// No need to lock for reading since the only writer of this set is on `syncLoop` on the same goroutine.
|
||||
addresses := s.addressSet.strings()
|
||||
// It's important to check the mempool before calling `GetUTXOsByAddresses`:
|
||||
// If we would do it the other way around an output can be spent in the mempool
|
||||
// and not in consensus, and between the calls its spending transaction will be
|
||||
// added to consensus and removed from the mempool, so `getUTXOsByAddressesResponse`
|
||||
// will include an obsolete output.
|
||||
mempoolEntriesByAddresses, err := s.rpcClient.GetMempoolEntriesByAddresses(s.addressSet.strings(), true, true)
|
||||
mempoolEntriesByAddresses, err := s.backgroundRPCClient.GetMempoolEntriesByAddresses(addresses, true, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
getUTXOsByAddressesResponse, err := s.rpcClient.GetUTXOsByAddresses(s.addressSet.strings())
|
||||
getUTXOsByAddressesResponse, err := s.backgroundRPCClient.GetUTXOsByAddresses(addresses)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.updateUTXOSet(getUTXOsByAddressesResponse.Entries, mempoolEntriesByAddresses.Entries)
|
||||
return s.updateUTXOSet(getUTXOsByAddressesResponse.Entries, mempoolEntriesByAddresses.Entries, refreshStart)
|
||||
}
|
||||
|
||||
func (s *server) forceSync() {
|
||||
// Technically if two callers check the `if` simultaneously they will both spawn a
|
||||
// goroutine, but we don't care about the small redundancy in such a rare case.
|
||||
if len(s.forceSyncChan) == 0 {
|
||||
go func() {
|
||||
s.forceSyncChan <- struct{}{}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) isSynced() bool {
|
||||
return s.nextSyncStartIndex > s.maxUsedIndex()
|
||||
return s.nextSyncStartIndex > s.maxUsedIndex() && s.firstSyncDone.Load()
|
||||
}
|
||||
|
||||
func (s *server) formatSyncStateReport() string {
|
||||
@@ -291,8 +328,11 @@ func (s *server) formatSyncStateReport() string {
|
||||
maxUsedIndex = s.nextSyncStartIndex
|
||||
}
|
||||
|
||||
return fmt.Sprintf("scanned %d out of %d addresses (%.2f%%)",
|
||||
s.nextSyncStartIndex, maxUsedIndex, float64(s.nextSyncStartIndex)*100.0/float64(maxUsedIndex))
|
||||
if s.nextSyncStartIndex < s.maxUsedIndex() {
|
||||
return fmt.Sprintf("scanned %d out of %d addresses (%.2f%%)",
|
||||
s.nextSyncStartIndex, maxUsedIndex, float64(s.nextSyncStartIndex)*100.0/float64(maxUsedIndex))
|
||||
}
|
||||
return "loading the wallet UTXO set"
|
||||
}
|
||||
|
||||
func (s *server) updateSyncingProgressLog(currProcessedAddresses, currMaxUsedAddresses uint32) {
|
||||
@@ -311,7 +351,7 @@ func (s *server) updateSyncingProgressLog(currProcessedAddresses, currMaxUsedAdd
|
||||
|
||||
if s.maxProcessedAddressesForLog >= s.maxUsedAddressesForLog {
|
||||
if !s.isLogFinalProgressLineShown {
|
||||
log.Infof("Wallet is synced, ready for queries")
|
||||
log.Infof("Finished scanning recent addresses")
|
||||
s.isLogFinalProgressLineShown = true
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -3,5 +3,5 @@ package main
|
||||
import "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
|
||||
func startDaemon(conf *startDaemonConfig) error {
|
||||
return server.Start(conf.NetParams(), conf.Listen, conf.RPCServer, conf.KeysFile, conf.Profile, conf.Timeout)
|
||||
return server.Start(conf.NetParams(), conf.Listen, conf.RPCServer, conf.KeysFile, conf.Profile, conf.Timeout, conf.MinFeePerTx)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,44 @@
|
||||
package rpcclient
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
)
|
||||
|
||||
// SubmitTransaction sends an RPC request respective to the function's name and returns the RPC server's response
|
||||
func (c *RPCClient) SubmitTransaction(transaction *appmessage.RPCTransaction, allowOrphan bool) (*appmessage.SubmitTransactionResponseMessage, error) {
|
||||
func (c *RPCClient) SubmitTransaction(transaction *appmessage.RPCTransaction, transactionID string, allowOrphan bool) (*appmessage.SubmitTransactionResponseMessage, error) {
|
||||
err := c.rpcRouter.outgoingRoute().Enqueue(appmessage.NewSubmitTransactionRequestMessage(transaction, allowOrphan))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := c.route(appmessage.CmdSubmitTransactionResponseMessage).DequeueWithTimeout(c.timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submitTransactionResponse := response.(*appmessage.SubmitTransactionResponseMessage)
|
||||
if submitTransactionResponse.Error != nil {
|
||||
return nil, c.convertRPCError(submitTransactionResponse.Error)
|
||||
}
|
||||
for {
|
||||
response, err := c.route(appmessage.CmdSubmitTransactionResponseMessage).DequeueWithTimeout(c.timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submitTransactionResponse := response.(*appmessage.SubmitTransactionResponseMessage)
|
||||
// Match the response to the expected ID. If they are different it means we got an old response which we
|
||||
// previously timed-out on, so we log and continue waiting for the correct current response.
|
||||
if submitTransactionResponse.TransactionID != transactionID {
|
||||
if submitTransactionResponse.Error != nil {
|
||||
// A non-updated Kaspad might return an empty ID in the case of error, so in
|
||||
// such a case we fallback to checking if the error contains the expected ID
|
||||
if submitTransactionResponse.TransactionID != "" || !strings.Contains(submitTransactionResponse.Error.Message, transactionID) {
|
||||
log.Warnf("SubmitTransaction: received an error response for previous request: %s", submitTransactionResponse.Error)
|
||||
continue
|
||||
}
|
||||
|
||||
return submitTransactionResponse, nil
|
||||
} else {
|
||||
log.Warnf("SubmitTransaction: received a successful response for previous request with ID %s",
|
||||
submitTransactionResponse.TransactionID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if submitTransactionResponse.Error != nil {
|
||||
return nil, c.convertRPCError(submitTransactionResponse.Error)
|
||||
}
|
||||
|
||||
return submitTransactionResponse, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func submitAnAmountOfTransactionsToTheMempool(t *testing.T, rpcClient *rpcclient
|
||||
|
||||
for i, transaction := range transactions {
|
||||
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(transaction)
|
||||
_, err := rpcClient.SubmitTransaction(rpcTransaction, false)
|
||||
_, err := rpcClient.SubmitTransaction(rpcTransaction, consensushashing.TransactionID(transaction).String(), false)
|
||||
if err != nil {
|
||||
if ignoreOrphanRejects && strings.Contains(err.Error(), "orphan") {
|
||||
continue
|
||||
|
||||
@@ -53,7 +53,7 @@ func TestTxRelay(t *testing.T) {
|
||||
msgTx := generateTx(t, secondBlock.Transactions[transactionhelper.CoinbaseTransactionIndex], payer, payee)
|
||||
domainTransaction := appmessage.MsgTxToDomainTransaction(msgTx)
|
||||
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(domainTransaction)
|
||||
response, err := payer.rpcClient.SubmitTransaction(rpcTransaction, false)
|
||||
response, err := payer.rpcClient.SubmitTransaction(rpcTransaction, consensushashing.TransactionID(domainTransaction).String(), false)
|
||||
if err != nil {
|
||||
t.Fatalf("Error submitting transaction: %+v", err)
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ func TestUTXOIndex(t *testing.T) {
|
||||
// Submit a few transactions that spends some UTXOs
|
||||
const transactionAmountToSpend = 5
|
||||
for i := 0; i < transactionAmountToSpend; i++ {
|
||||
rpcTransaction := buildTransactionForUTXOIndexTest(t, notificationEntries[i])
|
||||
_, err = kaspad.rpcClient.SubmitTransaction(rpcTransaction, false)
|
||||
rpcTransaction, transactionID := buildTransactionForUTXOIndexTest(t, notificationEntries[i])
|
||||
_, err = kaspad.rpcClient.SubmitTransaction(rpcTransaction, transactionID, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Error submitting transaction: %s", err)
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func TestUTXOIndex(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func buildTransactionForUTXOIndexTest(t *testing.T, entry *appmessage.UTXOsByAddressesEntry) *appmessage.RPCTransaction {
|
||||
func buildTransactionForUTXOIndexTest(t *testing.T, entry *appmessage.UTXOsByAddressesEntry) (*appmessage.RPCTransaction, string) {
|
||||
transactionIDBytes, err := hex.DecodeString(entry.Outpoint.TransactionID)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding transaction ID: %s", err)
|
||||
@@ -224,5 +224,5 @@ func buildTransactionForUTXOIndexTest(t *testing.T, entry *appmessage.UTXOsByAdd
|
||||
msgTx.TxIn[0].SignatureScript = signatureScript
|
||||
|
||||
domainTransaction := appmessage.MsgTxToDomainTransaction(msgTx)
|
||||
return appmessage.DomainTransactionToRPCTransaction(domainTransaction)
|
||||
return appmessage.DomainTransactionToRPCTransaction(domainTransaction), consensushashing.TransactionID(domainTransaction).String()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const validCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs
|
||||
const (
|
||||
appMajor uint = 0
|
||||
appMinor uint = 12
|
||||
appPatch uint = 16
|
||||
appPatch uint = 17
|
||||
)
|
||||
|
||||
// appBuild is defined as a variable so it can be overridden during the build
|
||||
|
||||
Reference in New Issue
Block a user