Skip to content

Commit ddff05f

Browse files
committed
backend/keystore: detect new keystores after a wrong one
With the current implementation, when the wrong keystore is connected after a keystore request, the app is not able to automatically detect the connection of the correct keystore. The only option for the user is to cancel the 'connect keystore' popup and repeat the action to create a new keystore connection request. This is counterintuitive and inefficient. This adds a `connectkeystore.retryCallback` that allows to wait for a new keystore to be connected and to check if that is the expected one.
1 parent 20b958f commit ddff05f

File tree

7 files changed

+132
-42
lines changed

7 files changed

+132
-42
lines changed

backend/accounts.go

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ func (backend *Backend) createAndAddAccount(coin coinpkg.Coin, persistedConfig *
660660
type data struct {
661661
Type string `json:"typ"`
662662
KeystoreName string `json:"keystoreName"`
663-
ErrorCode string `json:"errorCode"`
663+
ErrorCode string `json:"errorCode,omitempty"`
664664
ErrorMessage string `json:"errorMessage"`
665665
}
666666
accountRootFingerprint, err := persistedConfig.SigningConfigurations.RootFingerprint()
@@ -672,45 +672,81 @@ func (backend *Backend) createAndAddAccount(coin coinpkg.Coin, persistedConfig *
672672
if err == nil {
673673
keystoreName = persistedKeystore.Name
674674
}
675-
backend.Notify(observable.Event{
676-
Subject: "connect-keystore",
677-
Action: action.Replace,
678-
Object: data{
679-
Type: "connect",
680-
KeystoreName: keystoreName,
681-
},
682-
})
683-
ks, err := backend.connectKeystore.connect(
684-
backend.Keystore(),
685-
accountRootFingerprint,
686-
20*time.Minute,
687-
)
675+
var ks keystore.Keystore
676+
timeout := 20 * time.Minute
677+
outerLoop:
678+
for {
679+
backend.Notify(observable.Event{
680+
Subject: "connect-keystore",
681+
Action: action.Replace,
682+
Object: data{
683+
Type: "connect",
684+
KeystoreName: keystoreName,
685+
},
686+
})
687+
ks, err = backend.connectKeystore.connect(
688+
backend.Keystore(),
689+
accountRootFingerprint,
690+
timeout,
691+
)
692+
if err == nil || errp.Cause(err) != ErrWrongKeystore {
693+
break
694+
} else {
695+
backend.Notify(observable.Event{
696+
Subject: "connect-keystore",
697+
Action: action.Replace,
698+
Object: data{
699+
Type: "error",
700+
ErrorCode: "wrongKeystore",
701+
ErrorMessage: err.Error(),
702+
},
703+
})
704+
c := make(chan bool)
705+
// retryCallback is called when the current keystore is deregistered or when
706+
// CancelConnectKeystore() is called.
707+
// In the first case it allows to make a new connection attempt, in the last one
708+
// it'll make this function return ErrUserAbort.
709+
backend.connectKeystore.SetRetryConnect(func(retry bool) {
710+
c <- retry
711+
})
712+
select {
713+
case retry := <-c:
714+
if !retry {
715+
err = ErrUserAbort
716+
break outerLoop
717+
}
718+
case <-time.After(timeout):
719+
backend.connectKeystore.SetRetryConnect(nil)
720+
err = errTimeout
721+
break outerLoop
722+
}
723+
}
724+
}
688725
switch {
689726
case errp.Cause(err) == errReplaced:
690727
// If a previous connect-keystore request is in progress, the previous request is
691728
// failed, but we don't dismiss the prompt, as the new prompt has already been shown
692-
// by the above "connect" notification.y
693-
case err == nil || errp.Cause(err) == errUserAbort:
729+
// by the above "connect" notification.
730+
case err == nil || errp.Cause(err) == ErrUserAbort:
694731
// Dismiss prompt after success or upon user abort.
695-
696732
backend.Notify(observable.Event{
697733
Subject: "connect-keystore",
698734
Action: action.Replace,
699735
Object: nil,
700736
})
701737
default:
702-
// Display error to user.
703-
errorCode := ""
704-
if errp.Cause(err) == ErrWrongKeystore {
705-
errorCode = "wrongKeystore"
738+
var errorCode = ""
739+
if errp.Cause(err) == errTimeout {
740+
errorCode = "timeout"
706741
}
742+
// Display error to user.
707743
backend.Notify(observable.Event{
708744
Subject: "connect-keystore",
709745
Action: action.Replace,
710746
Object: data{
711747
Type: "error",
712-
ErrorCode: errorCode,
713748
ErrorMessage: err.Error(),
749+
ErrorCode: errorCode,
714750
},
715751
})
716752
}

backend/backend.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,7 @@ func (backend *Backend) DeregisterKeystore() {
687687
// keystore. For now we just remove all, then re-add the rest.
688688
backend.initPersistedAccounts()
689689
backend.emitAccountsStatusChanged()
690+
backend.connectKeystore.onDisconnect()
690691
}
691692

692693
// Register registers the given device at this backend.
@@ -890,7 +891,7 @@ func (backend *Backend) GetAccountFromCode(acctCode accountsTypes.Code) (account
890891

891892
// CancelConnectKeystore cancels a pending keystore connection request if one exists.
892893
func (backend *Backend) CancelConnectKeystore() {
893-
backend.connectKeystore.cancel(errUserAbort)
894+
backend.connectKeystore.cancel(ErrUserAbort)
894895
}
895896

896897
// SetWatchonly sets the keystore's watchonly flag to `watchonly`.

backend/connectkeystore.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,24 @@ import (
2626

2727
// ErrWrongKeystore is returned if the connected keystore is not the expected one.
2828
var ErrWrongKeystore = errors.New("Wrong device/keystore connected.")
29-
var errUserAbort = errors.New("aborted by user")
29+
30+
// ErrUserAbort is raised when a keystore connection is aborted calling CancelConnectKeystore().
31+
var ErrUserAbort = errors.New("aborted by user")
3032
var errReplaced = errors.New("replaced by new prompt")
3133

34+
var errTimeout = errors.New("timeout")
35+
3236
// connectKeystore is a helper struct to enable connecting to a keystore with a specific root
3337
// fingerprint.
3438
type connectKeystore struct {
3539
locker.Locker
3640
// connectKeystoreCallback, if not nil, is called when a keystore is registered.
3741
connectKeystoreCallback func(keystore.Keystore)
38-
cancelFunc context.CancelCauseFunc
42+
// retryCallback, if not nil, is called when a keystore is deregistered or when the cancel() is called.
43+
// It allows to make a new connect attempt after a wrong keystore was connected, unless the request has
44+
// been aborted.
45+
retryCallback func(retry bool)
46+
cancelFunc context.CancelCauseFunc
3947
}
4048

4149
func compareRootFingerprint(ks keystore.Keystore, rootFingerprint []byte) error {
@@ -108,6 +116,9 @@ func (c *connectKeystore) connect(
108116
case r := <-resultCh:
109117
return r.ks, r.err
110118
case <-ctx.Done():
119+
if context.Cause(ctx) == context.DeadlineExceeded {
120+
return nil, errTimeout
121+
}
111122
return nil, context.Cause(ctx)
112123
}
113124
}
@@ -123,6 +134,14 @@ func (c *connectKeystore) onConnect(keystore keystore.Keystore) {
123134
}
124135
}
125136

137+
func (c *connectKeystore) onDisconnect() {
138+
defer c.Lock()()
139+
if c.retryCallback != nil {
140+
c.retryCallback(true)
141+
c.retryCallback = nil
142+
}
143+
}
144+
126145
// cancel fails a pending call to `connect()`, making it return `cause` as the error.
127146
func (c *connectKeystore) cancel(cause error) {
128147
defer c.Lock()()
@@ -131,4 +150,13 @@ func (c *connectKeystore) cancel(cause error) {
131150
c.cancelFunc = nil
132151
c.connectKeystoreCallback = nil
133152
}
153+
if c.retryCallback != nil {
154+
c.retryCallback(false)
155+
c.retryCallback = nil
156+
}
157+
}
158+
159+
func (c *connectKeystore) SetRetryConnect(f func(retry bool)) {
160+
defer c.Lock()()
161+
c.retryCallback = f
134162
}

backend/connectkeystore_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package backend
1616

1717
import (
18-
"context"
1918
"sync"
2019
"testing"
2120
"time"
@@ -38,7 +37,7 @@ func TestConnectKeystore(t *testing.T) {
3837

3938
t.Run("timeout", func(t *testing.T) {
4039
_, err := ck.connect(nil, fingerprint, time.Millisecond)
41-
require.Equal(t, context.DeadlineExceeded, err)
40+
require.Equal(t, errTimeout, err)
4241
})
4342

4443
t.Run("already connected", func(t *testing.T) {
@@ -53,10 +52,10 @@ func TestConnectKeystore(t *testing.T) {
5352
go func() {
5453
defer wg.Done()
5554
time.Sleep(50 * time.Millisecond)
56-
ck.cancel(errUserAbort)
55+
ck.cancel(ErrUserAbort)
5756
}()
5857
_, err := ck.connect(nil, fingerprint, time.Second)
59-
require.Equal(t, errUserAbort, err)
58+
require.Equal(t, ErrUserAbort, err)
6059
wg.Wait()
6160
})
6261

frontends/web/src/api/backend.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,15 @@ export const socksProxyCheck = (proxyAddress: string): Promise<ISuccess> => {
7676
return apiPost('socksproxy/check', proxyAddress);
7777
};
7878

79+
export type TConnectKeystoreErrorCode = 'wrongKeystore' | 'timeout';
80+
7981
export type TSyncConnectKeystore = null | {
8082
typ: 'connect';
8183
keystoreName: string;
8284
} | {
8385
typ: 'error';
84-
errorCode: 'wrongKeystore';
85-
errorMessage: '';
86+
errorCode?: TConnectKeystoreErrorCode;
87+
errorMessage: string;
8688
};
8789

8890
/**

frontends/web/src/components/keystoreconnectprompt.tsx

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
*/
1616

1717
import { useTranslation } from 'react-i18next';
18+
import { ReactElement } from 'react';
1819
import { Button } from './forms';
19-
import { cancelConnectKeystore, syncConnectKeystore } from '../api/backend';
20+
import { TConnectKeystoreErrorCode, cancelConnectKeystore, syncConnectKeystore } from '../api/backend';
2021
import { useSubscribeReset } from '../hooks/api';
2122
import { Dialog, DialogButtons } from './dialog/dialog';
2223
import { BitBox02StylizedDark, BitBox02StylizedLight, Cancel, PointToBitBox02 } from './icon';
@@ -29,9 +30,35 @@ export function KeystoreConnectPrompt() {
2930
const { isDarkMode } = useDarkmode();
3031

3132
const [data, reset] = useSubscribeReset(syncConnectKeystore());
33+
34+
const cancelAndReset = () => {
35+
// This is needed to close the popup in case of timeout exception.
36+
reset();
37+
cancelConnectKeystore();
38+
};
39+
40+
const errorMessage = (errorCode: TConnectKeystoreErrorCode | undefined): ReactElement | null => {
41+
switch (errorCode) {
42+
case 'wrongKeystore':
43+
return (<>
44+
{t('error.wrongKeystore')}
45+
<br />
46+
<br />
47+
{t('error.wrongKeystore2')}
48+
</>);
49+
case 'timeout':
50+
return (<>
51+
{t('error.keystoreTimeout')}
52+
</>);
53+
default:
54+
return null;
55+
}
56+
};
57+
3258
if (!data) {
3359
return null;
3460
}
61+
3562
switch (data.typ) {
3663
case 'connect':
3764
return (
@@ -51,35 +78,31 @@ export function KeystoreConnectPrompt() {
5178
<SkipForTesting />
5279
</div>
5380
<DialogButtons>
54-
<Button secondary onClick={() => cancelConnectKeystore()}>{t('dialog.cancel')}</Button>
81+
<Button secondary onClick={cancelConnectKeystore}>{t('dialog.cancel')}</Button>
5582
</DialogButtons>
5683
</Dialog>
5784
);
5885
case 'error':
86+
const err = errorMessage(data.errorCode);
5987
return (
6088
<Dialog title={t('welcome.connect')} medium open>
6189
<p className={styles.text}>
62-
{data.errorCode === 'wrongKeystore' ?
63-
<>
64-
{t('error.wrongKeystore')}
65-
<br />
66-
<br />
67-
{t('error.wrongKeystore2')}
68-
</> : data.errorMessage}
90+
{ err ? err : data.errorMessage }
6991
</p>
7092
<div className={`${styles.bitboxContainer} ${styles.failed}`}>
7193
<Cancel className={styles.cancelIcon} />
7294
{isDarkMode ?
7395
<BitBox02StylizedLight className={styles.bitboxImage} /> :
7496
<BitBox02StylizedDark className={styles.bitboxImage} />
7597
}
98+
<SkipForTesting />
7699
</div>
77100
<DialogButtons>
78-
<Button secondary onClick={() => reset()}>{t('dialog.cancel')}</Button>
101+
<Button secondary onClick={cancelAndReset}>{t('dialog.cancel')}</Button>
79102
</DialogButtons>
80103
</Dialog>
81104
);
82105
default:
83106
return null;
84107
}
85-
}
108+
}

frontends/web/src/locales/en/app.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,7 @@
673673
"aoppUnsupportedFormat": "There are no available accounts that support the requested address format.",
674674
"aoppUnsupportedKeystore": "The connected device cannot sign messages for this asset.",
675675
"aoppVersion": "Unknown version.",
676+
"keystoreTimeout": "Wallet request expired. Please try again.",
676677
"wrongKeystore": "Wrong wallet connected. Please make sure to insert the correct device matching this account.",
677678
"wrongKeystore2": " If you are using the optional passphrase, make sure you have entered the correct passphrase for the account."
678679
},

0 commit comments

Comments
 (0)