Skip to content

Commit

Permalink
accounts, cmd, eth, internal, miner, node: wallets and HD APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
karalabe committed Feb 13, 2017
1 parent b3c0e9d commit fad5eb0
Show file tree
Hide file tree
Showing 23 changed files with 1,502 additions and 603 deletions.
256 changes: 111 additions & 145 deletions accounts/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,162 +18,128 @@
package accounts

import (
"encoding/json"
"errors"
"math/big"
"reflect"
"sync"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
)

// ErrUnknownAccount is returned for any requested operation for which no backend
// provides the specified account.
var ErrUnknownAccount = errors.New("unknown account")

// ErrNotSupported is returned when an operation is requested from an account
// backend that it does not support.
var ErrNotSupported = errors.New("not supported")

// Account represents a stored key.
// When used as an argument, it selects a unique key to act on.
// Account represents an Ethereum account located at a specific location defined
// by the optional URL field.
type Account struct {
Address common.Address // Ethereum account address derived from the key
URL string // Optional resource locator within a backend
backend Backend // Backend where this account originates from
}

func (acc *Account) MarshalJSON() ([]byte, error) {
return []byte(`"` + acc.Address.Hex() + `"`), nil
}

func (acc *Account) UnmarshalJSON(raw []byte) error {
return json.Unmarshal(raw, &acc.Address)
}

// Manager is an overarching account manager that can communicate with various
// backends for signing transactions.
type Manager struct {
backends []Backend // List of currently registered backends (ordered by registration)
index map[reflect.Type]Backend // Set of currently registered backends
lock sync.RWMutex
}

// NewManager creates a generic account manager to sign transaction via various
// supported backends.
func NewManager(backends ...Backend) *Manager {
am := &Manager{
backends: backends,
index: make(map[reflect.Type]Backend),
}
for _, backend := range backends {
am.index[reflect.TypeOf(backend)] = backend
}
return am
}

// Backend retrieves the backend with the given type from the account manager.
func (am *Manager) Backend(backend reflect.Type) Backend {
return am.index[backend]
Address common.Address `json:"address"` // Ethereum account address derived from the key
URL string `json:"url"` // Optional resource locator within a backend
}

// Accounts returns all signer accounts registered under this account manager.
func (am *Manager) Accounts() []Account {
am.lock.RLock()
defer am.lock.RUnlock()

var all []Account
for _, backend := range am.backends { // TODO(karalabe): cache these after subscriptions are in
accounts := backend.Accounts()
for i := 0; i < len(accounts); i++ {
accounts[i].backend = backend
}
all = append(all, accounts...)
}
return all
// Wallet represents a software or hardware wallet that might contain one or more
// accounts (derived from the same seed).
type Wallet interface {
// Type retrieves a textual representation of the type of the wallet.
Type() string

// URL retrieves the canonical path under which this wallet is reachable. It is
// user by upper layers to define a sorting order over all wallets from multiple
// backends.
URL() string

// Status returns a textual status to aid the user in the current state of the
// wallet.
Status() string

// Open initializes access to a wallet instance. It is not meant to unlock or
// decrypt account keys, rather simply to establish a connection to hardware
// wallets and/or to access derivation seeds.
//
// The passphrase parameter may or may not be used by the implementation of a
// particular wallet instance. The reason there is no passwordless open method
// is to strive towards a uniform wallet handling, oblivious to the different
// backend providers.
//
// Please note, if you open a wallet, you must close it to release any allocated
// resources (especially important when working with hardware wallets).
Open(passphrase string) error

// Close releases any resources held by an open wallet instance.
Close() error

// Accounts retrieves the list of signing accounts the wallet is currently aware
// of. For hierarchical deterministic wallets, the list will not be exhaustive,
// rather only contain the accounts explicitly pinned during account derivation.
Accounts() []Account

// Contains returns whether an account is part of this particular wallet or not.
Contains(account Account) bool

// Derive attempts to explicitly derive a hierarchical deterministic account at
// the specified derivation path. If requested, the derived account will be added
// to the wallet's tracked account list.
Derive(path string, pin bool) (Account, error)

// SignHash requests the wallet to sign the given hash.
//
// It looks up the account specified either solely via its address contained within,
// or optionally with the aid of any location metadata from the embedded URL field.
//
// If the wallet requires additional authentication to sign the request (e.g.
// a password to decrypt the account, or a PIN code o verify the transaction),
// an AuthNeededError instance will be returned, containing infos for the user
// about which fields or actions are needed. The user may retry by providing
// the needed details via SignHashWithPassphrase, or by other means (e.g. unlock
// the account in a keystore).
SignHash(account Account, hash []byte) ([]byte, error)

// SignTx requests the wallet to sign the given transaction.
//
// It looks up the account specified either solely via its address contained within,
// or optionally with the aid of any location metadata from the embedded URL field.
//
// If the wallet requires additional authentication to sign the request (e.g.
// a password to decrypt the account, or a PIN code o verify the transaction),
// an AuthNeededError instance will be returned, containing infos for the user
// about which fields or actions are needed. The user may retry by providing
// the needed details via SignTxWithPassphrase, or by other means (e.g. unlock
// the account in a keystore).
SignTx(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)

// SignHashWithPassphrase requests the wallet to sign the given hash with the
// given passphrase as extra authentication information.
//
// It looks up the account specified either solely via its address contained within,
// or optionally with the aid of any location metadata from the embedded URL field.
SignHashWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error)

// SignTxWithPassphrase requests the wallet to sign the given transaction, with the
// given passphrase as extra authentication information.
//
// It looks up the account specified either solely via its address contained within,
// or optionally with the aid of any location metadata from the embedded URL field.
SignTxWithPassphrase(account Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
}

// HasAddress reports whether a key with the given address is present.
func (am *Manager) HasAddress(addr common.Address) bool {
am.lock.RLock()
defer am.lock.RUnlock()

for _, backend := range am.backends {
if backend.HasAddress(addr) {
return true
}
}
return false
// Backend is a "wallet provider" that may contain a batch of accounts they can
// sign transactions with and upon request, do so.
type Backend interface {
// Wallets retrieves the list of wallets the backend is currently aware of.
//
// The returned wallets are not opened by default. For software HD wallets this
// means that no base seeds are decrypted, and for hardware wallets that no actual
// connection is established.
//
// The resulting wallet list will be sorted alphabetically based on its internal
// URL assigned by the backend. Since wallets (especially hardware) may come and
// go, the same wallet might appear at a different positions in the list during
// subsequent retrievals.
Wallets() []Wallet

// Subscribe creates an async subscription to receive notifications when the
// backend detects the arrival or departure of a wallet.
Subscribe(sink chan<- WalletEvent) event.Subscription
}

// SignHash requests the account manager to get the hash signed with an arbitrary
// signing backend holding the authorization for the specified account.
func (am *Manager) SignHash(acc Account, hash []byte) ([]byte, error) {
am.lock.RLock()
defer am.lock.RUnlock()

if err := am.ensureBackend(&acc); err != nil {
return nil, err
}
return acc.backend.SignHash(acc, hash)
}

// SignTx requests the account manager to get the transaction signed with an
// arbitrary signing backend holding the authorization for the specified account.
func (am *Manager) SignTx(acc Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
am.lock.RLock()
defer am.lock.RUnlock()

if err := am.ensureBackend(&acc); err != nil {
return nil, err
}
return acc.backend.SignTx(acc, tx, chainID)
}

// SignHashWithPassphrase requests the account manager to get the hash signed with
// an arbitrary signing backend holding the authorization for the specified account.
func (am *Manager) SignHashWithPassphrase(acc Account, passphrase string, hash []byte) ([]byte, error) {
am.lock.RLock()
defer am.lock.RUnlock()

if err := am.ensureBackend(&acc); err != nil {
return nil, err
}
return acc.backend.SignHashWithPassphrase(acc, passphrase, hash)
}

// SignTxWithPassphrase requests the account manager to get the transaction signed
// with an arbitrary signing backend holding the authorization for the specified
// account.
func (am *Manager) SignTxWithPassphrase(acc Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
am.lock.RLock()
defer am.lock.RUnlock()

if err := am.ensureBackend(&acc); err != nil {
return nil, err
}
return acc.backend.SignTxWithPassphrase(acc, passphrase, tx, chainID)
}

// ensureBackend ensures that the account has a correctly set backend and that
// it is still alive.
//
// Please note, this method assumes the manager lock is held!
func (am *Manager) ensureBackend(acc *Account) error {
// If we have a backend, make sure it's still live
if acc.backend != nil {
if _, exists := am.index[reflect.TypeOf(acc.backend)]; !exists {
return ErrUnknownAccount
}
return nil
}
// If we don't have a known backend, look up one that can service it
for _, backend := range am.backends {
if backend.HasAddress(acc.Address) { // TODO(karalabe): this assumes unique addresses per backend
acc.backend = backend
return nil
}
}
return ErrUnknownAccount
// WalletEvent is an event fired by an account backend when a wallet arrival or
// departure is detected.
type WalletEvent struct {
Wallet Wallet // Wallet instance arrived or departed
Arrive bool // Whether the wallet was added or removed
}
88 changes: 0 additions & 88 deletions accounts/backend.go

This file was deleted.

29 changes: 28 additions & 1 deletion accounts/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,34 @@

package accounts

import "fmt"
import (
"errors"
"fmt"
)

// ErrUnknownAccount is returned for any requested operation for which no backend
// provides the specified account.
var ErrUnknownAccount = errors.New("unknown account")

// ErrUnknownWallet is returned for any requested operation for which no backend
// provides the specified wallet.
var ErrUnknownWallet = errors.New("unknown wallet")

// ErrNotSupported is returned when an operation is requested from an account
// backend that it does not support.
var ErrNotSupported = errors.New("not supported")

// ErrInvalidPassphrase is returned when a decryption operation receives a bad
// passphrase.
var ErrInvalidPassphrase = errors.New("invalid passphrase")

// ErrWalletAlreadyOpen is returned if a wallet is attempted to be opened the
// secodn time.
var ErrWalletAlreadyOpen = errors.New("wallet already open")

// ErrWalletClosed is returned if a wallet is attempted to be opened the
// secodn time.
var ErrWalletClosed = errors.New("wallet closed")

// AuthNeededError is returned by backends for signing requests where the user
// is required to provide further authentication before signing can succeed.
Expand Down
Loading

0 comments on commit fad5eb0

Please sign in to comment.