forked from TrueCloudLab/neoneo-go
2012c56031
Closes #3177 Signed-off-by: Evgeny Baydakov <e.bajdakov@gmail.com>
252 lines
6.4 KiB
Go
252 lines
6.4 KiB
Go
package wallet
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"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/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
|
)
|
|
|
|
const (
|
|
// The current version of neo-go wallet implementations.
|
|
walletVersion = "1.0"
|
|
)
|
|
|
|
var (
|
|
// ErrPathIsEmpty appears if wallet was created without linking to file system path,
|
|
// for instance with [NewInMemoryWallet] or [NewWalletFromBytes].
|
|
// Despite this, there was an attempt to save it via [Wallet.Save] or [Wallet.SavePretty] without [Wallet.SetPath].
|
|
ErrPathIsEmpty = errors.New("path is empty")
|
|
)
|
|
|
|
// Wallet represents a NEO (NEP-2, NEP-6) compliant wallet.
|
|
type Wallet struct {
|
|
// Version of the wallet, used for later upgrades.
|
|
Version string `json:"version"`
|
|
|
|
// A list of accounts which describes the details of each account
|
|
// in the wallet.
|
|
Accounts []*Account `json:"accounts"`
|
|
|
|
Scrypt keys.ScryptParams `json:"scrypt"`
|
|
|
|
// Extra metadata can be used for storing arbitrary data.
|
|
// This field can be empty.
|
|
Extra Extra `json:"extra"`
|
|
|
|
// Path where the wallet file is located..
|
|
path string
|
|
}
|
|
|
|
// Extra stores imported token contracts.
|
|
type Extra struct {
|
|
// Tokens is a list of imported token contracts.
|
|
Tokens []*Token
|
|
}
|
|
|
|
// NewWallet creates a new NEO wallet at the given location.
|
|
func NewWallet(location string) (*Wallet, error) {
|
|
file, err := os.Create(location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
return newWallet(file), nil
|
|
}
|
|
|
|
// NewInMemoryWallet creates a new NEO wallet without linking to the read file on file system.
|
|
// If wallet required to be written to the file system, [Wallet.SetPath] should be used to set the path.
|
|
func NewInMemoryWallet() *Wallet {
|
|
return newWallet(nil)
|
|
}
|
|
|
|
// NewWalletFromFile creates a Wallet from the given wallet file path.
|
|
func NewWalletFromFile(path string) (*Wallet, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open wallet: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
wall := &Wallet{
|
|
path: file.Name(),
|
|
}
|
|
if err := json.NewDecoder(file).Decode(wall); err != nil {
|
|
return nil, fmt.Errorf("unmarshal wallet: %w", err)
|
|
}
|
|
return wall, nil
|
|
}
|
|
|
|
// NewWalletFromBytes creates a [Wallet] from the given byte slice.
|
|
// Parameter wallet contains JSON representation of wallet, see [Wallet.JSON] for details.
|
|
//
|
|
// NewWalletFromBytes constructor doesn't set wallet's path. If you want to save the wallet to file system,
|
|
// use [Wallet.SetPath].
|
|
func NewWalletFromBytes(wallet []byte) (*Wallet, error) {
|
|
wall := &Wallet{}
|
|
if err := json.NewDecoder(bytes.NewReader(wallet)).Decode(wall); err != nil {
|
|
return nil, fmt.Errorf("unmarshal wallet: %w", err)
|
|
}
|
|
|
|
return wall, nil
|
|
}
|
|
|
|
func newWallet(rw io.ReadWriter) *Wallet {
|
|
var path string
|
|
if f, ok := rw.(*os.File); ok {
|
|
path = f.Name()
|
|
}
|
|
return &Wallet{
|
|
Version: walletVersion,
|
|
Accounts: []*Account{},
|
|
Scrypt: keys.NEP2ScryptParams(),
|
|
path: path,
|
|
}
|
|
}
|
|
|
|
// CreateAccount generates a new account for the end user and encrypts
|
|
// the private key with the given passphrase.
|
|
func (w *Wallet) CreateAccount(name, passphrase string) error {
|
|
acc, err := NewAccount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
acc.Label = name
|
|
if err := acc.Encrypt(passphrase, w.Scrypt); err != nil {
|
|
return err
|
|
}
|
|
w.AddAccount(acc)
|
|
return w.Save()
|
|
}
|
|
|
|
// AddAccount adds an existing Account to the wallet.
|
|
func (w *Wallet) AddAccount(acc *Account) {
|
|
w.Accounts = append(w.Accounts, acc)
|
|
}
|
|
|
|
// RemoveAccount removes an Account with the specified addr
|
|
// from the wallet.
|
|
func (w *Wallet) RemoveAccount(addr string) error {
|
|
for i, acc := range w.Accounts {
|
|
if acc.Address == addr {
|
|
copy(w.Accounts[i:], w.Accounts[i+1:])
|
|
w.Accounts = w.Accounts[:len(w.Accounts)-1]
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("account wasn't found")
|
|
}
|
|
|
|
// AddToken adds a new token to a wallet.
|
|
func (w *Wallet) AddToken(tok *Token) {
|
|
w.Extra.Tokens = append(w.Extra.Tokens, tok)
|
|
}
|
|
|
|
// RemoveToken removes the token with the specified hash from the wallet.
|
|
func (w *Wallet) RemoveToken(h util.Uint160) error {
|
|
for i, tok := range w.Extra.Tokens {
|
|
if tok.Hash.Equals(h) {
|
|
copy(w.Extra.Tokens[i:], w.Extra.Tokens[i+1:])
|
|
w.Extra.Tokens = w.Extra.Tokens[:len(w.Extra.Tokens)-1]
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("token wasn't found")
|
|
}
|
|
|
|
// Path returns the location of the wallet on the filesystem.
|
|
func (w *Wallet) Path() string {
|
|
return w.path
|
|
}
|
|
|
|
// SetPath sets the location of the wallet on the filesystem.
|
|
func (w *Wallet) SetPath(path string) {
|
|
w.path = path
|
|
}
|
|
|
|
// Save saves the wallet data to the file located at the path that was either provided
|
|
// via [NewWalletFromFile] constructor or via [Wallet.SetPath].
|
|
//
|
|
// Returns [ErrPathIsEmpty] if wallet path is not set. See [Wallet.SetPath].
|
|
func (w *Wallet) Save() error {
|
|
data, err := json.Marshal(w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return w.writeRaw(data)
|
|
}
|
|
|
|
// SavePretty saves the wallet in a beautiful JSON.
|
|
//
|
|
// Returns [ErrPathIsEmpty] if wallet path is not set. See [Wallet.SetPath].
|
|
func (w *Wallet) SavePretty() error {
|
|
data, err := json.MarshalIndent(w, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return w.writeRaw(data)
|
|
}
|
|
|
|
func (w *Wallet) writeRaw(data []byte) error {
|
|
if w.path == "" {
|
|
return ErrPathIsEmpty
|
|
}
|
|
|
|
return os.WriteFile(w.path, data, 0644)
|
|
}
|
|
|
|
// JSON outputs a pretty JSON representation of the wallet.
|
|
func (w *Wallet) JSON() ([]byte, error) {
|
|
return json.MarshalIndent(w, " ", " ")
|
|
}
|
|
|
|
// Close closes all Wallet accounts making them incapable of signing anything
|
|
// (unless they're decrypted again). It's not doing anything to the underlying
|
|
// wallet file.
|
|
func (w *Wallet) Close() {
|
|
for _, acc := range w.Accounts {
|
|
acc.Close()
|
|
}
|
|
}
|
|
|
|
// GetAccount returns an account corresponding to the provided scripthash.
|
|
func (w *Wallet) GetAccount(h util.Uint160) *Account {
|
|
addr := address.Uint160ToString(h)
|
|
for _, acc := range w.Accounts {
|
|
if acc.Address == addr {
|
|
return acc
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetChangeAddress returns the default address to send transaction's change to.
|
|
func (w *Wallet) GetChangeAddress() util.Uint160 {
|
|
var res util.Uint160
|
|
var acc *Account
|
|
|
|
for i := range w.Accounts {
|
|
if acc == nil || w.Accounts[i].Default {
|
|
if w.Accounts[i].Contract != nil && vm.IsSignatureContract(w.Accounts[i].Contract.Script) {
|
|
acc = w.Accounts[i]
|
|
if w.Accounts[i].Default {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if acc != nil {
|
|
res = acc.Contract.ScriptHash()
|
|
}
|
|
return res
|
|
}
|