github.com/m3shine/gochain@v2.2.26+incompatible/accounts/usbwallet/hub.go (about) 1 // Copyright 2017 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package usbwallet 18 19 import ( 20 "context" 21 "errors" 22 "runtime" 23 "sync" 24 "time" 25 26 "go.opencensus.io/trace" 27 28 "github.com/gochain-io/gochain/accounts" 29 "github.com/gochain-io/gochain/event" 30 "github.com/gochain-io/gochain/log" 31 "github.com/karalabe/hid" 32 ) 33 34 // LedgerScheme is the protocol scheme prefixing account and wallet URLs. 35 const LedgerScheme = "ledger" 36 37 // TrezorScheme is the protocol scheme prefixing account and wallet URLs. 38 const TrezorScheme = "trezor" 39 40 // refreshCycle is the maximum time between wallet refreshes (if USB hotplug 41 // notifications don't work). 42 const refreshCycle = time.Second 43 44 // refreshThrottling is the minimum time between wallet refreshes to avoid USB 45 // trashing. 46 const refreshThrottling = 500 * time.Millisecond 47 48 // Hub is a accounts.Backend that can find and handle generic USB hardware wallets. 49 type Hub struct { 50 scheme string // Protocol scheme prefixing account and wallet URLs. 51 vendorID uint16 // USB vendor identifier used for device discovery 52 productIDs []uint16 // USB product identifiers used for device discovery 53 usageID uint16 // USB usage page identifier used for macOS device discovery 54 endpointID int // USB endpoint identifier used for non-macOS device discovery 55 makeDriver func(log.Logger) driver // Factory method to construct a vendor specific driver 56 57 refreshed time.Time // Time instance when the list of wallets was last refreshed 58 wallets []accounts.Wallet // List of USB wallet devices currently tracking 59 updateFeed event.Feed // Event feed to notify wallet additions/removals 60 updateScope event.SubscriptionScope // Subscription scope tracking current live listeners 61 updating bool // Whether the event notification loop is running 62 63 quit chan chan error 64 65 stateLock sync.RWMutex // Protects the internals of the hub from racey access 66 67 // TODO(karalabe): remove if hotplug lands on Windows 68 commsPend int // Number of operations blocking enumeration 69 commsLock sync.Mutex // Lock protecting the pending counter and enumeration 70 } 71 72 // NewLedgerHub creates a new hardware wallet manager for Ledger devices. 73 func NewLedgerHub() (*Hub, error) { 74 return newHub(LedgerScheme, 0x2c97, []uint16{0x0000 /* Ledger Blue */, 0x0001 /* Ledger Nano S */}, 0xffa0, 0, newLedgerDriver) 75 } 76 77 // NewTrezorHub creates a new hardware wallet manager for Trezor devices. 78 func NewTrezorHub() (*Hub, error) { 79 return newHub(TrezorScheme, 0x534c, []uint16{0x0001 /* Trezor 1 */}, 0xff00, 0, newTrezorDriver) 80 } 81 82 // newHub creates a new hardware wallet manager for generic USB devices. 83 func newHub(scheme string, vendorID uint16, productIDs []uint16, usageID uint16, endpointID int, makeDriver func(log.Logger) driver) (*Hub, error) { 84 if !hid.Supported() { 85 return nil, errors.New("unsupported platform") 86 } 87 hub := &Hub{ 88 scheme: scheme, 89 vendorID: vendorID, 90 productIDs: productIDs, 91 usageID: usageID, 92 endpointID: endpointID, 93 makeDriver: makeDriver, 94 quit: make(chan chan error), 95 } 96 hub.refreshWallets() 97 return hub, nil 98 } 99 100 // Wallets implements accounts.Backend, returning all the currently tracked USB 101 // devices that appear to be hardware wallets. 102 func (hub *Hub) Wallets() []accounts.Wallet { 103 // Make sure the list of wallets is up to date 104 hub.refreshWallets() 105 106 hub.stateLock.RLock() 107 defer hub.stateLock.RUnlock() 108 109 cpy := make([]accounts.Wallet, len(hub.wallets)) 110 copy(cpy, hub.wallets) 111 return cpy 112 } 113 114 // refreshWallets scans the USB devices attached to the machine and updates the 115 // list of wallets based on the found devices. 116 func (hub *Hub) refreshWallets() { 117 ctx, span := trace.StartSpan(context.Background(), "Hub.refreshWallets") 118 defer span.End() 119 120 // Don't scan the USB like crazy it the user fetches wallets in a loop 121 hub.stateLock.RLock() 122 elapsed := time.Since(hub.refreshed) 123 hub.stateLock.RUnlock() 124 125 if elapsed < refreshThrottling { 126 return 127 } 128 // Retrieve the current list of USB wallet devices 129 var devices []hid.DeviceInfo 130 131 if runtime.GOOS == "linux" { 132 // hidapi on Linux opens the device during enumeration to retrieve some infos, 133 // breaking the Ledger protocol if that is waiting for user confirmation. This 134 // is a bug acknowledged at Ledger, but it won't be fixed on old devices so we 135 // need to prevent concurrent comms ourselves. The more elegant solution would 136 // be to ditch enumeration in favor of hutplug events, but that don't work yet 137 // on Windows so if we need to hack it anyway, this is more elegant for now. 138 hub.commsLock.Lock() 139 if hub.commsPend > 0 { // A confirmation is pending, don't refresh 140 hub.commsLock.Unlock() 141 return 142 } 143 } 144 for _, info := range hid.Enumerate(hub.vendorID, 0) { 145 for _, id := range hub.productIDs { 146 if info.ProductID == id && (info.UsagePage == hub.usageID || info.Interface == hub.endpointID) { 147 devices = append(devices, info) 148 break 149 } 150 } 151 } 152 if runtime.GOOS == "linux" { 153 // See rationale before the enumeration why this is needed and only on Linux. 154 hub.commsLock.Unlock() 155 } 156 // Transform the current list of wallets into the new one 157 hub.stateLock.Lock() 158 159 wallets := make([]accounts.Wallet, 0, len(devices)) 160 events := []accounts.WalletEvent{} 161 162 for _, device := range devices { 163 url := accounts.URL{Scheme: hub.scheme, Path: device.Path} 164 165 // Drop wallets in front of the next device or those that failed for some reason 166 for len(hub.wallets) > 0 { 167 // Abort if we're past the current device and found an operational one 168 _, failure := hub.wallets[0].Status() 169 if hub.wallets[0].URL().Cmp(url) >= 0 || failure == nil { 170 break 171 } 172 // Drop the stale and failed devices 173 events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Kind: accounts.WalletDropped}) 174 hub.wallets = hub.wallets[1:] 175 } 176 // If there are no more wallets or the device is before the next, wrap new wallet 177 if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 { 178 logger := log.New("url", url) 179 wallet := &wallet{hub: hub, driver: hub.makeDriver(logger), url: &url, info: device, log: logger} 180 181 events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived}) 182 wallets = append(wallets, wallet) 183 continue 184 } 185 // If the device is the same as the first wallet, keep it 186 if hub.wallets[0].URL().Cmp(url) == 0 { 187 wallets = append(wallets, hub.wallets[0]) 188 hub.wallets = hub.wallets[1:] 189 continue 190 } 191 } 192 // Drop any leftover wallets and set the new batch 193 for _, wallet := range hub.wallets { 194 events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped}) 195 } 196 hub.refreshed = time.Now() 197 hub.wallets = wallets 198 hub.stateLock.Unlock() 199 200 // Fire all wallet events and return 201 for _, event := range events { 202 hub.updateFeed.SendCtx(ctx, event) 203 } 204 } 205 206 // Subscribe implements accounts.Backend, creating an async subscription to 207 // receive notifications on the addition or removal of USB wallets. 208 func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { 209 // We need the mutex to reliably start/stop the update loop 210 hub.stateLock.Lock() 211 defer hub.stateLock.Unlock() 212 213 // Subscribe the caller and track the subscriber count 214 sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink)) 215 216 // Subscribers require an active notification loop, start it 217 if !hub.updating { 218 hub.updating = true 219 go hub.updater() 220 } 221 return sub 222 } 223 224 // updater is responsible for maintaining an up-to-date list of wallets managed 225 // by the USB hub, and for firing wallet addition/removal events. 226 func (hub *Hub) updater() { 227 for { 228 // TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout 229 // <-hub.changes 230 time.Sleep(refreshCycle) 231 232 // Run the wallet refresher 233 hub.refreshWallets() 234 235 // If all our subscribers left, stop the updater 236 hub.stateLock.Lock() 237 if hub.updateScope.Count() == 0 { 238 hub.updating = false 239 hub.stateLock.Unlock() 240 return 241 } 242 hub.stateLock.Unlock() 243 } 244 }