From 02954285c1c7cad7f0d290fb2f8837c27d860bf1 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 12:05:55 +0300 Subject: [PATCH 01/13] cli: declare wallet path flag once --- cli/wallet/wallet.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 503508503..f94990d2f 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -18,6 +18,13 @@ var ( errPhraseMismatch = errors.New("the entered pass-phrases do not match. Maybe you have misspelled them") ) +var ( + walletPathFlag = cli.StringFlag{ + Name: "path, p", + Usage: "Target location of the wallet file.", + } +) + // NewCommands returns 'wallet' command. func NewCommands() []cli.Command { return []cli.Command{{ @@ -29,10 +36,7 @@ func NewCommands() []cli.Command { Usage: "create a new wallet", Action: createWallet, Flags: []cli.Flag{ - cli.StringFlag{ - Name: "path, p", - Usage: "Target location of the wallet file.", - }, + walletPathFlag, cli.BoolFlag{ Name: "account, a", Usage: "Create a new account", @@ -44,10 +48,7 @@ func NewCommands() []cli.Command { Usage: "check and dump an existing NEO wallet", Action: dumpWallet, Flags: []cli.Flag{ - cli.StringFlag{ - Name: "path, p", - Usage: "Target location of the wallet file.", - }, + walletPathFlag, cli.BoolFlag{ Name: "decrypt, d", Usage: "Decrypt encrypted keys.", From d837eb37618e7e4dec076a73d9bedbc14435a088 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 12:22:40 +0300 Subject: [PATCH 02/13] wallet: allow to create accounts from encrypted WIFs --- pkg/wallet/account.go | 13 +++++++++++++ pkg/wallet/account_test.go | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index 591e1971e..25f9eae02 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -150,6 +150,19 @@ func NewAccountFromWIF(wif string) (*Account, error) { return newAccountFromPrivateKey(privKey), nil } +// NewAccountFromEncryptedWIF creates a new Account from the given encrypted WIF. +func NewAccountFromEncryptedWIF(wif string, pass string) (*Account, error) { + priv, err := keys.NEP2Decrypt(wif, pass) + if err != nil { + return nil, err + } + + a := newAccountFromPrivateKey(priv) + a.EncryptedWIF = wif + + return a, nil +} + // newAccountFromPrivateKey creates a wallet from the given PrivateKey. func newAccountFromPrivateKey(p *keys.PrivateKey) *Account { pubKey := p.PublicKey() diff --git a/pkg/wallet/account_test.go b/pkg/wallet/account_test.go index 81eef739b..93847b991 100644 --- a/pkg/wallet/account_test.go +++ b/pkg/wallet/account_test.go @@ -49,6 +49,19 @@ func TestNewFromWif(t *testing.T) { } } +func TestNewAccountFromEncryptedWIF(t *testing.T) { + for _, tc := range keytestcases.Arr { + acc, err := NewAccountFromEncryptedWIF(tc.EncryptedWif, tc.Passphrase) + if tc.Invalid { + assert.Error(t, err) + continue + } + + assert.NoError(t, err) + compareFields(t, tc, acc) + } +} + func TestContract_MarshalJSON(t *testing.T) { var c Contract From ad6fa2aea9437fb3b63f57a9ebcfa3e1df73e374 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 13:06:10 +0300 Subject: [PATCH 03/13] wallet: set WIF and public key on account decrypt They are set during account creation and open+decrypt is expected to put account in the same state. --- pkg/wallet/account.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index 25f9eae02..9b87c2e8f 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -122,7 +122,14 @@ func (a *Account) Decrypt(passphrase string) error { return errors.New("no encrypted wif in the account") } a.privateKey, err = keys.NEP2Decrypt(a.EncryptedWIF, passphrase) - return err + if err != nil { + return err + } + + a.publicKey = a.privateKey.PublicKey().Bytes() + a.wif = a.privateKey.WIF() + + return nil } // Encrypt encrypts the wallet's PrivateKey with the given passphrase From ea30122a09ba6d1a7a25627484af89cb6b8d8c6d Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 12:55:03 +0300 Subject: [PATCH 04/13] wallet: rewrite file on save When we are opening a file, it is expected that it will be rewritten, not appended to a already existing wallet. --- pkg/wallet/wallet.go | 5 +++++ pkg/wallet/wallet_test.go | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/pkg/wallet/wallet.go b/pkg/wallet/wallet.go index fd9c9ab61..8a6826f5b 100644 --- a/pkg/wallet/wallet.go +++ b/pkg/wallet/wallet.go @@ -105,6 +105,11 @@ func (w *Wallet) Path() string { // that is responsible for saving the data. This can // be a buffer, file, etc.. func (w *Wallet) Save() error { + if s, ok := w.rw.(io.Seeker); ok { + if _, err := s.Seek(0, 0); err != nil { + return err + } + } return json.NewEncoder(w.rw).Encode(w) } diff --git a/pkg/wallet/wallet_test.go b/pkg/wallet/wallet_test.go index c22972713..eee01b199 100644 --- a/pkg/wallet/wallet_test.go +++ b/pkg/wallet/wallet_test.go @@ -90,6 +90,17 @@ func TestSave(t *testing.T) { openedWallet, err := NewWalletFromFile(wallet.path) require.NoError(t, err) require.Equal(t, wallet.Accounts, openedWallet.Accounts) + + t.Run("change and rewrite", func(t *testing.T) { + err := openedWallet.CreateAccount("test", "pass") + require.NoError(t, err) + + w2, err := NewWalletFromFile(openedWallet.path) + require.NoError(t, err) + require.Equal(t, 2, len(w2.Accounts)) + require.NoError(t, w2.Accounts[1].Decrypt("pass")) + require.Equal(t, openedWallet.Accounts, w2.Accounts) + }) } func TestJSONMarshallUnmarshal(t *testing.T) { From eacea8bff5f40b856eb7e20ad032f2a64143def7 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 13:25:01 +0300 Subject: [PATCH 05/13] smartcontract: implement json.Unmarshaler for ParamType --- pkg/smartcontract/param_context.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/smartcontract/param_context.go b/pkg/smartcontract/param_context.go index b53b0cadd..5e2f6f8ff 100644 --- a/pkg/smartcontract/param_context.go +++ b/pkg/smartcontract/param_context.go @@ -2,6 +2,7 @@ package smartcontract import ( "encoding/hex" + "encoding/json" "errors" "strconv" "strings" @@ -78,6 +79,22 @@ func (pt ParamType) MarshalJSON() ([]byte, error) { return []byte(`"` + pt.String() + `"`), nil } +// UnmarshalJSON implements json.Unmarshaler interface. +func (pt *ParamType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + p, err := parseParamType(s) + if err != nil { + return err + } + + *pt = p + return nil +} + // EncodeBinary implements io.Serializable interface. func (pt ParamType) EncodeBinary(w *io.BinWriter) { w.WriteB(byte(pt)) From 5a727cabf8e717e87ab00e4b14b7a6fc29e1d2aa Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 13:28:35 +0300 Subject: [PATCH 06/13] wallet: add signature check contracts to new accounts --- pkg/wallet/account.go | 25 +++++++++++++++++++++++-- pkg/wallet/account_test.go | 2 +- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index 9b87c2e8f..a503d4906 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -4,9 +4,11 @@ import ( "encoding/hex" "encoding/json" "errors" + "fmt" "github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/crypto/keys" + "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" ) @@ -50,7 +52,7 @@ type Contract struct { Script []byte `json:"script"` // A list of parameters used deploying this contract. - Parameters []interface{} `json:"parameters"` + Parameters []contractParam `json:"parameters"` // Indicates whether the contract has been deployed to the blockchain. Deployed bool `json:"deployed"` @@ -62,12 +64,17 @@ type contract struct { Script string `json:"script"` // A list of parameters used deploying this contract. - Parameters []interface{} `json:"parameters"` + Parameters []contractParam `json:"parameters"` // Indicates whether the contract has been deployed to the blockchain. Deployed bool `json:"deployed"` } +type contractParam struct { + Name string `json:"name"` + Type smartcontract.ParamType `json:"type"` +} + // ScriptHash returns the hash of contract's script. func (c Contract) ScriptHash() util.Uint160 { return hash.Hash160(c.Script) @@ -181,7 +188,21 @@ func newAccountFromPrivateKey(p *keys.PrivateKey) *Account { privateKey: p, Address: pubAddr, wif: wif, + Contract: &Contract{ + Script: pubKey.GetVerificationScript(), + Parameters: getContractParams(1), + }, } return a } + +func getContractParams(n int) []contractParam { + params := make([]contractParam, n) + for i := range params { + params[i].Name = fmt.Sprintf("parameter%d", i) + params[i].Type = smartcontract.SignatureType + } + + return params +} diff --git a/pkg/wallet/account_test.go b/pkg/wallet/account_test.go index 93847b991..f5e7db583 100644 --- a/pkg/wallet/account_test.go +++ b/pkg/wallet/account_test.go @@ -65,7 +65,7 @@ func TestNewAccountFromEncryptedWIF(t *testing.T) { func TestContract_MarshalJSON(t *testing.T) { var c Contract - data := []byte(`{"script":"0102","parameters":[1],"deployed":false}`) + data := []byte(`{"script":"0102","parameters":[{"name":"name0", "type":"Signature"}],"deployed":false}`) require.NoError(t, json.Unmarshal(data, &c)) require.Equal(t, []byte{1, 2}, c.Script) From a71c2c4bfd131186ab64822d0bc152a8ec4b3a6b Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 13:34:27 +0300 Subject: [PATCH 07/13] cli: move password getting to a separate function Also do not ignore errors from `terminal.ReadPassword`. --- cli/wallet/wallet.go | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index f94990d2f..b8f08c691 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -69,14 +69,13 @@ func dumpWallet(ctx *cli.Context) error { return cli.NewExitError(err, 1) } if ctx.Bool("decrypt") { - fmt.Print("Wallet password: ") - pass, err := terminal.ReadPassword(int(syscall.Stdin)) + pass, err := readPassword("Enter wallet password > ") if err != nil { return cli.NewExitError(err, 1) } for i := range wall.Accounts { // Just testing the decryption here. - err := wall.Accounts[i].Decrypt(string(pass)) + err := wall.Accounts[i].Decrypt(pass) if err != nil { return cli.NewExitError(err, 1) } @@ -111,33 +110,36 @@ func createWallet(ctx *cli.Context) error { } func createAccount(ctx *cli.Context, wall *wallet.Wallet) error { - var ( - rawName, - rawPhrase, - rawPhraseCheck []byte - ) buf := bufio.NewReader(os.Stdin) fmt.Print("Enter the name of the account > ") - rawName, _ = buf.ReadBytes('\n') - fmt.Print("Enter passphrase > ") - rawPhrase, _ = terminal.ReadPassword(int(syscall.Stdin)) - fmt.Print("\nConfirm passphrase > ") - rawPhraseCheck, _ = terminal.ReadPassword(int(syscall.Stdin)) - - // Clean data - var ( - name = strings.TrimRight(string(rawName), "\n") - phrase = strings.TrimRight(string(rawPhrase), "\n") - phraseCheck = strings.TrimRight(string(rawPhraseCheck), "\n") - ) + rawName, _ := buf.ReadBytes('\n') + phrase, err := readPassword("Enter passphrase > ") + if err != nil { + return cli.NewExitError(err, 1) + } + phraseCheck, err := readPassword("Confirm passphrase > ") + if err != nil { + return cli.NewExitError(err, 1) + } if phrase != phraseCheck { return errPhraseMismatch } + name := strings.TrimRight(string(rawName), "\n") return wall.CreateAccount(name, phrase) } +func readPassword(prompt string) (string, error) { + fmt.Print(prompt) + rawPass, err := terminal.ReadPassword(syscall.Stdin) + fmt.Println() + if err != nil { + return "", err + } + return strings.TrimRight(string(rawPass), "\n"), nil +} + func fmtPrintWallet(wall *wallet.Wallet) { b, _ := wall.JSON() fmt.Println("") From baa68e1d464cf407408cbbf59fb26e7cccc29943 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 13:48:59 +0300 Subject: [PATCH 08/13] cli: implement wallet WIF import --- cli/wallet/wallet.go | 101 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 4 deletions(-) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index b8f08c691..65e91bc7e 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -23,6 +23,10 @@ var ( Name: "path, p", Usage: "Target location of the wallet file.", } + wifFlag = cli.StringFlag{ + Name: "wif", + Usage: "WIF to import", + } ) // NewCommands returns 'wallet' command. @@ -55,10 +59,49 @@ func NewCommands() []cli.Command { }, }, }, + { + Name: "import", + Usage: "import WIF", + Action: importWallet, + Flags: []cli.Flag{ + walletPathFlag, + wifFlag, + cli.StringFlag{ + Name: "name, n", + Usage: "Optional account name", + }, + }, + }, }, }} } +func importWallet(ctx *cli.Context) error { + path := ctx.String("path") + if len(path) == 0 { + return cli.NewExitError(errNoPath, 1) + } + + wall, err := wallet.NewWalletFromFile(path) + if err != nil { + return cli.NewExitError(err, 1) + } + + defer wall.Close() + + acc, err := newAccountFromWIF(ctx.String("wif")) + if err != nil { + return cli.NewExitError(err, 1) + } + + acc.Label = ctx.String("name") + if err := addAccountAndSave(wall, acc); err != nil { + return cli.NewExitError(err, 1) + } + + return nil +} + func dumpWallet(ctx *cli.Context) error { path := ctx.String("path") if len(path) == 0 { @@ -109,27 +152,77 @@ func createWallet(ctx *cli.Context) error { return nil } -func createAccount(ctx *cli.Context, wall *wallet.Wallet) error { +func readAccountInfo() (string, string, error) { buf := bufio.NewReader(os.Stdin) fmt.Print("Enter the name of the account > ") rawName, _ := buf.ReadBytes('\n') phrase, err := readPassword("Enter passphrase > ") if err != nil { - return cli.NewExitError(err, 1) + return "", "", err } phraseCheck, err := readPassword("Confirm passphrase > ") if err != nil { - return cli.NewExitError(err, 1) + return "", "", err } if phrase != phraseCheck { - return errPhraseMismatch + return "", "", errPhraseMismatch } name := strings.TrimRight(string(rawName), "\n") + return name, phrase, nil +} + +func createAccount(ctx *cli.Context, wall *wallet.Wallet) error { + name, phrase, err := readAccountInfo() + if err != nil { + return err + } return wall.CreateAccount(name, phrase) } +func newAccountFromWIF(wif string) (*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 := readPassword("Enter password > ") + if err != nil { + return nil, err + } + + return wallet.NewAccountFromEncryptedWIF(wif, pass) + } + + acc, err := wallet.NewAccountFromWIF(wif) + if err != nil { + return nil, err + } + + fmt.Println("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); 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 readPassword(prompt string) (string, error) { fmt.Print(prompt) rawPass, err := terminal.ReadPassword(syscall.Stdin) From 25ffb56982af5d3d6b20b9030c573c70b7f6215b Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 15:47:36 +0300 Subject: [PATCH 09/13] wallet: support creating multisig accounts (*Account).ConvertMultisig() will convert an existing account into a multisig one. --- pkg/wallet/account.go | 30 +++++++++++++++++++++++++ pkg/wallet/account_test.go | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index a503d4906..90635e5c6 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -1,6 +1,7 @@ package wallet import ( + "bytes" "encoding/hex" "encoding/json" "errors" @@ -8,6 +9,7 @@ import ( "github.com/CityOfZion/neo-go/pkg/crypto/hash" "github.com/CityOfZion/neo-go/pkg/crypto/keys" + "github.com/CityOfZion/neo-go/pkg/encoding/address" "github.com/CityOfZion/neo-go/pkg/smartcontract" "github.com/CityOfZion/neo-go/pkg/util" ) @@ -177,6 +179,34 @@ func NewAccountFromEncryptedWIF(wif string, pass string) (*Account, error) { return a, nil } +// ConvertMultisig sets a's contract to multisig contract with m sufficient signatures. +func (a *Account) ConvertMultisig(m int, pubs []*keys.PublicKey) error { + var found bool + for i := range pubs { + if bytes.Equal(a.publicKey, pubs[i].Bytes()) { + found = true + break + } + } + + if !found { + return errors.New("own public key was not found among multisig keys") + } + + script, err := smartcontract.CreateMultiSigRedeemScript(m, pubs) + if err != nil { + return err + } + + a.Address = address.Uint160ToString(hash.Hash160(script)) + a.Contract = &Contract{ + Script: script, + Parameters: getContractParams(m), + } + + return nil +} + // newAccountFromPrivateKey creates a wallet from the given PrivateKey. func newAccountFromPrivateKey(p *keys.PrivateKey) *Account { pubKey := p.PublicKey() diff --git a/pkg/wallet/account_test.go b/pkg/wallet/account_test.go index f5e7db583..654566c1e 100644 --- a/pkg/wallet/account_test.go +++ b/pkg/wallet/account_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/CityOfZion/neo-go/pkg/crypto/hash" + "github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/internal/keytestcases" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -87,6 +88,51 @@ func TestContract_ScriptHash(t *testing.T) { require.Equal(t, hash.Hash160(script), c.ScriptHash()) } +func TestAccount_ConvertMultisig(t *testing.T) { + // test is based on a wallet1_solo.json accounts from neo-local + a, err := NewAccountFromEncryptedWIF("6PYLmjBYJ4wQTCEfqvnznGJwZeW9pfUcV5m5oreHxqryUgqKpTRAFt9L8Y", "one") + require.NoError(t, err) + + hexs := []string{ + "02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2", // <- this is our key + "02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e", + "02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62", + "03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699", + } + + t.Run("invalid number of signatures", func(t *testing.T) { + pubs := convertPubs(t, hexs) + require.Error(t, a.ConvertMultisig(0, pubs)) + }) + + t.Run("account key is missing from multisig", func(t *testing.T) { + pubs := convertPubs(t, hexs[1:]) + require.Error(t, a.ConvertMultisig(1, pubs)) + }) + + t.Run("1/1 multisig", func(t *testing.T) { + pubs := convertPubs(t, hexs[:1]) + require.NoError(t, a.ConvertMultisig(1, pubs)) + require.Equal(t, "AbU69m8WUZJSWanfr1Cy66cpEcsmMcX7BR", a.Address) + }) + + t.Run("3/4 multisig", func(t *testing.T) { + pubs := convertPubs(t, hexs) + require.NoError(t, a.ConvertMultisig(3, pubs)) + require.Equal(t, "AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU", a.Address) + }) +} + +func convertPubs(t *testing.T, hexKeys []string) []*keys.PublicKey { + pubs := make([]*keys.PublicKey, len(hexKeys)) + for i := range pubs { + var err error + pubs[i], err = keys.NewPublicKeyFromString(hexKeys[i]) + require.NoError(t, err) + } + return pubs +} + func compareFields(t *testing.T, tk keytestcases.Ktype, acc *Account) { if want, have := tk.Address, acc.Address; want != have { t.Fatalf("expected %s got %s", want, have) From a030411310c7e5b6dc4624f0937116f608725f14 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 15:50:17 +0300 Subject: [PATCH 10/13] cli: implement multisig import to wallet --- cli/wallet/wallet.go | 64 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 65e91bc7e..ddd2e8229 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -8,6 +8,7 @@ import ( "strings" "syscall" + "github.com/CityOfZion/neo-go/pkg/crypto/keys" "github.com/CityOfZion/neo-go/pkg/wallet" "github.com/urfave/cli" "golang.org/x/crypto/ssh/terminal" @@ -72,10 +73,73 @@ func NewCommands() []cli.Command { }, }, }, + { + Name: "import-multisig", + Usage: "import multisig contract", + UsageText: "import-multisig --path --wif --min " + + " [ [ [...]]]", + Action: importMultisig, + Flags: []cli.Flag{ + walletPathFlag, + wifFlag, + cli.StringFlag{ + Name: "name, n", + Usage: "Optional account name", + }, + cli.IntFlag{ + Name: "min, m", + Usage: "Minimal number of signatures", + }, + }, + }, }, }} } +func importMultisig(ctx *cli.Context) error { + path := ctx.String("path") + if len(path) == 0 { + return cli.NewExitError(errNoPath, 1) + } + + wall, err := wallet.NewWalletFromFile(path) + if err != nil { + return cli.NewExitError(err, 1) + } + + defer wall.Close() + + 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: %v", i, err), 1) + } + } + + acc, err := newAccountFromWIF(ctx.String("wif")) + if err != nil { + return cli.NewExitError(err, 1) + } + + if err := acc.ConvertMultisig(m, pubs); err != nil { + return cli.NewExitError(err, 1) + } + + if err := addAccountAndSave(wall, acc); err != nil { + return cli.NewExitError(err, 1) + } + + return nil +} + func importWallet(ctx *cli.Context) error { path := ctx.String("path") if len(path) == 0 { From 20c98411fdb9e4061b9ec2b3403581029389d9e1 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 16:14:14 +0300 Subject: [PATCH 11/13] cli: implement wallet WIF/NEP2 export One positional argument can be provided. If so, it is interpreted as address and only WIFs corresponding to it are exported. If address is provided '--decrypt' flag can be specified to export unencrypted WIFs. --- cli/wallet/wallet.go | 83 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index ddd2e8229..c0bd070e0 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -9,6 +9,7 @@ import ( "syscall" "github.com/CityOfZion/neo-go/pkg/crypto/keys" + "github.com/CityOfZion/neo-go/pkg/encoding/address" "github.com/CityOfZion/neo-go/pkg/wallet" "github.com/urfave/cli" "golang.org/x/crypto/ssh/terminal" @@ -28,6 +29,10 @@ var ( Name: "wif", Usage: "WIF to import", } + decryptFlag = cli.BoolFlag{ + Name: "decrypt, d", + Usage: "Decrypt encrypted keys.", + } ) // NewCommands returns 'wallet' command. @@ -54,10 +59,17 @@ func NewCommands() []cli.Command { Action: dumpWallet, Flags: []cli.Flag{ walletPathFlag, - cli.BoolFlag{ - Name: "decrypt, d", - Usage: "Decrypt encrypted keys.", - }, + decryptFlag, + }, + }, + { + Name: "export", + Usage: "export keys for address", + UsageText: "export --path [--decrypt] [
]", + Action: exportKeys, + Flags: []cli.Flag{ + walletPathFlag, + decryptFlag, }, }, { @@ -96,6 +108,69 @@ func NewCommands() []cli.Command { }} } +func exportKeys(ctx *cli.Context) error { + path := ctx.String("path") + if len(path) == 0 { + return cli.NewExitError(errNoPath, 1) + } + + wall, err := wallet.NewWalletFromFile(path) + 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: %v", 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 { + pass, err := readPassword("Enter password > ") + if err != nil { + return cli.NewExitError(err, 1) + } + + pk, err := keys.NEP2Decrypt(wif, pass) + if err != nil { + return cli.NewExitError(err, 1) + } + + wif = pk.WIF() + } + + fmt.Println(wif) + } + + return nil +} + func importMultisig(ctx *cli.Context) error { path := ctx.String("path") if len(path) == 0 { From 3d67d525373cc513abc2b88aec929310532cf1c4 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 20 Feb 2020 16:39:15 +0300 Subject: [PATCH 12/13] cli: support account creation in existing wallet --- cli/wallet/wallet.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index c0bd070e0..01ba11014 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -53,6 +53,14 @@ func NewCommands() []cli.Command { }, }, }, + { + Name: "create-account", + Usage: "add an account to the existing wallet", + Action: addAccount, + Flags: []cli.Flag{ + walletPathFlag, + }, + }, { Name: "dump", Usage: "check and dump an existing NEO wallet", @@ -108,6 +116,26 @@ func NewCommands() []cli.Command { }} } +func addAccount(ctx *cli.Context) error { + path := ctx.String("path") + if len(path) == 0 { + return cli.NewExitError(errNoPath, 1) + } + + wall, err := wallet.NewWalletFromFile(path) + if err != nil { + return cli.NewExitError(err, 1) + } + + defer wall.Close() + + if err := createAccount(ctx, wall); err != nil { + return cli.NewExitError(err, 1) + } + + return nil +} + func exportKeys(ctx *cli.Context) error { path := ctx.String("path") if len(path) == 0 { From 2fc63759586e48d44ea088ac051e8d1d7977c2ce Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 21 Feb 2020 11:28:49 +0300 Subject: [PATCH 13/13] cli: provide separate function for opening wallet --- cli/wallet/wallet.go | 41 ++++++++++++----------------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 01ba11014..1433c1fe6 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -117,12 +117,7 @@ func NewCommands() []cli.Command { } func addAccount(ctx *cli.Context) error { - path := ctx.String("path") - if len(path) == 0 { - return cli.NewExitError(errNoPath, 1) - } - - wall, err := wallet.NewWalletFromFile(path) + wall, err := openWallet(ctx.String("path")) if err != nil { return cli.NewExitError(err, 1) } @@ -137,12 +132,7 @@ func addAccount(ctx *cli.Context) error { } func exportKeys(ctx *cli.Context) error { - path := ctx.String("path") - if len(path) == 0 { - return cli.NewExitError(errNoPath, 1) - } - - wall, err := wallet.NewWalletFromFile(path) + wall, err := openWallet(ctx.String("path")) if err != nil { return cli.NewExitError(err, 1) } @@ -200,12 +190,7 @@ loop: } func importMultisig(ctx *cli.Context) error { - path := ctx.String("path") - if len(path) == 0 { - return cli.NewExitError(errNoPath, 1) - } - - wall, err := wallet.NewWalletFromFile(path) + wall, err := openWallet(ctx.String("path")) if err != nil { return cli.NewExitError(err, 1) } @@ -244,12 +229,7 @@ func importMultisig(ctx *cli.Context) error { } func importWallet(ctx *cli.Context) error { - path := ctx.String("path") - if len(path) == 0 { - return cli.NewExitError(errNoPath, 1) - } - - wall, err := wallet.NewWalletFromFile(path) + wall, err := openWallet(ctx.String("path")) if err != nil { return cli.NewExitError(err, 1) } @@ -270,11 +250,7 @@ func importWallet(ctx *cli.Context) error { } func dumpWallet(ctx *cli.Context) error { - path := ctx.String("path") - if len(path) == 0 { - return cli.NewExitError(errNoPath, 1) - } - wall, err := wallet.NewWalletFromFile(path) + wall, err := openWallet(ctx.String("path")) if err != nil { return cli.NewExitError(err, 1) } @@ -348,6 +324,13 @@ func createAccount(ctx *cli.Context, wall *wallet.Wallet) error { return wall.CreateAccount(name, phrase) } +func openWallet(path string) (*wallet.Wallet, error) { + if len(path) == 0 { + return nil, errNoPath + } + return wallet.NewWalletFromFile(path) +} + func newAccountFromWIF(wif string) (*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