diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 980a78ebac..e3b51d1145 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -236,8 +236,15 @@ func NewCommands() []cli.Command { { Name: "import-multisig", Usage: "import multisig contract", - UsageText: "import-multisig -w wallet [--wallet-config path] --wif [--name ] --min " + + UsageText: "import-multisig -w wallet [--wallet-config path] [--wif ] [--name ] --min " + " [ [ [...]]]", + Description: `Imports a standard multisignature contract with "m out of n" signatures required where "m" is + specified by --min flag and "n" is the length of provided set of public keys. If + --wif flag is provided, it's used to create an account with the given name (or + without a name if --name flag is not provided). Otherwise, the command tries to + find an account with one of the given public keys and convert it to multisig. If + no suitable account is found and no --wif flag is specified, an error is returned. +`, Action: importMultisig, Flags: []cli.Flag{ walletPathFlag, @@ -521,6 +528,12 @@ loop: } func importMultisig(ctx *cli.Context) error { + var ( + label *string + acc *wallet.Account + accPub *keys.PublicKey + ) + wall, pass, err := openWallet(ctx, true) if err != nil { return cli.NewExitError(err, 1) @@ -542,12 +555,45 @@ func importMultisig(ctx *cli.Context) error { } } - var label *string if ctx.IsSet("name") { l := ctx.String("name") label = &l } - acc, err := newAccountFromWIF(ctx.App.Writer, ctx.String("wif"), wall.Scrypt, label, pass) + +loop: + for _, pub := range pubs { + for _, wallAcc := range wall.Accounts { + if wallAcc.ScriptHash().Equals(pub.GetScriptHash()) { + if acc != nil { + // Multiple matching accounts found, fallback to WIF-based conversion. + acc = nil + break loop + } + acc = new(wallet.Account) + *acc = *wallAcc + accPub = pub + } + } + } + + if acc != nil { + err = acc.ConvertMultisigEncrypted(accPub, m, pubs) + if err != nil { + return cli.NewExitError(err, 1) + } + if label != nil { + acc.Label = *label + } + if err := addAccountAndSave(wall, acc); err != nil { + return cli.NewExitError(err, 1) + } + return nil + } + + if !ctx.IsSet("wif") { + return cli.NewExitError(errors.New("none of the provided public keys correspond to an existing key in the wallet or multiple matching accounts found in the wallet, and no WIF is provided"), 1) + } + acc, err = newAccountFromWIF(ctx.App.Writer, ctx.String("wif"), wall.Scrypt, label, pass) if err != nil { return cli.NewExitError(err, 1) } diff --git a/cli/wallet/wallet_test.go b/cli/wallet/wallet_test.go index 26e43ed833..6c312ea6f9 100644 --- a/cli/wallet/wallet_test.go +++ b/cli/wallet/wallet_test.go @@ -487,6 +487,58 @@ func TestWalletInit(t *testing.T) { hex.EncodeToString(pubs[2].Bytes()), hex.EncodeToString(pubs[3].Bytes()))...) }) + + privs, pubs = testcli.GenerateKeys(t, 3) + script, err = smartcontract.CreateMultiSigRedeemScript(2, pubs) + require.NoError(t, err) + // Create a wallet and import a standard account + e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath) + e.In.WriteString("standardacc\rstdpass\rstdpass\r") + e.Run(t, "neo-go", "wallet", "import", + "--wallet", walletPath, + "--wif", privs[0].WIF()) + w, err = wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + actual = w.GetAccount(privs[0].GetScriptHash()) + require.NotNil(t, actual) + require.NotEqual(t, actual.Contract.Script, script) + + // Test when a public key of an already imported account is present + t.Run("existing account public key, no WIF", func(t *testing.T) { + e.Run(t, "neo-go", "wallet", "import-multisig", + "--wallet", walletPath, + "--min", "2", + hex.EncodeToString(pubs[0].Bytes()), // Public key of the already imported account + hex.EncodeToString(pubs[1].Bytes()), + hex.EncodeToString(pubs[2].Bytes())) + + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + actual := w.GetAccount(hash.Hash160(script)) + require.NotNil(t, actual) + require.Equal(t, actual.Contract.Script, script) + require.NoError(t, actual.Decrypt("stdpass", w.Scrypt)) + require.NotEqual(t, actual.Address, w.GetAccount(privs[0].GetScriptHash()).Address) + }) + + // Test when no public key of an already imported account is present, and no WIF is provided + t.Run("no existing account public key, no WIF", func(t *testing.T) { + _, pubsNew := testcli.GenerateKeys(t, 3) + scriptNew, err := smartcontract.CreateMultiSigRedeemScript(2, pubsNew) + require.NoError(t, err) + + e.RunWithError(t, "neo-go", "wallet", "import-multisig", + "--wallet", walletPath, + "--min", "2", + hex.EncodeToString(pubsNew[0].Bytes()), + hex.EncodeToString(pubsNew[1].Bytes()), + hex.EncodeToString(pubsNew[2].Bytes())) + + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + actual := w.GetAccount(hash.Hash160(scriptNew)) + require.Nil(t, actual) + }) }) }) } diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index 86b14fff59..558e4449ac 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -283,8 +283,15 @@ func (a *Account) ConvertMultisig(m int, pubs []*keys.PublicKey) error { if a.privateKey == nil { return errors.New("account key is not available (need to decrypt?)") } - var found bool accKey := a.privateKey.PublicKey() + return a.ConvertMultisigEncrypted(accKey, m, pubs) +} + +// ConvertMultisigEncrypted sets a's contract to an encrypted multisig contract +// with m sufficient signatures. The encrypted private key is not modified and +// remains the same. +func (a *Account) ConvertMultisigEncrypted(accKey *keys.PublicKey, m int, pubs []*keys.PublicKey) error { + var found bool for i := range pubs { if accKey.Equal(pubs[i]) { found = true