aboutsummaryrefslogtreecommitdiffstats
path: root/accounts/usbwallet/ledger_hub.go
blob: bd397249f1bee488fd5312af944fdd4ea2e266dd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// 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"
)

// LedgerScheme is the protocol scheme prefixing account and wallet URLs.
var LedgerScheme = "ledger"

// 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 := accounts.URL{Scheme: LedgerScheme, Path: fmt.Sprintf("%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().Cmp(url) < 0 {
            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().Cmp(url) > 0 {
            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().Cmp(url) == 0 {
            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()
    }
}