Skip to content

Commit 99c500f

Browse files
benma's agentbenma
authored andcommitted
backend/frontend: gate send-to-self dropdown on firmware support
1 parent 46e7b0d commit 99c500f

File tree

7 files changed

+138
-3
lines changed

7 files changed

+138
-3
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/routes/account/send/send.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { NoteInput } from './components/inputs/note-input';
4242
import { FiatValue } from './components/fiat-value';
4343
import { TProposalError, txProposalErrorHandling } from './services';
4444
import { CoinControl } from './coin-control';
45-
import { connectKeystore } from '@/api/keystores';
45+
import { connectKeystore, getKeystoreFeatures } from '@/api/keystores';
4646
import style from './send.module.css';
4747

4848
type TProps = {
@@ -135,18 +135,28 @@ export const Send = ({
135135
if (!connectResult.success) {
136136
return;
137137
}
138+
if (selectedReceiverAccount) {
139+
const featuresResult = await getKeystoreFeatures(rootFingerprint);
140+
if (!featuresResult.success) {
141+
alertUser(featuresResult.errorMessage || t('genericError'));
142+
return;
143+
}
144+
if (!featuresResult.features?.supportsSendToSelf) {
145+
alertUser(t('device.firmwareUpgradeRequired'));
146+
return;
147+
}
148+
}
138149
setIsConfirming(true);
139150
try {
140151
const result = await accountApi.sendTx(account.code, note);
141152
setSendResult(result);
142-
setIsConfirming(false);
143153
} catch (err) {
144154
console.error(err);
145155
} finally {
146156
// The following method allows pressing escape again.
147157
setIsConfirming(false);
148158
}
149-
}, [account.code, account.keystore.rootFingerprint, note]);
159+
}, [account.code, account.keystore.rootFingerprint, note, selectedReceiverAccount, t]);
150160

151161
const getValidTxInputData = useCallback((): Required<accountApi.TTxInput> | false => {
152162
if (

0 commit comments

Comments
 (0)