Skip to content

Commit 443e66b

Browse files
committed
Merge branch 'backend-keystore-retry'
2 parents b02f020 + d4559c3 commit 443e66b

File tree

9 files changed

+140
-55
lines changed

9 files changed

+140
-55
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: err.Error(),
701+
ErrorMessage: "",
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 = errp.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) == errp.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 = err.Error()
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(errp.ErrUserAbort)
894895
}
895896

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

backend/coins/btc/handlers/handlers.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,6 @@ type Handlers struct {
5151
log *logrus.Entry
5252
}
5353

54-
const (
55-
// ErrUserAbort is returned if the user aborted the current operation.
56-
errUserAbort errp.ErrorCode = "userAbort"
57-
)
58-
5954
// NewHandlers creates a new Handlers instance.
6055
func NewHandlers(
6156
handleFunc func(string, func(*http.Request) (interface{}, error)) *mux.Route, log *logrus.Entry) *Handlers {
@@ -739,10 +734,10 @@ func (handlers *Handlers) postSignBTCAddress(r *http.Request) (interface{}, erro
739734
request.Format)
740735
if err != nil {
741736
if firmware.IsErrorAbort(err) {
742-
return response{Success: false, ErrorCode: string(errUserAbort)}, nil
737+
return response{Success: false, ErrorCode: errp.ErrUserAbort.Error()}, nil
743738
}
744739
if errp.Cause(err) == backend.ErrWrongKeystore {
745-
return response{Success: false, ErrorCode: string("wrongKeystore")}, nil
740+
return response{Success: false, ErrorCode: backend.ErrWrongKeystore.Error()}, nil
746741
}
747742

748743
handlers.log.WithField("code", account.Config().Config.Code).Error(err)

backend/connectkeystore.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,32 @@ package backend
1717
import (
1818
"bytes"
1919
"context"
20-
"errors"
2120
"time"
2221

2322
"github.com/digitalbitbox/bitbox-wallet-app/backend/keystore"
23+
"github.com/digitalbitbox/bitbox-wallet-app/util/errp"
2424
"github.com/digitalbitbox/bitbox-wallet-app/util/locker"
2525
)
2626

27-
// ErrWrongKeystore is returned if the connected keystore is not the expected one.
28-
var ErrWrongKeystore = errors.New("Wrong device/keystore connected.")
29-
var errUserAbort = errors.New("aborted by user")
30-
var errReplaced = errors.New("replaced by new prompt")
27+
const (
28+
// ErrWrongKeystore is returned if the connected keystore is not the expected one.
29+
ErrWrongKeystore errp.ErrorCode = "wrongKeystore"
30+
31+
errTimeout errp.ErrorCode = "timeout"
32+
errReplaced errp.ErrorCode = "connectReplaced"
33+
)
3134

3235
// connectKeystore is a helper struct to enable connecting to a keystore with a specific root
3336
// fingerprint.
3437
type connectKeystore struct {
3538
locker.Locker
3639
// connectKeystoreCallback, if not nil, is called when a keystore is registered.
3740
connectKeystoreCallback func(keystore.Keystore)
38-
cancelFunc context.CancelCauseFunc
41+
// retryCallback, if not nil, is called when a keystore is deregistered or when the cancel() is called.
42+
// It allows to make a new connect attempt after a wrong keystore was connected, unless the request has
43+
// been aborted.
44+
retryCallback func(retry bool)
45+
cancelFunc context.CancelCauseFunc
3946
}
4047

4148
func compareRootFingerprint(ks keystore.Keystore, rootFingerprint []byte) error {
@@ -108,6 +115,9 @@ func (c *connectKeystore) connect(
108115
case r := <-resultCh:
109116
return r.ks, r.err
110117
case <-ctx.Done():
118+
if context.Cause(ctx) == context.DeadlineExceeded {
119+
return nil, errTimeout
120+
}
111121
return nil, context.Cause(ctx)
112122
}
113123
}
@@ -123,6 +133,14 @@ func (c *connectKeystore) onConnect(keystore keystore.Keystore) {
123133
}
124134
}
125135

136+
func (c *connectKeystore) onDisconnect() {
137+
defer c.Lock()()
138+
if c.retryCallback != nil {
139+
c.retryCallback(true)
140+
c.retryCallback = nil
141+
}
142+
}
143+
126144
// cancel fails a pending call to `connect()`, making it return `cause` as the error.
127145
func (c *connectKeystore) cancel(cause error) {
128146
defer c.Lock()()
@@ -131,4 +149,13 @@ func (c *connectKeystore) cancel(cause error) {
131149
c.cancelFunc = nil
132150
c.connectKeystoreCallback = nil
133151
}
152+
if c.retryCallback != nil {
153+
c.retryCallback(false)
154+
c.retryCallback = nil
155+
}
156+
}
157+
158+
func (c *connectKeystore) SetRetryConnect(f func(retry bool)) {
159+
defer c.Lock()()
160+
c.retryCallback = f
134161
}

backend/connectkeystore_test.go

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

1717
import (
18-
"context"
1918
"sync"
2019
"testing"
2120
"time"
2221

2322
keystoremock "github.com/digitalbitbox/bitbox-wallet-app/backend/keystore/mocks"
23+
"github.com/digitalbitbox/bitbox-wallet-app/util/errp"
2424

2525
"github.com/stretchr/testify/require"
2626
)
@@ -38,7 +38,7 @@ func TestConnectKeystore(t *testing.T) {
3838

3939
t.Run("timeout", func(t *testing.T) {
4040
_, err := ck.connect(nil, fingerprint, time.Millisecond)
41-
require.Equal(t, context.DeadlineExceeded, err)
41+
require.Equal(t, errTimeout, err)
4242
})
4343

4444
t.Run("already connected", func(t *testing.T) {
@@ -53,10 +53,10 @@ func TestConnectKeystore(t *testing.T) {
5353
go func() {
5454
defer wg.Done()
5555
time.Sleep(50 * time.Millisecond)
56-
ck.cancel(errUserAbort)
56+
ck.cancel(errp.ErrUserAbort)
5757
}()
5858
_, err := ck.connect(nil, fingerprint, time.Second)
59-
require.Equal(t, errUserAbort, err)
59+
require.Equal(t, errp.ErrUserAbort, err)
6060
wg.Wait()
6161
})
6262

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
},

util/errp/errp.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,6 @@ func (e ErrorCode) Error() string {
6565
// The follwing error codes are defined here because they are shared between packages.
6666
// Package specific error codes should be defined inside the package itself.
6767
const (
68-
// ErrUserAbort is returned if the user aborted the current operation.
69-
// This is just an example: ErrUserAbort ErrorCode = "userAbort"
68+
// ErrUserAbort is returned if the user aborted the current operation.
69+
ErrUserAbort ErrorCode = "userAbort"
7070
)

0 commit comments

Comments
 (0)