kaspad/cmd/kaspawallet/config.go
coderofstuff 387fade044
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
2023-09-23 11:28:38 +03:00

324 lines
14 KiB
Go

package main
import (
"os"
"github.com/kaspanet/kaspad/infrastructure/config"
"github.com/pkg/errors"
"github.com/jessevdk/go-flags"
)
const (
createSubCmd = "create"
balanceSubCmd = "balance"
sendSubCmd = "send"
sweepSubCmd = "sweep"
createUnsignedTransactionSubCmd = "create-unsigned-transaction"
signSubCmd = "sign"
broadcastSubCmd = "broadcast"
parseSubCmd = "parse"
showAddressesSubCmd = "show-addresses"
newAddressSubCmd = "new-address"
dumpUnencryptedDataSubCmd = "dump-unencrypted-data"
startDaemonSubCmd = "start-daemon"
)
const (
defaultListen = "localhost:8082"
defaultRPCServer = "localhost"
)
type configFlags struct {
config.NetworkFlags
}
type createConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Password string `long:"password" short:"p" description:"Wallet password"`
Yes bool `long:"yes" short:"y" description:"Assume \"yes\" to all questions"`
MinimumSignatures uint32 `long:"min-signatures" short:"m" description:"Minimum required signatures" default:"1"`
NumPrivateKeys uint32 `long:"num-private-keys" short:"k" description:"Number of private keys" default:"1"`
NumPublicKeys uint32 `long:"num-public-keys" short:"n" description:"Total number of keys" default:"1"`
ECDSA bool `long:"ecdsa" description:"Create an ECDSA wallet"`
Import bool `long:"import" short:"i" description:"Import private keys (as opposed to generating them)"`
config.NetworkFlags
}
type balanceConfig struct {
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
Verbose bool `long:"verbose" short:"v" description:"Verbose: show addresses with balance"`
config.NetworkFlags
}
type sendConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Password string `long:"password" short:"p" description:"Wallet password"`
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 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"`
config.NetworkFlags
}
type sweepConfig struct {
PrivateKey string `long:"private-key" short:"k" description:"Private key in hex format"`
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
config.NetworkFlags
}
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 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
}
type signConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Password string `long:"password" short:"p" description:"Wallet password"`
Transaction string `long:"transaction" short:"t" description:"The unsigned transaction(s) to sign on (encoded in hex)"`
TransactionFile string `long:"transaction-file" short:"F" description:"The file containing the unsigned transaction(s) to sign on (encoded in hex)"`
config.NetworkFlags
}
type broadcastConfig struct {
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
Transactions string `long:"transaction" short:"t" description:"The signed transaction to broadcast (encoded in hex)"`
TransactionsFile string `long:"transaction-file" short:"F" description:"The file containing the unsigned transaction to sign on (encoded in hex)"`
config.NetworkFlags
}
type parseConfig struct {
Transaction string `long:"transaction" short:"t" description:"The transaction to parse (encoded in hex)"`
TransactionFile string `long:"transaction-file" short:"F" description:"The file containing the transaction to parse (encoded in hex)"`
Verbose bool `long:"verbose" short:"v" description:"Verbose: show transaction inputs"`
config.NetworkFlags
}
type showAddressesConfig struct {
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
config.NetworkFlags
}
type newAddressConfig struct {
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
config.NetworkFlags
}
type startDaemonConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Password string `long:"password" short:"p" description:"Wallet password"`
RPCServer string `long:"rpcserver" short:"s" description:"RPC server to connect to"`
Listen string `long:"listen" short:"l" description:"Address to listen on (default: 0.0.0.0:8082)"`
Timeout uint32 `long:"wait-timeout" short:"w" description:"Waiting timeout for RPC calls, seconds (default: 30 s)"`
Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"`
config.NetworkFlags
}
type dumpUnencryptedDataConfig struct {
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
Password string `long:"password" short:"p" description:"Wallet password"`
Yes bool `long:"yes" short:"y" description:"Assume \"yes\" to all questions"`
config.NetworkFlags
}
func parseCommandLine() (subCommand string, config interface{}) {
cfg := &configFlags{}
parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag)
createConf := &createConfig{}
parser.AddCommand(createSubCmd, "Creates a new wallet",
"Creates a private key and 3 public addresses, one for each of MainNet, TestNet and DevNet", createConf)
balanceConf := &balanceConfig{DaemonAddress: defaultListen}
parser.AddCommand(balanceSubCmd, "Shows the balance of a public address",
"Shows the balance for a public address in Kaspa", balanceConf)
sendConf := &sendConfig{DaemonAddress: defaultListen}
parser.AddCommand(sendSubCmd, "Sends a Kaspa transaction to a public address",
"Sends a Kaspa transaction to a public address", sendConf)
sweepConf := &sweepConfig{DaemonAddress: defaultListen}
parser.AddCommand(sweepSubCmd, "Sends all funds associated with the given schnorr private key to a new address of the current wallet",
"Sends all funds associated with the given schnorr private key to a newly created external (i.e. not a change) address of the "+
"keyfile that is under the daemon's contol. Can be used with a private key generated with the genkeypair utilily "+
"to send funds to your main wallet.", sweepConf)
createUnsignedTransactionConf := &createUnsignedTransactionConfig{DaemonAddress: defaultListen}
parser.AddCommand(createUnsignedTransactionSubCmd, "Create an unsigned Kaspa transaction",
"Create an unsigned Kaspa transaction", createUnsignedTransactionConf)
signConf := &signConfig{}
parser.AddCommand(signSubCmd, "Sign the given partially signed transaction",
"Sign the given partially signed transaction", signConf)
broadcastConf := &broadcastConfig{DaemonAddress: defaultListen}
parser.AddCommand(broadcastSubCmd, "Broadcast the given transaction",
"Broadcast the given transaction", broadcastConf)
parseConf := &parseConfig{}
parser.AddCommand(parseSubCmd, "Parse the given transaction and print its contents",
"Parse the given transaction and print its contents", parseConf)
showAddressesConf := &showAddressesConfig{DaemonAddress: defaultListen}
parser.AddCommand(showAddressesSubCmd, "Shows all generated public addresses of the current wallet",
"Shows all generated public addresses of the current wallet", showAddressesConf)
newAddressConf := &newAddressConfig{DaemonAddress: defaultListen}
parser.AddCommand(newAddressSubCmd, "Generates new public address of the current wallet and shows it",
"Generates new public address of the current wallet and shows it", newAddressConf)
dumpUnencryptedDataConf := &dumpUnencryptedDataConfig{}
parser.AddCommand(dumpUnencryptedDataSubCmd, "Prints the unencrypted wallet data",
"Prints the unencrypted wallet data including its private keys. Anyone that sees it can access "+
"the funds. Use only on safe environment.", dumpUnencryptedDataConf)
startDaemonConf := &startDaemonConfig{
RPCServer: defaultRPCServer,
Listen: defaultListen,
}
parser.AddCommand(startDaemonSubCmd, "Start the wallet daemon", "Start the wallet daemon", startDaemonConf)
_, err := parser.Parse()
if err != nil {
var flagsErr *flags.Error
if ok := errors.As(err, &flagsErr); ok && flagsErr.Type == flags.ErrHelp {
os.Exit(0)
} else {
os.Exit(1)
}
return "", nil
}
switch parser.Command.Active.Name {
case createSubCmd:
combineNetworkFlags(&createConf.NetworkFlags, &cfg.NetworkFlags)
err := createConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = createConf
case balanceSubCmd:
combineNetworkFlags(&balanceConf.NetworkFlags, &cfg.NetworkFlags)
err := balanceConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = balanceConf
case sendSubCmd:
combineNetworkFlags(&sendConf.NetworkFlags, &cfg.NetworkFlags)
err := sendConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
err = validateSendConfig(sendConf)
if err != nil {
printErrorAndExit(err)
}
config = sendConf
case sweepSubCmd:
combineNetworkFlags(&sweepConf.NetworkFlags, &cfg.NetworkFlags)
err := sweepConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = sweepConf
case createUnsignedTransactionSubCmd:
combineNetworkFlags(&createUnsignedTransactionConf.NetworkFlags, &cfg.NetworkFlags)
err := createUnsignedTransactionConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
err = validateCreateUnsignedTransactionConf(createUnsignedTransactionConf)
if err != nil {
printErrorAndExit(err)
}
config = createUnsignedTransactionConf
case signSubCmd:
combineNetworkFlags(&signConf.NetworkFlags, &cfg.NetworkFlags)
err := signConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = signConf
case broadcastSubCmd:
combineNetworkFlags(&broadcastConf.NetworkFlags, &cfg.NetworkFlags)
err := broadcastConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = broadcastConf
case parseSubCmd:
combineNetworkFlags(&parseConf.NetworkFlags, &cfg.NetworkFlags)
err := parseConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = parseConf
case showAddressesSubCmd:
combineNetworkFlags(&showAddressesConf.NetworkFlags, &cfg.NetworkFlags)
err := showAddressesConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = showAddressesConf
case newAddressSubCmd:
combineNetworkFlags(&newAddressConf.NetworkFlags, &cfg.NetworkFlags)
err := newAddressConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = newAddressConf
case dumpUnencryptedDataSubCmd:
combineNetworkFlags(&dumpUnencryptedDataConf.NetworkFlags, &cfg.NetworkFlags)
err := dumpUnencryptedDataConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = dumpUnencryptedDataConf
case startDaemonSubCmd:
combineNetworkFlags(&startDaemonConf.NetworkFlags, &cfg.NetworkFlags)
err := startDaemonConf.ResolveNetwork(parser)
if err != nil {
printErrorAndExit(err)
}
config = startDaemonConf
}
return parser.Command.Active.Name, config
}
func validateCreateUnsignedTransactionConf(conf *createUnsignedTransactionConfig) error {
if (!conf.IsSendAll && conf.SendAmount == "") ||
(conf.IsSendAll && conf.SendAmount != "") {
return errors.New("exactly one of '--send-amount' or '--all' must be specified")
}
return nil
}
func validateSendConfig(conf *sendConfig) error {
if (!conf.IsSendAll && conf.SendAmount == "") ||
(conf.IsSendAll && conf.SendAmount != "") {
return errors.New("exactly one of '--send-amount' or '--all' must be specified")
}
return nil
}
func combineNetworkFlags(dst, src *config.NetworkFlags) {
dst.Testnet = dst.Testnet || src.Testnet
dst.Simnet = dst.Simnet || src.Simnet
dst.Devnet = dst.Devnet || src.Devnet
if dst.OverrideDAGParamsFile == "" {
dst.OverrideDAGParamsFile = src.OverrideDAGParamsFile
}
}