16 "github.com/bytom/crypto/ed25519/chainkd"
19 // Minimum amount of time between cache reloads. This limit applies if the platform does
20 // not support change notifications. It also applies if the keystore directory does not
21 // exist yet, the code will attempt to create a watcher at most this often.
22 const minReloadInterval = 2 * time.Second
24 type keysByFile []XPub
26 func (s keysByFile) Len() int { return len(s) }
27 func (s keysByFile) Less(i, j int) bool { return s[i].File < s[j].File }
28 func (s keysByFile) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
30 // AmbiguousKeyError is returned when attempting to unlock
31 // an XPub for which more than one file exists.
32 type AmbiguousKeyError struct {
37 func (err *AmbiguousKeyError) Error() string {
39 for i, a := range err.Matches {
41 if i < len(err.Matches)-1 {
45 return fmt.Sprintf("multiple keys match keys (%s)", files)
48 // keyCache is a live index of all keys in the keystore.
49 type keyCache struct {
54 byPubs map[chainkd.XPub][]XPub
58 func newKeyCache(keydir string) *keyCache {
61 byPubs: make(map[chainkd.XPub][]XPub),
63 kc.watcher = newWatcher(kc)
67 func (kc *keyCache) hasKey(xpub chainkd.XPub) bool {
71 return len(kc.byPubs[xpub]) > 0
74 func (kc *keyCache) hasAlias(alias string) bool {
76 for _, xpub := range xpubs {
77 if xpub.Alias == alias {
84 func (kc *keyCache) add(newKey XPub) {
88 i := sort.Search(len(kc.all), func(i int) bool { return kc.all[i].File >= newKey.File })
89 if i < len(kc.all) && kc.all[i] == newKey {
92 // newKey is not in the cache.
93 kc.all = append(kc.all, XPub{})
94 copy(kc.all[i+1:], kc.all[i:])
96 kc.byPubs[newKey.XPub] = append(kc.byPubs[newKey.XPub], newKey)
99 func (kc *keyCache) keys() []XPub {
103 cpy := make([]XPub, len(kc.all))
108 func (kc *keyCache) maybeReload() {
112 if kc.watcher.running {
113 return // A watcher is running and will keep the cache up-to-date.
116 if kc.throttle == nil {
117 kc.throttle = time.NewTimer(0)
120 case <-kc.throttle.C:
122 return // The cache was reloaded recently.
127 kc.throttle.Reset(minReloadInterval)
130 // find returns the cached keys for alias if there is a unique match.
131 // The exact matching rules are explained by the documentation of Account.
132 // Callers must hold ac.mu.
133 func (kc *keyCache) find(xpub XPub) (XPub, error) {
134 // Limit search to xpub candidates if possible.
136 if (xpub.XPub != chainkd.XPub{}) {
137 matches = kc.byPubs[xpub.XPub]
140 // If only the basename is specified, complete the path.
141 if !strings.ContainsRune(xpub.File, filepath.Separator) {
142 xpub.File = filepath.Join(kc.keydir, xpub.File)
144 for i := range matches {
145 if matches[i].File == xpub.File {
146 return matches[i], nil
149 if (xpub.XPub == chainkd.XPub{}) {
150 return XPub{}, ErrLoadKey
153 switch len(matches) {
155 return matches[0], nil
157 return XPub{}, ErrLoadKey
159 err := &AmbiguousKeyError{Pubkey: hex.EncodeToString(xpub.XPub[:]), Matches: make([]XPub, len(matches))}
160 copy(err.Matches, matches)
165 // reload caches addresses of existing key.
166 // Callers must hold ac.mu.
167 func (kc *keyCache) reload() {
168 keys, err := kc.scan()
170 fmt.Printf("can't load keys: %v\n", err.Error())
174 for k := range kc.byPubs {
177 for _, k := range keys {
178 kc.byPubs[k.XPub] = append(kc.byPubs[k.XPub], k)
180 fmt.Printf("reloaded keys, cache has %d keys\n", len(kc.all))
183 func (kc *keyCache) scan() ([]XPub, error) {
184 files, err := ioutil.ReadDir(kc.keydir)
189 buf = new(bufio.Reader)
192 Alias string `json:"alias"`
193 XPub chainkd.XPub `json:"xpub"`
196 for _, fi := range files {
197 path := filepath.Join(kc.keydir, fi.Name())
199 //log.Printf("ignoring file %v", path)
200 //fmt.Printf("ignoring file %v", path)
203 fd, err := os.Open(path)
210 // Parse the address.
212 err = json.NewDecoder(buf).Decode(&keyJSON)
215 fmt.Printf("can't decode key %s: %v", path, err)
216 case (keyJSON.Alias == ""):
217 fmt.Printf("can't decode key %s: missing or void alias", path)
219 keys = append(keys, XPub{XPub: keyJSON.XPub, Alias: keyJSON.Alias, File: path})
226 func (kc *keyCache) delete(removed XPub) {
229 kc.all = removeKey(kc.all, removed)
230 if ba := removeKey(kc.byPubs[removed.XPub], removed); len(ba) == 0 {
231 delete(kc.byPubs, removed.XPub)
233 kc.byPubs[removed.XPub] = ba
237 func removeKey(slice []XPub, elem XPub) []XPub {
238 for i := range slice {
239 if slice[i] == elem {
240 return append(slice[:i], slice[i+1:]...)
246 func skipKeyFile(fi os.FileInfo) bool {
247 // Skip editor backups and UNIX-style hidden files.
248 if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") {
251 // Skip misc special files, directories (yes, symlinks too).
252 if fi.IsDir() || fi.Mode()&os.ModeType != 0 {
258 func (kc *keyCache) close() {
261 if kc.throttle != nil {