github.com/devfans/go-ethereum@v1.5.10-0.20170326212234-7419d0c38291/accounts/usbwallet/ledger_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 // This file contains the implementation for interacting with the Ledger hardware 18 // wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo: 19 // https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc 20 21 package usbwallet 22 23 import ( 24 "errors" 25 "sync" 26 "time" 27 28 "github.com/ethereum/go-ethereum/accounts" 29 "github.com/ethereum/go-ethereum/event" 30 "github.com/ethereum/go-ethereum/log" 31 "github.com/karalabe/hid" 32 ) 33 34 // LedgerScheme is the protocol scheme prefixing account and wallet URLs. 35 var LedgerScheme = "ledger" 36 37 // ledgerDeviceIDs are the known device IDs that Ledger wallets use. 38 var ledgerDeviceIDs = []deviceID{ 39 {Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue 40 {Vendor: 0x2c97, Product: 0x0001}, // Ledger Nano S 41 } 42 43 // Maximum time between wallet refreshes (if USB hotplug notifications don't work). 44 const ledgerRefreshCycle = time.Second 45 46 // Minimum time between wallet refreshes to avoid USB trashing. 47 const ledgerRefreshThrottling = 500 * time.Millisecond 48 49 // LedgerHub is a accounts.Backend that can find and handle Ledger hardware wallets. 50 type LedgerHub struct { 51 refreshed time.Time // Time instance when the list of wallets was last refreshed 52 wallets []accounts.Wallet // List of Ledger devices currently tracking 53 updateFeed event.Feed // Event feed to notify wallet additions/removals 54 updateScope event.SubscriptionScope // Subscription scope tracking current live listeners 55 updating bool // Whether the event notification loop is running 56 57 quit chan chan error 58 lock sync.RWMutex 59 } 60 61 // NewLedgerHub creates a new hardware wallet manager for Ledger devices. 62 func NewLedgerHub() (*LedgerHub, error) { 63 if !hid.Supported() { 64 return nil, errors.New("unsupported platform") 65 } 66 hub := &LedgerHub{ 67 quit: make(chan chan error), 68 } 69 hub.refreshWallets() 70 return hub, nil 71 } 72 73 // Wallets implements accounts.Backend, returning all the currently tracked USB 74 // devices that appear to be Ledger hardware wallets. 75 func (hub *LedgerHub) Wallets() []accounts.Wallet { 76 // Make sure the list of wallets is up to date 77 hub.refreshWallets() 78 79 hub.lock.RLock() 80 defer hub.lock.RUnlock() 81 82 cpy := make([]accounts.Wallet, len(hub.wallets)) 83 copy(cpy, hub.wallets) 84 return cpy 85 } 86 87 // refreshWallets scans the USB devices attached to the machine and updates the 88 // list of wallets based on the found devices. 89 func (hub *LedgerHub) refreshWallets() { 90 // Don't scan the USB like crazy it the user fetches wallets in a loop 91 hub.lock.RLock() 92 elapsed := time.Since(hub.refreshed) 93 hub.lock.RUnlock() 94 95 if elapsed < ledgerRefreshThrottling { 96 return 97 } 98 // Retrieve the current list of Ledger devices 99 var ledgers []hid.DeviceInfo 100 for _, info := range hid.Enumerate(0, 0) { // Can't enumerate directly, one valid ID is the 0 wildcard 101 for _, id := range ledgerDeviceIDs { 102 if info.VendorID == id.Vendor && info.ProductID == id.Product { 103 ledgers = append(ledgers, info) 104 break 105 } 106 } 107 } 108 // Transform the current list of wallets into the new one 109 hub.lock.Lock() 110 111 wallets := make([]accounts.Wallet, 0, len(ledgers)) 112 events := []accounts.WalletEvent{} 113 114 for _, ledger := range ledgers { 115 url := accounts.URL{Scheme: LedgerScheme, Path: ledger.Path} 116 117 // Drop wallets in front of the next device or those that failed for some reason 118 for len(hub.wallets) > 0 && (hub.wallets[0].URL().Cmp(url) < 0 || hub.wallets[0].(*ledgerWallet).failed()) { 119 events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false}) 120 hub.wallets = hub.wallets[1:] 121 } 122 // If there are no more wallets or the device is before the next, wrap new wallet 123 if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 { 124 wallet := &ledgerWallet{url: &url, info: ledger, log: log.New("url", url)} 125 126 events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) 127 wallets = append(wallets, wallet) 128 continue 129 } 130 // If the device is the same as the first wallet, keep it 131 if hub.wallets[0].URL().Cmp(url) == 0 { 132 wallets = append(wallets, hub.wallets[0]) 133 hub.wallets = hub.wallets[1:] 134 continue 135 } 136 } 137 // Drop any leftover wallets and set the new batch 138 for _, wallet := range hub.wallets { 139 events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false}) 140 } 141 hub.refreshed = time.Now() 142 hub.wallets = wallets 143 hub.lock.Unlock() 144 145 // Fire all wallet events and return 146 for _, event := range events { 147 hub.updateFeed.Send(event) 148 } 149 } 150 151 // Subscribe implements accounts.Backend, creating an async subscription to 152 // receive notifications on the addition or removal of Ledger wallets. 153 func (hub *LedgerHub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { 154 // We need the mutex to reliably start/stop the update loop 155 hub.lock.Lock() 156 defer hub.lock.Unlock() 157 158 // Subscribe the caller and track the subscriber count 159 sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink)) 160 161 // Subscribers require an active notification loop, start it 162 if !hub.updating { 163 hub.updating = true 164 go hub.updater() 165 } 166 return sub 167 } 168 169 // updater is responsible for maintaining an up-to-date list of wallets stored in 170 // the keystore, and for firing wallet addition/removal events. It listens for 171 // account change events from the underlying account cache, and also periodically 172 // forces a manual refresh (only triggers for systems where the filesystem notifier 173 // is not running). 174 func (hub *LedgerHub) updater() { 175 for { 176 // Wait for a USB hotplug event (not supported yet) or a refresh timeout 177 select { 178 //case <-hub.changes: // reenable on hutplug implementation 179 case <-time.After(ledgerRefreshCycle): 180 } 181 // Run the wallet refresher 182 hub.refreshWallets() 183 184 // If all our subscribers left, stop the updater 185 hub.lock.Lock() 186 if hub.updateScope.Count() == 0 { 187 hub.updating = false 188 hub.lock.Unlock() 189 return 190 } 191 hub.lock.Unlock() 192 } 193 }