neoneo-go/pkg/wallet/account.go
Roman Khimov 8d33206bb8 *: don't get private key from account if just public one is needed
Add PublicKey() API to the Account and use it as appropriate, avoid creating
additional references to the private key.
2022-09-02 14:43:28 +03:00

280 lines
7.4 KiB
Go

package wallet
import (
"errors"
"fmt"
"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/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/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
)
// Account represents a NEO account. It holds the private and the public key
// along with some metadata.
type Account struct {
// NEO private key.
privateKey *keys.PrivateKey
// NEO public address.
Address string `json:"address"`
// Encrypted WIF of the account also known as the key.
EncryptedWIF string `json:"key"`
// Label is a label the user had made for this account.
Label string `json:"label"`
// Contract is a Contract object which describes the details of the contract.
// This field can be null (for watch-only address).
Contract *Contract `json:"contract"`
// Indicates whether the account is locked by the user.
// the client shouldn't spend the funds in a locked account.
Locked bool `json:"lock"`
// Indicates whether the account is the default change account.
Default bool `json:"isDefault"`
}
// Contract represents a subset of the smartcontract to embed in the
// Account so it's NEP-6 compliant.
type Contract struct {
// Script of the contract deployed on the blockchain.
Script []byte `json:"script"`
// A list of parameters used deploying this contract.
Parameters []ContractParam `json:"parameters"`
// Indicates whether the contract has been deployed to the blockchain.
Deployed bool `json:"deployed"`
}
// ContractParam is a descriptor of a contract parameter
// containing type and optional name.
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)
}
// NewAccount creates a new Account with a random generated PrivateKey.
func NewAccount() (*Account, error) {
priv, err := keys.NewPrivateKey()
if err != nil {
return nil, err
}
return NewAccountFromPrivateKey(priv), nil
}
// SignTx signs transaction t and updates it's Witnesses.
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")
}
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) {
haveAcc = true
pos = i
break
}
}
if !haveAcc {
return errors.New("transaction is not signed by this account")
}
if len(t.Scripts) < pos {
return errors.New("transaction is not yet signed by the previous signer")
}
if len(t.Scripts) == pos {
t.Scripts = append(t.Scripts, transaction.Witness{
VerificationScript: a.Contract.Script, // Can be nil for deployed contract.
})
}
if len(a.Contract.Parameters) == 0 {
return nil
}
if a.privateKey == nil {
return errors.New("account key is not available (need to decrypt?)")
}
sign := a.privateKey.SignHashable(uint32(net), t)
invoc := append([]byte{byte(opcode.PUSHDATA1), 64}, sign...)
if len(a.Contract.Parameters) == 1 {
t.Scripts[pos].InvocationScript = invoc
} else {
t.Scripts[pos].InvocationScript = append(t.Scripts[pos].InvocationScript, invoc...)
}
return nil
}
// CanSign returns true when account is not locked and has a decrypted private
// key inside, so it's ready to create real signatures.
func (a *Account) CanSign() bool {
return !a.Locked && a.privateKey != nil
}
// GetVerificationScript returns account's verification script.
func (a *Account) GetVerificationScript() []byte {
if a.Contract != nil {
return a.Contract.Script
}
return a.privateKey.PublicKey().GetVerificationScript()
}
// Decrypt decrypts the EncryptedWIF with the given passphrase returning error
// if anything goes wrong.
func (a *Account) Decrypt(passphrase string, scrypt keys.ScryptParams) error {
var err error
if a.EncryptedWIF == "" {
return errors.New("no encrypted wif in the account")
}
a.privateKey, err = keys.NEP2Decrypt(a.EncryptedWIF, passphrase, scrypt)
if err != nil {
return err
}
return nil
}
// Encrypt encrypts the wallet's PrivateKey with the given passphrase
// under the NEP-2 standard.
func (a *Account) Encrypt(passphrase string, scrypt keys.ScryptParams) error {
wif, err := keys.NEP2Encrypt(a.privateKey, passphrase, scrypt)
if err != nil {
return err
}
a.EncryptedWIF = wif
return nil
}
// PrivateKey returns private key corresponding to the account.
func (a *Account) PrivateKey() *keys.PrivateKey {
return a.privateKey
}
// PublicKey returns the public key associated with the private key corresponding to
// the account. It can return nil if account is locked (use CanSign to check).
func (a *Account) PublicKey() *keys.PublicKey {
if !a.CanSign() {
return nil
}
return a.privateKey.PublicKey()
}
// 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.
func (a *Account) Close() {
if a.privateKey == nil {
return
}
a.privateKey.Destroy()
a.privateKey = nil
}
// NewAccountFromWIF creates a new Account from the given WIF.
func NewAccountFromWIF(wif string) (*Account, error) {
privKey, err := keys.NewPrivateKeyFromWIF(wif)
if err != nil {
return nil, err
}
return NewAccountFromPrivateKey(privKey), nil
}
// NewAccountFromEncryptedWIF creates a new Account from the given encrypted WIF.
func NewAccountFromEncryptedWIF(wif string, pass string, scrypt keys.ScryptParams) (*Account, error) {
priv, err := keys.NEP2Decrypt(wif, pass, scrypt)
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 {
if a.Locked {
return errors.New("account is locked")
}
if a.privateKey == nil {
return errors.New("account key is not available (need to decrypt?)")
}
var found bool
accKey := a.privateKey.PublicKey()
for i := range pubs {
if accKey.Equal(pubs[i]) {
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()
pubAddr := p.Address()
a := &Account{
privateKey: p,
Address: pubAddr,
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
}