diff --git a/cli/contract_test.go b/cli/contract_test.go index 82cd9d8c3..1e8bc956e 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -27,6 +27,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestCalcHash(t *testing.T) { @@ -464,30 +465,26 @@ func TestContractManifestGroups(t *testing.T) { e.RunWithError(t, "neo-go", "contract", "manifest", "add-group", "--wallet", t.TempDir()) }) - t.Run("invalid account", func(t *testing.T) { - e.RunWithError(t, "neo-go", "contract", "manifest", "add-group", - "--wallet", testWalletPath, "--account", "not-an-acc") - }) t.Run("invalid sender", func(t *testing.T) { e.RunWithError(t, "neo-go", "contract", "manifest", "add-group", - "--wallet", testWalletPath, "--account", testWalletAccount, + "--wallet", testWalletPath, "--address", testWalletAccount, "--sender", "not-a-sender") }) t.Run("invalid NEF file", func(t *testing.T) { e.RunWithError(t, "neo-go", "contract", "manifest", "add-group", - "--wallet", testWalletPath, "--account", testWalletAccount, + "--wallet", testWalletPath, "--address", testWalletAccount, "--sender", testWalletAccount, "--nef", tmpDir) }) t.Run("corrupted NEF file", func(t *testing.T) { f := filepath.Join(tmpDir, "invalid.nef") require.NoError(t, os.WriteFile(f, []byte{1, 2, 3}, os.ModePerm)) e.RunWithError(t, "neo-go", "contract", "manifest", "add-group", - "--wallet", testWalletPath, "--account", testWalletAccount, + "--wallet", testWalletPath, "--address", testWalletAccount, "--sender", testWalletAccount, "--nef", f) }) t.Run("invalid manifest file", func(t *testing.T) { e.RunWithError(t, "neo-go", "contract", "manifest", "add-group", - "--wallet", testWalletPath, "--account", testWalletAccount, + "--wallet", testWalletPath, "--address", testWalletAccount, "--sender", testWalletAccount, "--nef", nefName, "--manifest", tmpDir) }) @@ -495,13 +492,13 @@ func TestContractManifestGroups(t *testing.T) { f := filepath.Join(tmpDir, "invalid.manifest.json") require.NoError(t, os.WriteFile(f, []byte{1, 2, 3}, os.ModePerm)) e.RunWithError(t, "neo-go", "contract", "manifest", "add-group", - "--wallet", testWalletPath, "--account", testWalletAccount, + "--wallet", testWalletPath, "--address", testWalletAccount, "--sender", testWalletAccount, "--nef", nefName, "--manifest", f) }) t.Run("unknown account", func(t *testing.T) { e.RunWithError(t, "neo-go", "contract", "manifest", "add-group", - "--wallet", testWalletPath, "--account", util.Uint160{}.StringLE(), + "--wallet", testWalletPath, "--address", util.Uint160{}.StringLE(), "--sender", testWalletAccount, "--nef", nefName, "--manifest", manifestName) }) @@ -510,11 +507,11 @@ func TestContractManifestGroups(t *testing.T) { e.In.WriteString("testpass\r") e.Run(t, append(cmd, "--wallet", testWalletPath, - "--sender", testWalletAccount, "--account", testWalletAccount)...) + "--sender", testWalletAccount, "--address", testWalletAccount)...) e.In.WriteString("testpass\r") // should override signature with the previous sender e.Run(t, append(cmd, "--wallet", testWalletPath, - "--sender", validatorAddr, "--account", testWalletAccount)...) + "--sender", validatorAddr, "--address", testWalletAccount)...) e.In.WriteString("one\r") e.Run(t, "neo-go", "contract", "deploy", @@ -613,10 +610,18 @@ func TestComlileAndInvokeFunction(t *testing.T) { "--rpc-endpoint", "http://"+e.RPC.Addr, "--in", nefName, "--", address.Uint160ToString(util.Uint160{1, 2, 3})) - e.In.WriteString("one\r") + tmp := t.TempDir() + configPath := filepath.Join(tmp, "config.yaml") + cfg := config.Wallet{ + Path: validatorWallet, + Password: "one", + } + yml, err := yaml.Marshal(cfg) + require.NoError(t, err) + require.NoError(t, os.WriteFile(configPath, yml, 0666)) e.Run(t, "neo-go", "contract", "deploy", "--rpc-endpoint", "http://"+e.RPC.Addr, "--force", - "--wallet", validatorWallet, "--address", validatorAddr, + "--wallet-config", configPath, "--address", validatorAddr, "--in", nefName, "--manifest", manifestName) e.checkTxPersisted(t, "Sent invocation transaction ") @@ -713,6 +718,10 @@ func TestComlileAndInvokeFunction(t *testing.T) { e.In.WriteString("y\r") e.Run(t, append(cmd, "--wallet", validatorWallet, h.StringLE(), "getValue")...) }) + t.Run("good: from wallet config", func(t *testing.T) { + e.In.WriteString("y\r") + e.Run(t, append(cmd, "--wallet-config", configPath, h.StringLE(), "getValue")...) + }) cmd = append(cmd, "--wallet", validatorWallet, "--address", validatorAddr) t.Run("cancelled", func(t *testing.T) { diff --git a/cli/smartcontract/manifest.go b/cli/smartcontract/manifest.go index 20fa640b3..e84f808bf 100644 --- a/cli/smartcontract/manifest.go +++ b/cli/smartcontract/manifest.go @@ -10,27 +10,10 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" - "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" ) func manifestAddGroup(ctx *cli.Context) error { - walletPath := ctx.String("wallet") - if len(walletPath) == 0 { - return cli.NewExitError(errNoWallet, 1) - } - - w, err := wallet.NewWalletFromFile(walletPath) - if err != nil { - return cli.NewExitError(err, 1) - } - defer w.Close() - - addr, err := flags.ParseAddress(ctx.String("account")) - if err != nil { - return cli.NewExitError(fmt.Errorf("account is invalid or missing: %w", err), 1) - } - sender, err := flags.ParseAddress(ctx.String("sender")) if err != nil { return cli.NewExitError(fmt.Errorf("invalid sender: %w", err), 1) @@ -49,9 +32,9 @@ func manifestAddGroup(ctx *cli.Context) error { h := state.CreateContractHash(sender, nf.Checksum, m.Name) - gAcc, err := getUnlockedAccount(w, addr) + gAcc, _, err := getAccFromContext(ctx) if err != nil { - return err + return cli.NewExitError(fmt.Errorf("can't get account to sign group with: %w", err), 1) } var found bool diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 097529b1c..64202b439 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -14,6 +14,7 @@ import ( "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/paramcontext" + cliwallet "github.com/nspcc-dev/neo-go/cli/wallet" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/state" @@ -32,22 +33,31 @@ import ( "gopkg.in/yaml.v3" ) +// addressFlagName is a flag name used for address-related operations. It should be +// the same within the smartcontract package, thus, use this constant. +const addressFlagName = "address, a" + var ( - errNoInput = errors.New("no input file was found, specify an input file with the '--in or -i' flag") - errNoConfFile = errors.New("no config file was found, specify a config file with the '--config' or '-c' flag") - errNoManifestFile = errors.New("no manifest file was found, specify manifest file with '--manifest' or '-m' flag") - errNoMethod = errors.New("no method specified for function invocation command") - errNoWallet = errors.New("no wallet parameter found, specify it with the '--wallet or -w' flag") - errNoScriptHash = errors.New("no smart contract hash was provided, specify one as the first argument") - errNoSmartContractName = errors.New("no name was provided, specify the '--name or -n' flag") - errFileExist = errors.New("A file with given smart-contract name already exists") + errNoInput = errors.New("no input file was found, specify an input file with the '--in or -i' flag") + errNoConfFile = errors.New("no config file was found, specify a config file with the '--config' or '-c' flag") + errNoManifestFile = errors.New("no manifest file was found, specify manifest file with '--manifest' or '-m' flag") + errNoMethod = errors.New("no method specified for function invocation command") + errNoWallet = errors.New("no wallet parameter found, specify it with the '--wallet' or '-w' flag or specify wallet config file with the '--wallet-config' flag") + errConflictingWalletFlags = errors.New("--wallet flag conflicts with --wallet-config flag, please, provide one of them to specify wallet location") + errNoScriptHash = errors.New("no smart contract hash was provided, specify one as the first argument") + errNoSmartContractName = errors.New("no name was provided, specify the '--name or -n' flag") + errFileExist = errors.New("A file with given smart-contract name already exists") walletFlag = cli.StringFlag{ Name: "wallet, w", - Usage: "wallet to use to get the key for transaction signing", + Usage: "wallet to use to get the key for transaction signing; conflicts with --wallet-config flag", + } + walletConfigFlag = cli.StringFlag{ + Name: "wallet-config", + Usage: "path to wallet config to use to get the key for transaction signing; conflicts with --wallet flag", } addressFlag = flags.AddressFlag{ - Name: "address, a", + Name: addressFlagName, Usage: "address to use as transaction signee (and gas source)", } gasFlag = flags.Fixed8Flag{ @@ -103,6 +113,7 @@ func NewCommands() []cli.Command { testInvokeScriptFlags = append(testInvokeScriptFlags, options.RPC...) invokeFunctionFlags := []cli.Flag{ walletFlag, + walletConfigFlag, addressFlag, gasFlag, sysGasFlag, @@ -387,12 +398,13 @@ func NewCommands() []cli.Command { Action: manifestAddGroup, Flags: []cli.Flag{ walletFlag, + walletConfigFlag, cli.StringFlag{ Name: "sender, s", Usage: "deploy transaction sender", }, - cli.StringFlag{ - Name: "account, a", + flags.AddressFlag{ + Name: addressFlagName, // use the same name for handler code unification. Usage: "account to sign group with", }, cli.StringFlag{ @@ -818,13 +830,26 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, *wallet.Wallet, error var addr util.Uint160 wPath := ctx.String("wallet") - if len(wPath) == 0 { - return nil, nil, cli.NewExitError(errNoWallet, 1) + walletConfigPath := ctx.String("wallet-config") + if len(wPath) != 0 && len(walletConfigPath) != 0 { + return nil, nil, errConflictingWalletFlags + } + if len(wPath) == 0 && len(walletConfigPath) == 0 { + return nil, nil, errNoWallet + } + var pass *string + if len(walletConfigPath) != 0 { + cfg, err := cliwallet.ReadWalletConfig(walletConfigPath) + if err != nil { + return nil, nil, err + } + wPath = cfg.Path + pass = &cfg.Password } wall, err := wallet.NewWalletFromFile(wPath) if err != nil { - return nil, nil, cli.NewExitError(err, 1) + return nil, nil, err } addrFlag := ctx.Generic("address").(*flags.Address) if addrFlag.IsSet { @@ -833,29 +858,32 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, *wallet.Wallet, error addr = wall.GetChangeAddress() } - acc, err := getUnlockedAccount(wall, addr) + acc, err := getUnlockedAccount(wall, addr, pass) return acc, wall, err } -func getUnlockedAccount(wall *wallet.Wallet, addr util.Uint160) (*wallet.Account, error) { +func getUnlockedAccount(wall *wallet.Wallet, addr util.Uint160, pass *string) (*wallet.Account, error) { acc := wall.GetAccount(addr) if acc == nil { - return nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1) + return nil, fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)) } if acc.PrivateKey() != nil { return acc, nil } - rawPass, err := input.ReadPassword( - fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr))) - if err != nil { - return nil, cli.NewExitError(fmt.Errorf("Error reading password: %w", err), 1) + if pass == nil { + rawPass, err := input.ReadPassword( + fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr))) + if err != nil { + return nil, fmt.Errorf("Error reading password: %w", err) + } + trimmed := strings.TrimRight(string(rawPass), "\n") + pass = &trimmed } - pass := strings.TrimRight(string(rawPass), "\n") - err = acc.Decrypt(pass, wall.Scrypt) + err := acc.Decrypt(*pass, wall.Scrypt) if err != nil { - return nil, cli.NewExitError(err, 1) + return nil, err } return acc, nil } diff --git a/cli/wallet/legacy.go b/cli/wallet/legacy.go index a50a958aa..4f12ce256 100644 --- a/cli/wallet/legacy.go +++ b/cli/wallet/legacy.go @@ -37,15 +37,30 @@ type ( // newWalletV2FromFile reads a NEO2 wallet from the file. // This should be used read-only, no operations are supported on the returned wallet. -func newWalletV2FromFile(path string) (*walletV2, error) { +func newWalletV2FromFile(path string, configPath string) (*walletV2, *string, error) { + if len(path) != 0 && len(configPath) != 0 { + return nil, nil, errConflictingWalletFlags + } + if len(path) == 0 && len(configPath) == 0 { + return nil, nil, errNoPath + } + var pass *string + if len(configPath) != 0 { + cfg, err := ReadWalletConfig(configPath) + if err != nil { + return nil, nil, err + } + path = cfg.Path + pass = &cfg.Password + } file, err := os.OpenFile(path, os.O_RDWR, os.ModeAppend) if err != nil { - return nil, err + return nil, nil, err } defer file.Close() wall := new(walletV2) - return wall, json.NewDecoder(file).Decode(wall) + return wall, pass, json.NewDecoder(file).Decode(wall) } const simpleSigLen = 35 diff --git a/cli/wallet/multisig.go b/cli/wallet/multisig.go index 6612e5c56..2c7e021b9 100644 --- a/cli/wallet/multisig.go +++ b/cli/wallet/multisig.go @@ -12,7 +12,7 @@ import ( ) func signStoredTransaction(ctx *cli.Context) error { - wall, err := readWallet(ctx.String("wallet")) + wall, pass, err := readWallet(ctx) if err != nil { return cli.NewExitError(err, 1) } @@ -26,7 +26,7 @@ func signStoredTransaction(ctx *cli.Context) error { if !addrFlag.IsSet { return cli.NewExitError("address was not provided", 1) } - acc, err := getDecryptedAccount(ctx, wall, addrFlag.Uint160()) + acc, err := getDecryptedAccount(wall, addrFlag.Uint160(), pass) if err != nil { return cli.NewExitError(err, 1) } diff --git a/cli/wallet/nep11.go b/cli/wallet/nep11.go index 335d35bf9..6ccebf811 100644 --- a/cli/wallet/nep11.go +++ b/cli/wallet/nep11.go @@ -65,6 +65,7 @@ func newNEP11Commands() []cli.Command { Action: printNEP11Info, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, tokenFlag, }, }, @@ -75,6 +76,7 @@ func newNEP11Commands() []cli.Command { Action: removeNEP11Token, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, tokenFlag, forceFlag, }, @@ -161,7 +163,7 @@ func removeNEP11Token(ctx *cli.Context) error { func getNEP11Balance(ctx *cli.Context) error { var accounts []*wallet.Account - wall, err := readWallet(ctx.String("wallet")) + wall, _, err := readWallet(ctx) if err != nil { return cli.NewExitError(fmt.Errorf("bad wallet: %w", err), 1) } diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index a11766d83..a7135fffe 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -38,6 +38,7 @@ var ( } baseBalanceFlags = []cli.Flag{ walletPathFlag, + walletConfigFlag, tokenFlag, flags.AddressFlag{ Name: "address, a", @@ -46,6 +47,7 @@ var ( } importFlags = append([]cli.Flag{ walletPathFlag, + walletConfigFlag, flags.AddressFlag{ Name: "token", Usage: "Token contract address or hash in LE", @@ -53,6 +55,7 @@ var ( }, options.RPC...) baseTransferFlags = []cli.Flag{ walletPathFlag, + walletConfigFlag, outFlag, fromAddrFlag, toAddrFlag, @@ -67,6 +70,7 @@ var ( } multiTransferFlags = append([]cli.Flag{ walletPathFlag, + walletConfigFlag, outFlag, fromAddrFlag, gasFlag, @@ -104,6 +108,7 @@ func newNEP17Commands() []cli.Command { Action: printNEP17Info, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, tokenFlag, }, }, @@ -114,6 +119,7 @@ func newNEP17Commands() []cli.Command { Action: removeNEP17Token, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, tokenFlag, forceFlag, }, @@ -145,7 +151,7 @@ func newNEP17Commands() []cli.Command { func getNEP17Balance(ctx *cli.Context) error { var accounts []*wallet.Account - wall, err := readWallet(ctx.String("wallet")) + wall, _, err := readWallet(ctx) if err != nil { return cli.NewExitError(fmt.Errorf("bad wallet: %w", err), 1) } @@ -344,7 +350,7 @@ func importNEP17Token(ctx *cli.Context) error { } func importNEPToken(ctx *cli.Context, standard string) error { - wall, err := openWallet(ctx.String("wallet")) + wall, _, err := openWallet(ctx.String("wallet"), ctx.String("wallet-config")) if err != nil { return cli.NewExitError(err, 1) } @@ -407,7 +413,7 @@ func printNEP17Info(ctx *cli.Context) error { } func printNEPInfo(ctx *cli.Context, standard string) error { - wall, err := readWallet(ctx.String("wallet")) + wall, _, err := readWallet(ctx) if err != nil { return cli.NewExitError(err, 1) } @@ -440,7 +446,7 @@ func removeNEP17Token(ctx *cli.Context) error { } func removeNEPToken(ctx *cli.Context, standard string) error { - wall, err := openWallet(ctx.String("wallet")) + wall, _, err := openWallet(ctx.String("wallet"), ctx.String("wallet-config")) if err != nil { return cli.NewExitError(err, 1) } @@ -464,7 +470,7 @@ func removeNEPToken(ctx *cli.Context, standard string) error { } func multiTransferNEP17(ctx *cli.Context) error { - wall, err := readWallet(ctx.String("wallet")) + wall, pass, err := readWallet(ctx) if err != nil { return cli.NewExitError(err, 1) } @@ -475,7 +481,7 @@ func multiTransferNEP17(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - acc, err := getDecryptedAccount(ctx, wall, from) + acc, err := getDecryptedAccount(wall, from, pass) if err != nil { return cli.NewExitError(err, 1) } @@ -550,7 +556,7 @@ func transferNEP17(ctx *cli.Context) error { } func transferNEP(ctx *cli.Context, standard string) error { - wall, err := readWallet(ctx.String("wallet")) + wall, pass, err := readWallet(ctx) if err != nil { return cli.NewExitError(err, 1) } @@ -561,7 +567,7 @@ func transferNEP(ctx *cli.Context, standard string) error { if err != nil { return cli.NewExitError(err, 1) } - acc, err := getDecryptedAccount(ctx, wall, from) + acc, err := getDecryptedAccount(wall, from, pass) if err != nil { return cli.NewExitError(err, 1) } diff --git a/cli/wallet/validator.go b/cli/wallet/validator.go index 3c0ed4d0e..aed1dbda8 100644 --- a/cli/wallet/validator.go +++ b/cli/wallet/validator.go @@ -29,6 +29,7 @@ func newValidatorCommands() []cli.Command { Action: handleRegister, Flags: append([]cli.Flag{ walletPathFlag, + walletConfigFlag, gasFlag, flags.AddressFlag{ Name: "address, a", @@ -43,6 +44,7 @@ func newValidatorCommands() []cli.Command { Action: handleUnregister, Flags: append([]cli.Flag{ walletPathFlag, + walletConfigFlag, gasFlag, flags.AddressFlag{ Name: "address, a", @@ -60,6 +62,7 @@ func newValidatorCommands() []cli.Command { Action: handleVote, Flags: append([]cli.Flag{ walletPathFlag, + walletConfigFlag, gasFlag, flags.AddressFlag{ Name: "address, a", @@ -83,7 +86,7 @@ func handleUnregister(ctx *cli.Context) error { } func handleCandidate(ctx *cli.Context, method string, sysGas int64) error { - wall, err := readWallet(ctx.String("wallet")) + wall, pass, err := readWallet(ctx) if err != nil { return cli.NewExitError(err, 1) } @@ -94,7 +97,7 @@ func handleCandidate(ctx *cli.Context, method string, sysGas int64) error { return cli.NewExitError("address was not provided", 1) } addr := addrFlag.Uint160() - acc, err := getDecryptedAccount(ctx, wall, addr) + acc, err := getDecryptedAccount(wall, addr, pass) if err != nil { return cli.NewExitError(err, 1) } @@ -138,7 +141,7 @@ func handleCandidate(ctx *cli.Context, method string, sysGas int64) error { } func handleVote(ctx *cli.Context) error { - wall, err := readWallet(ctx.String("wallet")) + wall, pass, err := readWallet(ctx) if err != nil { return cli.NewExitError(err, 1) } @@ -149,7 +152,7 @@ func handleVote(ctx *cli.Context) error { return cli.NewExitError("address was not provided", 1) } addr := addrFlag.Uint160() - acc, err := getDecryptedAccount(ctx, wall, addr) + acc, err := getDecryptedAccount(wall, addr, pass) if err != nil { return cli.NewExitError(err, 1) } @@ -198,16 +201,23 @@ func handleVote(ctx *cli.Context) error { return nil } -func getDecryptedAccount(ctx *cli.Context, wall *wallet.Wallet, addr util.Uint160) (*wallet.Account, error) { +// getDecryptedAccount tries to unlock the specified account. If password is nil, it will be requested via terminal. +func getDecryptedAccount(wall *wallet.Wallet, addr util.Uint160, password *string) (*wallet.Account, error) { acc := wall.GetAccount(addr) if acc == nil { return nil, fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addr)) } - if pass, err := input.ReadPassword(EnterPasswordPrompt); err != nil { - fmt.Println("Error reading password", err) - return nil, err - } else if err := acc.Decrypt(pass, wall.Scrypt); err != nil { + if password == nil { + pass, err := input.ReadPassword(EnterPasswordPrompt) + if err != nil { + fmt.Println("Error reading password", err) + return nil, err + } + password = &pass + } + err := acc.Decrypt(*password, wall.Scrypt) + if err != nil { return nil, err } return acc, nil diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index bbf044e6a..6b52b0e1d 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -12,6 +12,7 @@ import ( "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/options" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" @@ -20,6 +21,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" + "gopkg.in/yaml.v3" ) const ( @@ -35,15 +37,20 @@ const ( ) var ( - errNoPath = errors.New("wallet path is mandatory and should be passed using (--wallet, -w) flags") - errPhraseMismatch = errors.New("the entered pass-phrases do not match. Maybe you have misspelled them") - errNoStdin = errors.New("can't read wallet from stdin for this command") + errNoPath = errors.New("wallet path is mandatory and should be passed using (--wallet, -w) flags or via wallet config using --wallet-config flag") + errConflictingWalletFlags = errors.New("--wallet flag conflicts with --wallet-config flag, please, provide one of them to specify wallet location") + errPhraseMismatch = errors.New("the entered pass-phrases do not match. Maybe you have misspelled them") + errNoStdin = errors.New("can't read wallet from stdin for this command") ) var ( walletPathFlag = cli.StringFlag{ Name: "wallet, w", - Usage: "Target location of the wallet file ('-' to read from stdin).", + Usage: "Target location of the wallet file ('-' to read from stdin); conflicts with --wallet-config flag.", + } + walletConfigFlag = cli.StringFlag{ + Name: "wallet-config", + Usage: "Target location of the wallet config file; conflicts with --wallet flag.", } wifFlag = cli.StringFlag{ Name: "wif", @@ -79,6 +86,7 @@ var ( func NewCommands() []cli.Command { claimFlags := []cli.Flag{ walletPathFlag, + walletConfigFlag, flags.AddressFlag{ Name: "address, a", Usage: "Address to claim GAS for", @@ -87,6 +95,7 @@ func NewCommands() []cli.Command { claimFlags = append(claimFlags, options.RPC...) signFlags := []cli.Flag{ walletPathFlag, + walletConfigFlag, outFlag, inFlag, flags.AddressFlag{ @@ -111,6 +120,7 @@ func NewCommands() []cli.Command { Action: createWallet, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, cli.BoolFlag{ Name: "account, a", Usage: "Create a new account", @@ -135,6 +145,7 @@ func NewCommands() []cli.Command { Action: convertWallet, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, cli.StringFlag{ Name: "out, o", Usage: "where to write converted wallet", @@ -147,6 +158,7 @@ func NewCommands() []cli.Command { Action: addAccount, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, }, }, { @@ -155,6 +167,7 @@ func NewCommands() []cli.Command { Action: dumpWallet, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, decryptFlag, }, }, @@ -164,6 +177,7 @@ func NewCommands() []cli.Command { Action: dumpKeys, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, flags.AddressFlag{ Name: "address, a", Usage: "address to print public keys for", @@ -177,6 +191,7 @@ func NewCommands() []cli.Command { Action: exportKeys, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, decryptFlag, }, }, @@ -187,6 +202,7 @@ func NewCommands() []cli.Command { Action: importWallet, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, wifFlag, cli.StringFlag{ Name: "name, n", @@ -206,6 +222,7 @@ func NewCommands() []cli.Command { Action: importMultisig, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, wifFlag, cli.StringFlag{ Name: "name, n", @@ -224,6 +241,7 @@ func NewCommands() []cli.Command { Action: importDeployed, Flags: append([]cli.Flag{ walletPathFlag, + walletConfigFlag, wifFlag, cli.StringFlag{ Name: "name, n", @@ -242,6 +260,7 @@ func NewCommands() []cli.Command { Action: removeAccount, Flags: []cli.Flag{ walletPathFlag, + walletConfigFlag, forceFlag, flags.AddressFlag{ Name: "address, a", @@ -276,7 +295,7 @@ func NewCommands() []cli.Command { } func claimGas(ctx *cli.Context) error { - wall, err := readWallet(ctx.String("wallet")) + wall, pass, err := readWallet(ctx) if err != nil { return cli.NewExitError(err, 1) } @@ -287,7 +306,7 @@ func claimGas(ctx *cli.Context) error { return cli.NewExitError("address was not provided", 1) } scriptHash := addrFlag.Uint160() - acc, err := getDecryptedAccount(ctx, wall, scriptHash) + acc, err := getDecryptedAccount(wall, scriptHash, pass) if err != nil { return cli.NewExitError(err, 1) } @@ -314,7 +333,7 @@ func claimGas(ctx *cli.Context) error { } func changePassword(ctx *cli.Context) error { - wall, err := openWallet(ctx.String("wallet")) + wall, _, err := openWallet(ctx.String("wallet"), "") if err != nil { return cli.NewExitError(err, 1) } @@ -366,7 +385,7 @@ func changePassword(ctx *cli.Context) error { } func convertWallet(ctx *cli.Context) error { - wall, err := newWalletV2FromFile(ctx.String("wallet")) + wall, pass, err := newWalletV2FromFile(ctx.String("wallet"), ctx.String("wallet-config")) if err != nil { return cli.NewExitError(err, 1) } @@ -383,11 +402,15 @@ func convertWallet(ctx *cli.Context) error { newWallet.Scrypt = wall.Scrypt for _, acc := range wall.Accounts { - pass, err := input.ReadPassword(fmt.Sprintf("Enter password for account %s (label '%s') > ", acc.Address, acc.Label)) - if err != nil { - return cli.NewExitError(fmt.Errorf("Error reading password: %w", err), 1) + if len(wall.Accounts) != 1 || pass == nil { + password, err := input.ReadPassword(fmt.Sprintf("Enter password for account %s (label '%s') > ", acc.Address, acc.Label)) + if err != nil { + return cli.NewExitError(fmt.Errorf("Error reading password: %w", err), 1) + } + pass = &password } - newAcc, err := acc.convert(pass, wall.Scrypt) + + newAcc, err := acc.convert(*pass, wall.Scrypt) if err != nil { return cli.NewExitError(err, 1) } @@ -400,14 +423,14 @@ func convertWallet(ctx *cli.Context) error { } func addAccount(ctx *cli.Context) error { - wall, err := openWallet(ctx.String("wallet")) + wall, pass, err := openWallet(ctx.String("wallet"), ctx.String("wallet-config")) if err != nil { return cli.NewExitError(err, 1) } defer wall.Close() - if err := createAccount(wall); err != nil { + if err := createAccount(wall, pass); err != nil { return cli.NewExitError(err, 1) } @@ -415,7 +438,7 @@ func addAccount(ctx *cli.Context) error { } func exportKeys(ctx *cli.Context) error { - wall, err := readWallet(ctx.String("wallet")) + wall, pass, err := readWallet(ctx) if err != nil { return cli.NewExitError(err, 1) } @@ -453,12 +476,15 @@ loop: for _, wif := range wifs { if decrypt { - pass, err := input.ReadPassword(EnterPasswordPrompt) - if err != nil { - return cli.NewExitError(fmt.Errorf("Error reading password: %w", err), 1) + if pass == nil { + password, err := input.ReadPassword(EnterPasswordPrompt) + if err != nil { + return cli.NewExitError(fmt.Errorf("Error reading password: %w", err), 1) + } + pass = &password } - pk, err := keys.NEP2Decrypt(wif, pass, wall.Scrypt) + pk, err := keys.NEP2Decrypt(wif, *pass, wall.Scrypt) if err != nil { return cli.NewExitError(err, 1) } @@ -473,7 +499,7 @@ loop: } func importMultisig(ctx *cli.Context) error { - wall, err := openWallet(ctx.String("wallet")) + wall, _, err := openWallet(ctx.String("wallet"), ctx.String("wallet-config")) if err != nil { return cli.NewExitError(err, 1) } @@ -515,7 +541,7 @@ func importMultisig(ctx *cli.Context) error { } func importDeployed(ctx *cli.Context) error { - wall, err := openWallet(ctx.String("wallet")) + wall, _, err := openWallet(ctx.String("wallet"), ctx.String("wallet-config")) if err != nil { return cli.NewExitError(err, 1) } @@ -570,7 +596,7 @@ func importDeployed(ctx *cli.Context) error { } func importWallet(ctx *cli.Context) error { - wall, err := openWallet(ctx.String("wallet")) + wall, _, err := openWallet(ctx.String("wallet"), ctx.String("wallet-config")) if err != nil { return cli.NewExitError(err, 1) } @@ -600,7 +626,7 @@ func importWallet(ctx *cli.Context) error { } func removeAccount(ctx *cli.Context) error { - wall, err := openWallet(ctx.String("wallet")) + wall, _, err := openWallet(ctx.String("wallet"), ctx.String("wallet-config")) if err != nil { return cli.NewExitError(err, 1) } @@ -644,18 +670,21 @@ func askForConsent(w io.Writer) bool { } func dumpWallet(ctx *cli.Context) error { - wall, err := readWallet(ctx.String("wallet")) + wall, pass, err := readWallet(ctx) if err != nil { return cli.NewExitError(err, 1) } if ctx.Bool("decrypt") { - pass, err := input.ReadPassword(EnterPasswordPrompt) - if err != nil { - return cli.NewExitError(fmt.Errorf("Error reading password: %w", err), 1) + if pass == nil { + password, err := input.ReadPassword(EnterPasswordPrompt) + if err != nil { + return cli.NewExitError(fmt.Errorf("Error reading password: %w", err), 1) + } + pass = &password } for i := range wall.Accounts { // Just testing the decryption here. - err := wall.Accounts[i].Decrypt(pass, wall.Scrypt) + err := wall.Accounts[i].Decrypt(*pass, wall.Scrypt) if err != nil { return cli.NewExitError(err, 1) } @@ -666,7 +695,7 @@ func dumpWallet(ctx *cli.Context) error { } func dumpKeys(ctx *cli.Context) error { - wall, err := readWallet(ctx.String("wallet")) + wall, _, err := readWallet(ctx) if err != nil { return cli.NewExitError(err, 1) } @@ -714,9 +743,23 @@ func dumpKeys(ctx *cli.Context) error { func createWallet(ctx *cli.Context) error { path := ctx.String("wallet") - if len(path) == 0 { + configPath := ctx.String("wallet-config") + + if len(path) != 0 && len(configPath) != 0 { + return errConflictingWalletFlags + } + if len(path) == 0 && len(configPath) == 0 { return cli.NewExitError(errNoPath, 1) } + var pass *string + if len(configPath) != 0 { + cfg, err := ReadWalletConfig(configPath) + if err != nil { + return cli.NewExitError(err, 1) + } + path = cfg.Path + pass = &cfg.Password + } wall, err := wallet.NewWallet(path) if err != nil { return cli.NewExitError(err, 1) @@ -726,7 +769,7 @@ func createWallet(ctx *cli.Context) error { } if ctx.Bool("account") { - if err := createAccount(wall); err != nil { + if err := createAccount(wall, pass); err != nil { return cli.NewExitError(err, 1) } } @@ -764,36 +807,98 @@ func readNewPassword() (string, error) { return phrase, nil } -func createAccount(wall *wallet.Wallet) error { - name, phrase, err := readAccountInfo() - if err != nil { - return err +func createAccount(wall *wallet.Wallet, pass *string) error { + var ( + name, phrase string + err error + ) + if pass == nil { + name, phrase, err = readAccountInfo() + if err != nil { + return err + } + } else { + phrase = *pass } return wall.CreateAccount(name, phrase) } -func openWallet(path string) (*wallet.Wallet, error) { - if len(path) == 0 { - return nil, errNoPath +func openWallet(path string, configPath string) (*wallet.Wallet, *string, error) { + if len(path) != 0 && len(configPath) != 0 { + return nil, nil, errConflictingWalletFlags + } + if len(path) == 0 && len(configPath) == 0 { + return nil, nil, errNoPath } if path == "-" { - return nil, errNoStdin + return nil, nil, errNoStdin } - return wallet.NewWalletFromFile(path) + var pass *string + if len(configPath) != 0 { + cfg, err := ReadWalletConfig(configPath) + if err != nil { + return nil, nil, err + } + path = cfg.Path + pass = &cfg.Password + } + w, err := wallet.NewWalletFromFile(path) + if err != nil { + return nil, nil, err + } + return w, pass, nil } -func readWallet(path string) (*wallet.Wallet, error) { - if len(path) == 0 { - return nil, errNoPath +func readWallet(ctx *cli.Context) (*wallet.Wallet, *string, error) { + path, configPath := ctx.String("wallet"), ctx.String("wallet-config") + if len(path) != 0 && len(configPath) != 0 { + return nil, nil, errConflictingWalletFlags + } + if len(path) == 0 && len(configPath) == 0 { + return nil, nil, errNoPath + } + var pass *string + if len(configPath) != 0 { + cfg, err := ReadWalletConfig(configPath) + if err != nil { + return nil, nil, err + } + path = cfg.Path + pass = &cfg.Password } if path == "-" { w := &wallet.Wallet{} if err := json.NewDecoder(os.Stdin).Decode(w); err != nil { - return nil, fmt.Errorf("js %s", err) + return nil, nil, fmt.Errorf("js %s", err) } - return w, nil + return w, nil, nil } - return wallet.NewWalletFromFile(path) + w, err := wallet.NewWalletFromFile(path) + if err != nil { + return nil, nil, err + } + return w, pass, nil +} + +func ReadWalletConfig(configPath string) (*config.Wallet, error) { + file, err := os.Open(configPath) + if err != nil { + return nil, err + } + defer file.Close() + + configData, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("unable to read wallet config: %w", err) + } + + cfg := &config.Wallet{} + + err = yaml.Unmarshal(configData, &cfg) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal wallet config YAML: %w", err) + } + return cfg, nil } func newAccountFromWIF(w io.Writer, wif string, scrypt keys.ScryptParams) (*wallet.Account, error) { diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 762221c34..5e3d6083b 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -4,11 +4,13 @@ import ( "encoding/hex" "encoding/json" "math/big" + "os" "path/filepath" "strings" "testing" "github.com/chzyer/readline" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" @@ -16,6 +18,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestWalletAccountRemove(t *testing.T) { @@ -177,6 +180,23 @@ func TestWalletInit(t *testing.T) { require.Equal(t, 1, len(w.Accounts)) require.Equal(t, "acc", w.Accounts[0].Label) }) + t.Run("with wallet config", func(t *testing.T) { + tmp := t.TempDir() + walletPath := filepath.Join(tmp, "wallet.json") + configPath := filepath.Join(tmp, "config.yaml") + cfg := config.Wallet{ + Path: walletPath, + Password: "pass", + } + res, err := yaml.Marshal(cfg) + require.NoError(t, err) + require.NoError(t, os.WriteFile(configPath, res, 0666)) + e.Run(t, "neo-go", "wallet", "init", "--wallet-config", configPath, "--account") + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + require.Equal(t, 1, len(w.Accounts)) + require.Equal(t, "", w.Accounts[0].Label) + }) tmpDir := t.TempDir() walletPath := filepath.Join(tmpDir, "wallet.json") @@ -279,20 +299,43 @@ func TestWalletInit(t *testing.T) { e.RunWithError(t, "neo-go", "wallet", "import", "--wallet", walletPath, "--wif", priv.WIF(), "--contract", "not-a-hex") }) + check := func(t *testing.T, expectedLabel string, pass string) { + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + t.Cleanup(w.Close) + acc := w.GetAccount(priv.GetScriptHash()) + require.NotNil(t, acc) + require.Equal(t, expectedLabel, acc.Label) + require.NoError(t, acc.Decrypt(pass, w.Scrypt)) + } + t.Run("good", func(t *testing.T) { + e.In.WriteString("test_account_3\r") + e.In.WriteString("qwerty\r") + e.In.WriteString("qwerty\r") + e.Run(t, "neo-go", "wallet", "import", + "--wallet", walletPath, "--wif", priv.WIF(), "--contract", "0a0b0c") + check(t, "test_account_3", "qwerty") + }) - e.In.WriteString("test_account_3\r") - e.In.WriteString("qwerty\r") - e.In.WriteString("qwerty\r") - e.Run(t, "neo-go", "wallet", "import", - "--wallet", walletPath, "--wif", priv.WIF(), "--contract", "0a0b0c") - - w, err := wallet.NewWalletFromFile(walletPath) - require.NoError(t, err) - t.Cleanup(w.Close) - acc := w.GetAccount(priv.GetScriptHash()) - require.NotNil(t, acc) - require.Equal(t, "test_account_3", acc.Label) - require.NoError(t, acc.Decrypt("qwerty", w.Scrypt)) + t.Run("from wallet config", func(t *testing.T) { + tmp := t.TempDir() + configPath := filepath.Join(tmp, "config.yaml") + cfg := config.Wallet{ + Path: walletPath, + Password: "pass", // This pass won't be taken into account. + } + res, err := yaml.Marshal(cfg) + require.NoError(t, err) + require.NoError(t, os.WriteFile(configPath, res, 0666)) + priv, err = keys.NewPrivateKey() + require.NoError(t, err) + e.In.WriteString("test_account_4\r") + e.In.WriteString("qwerty\r") + e.In.WriteString("qwerty\r") + e.Run(t, "neo-go", "wallet", "import", + "--wallet-config", configPath, "--wif", priv.WIF(), "--contract", "0a0b0c0d") + check(t, "test_account_4", "qwerty") + }) }) }) t.Run("EncryptedWIF", func(t *testing.T) { @@ -620,14 +663,32 @@ func TestWalletDump(t *testing.T) { e.In.WriteString("invalidpass\r") e.RunWithError(t, cmd...) }) - - e.In.WriteString("testpass\r") - e.Run(t, cmd...) - rawStr := strings.TrimSpace(e.Out.String()) - w := new(wallet.Wallet) - require.NoError(t, json.Unmarshal([]byte(rawStr), w)) - require.Equal(t, 1, len(w.Accounts)) - require.Equal(t, testWalletAccount, w.Accounts[0].Address) + t.Run("good", func(t *testing.T) { + e.In.WriteString("testpass\r") + e.Run(t, cmd...) + rawStr := strings.TrimSpace(e.Out.String()) + w := new(wallet.Wallet) + require.NoError(t, json.Unmarshal([]byte(rawStr), w)) + require.Equal(t, 1, len(w.Accounts)) + require.Equal(t, testWalletAccount, w.Accounts[0].Address) + }) + t.Run("good, from wallet config", func(t *testing.T) { + tmp := t.TempDir() + configPath := filepath.Join(tmp, "config.yaml") + cfg := config.Wallet{ + Path: testWalletPath, + Password: "testpass", + } + res, err := yaml.Marshal(cfg) + require.NoError(t, err) + require.NoError(t, os.WriteFile(configPath, res, 0666)) + e.Run(t, "neo-go", "wallet", "dump", "--wallet-config", configPath) + rawStr := strings.TrimSpace(e.Out.String()) + w := new(wallet.Wallet) + require.NoError(t, json.Unmarshal([]byte(rawStr), w)) + require.Equal(t, 1, len(w.Accounts)) + require.Equal(t, testWalletAccount, w.Accounts[0].Address) + }) }) }