From e569edc84149dc732386033614dc333f893e433e Mon Sep 17 00:00:00 2001
From: Roman Khimov <roman@nspcc.ru>
Date: Thu, 1 Sep 2022 19:04:47 +0300
Subject: [PATCH] wallet: add ScriptHash() to Account

It allows to simplify a lot of code and avoid getting a PrivateKey in some
cases.
---
 cli/cmdargs/parser.go                   |  5 +----
 cli/nep17_test.go                       |  7 +------
 cli/paramcontext/context.go             |  7 +------
 cli/wallet/nep17.go                     |  7 +------
 pkg/core/native/native_test/neo_test.go |  2 +-
 pkg/rpcclient/actor/actor_test.go       |  6 +-----
 pkg/rpcclient/nep11.go                  | 13 ++----------
 pkg/rpcclient/nep17.go                  |  6 +-----
 pkg/rpcclient/rpc.go                    | 10 ++-------
 pkg/services/notary/core_test.go        |  6 +++---
 pkg/services/rpcsrv/client_test.go      |  5 -----
 pkg/services/stateroot/network.go       |  5 ++---
 pkg/services/stateroot/validators.go    |  2 +-
 pkg/wallet/account.go                   | 28 ++++++++++++++++---------
 pkg/wallet/account_test.go              |  2 ++
 pkg/wallet/wallet_test.go               |  1 +
 16 files changed, 38 insertions(+), 74 deletions(-)

diff --git a/cli/cmdargs/parser.go b/cli/cmdargs/parser.go
index 2661d5831..ab8f8a34b 100644
--- a/cli/cmdargs/parser.go
+++ b/cli/cmdargs/parser.go
@@ -199,10 +199,7 @@ func ParseParams(args []string, calledFromMain bool) (int, []smartcontract.Param
 // accounts from the provided wallet.
 func GetSignersAccounts(senderAcc *wallet.Account, wall *wallet.Wallet, signers []transaction.Signer, accScope transaction.WitnessScope) ([]actor.SignerAccount, error) {
 	signersAccounts := make([]actor.SignerAccount, 0, len(signers)+1)
-	sender, err := address.StringToUint160(senderAcc.Address)
-	if err != nil {
-		return nil, err
-	}
+	sender := senderAcc.ScriptHash()
 	signersAccounts = append(signersAccounts, actor.SignerAccount{
 		Signer: transaction.Signer{
 			Account: sender,
diff --git a/cli/nep17_test.go b/cli/nep17_test.go
index b318616d1..6a43f96e6 100644
--- a/cli/nep17_test.go
+++ b/cli/nep17_test.go
@@ -162,8 +162,7 @@ func TestNEP17Transfer(t *testing.T) {
 	e.checkNextLine(t, `^Total fee:\s*(\d|\.)+`)
 	e.checkTxPersisted(t)
 
-	sh, err := address.StringToUint160(w.Accounts[0].Address)
-	require.NoError(t, err)
+	sh := w.Accounts[0].ScriptHash()
 	b, _ := e.Chain.GetGoverningTokenBalance(sh)
 	require.Equal(t, big.NewInt(1), b)
 
@@ -172,8 +171,6 @@ func TestNEP17Transfer(t *testing.T) {
 		e.Run(t, append(args, "--force")...)
 		e.checkTxPersisted(t)
 
-		sh, err := address.StringToUint160(w.Accounts[0].Address)
-		require.NoError(t, err)
 		b, _ := e.Chain.GetGoverningTokenBalance(sh)
 		require.Equal(t, big.NewInt(2), b)
 	})
@@ -198,8 +195,6 @@ func TestNEP17Transfer(t *testing.T) {
 		e.Run(t, args...)
 		e.checkTxPersisted(t)
 
-		sh, err := address.StringToUint160(w.Accounts[0].Address)
-		require.NoError(t, err)
 		b, _ := e.Chain.GetGoverningTokenBalance(sh)
 		require.Equal(t, big.NewInt(3), b)
 
diff --git a/cli/paramcontext/context.go b/cli/paramcontext/context.go
index 85175d4d2..bd1b8d5d9 100644
--- a/cli/paramcontext/context.go
+++ b/cli/paramcontext/context.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
 	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
-	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
 	"github.com/nspcc-dev/neo-go/pkg/smartcontract/context"
 	"github.com/nspcc-dev/neo-go/pkg/wallet"
 )
@@ -21,11 +20,7 @@ func InitAndSave(net netmode.Magic, tx *transaction.Transaction, acc *wallet.Acc
 		priv := acc.PrivateKey()
 		pub := priv.PublicKey()
 		sign := priv.SignHashable(uint32(net), tx)
-		h, err := address.StringToUint160(acc.Address)
-		if err != nil {
-			return fmt.Errorf("invalid address: %s", acc.Address)
-		}
-		if err := scCtx.AddSignature(h, acc.Contract, pub, sign); err != nil {
+		if err := scCtx.AddSignature(acc.ScriptHash(), acc.Contract, pub, sign); err != nil {
 			return fmt.Errorf("can't add signature: %w", err)
 		}
 	}
diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go
index 80623528b..94077b9ab 100644
--- a/cli/wallet/nep17.go
+++ b/cli/wallet/nep17.go
@@ -286,17 +286,12 @@ func getNEPBalance(ctx *cli.Context, standard string, accHandler func(*cli.Conte
 		}
 	}
 	for k, acc := range accounts {
-		addrHash, err := address.StringToUint160(acc.Address)
-		if err != nil {
-			return cli.NewExitError(fmt.Errorf("invalid account address: %w", err), 1)
-		}
-
 		if k != 0 {
 			fmt.Fprintln(ctx.App.Writer)
 		}
 		fmt.Fprintf(ctx.App.Writer, "Account %s\n", acc.Address)
 
-		err = accHandler(ctx, c, addrHash, name, token, tokenID)
+		err = accHandler(ctx, c, acc.ScriptHash(), name, token, tokenID)
 		if err != nil {
 			return cli.NewExitError(err, 1)
 		}
diff --git a/pkg/core/native/native_test/neo_test.go b/pkg/core/native/native_test/neo_test.go
index ef9e62433..011636ca9 100644
--- a/pkg/core/native/native_test/neo_test.go
+++ b/pkg/core/native/native_test/neo_test.go
@@ -166,7 +166,7 @@ func TestNEO_Vote(t *testing.T) {
 			txes = append(txes, voteTx)
 		}
 	}
-	txes = append(txes, policyInvoker.PrepareInvoke(t, "blockAccount", candidates[len(candidates)-1].(neotest.SingleSigner).Account().PublicKey().GetScriptHash()))
+	txes = append(txes, policyInvoker.PrepareInvoke(t, "blockAccount", candidates[len(candidates)-1].(neotest.SingleSigner).Account().ScriptHash()))
 	neoValidatorsInvoker.AddNewBlock(t, txes...)
 	for _, tx := range txes {
 		e.CheckHalt(t, tx.Hash(), stackitem.Make(true)) // luckily, both `transfer`, `registerCandidate` and `vote` return boolean values
diff --git a/pkg/rpcclient/actor/actor_test.go b/pkg/rpcclient/actor/actor_test.go
index 5ec1b4c4f..d621d2770 100644
--- a/pkg/rpcclient/actor/actor_test.go
+++ b/pkg/rpcclient/actor/actor_test.go
@@ -7,7 +7,6 @@ import (
 	"github.com/google/uuid"
 	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
 	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
-	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
 	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
 	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
 	"github.com/nspcc-dev/neo-go/pkg/util"
@@ -276,8 +275,5 @@ func TestSender(t *testing.T) {
 	client, acc := testRPCAndAccount(t)
 	a, err := NewSimple(client, acc)
 	require.NoError(t, err)
-
-	addr, err := address.StringToUint160(acc.Address)
-	require.NoError(t, err)
-	require.Equal(t, addr, a.Sender())
+	require.Equal(t, acc.ScriptHash(), a.Sender())
 }
diff --git a/pkg/rpcclient/nep11.go b/pkg/rpcclient/nep11.go
index 1c16990e9..723922759 100644
--- a/pkg/rpcclient/nep11.go
+++ b/pkg/rpcclient/nep11.go
@@ -6,7 +6,6 @@ import (
 	"github.com/google/uuid"
 	"github.com/nspcc-dev/neo-go/pkg/config"
 	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
-	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
 	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
 	"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
 	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
@@ -88,13 +87,9 @@ func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint1
 	if err != nil {
 		return nil, fmt.Errorf("failed to create NEP-11 transfer script: %w", err)
 	}
-	from, err := address.StringToUint160(acc.Address)
-	if err != nil {
-		return nil, fmt.Errorf("bad account address: %w", err)
-	}
 	return c.CreateTxFromScript(script, acc, -1, gas, append([]SignerAccount{{
 		Signer: transaction.Signer{
-			Account: from,
+			Account: acc.ScriptHash(),
 			Scopes:  transaction.CalledByEntry,
 		},
 		Account: acc,
@@ -148,11 +143,7 @@ func (c *Client) NEP11NDOwnerOf(tokenHash util.Uint160, tokenID []byte) (util.Ui
 // versions.
 func (c *Client) TransferNEP11D(acc *wallet.Account, to util.Uint160,
 	tokenHash util.Uint160, amount int64, tokenID []byte, data interface{}, gas int64, cosigners []SignerAccount) (util.Uint256, error) {
-	from, err := address.StringToUint160(acc.Address)
-	if err != nil {
-		return util.Uint256{}, fmt.Errorf("bad account address: %w", err)
-	}
-	tx, err := c.CreateNEP11TransferTx(acc, tokenHash, gas, cosigners, from, to, amount, tokenID, data)
+	tx, err := c.CreateNEP11TransferTx(acc, tokenHash, gas, cosigners, acc.ScriptHash(), to, amount, tokenID, data)
 	if err != nil {
 		return util.Uint256{}, err
 	}
diff --git a/pkg/rpcclient/nep17.go b/pkg/rpcclient/nep17.go
index e1b988976..aa73a8c28 100644
--- a/pkg/rpcclient/nep17.go
+++ b/pkg/rpcclient/nep17.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 
 	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
-	"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/util"
@@ -96,10 +95,7 @@ func (c *Client) CreateNEP17TransferTx(acc *wallet.Account, to util.Uint160,
 // be removed in future versions.
 func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64,
 	recipients []TransferTarget, cosigners []SignerAccount) (*transaction.Transaction, error) {
-	from, err := address.StringToUint160(acc.Address)
-	if err != nil {
-		return nil, fmt.Errorf("bad account address: %w", err)
-	}
+	from := acc.ScriptHash()
 	b := smartcontract.NewBuilder()
 	for i := range recipients {
 		b.InvokeWithAssert(recipients[i].Token, "transfer",
diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go
index 4de2a57c0..cacbc4ea9 100644
--- a/pkg/rpcclient/rpc.go
+++ b/pkg/rpcclient/rpc.go
@@ -827,10 +827,7 @@ func getSigners(sender *wallet.Account, cosigners []SignerAccount) ([]transactio
 		signers  []transaction.Signer
 		accounts []*wallet.Account
 	)
-	from, err := address.StringToUint160(sender.Address)
-	if err != nil {
-		return nil, nil, fmt.Errorf("bad sender account address: %v", err)
-	}
+	from := sender.ScriptHash()
 	s := transaction.Signer{
 		Account: from,
 		Scopes:  transaction.None,
@@ -875,10 +872,7 @@ func (c *Client) SignAndPushP2PNotaryRequest(mainTx *transaction.Transaction, fa
 	if err != nil {
 		return nil, fmt.Errorf("failed to get native Notary hash: %w", err)
 	}
-	from, err := address.StringToUint160(acc.Address)
-	if err != nil {
-		return nil, fmt.Errorf("bad account address: %v", err)
-	}
+	from := acc.ScriptHash()
 	signers := []transaction.Signer{{Account: notaryHash}, {Account: from}}
 	if fallbackSysFee < 0 {
 		result, err := c.InvokeScript(fallbackScript, signers)
diff --git a/pkg/services/notary/core_test.go b/pkg/services/notary/core_test.go
index 448f06b14..844aeab92 100644
--- a/pkg/services/notary/core_test.go
+++ b/pkg/services/notary/core_test.go
@@ -175,7 +175,7 @@ func TestNotary(t *testing.T) {
 				Scopes:  transaction.None,
 			},
 			{
-				Account: requester.PublicKey().GetScriptHash(),
+				Account: requester.ScriptHash(),
 				Scopes:  transaction.None,
 			},
 		}
@@ -721,9 +721,9 @@ func TestNotary(t *testing.T) {
 	requester1, _ := wallet.NewAccount()
 	requester2, _ := wallet.NewAccount()
 	amount := int64(100_0000_0000)
-	gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), bc.GetNotaryContractScriptHash(), amount, []interface{}{requester1.PublicKey().GetScriptHash(), int64(bc.BlockHeight() + 50)})
+	gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), bc.GetNotaryContractScriptHash(), amount, []interface{}{requester1.ScriptHash(), int64(bc.BlockHeight() + 50)})
 	e.CheckGASBalance(t, notaryHash, big.NewInt(amount))
-	gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), bc.GetNotaryContractScriptHash(), amount, []interface{}{requester2.PublicKey().GetScriptHash(), int64(bc.BlockHeight() + 50)})
+	gasValidatorInvoker.Invoke(t, true, "transfer", e.Validator.ScriptHash(), bc.GetNotaryContractScriptHash(), amount, []interface{}{requester2.ScriptHash(), int64(bc.BlockHeight() + 50)})
 	e.CheckGASBalance(t, notaryHash, big.NewInt(2*amount))
 
 	// create request for 2 standard signatures => main tx should be completed after the second request is added to the pool
diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go
index dc7fb1119..b07b09455 100644
--- a/pkg/services/rpcsrv/client_test.go
+++ b/pkg/services/rpcsrv/client_test.go
@@ -1033,11 +1033,6 @@ func TestSignAndPushP2PNotaryRequest(t *testing.T) {
 	})
 
 	require.NoError(t, c.Init())
-	t.Run("bad account address", func(t *testing.T) {
-		_, err := c.SignAndPushP2PNotaryRequest(nil, nil, 0, 0, 0, &wallet.Account{Address: "not-an-addr"}) //nolint:staticcheck // SA1019: c.SignAndPushP2PNotaryRequest is deprecated
-		require.NotNil(t, err)
-	})
-
 	t.Run("bad fallback script", func(t *testing.T) {
 		_, err := c.SignAndPushP2PNotaryRequest(nil, []byte{byte(opcode.ASSERT)}, -1, 0, 0, acc) //nolint:staticcheck // SA1019: c.SignAndPushP2PNotaryRequest is deprecated
 		require.NotNil(t, err)
diff --git a/pkg/services/stateroot/network.go b/pkg/services/stateroot/network.go
index e2eedcd8f..e91fb5100 100644
--- a/pkg/services/stateroot/network.go
+++ b/pkg/services/stateroot/network.go
@@ -90,7 +90,6 @@ func (s *service) trySendRoot(ir *incompleteRoot, acc *wallet.Account) {
 }
 
 func (s *service) sendValidatedRoot(r *state.MPTRoot, acc *wallet.Account) {
-	priv := acc.PrivateKey()
 	w := io.NewBufBinWriter()
 	m := NewMessage(RootT, r)
 	m.EncodeBinary(w.BinWriter)
@@ -98,13 +97,13 @@ func (s *service) sendValidatedRoot(r *state.MPTRoot, acc *wallet.Account) {
 		Category:        Category,
 		ValidBlockStart: r.Index,
 		ValidBlockEnd:   r.Index + rootValidEndInc,
-		Sender:          priv.GetScriptHash(),
+		Sender:          acc.ScriptHash(),
 		Data:            w.Bytes(),
 		Witness: transaction.Witness{
 			VerificationScript: acc.GetVerificationScript(),
 		},
 	}
-	sig := priv.SignHashable(uint32(s.Network), ep)
+	sig := acc.PrivateKey().SignHashable(uint32(s.Network), ep)
 	buf := io.NewBufBinWriter()
 	emit.Bytes(buf.BinWriter, sig)
 	ep.Witness.InvocationScript = buf.Bytes()
diff --git a/pkg/services/stateroot/validators.go b/pkg/services/stateroot/validators.go
index c0742d4f6..9bfe6e741 100644
--- a/pkg/services/stateroot/validators.go
+++ b/pkg/services/stateroot/validators.go
@@ -110,7 +110,7 @@ func (s *service) signAndSend(r *state.MPTRoot) error {
 		Category:        Category,
 		ValidBlockStart: r.Index,
 		ValidBlockEnd:   r.Index + voteValidEndInc,
-		Sender:          acc.PrivateKey().GetScriptHash(),
+		Sender:          acc.ScriptHash(),
 		Data:            w.Bytes(),
 		Witness: transaction.Witness{
 			VerificationScript: acc.GetVerificationScript(),
diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go
index 15df77eac..bd436d6ab 100644
--- a/pkg/wallet/account.go
+++ b/pkg/wallet/account.go
@@ -20,6 +20,9 @@ type Account struct {
 	// NEO private key.
 	privateKey *keys.PrivateKey
 
+	// Script hash corresponding to the Address.
+	scriptHash util.Uint160
+
 	// NEO public address.
 	Address string `json:"address"`
 
@@ -80,8 +83,6 @@ func (a *Account) SignTx(net netmode.Magic, t *transaction.Transaction) error {
 	var (
 		haveAcc bool
 		pos     int
-		accHash util.Uint160
-		err     error
 	)
 	if a.Locked {
 		return errors.New("account is locked")
@@ -89,12 +90,8 @@ func (a *Account) SignTx(net netmode.Magic, t *transaction.Transaction) error {
 	if a.Contract == nil {
 		return errors.New("account has no contract")
 	}
-	accHash, err = address.StringToUint160(a.Address)
-	if err != nil {
-		return err
-	}
 	for i := range t.Signers {
-		if t.Signers[i].Account.Equals(accHash) {
+		if t.Signers[i].Account.Equals(a.ScriptHash()) {
 			haveAcc = true
 			pos = i
 			break
@@ -184,6 +181,16 @@ func (a *Account) PublicKey() *keys.PublicKey {
 	return a.privateKey.PublicKey()
 }
 
+// ScriptHash returns the script hash (account) that the Account.Address is
+// derived from. It never returns an error, so if this Account has an invalid
+// Address you'll just get a zero script hash.
+func (a *Account) ScriptHash() util.Uint160 {
+	if a.scriptHash.Equals(util.Uint160{}) {
+		a.scriptHash, _ = address.StringToUint160(a.Address)
+	}
+	return a.scriptHash
+}
+
 // Close cleans up the private key used by Account and disassociates it from
 // Account. The Account can no longer sign anything after this call, but Decrypt
 // can make it usable again.
@@ -243,7 +250,8 @@ func (a *Account) ConvertMultisig(m int, pubs []*keys.PublicKey) error {
 		return err
 	}
 
-	a.Address = address.Uint160ToString(hash.Hash160(script))
+	a.scriptHash = hash.Hash160(script)
+	a.Address = address.Uint160ToString(a.scriptHash)
 	a.Contract = &Contract{
 		Script:     script,
 		Parameters: getContractParams(m),
@@ -255,11 +263,11 @@ func (a *Account) ConvertMultisig(m int, pubs []*keys.PublicKey) error {
 // NewAccountFromPrivateKey creates a wallet from the given PrivateKey.
 func NewAccountFromPrivateKey(p *keys.PrivateKey) *Account {
 	pubKey := p.PublicKey()
-	pubAddr := p.Address()
 
 	a := &Account{
 		privateKey: p,
-		Address:    pubAddr,
+		scriptHash: p.GetScriptHash(),
+		Address:    p.Address(),
 		Contract: &Contract{
 			Script:     pubKey.GetVerificationScript(),
 			Parameters: getContractParams(1),
diff --git a/pkg/wallet/account_test.go b/pkg/wallet/account_test.go
index 88118176e..714aa227e 100644
--- a/pkg/wallet/account_test.go
+++ b/pkg/wallet/account_test.go
@@ -9,6 +9,7 @@ import (
 	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
 	"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
 	"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/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -18,6 +19,7 @@ func TestNewAccount(t *testing.T) {
 	acc, err := NewAccount()
 	require.NoError(t, err)
 	require.NotNil(t, acc)
+	require.Equal(t, acc.Address, address.Uint160ToString(acc.ScriptHash()))
 }
 
 func TestDecryptAccount(t *testing.T) {
diff --git a/pkg/wallet/wallet_test.go b/pkg/wallet/wallet_test.go
index 7d155e9c4..9de064319 100644
--- a/pkg/wallet/wallet_test.go
+++ b/pkg/wallet/wallet_test.go
@@ -102,6 +102,7 @@ func TestSave(t *testing.T) {
 		require.NoError(t, err)
 		require.Equal(t, 2, len(w2.Accounts))
 		require.NoError(t, w2.Accounts[1].Decrypt("pass", w2.Scrypt))
+		_ = w2.Accounts[1].ScriptHash() // openedWallet has it for acc 1.
 		require.Equal(t, openedWallet.Accounts, w2.Accounts)
 	})
 }