mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-03-30 15:08:33 +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"`
|
||||
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"`
|
||||
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)"`
|
||||
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"`
|
||||
@ -74,7 +74,7 @@ type createUnsignedTransactionConfig struct {
|
||||
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"`
|
||||
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)"`
|
||||
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
|
||||
@ -296,8 +296,8 @@ func parseCommandLine() (subCommand string, config interface{}) {
|
||||
}
|
||||
|
||||
func validateCreateUnsignedTransactionConf(conf *createUnsignedTransactionConfig) error {
|
||||
if (!conf.IsSendAll && conf.SendAmount == 0) ||
|
||||
(conf.IsSendAll && conf.SendAmount > 0) {
|
||||
if (!conf.IsSendAll && conf.SendAmount == "") ||
|
||||
(conf.IsSendAll && conf.SendAmount != "") {
|
||||
|
||||
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 {
|
||||
if (!conf.IsSendAll && conf.SendAmount == 0) ||
|
||||
(conf.IsSendAll && conf.SendAmount > 0) {
|
||||
if (!conf.IsSendAll && conf.SendAmount == "") ||
|
||||
(conf.IsSendAll && conf.SendAmount != "") {
|
||||
|
||||
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/pb"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
|
||||
)
|
||||
|
||||
func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
|
||||
@ -20,7 +20,12 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
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{
|
||||
From: conf.FromAddresses,
|
||||
Address: conf.ToAddress,
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -35,7 +35,11 @@ func send(conf *sendConfig) error {
|
||||
|
||||
var sendAmountSompi uint64
|
||||
if !conf.IsSendAll {
|
||||
sendAmountSompi = uint64(conf.SendAmount * constants.SompiPerKaspa)
|
||||
sendAmountSompi, err = utils.KasToSompi(conf.SendAmount)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
createUnsignedTransactionsResponse, err :=
|
||||
|
@ -2,8 +2,13 @@ package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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
|
||||
@ -14,3 +19,50 @@ func FormatKas(amount uint64) string {
|
||||
}
|
||||
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