package wallet import ( "bytes" "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 key. publicKey []byte // 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 } a.publicKey = a.privateKey.PublicKey().Bytes() 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 } // 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 { 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. func NewAccountFromPrivateKey(p *keys.PrivateKey) *Account { pubKey := p.PublicKey() pubAddr := p.Address() a := &Account{ publicKey: pubKey.Bytes(), 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 }