mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-11-24 06:25:55 +00:00
Deal with SendAmount as strings all the way
This commit is contained in:
parent
471b7b2f16
commit
1a6e883ece
@ -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,
|
||||||
|
|||||||
@ -4,13 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"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/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/cmd/kaspawallet/utils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,7 +35,13 @@ func send(conf *sendConfig) error {
|
|||||||
|
|
||||||
var sendAmountSompi uint64
|
var sendAmountSompi uint64
|
||||||
if !conf.IsSendAll {
|
if !conf.IsSendAll {
|
||||||
sendAmountSompi = kasToSompi(conf.SendAmount)
|
parsedAmountSompi, err := utils.KasToSompi(conf.SendAmount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sendAmountSompi = parsedAmountSompi
|
||||||
}
|
}
|
||||||
|
|
||||||
createUnsignedTransactionsResponse, err :=
|
createUnsignedTransactionsResponse, err :=
|
||||||
@ -99,15 +105,3 @@ func send(conf *sendConfig) error {
|
|||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,6 +2,8 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||||
)
|
)
|
||||||
@ -14,3 +16,28 @@ func FormatKas(amount uint64) string {
|
|||||||
}
|
}
|
||||||
return res
|
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
|
||||||
|
}
|
||||||
|
|||||||
44
cmd/kaspawallet/utils/format_kas_test.go
Normal file
44
cmd/kaspawallet/utils/format_kas_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
cmd/kaspawallet/utils/validate_amount.go
Normal file
34
cmd/kaspawallet/utils/validate_amount.go
Normal 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
|
||||||
|
}
|
||||||
39
cmd/kaspawallet/utils/validate_amount_test.go
Normal file
39
cmd/kaspawallet/utils/validate_amount_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user