diff options
author | Péter Szilágyi <peterke@gmail.com> | 2017-02-08 21:53:02 +0800 |
---|---|---|
committer | Péter Szilágyi <peterke@gmail.com> | 2017-02-13 20:00:08 +0800 |
commit | c5215fdd48231622dd56aba63a5187c6e42828d4 (patch) | |
tree | 98bdb47dccb9ef3deaa571585c32db9d19166a80 /accounts | |
parent | fad5eb0a87abfc12812647344a26de8a43830182 (diff) | |
download | go-tangerine-c5215fdd48231622dd56aba63a5187c6e42828d4.tar go-tangerine-c5215fdd48231622dd56aba63a5187c6e42828d4.tar.gz go-tangerine-c5215fdd48231622dd56aba63a5187c6e42828d4.tar.bz2 go-tangerine-c5215fdd48231622dd56aba63a5187c6e42828d4.tar.lz go-tangerine-c5215fdd48231622dd56aba63a5187c6e42828d4.tar.xz go-tangerine-c5215fdd48231622dd56aba63a5187c6e42828d4.tar.zst go-tangerine-c5215fdd48231622dd56aba63a5187c6e42828d4.zip |
accounts, cmd, internal, mobile, node: canonical account URLs
Diffstat (limited to 'accounts')
-rw-r--r-- | accounts/accounts.go | 7 | ||||
-rw-r--r-- | accounts/keystore/account_cache.go | 14 | ||||
-rw-r--r-- | accounts/keystore/account_cache_test.go | 49 | ||||
-rw-r--r-- | accounts/keystore/key.go | 4 | ||||
-rw-r--r-- | accounts/keystore/keystore.go | 20 | ||||
-rw-r--r-- | accounts/keystore/keystore_plain_test.go | 8 | ||||
-rw-r--r-- | accounts/keystore/keystore_test.go | 10 | ||||
-rw-r--r-- | accounts/keystore/keystore_wallet.go | 25 | ||||
-rw-r--r-- | accounts/keystore/presale.go | 4 | ||||
-rw-r--r-- | accounts/manager.go | 10 | ||||
-rw-r--r-- | accounts/url.go | 79 | ||||
-rw-r--r-- | accounts/usbwallet/ledger_hub.go | 14 | ||||
-rw-r--r-- | accounts/usbwallet/ledger_wallet.go | 23 |
13 files changed, 174 insertions, 93 deletions
diff --git a/accounts/accounts.go b/accounts/accounts.go index b367ee2f4..6c9e0bbce 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 cc8626afc..e2f826250 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 ea6f7d011..3e68351ea 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 1cf5f9366..e2bdf09fe 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 ce4e87ce9..a01ff17e3 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 dcb2ab901..8c0eb52ea 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 6b7170a2f..1f1935be6 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 d92926478..7d5507a4f 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 948320e0a..5b883c45f 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 0822500eb..12a5bfcd9 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 000000000..a2d00c1c6 --- /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 <http://www.gnu.org/licenses/>. + +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 ebd6fddb4..bd397249f 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 35e671c46..f712a503f 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 { |