forked from TrueCloudLab/neoneo-go
Merge pull request #685 from nspcc-dev/feature/wallet
cli: implement wallet import/export functionality, part of #26.
This commit is contained in:
commit
ef31d0dd3c
6 changed files with 451 additions and 42 deletions
|
@ -8,6 +8,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"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/CityOfZion/neo-go/pkg/wallet"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
@ -18,6 +20,21 @@ var (
|
||||||
errPhraseMismatch = errors.New("the entered pass-phrases do not match. Maybe you have misspelled them")
|
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.",
|
||||||
|
}
|
||||||
|
wifFlag = cli.StringFlag{
|
||||||
|
Name: "wif",
|
||||||
|
Usage: "WIF to import",
|
||||||
|
}
|
||||||
|
decryptFlag = cli.BoolFlag{
|
||||||
|
Name: "decrypt, d",
|
||||||
|
Usage: "Decrypt encrypted keys.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// NewCommands returns 'wallet' command.
|
// NewCommands returns 'wallet' command.
|
||||||
func NewCommands() []cli.Command {
|
func NewCommands() []cli.Command {
|
||||||
return []cli.Command{{
|
return []cli.Command{{
|
||||||
|
@ -29,28 +46,69 @@ func NewCommands() []cli.Command {
|
||||||
Usage: "create a new wallet",
|
Usage: "create a new wallet",
|
||||||
Action: createWallet,
|
Action: createWallet,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{
|
walletPathFlag,
|
||||||
Name: "path, p",
|
|
||||||
Usage: "Target location of the wallet file.",
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "account, a",
|
Name: "account, a",
|
||||||
Usage: "Create a new account",
|
Usage: "Create a new account",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "create-account",
|
||||||
|
Usage: "add an account to the existing wallet",
|
||||||
|
Action: addAccount,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
walletPathFlag,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "dump",
|
Name: "dump",
|
||||||
Usage: "check and dump an existing NEO wallet",
|
Usage: "check and dump an existing NEO wallet",
|
||||||
Action: dumpWallet,
|
Action: dumpWallet,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.StringFlag{
|
walletPathFlag,
|
||||||
Name: "path, p",
|
decryptFlag,
|
||||||
Usage: "Target location of the wallet file.",
|
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
},
|
||||||
Name: "decrypt, d",
|
{
|
||||||
Usage: "Decrypt encrypted keys.",
|
Name: "export",
|
||||||
|
Usage: "export keys for address",
|
||||||
|
UsageText: "export --path <path> [--decrypt] [<address>]",
|
||||||
|
Action: exportKeys,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
walletPathFlag,
|
||||||
|
decryptFlag,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "import",
|
||||||
|
Usage: "import WIF",
|
||||||
|
Action: importWallet,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
walletPathFlag,
|
||||||
|
wifFlag,
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "name, n",
|
||||||
|
Usage: "Optional account name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "import-multisig",
|
||||||
|
Usage: "import multisig contract",
|
||||||
|
UsageText: "import-multisig --path <path> --wif <wif> --min <n>" +
|
||||||
|
" [<pubkey1> [<pubkey2> [...]]]",
|
||||||
|
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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -58,24 +116,152 @@ func NewCommands() []cli.Command {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dumpWallet(ctx *cli.Context) error {
|
func addAccount(ctx *cli.Context) error {
|
||||||
path := ctx.String("path")
|
wall, err := openWallet(ctx.String("path"))
|
||||||
if len(path) == 0 {
|
if err != nil {
|
||||||
return cli.NewExitError(errNoPath, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
wall, err := wallet.NewWalletFromFile(path)
|
|
||||||
|
defer wall.Close()
|
||||||
|
|
||||||
|
if err := createAccount(ctx, wall); err != nil {
|
||||||
|
return cli.NewExitError(err, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func exportKeys(ctx *cli.Context) error {
|
||||||
|
wall, err := openWallet(ctx.String("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 {
|
||||||
|
wall, err := openWallet(ctx.String("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 {
|
||||||
|
wall, err := openWallet(ctx.String("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 {
|
||||||
|
wall, err := openWallet(ctx.String("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
if ctx.Bool("decrypt") {
|
if ctx.Bool("decrypt") {
|
||||||
fmt.Print("Wallet password: ")
|
pass, err := readPassword("Enter wallet password > ")
|
||||||
pass, err := terminal.ReadPassword(int(syscall.Stdin))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
for i := range wall.Accounts {
|
for i := range wall.Accounts {
|
||||||
// Just testing the decryption here.
|
// Just testing the decryption here.
|
||||||
err := wall.Accounts[i].Decrypt(string(pass))
|
err := wall.Accounts[i].Decrypt(pass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
|
@ -109,34 +295,94 @@ func createWallet(ctx *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAccount(ctx *cli.Context, wall *wallet.Wallet) error {
|
func readAccountInfo() (string, string, error) {
|
||||||
var (
|
|
||||||
rawName,
|
|
||||||
rawPhrase,
|
|
||||||
rawPhraseCheck []byte
|
|
||||||
)
|
|
||||||
buf := bufio.NewReader(os.Stdin)
|
buf := bufio.NewReader(os.Stdin)
|
||||||
fmt.Print("Enter the name of the account > ")
|
fmt.Print("Enter the name of the account > ")
|
||||||
rawName, _ = buf.ReadBytes('\n')
|
rawName, _ := buf.ReadBytes('\n')
|
||||||
fmt.Print("Enter passphrase > ")
|
phrase, err := readPassword("Enter passphrase > ")
|
||||||
rawPhrase, _ = terminal.ReadPassword(int(syscall.Stdin))
|
if err != nil {
|
||||||
fmt.Print("\nConfirm passphrase > ")
|
return "", "", err
|
||||||
rawPhraseCheck, _ = terminal.ReadPassword(int(syscall.Stdin))
|
}
|
||||||
|
phraseCheck, err := readPassword("Confirm passphrase > ")
|
||||||
// Clean data
|
if err != nil {
|
||||||
var (
|
return "", "", err
|
||||||
name = strings.TrimRight(string(rawName), "\n")
|
|
||||||
phrase = strings.TrimRight(string(rawPhrase), "\n")
|
|
||||||
phraseCheck = strings.TrimRight(string(rawPhraseCheck), "\n")
|
|
||||||
)
|
|
||||||
|
|
||||||
if phrase != phraseCheck {
|
|
||||||
return errPhraseMismatch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if phrase != phraseCheck {
|
||||||
|
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)
|
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
|
||||||
|
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)
|
||||||
|
fmt.Println()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimRight(string(rawPass), "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
func fmtPrintWallet(wall *wallet.Wallet) {
|
func fmtPrintWallet(wall *wallet.Wallet) {
|
||||||
b, _ := wall.JSON()
|
b, _ := wall.JSON()
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
|
|
|
@ -2,6 +2,7 @@ package smartcontract
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -78,6 +79,22 @@ func (pt ParamType) MarshalJSON() ([]byte, error) {
|
||||||
return []byte(`"` + pt.String() + `"`), nil
|
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.
|
// EncodeBinary implements io.Serializable interface.
|
||||||
func (pt ParamType) EncodeBinary(w *io.BinWriter) {
|
func (pt ParamType) EncodeBinary(w *io.BinWriter) {
|
||||||
w.WriteB(byte(pt))
|
w.WriteB(byte(pt))
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
package wallet
|
package wallet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
|
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
|
||||||
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
|
"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"
|
"github.com/CityOfZion/neo-go/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,7 +54,7 @@ type Contract struct {
|
||||||
Script []byte `json:"script"`
|
Script []byte `json:"script"`
|
||||||
|
|
||||||
// A list of parameters used deploying this contract.
|
// 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.
|
// Indicates whether the contract has been deployed to the blockchain.
|
||||||
Deployed bool `json:"deployed"`
|
Deployed bool `json:"deployed"`
|
||||||
|
@ -62,12 +66,17 @@ type contract struct {
|
||||||
Script string `json:"script"`
|
Script string `json:"script"`
|
||||||
|
|
||||||
// A list of parameters used deploying this contract.
|
// 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.
|
// Indicates whether the contract has been deployed to the blockchain.
|
||||||
Deployed bool `json:"deployed"`
|
Deployed bool `json:"deployed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type contractParam struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type smartcontract.ParamType `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
// ScriptHash returns the hash of contract's script.
|
// ScriptHash returns the hash of contract's script.
|
||||||
func (c Contract) ScriptHash() util.Uint160 {
|
func (c Contract) ScriptHash() util.Uint160 {
|
||||||
return hash.Hash160(c.Script)
|
return hash.Hash160(c.Script)
|
||||||
|
@ -122,9 +131,16 @@ func (a *Account) Decrypt(passphrase string) error {
|
||||||
return errors.New("no encrypted wif in the account")
|
return errors.New("no encrypted wif in the account")
|
||||||
}
|
}
|
||||||
a.privateKey, err = keys.NEP2Decrypt(a.EncryptedWIF, passphrase)
|
a.privateKey, err = keys.NEP2Decrypt(a.EncryptedWIF, passphrase)
|
||||||
|
if err != nil {
|
||||||
return err
|
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
|
// Encrypt encrypts the wallet's PrivateKey with the given passphrase
|
||||||
// under the NEP-2 standard.
|
// under the NEP-2 standard.
|
||||||
func (a *Account) Encrypt(passphrase string) error {
|
func (a *Account) Encrypt(passphrase string) error {
|
||||||
|
@ -150,6 +166,47 @@ func NewAccountFromWIF(wif string) (*Account, error) {
|
||||||
return newAccountFromPrivateKey(privKey), nil
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// newAccountFromPrivateKey creates a wallet from the given PrivateKey.
|
||||||
func newAccountFromPrivateKey(p *keys.PrivateKey) *Account {
|
func newAccountFromPrivateKey(p *keys.PrivateKey) *Account {
|
||||||
pubKey := p.PublicKey()
|
pubKey := p.PublicKey()
|
||||||
|
@ -161,7 +218,21 @@ func newAccountFromPrivateKey(p *keys.PrivateKey) *Account {
|
||||||
privateKey: p,
|
privateKey: p,
|
||||||
Address: pubAddr,
|
Address: pubAddr,
|
||||||
wif: wif,
|
wif: wif,
|
||||||
|
Contract: &Contract{
|
||||||
|
Script: pubKey.GetVerificationScript(),
|
||||||
|
Parameters: getContractParams(1),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return a
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
|
"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/CityOfZion/neo-go/pkg/internal/keytestcases"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -49,10 +50,23 @@ 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) {
|
func TestContract_MarshalJSON(t *testing.T) {
|
||||||
var c Contract
|
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.NoError(t, json.Unmarshal(data, &c))
|
||||||
require.Equal(t, []byte{1, 2}, c.Script)
|
require.Equal(t, []byte{1, 2}, c.Script)
|
||||||
|
|
||||||
|
@ -74,6 +88,51 @@ func TestContract_ScriptHash(t *testing.T) {
|
||||||
require.Equal(t, hash.Hash160(script), c.ScriptHash())
|
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) {
|
func compareFields(t *testing.T, tk keytestcases.Ktype, acc *Account) {
|
||||||
if want, have := tk.Address, acc.Address; want != have {
|
if want, have := tk.Address, acc.Address; want != have {
|
||||||
t.Fatalf("expected %s got %s", want, have)
|
t.Fatalf("expected %s got %s", want, have)
|
||||||
|
|
|
@ -105,6 +105,11 @@ func (w *Wallet) Path() string {
|
||||||
// that is responsible for saving the data. This can
|
// that is responsible for saving the data. This can
|
||||||
// be a buffer, file, etc..
|
// be a buffer, file, etc..
|
||||||
func (w *Wallet) Save() error {
|
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)
|
return json.NewEncoder(w.rw).Encode(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,17 @@ func TestSave(t *testing.T) {
|
||||||
openedWallet, err := NewWalletFromFile(wallet.path)
|
openedWallet, err := NewWalletFromFile(wallet.path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, wallet.Accounts, openedWallet.Accounts)
|
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) {
|
func TestJSONMarshallUnmarshal(t *testing.T) {
|
||||||
|
|
Loading…
Reference in a new issue