962b161652
This was recently added in neotest, but working with the real RPC is still not enjoyable. This commit extends `wallet.Account` with invocation script builder to aid network fee calculations and signing. Signed-off-by: Evgenii Stratonikov <fyfyrchik@runbox.com>
339 lines
9.8 KiB
Go
339 lines
9.8 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/io"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
"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
|
|
|
|
// Script hash corresponding to the Address.
|
|
scriptHash util.Uint160
|
|
|
|
// 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"`
|
|
|
|
// InvocationBuilder returns invocation script for deployed contracts.
|
|
// In case contract is not deployed or has 0 arguments, this field is ignored.
|
|
// It might be executed on a partially formed tx, and is primarily needed to properly
|
|
// calculate network fee for complex contract signers.
|
|
InvocationBuilder func(tx *transaction.Transaction) ([]byte, error) `json:"-"`
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// NewContractAccount creates a contract account belonging to some deployed contract.
|
|
// SignTx can be called on this account with no error and will create invocation script,
|
|
// which puts provided arguments on stack for use in `verify`.
|
|
func NewContractAccount(hash util.Uint160, args ...any) *Account {
|
|
return &Account{
|
|
Address: address.Uint160ToString(hash),
|
|
Contract: &Contract{
|
|
Parameters: make([]ContractParam, len(args)),
|
|
Deployed: true,
|
|
InvocationBuilder: func(tx *transaction.Transaction) ([]byte, error) {
|
|
w := io.NewBufBinWriter()
|
|
for i := range args {
|
|
emit.Any(w.BinWriter, args[i])
|
|
}
|
|
if w.Err != nil {
|
|
return nil, w.Err
|
|
}
|
|
return w.Bytes(), 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
|
|
)
|
|
if a.Locked {
|
|
return errors.New("account is locked")
|
|
}
|
|
if a.Contract == nil {
|
|
return errors.New("account has no contract")
|
|
}
|
|
for i := range t.Signers {
|
|
if t.Signers[i].Account.Equals(a.ScriptHash()) {
|
|
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 a.Contract.Deployed && a.Contract.InvocationBuilder != nil {
|
|
invoc, err := a.Contract.InvocationBuilder(t)
|
|
t.Scripts[pos].InvocationScript = invoc
|
|
return err
|
|
}
|
|
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), keys.SignatureLen}, 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
|
|
}
|
|
|
|
// SignHashable signs the given Hashable item and returns the signature. If this
|
|
// account can't sign (CanSign() returns false) nil is returned.
|
|
func (a *Account) SignHashable(net netmode.Magic, item hash.Hashable) []byte {
|
|
if !a.CanSign() {
|
|
return nil
|
|
}
|
|
return a.privateKey.SignHashable(uint32(net), item)
|
|
}
|
|
|
|
// 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. After the decryption Account can be used to sign
|
|
// things unless it's locked. Don't decrypt the key unless you want to sign
|
|
// something and don't forget to call Close after use for maximum safety.
|
|
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 if it's unlocked.
|
|
// Please be very careful when using it, do not copy its contents and do not
|
|
// keep a pointer to it unless you absolutely need to. Most of the time you can
|
|
// use other methods (PublicKey, ScriptHash, SignHashable) depending on your
|
|
// needs and it'll be safer this way.
|
|
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()
|
|
}
|
|
|
|
// 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.
|
|
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.scriptHash = hash.Hash160(script)
|
|
a.Address = address.Uint160ToString(a.scriptHash)
|
|
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()
|
|
|
|
a := &Account{
|
|
privateKey: p,
|
|
scriptHash: p.GetScriptHash(),
|
|
Address: p.Address(),
|
|
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
|
|
}
|