From 7baba14e73ea8c06488fc5c4ba88ac7fb36d7ed2 Mon Sep 17 00:00:00 2001 From: strmci Date: Wed, 10 Sep 2025 16:29:10 +0200 Subject: [PATCH] backend/frontend: add option to configure bitcoin gap limit Add a new expert setting in BitBoxApp that allows changing the gap limit used for transaction discovery on bitcoin receive and change addresses. Until now, this was only possible via command-line. --- CHANGELOG.md | 1 + backend/accounts.go | 16 +- backend/config/config.go | 4 + frontends/web/src/locales/en/app.json | 9 ++ .../src/routes/settings/advanced-settings.tsx | 4 + .../custom-gap-limit-setting.tsx | 149 ++++++++++++++++++ 6 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 frontends/web/src/routes/settings/components/advanced-settings/custom-gap-limit-setting.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index e3f78dc31b..347d4ca7c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - iOS: add launch screen and smooth transition upon opening the app for the first time. - Android: fix screen lock authentication loop bug - Android/iOS: fix screen lock bug when no authentication is configured on the device +- Add expert setting to configure gap limit for bitcoin transaction discovery ## v4.48.4 - macOS: fix potential USB communication issue with BitBox02 bootloaders 0 && configChange > 0 { + gapLimits = &btctypes.GapLimits{ + Receive: configReceive, + Change: configChange, + } + } + } account = backend.makeBtcAccount( accountConfig, specificCoin, - backend.arguments.GapLimits(), + gapLimits, getAddressCallback, backend.log, ) diff --git a/backend/config/config.go b/backend/config/config.go index 5a17afad12..325b3d8373 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -106,6 +106,10 @@ type Backend struct { // StartInTestnet represents whether the app should launch in testnet on the next start. // It resets to `false` after the app starts. StartInTestnet bool `json:"startInTestnet"` + + // Gap limits optionally forces gap limits for receive/change addresses used in bitcoin accounts + GapLimitReceive int `json:"gapLimitReceive"` + GapLimitChange int `json:"gapLimitChange"` } // DeprecatedCoinActive returns the Active setting for a coin by code. This call is should not be diff --git a/frontends/web/src/locales/en/app.json b/frontends/web/src/locales/en/app.json index 4793dfbfa9..98c6fc2c6f 100644 --- a/frontends/web/src/locales/en/app.json +++ b/frontends/web/src/locales/en/app.json @@ -824,6 +824,15 @@ "footer": { "appVersion": "App version:" }, + "gapLimit": { + "change": "Gap limit for change addresses", + "description": "Configure custom gap limits for bitcoin receive and change addresses.", + "maxValue": "The gap limit must not exceed {{max}}.", + "minValue": "The gap limit must be at least {{min}}.", + "receive": "Gap limit for receive addresses", + "resetToDefault": "Reset to default", + "title": "Custom gap limit settings" + }, "generic": { "buy": "Buy {{coinCode}}", "buySell": "Buy & sell", diff --git a/frontends/web/src/routes/settings/advanced-settings.tsx b/frontends/web/src/routes/settings/advanced-settings.tsx index 16c2ca14f2..68c07d386e 100644 --- a/frontends/web/src/routes/settings/advanced-settings.tsx +++ b/frontends/web/src/routes/settings/advanced-settings.tsx @@ -28,6 +28,7 @@ import { EnableTorProxySetting } from './components/advanced-settings/enable-tor import { UnlockSoftwareKeystore } from './components/advanced-settings/unlock-software-keystore'; import { RestartInTestnetSetting } from './components/advanced-settings/restart-in-testnet-setting'; import { ExportLogSetting } from './components/advanced-settings/export-log-setting'; +import { CustomGapLimitSettings } from './components/advanced-settings/custom-gap-limit-setting'; import { getConfig } from '@/utils/config'; import { MobileHeader } from './components/mobile-header'; import { Guide } from '@/components/guide/guide'; @@ -50,6 +51,8 @@ export type TBackendConfig = { proxy?: TProxyConfig authentication?: boolean; startInTestnet?: boolean; + gapLimitReceive?: number; + gapLimitChange?: number; } export type TConfig = { @@ -99,6 +102,7 @@ export const AdvancedSettings = ({ devices, hasAccounts }: TPagePropsWithSetting + diff --git a/frontends/web/src/routes/settings/components/advanced-settings/custom-gap-limit-setting.tsx b/frontends/web/src/routes/settings/components/advanced-settings/custom-gap-limit-setting.tsx new file mode 100644 index 0000000000..0b425674c3 --- /dev/null +++ b/frontends/web/src/routes/settings/components/advanced-settings/custom-gap-limit-setting.tsx @@ -0,0 +1,149 @@ +/** + * Copyright 2025 Shift Crypto AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ChangeEvent, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SettingsItem } from '@/routes/settings/components/settingsItem/settingsItem'; +import { Dialog, DialogButtons } from '@/components/dialog/dialog'; +import { Button, Input } from '@/components/forms'; +import { setConfig } from '@/utils/config'; +import type { TBackendConfig, TConfig } from '@/routes/settings/advanced-settings'; +import { Message } from '@/components/message/message'; + +type TProps = { + backendConfig?: TBackendConfig; + onChangeConfig: (config: TConfig) => void; +} + +export const CustomGapLimitSettings = ({ backendConfig, onChangeConfig }: TProps) => { + const { t } = useTranslation(); + const [showDialog, setShowDialog] = useState(false); + + const DEFAULT_GAP_LIMIT_RECEIVE = 20; + const DEFAULT_GAP_LIMIT_CHANGE = 6; + const MAX_LIMIT = 2000; + + const [showRestartMessage, setShowRestartMessage] = useState(false); + const [gapLimitReceive, setGapLimitReceive] = useState(backendConfig?.gapLimitReceive || DEFAULT_GAP_LIMIT_RECEIVE); + const [gapLimitChange, setGapLimitChange] = useState(backendConfig?.gapLimitChange || DEFAULT_GAP_LIMIT_CHANGE); + + useEffect(() => { + if (backendConfig) { + setGapLimitReceive(backendConfig.gapLimitReceive || DEFAULT_GAP_LIMIT_RECEIVE); + setGapLimitChange(backendConfig.gapLimitChange || DEFAULT_GAP_LIMIT_CHANGE); + } + }, [backendConfig]); + + const handleSave = async () => { + const config = await setConfig({ + backend: { + ...backendConfig, + gapLimitReceive, + gapLimitChange, + }, + }); + onChangeConfig(config); + setShowDialog(false); + }; + + const getGapLimitError = (value: number | string, min: number) => { + if (typeof value !== 'number') { + return undefined; + } + if (value < min) { + return t('gapLimit.minValue', { min }); + } + if (value > MAX_LIMIT) { + return t('gapLimit.maxValue', { max: MAX_LIMIT }); + } + return undefined; + }; + + return ( + <> + {showRestartMessage ? ( + + {t('settings.restart')} + + ) : null} + setShowDialog(true)} + /> + setShowDialog(false)} + title={t('gapLimit.title')} + small> + ) => { + const value = e.target.value; + setGapLimitReceive(value === '' ? '' : parseInt(value, 10) || DEFAULT_GAP_LIMIT_RECEIVE); + }} + value={gapLimitReceive} + error={getGapLimitError(gapLimitReceive, DEFAULT_GAP_LIMIT_RECEIVE)} + /> + ) => { + const value = e.target.value; + setGapLimitChange(value === '' ? '' : parseInt(value, 10) || DEFAULT_GAP_LIMIT_CHANGE); + }} + value={gapLimitChange} + error={getGapLimitError(gapLimitChange, DEFAULT_GAP_LIMIT_CHANGE)} + /> + + + + + + + + ); +}; \ No newline at end of file