Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add watchonly setting to accounts & handle watchonly account loading/unloading #2327

Merged
merged 6 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
137 changes: 112 additions & 25 deletions backend/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ import (
"github.com/ethereum/go-ethereum/params"
)

// Set the `watch` setting on new accounts to this default value.
// For now we keep it unset, and have users opt-in to watching specific accounts.
//
// We set it to `nil` not `false` so that we reserve the possibility to default all accounts to
// watch-only for accounts where the user hasn't made an active decision (e.g. turn on watch-only
// for all accounts of a keystore if the user did not activate/deactivate watch-only manually on any
// of them).
var defaultWatch *bool = nil

// hardenedKeystart is the BIP44 offset to make a keypath element hardened.
const hardenedKeystart uint32 = hdkeychain.HardenedKeyStart

Expand Down Expand Up @@ -478,6 +487,46 @@ func (backend *Backend) RenameAccount(accountCode accountsTypes.Code, name strin
return nil
}

// AccountSetWatch sets the account's persisted watch flag to `watch`. Set to `true` if the account
// should be loaded even if its keystore is not connected.
// If `watch` is set to `false`, the account is unloaded and the frontend notified.
func (backend *Backend) AccountSetWatch(accountCode accountsTypes.Code, watch bool) error {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not unit tested yet, i plan on adding one at some point

err := backend.config.ModifyAccountsConfig(func(accountsConfig *config.AccountsConfig) error {
acct := accountsConfig.Lookup(accountCode)
if acct == nil {
return errp.Newf("Could not find account %s", accountCode)
}
acct.Watch = &watch
return nil
})
if err != nil {
return err
}
defer backend.accountsAndKeystoreLock.Lock()()

// ETH tokens inherit the Watch-flag from the parent ETH account.
// If the watch status of an ETH account was changed, we update it for its tokens as well.
//
// This ensures that removing a keystore after setting an ETH account including tokens to
// watchonly results in the account *and* the tokens remaining loaded.
if acct := backend.accounts.lookup(accountCode); acct != nil {
if acct.Config().Config.CoinCode == coinpkg.CodeETH {
for _, erc20TokenCode := range acct.Config().Config.ActiveTokens {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jadzeidan fyi the erc20-as-their-own-accounts model is really quite bad for the code 🙈 special cases everywhere. it would be very nice (from the code point of view) if we could bundle the tokens into the parent ETH account. could also reduce the confusion about that the fees for an erc20 token tx are coming from a "different" account.

erc20AccountCode := Erc20AccountCode(accountCode, erc20TokenCode)
if tokenAcct := backend.accounts.lookup(erc20AccountCode); tokenAcct != nil {
tokenAcct.Config().Config.Watch = &watch
}
}
}
}

if !watch {
backend.initAccounts(false)
backend.emitAccountsStatusChanged()
}
return nil
}

// addAccount adds the given account to the backend.
// The accountsAndKeystoreLock must be held when calling this function.
func (backend *Backend) addAccount(account accounts.Interface) {
Expand All @@ -493,6 +542,10 @@ func (backend *Backend) addAccount(account accounts.Interface) {

// The accountsAndKeystoreLock must be held when calling this function.
func (backend *Backend) createAndAddAccount(coin coinpkg.Coin, persistedConfig *config.Account) {
if backend.accounts.lookup(persistedConfig.Code) != nil {
// Do not create/load account if it is already loaded.
return
}
var account accounts.Interface
accountConfig := &accounts.AccountConfig{
Config: persistedConfig,
Expand Down Expand Up @@ -549,9 +602,15 @@ func (backend *Backend) createAndAddAccount(coin coinpkg.Coin, persistedConfig *
tokenName = fmt.Sprintf("%s %d", tokenName, accountNumber+1)
}

var watchToken *bool
if persistedConfig.Watch != nil {
wCopy := *persistedConfig.Watch
watchToken = &wCopy
}
erc20Config := &config.Account{
Inactive: persistedConfig.Inactive,
HiddenBecauseUnused: persistedConfig.HiddenBecauseUnused,
Watch: watchToken,
CoinCode: erc20CoinCode,
Name: tokenName,
Code: erc20AccountCode,
Expand Down Expand Up @@ -655,6 +714,7 @@ func (backend *Backend) persistBTCAccountConfig(
if keystore.SupportsUnifiedAccounts() {
return backend.persistAccount(config.Account{
HiddenBecauseUnused: hiddenBecauseUnused,
Watch: defaultWatch,
CoinCode: coin.Code(),
Name: name,
Code: code,
Expand All @@ -674,6 +734,7 @@ func (backend *Backend) persistBTCAccountConfig(

err := backend.persistAccount(config.Account{
HiddenBecauseUnused: hiddenBecauseUnused,
Watch: defaultWatch,
CoinCode: coin.Code(),
Name: suffixedName,
Code: splitAccountCode(code, cfg.ScriptType()),
Expand Down Expand Up @@ -730,6 +791,7 @@ func (backend *Backend) persistETHAccountConfig(

return backend.persistAccount(config.Account{
HiddenBecauseUnused: hiddenBecauseUnused,
Watch: defaultWatch,
CoinCode: coin.Code(),
Name: name,
Code: code,
Expand All @@ -740,39 +802,56 @@ func (backend *Backend) persistETHAccountConfig(

// The accountsAndKeystoreLock must be held when calling this function.
func (backend *Backend) initPersistedAccounts() {
if backend.keystore == nil {
return
}
// Only load accounts which belong to connected keystores.
rootFingerprint, err := backend.keystore.RootFingerprint()
if err != nil {
backend.log.WithError(err).Error("Could not retrieve root fingerprint")
return
}
keystoreConnected := func(account *config.Account) bool {
// Only load accounts which belong to connected keystores or for which watchonly is enabled.
keystoreConnectedOrWatch := func(account *config.Account) bool {
if account.IsWatch() {
return true
}

if backend.keystore == nil {
return false
}
rootFingerprint, err := backend.keystore.RootFingerprint()
if err != nil {
backend.log.WithError(err).Error("Could not retrieve root fingerprint")
return false
}

return account.SigningConfigurations.ContainsRootFingerprint(rootFingerprint)
}

persistedAccounts := backend.config.AccountsConfig()

// In this loop, we add all accounts that match the filter, except for the ones whose signing
// configuration is not supported by the connected keystore. The latter can happen for example
// if a user connects a BitBox02 Multi edition first, which persists some altcoin accounts, and
// then connects a BitBox02 BTC-only with the same seed. In that case, the unsupported accounts
// will not be loaded, unless they have been marked as watch-only.
outer:
for _, account := range backend.filterAccounts(&persistedAccounts, keystoreConnected) {
for _, account := range backend.filterAccounts(&persistedAccounts, keystoreConnectedOrWatch) {
account := account
coin, err := backend.Coin(account.CoinCode)
if err != nil {
backend.log.Errorf("skipping persisted account %s/%s, could not find coin",
account.CoinCode, account.Code)
continue
}
switch coin.(type) {
case *btc.Coin:
for _, cfg := range account.SigningConfigurations {
if !backend.keystore.SupportsAccount(coin, cfg.ScriptType()) {
continue outer

// Watch-only accounts are loaded regardless, and if later e.g. a BitBox02 BTC-only is
// inserted with the same seed as a Multi, we will need to catch that mismatch when the
// keystore will be used to e.g. display an Ethereum address etc.
if backend.keystore != nil && !account.IsWatch() {
switch coin.(type) {
case *btc.Coin:
for _, cfg := range account.SigningConfigurations {
if !backend.keystore.SupportsAccount(coin, cfg.ScriptType()) {
continue outer
}
}
default:
if !backend.keystore.SupportsAccount(coin, nil) {
continue
}
}
default:
if !backend.keystore.SupportsAccount(coin, nil) {
continue
}
}

Expand Down Expand Up @@ -898,9 +977,10 @@ func (backend *Backend) updatePersistedAccounts(
}

// The accountsAndKeystoreLock must be held when calling this function.
func (backend *Backend) initAccounts() {
// if force is true, all accounts are uninitialized first, even if they are watch-only.
func (backend *Backend) initAccounts(force bool) {
// Since initAccounts replaces all previous accounts, we need to properly close them first.
backend.uninitAccounts()
backend.uninitAccounts(force)

backend.initPersistedAccounts()

Expand All @@ -920,19 +1000,26 @@ func (backend *Backend) ReinitializeAccounts() {
defer backend.accountsAndKeystoreLock.Lock()()

backend.log.Info("Reinitializing accounts")
backend.initAccounts()
backend.initAccounts(true)
}

// The accountsAndKeystoreLock must be held when calling this function.
func (backend *Backend) uninitAccounts() {
// if force is true, all accounts are uninitialized, even if they are watch-only.
func (backend *Backend) uninitAccounts(force bool) {
keep := []accounts.Interface{}
for _, account := range backend.accounts {
account := account
if !force && account.Config().Config.IsWatch() {
// Do not uninit/remove account that is being watched.
keep = append(keep, account)
continue
}
if backend.onAccountUninit != nil {
backend.onAccountUninit(account)
}
account.Close()
}
backend.accounts = []accounts.Interface{}
backend.accounts = keep
}

// maybeAddHiddenUnusedAccounts adds a hidden account for scanning to facilitate accounts discovery.
Expand Down
36 changes: 36 additions & 0 deletions backend/accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {

require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "btc",
Name: "bitcoin 2",
Code: "v0-55555555-btc-1",
Expand All @@ -583,6 +584,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
require.Equal(t, "v0-55555555-ltc-1", string(acctCode))
require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "ltc",
Name: "litecoin 2",
Code: "v0-55555555-ltc-1",
Expand All @@ -604,6 +606,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
require.Equal(t, "v0-55555555-eth-1", string(acctCode))
require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "eth",
Name: "ethereum 2",
Code: "v0-55555555-eth-1",
Expand All @@ -624,6 +627,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
require.Equal(t, "v0-55555555-btc-2", string(acctCode))
require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "btc",
Name: "bitcoin 3",
Code: "v0-55555555-btc-2",
Expand All @@ -646,6 +650,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
require.Equal(t, "v0-55555555-ltc-2", string(acctCode))
require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "ltc",
Name: "litecoin 2",
Code: "v0-55555555-ltc-2",
Expand All @@ -667,6 +672,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
require.Equal(t, "v0-55555555-eth-2", string(acctCode))
require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "eth",
Name: "ethereum 2",
Code: "v0-55555555-eth-2",
Expand All @@ -682,6 +688,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
require.Equal(t,
&config.Account{
HiddenBecauseUnused: true,
Watch: nil,
CoinCode: "btc",
Name: "Bitcoin 4",
Code: "v0-55555555-btc-3",
Expand All @@ -704,6 +711,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
require.Equal(t, "v0-55555555-btc-3", string(acctCode))
require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "btc",
Name: "bitcoin 4 new name",
Code: "v0-55555555-btc-3",
Expand Down Expand Up @@ -734,6 +742,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
require.Equal(t, "v0-55555555-btc-0", string(acctCode))
require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "btc",
Name: "bitcoin 1: bech32",
Code: "v0-55555555-btc-0-p2wpkh",
Expand All @@ -745,6 +754,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
)
require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "btc",
Name: "bitcoin 1",
Code: "v0-55555555-btc-0-p2wpkh-p2sh",
Expand All @@ -756,6 +766,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
)
require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "btc",
Name: "bitcoin 1: legacy",
Code: "v0-55555555-btc-0-p2pkh",
Expand All @@ -777,6 +788,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
require.Equal(t, "v0-55555555-ltc-0", string(acctCode))
require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "ltc",
Name: "litecoin 1: bech32",
Code: "v0-55555555-ltc-0-p2wpkh",
Expand All @@ -788,6 +800,7 @@ func TestCreateAndPersistAccountConfig(t *testing.T) {
)
require.Equal(t,
&config.Account{
Watch: nil,
CoinCode: "ltc",
Name: "litecoin 1",
Code: "v0-55555555-ltc-0-p2wpkh-p2sh",
Expand Down Expand Up @@ -1037,8 +1050,31 @@ func TestAccountSupported(t *testing.T) {
b.registerKeystore(bb02Multi)
require.Len(t, b.Accounts(), 3)
require.Len(t, b.Config().AccountsConfig().Accounts, 3)
// Mark all as watch-only.
require.NoError(t, b.config.ModifyAccountsConfig(func(cfg *config.AccountsConfig) error {
for _, acct := range cfg.Accounts {
f := true
acct.Watch = &f
}
return nil
}))

b.DeregisterKeystore()
// Registering a Bitcoin-only like keystore loads also the altcoins that were persisted
// previously, because they are marked watch-only, so they should be visible.
b.registerKeystore(bb02BtcOnly)
require.Len(t, b.Accounts(), 3)
require.Len(t, b.Config().AccountsConfig().Accounts, 3)

b.DeregisterKeystore()
// If watch-only is disabled, then these will not be loaded if not supported by the keystore.
require.NoError(t, b.config.ModifyAccountsConfig(func(cfg *config.AccountsConfig) error {
for _, acct := range cfg.Accounts {
f := false
acct.Watch = &f
}
return nil
}))
// Registering a Bitcoin-only like keystore loads only the Bitcoin account, even though altcoins
// were persisted previously.
b.registerKeystore(bb02BtcOnly)
Expand Down
6 changes: 3 additions & 3 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ func (backend *Backend) registerKeystore(keystore keystore.Keystore) {
log.WithError(err).Error("Could not persist default accounts")
}

backend.initAccounts()
backend.initAccounts(false)

backend.aoppKeystoreRegistered()
}
Expand All @@ -578,7 +578,7 @@ func (backend *Backend) DeregisterKeystore() {
Action: action.Reload,
})

backend.uninitAccounts()
backend.uninitAccounts(false)
// TODO: classify accounts by keystore, remove only the ones belonging to the deregistered
// keystore. For now we just remove all, then re-add the rest.
backend.initPersistedAccounts()
Expand Down Expand Up @@ -721,7 +721,7 @@ func (backend *Backend) Close() error {

backend.ratesUpdater.Stop()

backend.uninitAccounts()
backend.uninitAccounts(true)

for _, coin := range backend.coins {
if err := coin.Close(); err != nil {
Expand Down