diff --git a/backend/accounts_test.go b/backend/accounts_test.go index 672ef8224e..153c4ec010 100644 --- a/backend/accounts_test.go +++ b/backend/accounts_test.go @@ -88,9 +88,9 @@ func checkShownAccountsLen(t *testing.T, b *Backend, expectedLoaded int, expecte require.Equal(t, expectedPersisted, cntPersisted) } -// A keystore with a similar config to a BitBox02 - supporting unified and multiple accounts, no -// legacy P2PKH. -func makeBitbox02LikeKeystore() *keystoremock.KeystoreMock { +// A keystore with a similar config to a BitBox02 Multi - supporting unified and multiple accounts, +// no legacy P2PKH. +func makeBitBox02Multi() *keystoremock.KeystoreMock { fingerprint := []byte{0x55, 0x055, 0x55, 0x55} // From mnemonic: wisdom minute home employ west tail liquid mad deal catalog narrow mistake rootKey := mustXKey("xprv9s21ZrQH143K3gie3VFLgx8JcmqZNsBcBc6vAdJrsf4bPRhx69U8qZe3EYAyvRWyQdEfz7ZpyYtL8jW2d2Lfkfh6g2zivq8JdZPQqxoxLwB") @@ -122,6 +122,22 @@ func makeBitbox02LikeKeystore() *keystoremock.KeystoreMock { } } +// A keystore with a similar config to a BitBox02 Bitcon-only - supporting unified and multiple +// accounts, no legacy P2PKH. +func makeBitBox02BTCOnly() *keystoremock.KeystoreMock { + ks := makeBitBox02Multi() + ks.SupportsAccountFunc = func(coin coinpkg.Coin, meta interface{}) bool { + switch coin.(type) { + case *btc.Coin: + scriptType := meta.(signing.ScriptType) + return coin.Code() == coinpkg.CodeBTC && scriptType != signing.ScriptTypeP2PKH + default: + return false + } + } + return ks +} + func TestSortAccounts(t *testing.T) { xpub, err := hdkeychain.NewMaster(make([]byte, 32), &chaincfg.TestNet3Params) require.NoError(t, err) @@ -535,7 +551,7 @@ func TestSupportedCoins(t *testing.T) { } func TestCreateAndPersistAccountConfig(t *testing.T) { - bitbox02LikeKeystore := makeBitbox02LikeKeystore() + bitbox02LikeKeystore := makeBitBox02Multi() fingerprint := []byte{0x55, 0x055, 0x55, 0x55} // From mnemonic: wisdom minute home employ west tail liquid mad deal catalog narrow mistake @@ -1012,59 +1028,8 @@ func TestCreateAndAddAccount(t *testing.T) { // The second point is important because it's possible to use e.g. a BitBox02-Multi and a // Bitbox02-btconly with the same seed, so we shouldn't load all persisted accounts without checking. func TestAccountSupported(t *testing.T) { - // From mnemonic: wisdom minute home employ west tail liquid mad deal catalog narrow mistake - rootKey := mustXKey("xprv9s21ZrQH143K3gie3VFLgx8JcmqZNsBcBc6vAdJrsf4bPRhx69U8qZe3EYAyvRWyQdEfz7ZpyYtL8jW2d2Lfkfh6g2zivq8JdZPQqxoxLwB") - keystoreHelper := software.NewKeystore(rootKey) - - fingerprint := []byte{0x55, 0x055, 0x55, 0x55} - bb02Multi := &keystoremock.KeystoreMock{ - NameFunc: func() (string, error) { - return "Mock multi", nil - }, - RootFingerprintFunc: func() ([]byte, error) { - return fingerprint, nil - }, - SupportsAccountFunc: func(coin coinpkg.Coin, meta interface{}) bool { - switch coin.(type) { - case *btc.Coin: - scriptType := meta.(signing.ScriptType) - return scriptType != signing.ScriptTypeP2PKH - default: - return true - } - }, - SupportsUnifiedAccountsFunc: func() bool { - return true - }, - SupportsMultipleAccountsFunc: func() bool { - return true - }, - ExtendedPublicKeyFunc: keystoreHelper.ExtendedPublicKey, - } - bb02BtcOnly := &keystoremock.KeystoreMock{ - NameFunc: func() (string, error) { - return "Mock btconly", nil - }, - RootFingerprintFunc: func() ([]byte, error) { - return fingerprint, nil - }, - SupportsAccountFunc: func(coin coinpkg.Coin, meta interface{}) bool { - switch coin.(type) { - case *btc.Coin: - scriptType := meta.(signing.ScriptType) - return coin.Code() == coinpkg.CodeBTC && scriptType != signing.ScriptTypeP2PKH - default: - return false - } - }, - SupportsUnifiedAccountsFunc: func() bool { - return true - }, - SupportsMultipleAccountsFunc: func() bool { - return true - }, - ExtendedPublicKeyFunc: keystoreHelper.ExtendedPublicKey, - } + bb02Multi := makeBitBox02Multi() + bb02BtcOnly := makeBitBox02BTCOnly() b := newBackend(t, testnetDisabled, regtestDisabled) defer b.Close() @@ -1073,8 +1038,7 @@ func TestAccountSupported(t *testing.T) { // Registering a new keystore persists a set of initial default accounts. b.registerKeystore(bb02Multi) - require.Len(t, b.Accounts(), 3) - require.Len(t, b.Config().AccountsConfig().Accounts, 3) + checkShownAccountsLen(t, b, 3, 3) b.DeregisterKeystore() // Registering a Bitcoin-only like keystore loads also the altcoins that were persisted @@ -1093,14 +1057,14 @@ func TestAccountSupported(t *testing.T) { } func TestInactiveAccount(t *testing.T) { - bitbox02LikeKeystore := makeBitbox02LikeKeystore() + bitbox02LikeKeystore := makeBitBox02Multi() b := newBackend(t, testnetDisabled, regtestDisabled) defer b.Close() // 1) Registering a new keystore persists a set of initial default accounts. b.registerKeystore(bitbox02LikeKeystore) - require.Len(t, b.Accounts(), 3) - require.Len(t, b.Config().AccountsConfig().Accounts, 3) + + checkShownAccountsLen(t, b, 3, 3) require.NotNil(t, b.Config().AccountsConfig().Lookup("v0-55555555-btc-0")) require.False(t, b.Config().AccountsConfig().Lookup("v0-55555555-btc-0").Inactive) require.True(t, !lookup(b.Accounts(), "v0-55555555-btc-0").Config().Config.Inactive) @@ -1264,7 +1228,7 @@ func TestRenameAccount(t *testing.T) { b := newBackend(t, testnetDisabled, regtestDisabled) defer b.Close() - b.registerKeystore(makeBitbox02LikeKeystore()) + b.registerKeystore(makeBitBox02Multi()) require.NoError(t, b.RenameAccount("v0-55555555-btc-0", "renamed")) require.Equal(t, "renamed", b.accounts.lookup("v0-55555555-btc-0").Config().Config.Name) @@ -1275,7 +1239,7 @@ func TestMaybeAddHiddenUnusedAccounts(t *testing.T) { b := newBackend(t, testnetDisabled, regtestDisabled) defer b.Close() - b.registerKeystore(makeBitbox02LikeKeystore()) + b.registerKeystore(makeBitBox02Multi()) // Initial accounts added: Bitcoin, Litecoin, Ethereum. checkShownAccountsLen(t, b, 3, 3) @@ -1319,3 +1283,139 @@ func TestMaybeAddHiddenUnusedAccounts(t *testing.T) { require.Len(t, b.config.AccountsConfig().Accounts, 14) require.NotNil(t, b.config.AccountsConfig().Lookup("v0-55555555-btc-6")) } + +func TestWatchonly(t *testing.T) { + filterAcct := func(code accountsTypes.Code) func(acct *config.Account) bool { + return func(acct *config.Account) bool { + return acct.Code == code + } + } + + // No watchonly - accounts are loaded when registering keystore and unloaded when deregistering + // keystore. + t.Run("", func(t *testing.T) { + b := newBackend(t, testnetDisabled, regtestDisabled) + defer b.Close() + b.registerKeystore(makeBitBox02Multi()) + checkShownAccountsLen(t, b, 3, 3) + b.DeregisterKeystore() + checkShownAccountsLen(t, b, 0, 3) + }) + + // Watchonly enabled before keystore is registered - all loaded accounts thereafter become + // watched. + t.Run("", func(t *testing.T) { + b := newBackend(t, testnetDisabled, regtestDisabled) + defer b.Close() + require.NoError(t, b.SetWatchonly(true)) + b.registerKeystore(makeBitBox02Multi()) + checkShownAccountsLen(t, b, 3, 3) + b.DeregisterKeystore() + // Accounts remain loaded. + checkShownAccountsLen(t, b, 3, 3) + }) + + // Watchonly enabled while keystore is registered - all already loaded accounts become watched. + t.Run("", func(t *testing.T) { + b := newBackend(t, testnetDisabled, regtestDisabled) + defer b.Close() + b.registerKeystore(makeBitBox02Multi()) + checkShownAccountsLen(t, b, 3, 3) + require.NoError(t, b.SetWatchonly(true)) + b.DeregisterKeystore() + // Accounts remain loaded. + checkShownAccountsLen(t, b, 3, 3) + }) + + // A specific account is excluded from watchonly, then re-included. + t.Run("", func(t *testing.T) { + b := newBackend(t, testnetDisabled, regtestDisabled) + defer b.Close() + require.NoError(t, b.SetWatchonly(true)) + b.registerKeystore(makeBitBox02Multi()) + checkShownAccountsLen(t, b, 3, 3) + exclude := accountsTypes.Code("v0-55555555-btc-0") + require.NotNil(t, lookup(b.Accounts(), exclude)) + _false := false + require.NoError(t, b.AccountSetWatch(filterAcct(exclude), &_false)) + b.DeregisterKeystore() + // Accounts remain loaded except one. + checkShownAccountsLen(t, b, 2, 3) + require.Nil(t, lookup(b.Accounts(), exclude)) + + // Re-watch that account. In the UI this is possible as the edit dialog remains open after + // disabling watchonly. + _true := true + require.NoError(t, b.AccountSetWatch(filterAcct(exclude), &_true)) + checkShownAccountsLen(t, b, 3, 3) + require.NotNil(t, lookup(b.Accounts(), exclude)) + }) + + // Watchonly is disabled while some watched accounts are shown with no keystore connected. All + // accounts should disappear. When re-enabling watchonly, they do not reappear - connecting the + // keystore again is necessary. + t.Run("", func(t *testing.T) { + b := newBackend(t, testnetDisabled, regtestDisabled) + defer b.Close() + require.NoError(t, b.SetWatchonly(true)) + b.registerKeystore(makeBitBox02Multi()) + b.DeregisterKeystore() + // Accounts remain loaded. + checkShownAccountsLen(t, b, 3, 3) + + // Disable watchonly, all accounts disappear. + require.NoError(t, b.SetWatchonly(false)) + checkShownAccountsLen(t, b, 0, 3) + + // Re-enable watchonly - accounts do not show up yet. + require.NoError(t, b.SetWatchonly(true)) + checkShownAccountsLen(t, b, 0, 3) + + // Reconnecting the keystore brings back the watched accounts. + b.registerKeystore(makeBitBox02Multi()) + checkShownAccountsLen(t, b, 3, 3) + b.DeregisterKeystore() + checkShownAccountsLen(t, b, 3, 3) + }) + + // Disable global watchonly while keystore is connected does not make the accounts disappear + // yet. They only disappear once the keytore is disconnected. + t.Run("", func(t *testing.T) { + b := newBackend(t, testnetDisabled, regtestDisabled) + defer b.Close() + require.NoError(t, b.SetWatchonly(true)) + b.registerKeystore(makeBitBox02Multi()) + checkShownAccountsLen(t, b, 3, 3) + + // Disable watchonly, all accounts remain as the keystore is still connected. + require.NoError(t, b.SetWatchonly(false)) + checkShownAccountsLen(t, b, 3, 3) + + // Accounts disappear when the keystore is disconnected. + b.DeregisterKeystore() + checkShownAccountsLen(t, b, 0, 3) + }) + + // Disable watchonly for a specific account while keystore is connected does not make the + // account disappear yet. It only disappears once the keytore is disconnected. + t.Run("", func(t *testing.T) { + b := newBackend(t, testnetDisabled, regtestDisabled) + defer b.Close() + require.NoError(t, b.SetWatchonly(true)) + b.registerKeystore(makeBitBox02Multi()) + checkShownAccountsLen(t, b, 3, 3) + + exclude := accountsTypes.Code("v0-55555555-btc-0") + require.NotNil(t, lookup(b.Accounts(), exclude)) + _false := false + require.NoError(t, b.AccountSetWatch(filterAcct(exclude), &_false)) + + // Account remains loaded as the keystore is still connected. + checkShownAccountsLen(t, b, 3, 3) + + // Disconnecting the keystore makes the one account disappear that is not being watched. + b.DeregisterKeystore() + checkShownAccountsLen(t, b, 2, 3) + require.Nil(t, lookup(b.Accounts(), exclude)) + }) +} diff --git a/backend/backend_test.go b/backend/backend_test.go index 77c29b12e9..e5b463a05d 100644 --- a/backend/backend_test.go +++ b/backend/backend_test.go @@ -88,14 +88,12 @@ func TestRegisterKeystore(t *testing.T) { b := newBackend(t, testnetDisabled, regtestDisabled) defer b.Close() - require.Len(t, b.Accounts(), 0) - require.Len(t, b.Config().AccountsConfig().Accounts, 0) + checkShownAccountsLen(t, b, 0, 0) // Registering a new keystore persists a set of initial default accounts and the keystore. b.registerKeystore(ks1) require.Equal(t, ks1, b.Keystore()) - require.Len(t, b.Accounts(), 3) - require.Len(t, b.Config().AccountsConfig().Accounts, 3) + checkShownAccountsLen(t, b, 3, 3) require.NotNil(t, b.Config().AccountsConfig().Lookup("v0-55555555-btc-0")) require.NotNil(t, b.Config().AccountsConfig().Lookup("v0-55555555-ltc-0")) require.NotNil(t, b.Config().AccountsConfig().Lookup("v0-55555555-eth-0")) @@ -198,34 +196,7 @@ func lookup(accts []accounts.Interface, code accountsTypes.Code) accounts.Interf // 6) Deactivate an account. // 7) Rename an inactive account. func TestAccounts(t *testing.T) { - // From mnemonic: wisdom minute home employ west tail liquid mad deal catalog narrow mistake - rootKey := mustXKey("xprv9s21ZrQH143K3gie3VFLgx8JcmqZNsBcBc6vAdJrsf4bPRhx69U8qZe3EYAyvRWyQdEfz7ZpyYtL8jW2d2Lfkfh6g2zivq8JdZPQqxoxLwB") - keystoreHelper := software.NewKeystore(rootKey) - - ks := &keystoremock.KeystoreMock{ - NameFunc: func() (string, error) { - return "Mock keystore", nil - }, - RootFingerprintFunc: func() ([]byte, error) { - return []byte{0x55, 0x055, 0x55, 0x55}, nil - }, - SupportsAccountFunc: func(coin coinpkg.Coin, meta interface{}) bool { - switch coin.(type) { - case *btc.Coin: - scriptType := meta.(signing.ScriptType) - return scriptType != signing.ScriptTypeP2PKH - default: - return true - } - }, - SupportsUnifiedAccountsFunc: func() bool { - return true - }, - SupportsMultipleAccountsFunc: func() bool { - return true - }, - ExtendedPublicKeyFunc: keystoreHelper.ExtendedPublicKey, - } + ks := makeBitBox02Multi() b := newBackend(t, testnetDisabled, regtestDisabled) defer b.Close() @@ -235,8 +206,7 @@ func TestAccounts(t *testing.T) { // 1) Registering a new keystore persists a set of initial default accounts. b.registerKeystore(ks) - require.Len(t, b.Accounts(), 3) - require.Len(t, b.Config().AccountsConfig().Accounts, 3) + checkShownAccountsLen(t, b, 3, 3) require.NotNil(t, b.Config().AccountsConfig().Lookup("v0-55555555-btc-0")) require.NotNil(t, b.Config().AccountsConfig().Lookup("v0-55555555-ltc-0")) require.NotNil(t, b.Config().AccountsConfig().Lookup("v0-55555555-eth-0"))