github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/wallet/account.go (about) 1 package wallet 2 3 import ( 4 "errors" 5 "fmt" 6 7 "github.com/nspcc-dev/neo-go/pkg/config/netmode" 8 "github.com/nspcc-dev/neo-go/pkg/core/transaction" 9 "github.com/nspcc-dev/neo-go/pkg/crypto/hash" 10 "github.com/nspcc-dev/neo-go/pkg/crypto/keys" 11 "github.com/nspcc-dev/neo-go/pkg/encoding/address" 12 "github.com/nspcc-dev/neo-go/pkg/io" 13 "github.com/nspcc-dev/neo-go/pkg/smartcontract" 14 "github.com/nspcc-dev/neo-go/pkg/util" 15 "github.com/nspcc-dev/neo-go/pkg/vm/emit" 16 "github.com/nspcc-dev/neo-go/pkg/vm/opcode" 17 ) 18 19 // Account represents a NEO account. It holds the private and the public key 20 // along with some metadata. 21 type Account struct { 22 // NEO private key. 23 privateKey *keys.PrivateKey 24 25 // Script hash corresponding to the Address. 26 scriptHash util.Uint160 27 28 // NEO public address. 29 Address string `json:"address"` 30 31 // Encrypted WIF of the account also known as the key. 32 EncryptedWIF string `json:"key"` 33 34 // Label is a label the user had made for this account. 35 Label string `json:"label"` 36 37 // Contract is a Contract object which describes the details of the contract. 38 // This field can be null (for watch-only address). 39 Contract *Contract `json:"contract"` 40 41 // Indicates whether the account is locked by the user. 42 // the client shouldn't spend the funds in a locked account. 43 Locked bool `json:"lock"` 44 45 // Indicates whether the account is the default change account. 46 Default bool `json:"isDefault"` 47 } 48 49 // Contract represents a subset of the smartcontract to embed in the 50 // Account so it's NEP-6 compliant. 51 type Contract struct { 52 // Script of the contract deployed on the blockchain. 53 Script []byte `json:"script"` 54 55 // A list of parameters used deploying this contract. 56 Parameters []ContractParam `json:"parameters"` 57 58 // Indicates whether the contract has been deployed to the blockchain. 59 Deployed bool `json:"deployed"` 60 61 // InvocationBuilder returns invocation script for deployed contracts. 62 // In case contract is not deployed or has 0 arguments, this field is ignored. 63 // It might be executed on a partially formed tx, and is primarily needed to properly 64 // calculate network fee for complex contract signers. 65 InvocationBuilder func(tx *transaction.Transaction) ([]byte, error) `json:"-"` 66 } 67 68 // ContractParam is a descriptor of a contract parameter 69 // containing type and optional name. 70 type ContractParam struct { 71 Name string `json:"name"` 72 Type smartcontract.ParamType `json:"type"` 73 } 74 75 // ScriptHash returns the hash of contract's script. 76 func (c Contract) ScriptHash() util.Uint160 { 77 return hash.Hash160(c.Script) 78 } 79 80 // NewAccount creates a new Account with a random generated PrivateKey. 81 func NewAccount() (*Account, error) { 82 priv, err := keys.NewPrivateKey() 83 if err != nil { 84 return nil, err 85 } 86 return NewAccountFromPrivateKey(priv), nil 87 } 88 89 // NewContractAccount creates a contract account belonging to some deployed contract. 90 // SignTx can be called on this account with no error and will create invocation script, 91 // which puts provided arguments on stack for use in `verify`. 92 func NewContractAccount(hash util.Uint160, args ...any) *Account { 93 return &Account{ 94 Address: address.Uint160ToString(hash), 95 Contract: &Contract{ 96 Parameters: make([]ContractParam, len(args)), 97 Deployed: true, 98 InvocationBuilder: func(tx *transaction.Transaction) ([]byte, error) { 99 w := io.NewBufBinWriter() 100 for i := range args { 101 emit.Any(w.BinWriter, args[i]) 102 } 103 if w.Err != nil { 104 return nil, w.Err 105 } 106 return w.Bytes(), nil 107 }, 108 }, 109 } 110 } 111 112 // SignTx signs transaction t and updates it's Witnesses. 113 func (a *Account) SignTx(net netmode.Magic, t *transaction.Transaction) error { 114 var ( 115 haveAcc bool 116 pos int 117 ) 118 if a.Locked { 119 return errors.New("account is locked") 120 } 121 if a.Contract == nil { 122 return errors.New("account has no contract") 123 } 124 for i := range t.Signers { 125 if t.Signers[i].Account.Equals(a.ScriptHash()) { 126 haveAcc = true 127 pos = i 128 break 129 } 130 } 131 if !haveAcc { 132 return errors.New("transaction is not signed by this account") 133 } 134 if len(t.Scripts) < pos { 135 return errors.New("transaction is not yet signed by the previous signer") 136 } 137 if len(t.Scripts) == pos { 138 t.Scripts = append(t.Scripts, transaction.Witness{ 139 VerificationScript: a.Contract.Script, // Can be nil for deployed contract. 140 }) 141 } 142 if a.Contract.Deployed && a.Contract.InvocationBuilder != nil { 143 invoc, err := a.Contract.InvocationBuilder(t) 144 t.Scripts[pos].InvocationScript = invoc 145 return err 146 } 147 if len(a.Contract.Parameters) == 0 { 148 return nil 149 } 150 if a.privateKey == nil { 151 return errors.New("account key is not available (need to decrypt?)") 152 } 153 sign := a.privateKey.SignHashable(uint32(net), t) 154 155 invoc := append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, sign...) 156 if len(a.Contract.Parameters) == 1 { 157 t.Scripts[pos].InvocationScript = invoc 158 } else { 159 t.Scripts[pos].InvocationScript = append(t.Scripts[pos].InvocationScript, invoc...) 160 } 161 162 return nil 163 } 164 165 // SignHashable signs the given Hashable item and returns the signature. If this 166 // account can't sign (CanSign() returns false) nil is returned. 167 func (a *Account) SignHashable(net netmode.Magic, item hash.Hashable) []byte { 168 if !a.CanSign() { 169 return nil 170 } 171 return a.privateKey.SignHashable(uint32(net), item) 172 } 173 174 // CanSign returns true when account is not locked and has a decrypted private 175 // key inside, so it's ready to create real signatures. 176 func (a *Account) CanSign() bool { 177 return !a.Locked && a.privateKey != nil 178 } 179 180 // GetVerificationScript returns account's verification script. 181 func (a *Account) GetVerificationScript() []byte { 182 if a.Contract != nil { 183 return a.Contract.Script 184 } 185 return a.privateKey.PublicKey().GetVerificationScript() 186 } 187 188 // Decrypt decrypts the EncryptedWIF with the given passphrase returning error 189 // if anything goes wrong. After the decryption Account can be used to sign 190 // things unless it's locked. Don't decrypt the key unless you want to sign 191 // something and don't forget to call Close after use for maximum safety. 192 func (a *Account) Decrypt(passphrase string, scrypt keys.ScryptParams) error { 193 var err error 194 195 if a.EncryptedWIF == "" { 196 return errors.New("no encrypted wif in the account") 197 } 198 a.privateKey, err = keys.NEP2Decrypt(a.EncryptedWIF, passphrase, scrypt) 199 if err != nil { 200 return err 201 } 202 203 return nil 204 } 205 206 // Encrypt encrypts the wallet's PrivateKey with the given passphrase 207 // under the NEP-2 standard. 208 func (a *Account) Encrypt(passphrase string, scrypt keys.ScryptParams) error { 209 wif, err := keys.NEP2Encrypt(a.privateKey, passphrase, scrypt) 210 if err != nil { 211 return err 212 } 213 a.EncryptedWIF = wif 214 return nil 215 } 216 217 // PrivateKey returns private key corresponding to the account if it's unlocked. 218 // Please be very careful when using it, do not copy its contents and do not 219 // keep a pointer to it unless you absolutely need to. Most of the time you can 220 // use other methods (PublicKey, ScriptHash, SignHashable) depending on your 221 // needs and it'll be safer this way. 222 func (a *Account) PrivateKey() *keys.PrivateKey { 223 return a.privateKey 224 } 225 226 // PublicKey returns the public key associated with the private key corresponding to 227 // the account. It can return nil if account is locked (use CanSign to check). 228 func (a *Account) PublicKey() *keys.PublicKey { 229 if !a.CanSign() { 230 return nil 231 } 232 return a.privateKey.PublicKey() 233 } 234 235 // ScriptHash returns the script hash (account) that the Account.Address is 236 // derived from. It never returns an error, so if this Account has an invalid 237 // Address you'll just get a zero script hash. 238 func (a *Account) ScriptHash() util.Uint160 { 239 if a.scriptHash.Equals(util.Uint160{}) { 240 a.scriptHash, _ = address.StringToUint160(a.Address) 241 } 242 return a.scriptHash 243 } 244 245 // Close cleans up the private key used by Account and disassociates it from 246 // Account. The Account can no longer sign anything after this call, but Decrypt 247 // can make it usable again. 248 func (a *Account) Close() { 249 if a.privateKey == nil { 250 return 251 } 252 a.privateKey.Destroy() 253 a.privateKey = nil 254 } 255 256 // NewAccountFromWIF creates a new Account from the given WIF. 257 func NewAccountFromWIF(wif string) (*Account, error) { 258 privKey, err := keys.NewPrivateKeyFromWIF(wif) 259 if err != nil { 260 return nil, err 261 } 262 return NewAccountFromPrivateKey(privKey), nil 263 } 264 265 // NewAccountFromEncryptedWIF creates a new Account from the given encrypted WIF. 266 func NewAccountFromEncryptedWIF(wif string, pass string, scrypt keys.ScryptParams) (*Account, error) { 267 priv, err := keys.NEP2Decrypt(wif, pass, scrypt) 268 if err != nil { 269 return nil, err 270 } 271 272 a := NewAccountFromPrivateKey(priv) 273 a.EncryptedWIF = wif 274 275 return a, nil 276 } 277 278 // ConvertMultisig sets a's contract to multisig contract with m sufficient signatures. 279 func (a *Account) ConvertMultisig(m int, pubs []*keys.PublicKey) error { 280 if a.Locked { 281 return errors.New("account is locked") 282 } 283 if a.privateKey == nil { 284 return errors.New("account key is not available (need to decrypt?)") 285 } 286 accKey := a.privateKey.PublicKey() 287 return a.ConvertMultisigEncrypted(accKey, m, pubs) 288 } 289 290 // ConvertMultisigEncrypted sets a's contract to an encrypted multisig contract 291 // with m sufficient signatures. The encrypted private key is not modified and 292 // remains the same. 293 func (a *Account) ConvertMultisigEncrypted(accKey *keys.PublicKey, m int, pubs []*keys.PublicKey) error { 294 var found bool 295 for i := range pubs { 296 if accKey.Equal(pubs[i]) { 297 found = true 298 break 299 } 300 } 301 302 if !found { 303 return errors.New("own public key was not found among multisig keys") 304 } 305 306 script, err := smartcontract.CreateMultiSigRedeemScript(m, pubs) 307 if err != nil { 308 return err 309 } 310 311 a.scriptHash = hash.Hash160(script) 312 a.Address = address.Uint160ToString(a.scriptHash) 313 a.Contract = &Contract{ 314 Script: script, 315 Parameters: getContractParams(m), 316 } 317 318 return nil 319 } 320 321 // NewAccountFromPrivateKey creates a wallet from the given PrivateKey. 322 func NewAccountFromPrivateKey(p *keys.PrivateKey) *Account { 323 pubKey := p.PublicKey() 324 325 a := &Account{ 326 privateKey: p, 327 scriptHash: p.GetScriptHash(), 328 Address: p.Address(), 329 Contract: &Contract{ 330 Script: pubKey.GetVerificationScript(), 331 Parameters: getContractParams(1), 332 }, 333 } 334 335 return a 336 } 337 338 func getContractParams(n int) []ContractParam { 339 params := make([]ContractParam, n) 340 for i := range params { 341 params[i].Name = fmt.Sprintf("parameter%d", i) 342 params[i].Type = smartcontract.SignatureType 343 } 344 345 return params 346 }