Deal with SendAmount as strings all the way

This commit is contained in:
coderofstuff 2023-09-22 08:45:26 -06:00
parent 471b7b2f16
commit 1a6e883ece
8 changed files with 165 additions and 47 deletions

View File

@ -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")
}

View File

@ -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,

View File

@ -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
}

View File

@ -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))
}
}
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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)
}
}
}