mirror of https://github.com/ethereum/go-ethereum
parent
b3c0e9d3cc
commit
fad5eb0a87
@ -1,88 +0,0 @@ |
|||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package accounts |
|
||||||
|
|
||||||
import ( |
|
||||||
"math/big" |
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common" |
|
||||||
"github.com/ethereum/go-ethereum/core/types" |
|
||||||
) |
|
||||||
|
|
||||||
// Backend is an "account provider" that can specify a batch of accounts it can
|
|
||||||
// sign transactions with and upon request, do so.
|
|
||||||
type Backend interface { |
|
||||||
// Accounts retrieves the list of signing accounts the backend is currently aware of.
|
|
||||||
Accounts() []Account |
|
||||||
|
|
||||||
// HasAddress reports whether an account with the given address is present.
|
|
||||||
HasAddress(addr common.Address) bool |
|
||||||
|
|
||||||
// SignHash requests the backend 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 backend 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(acc Account, hash []byte) ([]byte, error) |
|
||||||
|
|
||||||
// SignTx requests the backend 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 backend 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(acc Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) |
|
||||||
|
|
||||||
// SignHashWithPassphrase requests the backend 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.
|
|
||||||
SignHashWithPassphrase(acc Account, passphrase string, hash []byte) ([]byte, error) |
|
||||||
|
|
||||||
// SignTxWithPassphrase requests the backend 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(acc Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) |
|
||||||
|
|
||||||
// TODO(karalabe,fjl): watching and caching needs the Go subscription system
|
|
||||||
// Watch requests the backend to send a notification to the specified channel whenever
|
|
||||||
// an new account appears or an existing one disappears.
|
|
||||||
//Watch(chan AccountEvent) error
|
|
||||||
|
|
||||||
// Unwatch requests the backend stop sending notifications to the given channel.
|
|
||||||
//Unwatch(chan AccountEvent) error
|
|
||||||
} |
|
||||||
|
|
||||||
// TODO(karalabe,fjl): watching and caching needs the Go subscription system
|
|
||||||
// type AccountEvent struct {
|
|
||||||
// Account Account
|
|
||||||
// Added bool
|
|
||||||
// }
|
|
@ -0,0 +1,133 @@ |
|||||||
|
// Copyright 2017 The go-ethereum Authors
|
||||||
|
// This file is part of the go-ethereum library.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package keystore |
||||||
|
|
||||||
|
import ( |
||||||
|
"math/big" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts" |
||||||
|
"github.com/ethereum/go-ethereum/core/types" |
||||||
|
) |
||||||
|
|
||||||
|
// keystoreWallet implements the accounts.Wallet interface for the original
|
||||||
|
// keystore.
|
||||||
|
type keystoreWallet struct { |
||||||
|
account accounts.Account // Single account contained in this wallet
|
||||||
|
keystore *KeyStore // Keystore where the account originates from
|
||||||
|
} |
||||||
|
|
||||||
|
// Type implements accounts.Wallet, returning the textual type of the wallet.
|
||||||
|
func (w *keystoreWallet) Type() string { |
||||||
|
return "secret-storage" |
||||||
|
} |
||||||
|
|
||||||
|
// URL implements accounts.Wallet, returning the URL of the account within.
|
||||||
|
func (w *keystoreWallet) URL() string { |
||||||
|
return w.account.URL |
||||||
|
} |
||||||
|
|
||||||
|
// Status implements accounts.Wallet, always returning "open", since there is no
|
||||||
|
// concept of open/close for plain keystore accounts.
|
||||||
|
func (w *keystoreWallet) Status() string { |
||||||
|
return "Open" |
||||||
|
} |
||||||
|
|
||||||
|
// Open implements accounts.Wallet, but is a noop for plain wallets since there
|
||||||
|
// is no connection or decryption step necessary to access the list of accounts.
|
||||||
|
func (w *keystoreWallet) Open(passphrase string) error { return nil } |
||||||
|
|
||||||
|
// Close implements accounts.Wallet, but is a noop for plain wallets since is no
|
||||||
|
// meaningful open operation.
|
||||||
|
func (w *keystoreWallet) Close() error { return nil } |
||||||
|
|
||||||
|
// Accounts implements accounts.Wallet, returning an account list consisting of
|
||||||
|
// a single account that the plain kestore wallet contains.
|
||||||
|
func (w *keystoreWallet) Accounts() []accounts.Account { |
||||||
|
return []accounts.Account{w.account} |
||||||
|
} |
||||||
|
|
||||||
|
// Contains implements accounts.Wallet, returning whether a particular account is
|
||||||
|
// or is not wrapped by this wallet instance.
|
||||||
|
func (w *keystoreWallet) Contains(account accounts.Account) bool { |
||||||
|
return account.Address == w.account.Address && (account.URL == "" || account.URL == w.account.URL) |
||||||
|
} |
||||||
|
|
||||||
|
// Derive implements accounts.Wallet, but is a noop for plain wallets since there
|
||||||
|
// is no notion of hierarchical account derivation for plain keystore accounts.
|
||||||
|
func (w *keystoreWallet) Derive(path string, pin bool) (accounts.Account, error) { |
||||||
|
return accounts.Account{}, accounts.ErrNotSupported |
||||||
|
} |
||||||
|
|
||||||
|
// SignHash implements accounts.Wallet, attempting to sign the given hash with
|
||||||
|
// the given account. If the wallet does not wrap this particular account, an
|
||||||
|
// error is returned to avoid account leakage (even though in theory we may be
|
||||||
|
// able to sign via our shared keystore backend).
|
||||||
|
func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) { |
||||||
|
// Make sure the requested account is contained within
|
||||||
|
if account.Address != w.account.Address { |
||||||
|
return nil, accounts.ErrUnknownAccount |
||||||
|
} |
||||||
|
if account.URL != "" && account.URL != w.account.URL { |
||||||
|
return nil, accounts.ErrUnknownAccount |
||||||
|
} |
||||||
|
// Account seems valid, request the keystore to sign
|
||||||
|
return w.keystore.SignHash(account, hash) |
||||||
|
} |
||||||
|
|
||||||
|
// SignTx implements accounts.Wallet, attempting to sign the given transaction
|
||||||
|
// with the given account. If the wallet does not wrap this particular account,
|
||||||
|
// an error is returned to avoid account leakage (even though in theory we may
|
||||||
|
// be able to sign via our shared keystore backend).
|
||||||
|
func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { |
||||||
|
// Make sure the requested account is contained within
|
||||||
|
if account.Address != w.account.Address { |
||||||
|
return nil, accounts.ErrUnknownAccount |
||||||
|
} |
||||||
|
if account.URL != "" && account.URL != w.account.URL { |
||||||
|
return nil, accounts.ErrUnknownAccount |
||||||
|
} |
||||||
|
// Account seems valid, request the keystore to sign
|
||||||
|
return w.keystore.SignTx(account, tx, chainID) |
||||||
|
} |
||||||
|
|
||||||
|
// SignHashWithPassphrase implements accounts.Wallet, attempting to sign the
|
||||||
|
// given hash with the given account using passphrase as extra authentication.
|
||||||
|
func (w *keystoreWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { |
||||||
|
// Make sure the requested account is contained within
|
||||||
|
if account.Address != w.account.Address { |
||||||
|
return nil, accounts.ErrUnknownAccount |
||||||
|
} |
||||||
|
if account.URL != "" && account.URL != w.account.URL { |
||||||
|
return nil, accounts.ErrUnknownAccount |
||||||
|
} |
||||||
|
// Account seems valid, request the keystore to sign
|
||||||
|
return w.keystore.SignHashWithPassphrase(account, passphrase, hash) |
||||||
|
} |
||||||
|
|
||||||
|
// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given
|
||||||
|
// transaction with the given account using passphrase as extra authentication.
|
||||||
|
func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { |
||||||
|
// Make sure the requested account is contained within
|
||||||
|
if account.Address != w.account.Address { |
||||||
|
return nil, accounts.ErrUnknownAccount |
||||||
|
} |
||||||
|
if account.URL != "" && account.URL != w.account.URL { |
||||||
|
return nil, accounts.ErrUnknownAccount |
||||||
|
} |
||||||
|
// Account seems valid, request the keystore to sign
|
||||||
|
return w.keystore.SignTxWithPassphrase(account, passphrase, tx, chainID) |
||||||
|
} |
@ -0,0 +1,194 @@ |
|||||||
|
// Copyright 2017 The go-ethereum Authors
|
||||||
|
// This file is part of the go-ethereum library.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package accounts |
||||||
|
|
||||||
|
import ( |
||||||
|
"reflect" |
||||||
|
"sort" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/event" |
||||||
|
) |
||||||
|
|
||||||
|
// Manager is an overarching account manager that can communicate with various
|
||||||
|
// backends for signing transactions.
|
||||||
|
type Manager struct { |
||||||
|
backends map[reflect.Type][]Backend // Index of backends currently registered
|
||||||
|
updaters []event.Subscription // Wallet update subscriptions for all backends
|
||||||
|
updates chan WalletEvent // Subscription sink for backend wallet changes
|
||||||
|
wallets []Wallet // Cache of all wallets from all registered backends
|
||||||
|
|
||||||
|
feed event.Feed // Wallet feed notifying of arrivals/departures
|
||||||
|
|
||||||
|
quit chan chan error |
||||||
|
lock sync.RWMutex |
||||||
|
} |
||||||
|
|
||||||
|
// NewManager creates a generic account manager to sign transaction via various
|
||||||
|
// supported backends.
|
||||||
|
func NewManager(backends ...Backend) *Manager { |
||||||
|
// Subscribe to wallet notifications from all backends
|
||||||
|
updates := make(chan WalletEvent, 4*len(backends)) |
||||||
|
|
||||||
|
subs := make([]event.Subscription, len(backends)) |
||||||
|
for i, backend := range backends { |
||||||
|
subs[i] = backend.Subscribe(updates) |
||||||
|
} |
||||||
|
// Retrieve the initial list of wallets from the backends and sort by URL
|
||||||
|
var wallets []Wallet |
||||||
|
for _, backend := range backends { |
||||||
|
wallets = merge(wallets, backend.Wallets()...) |
||||||
|
} |
||||||
|
// Assemble the account manager and return
|
||||||
|
am := &Manager{ |
||||||
|
backends: make(map[reflect.Type][]Backend), |
||||||
|
updaters: subs, |
||||||
|
updates: updates, |
||||||
|
wallets: wallets, |
||||||
|
quit: make(chan chan error), |
||||||
|
} |
||||||
|
for _, backend := range backends { |
||||||
|
kind := reflect.TypeOf(backend) |
||||||
|
am.backends[kind] = append(am.backends[kind], backend) |
||||||
|
} |
||||||
|
go am.update() |
||||||
|
|
||||||
|
return am |
||||||
|
} |
||||||
|
|
||||||
|
// Close terminates the account manager's internal notification processes.
|
||||||
|
func (am *Manager) Close() error { |
||||||
|
errc := make(chan error) |
||||||
|
am.quit <- errc |
||||||
|
return <-errc |
||||||
|
} |
||||||
|
|
||||||
|
// update is the wallet event loop listening for notifications from the backends
|
||||||
|
// and updating the cache of wallets.
|
||||||
|
func (am *Manager) update() { |
||||||
|
// Close all subscriptions when the manager terminates
|
||||||
|
defer func() { |
||||||
|
am.lock.Lock() |
||||||
|
for _, sub := range am.updaters { |
||||||
|
sub.Unsubscribe() |
||||||
|
} |
||||||
|
am.updaters = nil |
||||||
|
am.lock.Unlock() |
||||||
|
}() |
||||||
|
|
||||||
|
// Loop until termination
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case event := <-am.updates: |
||||||
|
// Wallet event arrived, update local cache
|
||||||
|
am.lock.Lock() |
||||||
|
if event.Arrive { |
||||||
|
am.wallets = merge(am.wallets, event.Wallet) |
||||||
|
} else { |
||||||
|
am.wallets = drop(am.wallets, event.Wallet) |
||||||
|
} |
||||||
|
am.lock.Unlock() |
||||||
|
|
||||||
|
// Notify any listeners of the event
|
||||||
|
am.feed.Send(event) |
||||||
|
|
||||||
|
case errc := <-am.quit: |
||||||
|
// Manager terminating, return
|
||||||
|
errc <- nil |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Backends retrieves the backend(s) with the given type from the account manager.
|
||||||
|
func (am *Manager) Backends(kind reflect.Type) []Backend { |
||||||
|
return am.backends[kind] |
||||||
|
} |
||||||
|
|
||||||
|
// Wallets returns all signer accounts registered under this account manager.
|
||||||
|
func (am *Manager) Wallets() []Wallet { |
||||||
|
am.lock.RLock() |
||||||
|
defer am.lock.RUnlock() |
||||||
|
|
||||||
|
cpy := make([]Wallet, len(am.wallets)) |
||||||
|
copy(cpy, am.wallets) |
||||||
|
return cpy |
||||||
|
} |
||||||
|
|
||||||
|
// Wallet retrieves the wallet associated with a particular URL.
|
||||||
|
func (am *Manager) Wallet(url string) (Wallet, error) { |
||||||
|
am.lock.RLock() |
||||||
|
defer am.lock.RUnlock() |
||||||
|
|
||||||
|
for _, wallet := range am.Wallets() { |
||||||
|
if wallet.URL() == url { |
||||||
|
return wallet, nil |
||||||
|
} |
||||||
|
} |
||||||
|
return nil, ErrUnknownWallet |
||||||
|
} |
||||||
|
|
||||||
|
// Find attempts to locate the wallet corresponding to a specific account. Since
|
||||||
|
// accounts can be dynamically added to and removed from wallets, this method has
|
||||||
|
// a linear runtime in the number of wallets.
|
||||||
|
func (am *Manager) Find(account Account) (Wallet, error) { |
||||||
|
am.lock.RLock() |
||||||
|
defer am.lock.RUnlock() |
||||||
|
|
||||||
|
for _, wallet := range am.wallets { |
||||||
|
if wallet.Contains(account) { |
||||||
|
return wallet, nil |
||||||
|
} |
||||||
|
} |
||||||
|
return nil, ErrUnknownAccount |
||||||
|
} |
||||||
|
|
||||||
|
// Subscribe creates an async subscription to receive notifications when the
|
||||||
|
// manager detects the arrival or departure of a wallet from any of its backends.
|
||||||
|
func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription { |
||||||
|
return am.feed.Subscribe(sink) |
||||||
|
} |
||||||
|
|
||||||
|
// merge is a sorted analogue of append for wallets, where the ordering of the
|
||||||
|
// origin list is preserved by inserting new wallets at the correct position.
|
||||||
|
//
|
||||||
|
// The original slice is assumed to be already sorted by URL.
|
||||||
|
func merge(slice []Wallet, wallets ...Wallet) []Wallet { |
||||||
|
for _, wallet := range wallets { |
||||||
|
n := sort.Search(len(slice), func(i int) bool { return slice[i].URL() >= wallet.URL() }) |
||||||
|
if n == len(slice) { |
||||||
|
slice = append(slice, wallet) |
||||||
|
continue |
||||||
|
} |
||||||
|
slice = append(slice[:n], append([]Wallet{wallet}, slice[n:]...)...) |
||||||
|
} |
||||||
|
return slice |
||||||
|
} |
||||||
|
|
||||||
|
// drop is the couterpart of merge, which looks up wallets from within the sorted
|
||||||
|
// cache and removes the ones specified.
|
||||||
|
func drop(slice []Wallet, wallets ...Wallet) []Wallet { |
||||||
|
for _, wallet := range wallets { |
||||||
|
n := sort.Search(len(slice), func(i int) bool { return slice[i].URL() >= wallet.URL() }) |
||||||
|
if n == len(slice) { |
||||||
|
// Wallet not found, may happen during startup
|
||||||
|
continue |
||||||
|
} |
||||||
|
slice = append(slice[:n], slice[n+1:]...) |
||||||
|
} |
||||||
|
return slice |
||||||
|
} |
@ -0,0 +1,205 @@ |
|||||||
|
// Copyright 2017 The go-ethereum Authors
|
||||||
|
// This file is part of the go-ethereum library.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Lesser General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// This file contains the implementation for interacting with the Ledger hardware
|
||||||
|
// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo:
|
||||||
|
// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc
|
||||||
|
|
||||||
|
// +build !ios
|
||||||
|
|
||||||
|
package usbwallet |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts" |
||||||
|
"github.com/ethereum/go-ethereum/event" |
||||||
|
"github.com/karalabe/gousb/usb" |
||||||
|
) |
||||||
|
|
||||||
|
// ledgerDeviceIDs are the known device IDs that Ledger wallets use.
|
||||||
|
var ledgerDeviceIDs = []deviceID{ |
||||||
|
{Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue
|
||||||
|
{Vendor: 0x2c97, Product: 0x0001}, // Ledger Nano S
|
||||||
|
} |
||||||
|
|
||||||
|
// Maximum time between wallet refreshes (if USB hotplug notifications don't work).
|
||||||
|
const ledgerRefreshCycle = time.Second |
||||||
|
|
||||||
|
// Minimum time between wallet refreshes to avoid USB trashing.
|
||||||
|
const ledgerRefreshThrottling = 500 * time.Millisecond |
||||||
|
|
||||||
|
// LedgerHub is a accounts.Backend that can find and handle Ledger hardware wallets.
|
||||||
|
type LedgerHub struct { |
||||||
|
ctx *usb.Context // Context interfacing with a libusb instance
|
||||||
|
|
||||||
|
refreshed time.Time // Time instance when the list of wallets was last refreshed
|
||||||
|
wallets []accounts.Wallet // List of Ledger devices currently tracking
|
||||||
|
updateFeed event.Feed // Event feed to notify wallet additions/removals
|
||||||
|
updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
|
||||||
|
updating bool // Whether the event notification loop is running
|
||||||
|
|
||||||
|
quit chan chan error |
||||||
|
lock sync.RWMutex |
||||||
|
} |
||||||
|
|
||||||
|
// NewLedgerHub creates a new hardware wallet manager for Ledger devices.
|
||||||
|
func NewLedgerHub() (*LedgerHub, error) { |
||||||
|
// Initialize the USB library to access Ledgers through
|
||||||
|
ctx, err := usb.NewContext() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
// Create the USB hub, start and return it
|
||||||
|
hub := &LedgerHub{ |
||||||
|
ctx: ctx, |
||||||
|
quit: make(chan chan error), |
||||||
|
} |
||||||
|
hub.refreshWallets() |
||||||
|
|
||||||
|
return hub, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Wallets implements accounts.Backend, returning all the currently tracked USB
|
||||||
|
// devices that appear to be Ledger hardware wallets.
|
||||||
|
func (hub *LedgerHub) Wallets() []accounts.Wallet { |
||||||
|
// Make sure the list of wallets is up to date
|
||||||
|
hub.refreshWallets() |
||||||
|
|
||||||
|
hub.lock.RLock() |
||||||
|
defer hub.lock.RUnlock() |
||||||
|
|
||||||
|
cpy := make([]accounts.Wallet, len(hub.wallets)) |
||||||
|
copy(cpy, hub.wallets) |
||||||
|
return cpy |
||||||
|
} |
||||||
|
|
||||||
|
// refreshWallets scans the USB devices attached to the machine and updates the
|
||||||
|
// list of wallets based on the found devices.
|
||||||
|
func (hub *LedgerHub) refreshWallets() { |
||||||
|
// Don't scan the USB like crazy it the user fetches wallets in a loop
|
||||||
|
hub.lock.RLock() |
||||||
|
elapsed := time.Since(hub.refreshed) |
||||||
|
hub.lock.RUnlock() |
||||||
|
|
||||||
|
if elapsed < ledgerRefreshThrottling { |
||||||
|
return |
||||||
|
} |
||||||
|
// Retrieve the current list of Ledger devices
|
||||||
|
var devIDs []deviceID |
||||||
|
var busIDs []uint16 |
||||||
|
|
||||||
|
hub.ctx.ListDevices(func(desc *usb.Descriptor) bool { |
||||||
|
// Gather Ledger devices, don't connect any just yet
|
||||||
|
for _, id := range ledgerDeviceIDs { |
||||||
|
if desc.Vendor == id.Vendor && desc.Product == id.Product { |
||||||
|
devIDs = append(devIDs, deviceID{Vendor: desc.Vendor, Product: desc.Product}) |
||||||
|
busIDs = append(busIDs, uint16(desc.Bus)<<8+uint16(desc.Address)) |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
// Not ledger, ignore and don't connect either
|
||||||
|
return false |
||||||
|
}) |
||||||
|
// Transform the current list of wallets into the new one
|
||||||
|
hub.lock.Lock() |
||||||
|
|
||||||
|
wallets := make([]accounts.Wallet, 0, len(devIDs)) |
||||||
|
events := []accounts.WalletEvent{} |
||||||
|
|
||||||
|
for i := 0; i < len(devIDs); i++ { |
||||||
|
devID, busID := devIDs[i], busIDs[i] |
||||||
|
url := fmt.Sprintf("ledger://%03d:%03d", busID>>8, busID&0xff) |
||||||
|
|
||||||
|
// Drop wallets while they were in front of the next account
|
||||||
|
for len(hub.wallets) > 0 && hub.wallets[0].URL() < url { |
||||||
|
events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false}) |
||||||
|
hub.wallets = hub.wallets[1:] |
||||||
|
} |
||||||
|
// If there are no more wallets or the account is before the next, wrap new wallet
|
||||||
|
if len(hub.wallets) == 0 || hub.wallets[0].URL() > url { |
||||||
|
wallet := &ledgerWallet{context: hub.ctx, hardwareID: devID, locationID: busID, url: url} |
||||||
|
|
||||||
|
events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) |
||||||
|
wallets = append(wallets, wallet) |
||||||
|
continue |
||||||
|
} |
||||||
|
// If the account is the same as the first wallet, keep it
|
||||||
|
if hub.wallets[0].URL() == url { |
||||||
|
wallets = append(wallets, hub.wallets[0]) |
||||||
|
hub.wallets = hub.wallets[1:] |
||||||
|
continue |
||||||
|
} |
||||||
|
} |
||||||
|
// Drop any leftover wallets and set the new batch
|
||||||
|
for _, wallet := range hub.wallets { |
||||||
|
events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false}) |
||||||
|
} |
||||||
|
hub.refreshed = time.Now() |
||||||
|
hub.wallets = wallets |
||||||
|
hub.lock.Unlock() |
||||||
|
|
||||||
|
// Fire all wallet events and return
|
||||||
|
for _, event := range events { |
||||||
|
hub.updateFeed.Send(event) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Subscribe implements accounts.Backend, creating an async subscription to
|
||||||
|
// receive notifications on the addition or removal of Ledger wallets.
|
||||||
|
func (hub *LedgerHub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { |
||||||
|
// We need the mutex to reliably start/stop the update loop
|
||||||
|
hub.lock.Lock() |
||||||
|
defer hub.lock.Unlock() |
||||||
|
|
||||||
|
// Subscribe the caller and track the subscriber count
|
||||||
|
sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink)) |
||||||
|
|
||||||
|
// Subscribers require an active notification loop, start it
|
||||||
|
if !hub.updating { |
||||||
|
hub.updating = true |
||||||
|
go hub.updater() |
||||||
|
} |
||||||
|
return sub |
||||||
|
} |
||||||
|
|
||||||
|
// updater is responsible for maintaining an up-to-date list of wallets stored in
|
||||||
|
// the keystore, and for firing wallet addition/removal events. It listens for
|
||||||
|
// account change events from the underlying account cache, and also periodically
|
||||||
|
// forces a manual refresh (only triggers for systems where the filesystem notifier
|
||||||
|
// is not running).
|
||||||
|
func (hub *LedgerHub) updater() { |
||||||
|
for { |
||||||
|
// Wait for a USB hotplug event (not supported yet) or a refresh timeout
|
||||||
|
select { |
||||||
|
//case <-hub.changes: // reenable on hutplug implementation
|
||||||
|
case <-time.After(ledgerRefreshCycle): |
||||||
|
} |
||||||
|
// Run the wallet refresher
|
||||||
|
hub.refreshWallets() |
||||||
|
|
||||||
|
// If all our subscribers left, stop the updater
|
||||||
|
hub.lock.Lock() |
||||||
|
if hub.updateScope.Count() == 0 { |
||||||
|
hub.updating = false |
||||||
|
hub.lock.Unlock() |
||||||
|
return |
||||||
|
} |
||||||
|
hub.lock.Unlock() |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue