Repository: vegaprotocol/vegawallet (go)

Issue:
Functionality to delete wallets
**[Functionality to delete wallets vegaprotocol/token-frontend#302](https://vega-testnet.nolt.io/302)** by Mark Owen <hr/> Please could you implement command line functionality for vegawallet to delete or rename a wallet. This would be useful for many reasons, for example if a user creates a wallet without noting down the mnemonic for some reason, and then wishes to start over with the same/previous wallet name.

Pull request:
Add command to remove a wallet
Close #375 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad7b7ca..ce3fb89 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
 ### 🛠 Improvements
 - [383](https://github.com/vegaprotocol/vegawallet/pull/383) - Add wallet version to `key generate` output
 - [390](https://github.com/vegaprotocol/vegawallet/pull/390) - Add new project board Github action and update linked PR action version
+- [385](https://github.com/vegaprotocol/vegawallet/pull/385) - Add command `delete` to delete a wallet
 
 ### 🔥 Removal
 - [387](https://github.com/vegaprotocol/vegawallet/pull/387) - Remove network initialisation in favor of `network import`
diff --git a/cmd/flags/force.go b/cmd/flags/force.go
new file mode 100644
index 0000000..fe9ef8c
--- /dev/null
+++ b/cmd/flags/force.go
@@ -0,0 +1,32 @@
+package flags
+
+import (
+	"bufio"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+)
+
+var ErrExpectYesOrNo = errors.New("invalid answer, expect \"yes\" or \"no\"")
+
+func DoYouConfirm() (bool, error) {
+	reader := bufio.NewReader(os.Stdin)
+	fmt.Print("Are you sure? (y/n) ") //nolint:forbidigo
+	answer, err := reader.ReadString('\n')
+	if err != nil {
+		return false, fmt.Errorf("couldn't read password input: %w", err)
+	}
+	fmt.Println() //nolint:forbidigo
+
+	answer = strings.ToLower(strings.Trim(answer, " \n\t"))
+
+	switch answer {
+	case "yes", "y":
+		return true, nil
+	case "no", "n":
+		return false, nil
+	default:
+		return false, ErrExpectYesOrNo
+	}
+}
diff --git a/cmd/root.go b/cmd/root.go
index b60812d..14f3042 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -116,6 +116,7 @@ func BuildCmdRoot(w io.Writer, vh CheckVersionHandler) *cobra.Command {
 
 	// Wallet commands
 	// We don't have a wrapper sub-command for wallet commands.
+	cmd.AddCommand(NewCmdDeleteWallet(w, f))
 	cmd.AddCommand(NewCmdGetInfoWallet(w, f))
 	cmd.AddCommand(NewCmdImportWallet(w, f))
 	cmd.AddCommand(NewCmdListWallets(w, f))
diff --git a/cmd/wallet_delete.go b/cmd/wallet_delete.go
new file mode 100644
index 0000000..ad78053
--- /dev/null
+++ b/cmd/wallet_delete.go
@@ -0,0 +1,121 @@
+package cmd
+
+import (
+	"errors"
+	"fmt"
+	"io"
+
+	"code.vegaprotocol.io/vegawallet/cmd/cli"
+	"code.vegaprotocol.io/vegawallet/cmd/flags"
+	"code.vegaprotocol.io/vegawallet/cmd/printer"
+	vgterm "code.vegaprotocol.io/vegawallet/libs/term"
+	"code.vegaprotocol.io/vegawallet/wallets"
+	"github.com/spf13/cobra"
+)
+
+var (
+	ErrForceFlagIsRequiredWithoutTTY = errors.New("--force is required without TTY")
+
+	deleteWalletLong = cli.LongDesc(`
+		Delete a given wallet.
+
+		Be sure to have a backup of its mnemonic, otherwise you won't be able to restore
+		it. If you lost it, you should transfer anything attached to this wallet
+		to another wallet.
+	`)
+
+	deleteWalletExample = cli.Examples(`
+		# Delete the specified wallet
+		vegawallet delete --wallet WALLET
+	`)
+)
+
+type DeleteWalletHandler func(wallet string) error
+
+func NewCmdDeleteWallet(w io.Writer, rf *RootFlags) *cobra.Command {
+	h := func(wallet string) error {
+		s, err := wallets.InitialiseStore(rf.Home)
+		if err != nil {
+			return fmt.Errorf("couldn't initialise wallets store: %w", err)
+		}
+
+		return s.DeleteWallet(wallet)
+	}
+
+	return BuildCmdDeleteWallet(w, h, rf)
+}
+
+func BuildCmdDeleteWallet(w io.Writer, handler DeleteWalletHandler, rf *RootFlags) *cobra.Command {
+	f := &DeleteWalletFlags{}
+
+	cmd := &cobra.Command{
+		Use:     "delete",
+		Short:   "Delete a given wallet",
+		Long:    deleteWalletLong,
+		Example: deleteWalletExample,
+		RunE: func(_ *cobra.Command, _ []string) error {
+			if err := f.Validate(); err != nil {
+				return err
+			}
+
+			if !f.Force && vgterm.HasTTY() {
+				confirm, err := flags.DoYouConfirm()
+				if err != nil {
+					return err
+				}
+				if !confirm {
+					return nil
+				}
+			}
+
+			if err := handler(f.Wallet); err != nil {
+				return err
+			}
+
+			switch rf.Output {
+			case flags.InteractiveOutput:
+				PrintDeleteWalletResponse(w)
+			case flags.JSONOutput:
+				return nil
+			}
+
+			return nil
+		},
+	}
+
+	cmd.Flags().StringVarP(&f.Wallet,
+		"wallet", "w",
+		"",
+		"Wallet to delete",
+	)
+	cmd.Flags().BoolVarP(&f.Force,
+		"force", "f",
+		false,
+		"Do not ask for confirmation",
+	)
+
+	return cmd
+}
+
+type DeleteWalletFlags struct {
+	Wallet string
+	Force  bool
+}
+
+func (f *DeleteWalletFlags) Validate() error {
+	if len(f.Wallet) == 0 {
+		return flags.FlagMustBeSpecifiedError("wallet")
+	}
+
+	if !f.Force && vgterm.HasNoTTY() {
+		return ErrForceFlagIsRequiredWithoutTTY
+	}
+
+	return nil
+}
+
+func PrintDeleteWalletResponse(w io.Writer) {
+	p := printer.NewInteractivePrinter(w)
+
+	p.CheckMark().SuccessText("Deletion succeeded").NextSection()
+}
diff --git a/wallet/store/v1/store.go b/wallet/store/v1/store.go
index 195cda3..20a61a5 100644
--- a/wallet/store/v1/store.go
+++ b/wallet/store/v1/store.go
@@ -27,6 +27,19 @@ func InitialiseStore(walletsHome string) (*Store, error) {
 	}, nil
 }
 
+func (s *Store) DeleteWallet(name string) error {
+	walletPath := s.walletPath(name)
+
+	if exists, _ := vgfs.PathExists(walletPath); !exists {
+		return wallet.ErrWalletDoesNotExists
+	}
+
+	if err := os.Remove(walletPath); err != nil {
+		return fmt.Errorf("couldn't remove wallet file at %s: %w", walletPath, err)
+	}
+	return nil
+}
+
 func (s *Store) WalletExists(name string) bool {
 	walletPath := s.walletPath(name)
 


Test patch:
diff --git a/cmd/wallet_delete_test.go b/cmd/wallet_delete_test.go
new file mode 100644
index 0000000..b86ce57
--- /dev/null
+++ b/cmd/wallet_delete_test.go
@@ -0,0 +1,54 @@
+package cmd_test
+
+import (
+	"testing"
+
+	vgrand "code.vegaprotocol.io/shared/libs/rand"
+	"code.vegaprotocol.io/vegawallet/cmd"
+	"code.vegaprotocol.io/vegawallet/cmd/flags"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDeleteWalletFlags(t *testing.T) {
+	t.Run("Valid flags succeeds", testDeleteWalletFlagsValidFlagsSucceeds)
+	t.Run("Missing wallet fails", testDeleteWalletFlagsMissingWalletFails)
+}
+
+func testDeleteWalletFlagsValidFlagsSucceeds(t *testing.T) {
+	// given
+	walletName := vgrand.RandomStr(10)
+
+	f := &cmd.DeleteWalletFlags{
+		Wallet: walletName,
+		Force:  true,
+	}
+
+	// when
+	err := f.Validate()
+
+	// then
+	require.NoError(t, err)
+}
+
+func testDeleteWalletFlagsMissingWalletFails(t *testing.T) {
+	// given
+	f := newDeleteWalletFlags(t)
+	f.Wallet = ""
+
+	// when
+	err := f.Validate()
+
+	// then
+	assert.ErrorIs(t, err, flags.FlagMustBeSpecifiedError("wallet"))
+}
+
+func newDeleteWalletFlags(t *testing.T) *cmd.DeleteWalletFlags {
+	t.Helper()
+
+	walletName := vgrand.RandomStr(10)
+
+	return &cmd.DeleteWalletFlags{
+		Wallet: walletName,
+	}
+}
diff --git a/tests/commands_for_test.go b/tests/commands_for_test.go
index 2724ee5..8b291f6 100644
--- a/tests/commands_for_test.go
+++ b/tests/commands_for_test.go
@@ -741,3 +741,14 @@ func WalletList(t *testing.T, args []string) (*ListWalletsResponse, error) {
 	}
 	return resp, nil
 }
+
+func WalletDelete(t *testing.T, args []string) error {
+	t.Helper()
+	argsWithCmd := []string{"delete"}
+	argsWithCmd = append(argsWithCmd, args...)
+	_, err := ExecuteCmd(t, argsWithCmd)
+	if err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/tests/sc_delete_wallet_test.go b/tests/sc_delete_wallet_test.go
new file mode 100644
index 0000000..782f022
--- /dev/null
+++ b/tests/sc_delete_wallet_test.go
@@ -0,0 +1,65 @@
+package tests_test
+
+import (
+	"testing"
+
+	vgrand "code.vegaprotocol.io/shared/libs/rand"
+	"code.vegaprotocol.io/vegawallet/wallet"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDeleteWallet(t *testing.T) {
+	// given
+	home, cleanUpFn := NewTempDir(t)
+	defer cleanUpFn(t)
+
+	_, passphraseFilePath := NewPassphraseFile(t, home)
+
+	walletName := vgrand.RandomStr(5)
+
+	// when
+	generateKeyResp, err := KeyGenerate(t, []string{
+		"--home", home,
+		"--output", "json",
+		"--wallet", walletName,
+		"--passphrase-file", passphraseFilePath,
+		"--meta", "name:key-1,role:validation",
+	})
+
+	// then
+	require.NoError(t, err)
+	AssertGenerateKey(t, generateKeyResp).
+		WithWalletCreation().
+		WithName(walletName).
+		WithMeta(map[string]string{"name": "key-1", "role": "validation"}).
+		LocatedUnder(home)
+
+	// when
+	err = WalletDelete(t, []string{
+		"--home", home,
+		"--output", "json",
+		"--wallet", walletName,
+		"--force",
+	})
+
+	// then
+	require.NoError(t, err)
+	assert.NoFileExists(t, generateKeyResp.Wallet.FilePath)
+}
+
+func TestDeleteNonExistingWallet(t *testing.T) {
+	home, cleanUpFn := NewTempDir(t)
+	defer cleanUpFn(t)
+
+	// when
+	err := WalletDelete(t, []string{
+		"--home", home,
+		"--output", "json",
+		"--wallet", vgrand.RandomStr(5),
+		"--force",
+	})
+
+	// then
+	require.ErrorIs(t, err, wallet.ErrWalletDoesNotExists)
+}
diff --git a/tests/sc_list_wallets_test.go b/tests/sc_list_wallets_test.go
new file mode 100644
index 0000000..355ae40
--- /dev/null
+++ b/tests/sc_list_wallets_test.go
@@ -0,0 +1,79 @@
+package tests_test
+
+import (
+	"testing"
+
+	vgrand "code.vegaprotocol.io/shared/libs/rand"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestListWallets(t *testing.T) {
+	// given
+	home, cleanUpFn := NewTempDir(t)
+	defer cleanUpFn(t)
+
+	_, passphraseFilePath := NewPassphraseFile(t, home)
+
+	walletName1 := "a" + vgrand.RandomStr(5)
+
+	// when
+	generateKeyResp1, err := KeyGenerate(t, []string{
+		"--home", home,
+		"--output", "json",
+		"--wallet", walletName1,
+		"--passphrase-file", passphraseFilePath,
+	})
+
+	// then
+	require.NoError(t, err)
+	AssertGenerateKey(t, generateKeyResp1).
+		WithWalletCreation().
+		WithName(walletName1).
+		WithMeta(map[string]string{"name": DefaultMetaName(t, walletName1, 1)}).
+		LocatedUnder(home)
+
+	// when
+	listWalletsResp1, err := WalletList(t, []string{
+		"--home", home,
+		"--output", "json",
+	})
+
+	// then
+	require.NoError(t, err)
+	require.NotNil(t, listWalletsResp1)
+	require.Len(t, listWalletsResp1.Wallets, 1)
+	assert.Equal(t, listWalletsResp1.Wallets[0], generateKeyResp1.Wallet.Name)
+
+	// given
+	walletName2 := "b" + vgrand.RandomStr(5)
+
+	// when
+	generateKeyResp2, err := KeyGenerate(t, []string{
+		"--home", home,
+		"--output", "json",
+		"--wallet", walletName2,
+		"--passphrase-file", passphraseFilePath,
+	})
+
+	// then
+	require.NoError(t, err)
+	AssertGenerateKey(t, generateKeyResp2).
+		WithWalletCreation().
+		WithName(walletName2).
+		WithMeta(map[string]string{"name": DefaultMetaName(t, walletName2, 1)}).
+		LocatedUnder(home)
+
+	// when
+	listWalletsResp2, err := WalletList(t, []string{
+		"--home", home,
+		"--output", "json",
+	})
+
+	// then
+	require.NoError(t, err)
+	require.NotNil(t, listWalletsResp2)
+	require.Len(t, listWalletsResp2.Wallets, 2)
+	assert.Equal(t, listWalletsResp2.Wallets[0], generateKeyResp1.Wallet.Name)
+	assert.Equal(t, listWalletsResp2.Wallets[1], generateKeyResp2.Wallet.Name)
+}