Skip to content

Commit eba42d7

Browse files
committed
Merge branch 'staging-send-to-self-dropdown'
2 parents e4c2874 + 235cccd commit eba42d7

30 files changed

+935
-119
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Changelog
22

33
## Unreleased
4+
- Add a dropdown on the "Receiver address" input in the send screen to select an account
45
- Add feedback link to guide and about settings
56
- Move active currencies to top of currency dropdown
67

backend/devices/bitbox02/keystore.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,3 +556,10 @@ func (keystore *keystore) SupportsPaymentRequests() error {
556556
}
557557
return keystorePkg.ErrFirmwareUpgradeRequired
558558
}
559+
560+
// Features reports optional capabilities supported by the BitBox02 keystore.
561+
func (keystore *keystore) Features() *keystorePkg.Features {
562+
return &keystorePkg.Features{
563+
SupportsSendToSelf: keystore.device.Version().AtLeast(semver.NewSemVer(9, 22, 0)),
564+
}
565+
}

backend/handlers/handlers.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ func NewHandlers(
220220
getAPIRouterNoError(apiRouter)("/dev-servers", handlers.getDevServers).Methods("GET")
221221
getAPIRouterNoError(apiRouter)("/account-add", handlers.postAddAccount).Methods("POST")
222222
getAPIRouterNoError(apiRouter)("/keystores", handlers.getKeystores).Methods("GET")
223+
getAPIRouterNoError(apiRouter)("/keystore/{rootFingerprint}/features", handlers.getKeystoreFeatures).Methods("GET")
223224
getAPIRouterNoError(apiRouter)("/accounts", handlers.getAccounts).Methods("GET")
224225
getAPIRouter(apiRouter)("/accounts/balance-summary", handlers.getAccountsBalanceSummary).Methods("GET")
225226
getAPIRouterNoError(apiRouter)("/set-account-active", handlers.postSetAccountActive).Methods("POST")
@@ -401,6 +402,8 @@ type accountJSON struct {
401402
IsToken bool `json:"isToken"`
402403
ActiveTokens []activeToken `json:"activeTokens,omitempty"`
403404
BlockExplorerTxPrefix string `json:"blockExplorerTxPrefix"`
405+
// Number of the account per coin per keystore, starting at 0. Nil if unknown.
406+
AccountNumber *uint16 `json:"accountNumber"`
404407
}
405408

406409
func newAccountJSON(
@@ -411,6 +414,11 @@ func newAccountJSON(
411414
eth, ok := account.Coin().(*eth.Coin)
412415
isToken := ok && eth.ERC20Token() != nil
413416
watch := account.Config().Config.Watch
417+
var accountNumberPtr *uint16
418+
accountNumber, err := account.Config().Config.SigningConfigurations.AccountNumber()
419+
if err == nil {
420+
accountNumberPtr = &accountNumber
421+
}
414422
return &accountJSON{
415423
Keystore: keystoreJSON{
416424
Keystore: keystore,
@@ -427,6 +435,7 @@ func newAccountJSON(
427435
IsToken: isToken,
428436
ActiveTokens: activeTokens,
429437
BlockExplorerTxPrefix: account.Coin().BlockExplorerTransactionURLPrefix(),
438+
AccountNumber: accountNumberPtr,
430439
}
431440
}
432441

@@ -598,6 +607,55 @@ func (handlers *Handlers) getKeystores(*http.Request) interface{} {
598607
return keystores
599608
}
600609

610+
func (handlers *Handlers) getKeystoreFeatures(r *http.Request) interface{} {
611+
type response struct {
612+
Success bool `json:"success"`
613+
ErrorMessage string `json:"errorMessage,omitempty"`
614+
Features *keystore.Features `json:"features,omitempty"`
615+
}
616+
617+
rootFingerprintHex := mux.Vars(r)["rootFingerprint"]
618+
rootFingerprint, err := hex.DecodeString(rootFingerprintHex)
619+
if err != nil {
620+
handlers.log.WithError(err).Error("invalid root fingerprint for features request")
621+
return response{
622+
Success: false,
623+
ErrorMessage: err.Error(),
624+
}
625+
}
626+
627+
connectedKeystore := handlers.backend.Keystore()
628+
if connectedKeystore == nil {
629+
handlers.log.Warn("features requested but no keystore connected")
630+
return response{
631+
Success: false,
632+
ErrorMessage: "keystore not connected",
633+
}
634+
}
635+
636+
connectedRootFingerprint, err := connectedKeystore.RootFingerprint()
637+
if err != nil {
638+
handlers.log.WithError(err).Error("could not determine connected keystore root fingerprint")
639+
return response{
640+
Success: false,
641+
ErrorMessage: err.Error(),
642+
}
643+
}
644+
645+
if !bytes.Equal(rootFingerprint, connectedRootFingerprint) {
646+
handlers.log.WithField("requested", rootFingerprintHex).Warn("features requested for non-connected keystore")
647+
return response{
648+
Success: false,
649+
ErrorMessage: "wrong keystore connected",
650+
}
651+
}
652+
653+
return response{
654+
Success: true,
655+
Features: connectedKeystore.Features(),
656+
}
657+
}
658+
601659
func (handlers *Handlers) getAccounts(*http.Request) interface{} {
602660
persistedAccounts := handlers.backend.Config().AccountsConfig()
603661

backend/keystore/keystore.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,14 @@ type Keystore interface {
150150

151151
// SupportsPaymentRequests returns nil if the device supports silent payments, or an error indicating why it is not supported.
152152
SupportsPaymentRequests() error
153+
154+
// Features reports optional capabilities supported by this keystore.
155+
Features() *Features
156+
}
157+
158+
// Features enumerates optional capabilities that can differ per keystore implementation.
159+
type Features struct {
160+
// SupportsSendToSelf indicates whether the keystore can explicitly verify outputs that belong to
161+
// the same keystore (used for the send-to-self recipient dropdown flow).
162+
SupportsSendToSelf bool `json:"supportsSendToSelf"`
153163
}

backend/keystore/mocks/keystore.go

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/keystore/software/software.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,13 @@ func (keystore *Keystore) BTCXPubs(
201201
return xpubs, nil
202202
}
203203

204+
// Features reports optional capabilities supported by the software keystore.
205+
func (keystore *Keystore) Features() *keystorePkg.Features {
206+
return &keystorePkg.Features{
207+
SupportsSendToSelf: true,
208+
}
209+
}
210+
204211
func (keystore *Keystore) signBTCTransaction(btcProposedTx *btc.ProposedTransaction) error {
205212
keystore.log.Info("Sign transaction.")
206213
transaction := btcProposedTx.TXProposal.Psbt.UnsignedTx

backend/signing/configuration.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,15 @@ func (configuration *Configuration) String() string {
202202
// fingerprint.
203203
type Configurations []*Configuration
204204

205+
// AccountNumber returns the first config's account number. It assumes all configurations have the
206+
// same account number.
207+
func (configs Configurations) AccountNumber() (uint16, error) {
208+
for _, config := range configs {
209+
return config.AccountNumber()
210+
}
211+
return 0, errp.New("no configs")
212+
}
213+
205214
// RootFingerprint gets the fingerprint of the first config (assuming that all configurations have
206215
// the same rootFingerprint). Returns an error if the list has no entries or does not contain a
207216
// known config.

backend/signing/configuration_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,20 @@ func TestAccountNumber(t *testing.T) {
148148
require.Error(t, err)
149149
require.Equal(t, uint16(0), num)
150150
}
151+
152+
func TestAccountNumberOnConfigurations(t *testing.T) {
153+
xpub, err := hdkeychain.NewMaster(make([]byte, 32), &chaincfg.TestNet3Params)
154+
require.NoError(t, err)
155+
xpub, err = xpub.Neuter()
156+
require.NoError(t, err)
157+
rootFingerprint := []byte{1, 2, 3, 4}
158+
159+
cfg1 := NewBitcoinConfiguration(
160+
ScriptTypeP2WPKH, rootFingerprint, mustKeypath("m/48'/0'/10'"), xpub)
161+
cfg2 := NewBitcoinConfiguration(
162+
ScriptTypeP2WPKH, rootFingerprint, mustKeypath("m/84'/0'/10'"), xpub)
163+
cfgs := Configurations{cfg1, cfg2}
164+
num, err := cfgs.AccountNumber()
165+
require.NoError(t, err)
166+
require.Equal(t, uint16(10), num)
167+
}

frontends/web/src/api/account.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export type TAccount = {
7474
activeTokens?: TActiveToken[];
7575
blockExplorerTxPrefix: string;
7676
bitsuranceStatus?: TDetailStatus;
77+
accountNumber?: number;
7778
};
7879

7980
export const getAccounts = (): Promise<TAccount[]> => {

frontends/web/src/api/keystores.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ export type { TUnsubscribe };
2222
type TKeystore = { type: 'hardware' | 'software' };
2323
export type TKeystores = TKeystore[];
2424

25+
export type TKeystoreFeatures = {
26+
supportsSendToSelf: boolean;
27+
};
28+
29+
export type TKeystoreFeaturesResponse = {
30+
success: boolean;
31+
features?: TKeystoreFeatures | null;
32+
errorMessage?: string;
33+
};
34+
2535
export const subscribeKeystores = (
2636
cb: (keystores: TKeystores) => void
2737
) => {
@@ -43,3 +53,7 @@ export const deregisterTest = (): Promise<null> => {
4353
export const connectKeystore = (rootFingerprint: string): Promise<{ success: boolean }> => {
4454
return apiPost('connect-keystore', { rootFingerprint });
4555
};
56+
57+
export const getKeystoreFeatures = (rootFingerprint: string): Promise<TKeystoreFeaturesResponse> => {
58+
return apiGet(`keystore/${rootFingerprint}/features`);
59+
};

0 commit comments

Comments
 (0)