1518019be8
Some commands don't accept arguments, but users try giving them and don't notice a mistake. It's a bit more user-friendly to tell the user that there is something wrong with the way he tries to use the command.
974 lines
24 KiB
Go
974 lines
24 KiB
Go
package wallet
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/nspcc-dev/neo-go/cli/cmdargs"
|
|
"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"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
|
"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 (
|
|
// EnterPasswordPrompt is a prompt used to ask the user for a password.
|
|
EnterPasswordPrompt = "Enter password > "
|
|
// EnterNewPasswordPrompt is a prompt used to ask the user for a password on
|
|
// account creation.
|
|
EnterNewPasswordPrompt = "Enter new password > "
|
|
// EnterOldPasswordPrompt is a prompt used to ask the user for an old password.
|
|
EnterOldPasswordPrompt = "Enter old password > "
|
|
// ConfirmPasswordPrompt is a prompt used to confirm the password.
|
|
ConfirmPasswordPrompt = "Confirm password > "
|
|
)
|
|
|
|
var (
|
|
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); 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",
|
|
Usage: "WIF to import",
|
|
}
|
|
decryptFlag = cli.BoolFlag{
|
|
Name: "decrypt, d",
|
|
Usage: "Decrypt encrypted keys.",
|
|
}
|
|
outFlag = cli.StringFlag{
|
|
Name: "out",
|
|
Usage: "file to put JSON transaction to",
|
|
}
|
|
inFlag = cli.StringFlag{
|
|
Name: "in",
|
|
Usage: "file with JSON transaction",
|
|
}
|
|
fromAddrFlag = flags.AddressFlag{
|
|
Name: "from",
|
|
Usage: "Address to send an asset from",
|
|
}
|
|
toAddrFlag = flags.AddressFlag{
|
|
Name: "to",
|
|
Usage: "Address to send an asset to",
|
|
}
|
|
forceFlag = cli.BoolFlag{
|
|
Name: "force",
|
|
Usage: "Do not ask for a confirmation",
|
|
}
|
|
)
|
|
|
|
// NewCommands returns 'wallet' command.
|
|
func NewCommands() []cli.Command {
|
|
claimFlags := []cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
flags.AddressFlag{
|
|
Name: "address, a",
|
|
Usage: "Address to claim GAS for",
|
|
},
|
|
}
|
|
claimFlags = append(claimFlags, options.RPC...)
|
|
signFlags := []cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
outFlag,
|
|
inFlag,
|
|
flags.AddressFlag{
|
|
Name: "address, a",
|
|
Usage: "Address to use",
|
|
},
|
|
}
|
|
signFlags = append(signFlags, options.RPC...)
|
|
return []cli.Command{{
|
|
Name: "wallet",
|
|
Usage: "create, open and manage a NEO wallet",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "claim",
|
|
Usage: "claim GAS",
|
|
Action: claimGas,
|
|
Flags: claimFlags,
|
|
},
|
|
{
|
|
Name: "init",
|
|
Usage: "create a new wallet",
|
|
Action: createWallet,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
cli.BoolFlag{
|
|
Name: "account, a",
|
|
Usage: "Create a new account",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "change-password",
|
|
Usage: "change password for accounts",
|
|
Action: changePassword,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
flags.AddressFlag{
|
|
Name: "address, a",
|
|
Usage: "address to change password for",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "convert",
|
|
Usage: "convert addresses from existing NEO2 NEP6-wallet to NEO3 format",
|
|
Action: convertWallet,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
cli.StringFlag{
|
|
Name: "out, o",
|
|
Usage: "where to write converted wallet",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "create",
|
|
Usage: "add an account to the existing wallet",
|
|
Action: addAccount,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
},
|
|
},
|
|
{
|
|
Name: "dump",
|
|
Usage: "check and dump an existing NEO wallet",
|
|
Action: dumpWallet,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
decryptFlag,
|
|
},
|
|
},
|
|
{
|
|
Name: "dump-keys",
|
|
Usage: "dump public keys for account",
|
|
Action: dumpKeys,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
flags.AddressFlag{
|
|
Name: "address, a",
|
|
Usage: "address to print public keys for",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "export",
|
|
Usage: "export keys for address",
|
|
UsageText: "export --wallet <path> [--decrypt] [<address>]",
|
|
Action: exportKeys,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
decryptFlag,
|
|
},
|
|
},
|
|
{
|
|
Name: "import",
|
|
Usage: "import WIF of a standard signature contract",
|
|
UsageText: "import --wallet <path> --wif <wif> [--name <account_name>]",
|
|
Action: importWallet,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
wifFlag,
|
|
cli.StringFlag{
|
|
Name: "name, n",
|
|
Usage: "Optional account name",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "contract",
|
|
Usage: "Verification script for custom contracts",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "import-multisig",
|
|
Usage: "import multisig contract",
|
|
UsageText: "import-multisig --wallet <path> --wif <wif> [--name <account_name>] --min <n>" +
|
|
" [<pubkey1> [<pubkey2> [...]]]",
|
|
Action: importMultisig,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
wifFlag,
|
|
cli.StringFlag{
|
|
Name: "name, n",
|
|
Usage: "Optional account name",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "min, m",
|
|
Usage: "Minimal number of signatures",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "import-deployed",
|
|
Usage: "import deployed contract",
|
|
UsageText: "import-deployed --wallet <path> --wif <wif> --contract <hash> [--name <account_name>]",
|
|
Action: importDeployed,
|
|
Flags: append([]cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
wifFlag,
|
|
cli.StringFlag{
|
|
Name: "name, n",
|
|
Usage: "Optional account name",
|
|
},
|
|
flags.AddressFlag{
|
|
Name: "contract, c",
|
|
Usage: "Contract hash or address",
|
|
},
|
|
}, options.RPC...),
|
|
},
|
|
{
|
|
Name: "remove",
|
|
Usage: "remove an account from the wallet",
|
|
UsageText: "remove --wallet <path> [--force] --address <addr>",
|
|
Action: removeAccount,
|
|
Flags: []cli.Flag{
|
|
walletPathFlag,
|
|
walletConfigFlag,
|
|
forceFlag,
|
|
flags.AddressFlag{
|
|
Name: "address, a",
|
|
Usage: "Account address or hash in LE form to be removed",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "sign",
|
|
Usage: "cosign transaction with multisig/contract/additional account",
|
|
UsageText: "sign --wallet <path> --address <address> --in <file.in> --out <file.out> [-r <endpoint>]",
|
|
Action: signStoredTransaction,
|
|
Flags: signFlags,
|
|
},
|
|
{
|
|
Name: "nep17",
|
|
Usage: "work with NEP-17 contracts",
|
|
Subcommands: newNEP17Commands(),
|
|
},
|
|
{
|
|
Name: "nep11",
|
|
Usage: "work with NEP-11 contracts",
|
|
Subcommands: newNEP11Commands(),
|
|
},
|
|
{
|
|
Name: "candidate",
|
|
Usage: "work with candidates",
|
|
Subcommands: newValidatorCommands(),
|
|
},
|
|
},
|
|
}}
|
|
}
|
|
|
|
func claimGas(ctx *cli.Context) error {
|
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
|
return err
|
|
}
|
|
wall, pass, err := readWallet(ctx)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
addrFlag := ctx.Generic("address").(*flags.Address)
|
|
if !addrFlag.IsSet {
|
|
return cli.NewExitError("address was not provided", 1)
|
|
}
|
|
scriptHash := addrFlag.Uint160()
|
|
acc, err := getDecryptedAccount(wall, scriptHash, pass)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
gctx, cancel := options.GetTimeoutContext(ctx)
|
|
defer cancel()
|
|
|
|
c, err := options.GetRPCClient(gctx, ctx)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
neoContractHash, err := c.GetNativeContractHash(nativenames.Neo)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
hash, err := c.TransferNEP17(acc, scriptHash, neoContractHash, 0, 0, nil, nil)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
fmt.Fprintln(ctx.App.Writer, hash.StringLE())
|
|
return nil
|
|
}
|
|
|
|
func changePassword(ctx *cli.Context) error {
|
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
|
return err
|
|
}
|
|
wall, _, err := openWallet(ctx, false)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
if len(wall.Accounts) == 0 {
|
|
return cli.NewExitError("wallet has no accounts", 1)
|
|
}
|
|
addrFlag := ctx.Generic("address").(*flags.Address)
|
|
if addrFlag.IsSet {
|
|
// Check for account presence first before asking for password.
|
|
acc := wall.GetAccount(addrFlag.Uint160())
|
|
if acc == nil {
|
|
return cli.NewExitError("account is missing", 1)
|
|
}
|
|
}
|
|
|
|
oldPass, err := input.ReadPassword(EnterOldPasswordPrompt)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("Error reading old password: %w", err), 1)
|
|
}
|
|
|
|
for i := range wall.Accounts {
|
|
if addrFlag.IsSet && wall.Accounts[i].Address != addrFlag.String() {
|
|
continue
|
|
}
|
|
err := wall.Accounts[i].Decrypt(oldPass, wall.Scrypt)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("unable to decrypt account %s: %w", wall.Accounts[i].Address, err), 1)
|
|
}
|
|
}
|
|
|
|
pass, err := readNewPassword()
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("Error reading new password: %w", err), 1)
|
|
}
|
|
for i := range wall.Accounts {
|
|
if addrFlag.IsSet && wall.Accounts[i].Address != addrFlag.String() {
|
|
continue
|
|
}
|
|
err := wall.Accounts[i].Encrypt(pass, wall.Scrypt)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
}
|
|
err = wall.Save()
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("Error saving the wallet: %w", err), 1)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func convertWallet(ctx *cli.Context) error {
|
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
|
return err
|
|
}
|
|
wall, pass, err := newWalletV2FromFile(ctx.String("wallet"), ctx.String("wallet-config"))
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
out := ctx.String("out")
|
|
if len(out) == 0 {
|
|
return cli.NewExitError("missing out path", 1)
|
|
}
|
|
newWallet, err := wallet.NewWallet(out)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
newWallet.Scrypt = wall.Scrypt
|
|
|
|
for _, acc := range wall.Accounts {
|
|
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)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
newWallet.AddAccount(newAcc)
|
|
}
|
|
if err := newWallet.Save(); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func addAccount(ctx *cli.Context) error {
|
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
|
return err
|
|
}
|
|
wall, pass, err := openWallet(ctx, true)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
if err := createAccount(wall, pass); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func exportKeys(ctx *cli.Context) error {
|
|
wall, pass, err := readWallet(ctx)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
var addr string
|
|
|
|
decrypt := ctx.Bool("decrypt")
|
|
if ctx.NArg() == 0 && decrypt {
|
|
return cli.NewExitError(errors.New("address must be provided if '--decrypt' flag is used"), 1)
|
|
} else if ctx.NArg() > 0 {
|
|
// check address format just to catch possible typos
|
|
addr = ctx.Args().First()
|
|
_, err := address.StringToUint160(addr)
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("can't parse address: %w", err), 1)
|
|
}
|
|
}
|
|
|
|
var wifs []string
|
|
|
|
loop:
|
|
for _, a := range wall.Accounts {
|
|
if addr != "" && a.Address != addr {
|
|
continue
|
|
}
|
|
|
|
for i := range wifs {
|
|
if a.EncryptedWIF == wifs[i] {
|
|
continue loop
|
|
}
|
|
}
|
|
|
|
wifs = append(wifs, a.EncryptedWIF)
|
|
}
|
|
|
|
for _, wif := range wifs {
|
|
if decrypt {
|
|
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)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
wif = pk.WIF()
|
|
}
|
|
|
|
fmt.Fprintln(ctx.App.Writer, wif)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func importMultisig(ctx *cli.Context) error {
|
|
wall, _, err := openWallet(ctx, true)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
m := ctx.Int("min")
|
|
if ctx.NArg() < m {
|
|
return cli.NewExitError(errors.New("insufficient number of public keys"), 1)
|
|
}
|
|
|
|
args := []string(ctx.Args())
|
|
pubs := make([]*keys.PublicKey, len(args))
|
|
|
|
for i := range args {
|
|
pubs[i], err = keys.NewPublicKeyFromString(args[i])
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("can't decode public key %d: %w", i, err), 1)
|
|
}
|
|
}
|
|
|
|
acc, err := newAccountFromWIF(ctx.App.Writer, ctx.String("wif"), wall.Scrypt)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
if err := acc.ConvertMultisig(m, pubs); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
if acc.Label == "" {
|
|
acc.Label = ctx.String("name")
|
|
}
|
|
if err := addAccountAndSave(wall, acc); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func importDeployed(ctx *cli.Context) error {
|
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
|
return err
|
|
}
|
|
wall, _, err := openWallet(ctx, true)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
rawHash := ctx.Generic("contract").(*flags.Address)
|
|
if !rawHash.IsSet {
|
|
return cli.NewExitError("contract hash was not provided", 1)
|
|
}
|
|
|
|
acc, err := newAccountFromWIF(ctx.App.Writer, ctx.String("wif"), wall.Scrypt)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
gctx, cancel := options.GetTimeoutContext(ctx)
|
|
defer cancel()
|
|
|
|
c, err := options.GetRPCClient(gctx, ctx)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
cs, err := c.GetContractStateByHash(rawHash.Uint160())
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("can't fetch contract info: %w", err), 1)
|
|
}
|
|
md := cs.Manifest.ABI.GetMethod(manifest.MethodVerify, -1)
|
|
if md == nil || md.ReturnType != smartcontract.BoolType {
|
|
return cli.NewExitError("contract has no `verify` method with boolean return", 1)
|
|
}
|
|
acc.Address = address.Uint160ToString(cs.Hash)
|
|
acc.Contract.Script = cs.NEF.Script
|
|
acc.Contract.Parameters = acc.Contract.Parameters[:0]
|
|
for _, p := range md.Parameters {
|
|
acc.Contract.Parameters = append(acc.Contract.Parameters, wallet.ContractParam{
|
|
Name: p.Name,
|
|
Type: p.Type,
|
|
})
|
|
}
|
|
acc.Contract.Deployed = true
|
|
|
|
if acc.Label == "" {
|
|
acc.Label = ctx.String("name")
|
|
}
|
|
if err := addAccountAndSave(wall, acc); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func importWallet(ctx *cli.Context) error {
|
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
|
return err
|
|
}
|
|
wall, _, err := openWallet(ctx, true)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
acc, err := newAccountFromWIF(ctx.App.Writer, ctx.String("wif"), wall.Scrypt)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
if ctrFlag := ctx.String("contract"); ctrFlag != "" {
|
|
ctr, err := hex.DecodeString(ctrFlag)
|
|
if err != nil {
|
|
return cli.NewExitError("invalid contract", 1)
|
|
}
|
|
acc.Contract.Script = ctr
|
|
}
|
|
|
|
if acc.Label == "" {
|
|
acc.Label = ctx.String("name")
|
|
}
|
|
if err := addAccountAndSave(wall, acc); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func removeAccount(ctx *cli.Context) error {
|
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
|
return err
|
|
}
|
|
wall, _, err := openWallet(ctx, true)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
addr := ctx.Generic("address").(*flags.Address)
|
|
if !addr.IsSet {
|
|
return cli.NewExitError("valid account address must be provided", 1)
|
|
}
|
|
acc := wall.GetAccount(addr.Uint160())
|
|
if acc == nil {
|
|
return cli.NewExitError("account wasn't found", 1)
|
|
}
|
|
|
|
if !ctx.Bool("force") {
|
|
fmt.Fprintf(ctx.App.Writer, "Account %s will be removed. This action is irreversible.\n", addr.Uint160())
|
|
if ok := askForConsent(ctx.App.Writer); !ok {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if err := wall.RemoveAccount(acc.Address); err != nil {
|
|
return cli.NewExitError(fmt.Errorf("error on remove: %w", err), 1)
|
|
}
|
|
if err := wall.Save(); err != nil {
|
|
return cli.NewExitError(fmt.Errorf("error while saving wallet: %w", err), 1)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func askForConsent(w io.Writer) bool {
|
|
response, err := input.ReadLine("Are you sure? [y/N]: ")
|
|
if err == nil {
|
|
response = strings.ToLower(strings.TrimSpace(response))
|
|
if response == "y" || response == "yes" {
|
|
return true
|
|
}
|
|
}
|
|
fmt.Fprintln(w, "Cancelled.")
|
|
return false
|
|
}
|
|
|
|
func dumpWallet(ctx *cli.Context) error {
|
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
|
return err
|
|
}
|
|
wall, pass, err := readWallet(ctx)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
if ctx.Bool("decrypt") {
|
|
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)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
}
|
|
}
|
|
fmtPrintWallet(ctx.App.Writer, wall)
|
|
return nil
|
|
}
|
|
|
|
func dumpKeys(ctx *cli.Context) error {
|
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
|
return err
|
|
}
|
|
wall, _, err := readWallet(ctx)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
accounts := wall.Accounts
|
|
|
|
addrFlag := ctx.Generic("address").(*flags.Address)
|
|
if addrFlag.IsSet {
|
|
acc := wall.GetAccount(addrFlag.Uint160())
|
|
if acc == nil {
|
|
return cli.NewExitError("account is missing", 1)
|
|
}
|
|
accounts = []*wallet.Account{acc}
|
|
}
|
|
|
|
hasPrinted := false
|
|
for _, acc := range accounts {
|
|
pub, ok := vm.ParseSignatureContract(acc.Contract.Script)
|
|
if ok {
|
|
if hasPrinted {
|
|
fmt.Fprintln(ctx.App.Writer)
|
|
}
|
|
fmt.Fprintf(ctx.App.Writer, "%s (simple signature contract):\n", acc.Address)
|
|
fmt.Fprintln(ctx.App.Writer, hex.EncodeToString(pub))
|
|
hasPrinted = true
|
|
continue
|
|
}
|
|
n, bs, ok := vm.ParseMultiSigContract(acc.Contract.Script)
|
|
if ok {
|
|
if hasPrinted {
|
|
fmt.Fprintln(ctx.App.Writer)
|
|
}
|
|
fmt.Fprintf(ctx.App.Writer, "%s (%d out of %d multisig contract):\n", acc.Address, n, len(bs))
|
|
for i := range bs {
|
|
fmt.Fprintln(ctx.App.Writer, hex.EncodeToString(bs[i]))
|
|
}
|
|
hasPrinted = true
|
|
continue
|
|
}
|
|
if addrFlag.IsSet {
|
|
return cli.NewExitError(fmt.Errorf("unknown script type for address %s", address.Uint160ToString(addrFlag.Uint160())), 1)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func createWallet(ctx *cli.Context) error {
|
|
if err := cmdargs.EnsureNone(ctx); err != nil {
|
|
return err
|
|
}
|
|
path := ctx.String("wallet")
|
|
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)
|
|
}
|
|
if err := wall.Save(); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
if ctx.Bool("account") {
|
|
if err := createAccount(wall, pass); err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
}
|
|
|
|
fmtPrintWallet(ctx.App.Writer, wall)
|
|
fmt.Fprintf(ctx.App.Writer, "wallet successfully created, file location is %s\n", wall.Path())
|
|
return nil
|
|
}
|
|
|
|
func readAccountInfo() (string, string, error) {
|
|
name, err := input.ReadLine("Enter the name of the account > ")
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
phrase, err := readNewPassword()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return name, phrase, nil
|
|
}
|
|
|
|
func readNewPassword() (string, error) {
|
|
phrase, err := input.ReadPassword(EnterNewPasswordPrompt)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error reading password: %w", err)
|
|
}
|
|
phraseCheck, err := input.ReadPassword(ConfirmPasswordPrompt)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error reading password: %w", err)
|
|
}
|
|
|
|
if phrase != phraseCheck {
|
|
return "", errPhraseMismatch
|
|
}
|
|
return phrase, nil
|
|
}
|
|
|
|
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(ctx *cli.Context, canUseWalletConfig bool) (*wallet.Wallet, *string, error) {
|
|
path, pass, err := getWalletPathAndPass(ctx, canUseWalletConfig)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if path == "-" {
|
|
return nil, nil, errNoStdin
|
|
}
|
|
w, err := wallet.NewWalletFromFile(path)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return w, pass, nil
|
|
}
|
|
|
|
func readWallet(ctx *cli.Context) (*wallet.Wallet, *string, error) {
|
|
path, pass, err := getWalletPathAndPass(ctx, true)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if path == "-" {
|
|
w := &wallet.Wallet{}
|
|
if err := json.NewDecoder(os.Stdin).Decode(w); err != nil {
|
|
return nil, nil, fmt.Errorf("js %s", err)
|
|
}
|
|
return w, nil, nil
|
|
}
|
|
w, err := wallet.NewWalletFromFile(path)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return w, pass, nil
|
|
}
|
|
|
|
// getWalletPathAndPass retrieves wallet path from context or from wallet configuration file.
|
|
// If wallet configuration file is specified, then account password is returned.
|
|
func getWalletPathAndPass(ctx *cli.Context, canUseWalletConfig bool) (string, *string, error) {
|
|
path, configPath := ctx.String("wallet"), ctx.String("wallet-config")
|
|
if !canUseWalletConfig && len(configPath) != 0 {
|
|
return "", nil, errors.New("can't use wallet configuration file for this command")
|
|
}
|
|
if len(path) != 0 && len(configPath) != 0 {
|
|
return "", nil, errConflictingWalletFlags
|
|
}
|
|
if len(path) == 0 && len(configPath) == 0 {
|
|
return "", nil, errNoPath
|
|
}
|
|
var pass *string
|
|
if len(configPath) != 0 {
|
|
cfg, err := ReadWalletConfig(configPath)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
path = cfg.Path
|
|
pass = &cfg.Password
|
|
}
|
|
return path, 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) {
|
|
// note: NEP2 strings always have length of 58 even though
|
|
// base58 strings can have different lengths even if slice lengths are equal
|
|
if len(wif) == 58 {
|
|
pass, err := input.ReadPassword(EnterPasswordPrompt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error reading password: %w", err)
|
|
}
|
|
|
|
return wallet.NewAccountFromEncryptedWIF(wif, pass, scrypt)
|
|
}
|
|
|
|
acc, err := wallet.NewAccountFromWIF(wif)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fmt.Fprintln(w, "Provided WIF was unencrypted. Wallet can contain only encrypted keys.")
|
|
name, pass, err := readAccountInfo()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
acc.Label = name
|
|
if err := acc.Encrypt(pass, scrypt); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return acc, nil
|
|
}
|
|
|
|
func addAccountAndSave(w *wallet.Wallet, acc *wallet.Account) error {
|
|
for i := range w.Accounts {
|
|
if w.Accounts[i].Address == acc.Address {
|
|
return fmt.Errorf("address '%s' is already in wallet", acc.Address)
|
|
}
|
|
}
|
|
|
|
w.AddAccount(acc)
|
|
return w.Save()
|
|
}
|
|
|
|
func fmtPrintWallet(w io.Writer, wall *wallet.Wallet) {
|
|
b, _ := wall.JSON()
|
|
fmt.Fprintln(w, "")
|
|
fmt.Fprintln(w, string(b))
|
|
fmt.Fprintln(w, "")
|
|
}
|