mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-06-06 14:16:43 +00:00
Fix off by small amounts in sent amount kaspa (#2220)
* Fix off by small amounts in sent amount kaspa Floating point math causes inconsistencies when converting kas to sompi. Use a method that parses the amount as a string, the converts it to sompi then parse back to uint64 * Deal with SendAmount as strings all the way * Consistent config handling * Set variables directly from utils.KasToSompi Use = instead of := to ensure no shadowing * Fix validate amount regex * Use decimal places as defined by constants Also check if SompiPerKaspa is multiple of 10 * Minor updates for context clarity
This commit is contained in:
parent
c417c8b525
commit
387fade044
@ -57,7 +57,7 @@ type sendConfig struct {
|
|||||||
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
|
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"`
|
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. Use multiple times to accept several addresses" required:"false"`
|
||||||
SendAmount float64 `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)"`
|
||||||
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"`
|
||||||
@ -74,7 +74,7 @@ type createUnsignedTransactionConfig struct {
|
|||||||
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
|
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"`
|
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. Use multiple times to accept several addresses" required:"false"`
|
||||||
SendAmount float64 `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)"`
|
||||||
config.NetworkFlags
|
config.NetworkFlags
|
||||||
@ -296,8 +296,8 @@ func parseCommandLine() (subCommand string, config interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateCreateUnsignedTransactionConf(conf *createUnsignedTransactionConfig) error {
|
func validateCreateUnsignedTransactionConf(conf *createUnsignedTransactionConfig) error {
|
||||||
if (!conf.IsSendAll && conf.SendAmount == 0) ||
|
if (!conf.IsSendAll && conf.SendAmount == "") ||
|
||||||
(conf.IsSendAll && conf.SendAmount > 0) {
|
(conf.IsSendAll && conf.SendAmount != "") {
|
||||||
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
@ -305,8 +305,8 @@ func validateCreateUnsignedTransactionConf(conf *createUnsignedTransactionConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateSendConfig(conf *sendConfig) error {
|
func validateSendConfig(conf *sendConfig) error {
|
||||||
if (!conf.IsSendAll && conf.SendAmount == 0) ||
|
if (!conf.IsSendAll && conf.SendAmount == "") ||
|
||||||
(conf.IsSendAll && conf.SendAmount > 0) {
|
(conf.IsSendAll && conf.SendAmount != "") {
|
||||||
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
|
func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
|
||||||
@ -20,7 +20,12 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
sendAmountSompi := uint64(conf.SendAmount * constants.SompiPerKaspa)
|
sendAmountSompi, err := utils.KasToSompi(conf.SendAmount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
|
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
|
||||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,7 +35,11 @@ func send(conf *sendConfig) error {
|
|||||||
|
|
||||||
var sendAmountSompi uint64
|
var sendAmountSompi uint64
|
||||||
if !conf.IsSendAll {
|
if !conf.IsSendAll {
|
||||||
sendAmountSompi = uint64(conf.SendAmount * constants.SompiPerKaspa)
|
sendAmountSompi, err = utils.KasToSompi(conf.SendAmount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createUnsignedTransactionsResponse, err :=
|
createUnsignedTransactionsResponse, err :=
|
||||||
|
@ -2,8 +2,13 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FormatKas takes the amount of sompis as uint64, and returns amount of KAS with 8 decimal places
|
// FormatKas takes the amount of sompis as uint64, and returns amount of KAS with 8 decimal places
|
||||||
@ -14,3 +19,50 @@ func FormatKas(amount uint64) string {
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KasToSompi takes in a string representation of the Kas value to convert to Sompi
|
||||||
|
func KasToSompi(amount string) (uint64, error) {
|
||||||
|
err := validateKASAmountFormat(amount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// after validation, amount can only be either an int OR
|
||||||
|
// a float with an int component and decimal places
|
||||||
|
parts := strings.Split(amount, ".")
|
||||||
|
amountStr := ""
|
||||||
|
|
||||||
|
if constants.SompiPerKaspa%10 != 0 {
|
||||||
|
return 0, errors.Errorf("Unable to convert to sompi when SompiPerKaspa is not a multiple of 10")
|
||||||
|
}
|
||||||
|
|
||||||
|
decimalPlaces := int(math.Log10(constants.SompiPerKaspa))
|
||||||
|
decimalStr := ""
|
||||||
|
|
||||||
|
if len(parts) == 2 {
|
||||||
|
decimalStr = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
amountStr = fmt.Sprintf("%s%-*s", parts[0], decimalPlaces, decimalStr) // Padded with spaces at the end to fill for missing decimals: Sample "0.01234 "
|
||||||
|
amountStr = strings.ReplaceAll(amountStr, " ", "0") // Make the spaces be 0s. Sample "0.012340000"
|
||||||
|
|
||||||
|
convertedAmount, err := strconv.ParseUint(amountStr, 10, 64)
|
||||||
|
|
||||||
|
return convertedAmount, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateKASAmountFormat(amount string) error {
|
||||||
|
// Check whether it's an integer, or a float with max 8 digits
|
||||||
|
match, err := regexp.MatchString("^([1-9]\\d{0,11}|0)(\\.\\d{0,8})?$", amount)
|
||||||
|
|
||||||
|
if !match {
|
||||||
|
return errors.Errorf("Invalid amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
90
cmd/kaspawallet/utils/format_kas_test.go
Normal file
90
cmd/kaspawallet/utils/format_kas_test.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Takes in a string representation of the Kas value to convert to Sompi
|
||||||
|
func TestKasToSompi(t *testing.T) {
|
||||||
|
type testVector struct {
|
||||||
|
originalAmount string
|
||||||
|
convertedAmount uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
validCases := []testVector{
|
||||||
|
{originalAmount: "0", convertedAmount: 0},
|
||||||
|
{originalAmount: "1", convertedAmount: 100000000},
|
||||||
|
{originalAmount: "33184.1489732", convertedAmount: 3318414897320},
|
||||||
|
{originalAmount: "21.35808032", convertedAmount: 2135808032},
|
||||||
|
{originalAmount: "184467440737.09551615", convertedAmount: 18446744073709551615},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, currentTestVector := range validCases {
|
||||||
|
convertedAmount, err := KasToSompi(currentTestVector.originalAmount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if convertedAmount != currentTestVector.convertedAmount {
|
||||||
|
t.Errorf("Expected %s, to convert to %d. Got: %d", currentTestVector.originalAmount, currentTestVector.convertedAmount, convertedAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidCases := []string{
|
||||||
|
"184467440737.09551616", // Bigger than max uint64
|
||||||
|
"-1",
|
||||||
|
"a",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, currentTestVector := range invalidCases {
|
||||||
|
_, err := KasToSompi(currentTestVector)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected an error but succeeded validation for test case %s", currentTestVector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateAmountFormat(t *testing.T) {
|
||||||
|
validCases := []string{
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"1.0",
|
||||||
|
"0.1",
|
||||||
|
"0.12345678",
|
||||||
|
"111111111111.11111111", // 12 digits to the left of decimal, 8 digits to the right
|
||||||
|
"184467440737.09551615", // Maximum input that can be represented in sompi later
|
||||||
|
"184467440737.09551616", // Cannot be represented in sompi, but we'll acccept for "correct format"
|
||||||
|
"999999999999.99999999", // Cannot be represented in sompi, but we'll acccept for "correct format"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range validCases {
|
||||||
|
err := validateKASAmountFormat(testCase)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidCases := []string{
|
||||||
|
"",
|
||||||
|
"a",
|
||||||
|
"-1",
|
||||||
|
"0.123456789", // 9 decimal digits
|
||||||
|
".1", // decimal but no integer component
|
||||||
|
"0a", // Extra character
|
||||||
|
"0000000000000", // 13 zeros
|
||||||
|
"012", // Int padded with zero
|
||||||
|
"00.1", // Decimal padded with zeros
|
||||||
|
"111111111111111111111", // all digits
|
||||||
|
"111111111111A11111111", // non-period/non-digit where decimal would be
|
||||||
|
"000000000000.00000000", // all zeros
|
||||||
|
"kaspa", // all text
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range invalidCases {
|
||||||
|
err := validateKASAmountFormat(testCase)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected an error but succeeded validation for test case %s", testCase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user