diff --git a/accounts/accounts.go b/accounts/accounts.go index b367ee2f4b3562d79597f386bddf249259e2021b..6c9e0bbcee4cd530fb5c613a31ebe2f2c66e702b 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -29,19 +29,16 @@ import ( // by the optional URL field. type Account struct { Address common.Address `json:"address"` // Ethereum account address derived from the key - URL string `json:"url"` // Optional resource locator within a backend + URL URL `json:"url"` // Optional resource locator within a backend } // Wallet represents a software or hardware wallet that might contain one or more // accounts (derived from the same seed). type Wallet interface { - // Type retrieves a textual representation of the type of the wallet. - Type() string - // URL retrieves the canonical path under which this wallet is reachable. It is // user by upper layers to define a sorting order over all wallets from multiple // backends. - URL() string + URL() URL // Status returns a textual status to aid the user in the current state of the // wallet. diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index cc8626afced6c8acfe5601b530b09bd5b606da65..e2f8262500b34e469d4bc008cef04ce7674a772e 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -42,7 +42,7 @@ const minReloadInterval = 2 * time.Second type accountsByURL []accounts.Account func (s accountsByURL) Len() int { return len(s) } -func (s accountsByURL) Less(i, j int) bool { return s[i].URL < s[j].URL } +func (s accountsByURL) Less(i, j int) bool { return s[i].URL.Cmp(s[j].URL) < 0 } func (s accountsByURL) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // AmbiguousAddrError is returned when attempting to unlock @@ -55,7 +55,7 @@ type AmbiguousAddrError struct { func (err *AmbiguousAddrError) Error() string { files := "" for i, a := range err.Matches { - files += a.URL + files += a.URL.Path if i < len(err.Matches)-1 { files += ", " } @@ -104,7 +104,7 @@ func (ac *accountCache) add(newAccount accounts.Account) { ac.mu.Lock() defer ac.mu.Unlock() - i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL >= newAccount.URL }) + i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Cmp(newAccount.URL) >= 0 }) if i < len(ac.all) && ac.all[i] == newAccount { return } @@ -155,10 +155,10 @@ func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) { if (a.Address != common.Address{}) { matches = ac.byAddr[a.Address] } - if a.URL != "" { + if a.URL.Path != "" { // If only the basename is specified, complete the path. - if !strings.ContainsRune(a.URL, filepath.Separator) { - a.URL = filepath.Join(ac.keydir, a.URL) + if !strings.ContainsRune(a.URL.Path, filepath.Separator) { + a.URL.Path = filepath.Join(ac.keydir, a.URL.Path) } for i := range matches { if matches[i].URL == a.URL { @@ -272,7 +272,7 @@ func (ac *accountCache) scan() ([]accounts.Account, error) { case (addr == common.Address{}): glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path) default: - addrs = append(addrs, accounts.Account{Address: addr, URL: path}) + addrs = append(addrs, accounts.Account{Address: addr, URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}}) } fd.Close() } diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index ea6f7d01189466340e9d1e332825903d9c720f28..3e68351ea5db7a91ace77e1ba044fd6694e50324 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -37,15 +37,15 @@ var ( cachetestAccounts = []accounts.Account{ { Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - URL: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8")}, }, { Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: filepath.Join(cachetestDir, "aaa"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")}, }, { Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - URL: filepath.Join(cachetestDir, "zzz"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")}, }, } ) @@ -63,10 +63,11 @@ func TestWatchNewFile(t *testing.T) { // Move in the files. wantAccounts := make([]accounts.Account, len(cachetestAccounts)) for i := range cachetestAccounts { - a := cachetestAccounts[i] - a.URL = filepath.Join(dir, filepath.Base(a.URL)) - wantAccounts[i] = a - if err := cp.CopyFile(a.URL, cachetestAccounts[i].URL); err != nil { + wantAccounts[i] = accounts.Account{ + Address: cachetestAccounts[i].Address, + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))}, + } + if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil { t.Fatal(err) } } @@ -107,13 +108,13 @@ func TestWatchNoDir(t *testing.T) { os.MkdirAll(dir, 0700) defer os.RemoveAll(dir) file := filepath.Join(dir, "aaa") - if err := cp.CopyFile(file, cachetestAccounts[0].URL); err != nil { + if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { t.Fatal(err) } // ks should see the account. wantAccounts := []accounts.Account{cachetestAccounts[0]} - wantAccounts[0].URL = file + wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { list = ks.Accounts() if reflect.DeepEqual(list, wantAccounts) { @@ -145,31 +146,31 @@ func TestCacheAddDeleteOrder(t *testing.T) { accs := []accounts.Account{ { Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - URL: "-309830980", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"}, }, { Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - URL: "ggg", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"}, }, { Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), - URL: "zzzzzz-the-very-last-one.keyXXX", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: "SOMETHING.key", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"}, }, { Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - URL: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"}, }, { Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: "aaa", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"}, }, { Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - URL: "zzz", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"}, }, } for _, a := range accs { @@ -210,7 +211,7 @@ func TestCacheAddDeleteOrder(t *testing.T) { for i := 0; i < len(accs); i += 2 { cache.delete(wantAccounts[i]) } - cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: "something"}) + cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}}) select { case <-notify: @@ -245,19 +246,19 @@ func TestCacheFind(t *testing.T) { accs := []accounts.Account{ { Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - URL: filepath.Join(dir, "a.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")}, }, { Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - URL: filepath.Join(dir, "b.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: filepath.Join(dir, "c.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: filepath.Join(dir, "c2.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")}, }, } for _, a := range accs { @@ -266,7 +267,7 @@ func TestCacheFind(t *testing.T) { nomatchAccount := accounts.Account{ Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: filepath.Join(dir, "something"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")}, } tests := []struct { Query accounts.Account @@ -278,7 +279,7 @@ func TestCacheFind(t *testing.T) { // by file {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, // by basename - {Query: accounts.Account{URL: filepath.Base(accs[0].URL)}, WantResult: accs[0]}, + {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]}, // by file and address {Query: accs[0], WantResult: accs[0]}, // ambiguous address, tie resolved by file @@ -294,7 +295,7 @@ func TestCacheFind(t *testing.T) { // no match error {Query: nomatchAccount, WantError: ErrNoMatch}, {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch}, - {Query: accounts.Account{URL: filepath.Base(nomatchAccount.URL)}, WantError: ErrNoMatch}, + {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, WantError: ErrNoMatch}, {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch}, } for i, test := range tests { diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 1cf5f9366281cc2c248aed5aaa1e83d03bac70cf..e2bdf09feec55013af4ee273f002fbb9aea9ec6c 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -181,8 +181,8 @@ func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Accou if err != nil { return nil, accounts.Account{}, err } - a := accounts.Account{Address: key.Address, URL: ks.JoinPath(keyFileName(key.Address))} - if err := ks.StoreKey(a.URL, key, auth); err != nil { + a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}} + if err := ks.StoreKey(a.URL.Path, key, auth); err != nil { zeroKey(key.PrivateKey) return nil, a, err } diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index ce4e87ce90f9c0f4a90e27711145e52f3a40ef5c..a01ff17e334408ded335c808ac02eb760854f20c 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -49,6 +49,9 @@ var ( // KeyStoreType is the reflect type of a keystore backend. var KeyStoreType = reflect.TypeOf(&KeyStore{}) +// KeyStoreScheme is the protocol scheme prefixing account and wallet URLs. +var KeyStoreScheme = "keystore" + // Maximum time between wallet refreshes (if filesystem notifications don't work). const walletRefreshCycle = 3 * time.Second @@ -130,22 +133,21 @@ func (ks *KeyStore) Wallets() []accounts.Wallet { // necessary wallet refreshes. func (ks *KeyStore) refreshWallets() { // Retrieve the current list of accounts + ks.mu.Lock() accs := ks.cache.accounts() // Transform the current list of wallets into the new one - ks.mu.Lock() - wallets := make([]accounts.Wallet, 0, len(accs)) events := []accounts.WalletEvent{} for _, account := range accs { // Drop wallets while they were in front of the next account - for len(ks.wallets) > 0 && ks.wallets[0].URL() < account.URL { + for len(ks.wallets) > 0 && ks.wallets[0].URL().Cmp(account.URL) < 0 { events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Arrive: false}) ks.wallets = ks.wallets[1:] } // If there are no more wallets or the account is before the next, wrap new wallet - if len(ks.wallets) == 0 || ks.wallets[0].URL() > account.URL { + if len(ks.wallets) == 0 || ks.wallets[0].URL().Cmp(account.URL) > 0 { wallet := &keystoreWallet{account: account, keystore: ks} events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) @@ -242,7 +244,7 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { // The order is crucial here. The key is dropped from the // cache after the file is gone so that a reload happening in // between won't insert it into the cache again. - err = os.Remove(a.URL) + err = os.Remove(a.URL.Path) if err == nil { ks.cache.delete(a) ks.refreshWallets() @@ -377,7 +379,7 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A if err != nil { return a, nil, err } - key, err := ks.storage.GetKey(a.Address, a.URL, auth) + key, err := ks.storage.GetKey(a.Address, a.URL.Path, auth) return a, key, err } @@ -453,8 +455,8 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco } func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { - a := accounts.Account{Address: key.Address, URL: ks.storage.JoinPath(keyFileName(key.Address))} - if err := ks.storage.StoreKey(a.URL, key, passphrase); err != nil { + a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}} + if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil { return accounts.Account{}, err } ks.cache.add(a) @@ -468,7 +470,7 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) if err != nil { return err } - return ks.storage.StoreKey(a.URL, key, newPassphrase) + return ks.storage.StoreKey(a.URL.Path, key, newPassphrase) } // ImportPreSaleKey decrypts the given Ethereum presale wallet and stores diff --git a/accounts/keystore/keystore_plain_test.go b/accounts/keystore/keystore_plain_test.go index dcb2ab901a27c2817b5b53ab47e7390f8a29d9a8..8c0eb52ea1fad943626beba324a270c4a6692ad0 100644 --- a/accounts/keystore/keystore_plain_test.go +++ b/accounts/keystore/keystore_plain_test.go @@ -52,7 +52,7 @@ func TestKeyStorePlain(t *testing.T) { if err != nil { t.Fatal(err) } - k2, err := ks.GetKey(k1.Address, account.URL, pass) + k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) if err != nil { t.Fatal(err) } @@ -73,7 +73,7 @@ func TestKeyStorePassphrase(t *testing.T) { if err != nil { t.Fatal(err) } - k2, err := ks.GetKey(k1.Address, account.URL, pass) + k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) if err != nil { t.Fatal(err) } @@ -94,7 +94,7 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err = ks.GetKey(k1.Address, account.URL, "bar"); err != ErrDecrypt { + if _, err = ks.GetKey(k1.Address, account.URL.Path, "bar"); err != ErrDecrypt { t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt) } } @@ -115,7 +115,7 @@ func TestImportPreSaleKey(t *testing.T) { if account.Address != common.HexToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { t.Errorf("imported account has wrong address %x", account.Address) } - if !strings.HasPrefix(account.URL, dir) { + if !strings.HasPrefix(account.URL.Path, dir) { t.Errorf("imported account file not in keystore directory: %q", account.URL) } } diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 6b7170a2fb444d5a6b88d27e0350243aa45c96b2..1f1935be60739c119f870d24288e57addc38dd4d 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -41,10 +41,10 @@ func TestKeyStore(t *testing.T) { if err != nil { t.Fatal(err) } - if !strings.HasPrefix(a.URL, dir) { + if !strings.HasPrefix(a.URL.Path, dir) { t.Errorf("account file %s doesn't have dir prefix", a.URL) } - stat, err := os.Stat(a.URL) + stat, err := os.Stat(a.URL.Path) if err != nil { t.Fatalf("account file %s doesn't exist (%v)", a.URL, err) } @@ -60,7 +60,7 @@ func TestKeyStore(t *testing.T) { if err := ks.Delete(a, "bar"); err != nil { t.Errorf("Delete error: %v", err) } - if common.FileExist(a.URL) { + if common.FileExist(a.URL.Path) { t.Errorf("account file %s should be gone after Delete", a.URL) } if ks.HasAddress(a.Address) { @@ -286,7 +286,7 @@ func TestWalletNotifications(t *testing.T) { // Randomly add and remove account and make sure events and wallets are in sync live := make(map[common.Address]accounts.Account) - for i := 0; i < 1024; i++ { + for i := 0; i < 256; i++ { // Execute a creation or deletion and ensure event arrival if create := len(live) == 0 || rand.Int()%4 > 0; create { // Add a new account and ensure wallet notifications arrives @@ -349,8 +349,6 @@ func TestWalletNotifications(t *testing.T) { } } } - // Sleep a bit to avoid same-timestamp keyfiles - time.Sleep(10 * time.Millisecond) } } diff --git a/accounts/keystore/keystore_wallet.go b/accounts/keystore/keystore_wallet.go index d92926478bfd8bef7ec76c1c527356a67fc5ed64..7d5507a4fc50539a0744a9c31ef7cd782f67d58f 100644 --- a/accounts/keystore/keystore_wallet.go +++ b/accounts/keystore/keystore_wallet.go @@ -30,20 +30,21 @@ type keystoreWallet struct { 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 { +func (w *keystoreWallet) URL() accounts.URL { 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" + w.keystore.mu.RLock() + defer w.keystore.mu.RUnlock() + + if _, ok := w.keystore.unlocked[w.account.Address]; ok { + return "Unlocked" + } + return "Locked" } // Open implements accounts.Wallet, but is a noop for plain wallets since there @@ -63,7 +64,7 @@ func (w *keystoreWallet) Accounts() []accounts.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) + return account.Address == w.account.Address && (account.URL == (accounts.URL{}) || account.URL == w.account.URL) } // Derive implements accounts.Wallet, but is a noop for plain wallets since there @@ -81,7 +82,7 @@ func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign @@ -97,7 +98,7 @@ func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign @@ -111,7 +112,7 @@ func (w *keystoreWallet) SignHashWithPassphrase(account accounts.Account, passph if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign @@ -125,7 +126,7 @@ func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphra if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign diff --git a/accounts/keystore/presale.go b/accounts/keystore/presale.go index 948320e0a83f37f402f80628de317fec49a6ac53..5b883c45fab0d9ed621c9dfd56ac47a0c0cd351e 100644 --- a/accounts/keystore/presale.go +++ b/accounts/keystore/presale.go @@ -38,8 +38,8 @@ func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accou return accounts.Account{}, nil, err } key.Id = uuid.NewRandom() - a := accounts.Account{Address: key.Address, URL: keyStore.JoinPath(keyFileName(key.Address))} - err = keyStore.StoreKey(a.URL, key, password) + a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: keyStore.JoinPath(keyFileName(key.Address))}} + err = keyStore.StoreKey(a.URL.Path, key, password) return a, key, err } diff --git a/accounts/manager.go b/accounts/manager.go index 0822500eb6f1a6f952e6364aae287d6dfdf30174..12a5bfcd9009991bc827aa31af35a4974ff87f35 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -134,8 +134,12 @@ func (am *Manager) Wallet(url string) (Wallet, error) { am.lock.RLock() defer am.lock.RUnlock() + parsed, err := parseURL(url) + if err != nil { + return nil, err + } for _, wallet := range am.Wallets() { - if wallet.URL() == url { + if wallet.URL() == parsed { return wallet, nil } } @@ -169,7 +173,7 @@ func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription { // 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() }) + n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) if n == len(slice) { slice = append(slice, wallet) continue @@ -183,7 +187,7 @@ func merge(slice []Wallet, wallets ...Wallet) []Wallet { // 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() }) + n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) if n == len(slice) { // Wallet not found, may happen during startup continue diff --git a/accounts/url.go b/accounts/url.go new file mode 100644 index 0000000000000000000000000000000000000000..a2d00c1c6217a7a09503778ae6971a6ead568ef7 --- /dev/null +++ b/accounts/url.go @@ -0,0 +1,79 @@ +// 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 . + +package accounts + +import ( + "encoding/json" + "errors" + "fmt" + "strings" +) + +// URL represents the canonical identification URL of a wallet or account. +// +// It is a simplified version of url.URL, with the important limitations (which +// are considered features here) that it contains value-copyable components only, +// as well as that it doesn't do any URL encoding/decoding of special characters. +// +// The former is important to allow an account to be copied without leaving live +// references to the original version, whereas the latter is important to ensure +// one single canonical form opposed to many allowed ones by the RFC 3986 spec. +// +// As such, these URLs should not be used outside of the scope of an Ethereum +// wallet or account. +type URL struct { + Scheme string // Protocol scheme to identify a capable account backend + Path string // Path for the backend to identify a unique entity +} + +// parseURL converts a user supplied URL into the accounts specific structure. +func parseURL(url string) (URL, error) { + parts := strings.Split(url, "://") + if len(parts) != 2 || parts[0] == "" { + return URL{}, errors.New("protocol scheme missing") + } + return URL{ + Scheme: parts[0], + Path: parts[1], + }, nil +} + +// String implements the stringer interface. +func (u URL) String() string { + if u.Scheme != "" { + return fmt.Sprintf("%s://%s", u.Scheme, u.Path) + } + return u.Path +} + +// MarshalJSON implements the json.Marshaller interface. +func (u URL) MarshalJSON() ([]byte, error) { + return json.Marshal(u.String()) +} + +// Cmp compares x and y and returns: +// +// -1 if x < y +// 0 if x == y +// +1 if x > y +// +func (u URL) Cmp(url URL) int { + if u.Scheme == url.Scheme { + return strings.Compare(u.Path, url.Path) + } + return strings.Compare(u.Scheme, url.Scheme) +} diff --git a/accounts/usbwallet/ledger_hub.go b/accounts/usbwallet/ledger_hub.go index ebd6fddb40f538967a57d4006d72ec21be728b9e..bd397249f1bee488fd5312af944fdd4ea2e266dd 100644 --- a/accounts/usbwallet/ledger_hub.go +++ b/accounts/usbwallet/ledger_hub.go @@ -32,6 +32,9 @@ import ( "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 @@ -124,23 +127,24 @@ func (hub *LedgerHub) refreshWallets() { for i := 0; i < len(devIDs); i++ { devID, busID := devIDs[i], busIDs[i] - url := fmt.Sprintf("ledger://%03d:%03d", busID>>8, busID&0xff) + + 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() < url { + 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() > url { - wallet := &ledgerWallet{context: hub.ctx, hardwareID: devID, locationID: busID, url: url} + 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() == url { + if hub.wallets[0].URL().Cmp(url) == 0 { wallets = append(wallets, hub.wallets[0]) hub.wallets = hub.wallets[1:] continue diff --git a/accounts/usbwallet/ledger_wallet.go b/accounts/usbwallet/ledger_wallet.go index 35e671c46ac3b0fc3695e139fdb15cd7bd70721d..f712a503f42137efdf824342775e3f6ab9f7c341 100644 --- a/accounts/usbwallet/ledger_wallet.go +++ b/accounts/usbwallet/ledger_wallet.go @@ -72,10 +72,10 @@ const ( // ledgerWallet represents a live USB Ledger hardware wallet. type ledgerWallet struct { - context *usb.Context // USB context to interface libusb through - hardwareID deviceID // USB identifiers to identify this device type - locationID uint16 // USB bus and address to identify this device instance - url string // Textual URL uniquely identifying this wallet + context *usb.Context // USB context to interface libusb through + hardwareID deviceID // USB identifiers to identify this device type + locationID uint16 // USB bus and address to identify this device instance + url *accounts.URL // Textual URL uniquely identifying this wallet device *usb.Device // USB device advertising itself as a Ledger wallet input usb.Endpoint // Input endpoint to send data to this device @@ -90,14 +90,9 @@ type ledgerWallet struct { lock sync.RWMutex } -// Type implements accounts.Wallet, returning the textual type of the wallet. -func (w *ledgerWallet) Type() string { - return "ledger" -} - // URL implements accounts.Wallet, returning the URL of the Ledger device. -func (w *ledgerWallet) URL() string { - return w.url +func (w *ledgerWallet) URL() accounts.URL { + return *w.url } // Status implements accounts.Wallet, always whether the Ledger is opened, closed @@ -113,9 +108,9 @@ func (w *ledgerWallet) Status() string { return "Closed" } if w.version == [3]byte{0, 0, 0} { - return "Ethereum app not started" + return "Ethereum app offline" } - return fmt.Sprintf("Ethereum app v%d.%d.%d", w.version[0], w.version[1], w.version[2]) + return fmt.Sprintf("Ethereum app v%d.%d.%d online", w.version[0], w.version[1], w.version[2]) } // Open implements accounts.Wallet, attempting to open a USB connection to the @@ -309,7 +304,7 @@ func (w *ledgerWallet) Derive(path string, pin bool) (accounts.Account, error) { } account := accounts.Account{ Address: address, - URL: fmt.Sprintf("%s/%s", w.url, path), + URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)}, } // If pinning was requested, track the account if pin { diff --git a/cmd/geth/accountcmd.go b/cmd/geth/accountcmd.go index 97a060e4851d29bfd009d9eebe7170d93a75948c..cd398eadb1405daec3472ae4f623d3ec3e4fe490 100644 --- a/cmd/geth/accountcmd.go +++ b/cmd/geth/accountcmd.go @@ -185,7 +185,7 @@ func accountList(ctx *cli.Context) error { var index int for _, wallet := range stack.AccountManager().Wallets() { for _, account := range wallet.Accounts() { - fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, account.URL) + fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, &account.URL) index++ } } diff --git a/cmd/geth/accountcmd_test.go b/cmd/geth/accountcmd_test.go index 7e03d7548d57dd865892751d1b3350a737ddcb58..679a7ec30ee73bea5ab00543ab1e88586c5a3ccb 100644 --- a/cmd/geth/accountcmd_test.go +++ b/cmd/geth/accountcmd_test.go @@ -53,15 +53,15 @@ func TestAccountList(t *testing.T) { defer geth.expectExit() if runtime.GOOS == "windows" { geth.expect(` -Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} {{.Datadir}}\keystore\UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 -Account #1: {f466859ead1932d743d622cb74fc058882e8648a} {{.Datadir}}\keystore\aaa -Account #2: {289d485d9771714cce91d3393d764e1311907acc} {{.Datadir}}\keystore\zzz +Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}\keystore\UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 +Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}\keystore\aaa +Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}\keystore\zzz `) } else { geth.expect(` -Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} {{.Datadir}}/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 -Account #1: {f466859ead1932d743d622cb74fc058882e8648a} {{.Datadir}}/keystore/aaa -Account #2: {289d485d9771714cce91d3393d764e1311907acc} {{.Datadir}}/keystore/zzz +Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 +Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}/keystore/aaa +Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}/keystore/zzz `) } } @@ -247,12 +247,12 @@ Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3 !! Unsupported terminal, password will be echoed. Passphrase: {{.InputLine "foobar"}} Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a: - {{keypath "1"}} - {{keypath "2"}} + keystore://{{keypath "1"}} + keystore://{{keypath "2"}} Testing your passphrase against all of them... -Your passphrase unlocked {{keypath "1"}} +Your passphrase unlocked keystore://{{keypath "1"}} In order to avoid this warning, you need to remove the following duplicate key files: - {{keypath "2"}} + keystore://{{keypath "2"}} `) geth.expectExit() @@ -283,8 +283,8 @@ Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3 !! Unsupported terminal, password will be echoed. Passphrase: {{.InputLine "wrong"}} Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a: - {{keypath "1"}} - {{keypath "2"}} + keystore://{{keypath "1"}} + keystore://{{keypath "2"}} Testing your passphrase against all of them... Fatal: None of the listed files could be unlocked. `) diff --git a/cmd/swarm/main.go b/cmd/swarm/main.go index a7fc72d57a0bc74184cc0d4b24038b3ec702bde7..f9d661bb3d8561c28e71d9e5f38a7c59b88fdf16 100644 --- a/cmd/swarm/main.go +++ b/cmd/swarm/main.go @@ -351,7 +351,7 @@ func decryptStoreAccount(ks *keystore.KeyStore, account string) *ecdsa.PrivateKe if err != nil { utils.Fatalf("Can't find swarm account key: %v", err) } - keyjson, err := ioutil.ReadFile(a.URL) + keyjson, err := ioutil.ReadFile(a.URL.Path) if err != nil { utils.Fatalf("Can't load swarm account key: %v", err) } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 0e75eb0e063be766a2150157b4d5bbb34875dc93..85bb10f17442bd0cfc6c15d3cf1367a3d6c6b797 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -228,7 +228,6 @@ func (s *PrivateAccountAPI) ListAccounts() []common.Address { // rawWallet is a JSON representation of an accounts.Wallet interface, with its // data contents extracted into plain fields. type rawWallet struct { - Type string `json:"type"` URL string `json:"url"` Status string `json:"status"` Accounts []accounts.Account `json:"accounts"` @@ -239,8 +238,7 @@ func (s *PrivateAccountAPI) ListWallets() []rawWallet { var wallets []rawWallet for _, wallet := range s.am.Wallets() { wallets = append(wallets, rawWallet{ - Type: wallet.Type(), - URL: wallet.URL(), + URL: wallet.URL().String(), Status: wallet.Status(), Accounts: wallet.Accounts(), }) diff --git a/mobile/accounts.go b/mobile/accounts.go index 897549a6b918345cd99ece377f87925360e085e5..fbaa3bf40e854fc4ef332fe2641c1dd9dc1bd0d7 100644 --- a/mobile/accounts.go +++ b/mobile/accounts.go @@ -78,9 +78,9 @@ func (a *Account) GetAddress() *Address { return &Address{a.account.Address} } -// GetFile retrieves the path of the file containing the account key. -func (a *Account) GetFile() string { - return a.account.URL +// GetURL retrieves the canonical URL of the account. +func (a *Account) GetURL() string { + return a.account.URL.String() } // KeyStore manages a key storage directory on disk. diff --git a/node/config.go b/node/config.go index 9dc1642a59dd9cc6b97d6f7a2211618bd59b217e..47ecd22a33489db1d8fe18a5c430b889a0aaee75 100644 --- a/node/config.go +++ b/node/config.go @@ -454,12 +454,12 @@ func makeAccountManager(conf *Config) (*accounts.Manager, string, error) { go func() { for event := range changes { if event.Arrive { - glog.V(logger.Info).Infof("New %s wallet appeared: %s", event.Wallet.Type(), event.Wallet.URL()) + glog.V(logger.Info).Infof("New wallet appeared: %s", event.Wallet.URL()) if err := event.Wallet.Open(""); err != nil { - glog.V(logger.Warn).Infof("Failed to open %s wallet %s: %v", event.Wallet.Type(), event.Wallet.URL(), err) + glog.V(logger.Warn).Infof("Failed to open wallet %s: %v", event.Wallet.URL(), err) } } else { - glog.V(logger.Info).Infof("Old %s wallet disappeared: %s", event.Wallet.Type(), event.Wallet.URL()) + glog.V(logger.Info).Infof("Old wallet disappeared: %s", event.Wallet.URL()) } } }()