Skip to content

Commit f0dce1b

Browse files
committed
Merge remote-tracking branch 'benma/send-to-self-version-check' into staging-send-to-self-dropdown
2 parents 46e7b0d + cea342e commit f0dce1b

File tree

12 files changed

+264
-66
lines changed

12 files changed

+264
-66
lines changed

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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ func NewHandlers(
219219
getAPIRouterNoError(apiRouter)("/dev-servers", handlers.getDevServers).Methods("GET")
220220
getAPIRouterNoError(apiRouter)("/account-add", handlers.postAddAccount).Methods("POST")
221221
getAPIRouterNoError(apiRouter)("/keystores", handlers.getKeystores).Methods("GET")
222+
getAPIRouterNoError(apiRouter)("/keystore/{rootFingerprint}/features", handlers.getKeystoreFeatures).Methods("GET")
222223
getAPIRouterNoError(apiRouter)("/accounts", handlers.getAccounts).Methods("GET")
223224
getAPIRouter(apiRouter)("/accounts/balance-summary", handlers.getAccountsBalanceSummary).Methods("GET")
224225
getAPIRouterNoError(apiRouter)("/set-account-active", handlers.postSetAccountActive).Methods("POST")
@@ -605,6 +606,55 @@ func (handlers *Handlers) getKeystores(*http.Request) interface{} {
605606
return keystores
606607
}
607608

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

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

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+
};

frontends/web/src/app.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,6 @@ export const App = () => {
216216
<AppRouter
217217
accounts={accounts}
218218
activeAccounts={activeAccounts}
219-
deviceIDs={deviceIDs}
220219
devices={devices}
221220
devicesKey={devicesKey}
222221
/>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright 2025 Shift Crypto AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { useCallback, useContext } from 'react';
18+
import { useNavigate } from 'react-router-dom';
19+
import { useTranslation } from 'react-i18next';
20+
import { Dialog, DialogButtons } from './dialog';
21+
import { getDeviceList } from '@/api/devices';
22+
import { syncDeviceList } from '@/api/devicessync';
23+
import { useSync } from '@/hooks/api';
24+
import { useDefault } from '@/hooks/default';
25+
import { Button } from '@/components/forms';
26+
import { AppContext } from '@/contexts/AppContext';
27+
28+
type TFirmwareUpgradeRequiredDialogProps = {
29+
open: boolean;
30+
onClose: () => void;
31+
};
32+
33+
export const FirmwareUpgradeRequiredDialog = ({ open, onClose }: TFirmwareUpgradeRequiredDialogProps) => {
34+
const { t } = useTranslation();
35+
const navigate = useNavigate();
36+
const { setFirmwareUpdateDialogOpen } = useContext(AppContext);
37+
const devices = useDefault(useSync(getDeviceList, syncDeviceList), {});
38+
const deviceIDs = Object.keys(devices);
39+
40+
const handleUpgrade = useCallback(() => {
41+
setFirmwareUpdateDialogOpen(true);
42+
if (deviceIDs && deviceIDs.length > 0) {
43+
navigate(`/settings/device-settings/${deviceIDs[0] as string}`);
44+
}
45+
onClose();
46+
}, [setFirmwareUpdateDialogOpen, deviceIDs, navigate, onClose]);
47+
48+
return (
49+
<Dialog
50+
title={t('upgradeFirmware.title')}
51+
open={open}
52+
onClose={onClose}>
53+
<p>{t('device.firmwareUpgradeRequired')}</p>
54+
<DialogButtons>
55+
<Button primary onClick={handleUpgrade}>
56+
{t('upgradeFirmware.button')}
57+
</Button>
58+
<Button secondary onClick={onClose}>
59+
{t('dialog.cancel')}
60+
</Button>
61+
</DialogButtons>
62+
</Dialog>
63+
);
64+
};

0 commit comments

Comments
 (0)