diff --git a/cmd/kaspawallet/config.go b/cmd/kaspawallet/config.go index 6313167bf..60779d0ca 100644 --- a/cmd/kaspawallet/config.go +++ b/cmd/kaspawallet/config.go @@ -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") } diff --git a/cmd/kaspawallet/create_unsigned_tx.go b/cmd/kaspawallet/create_unsigned_tx.go index 8f201685a..f213bc958 100644 --- a/cmd/kaspawallet/create_unsigned_tx.go +++ b/cmd/kaspawallet/create_unsigned_tx.go @@ -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, diff --git a/cmd/kaspawallet/send.go b/cmd/kaspawallet/send.go index 71d6fdb24..fc961c544 100644 --- a/cmd/kaspawallet/send.go +++ b/cmd/kaspawallet/send.go @@ -4,13 +4,13 @@ import ( "context" "fmt" "os" - "strconv" "strings" "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client" "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/cmd/kaspawallet/utils" "github.com/pkg/errors" ) @@ -35,7 +35,13 @@ func send(conf *sendConfig) error { var sendAmountSompi uint64 if !conf.IsSendAll { - sendAmountSompi = kasToSompi(conf.SendAmount) + parsedAmountSompi, err := utils.KasToSompi(conf.SendAmount) + + if err != nil { + return err + } + + sendAmountSompi = parsedAmountSompi } createUnsignedTransactionsResponse, err := @@ -99,15 +105,3 @@ func send(conf *sendConfig) error { return nil } - -/** - */ -func kasToSompi(amount float64) uint64 { - amountInStr := fmt.Sprintf("%.8f", amount) - - parts := strings.Split(amountInStr, ".") - - convertedAmount, _ := strconv.ParseUint(strings.Join(parts, ""), 10, 64) - - return convertedAmount -} diff --git a/cmd/kaspawallet/send_test.go b/cmd/kaspawallet/send_test.go deleted file mode 100644 index dcd04b4e0..000000000 --- a/cmd/kaspawallet/send_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import "testing" - -func TestKasToSompi(t *testing.T) { - type testVector struct { - originalAmount float64 - convertedAmount uint64 - } - - testVectors := []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 testVectors { - if kasToSompi(currentTestVector.originalAmount) != currentTestVector.convertedAmount { - t.Fail() - t.Logf("Expected %.8f, to convert to %d. Got: %d", currentTestVector.originalAmount, currentTestVector.convertedAmount, kasToSompi(currentTestVector.originalAmount)) - } - } -} diff --git a/cmd/kaspawallet/utils/format_kas.go b/cmd/kaspawallet/utils/format_kas.go index 5b1de0bea..7b2e616ad 100644 --- a/cmd/kaspawallet/utils/format_kas.go +++ b/cmd/kaspawallet/utils/format_kas.go @@ -2,6 +2,8 @@ package utils import ( "fmt" + "strconv" + "strings" "github.com/kaspanet/kaspad/domain/consensus/utils/constants" ) @@ -14,3 +16,28 @@ func FormatKas(amount uint64) string { } return res } + +// Takes in a string representation of the Kas value to convert to Sompi +func KasToSompi(amount string) (uint64, error) { + err := ValidateAmountFormat(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 len(parts) == 2 { + amountStr = fmt.Sprintf("%s%-*s", parts[0], 8, parts[1]) // 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" + } else { + amountStr = fmt.Sprintf("%s00000000", parts[0]) + } + + convertedAmount, err := strconv.ParseUint(amountStr, 10, 64) + + return convertedAmount, err +} diff --git a/cmd/kaspawallet/utils/format_kas_test.go b/cmd/kaspawallet/utils/format_kas_test.go new file mode 100644 index 000000000..56b04e1df --- /dev/null +++ b/cmd/kaspawallet/utils/format_kas_test.go @@ -0,0 +1,44 @@ +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) + } + } +} diff --git a/cmd/kaspawallet/utils/validate_amount.go b/cmd/kaspawallet/utils/validate_amount.go new file mode 100644 index 000000000..44aca96c9 --- /dev/null +++ b/cmd/kaspawallet/utils/validate_amount.go @@ -0,0 +1,34 @@ +package utils + +import ( + "regexp" + "strconv" + + "github.com/pkg/errors" +) + +/** + * 1. May be an integer (no decimal components) + * 2. May be float with up to 8 decimal places + */ +func ValidateAmountFormat(amount string) error { + // Check whether it's an integer, or a float with max 8 digits + match, err := regexp.MatchString("^\\d{1,19}(.\\d{0,8})?$", amount) + + if !match { + return errors.Errorf("Invalid send amount") + } + + if err != nil { + return err + } + + // If it parses properly, then this is valid + _, err = strconv.ParseFloat(amount, 64) + + if err != nil { + return err + } + + return nil +} diff --git a/cmd/kaspawallet/utils/validate_amount_test.go b/cmd/kaspawallet/utils/validate_amount_test.go new file mode 100644 index 000000000..4e4251ec7 --- /dev/null +++ b/cmd/kaspawallet/utils/validate_amount_test.go @@ -0,0 +1,39 @@ +package utils + +import ( + "testing" +) + +func TestValidateAmountFormat(t *testing.T) { + validCases := []string{ + "0", + "1", + "1.0", + "0.1", + "0.12345678", + } + + for _, testCase := range validCases { + err := ValidateAmountFormat(testCase) + + if err != nil { + t.Error(err) + } + } + + invalidCases := []string{ + "", + "a", + "-1", + "0.123456789", // 9 decimal digits + ".1", // decimal but no integer component + } + + for _, testCase := range invalidCases { + err := ValidateAmountFormat(testCase) + + if err == nil { + t.Errorf("Expected an error but succeeded validation for test case %s", testCase) + } + } +}