diff --git a/.containerversion b/.containerversion index bb95160cb6..a787364590 100644 --- a/.containerversion +++ b/.containerversion @@ -1 +1 @@ -33 +34 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53c813ebd1..8f5a051002 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -239,8 +239,6 @@ jobs: - name: Build Windows app run: | make qt-windows - cd frontends/qt - makensis setup.nsi - name: Upload Installer id: upload uses: actions/upload-artifact@v4 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7dee330ef2..457d24417d 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -10,11 +10,66 @@ on: workflow_dispatch: jobs: + compute-version: + name: Compute firmware container version + runs-on: ubuntu-latest + outputs: + firmware_version: ${{ steps.get_version.outputs.firmware_version }} + steps: + - name: Checkout firmware repo + uses: actions/checkout@v4 + with: + repository: BitBoxSwiss/bitbox02-firmware + path: bitbox02-firmware + + - name: Read .containerversion + id: get_version + run: | + version=$(cat bitbox02-firmware/.containerversion) + echo "Firmware version: $version" + echo "firmware_version=$version" >> $GITHUB_OUTPUT + + build-simulator: + name: Build simulator in container + runs-on: ubuntu-latest + needs: compute-version + container: + image: shiftcrypto/firmware_v2:${{ needs.compute-version.outputs.firmware_version }} + steps: + - name: Checkout firmware repo + uses: actions/checkout@v4 + with: + repository: BitBoxSwiss/bitbox02-firmware + submodules: true + path: bitbox02-firmware + + - name: Fetch tags + working-directory: bitbox02-firmware + run: git fetch --tags + + - name: Build simulator + working-directory: bitbox02-firmware + run: make simulator + + - name: Upload simulator binary + uses: actions/upload-artifact@v4 + with: + name: simulator-binary + path: bitbox02-firmware/build-build-noasan/bin/simulator + run-tests: runs-on: ubuntu-latest + needs: build-simulator steps: - uses: actions/checkout@v3 + - name: Download simulator artifact + uses: actions/download-artifact@v4 + with: + name: simulator-binary + path: ./simulator-bin + + - name: Setup Node.js uses: actions/setup-node@v3 with: @@ -28,13 +83,19 @@ jobs: - name: Install Playwright browsers run: | cd frontends/web - npx playwright install --with-deps + npx playwright install --with-deps chromium webkit + + + - name: Restore executable permission + run: chmod +x simulator-bin/simulator - name: Run Playwright tests + env: + SIMULATOR_PATH: ${{ github.workspace }}/simulator-bin/simulator run: make webe2etest - name: Upload Playwright artifacts - if: failure() + if: always() uses: actions/upload-artifact@v4 with: name: playwright-artifacts diff --git a/APP_VERSION b/APP_VERSION new file mode 100644 index 0000000000..9590e88895 --- /dev/null +++ b/APP_VERSION @@ -0,0 +1 @@ +4.49.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6799c964..3c388b2b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## Unreleased +- Add a dropdown on the "Receiver address" input in the send screen to select an account + +## v4.49.0 - macOS: fix potential USB communication issue with BitBox02 bootloaders ` must also be provided. + +Note: the simulator is currently only supported in the servewallet and in the Qt app and only when the app runs in testnet mode. + #### Watch and build the UI Run `make webdev` to develop the UI inside a web browser (for quick development, automatic rebuilds diff --git a/backend/accounts.go b/backend/accounts.go index f40141f2ee..38c503c96f 100644 --- a/backend/accounts.go +++ b/backend/accounts.go @@ -1722,20 +1722,23 @@ func (backend *Backend) checkAccountUsed(account accounts.Interface) { return } } + log := backend.log.WithField("accountCode", account.Config().Config.Code) - txs, err := account.Transactions() - if err != nil { - log.WithError(err).Error("discoverAccount") - return - } + if !account.Config().Config.Used { + txs, err := account.Transactions() + if err != nil { + log.WithError(err).Error("discoverAccount") + return + } - if len(txs) == 0 { - // Invoke this here too because even if an account is unused, we scan up to 5 accounts. - backend.maybeAddHiddenUnusedAccounts() - return + if len(txs) == 0 { + // Invoke this here too because even if an account is unused, we scan up to 5 accounts. + backend.maybeAddHiddenUnusedAccounts() + return + } } log.Info("marking account as used") - err = backend.config.ModifyAccountsConfig(func(accountsConfig *config.AccountsConfig) error { + err := backend.config.ModifyAccountsConfig(func(accountsConfig *config.AccountsConfig) error { acct := accountsConfig.Lookup(account.Config().Config.Code) if acct == nil { return errp.Newf("could not find account") diff --git a/backend/accounts_test.go b/backend/accounts_test.go index 77c7536dea..1887035406 100644 --- a/backend/accounts_test.go +++ b/backend/accounts_test.go @@ -22,6 +22,7 @@ import ( "time" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts" + accountsMocks "github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts/mocks" accountsTypes "github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts/types" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/addresses" @@ -1676,3 +1677,60 @@ func TestAccountsFiatAndCoinBalance(t *testing.T) { } } + +func TestCheckAccountUsed(t *testing.T) { + b := newBackend(t, testnetDisabled, regtestDisabled) + b.tstCheckAccountUsed = nil + defer b.Close() + accountMocks := map[accountsTypes.Code]*accountsMocks.InterfaceMock{} + // A Transactions function that always returns one transaction, so the account is always used. + txFunc := func() (accounts.OrderedTransactions, error) { + return accounts.OrderedTransactions{&accounts.TransactionData{}}, nil + } + + b.makeBtcAccount = func(config *accounts.AccountConfig, coin *btc.Coin, gapLimits *types.GapLimits, getAddress func(coinpkg.Code, blockchain.ScriptHashHex) (*addresses.AccountAddress, error), log *logrus.Entry) accounts.Interface { + accountMock := MockBtcAccount(t, config, coin, gapLimits, log) + accountMock.TransactionsFunc = txFunc + accountMocks[config.Config.Code] = accountMock + return accountMock + } + + b.makeEthAccount = func(config *accounts.AccountConfig, coin *eth.Coin, httpClient *http.Client, log *logrus.Entry) accounts.Interface { + accountMock := MockEthAccount(config, coin, httpClient, log) + accountMock.TransactionsFunc = txFunc + accountMocks[config.Config.Code] = accountMock + return accountMock + } + + ks1 := makeBitBox02Multi() + + ks1Fingerprint, err := ks1.RootFingerprint() + require.NoError(t, err) + + b.registerKeystore(ks1) + require.NoError(t, b.SetWatchonly(ks1Fingerprint, true)) + + accountsByKestore, err := b.AccountsByKeystore() + require.NoError(t, err) + + accountList, ok := accountsByKestore[hex.EncodeToString(ks1Fingerprint)] + require.True(t, ok, "Expected accounts for keystore with fingerprint %s", hex.EncodeToString(ks1Fingerprint)) + + // Check all accounts, make sure they are set as used. + for _, acct := range accountList { + mock, ok := accountMocks[acct.Config().Config.Code] + require.True(t, ok, "No mock for account %s", acct.Config().Config.Code) + + b.checkAccountUsed(acct) + // Ensure that Transactions is called + require.Len(t, mock.TransactionsCalls(), 1) + require.True(t, acct.Config().Config.Used) + + // Call checkAccountUsed again, Transactions should not be called again. + b.checkAccountUsed(acct) + require.Len(t, mock.TransactionsCalls(), 1) + // And Used should still be true. + require.True(t, acct.Config().Config.Used) + } + +} diff --git a/backend/backend.go b/backend/backend.go index d29f11bfa7..62a5fabdb6 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -52,6 +52,7 @@ import ( "github.com/BitBoxSwiss/bitbox-wallet-app/backend/keystore" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/keystore/software" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/rates" + "github.com/BitBoxSwiss/bitbox-wallet-app/backend/versioninfo" utilConfig "github.com/BitBoxSwiss/bitbox-wallet-app/util/config" "github.com/BitBoxSwiss/bitbox-wallet-app/util/errp" "github.com/BitBoxSwiss/bitbox-wallet-app/util/locker" @@ -66,7 +67,7 @@ import ( ) func init() { - electrum.SetClientSoftwareVersion(Version) + electrum.SetClientSoftwareVersion(versioninfo.Version) } // fixedURLWhitelist is always allowed by SystemOpen, in addition to some diff --git a/backend/bridgecommon/bridgecommon.go b/backend/bridgecommon/bridgecommon.go index c57e14f434..760d63eafe 100644 --- a/backend/bridgecommon/bridgecommon.go +++ b/backend/bridgecommon/bridgecommon.go @@ -32,6 +32,7 @@ import ( "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bluetooth" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/usb" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/handlers" + "github.com/BitBoxSwiss/bitbox-wallet-app/backend/versioninfo" "github.com/BitBoxSwiss/bitbox-wallet-app/util/config" "github.com/BitBoxSwiss/bitbox-wallet-app/util/errp" "github.com/BitBoxSwiss/bitbox-wallet-app/util/jsonp" @@ -286,6 +287,7 @@ func (env *BackendEnvironment) BluetoothConnect(identifier string) { // Serve serves the BitBox API for use in a native client. func Serve( testnet bool, + simulator bool, gapLimits *btctypes.GapLimits, communication NativeCommunication, backendEnvironment backend.Environment) { @@ -301,9 +303,14 @@ func Serve( log.Info("--------------- Started application --------------") log.WithField("goos", runtime.GOOS). WithField("goarch", runtime.GOARCH). - WithField("version", backend.Version). + WithField("version", versioninfo.Version). Info("environment") + if simulator { + log.Info("Simulator mode enabled, ensuring testnet mode is enabled") + testnet = true + } + var err error globalBackend, err = backend.NewBackend( arguments.NewArguments( diff --git a/backend/bridgecommon/bridgecommon_test.go b/backend/bridgecommon/bridgecommon_test.go index 4a41f47337..1dce858b74 100644 --- a/backend/bridgecommon/bridgecommon_test.go +++ b/backend/bridgecommon/bridgecommon_test.go @@ -78,6 +78,7 @@ func (e environment) BluetoothConnect(string) {} // TestServeShutdownServe checks that you can call Serve twice in a row. func TestServeShutdownServe(t *testing.T) { bridgecommon.Serve( + false, false, nil, communication{}, @@ -88,6 +89,7 @@ func TestServeShutdownServe(t *testing.T) { done := make(chan struct{}) go func() { bridgecommon.Serve( + false, false, nil, communication{}, diff --git a/backend/coins/eth/account.go b/backend/coins/eth/account.go index 693fb97185..88bd70ab5a 100644 --- a/backend/coins/eth/account.go +++ b/backend/coins/eth/account.go @@ -702,11 +702,13 @@ func (account *Account) SendTx(txNote string) (string, error) { // If the service should not be reachable, we fallback to only one priority, estimated by // the ETH RPC eth_gasPrice endpoint. func (account *Account) feeTargets() []*ethtypes.FeeTarget { - etherscanFeeTargets, err := account.coin.client.FeeTargets(context.TODO()) - if err == nil { - return etherscanFeeTargets + if account.coin.code != coin.CodeSEPETH { + etherscanFeeTargets, err := account.coin.client.FeeTargets(context.TODO()) + if err == nil { + return etherscanFeeTargets + } + account.log.WithError(err).Error("Could not get fee targets from eth gas station, falling back to RPC eth_gasPrice") } - account.log.WithError(err).Error("Could not get fee targets from eth gas station, falling back to RPC eth_gasPrice") suggestedGasPrice, err := account.coin.client.SuggestGasPrice(context.TODO()) if err != nil { account.log.WithError(err).Error("Fallback to RPC eth_gasPrice failed") diff --git a/backend/coins/eth/coin.go b/backend/coins/eth/coin.go index ad4162ffb1..ecc29b1c65 100644 --- a/backend/coins/eth/coin.go +++ b/backend/coins/eth/coin.go @@ -158,7 +158,15 @@ func (coin *Coin) unitFactor(isFee bool) *big.Int { func (coin *Coin) FormatAmount(amount coinpkg.Amount, isFee bool) string { factor := coin.unitFactor(isFee) s := new(big.Rat).SetFrac(amount.BigInt(), factor).FloatString(18) - return strings.TrimRight(strings.TrimRight(s, "0"), ".") + s = strings.TrimRight(strings.TrimRight(s, "0"), ".") + // For USDC/USDT, display 2 decimals when there's only 1 + if coin.unit == "USDC" || coin.unit == "USDT" { + parts := strings.Split(s, ".") + if len(parts) == 2 && len(parts[1]) == 1 { + s += "0" + } + } + return s } // ToUnit implements coin.Coin. diff --git a/backend/coins/eth/coin_test.go b/backend/coins/eth/coin_test.go index 94d61eddf7..a35cf205e7 100644 --- a/backend/coins/eth/coin_test.go +++ b/backend/coins/eth/coin_test.go @@ -28,6 +28,8 @@ type testSuite struct { suite.Suite coin *Coin ERC20Coin *Coin + USDTCoin *Coin + USDCCoin *Coin } func (s *testSuite) SetupTest() { @@ -54,6 +56,30 @@ func (s *testSuite) SetupTest() { nil, erc20.NewToken("0x0000000000000000000000000000000000000001", 12), ) + + s.USDTCoin = NewCoin( + nil, + "eth-erc20-usdt", + "Tether USD", + "USDT", + "ETH", + params.MainnetChainConfig, + "", + nil, + erc20.NewToken("0xdac17f958d2ee523a2206206994597c13d831ec7", 6), + ) + + s.USDCCoin = NewCoin( + nil, + "eth-erc20-usdc", + "USD Coin", + "USDC", + "ETH", + params.MainnetChainConfig, + "", + nil, + erc20.NewToken("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 6), + ) } func TestSuite(t *testing.T) { @@ -92,6 +118,29 @@ func (s *testSuite) TestFormatAmount() { "0.123456789012345678", s.ERC20Coin.FormatAmount(coin.NewAmountFromInt64(123456789012345678), true), ) + + stablecoinTests := []struct { + amount int64 + expected string + }{ + {1100000, "1.10"}, + {1000000, "1"}, + {1120000, "1.12"}, + {1123000, "1.123"}, + {100000, "0.10"}, + {1000000000, "1000"}, + } + + for _, test := range stablecoinTests { + s.Require().Equal( + test.expected, + s.USDTCoin.FormatAmount(coin.NewAmountFromInt64(test.amount), false), + ) + s.Require().Equal( + test.expected, + s.USDCCoin.FormatAmount(coin.NewAmountFromInt64(test.amount), false), + ) + } } func (s *testSuite) TestSetAmount() { diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.23.2.signed.bin.gz b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.23.2.signed.bin.gz deleted file mode 100644 index 4de25f9aa8..0000000000 Binary files a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.23.2.signed.bin.gz and /dev/null differ diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.23.2.signed.bin.sha256 b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.23.2.signed.bin.sha256 deleted file mode 100644 index 380a07afdc..0000000000 --- a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.23.2.signed.bin.sha256 +++ /dev/null @@ -1 +0,0 @@ -7476febce1e914f52c9d1c7ce860823cf5e965bded76eb420982c55f8dbd9dfe diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.24.0.signed.bin.gz b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.24.0.signed.bin.gz new file mode 100644 index 0000000000..590a7e8ace Binary files /dev/null and b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.24.0.signed.bin.gz differ diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.24.0.signed.bin.sha256 b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.24.0.signed.bin.sha256 new file mode 100644 index 0000000000..ea2682b1a7 --- /dev/null +++ b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-btconly.v9.24.0.signed.bin.sha256 @@ -0,0 +1 @@ +eea1dce4d281b557e8034ffe3fac8ba24432e2c1b583b6038cb8be730e93d7c3 diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.23.2.signed.bin.gz b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.23.2.signed.bin.gz deleted file mode 100644 index eb9a81cfdb..0000000000 Binary files a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.23.2.signed.bin.gz and /dev/null differ diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.23.2.signed.bin.gz.sha256 b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.23.2.signed.bin.gz.sha256 deleted file mode 100644 index 86bbf80e8e..0000000000 --- a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.23.2.signed.bin.gz.sha256 +++ /dev/null @@ -1 +0,0 @@ -473f0f110ffa969444e3b37dd1399a6fac8463e8cbaed267af88f92f1c2c8281 diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.24.0.signed.bin.gz b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.24.0.signed.bin.gz new file mode 100644 index 0000000000..e202cb92f2 Binary files /dev/null and b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.24.0.signed.bin.gz differ diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.24.0.signed.bin.sha256 b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.24.0.signed.bin.sha256 new file mode 100644 index 0000000000..e1cae5a734 --- /dev/null +++ b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02-multi.v9.24.0.signed.bin.sha256 @@ -0,0 +1 @@ +41f1053ecdf2edc02834408f5cef0cd3d685913f3846619fbbbfa29b0461d9bc diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.23.2.signed.bin.gz b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.23.2.signed.bin.gz deleted file mode 100644 index 0d6540b092..0000000000 Binary files a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.23.2.signed.bin.gz and /dev/null differ diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.23.2.signed.bin.gz.sha256 b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.23.2.signed.bin.gz.sha256 deleted file mode 100644 index 380a07afdc..0000000000 --- a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.23.2.signed.bin.gz.sha256 +++ /dev/null @@ -1 +0,0 @@ -7476febce1e914f52c9d1c7ce860823cf5e965bded76eb420982c55f8dbd9dfe diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.24.0.signed.bin.gz b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.24.0.signed.bin.gz new file mode 100644 index 0000000000..585b689b0e Binary files /dev/null and b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.24.0.signed.bin.gz differ diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.24.0.signed.bin.sha256 b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.24.0.signed.bin.sha256 new file mode 100644 index 0000000000..ea2682b1a7 --- /dev/null +++ b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-btconly.v9.24.0.signed.bin.sha256 @@ -0,0 +1 @@ +eea1dce4d281b557e8034ffe3fac8ba24432e2c1b583b6038cb8be730e93d7c3 diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.23.2.signed.bin.gz b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.23.2.signed.bin.gz deleted file mode 100644 index 1104ba11f6..0000000000 Binary files a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.23.2.signed.bin.gz and /dev/null differ diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.23.2.signed.bin.gz.sha256 b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.23.2.signed.bin.gz.sha256 deleted file mode 100644 index 86bbf80e8e..0000000000 --- a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.23.2.signed.bin.gz.sha256 +++ /dev/null @@ -1 +0,0 @@ -473f0f110ffa969444e3b37dd1399a6fac8463e8cbaed267af88f92f1c2c8281 diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.24.0.signed.bin.gz b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.24.0.signed.bin.gz new file mode 100644 index 0000000000..7a3213d247 Binary files /dev/null and b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.24.0.signed.bin.gz differ diff --git a/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.24.0.signed.bin.sha256 b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.24.0.signed.bin.sha256 new file mode 100644 index 0000000000..e1cae5a734 --- /dev/null +++ b/backend/devices/bitbox02bootloader/assets/firmware-bitbox02nova-multi.v9.24.0.signed.bin.sha256 @@ -0,0 +1 @@ +41f1053ecdf2edc02834408f5cef0cd3d685913f3846619fbbbfa29b0461d9bc diff --git a/backend/devices/bitbox02bootloader/firmware.go b/backend/devices/bitbox02bootloader/firmware.go index 50fc855f8b..1d9c687dcd 100644 --- a/backend/devices/bitbox02bootloader/firmware.go +++ b/backend/devices/bitbox02bootloader/firmware.go @@ -34,30 +34,30 @@ var intermediateFirmwareBinaryMulti_9_17_1 []byte // BitBox02 -//go:embed assets/firmware-bitbox02-btconly.v9.23.2.signed.bin.gz +//go:embed assets/firmware-bitbox02-btconly.v9.24.0.signed.bin.gz var firmwareBinaryBTCOnly []byte -var firmwareVersionBTCOnly = semver.NewSemVer(9, 23, 2) -var firmwareMonotonicVersionBtcOnly uint32 = 45 +var firmwareVersionBTCOnly = semver.NewSemVer(9, 24, 0) +var firmwareMonotonicVersionBtcOnly uint32 = 47 -//go:embed assets/firmware-bitbox02-multi.v9.23.2.signed.bin.gz +//go:embed assets/firmware-bitbox02-multi.v9.24.0.signed.bin.gz var firmwareBinaryMulti []byte -var firmwareVersionMulti = semver.NewSemVer(9, 23, 2) -var firmwareMonotonicVersionMulti uint32 = 45 +var firmwareVersionMulti = semver.NewSemVer(9, 24, 0) +var firmwareMonotonicVersionMulti uint32 = 47 // BitBox02 Nova. -//go:embed assets/firmware-bitbox02nova-btconly.v9.23.2.signed.bin.gz +//go:embed assets/firmware-bitbox02nova-btconly.v9.24.0.signed.bin.gz var firmwareBB02PlusBinaryBTCOnly []byte -var firmwareBB02PlusVersionBTCOnly = semver.NewSemVer(9, 23, 2) -var firmwareBB02PlusMonotonicVersionBtcOnly uint32 = 45 +var firmwareBB02PlusVersionBTCOnly = semver.NewSemVer(9, 24, 0) +var firmwareBB02PlusMonotonicVersionBtcOnly uint32 = 47 // TODO: set to false / remove before production. This is only to allow upgrading unsigned firmware. const plusIsPlaceholder = false -//go:embed assets/firmware-bitbox02nova-multi.v9.23.2.signed.bin.gz +//go:embed assets/firmware-bitbox02nova-multi.v9.24.0.signed.bin.gz var firmwareBB02PlusBinaryMulti []byte -var firmwareBB02PlusVersionMulti = semver.NewSemVer(9, 23, 2) -var firmwareBB02PlusMonotonicVersionMulti uint32 = 45 +var firmwareBB02PlusVersionMulti = semver.NewSemVer(9, 24, 0) +var firmwareBB02PlusMonotonicVersionMulti uint32 = 47 type firmwareInfo struct { version *semver.SemVer diff --git a/backend/devices/bitbox02bootloader/firmware_test.go b/backend/devices/bitbox02bootloader/firmware_test.go index 232d655153..cbf9ad882f 100644 --- a/backend/devices/bitbox02bootloader/firmware_test.go +++ b/backend/devices/bitbox02bootloader/firmware_test.go @@ -45,7 +45,7 @@ func testHash(t *testing.T, info firmwareInfo, expectedProduct bitbox02common.Pr func TestBundledFirmware(t *testing.T) { for _, fw := range bundledFirmwares[bitbox02common.ProductBitBox02Multi] { t.Run("bitbox02-multi/"+fw.version.String(), func(t *testing.T) { - filename := fmt.Sprintf("assets/firmware-bitbox02-multi.v%s.signed.bin.gz.sha256", fw.version) + filename := fmt.Sprintf("assets/firmware-bitbox02-multi.v%s.signed.bin.sha256", fw.version) if fw.version.String() == "9.17.1" { filename = fmt.Sprintf("assets/firmware.v%s.signed.bin.sha256", fw.version) } @@ -65,13 +65,13 @@ func TestBundledFirmware(t *testing.T) { for _, fw := range bundledFirmwares[bitbox02common.ProductBitBox02PlusMulti] { t.Run("bitbox02nova-multi/"+fw.version.String(), func(t *testing.T) { - testHash(t, fw, bitbox02common.ProductBitBox02PlusMulti, fmt.Sprintf("assets/firmware-bitbox02nova-multi.v%s.signed.bin.gz.sha256", fw.version)) + testHash(t, fw, bitbox02common.ProductBitBox02PlusMulti, fmt.Sprintf("assets/firmware-bitbox02nova-multi.v%s.signed.bin.sha256", fw.version)) }) } for _, fw := range bundledFirmwares[bitbox02common.ProductBitBox02PlusBTCOnly] { t.Run("bitbox02nova-btconly/"+fw.version.String(), func(t *testing.T) { - testHash(t, fw, bitbox02common.ProductBitBox02PlusBTCOnly, fmt.Sprintf("assets/firmware-bitbox02nova-btconly.v%s.signed.bin.gz.sha256", fw.version)) + testHash(t, fw, bitbox02common.ProductBitBox02PlusBTCOnly, fmt.Sprintf("assets/firmware-bitbox02nova-btconly.v%s.signed.bin.sha256", fw.version)) }) } } diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index d137bb33f7..809dbc15ff 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -50,6 +50,7 @@ import ( "github.com/BitBoxSwiss/bitbox-wallet-app/backend/keystore" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/market" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/rates" + "github.com/BitBoxSwiss/bitbox-wallet-app/backend/versioninfo" utilConfig "github.com/BitBoxSwiss/bitbox-wallet-app/util/config" "github.com/BitBoxSwiss/bitbox-wallet-app/util/errp" "github.com/BitBoxSwiss/bitbox-wallet-app/util/jsonp" @@ -547,7 +548,7 @@ func (handlers *Handlers) getDetectDarkTheme(r *http.Request) interface{} { } func (handlers *Handlers) getVersion(*http.Request) interface{} { - return backend.Version.String() + return versioninfo.Version.String() } func (handlers *Handlers) getTesting(*http.Request) interface{} { diff --git a/backend/mobileserver/Makefile b/backend/mobileserver/Makefile index 0621d8ae74..3da5c3d6e8 100644 --- a/backend/mobileserver/Makefile +++ b/backend/mobileserver/Makefile @@ -1,10 +1,11 @@ include ../../android-env.mk.inc +include ../../version.mk.inc # Set -glflags to fix the vendor issue with gomobile, see: https://github.com/golang/go/issues/67927#issuecomment-2241523694 build-android: # androidapi version should match minSdkVersion in frontends/android/BitBoxApp/app/build.gradle - ANDROID_HOME=${ANDROID_SDK_ROOT} gomobile bind -x -a -glflags="-mod=readonly" -ldflags="-s -w" -trimpath -target android -androidapi 21 . + ANDROID_HOME=${ANDROID_SDK_ROOT} gomobile bind -x -a -glflags="-mod=readonly" -ldflags="-s -w $(GO_VERSION_LDFLAGS)" -trimpath -target android -androidapi 21 . build-ios: - gomobile bind -x -a -glflags="-mod=readonly" -tags="timetzdata" -trimpath -ldflags="-s -w" -target ios,iossimulator . + gomobile bind -x -a -glflags="-mod=readonly" -tags="timetzdata" -trimpath -ldflags="-s -w $(GO_VERSION_LDFLAGS)" -target ios,iossimulator . clean: rm -f mobileserver.aar mobileserver-sources.jar diff --git a/backend/mobileserver/mobileserver.go b/backend/mobileserver/mobileserver.go index c3856946e3..59417f34b4 100644 --- a/backend/mobileserver/mobileserver.go +++ b/backend/mobileserver/mobileserver.go @@ -185,6 +185,7 @@ func Serve(dataDir string, testnet bool, environment GoEnvironmentInterface, goA bridgecommon.Serve( testnet, + false, nil, goAPI, &bridgecommon.BackendEnvironment{ diff --git a/backend/update.go b/backend/update.go index 6d67a81aa3..f87e2fce9a 100644 --- a/backend/update.go +++ b/backend/update.go @@ -18,6 +18,7 @@ import ( "encoding/json" "net/http" + "github.com/BitBoxSwiss/bitbox-wallet-app/backend/versioninfo" "github.com/BitBoxSwiss/bitbox-wallet-app/util/errp" "github.com/BitBoxSwiss/bitbox-wallet-app/util/logging" "github.com/BitBoxSwiss/bitbox02-api-go/util/semver" @@ -25,11 +26,6 @@ import ( const updateFileURL = "https://bitboxapp.shiftcrypto.io/desktop.json" -var ( - // Version of the backend as displayed to the user. - Version = semver.NewSemVer(4, 48, 7) -) - // UpdateFile is retrieved from the server. type UpdateFile struct { // CurrentVersion stores the current version and is not loaded from the server. @@ -66,11 +62,11 @@ func (backend *Backend) checkForUpdate() (*UpdateFile, error) { return nil, errp.WithStack(err) } - if Version.AtLeast(updateFile.NewVersion) { + if versioninfo.Version.AtLeast(updateFile.NewVersion) { return nil, nil } - updateFile.CurrentVersion = Version + updateFile.CurrentVersion = versioninfo.Version return &updateFile, nil } diff --git a/backend/versioninfo/versioninfo.go b/backend/versioninfo/versioninfo.go new file mode 100644 index 0000000000..990854d517 --- /dev/null +++ b/backend/versioninfo/versioninfo.go @@ -0,0 +1,34 @@ +// 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. + +package versioninfo + +import ( + "github.com/BitBoxSwiss/bitbox02-api-go/util/semver" +) + +var ( + // versionString is overridden at build time via go ldflags. + versionString = "0.0.1" + // Version of the backend as displayed to the user. + Version *semver.SemVer +) + +func init() { + parsed, err := semver.NewSemVerFromString(versionString) + if err != nil { + panic(err) + } + Version = parsed +} diff --git a/cmd/servewallet/main.go b/cmd/servewallet/main.go index bcb9f95273..afac902fb1 100644 --- a/cmd/servewallet/main.go +++ b/cmd/servewallet/main.go @@ -29,6 +29,7 @@ import ( "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bitbox02/simulator" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/usb" backendHandlers "github.com/BitBoxSwiss/bitbox-wallet-app/backend/handlers" + "github.com/BitBoxSwiss/bitbox-wallet-app/backend/versioninfo" "github.com/BitBoxSwiss/bitbox-wallet-app/util/config" "github.com/BitBoxSwiss/bitbox-wallet-app/util/logging" "github.com/BitBoxSwiss/bitbox-wallet-app/util/system" @@ -74,7 +75,7 @@ func (webdevEnvironment) DeviceInfos() []usb.DeviceInfo { testDeviceInfo := simulator.TestDeviceInfo() if testDeviceInfo != nil { // We are in "test device" mode. - return []usb.DeviceInfo{*testDeviceInfo} + return []usb.DeviceInfo{testDeviceInfo} } return usb.DeviceInfos() } @@ -179,6 +180,11 @@ func main() { } }(log) log.Info("--------------- Started application --------------") + log.WithField("goos", runtime.GOOS). + WithField("goarch", runtime.GOARCH). + WithField("version", versioninfo.Version). + Info("environment") + // since we are in dev-mode, we can drop the authorization token connectionData := backendHandlers.NewConnectionData(-1, "") newBackend, err := backendPkg.NewBackend( @@ -199,6 +205,10 @@ func main() { fmt.Printf("Listening on: http://localhost:%d\n", port) if *useSimulator { + // The simulator is only allowed when running in testnet mode. + if !backend.Testing() { + log.Panic("The BitBox02 simulator can only be used in testnet mode.") + } simulator.Init(*simulatorPort) } diff --git a/docs/BUILD.md b/docs/BUILD.md index 1a691c96a9..436befb8ab 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -105,9 +105,7 @@ Build the QT frontend for Windows: `make qt-windows` Build artifacts: * `frontends\qt\build\windows\*` - -To create the installer, run the NSIS UI, then: compile NSI scripts -> frontend/qt/setup.nsi, or run -`makensis setup.nsi`. +* `frontends\qt\build\BitBox-installer.exe` ## Android diff --git a/frontends/android/BitBoxApp/app/build.gradle b/frontends/android/BitBoxApp/app/build.gradle index 737f9fcf5c..d984940ad0 100644 --- a/frontends/android/BitBoxApp/app/build.gradle +++ b/frontends/android/BitBoxApp/app/build.gradle @@ -1,5 +1,12 @@ apply plugin: 'com.android.application' +def repoRoot = project.projectDir.parentFile.parentFile.parentFile.parentFile +def appVersionFile = new File(repoRoot, "APP_VERSION") +if (!appVersionFile.exists()) { + throw new GradleException("APP_VERSION not found at ${appVersionFile}") +} +def appVersion = appVersionFile.text.trim() + android { compileSdk 35 // Keep in sync with android-env.mk.inc and the NDK installed in docker_install.sh. @@ -10,8 +17,8 @@ android { // in backend/mobileserver/Makefile minSdkVersion 21 targetSdkVersion 35 - versionCode 64 - versionName "android-4.48.6" + versionCode 66 + versionName "${appVersion}" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { diff --git a/frontends/ios/.gitignore b/frontends/ios/.gitignore index 48b07741bb..78a36e109c 100644 --- a/frontends/ios/.gitignore +++ b/frontends/ios/.gitignore @@ -1,2 +1,3 @@ xcuserdata -/BitBoxApp/BitBoxApp/assets/web/ \ No newline at end of file +/BitBoxApp/BitBoxApp/assets/web/ +/BitBoxApp/AppVersion.xcconfig diff --git a/frontends/ios/BitBoxApp/BitBoxApp.xcodeproj/project.pbxproj b/frontends/ios/BitBoxApp/BitBoxApp.xcodeproj/project.pbxproj index 1ed49e2122..9fabce9c32 100644 --- a/frontends/ios/BitBoxApp/BitBoxApp.xcodeproj/project.pbxproj +++ b/frontends/ios/BitBoxApp/BitBoxApp.xcodeproj/project.pbxproj @@ -657,7 +657,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"BitBoxApp/Preview Content\""; - DEVELOPMENT_TEAM = MXZQ99HRD6; + DEVELOPMENT_TEAM = XK248TQN88; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BitBoxApp/Info.plist; @@ -689,7 +689,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"BitBoxApp/Preview Content\""; - DEVELOPMENT_TEAM = MXZQ99HRD6; + DEVELOPMENT_TEAM = XK248TQN88; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BitBoxApp/Info.plist; diff --git a/frontends/ios/BitBoxApp/Config.xcconfig b/frontends/ios/BitBoxApp/Config.xcconfig index e0dd7c43a7..69ffa6a244 100644 --- a/frontends/ios/BitBoxApp/Config.xcconfig +++ b/frontends/ios/BitBoxApp/Config.xcconfig @@ -1,9 +1,9 @@ // Configuration settings file format documentation can be found at: // https://help.apple.com/xcode/#/dev745c5c974 -// App version -MARKETING_VERSION = 4.48.7 +// AppVersion.xcconfig is generated by `make prepare`. +#include "AppVersion.xcconfig" // Build number, increment this for every separate publication, // even if the app version is the same. -CURRENT_PROJECT_VERSION = 12 +CURRENT_PROJECT_VERSION = 14 diff --git a/frontends/ios/Makefile b/frontends/ios/Makefile index f9b0882d52..0acd6c17c3 100644 --- a/frontends/ios/Makefile +++ b/frontends/ios/Makefile @@ -3,6 +3,7 @@ prepare: rm -rf BitBoxApp/BitBoxApp/assets/web/ mkdir -p BitBoxApp/BitBoxApp/assets/web/ cp -aR ../web/build/* BitBoxApp/BitBoxApp/assets/web/ + echo MARKETING_VERSION = $(shell cat ../../APP_VERSION) > BitBoxApp/AppVersion.xcconfig # Build Mobileserver.xcframework. No need to copy it, the xcode project references it # directly from the mobileserver folder. cd ../../backend/mobileserver && ${MAKE} build-ios diff --git a/frontends/qt/BitBox.pro b/frontends/qt/BitBox.pro index ae95c46509..3c80241ff4 100644 --- a/frontends/qt/BitBox.pro +++ b/frontends/qt/BitBox.pro @@ -32,7 +32,6 @@ include(external/singleapplication/singleapplication.pri) DEFINES += QAPPLICATION_CLASS=QApplication win32 { - # -llibssp would be nice to have on Windows LIBS += -L$$PWD/server/ -llibserver DESTDIR = $$PWD/build/windows RC_ICONS += $$PWD/resources/win/icon.ico diff --git a/frontends/qt/Makefile b/frontends/qt/Makefile index 0268a65051..a85b196d52 100644 --- a/frontends/qt/Makefile +++ b/frontends/qt/Makefile @@ -13,6 +13,7 @@ # limitations under the License. include ../../env.mk.inc +include ../../version.mk.inc # Add Wayland libs so the app can run natively on Wayland too. # The linuxdeployqt maintainer unfortunately refuses to support it automatically: https://github.com/probonopd/linuxdeployqt/issues/189 @@ -54,8 +55,8 @@ linux: cp resources/linux/usr/share/icons/hicolor/128x128/apps/bitbox.png build/linux-tmp mkdir build/tmp-deb/opt/ cp -aR build/linux-tmp build/tmp-deb/opt/bitbox - cd build/linux && fpm --after-install ../../resources/deb-afterinstall.sh -s dir -t deb -n bitbox -v 4.48.5 -C ../tmp-deb/ - cd build/linux && fpm --after-install ../../resources/deb-afterinstall.sh -s dir -t rpm -n bitbox -v 4.48.5 -C ../tmp-deb/ + cd build/linux && fpm --after-install ../../resources/deb-afterinstall.sh -s dir -t deb -n bitbox -v $(APP_VERSION) -C ../tmp-deb/ + cd build/linux && fpm --after-install ../../resources/deb-afterinstall.sh -s dir -t rpm -n bitbox -v $(APP_VERSION) -C ../tmp-deb/ # create AppImage cd build/linux-tmp && \ ./squashfs-root/AppRun BitBox \ @@ -74,6 +75,8 @@ osx: mkdir build/osx mv build/BitBox.app build/osx/ cp resources/MacOS/Info.plist build/osx/BitBox.app/Contents/ + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $(APP_VERSION)" build/osx/BitBox.app/Contents/Info.plist + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $(APP_VERSION)" build/osx/BitBox.app/Contents/Info.plist mkdir -p build/osx/BitBox.app/Contents/Resources cp resources/MacOS/icon.icns build/osx/BitBox.app/Contents/Resources/ cd build/osx/ && macdeployqt BitBox.app diff --git a/frontends/qt/make_windows.sh b/frontends/qt/make_windows.sh index 1d268d528d..65026c1f6a 100644 --- a/frontends/qt/make_windows.sh +++ b/frontends/qt/make_windows.sh @@ -21,4 +21,6 @@ env -u MAKE -u MAKEFLAGS cmd "/C compile_windows.bat" cp build/assets.rcc build/windows/ cp server/libserver.dll build/windows/ windeployqt build/windows/BitBox.exe -cp "$MINGW_BIN/libssp-0.dll" build/windows/ + +APP_VERSION="$(cat ../../APP_VERSION)" +makensis -DVERSION="${APP_VERSION}.0" setup.nsi diff --git a/frontends/qt/resources/MacOS/Info.plist b/frontends/qt/resources/MacOS/Info.plist index 8182449535..b11767e66a 100644 --- a/frontends/qt/resources/MacOS/Info.plist +++ b/frontends/qt/resources/MacOS/Info.plist @@ -21,10 +21,10 @@ APPL CFBundleVersion - 4.48.5 + 0.0.1 CFBundleShortVersionString - 4.48.5 + 0.0.1 CFBundleSignature ???? diff --git a/frontends/qt/server/Makefile.linux b/frontends/qt/server/Makefile.linux index 70e0c04a25..1e4961db87 100644 --- a/frontends/qt/server/Makefile.linux +++ b/frontends/qt/server/Makefile.linux @@ -1,5 +1,6 @@ include ../../../hardening.mk.inc include ../../../env.mk.inc +include ../../../version.mk.inc CGO=1 BUILDMODE=c-shared GOARCH=amd64 @@ -12,7 +13,8 @@ linux: CGO_LDFLAGS="${GOLNXLDFLAGS} ${LFLAGS}" \ GOARCH=${GOARCH} CGO_ENABLED=${CGO} GOOS=${GOOS} GOTOOLCHAIN=${GOTOOLCHAIN} \ go build -trimpath -x -mod=vendor -buildmode=${BUILDMODE} -ldflags \ - -extldflags="${GOLNXEXTLDFLAGS}" -o ${LIBNAME}.so + "$(GO_VERSION_LDFLAGS) -extldflags=\"$(GOLNXEXTLDFLAGS)\"" \ + -o ${LIBNAME}.so clean: -rm -f ${LIBNAME}.so diff --git a/frontends/qt/server/Makefile.macosx b/frontends/qt/server/Makefile.macosx index c76d0d816c..aa52ad15d8 100644 --- a/frontends/qt/server/Makefile.macosx +++ b/frontends/qt/server/Makefile.macosx @@ -1,5 +1,6 @@ include ../../../hardening.mk.inc include ../../../env.mk.inc +include ../../../version.mk.inc CGO=1 BUILDMODE=c-shared GOOS=darwin @@ -9,8 +10,8 @@ darwin: CGO_CFLAGS="-g ${GODARWINSECFLAGS} ${CFLAGS}" \ CGO_LDFLAGS="${GODARWINLDFLAGS} ${LFLAGS}" \ MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION} \ - GOARCH=arm64 CGO_ENABLED=${CGO} GOOS=${GOOS} GOTOOLCHAIN=${GOTOOLCHAIN} go build -trimpath -ldflags='-s' -x -mod=vendor -buildmode="c-shared" -o ${LIBNAME}_arm64.so - GOARCH=amd64 CGO_ENABLED=${CGO} GOOS=${GOOS} GOTOOLCHAIN=${GOTOOLCHAIN} go build -trimpath -ldflags='-s' -x -mod=vendor -buildmode="c-shared" -o ${LIBNAME}_amd64.so + GOARCH=arm64 CGO_ENABLED=${CGO} GOOS=${GOOS} GOTOOLCHAIN=${GOTOOLCHAIN} go build -trimpath -ldflags="-s $(GO_VERSION_LDFLAGS)" -x -mod=vendor -buildmode="c-shared" -o ${LIBNAME}_arm64.so + GOARCH=amd64 CGO_ENABLED=${CGO} GOOS=${GOOS} GOTOOLCHAIN=${GOTOOLCHAIN} go build -trimpath -ldflags="-s $(GO_VERSION_LDFLAGS)" -x -mod=vendor -buildmode="c-shared" -o ${LIBNAME}_amd64.so lipo -create -output ${LIBNAME}.so ${LIBNAME}_arm64.so ${LIBNAME}_amd64.so # Without this, the internal names of the two libs are libserver_arm64.so and libserver_amd64.so, and # will be linked with these names in the final executable (even though the library is fat and its name is libserver.so). diff --git a/frontends/qt/server/Makefile.windows b/frontends/qt/server/Makefile.windows index a36ad02de5..95f45e218f 100644 --- a/frontends/qt/server/Makefile.windows +++ b/frontends/qt/server/Makefile.windows @@ -1,5 +1,6 @@ include ../../../hardening.mk.inc include ../../../env.mk.inc +include ../../../version.mk.inc CGO=1 BUILDMODE=c-shared GOARCH=amd64 @@ -10,8 +11,9 @@ windows: CGO_CFLAGS="${GOWINSECFLAGS} ${CFLAGS}" \ CGO_LDFLAGS="${GOWINLDFLAGS} ${LFLAGS}" \ GOARCH=${GOARCH} CGO_ENABLED=${CGO} GOOS=${GOOS} GOTOOLCHAIN=${GOTOOLCHAIN} \ - go build -trimpath -x -mod=vendor \ - -buildmode="${BUILDMODE}" -o ${LIBNAME}.dll + go build -trimpath -x -mod=vendor \ + -ldflags "$(GO_VERSION_LDFLAGS)" \ + -buildmode="${BUILDMODE}" -o ${LIBNAME}.dll windows-cross: CC=/usr/bin/x86_64-w64-mingw32-gcc \ @@ -19,11 +21,12 @@ windows-cross: CGO_CFLAGS="-g ${GOWINSECFLAGS} ${CFLAGS}" \ CGO_LDFLAGS="${GOWINLDFLAGS} ${LFLAGS}" \ GOARCH=${GOARCH} CGO_ENABLED=${CGO} GOOS=${GOOS} GOTOOLCHAIN=${GOTOOLCHAIN} \ - go build -trimpath -x -mod=vendor \ - -buildmode="${BUILDMODE}" -o ${LIBNAME}.dll + go build -trimpath -x -mod=vendor \ + -ldflags "$(GO_VERSION_LDFLAGS)" \ + -buildmode="${BUILDMODE}" -o ${LIBNAME}.dll windows-legacy: - CGO_ENABLED=1 go build -mod=vendor -ldflags="-s -w" -buildmode=c-archive \ + CGO_ENABLED=1 go build -mod=vendor -ldflags="-s -w $(GO_VERSION_LDFLAGS)" -buildmode=c-archive \ -o libserver.a gcc server.def libserver.a -shared -lwinmm -lhid -lsetupapi -lWs2_32 \ -o libserver.dll -Wl,--out-implib,libserver.lib diff --git a/frontends/qt/server/server.go b/frontends/qt/server/server.go index 82f5dfe364..3fc2929e25 100644 --- a/frontends/qt/server/server.go +++ b/frontends/qt/server/server.go @@ -68,6 +68,7 @@ import ( "github.com/BitBoxSwiss/bitbox-wallet-app/backend/bridgecommon" btctypes "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/types" + "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/bitbox02/simulator" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/usb" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/mobileserver" "github.com/BitBoxSwiss/bitbox-wallet-app/util/logging" @@ -109,6 +110,14 @@ func setOnline(isReachable bool) { bridgecommon.SetOnline(isReachable) } +func deviceInfos() []usb.DeviceInfo { + testDeviceInfo := simulator.TestDeviceInfo() + if testDeviceInfo != nil { + return []usb.DeviceInfo{testDeviceInfo} + } + return usb.DeviceInfos() +} + //export serve func serve( cppHeapFreeFn C.cppHeapFree, @@ -121,6 +130,8 @@ func serve( log := logging.Get().WithGroup("server") log.WithField("args", os.Args).Info("Started Qt application") testnet := flag.Bool("testnet", false, "activate testnets") + simulatorPort := flag.Int("simulatorPort", 15423, "port for the BitBox02 simulator") + useSimulator := flag.Bool("simulator", false, "use the BitBox02 simulator. It implies --testnet.") if runtime.GOOS == "darwin" { // eat "-psn_xxxx" on Mac, which is passed when starting an app from Finder for the first time. @@ -146,12 +157,17 @@ func serve( } } + if *useSimulator { + simulator.Init(*simulatorPort) + } + // Capture C string early to avoid potential use when it's already popped // from the stack. nativeLocale := C.GoString(preferredLocale) bridgecommon.Serve( *testnet, + *useSimulator, gapLimits, &nativeCommunication{ respond: func(queryID int, response string) { @@ -171,7 +187,7 @@ func serve( defer C.free(unsafe.Pointer(cText)) C.notifyUser(notifyUserFn, cText) }, - DeviceInfosFunc: usb.DeviceInfos, + DeviceInfosFunc: deviceInfos, SystemOpenFunc: system.Open, UsingMobileDataFunc: func() bool { return false }, NativeLocaleFunc: func() string { return nativeLocale }, diff --git a/frontends/qt/setup.nsi b/frontends/qt/setup.nsi index 73983e84cd..5cb13bf909 100755 --- a/frontends/qt/setup.nsi +++ b/frontends/qt/setup.nsi @@ -22,7 +22,9 @@ SetCompressor /SOLID lzma # General Symbol Definitions !define REGKEY "SOFTWARE\$(^Name)" -!define VERSION 4.48.5.0 +!ifndef VERSION +!error "VERSION define not provided (pass -DVERSION=major.minor.patch.build)" +!endif !define COMPANY "Shift Crypto AG" !define URL https://github.com/BitBoxSwiss/bitbox-wallet-app/releases/ !define BINDIR "build\windows" diff --git a/frontends/web/eslint.config.mjs b/frontends/web/eslint.config.mjs index 9e84b5c34b..816a435dc3 100644 --- a/frontends/web/eslint.config.mjs +++ b/frontends/web/eslint.config.mjs @@ -26,7 +26,6 @@ export default tseslint.config( 'brace-style': ['error', '1tbs'], 'comma-spacing': ['error', { 'before': false, 'after': true }], 'curly': 'error', - 'indent': ['error', 2, { 'SwitchCase': 0 }], 'jsx-a11y/anchor-is-valid': 0, 'jsx-a11y/alt-text' : 0, 'jsx-quotes': ['error', 'prefer-double'], @@ -36,7 +35,6 @@ export default tseslint.config( 'no-trailing-spaces': 'error', 'object-curly-spacing': ['error', 'always'], 'quotes': ['error', 'single'], - 'semi': 'error', 'space-before-blocks': ['error', 'always'], 'space-in-parens': ['error', 'never'], 'no-extra-semi': 'error', @@ -63,6 +61,19 @@ export default tseslint.config( 'logical': 'parens-new-line' }], '@stylistic/type-generic-spacing': ['error'], + '@stylistic/indent': ['error', 2, { "SwitchCase": 0 }], + '@stylistic/semi': ["error", "always"], + '@stylistic/member-delimiter-style': ['error', { + "multiline": { + "delimiter": "semi", + "requireLast": true + }, + "singleline": { + "delimiter": "semi", + "requireLast": false + }, + "multilineDetection": "brackets" + }], }, }, { diff --git a/frontends/web/package-lock.json b/frontends/web/package-lock.json index dc7a19b42e..3df33a8057 100644 --- a/frontends/web/package-lock.json +++ b/frontends/web/package-lock.json @@ -1620,6 +1620,7 @@ }, "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { "version": "1.1.0", + "extraneous": true, "inBundle": true, "license": "MIT" }, diff --git a/frontends/web/package.json b/frontends/web/package.json index e6096e497b..cd57026f3e 100644 --- a/frontends/web/package.json +++ b/frontends/web/package.json @@ -51,7 +51,7 @@ "dev": "npm run start", "start": "vite --cors --host", "build": "npm run typescript && vite build", - "lint": "npm run typescript && eslint ./src", + "lint": "npm run typescript && eslint ./src ./tests", "lint-debug": "eslint ./src --inspect-config", "typescript": "tsc -p tsconfig.json", "test": "vitest", diff --git a/frontends/web/playwright.config.ts b/frontends/web/playwright.config.ts index 021295b9e1..b9aabd4df3 100644 --- a/frontends/web/playwright.config.ts +++ b/frontends/web/playwright.config.ts @@ -4,6 +4,7 @@ import * as path from 'path'; // Read defaults from environment variables if set, otherwise fallback const HOST = process.env.HOST || 'localhost'; const FRONTEND_PORT = parseInt(process.env.FRONTEND_PORT || '8080', 10); +const PLAYWRIGHT_SLOW_MO = parseInt(process.env.PLAYWRIGHT_SLOW_MO || '0', 10); export default defineConfig({ testDir: path.join(__dirname, 'tests'), @@ -17,12 +18,19 @@ export default defineConfig({ }, ], timeout: 120_000, + workers: 1, // Tests are not parallel-safe yet. use: { baseURL: `http://${HOST}:${FRONTEND_PORT}`, headless: true, - video: 'retain-on-failure', + video: 'on', screenshot: 'only-on-failure', - trace: 'retain-on-failure', + trace: 'on', + launchOptions: { + // By default, tests are not run in slow motion. + // Can be enabled by setting the PLAYWRIGHT_SLOW_MO environment variable to a value > 0. + // This is useful for running tests locally. + slowMo: PLAYWRIGHT_SLOW_MO, + }, }, reporter: [['html', { open: 'never' }], ['list']], outputDir: 'test-results/', diff --git a/frontends/web/src/api/account.ts b/frontends/web/src/api/account.ts index 0aaada6701..602dcd2749 100644 --- a/frontends/web/src/api/account.ts +++ b/frontends/web/src/api/account.ts @@ -27,7 +27,7 @@ export type AccountCode = string; export type Fiat = 'AUD' | 'BRL' | 'BTC' | 'CAD' | 'CHF' | 'CNY' | 'CZK' | 'EUR' | 'GBP' | 'HKD' | 'ILS' | 'JPY' | 'KRW' | 'NOK' | 'NZD' | 'PLN' | 'RUB' | 'sat' | 'SEK' | 'SGD' | 'USD'; -export type ConversionUnit = Fiat | 'sat' +export type ConversionUnit = Fiat | 'sat'; export type CoinUnit = 'BTC' | 'sat' | 'LTC' | 'ETH' | 'TBTC' | 'tsat' | 'TLTC' | 'SEPETH'; @@ -38,9 +38,9 @@ export type ERC20CoinCode = 'erc20Test' | 'eth-erc20-usdt' | 'eth-erc20-usdc' | export type CoinCode = NativeCoinCode | ERC20CoinCode; export type FiatWithDisplayName = { - currency: Fiat, - displayName: string -} + currency: Fiat; + displayName: string; +}; export type Terc20Token = { code: ERC20CoinCode; @@ -48,10 +48,10 @@ export type Terc20Token = { unit: ERC20TokenUnit; }; -export interface IActiveToken { +export type TActiveToken = { tokenCode: ERC20CoinCode; accountCode: AccountCode; -} +}; export type TKeystore = { watchonly: boolean; @@ -61,7 +61,7 @@ export type TKeystore = { connected: boolean; }; -export interface IAccount { +export type TAccount = { keystore: TKeystore; active: boolean; watch: boolean; @@ -71,13 +71,13 @@ export interface IAccount { code: AccountCode; name: string; isToken: boolean; - activeTokens?: IActiveToken[]; + activeTokens?: TActiveToken[]; blockExplorerTxPrefix: string; bitsuranceStatus?: TDetailStatus; accountNumber?: number; -} +}; -export const getAccounts = (): Promise => { +export const getAccounts = (): Promise => { return apiGet('accounts'); }; @@ -92,9 +92,9 @@ export type TAmountsByCoin = { }; export type TKeystoreBalance = { - fiatUnit: ConversionUnit; - total: string; - coinsBalance?: TAmountsByCoin; + fiatUnit: ConversionUnit; + total: string; + coinsBalance?: TAmountsByCoin; }; export type TKeystoresBalance = { @@ -111,7 +111,7 @@ export type TAccountsBalanceSummaryResponse = { accountsBalanceSummary: TAccountsBalanceSummary; } | { success: false; -} +}; export const getAccountsBalanceSummary = (): Promise => { return apiGet('accounts/balance-summary'); @@ -120,20 +120,20 @@ export const getAccountsBalanceSummary = (): Promise => { return apiPost('accounts/eth-account-code', { address }); }; -export interface IStatus { - disabled: boolean; - synced: boolean; - fatalError: boolean; - offlineError: string | null; -} +export type TStatus = { + disabled: boolean; + synced: boolean; + fatalError: boolean; + offlineError: string | null; +}; -export const getStatus = (code: AccountCode): Promise => { +export const getStatus = (code: AccountCode): Promise => { return apiGet(`account/${code}/status`); }; @@ -141,32 +141,32 @@ export type ScriptType = 'p2pkh' | 'p2wpkh-p2sh' | 'p2wpkh' | 'p2tr'; export const allScriptTypes: ScriptType[] = ['p2pkh', 'p2wpkh-p2sh', 'p2wpkh', 'p2tr']; -export interface IKeyInfo { - keypath: string; - rootFingerprint: string; - xpub: string; -} +type TKeyInfo = { + keypath: string; + rootFingerprint: string; + xpub: string; +}; export type TBitcoinSimple = { - keyInfo: IKeyInfo; - scriptType: ScriptType; -} + keyInfo: TKeyInfo; + scriptType: ScriptType; +}; export type TEthereumSimple = { - keyInfo: IKeyInfo; -} + keyInfo: TKeyInfo; +}; export type TSigningConfiguration = { - bitcoinSimple: TBitcoinSimple; - ethereumSimple?: never; + bitcoinSimple: TBitcoinSimple; + ethereumSimple?: never; } | { - bitcoinSimple?: never; - ethereumSimple: TEthereumSimple; -} + bitcoinSimple?: never; + ethereumSimple: TEthereumSimple; +}; export type TSigningConfigurationList = null | { - signingConfigurations: TSigningConfiguration[]; -} + signingConfigurations: TSigningConfiguration[]; +}; export const getInfo = (code: AccountCode) => { return (): Promise => { @@ -184,52 +184,52 @@ export type FormattedLineData = LineData & { export type ChartData = FormattedLineData[]; -export type TChartDataResponse = { - success: true; - data: TChartData; +type TChartDataResponse = { + success: true; + data: TChartData; } | { success: false; -} +}; export type TChartData = { - chartDataMissing: boolean; - chartDataDaily: ChartData; - chartDataHourly: ChartData; - chartFiat: ConversionUnit; - chartTotal: number | null; - formattedChartTotal: string | null; - chartIsUpToDate: boolean; // only valid if chartDataMissing is false - lastTimestamp: number; -} + chartDataMissing: boolean; + chartDataDaily: ChartData; + chartDataHourly: ChartData; + chartFiat: ConversionUnit; + chartTotal: number | null; + formattedChartTotal: string | null; + chartIsUpToDate: boolean; // only valid if chartDataMissing is false + lastTimestamp: number; +}; export const getChartData = (): Promise => { return apiGet('chart-data'); }; -export type Conversions = { - [key in Fiat]?: string; +type Conversions = { + [key in Fiat]?: string; }; export type TAmountWithConversions = { - amount: string; - conversions?: Conversions; - unit: CoinUnit; - estimated: boolean; + amount: string; + conversions?: Conversions; + unit: CoinUnit; + estimated: boolean; }; -export interface IBalance { - hasAvailable: boolean; - available: TAmountWithConversions; - hasIncoming: boolean; - incoming: TAmountWithConversions; -} +export type TBalance = { + hasAvailable: boolean; + available: TAmountWithConversions; + hasIncoming: boolean; + incoming: TAmountWithConversions; +}; -export type TBalanceResponse = { +type TBalanceResponse = { success: true; - balance: IBalance; + balance: TBalance; } | { success: false; -} +}; export const getBalance = (code: AccountCode): Promise => { return apiGet(`account/${code}/balance`); @@ -238,39 +238,39 @@ export const getBalance = (code: AccountCode): Promise => { export type TTransactionStatus = 'complete' | 'pending' | 'failed'; export type TTransactionType = 'send' | 'receive' | 'send_to_self'; -export interface ITransaction { - addresses: string[]; - amount: TAmountWithConversions; - amountAtTime: TAmountWithConversions; - fee: TAmountWithConversions; - feeRatePerKb: TAmountWithConversions; - deductedAmountAtTime: TAmountWithConversions; - gas: number; - nonce: number | null; - internalID: string; - note: string; - numConfirmations: number; - numConfirmationsComplete: number; - size: number; - status: TTransactionStatus; - time: string | null; - type: TTransactionType; - txID: string; - vsize: number; - weight: number; -} - -export type TTransactions = { success: false } | { success: true; list: ITransaction[]; }; - -export interface INoteTx { - internalTxID: string; - note: string; -} +export type TTransaction = { + addresses: string[]; + amount: TAmountWithConversions; + amountAtTime: TAmountWithConversions; + fee: TAmountWithConversions; + feeRatePerKb: TAmountWithConversions; + deductedAmountAtTime: TAmountWithConversions; + gas: number; + nonce: number | null; + internalID: string; + note: string; + numConfirmations: number; + numConfirmationsComplete: number; + size: number; + status: TTransactionStatus; + time: string | null; + type: TTransactionType; + txID: string; + vsize: number; + weight: number; +}; + +export type TTransactions = { success: false } | { success: true; list: TTransaction[] }; + +type TNoteTx = { + internalTxID: string; + note: string; +}; export const postNotesTx = (code: AccountCode, { internalTxID, note, -}: INoteTx): Promise => { +}: TNoteTx): Promise => { return apiPost(`account/${code}/notes/tx`, { internalTxID, note }); }; @@ -278,39 +278,39 @@ export const getTransactionList = (code: AccountCode): Promise => return apiGet(`account/${code}/transactions`); }; -export const getTransaction = (code: AccountCode, id: ITransaction['internalID']): Promise => { +export const getTransaction = (code: AccountCode, id: TTransaction['internalID']): Promise => { return apiGet(`account/${code}/transaction?id=${id}`); }; -export interface IExport { - success: boolean; - path: string; - errorMessage: string; -} +type TExport = { + success: boolean; + path: string; + errorMessage: string; +}; -export const exportAccount = (code: AccountCode): Promise => { +export const exportAccount = (code: AccountCode): Promise => { return apiPost(`account/${code}/export`); }; export const verifyXPub = ( code: AccountCode, signingConfigIndex: number, -): Promise<{ success: true; } | { success: false; errorMessage: string; }> => { +): Promise<{ success: true } | { success: false; errorMessage: string }> => { return apiPost(`account/${code}/verify-extended-public-key`, { signingConfigIndex }); }; -export interface IReceiveAddress { - addressID: string; - address: string; -} +export type TReceiveAddress = { + addressID: string; + address: string; +}; -export interface ReceiveAddressList { - scriptType: ScriptType | null; - addresses: NonEmptyArray; -} +export type TReceiveAddressList = { + scriptType: ScriptType | null; + addresses: NonEmptyArray; +}; export const getReceiveAddressList = (code: AccountCode) => { - return (): Promise | null> => { + return (): Promise | null> => { return apiGet(`account/${code}/receive-addresses`); }; }; @@ -369,33 +369,17 @@ export const sendTx = ( export type FeeTargetCode = 'custom' | 'low' | 'economy' | 'normal' | 'high' | 'mHour' | 'mHalfHour' | 'mFastest'; -export interface IProposeTxData { - address?: string; - amount?: number; - // data?: string; - feePerByte: string; - feeTarget: FeeTargetCode; - selectedUTXOs: string[]; - sendAll: 'yes' | 'no'; -} - -export interface IProposeTx { - aborted?: boolean; - success?: boolean; - errorMessage?: string; -} - -export interface IFeeTarget { - code: FeeTargetCode; - feeRateInfo: string; -} - -export interface IFeeTargetList { - feeTargets: IFeeTarget[]; - defaultFeeTarget: FeeTargetCode; -} - -export const getFeeTargetList = (code: AccountCode): Promise => { +export type TFeeTarget = { + code: FeeTargetCode; + feeRateInfo: string; +}; + +export type TFeeTargetList = { + feeTargets: TFeeTarget[]; + defaultFeeTarget: FeeTargetCode; +}; + +export const getFeeTargetList = (code: AccountCode): Promise => { return apiGet(`account/${code}/fee-targets`); }; @@ -421,8 +405,8 @@ export const getUTXOs = (code: AccountCode): Promise => { }; type TSecureOutput = { - hasSecureOutput: boolean; - optional: boolean; + hasSecureOutput: boolean; + optional: boolean; }; export const hasSecureOutput = (code: AccountCode) => { @@ -446,7 +430,7 @@ export type TAddAccount = { accountCode?: string; errorCode?: 'accountAlreadyExists' | 'accountLimitReached'; errorMessage?: string; -} +}; export const addAccount = (coinCode: string, name: string): Promise => { return apiPost('account-add', { @@ -455,7 +439,7 @@ export const addAccount = (coinCode: string, name: string): Promise }); }; -export type TSignMessage = { success: false, aborted?: boolean; errorMessage?: string; } | { success: true; signature: string; } +export type TSignMessage = { success: false; aborted?: boolean; errorMessage?: string } | { success: true; signature: string }; export type TSignWalletConnectTx = { success: false; @@ -465,7 +449,7 @@ export type TSignWalletConnectTx = { success: true; txHash: string; rawTx: string; -} +}; export const ethSignMessage = (code: AccountCode, message: string): Promise => { return apiPost(`account/${code}/eth-sign-msg`, message); @@ -479,7 +463,7 @@ export const ethSignWalletConnectTx = (code: AccountCode, send: boolean, chainId return apiPost(`account/${code}/eth-sign-wallet-connect-tx`, { send, chainId, tx }); }; -export type AddressSignResponse = { +type AddressSignResponse = { success: true; signature: string; address: string; @@ -487,7 +471,7 @@ export type AddressSignResponse = { success: false; errorMessage?: string; errorCode?: 'userAbort' | 'wrongKeystore'; -} +}; export const signAddress = (format: ScriptType | '', msg: string, code: AccountCode): Promise => { return apiPost(`account/${code}/sign-address`, { format, msg, code }); diff --git a/frontends/web/src/api/accountsync.ts b/frontends/web/src/api/accountsync.ts index 1b4ec60a24..6a6544e415 100644 --- a/frontends/web/src/api/accountsync.ts +++ b/frontends/web/src/api/accountsync.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { TUnsubscribe } from '@/utils/transport-common'; -import * as accountAPI from './account'; +import type { TUnsubscribe } from '@/utils/transport-common'; +import type { AccountCode, TAccount, TStatus } from './account'; import { TSubscriptionCallback, subscribeEndpoint } from './subscribe'; /** @@ -24,7 +24,7 @@ import { TSubscriptionCallback, subscribeEndpoint } from './subscribe'; * Returns a method to unsubscribe. */ export const syncAccountsList = ( - cb: (accounts: accountAPI.IAccount[],) => void + cb: (accounts: TAccount[]) => void ): TUnsubscribe => { return subscribeEndpoint('accounts', cb); }; @@ -34,7 +34,7 @@ export const syncAccountsList = ( * event to receive the progress of the address sync. * Meant to be used with `useSubscribe`. */ -export const syncAddressesCount = (code: accountAPI.AccountCode) => { +export const syncAddressesCount = (code: AccountCode) => { return ( cb: TSubscriptionCallback ) => { @@ -48,8 +48,8 @@ export const syncAddressesCount = (code: accountAPI.AccountCode) => { * Returns a method to unsubscribe. */ export const statusChanged = ( - code: accountAPI.AccountCode, - cb: TSubscriptionCallback, + code: AccountCode, + cb: TSubscriptionCallback, ): TUnsubscribe => { return subscribeEndpoint(`account/${code}/status`, cb); }; @@ -59,7 +59,7 @@ export const statusChanged = ( * Returns a method to unsubscribe. */ export const syncdone = ( - code: accountAPI.AccountCode, + code: AccountCode, cb: () => void, ): TUnsubscribe => { return subscribeEndpoint(`account/${code}/sync-done`, cb); diff --git a/frontends/web/src/api/aopp.ts b/frontends/web/src/api/aopp.ts index f0ca3f794f..bc92f65648 100644 --- a/frontends/web/src/api/aopp.ts +++ b/frontends/web/src/api/aopp.ts @@ -14,43 +14,42 @@ * limitations under the License. */ -import { AccountCode } from './account'; -import { apiGet, apiPost } from '@/utils/request'; +import type { AccountCode } from './account'; import type { TUnsubscribe } from '@/utils/transport-common'; +import type { NonEmptyArray } from '@/utils/types'; +import { apiGet, apiPost } from '@/utils/request'; import { subscribeEndpoint } from './subscribe'; -export interface Account { - name: string; - code: AccountCode; -} +type TAccount = { + name: string; + code: AccountCode; +}; -interface Accounts extends Array { - 0: Account, -} +type Accounts = NonEmptyArray; export type Aopp = { - state: 'error'; - errorCode: 'aoppUnsupportedAsset' | 'aoppVersion' | 'aoppInvalidRequest' | 'aoppNoAccounts' | 'aoppUnsupportedKeystore' | 'aoppUnknown' | 'aoppSigningAborted' | 'aoppCallback'; - callback: string; + state: 'error'; + errorCode: 'aoppUnsupportedAsset' | 'aoppVersion' | 'aoppInvalidRequest' | 'aoppNoAccounts' | 'aoppUnsupportedKeystore' | 'aoppUnknown' | 'aoppSigningAborted' | 'aoppCallback'; + callback: string; } | { - state: 'inactive'; + state: 'inactive'; } | { - state: 'user-approval' | 'awaiting-keystore' | 'syncing'; - message: string; - callback: string; - xpubRequired: boolean; + state: 'user-approval' | 'awaiting-keystore' | 'syncing'; + message: string; + callback: string; + xpubRequired: boolean; } | { - state: 'choosing-account'; - accounts: Accounts; - message: string; - callback: string; + state: 'choosing-account'; + accounts: Accounts; + message: string; + callback: string; } | { - state: 'signing' | 'success'; - address: string; - addressID: string; - message: string; - callback: string; - accountCode: AccountCode; + state: 'signing' | 'success'; + address: string; + addressID: string; + message: string; + callback: string; + accountCode: AccountCode; }; export const cancel = (): Promise => { diff --git a/frontends/web/src/api/backend.ts b/frontends/web/src/api/backend.ts index da069adddd..4e980a40d9 100644 --- a/frontends/web/src/api/backend.ts +++ b/frontends/web/src/api/backend.ts @@ -1,5 +1,5 @@ /** - * Copyright 2021-2024 Shift Crypto AG + * Copyright 2021-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. @@ -19,24 +19,27 @@ import type { FailResponse, SuccessResponse } from './response'; import { apiGet, apiPost } from '@/utils/request'; import { TSubscriptionCallback, subscribeEndpoint } from './subscribe'; -export interface ICoin { - coinCode: CoinCode; - name: string; - canAddAccount: boolean; - suggestedAccountName: string; -} +export type TCoin = { + coinCode: CoinCode; + name: string; + canAddAccount: boolean; + suggestedAccountName: string; +}; -export interface ISuccess { - success: boolean; - errorMessage?: string; - errorCode?: string; -} +// In other places we use type { FailResponse, SuccessResponse } from './response' +// which has slightly different FailResponse structure ( message?: string; code?: number) +// but here we use errorMessage and errorCode instead +type TSuccess = { + success: boolean; + errorMessage?: string; + errorCode?: string; +}; -export const getSupportedCoins = (): Promise => { +export const getSupportedCoins = (): Promise => { return apiGet('supported-coins'); }; -export const setAccountActive = (accountCode: AccountCode, active: boolean): Promise => { +export const setAccountActive = (accountCode: AccountCode, active: boolean): Promise => { return apiPost('set-account-active', { accountCode, active }); }; @@ -44,11 +47,11 @@ export const setTokenActive = ( accountCode: AccountCode, tokenCode: ERC20CoinCode, active: boolean, -): Promise => { +): Promise => { return apiPost('set-token-active', { accountCode, tokenCode, active }); }; -export const renameAccount = (accountCode: AccountCode, name: string): Promise => { +export const renameAccount = (accountCode: AccountCode, name: string): Promise => { return apiPost('rename-account', { accountCode, name }); }; @@ -64,7 +67,7 @@ export const getDevServers = (): Promise => { return apiGet('dev-servers'); }; -export type TQRCode = FailResponse | (SuccessResponse & { data: string; }); +type TQRCode = FailResponse | (SuccessResponse & { data: string }); export const getQRCode = (data: string) => { return (): Promise => { @@ -76,7 +79,7 @@ export const getDefaultConfig = (): Promise => { return apiGet('config/default'); }; -export const socksProxyCheck = (proxyAddress: string): Promise => { +export const socksProxyCheck = (proxyAddress: string): Promise => { return apiPost('socksproxy/check', proxyAddress); }; @@ -111,7 +114,7 @@ export const cancelConnectKeystore = (): Promise => { return apiPost('cancel-connect-keystore'); }; -export const setWatchonly = (rootFingerprint: string, watchonly: boolean): Promise => { +export const setWatchonly = (rootFingerprint: string, watchonly: boolean): Promise => { return apiPost('set-watchonly', { rootFingerprint, watchonly }); }; @@ -124,10 +127,10 @@ export const forceAuth = (): Promise => { }; export type TAuthEventObject = { - typ: 'auth-required' | 'auth-forced' ; + typ: 'auth-required' | 'auth-forced'; } | { typ: 'auth-result'; - result: 'authres-cancel' | 'authres-ok' | 'authres-err' | 'authres-missing' + result: 'authres-cancel' | 'authres-ok' | 'authres-err' | 'authres-missing'; }; export const subscribeAuth = ( @@ -140,20 +143,20 @@ export const onAuthSettingChanged = (): Promise => { return apiPost('on-auth-setting-changed'); }; -export const exportLogs = (): Promise => { +export const exportLogs = (): Promise => { return apiPost('export-log'); }; -export const exportNotes = (): Promise<(FailResponse & { aborted: boolean; }) | SuccessResponse> => { +export const exportNotes = (): Promise<(FailResponse & { aborted: boolean }) | SuccessResponse> => { return apiPost('notes/export'); }; -export type TImportNotes = { +type TImportNotes = { accountCount: number; transactionCount: number; }; -export const importNotes = (fileContents: ArrayBuffer): Promise => { +export const importNotes = (fileContents: ArrayBuffer): Promise => { const hexString = Array.from(new Uint8Array(fileContents)) .map(byte => byte.toString(16).padStart(2, '0')) .join(''); diff --git a/frontends/web/src/api/backup.ts b/frontends/web/src/api/backup.ts index 5a4bd6b20a..b217dcd776 100644 --- a/frontends/web/src/api/backup.ts +++ b/frontends/web/src/api/backup.ts @@ -1,17 +1,33 @@ +/** + * Copyright 2023-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 type { FailResponse } from './response'; import { apiGet } from '@/utils/request'; -import { FailResponse } from './response'; import { TSubscriptionCallback, subscribeEndpoint } from './subscribe'; export type Backup = { - id: string; - date: string; - name: string; + id: string; + date: string; + name: string; }; type BackupResponse = { - success: true; - backups: Backup[]; -} + success: true; + backups: Backup[]; +}; export const getBackupList = ( deviceID: string diff --git a/frontends/web/src/api/backupBanner.ts b/frontends/web/src/api/backupBanner.ts index 67a8ac1a87..7d744b244a 100644 --- a/frontends/web/src/api/backupBanner.ts +++ b/frontends/web/src/api/backupBanner.ts @@ -15,12 +15,17 @@ * limitations under the License. */ -import { apiGet } from '@/utils/request'; import type { Fiat } from './account'; +import { apiGet } from '@/utils/request'; -export type TShowBackupBannerResponse = - | { success: false } - | { success: true; show: boolean; fiat: Fiat; threshold: string }; +export type TShowBackupBannerResponse = { + success: false; +} | { + success: true; + show: boolean; + fiat: Fiat; + threshold: string; +}; export const getShowBackupBanner = (rootFingerprint: string): Promise => { return apiGet(`keystore/show-backup-banner/${rootFingerprint}`); diff --git a/frontends/web/src/api/banners.ts b/frontends/web/src/api/banners.ts index aae838b2ac..7762488d35 100644 --- a/frontends/web/src/api/banners.ts +++ b/frontends/web/src/api/banners.ts @@ -14,20 +14,20 @@ * limitations under the License. */ +import type { TMessageTypes } from '@/utils/types'; import { apiGet } from '@/utils/request'; import { subscribeEndpoint, TUnsubscribe } from './subscribe'; -import type { TMessageTypes } from '@/utils/types'; export type TBannerInfo = { id: string; - message: { [key: string]: string; }; + message: { [key: string]: string }; link?: { href: string; text?: string; }; dismissible?: boolean; type?: TMessageTypes; -} +}; export const getBanner = (msgKey: string): Promise => { return apiGet(`banners/${msgKey}`); diff --git a/frontends/web/src/api/bitbox02.ts b/frontends/web/src/api/bitbox02.ts index d178f6e9a0..78ea8f3964 100644 --- a/frontends/web/src/api/bitbox02.ts +++ b/frontends/web/src/api/bitbox02.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { TUnsubscribe } from '@/utils/transport-common'; +import type { TUnsubscribe } from '@/utils/transport-common'; +import type { SuccessResponse, FailResponse } from './response'; import { apiGet, apiPost } from '@/utils/request'; -import { SuccessResponse, FailResponse } from './response'; import { TSubscriptionCallback, subscribeEndpoint } from './subscribe'; // BitBox02 error codes. @@ -32,7 +32,7 @@ export type DeviceInfo = { firmwareVersion: string; enabled: boolean; }; -} +}; type DeviceInfoResponse = SuccessResponse & { deviceInfo: DeviceInfo; @@ -82,11 +82,11 @@ type VersionInfoCommon = { canBackupWithRecoveryWords: boolean; canCreate12Words: boolean; canBIP85: boolean; -} +}; export type VersionInfo = VersionInfoCommon & ( - { canUpgrade: true, newVersion: string; } | - { canUpgrade: false; }) + { canUpgrade: true; newVersion: string } | + { canUpgrade: false }); export const getVersion = ( deviceID: string @@ -110,7 +110,7 @@ export const verifyAttestation = ( export const checkBackup = ( deviceID: string, silent: boolean, -): Promise => { +): Promise => { return apiPost(`devices/bitbox02/${deviceID}/backups/check`, { silent }); }; diff --git a/frontends/web/src/api/bitbox02bootloader.ts b/frontends/web/src/api/bitbox02bootloader.ts index 74cbd11942..52f0f6b6a0 100644 --- a/frontends/web/src/api/bitbox02bootloader.ts +++ b/frontends/web/src/api/bitbox02bootloader.ts @@ -38,13 +38,13 @@ export const syncStatus = (deviceID: string) => { }; }; -export type TProduct = +type TProduct = 'bitbox02-multi' | 'bitbox02-btconly' | 'bitbox02-plus-multi' | 'bitbox02-plus-btconly'; -export type TInfo = { +type TInfo = { product: TProduct; // Indicates whether the device has any firmware already installed on it. // It is considered "erased" if there's no firmware, and it also happens diff --git a/frontends/web/src/api/bitsurance.ts b/frontends/web/src/api/bitsurance.ts index b98b5c5755..b87f562b58 100644 --- a/frontends/web/src/api/bitsurance.ts +++ b/frontends/web/src/api/bitsurance.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import type { AccountCode } from './account'; import { apiGet, apiPost } from '@/utils/request'; -import { AccountCode } from './account'; export type TDetailStatus = 'active' | 'processing' | 'refused' | 'waitpayment' | 'inactive' | 'canceled'; @@ -30,11 +30,12 @@ export type TAccountDetails = { }; }; -export type TInsuredAccounts = { +type TInsuredAccounts = { success: boolean; errorMessage: string; bitsuranceAccounts: TAccountDetails[]; }; + export const getBitsuranceURL = (): Promise => { return apiGet('bitsurance/url'); }; diff --git a/frontends/web/src/api/coins.ts b/frontends/web/src/api/coins.ts index 2a047353ac..bdfe3b53d8 100644 --- a/frontends/web/src/api/coins.ts +++ b/frontends/web/src/api/coins.ts @@ -14,18 +14,18 @@ * limitations under the License. */ -import { subscribeEndpoint, TSubscriptionCallback } from './subscribe'; import type { CoinCode, Fiat } from './account'; +import { subscribeEndpoint, TSubscriptionCallback } from './subscribe'; import { apiPost, apiGet } from '@/utils/request'; export type BtcUnit = 'default' | 'sat'; export type TStatus = { - targetHeight: number; - tip: number; - tipAtInitTime: number; - tipHashHex: string; -} + targetHeight: number; + tip: number; + tipAtInitTime: number; + tipHashHex: string; +}; export const subscribeCoinHeaders = (coinCode: CoinCode) => ( (cb: TSubscriptionCallback) => ( @@ -44,7 +44,7 @@ export const setBtcUnit = (unit: BtcUnit): Promise => { export type TAmount = { success: boolean; amount: string; -} +}; export const parseExternalBtcAmount = (amount: string): Promise => { return apiGet(`coins/btc/parse-external-amount?amount=${amount}`); diff --git a/frontends/web/src/api/devices.ts b/frontends/web/src/api/devices.ts index b05c119b9a..7e2109fe5f 100644 --- a/frontends/web/src/api/devices.ts +++ b/frontends/web/src/api/devices.ts @@ -19,7 +19,7 @@ import { apiGet } from '@/utils/request'; export type TPlatformName = 'bitbox' | 'bitbox02' | 'bitbox02-bootloader'; export type TDevices = { - readonly [key in string]: TPlatformName; + readonly [key in string]: TPlatformName; }; export const getDeviceList = (): Promise => { diff --git a/frontends/web/src/api/devicessync.ts b/frontends/web/src/api/devicessync.ts index fb5b6f6beb..5c51020d2f 100644 --- a/frontends/web/src/api/devicessync.ts +++ b/frontends/web/src/api/devicessync.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import type { TDevices } from './devices'; import { subscribeEndpoint, TSubscriptionCallback, TUnsubscribe } from './subscribe'; -import { TDevices } from './devices'; /** * Subscribes the given function on the "devices/registered" event diff --git a/frontends/web/src/api/keystores.ts b/frontends/web/src/api/keystores.ts index 5bbb587e09..e05d8bb8ce 100644 --- a/frontends/web/src/api/keystores.ts +++ b/frontends/web/src/api/keystores.ts @@ -50,7 +50,7 @@ export const deregisterTest = (): Promise => { return apiPost('test/deregister'); }; -export const connectKeystore = (rootFingerprint: string): Promise<{ success: boolean; }> => { +export const connectKeystore = (rootFingerprint: string): Promise<{ success: boolean }> => { return apiPost('connect-keystore', { rootFingerprint }); }; diff --git a/frontends/web/src/api/market.ts b/frontends/web/src/api/market.ts index 346a0aab8c..e4aa5c1cb2 100644 --- a/frontends/web/src/api/market.ts +++ b/frontends/web/src/api/market.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AccountCode } from './account'; +import type { AccountCode } from './account'; import { apiGet, apiPost } from '@/utils/request'; export const getMarketRegionCodes = (): Promise => { @@ -29,27 +29,27 @@ export type TMarketDeal = { isFast: boolean; isBest: boolean; isHidden: boolean; -} +}; export type TVendorName = 'moonpay' | 'pocket' | 'btcdirect' | 'btcdirect-otc' | 'bitrefill'; export type TMarketDeals = { vendorName: TVendorName; deals: TMarketDeal[]; -} +}; export type TMarketDealsList = { deals: TMarketDeals[]; success: true; -} +}; export type TMarketError = { success: false; errorCode?: 'coinNotSupported' | 'regionNotSupported'; errorMessage?: string; -} +}; -export type TMarketDealsResponse = TMarketDealsList | TMarketError +export type TMarketDealsResponse = TMarketDealsList | TMarketError; export type TMarketAction = 'buy' | 'sell' | 'spend'; @@ -60,7 +60,7 @@ export const getMarketDeals = (action: TMarketAction, accountCode: AccountCode, export type MoonpayBuyInfo = { url: string; address: string; -} +}; export const getMoonpayBuyInfo = (code: AccountCode) => { return (): Promise => { @@ -72,7 +72,7 @@ export type AddressVerificationResponse = { success: boolean; errorMessage?: string; errorCode?: 'addressNotFound' | 'userAbort'; -} +}; export const verifyAddress = (address: string, accountCode: AccountCode): Promise => { return apiPost('market/pocket/verify-address', { address, accountCode }); diff --git a/frontends/web/src/api/nativelocale.ts b/frontends/web/src/api/nativelocale.ts index 491a43e81a..b57e39bc03 100644 --- a/frontends/web/src/api/nativelocale.ts +++ b/frontends/web/src/api/nativelocale.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2022 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 { apiGet } from '@/utils/request'; export const getNativeLocale = (): Promise => { diff --git a/frontends/web/src/api/node.ts b/frontends/web/src/api/node.ts index 9ad668e669..33c6377ff6 100644 --- a/frontends/web/src/api/node.ts +++ b/frontends/web/src/api/node.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import type { SuccessResponse } from './response'; import { apiPost } from '@/utils/request'; -import { SuccessResponse } from './response'; type TCertResponse = { success: true; diff --git a/frontends/web/src/api/response.ts b/frontends/web/src/api/response.ts index f55b3c0b46..a5743bfdeb 100644 --- a/frontends/web/src/api/response.ts +++ b/frontends/web/src/api/response.ts @@ -15,12 +15,12 @@ */ export type SuccessResponse = { - success: true; -} + success: true; +}; // if the backend uses maybeBB02Err export type FailResponse = { - code?: number; - message?: string; - success: false; -} + code?: number; + message?: string; + success: false; +}; diff --git a/frontends/web/src/api/transactions.ts b/frontends/web/src/api/transactions.ts index fd8e40e285..9c91bf659a 100644 --- a/frontends/web/src/api/transactions.ts +++ b/frontends/web/src/api/transactions.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { TUnsubscribe } from '@/utils/transport-common'; +import type { TUnsubscribe } from '@/utils/transport-common'; import { TSubscriptionCallback, subscribeEndpoint } from './subscribe'; export type TNewTxs = { - count: number, - accountName: string, + count: number; + accountName: string; }; export const syncNewTxs = (cb: TSubscriptionCallback): TUnsubscribe => { diff --git a/frontends/web/src/api/version.ts b/frontends/web/src/api/version.ts index 0614698a6b..6cf484041d 100644 --- a/frontends/web/src/api/version.ts +++ b/frontends/web/src/api/version.ts @@ -19,11 +19,11 @@ import { apiGet } from '@/utils/request'; /** * Describes the file that is loaded from 'https://bitbox.swiss/updates/desktop.json'. */ -export type TUpdateFile = { - current: string; - version: string; - description: string; -} +type TUpdateFile = { + current: string; + version: string; + description: string; +}; export const getVersion = (): Promise => { return apiGet('version'); diff --git a/frontends/web/src/assets/exchanges/logos/coinfinity.svg b/frontends/web/src/assets/exchanges/logos/coinfinity.svg new file mode 100644 index 0000000000..c326f37f45 --- /dev/null +++ b/frontends/web/src/assets/exchanges/logos/coinfinity.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontends/web/src/components/actionable-item/actionable-item.tsx b/frontends/web/src/components/actionable-item/actionable-item.tsx index f629c0f5bc..7d143504be 100644 --- a/frontends/web/src/components/actionable-item/actionable-item.tsx +++ b/frontends/web/src/components/actionable-item/actionable-item.tsx @@ -24,7 +24,7 @@ type TProps = { children: ReactNode; icon?: ReactNode; onClick?: () => void; -} +}; export const ActionableItem = ({ className = '', diff --git a/frontends/web/src/components/amount/amount-with-unit.tsx b/frontends/web/src/components/amount/amount-with-unit.tsx index bb18f6c3f6..8e5ef37be9 100644 --- a/frontends/web/src/components/amount/amount-with-unit.tsx +++ b/frontends/web/src/components/amount/amount-with-unit.tsx @@ -29,7 +29,7 @@ type TAmountWithUnitProps = { sign?: string; alwaysShowAmounts?: boolean; convertToFiat?: boolean; -} +}; export const AmountWithUnit = ({ amount, @@ -98,7 +98,7 @@ export const AmountWithUnit = ({ type TAmountUnitProps = { rotateUnit?: () => Promise; unit: ConversionUnit | CoinUnit; -} +}; export const AmountUnit = ({ rotateUnit, unit }: TAmountUnitProps) => { const classRototable = rotateUnit ? (style.rotatable || '') : ''; diff --git a/frontends/web/src/components/anchor/anchor.tsx b/frontends/web/src/components/anchor/anchor.tsx index cbb226eff0..42067e6ee6 100644 --- a/frontends/web/src/components/anchor/anchor.tsx +++ b/frontends/web/src/components/anchor/anchor.tsx @@ -26,7 +26,7 @@ type TProps = { href: string; icon?: ReactNode; title?: string; -} +}; /** * Renders a link to an external URL or file, which will open in the native browser or application. diff --git a/frontends/web/src/components/aopp/aopp.tsx b/frontends/web/src/components/aopp/aopp.tsx index 14d67213ef..087a3666c6 100644 --- a/frontends/web/src/components/aopp/aopp.tsx +++ b/frontends/web/src/components/aopp/aopp.tsx @@ -31,7 +31,7 @@ import styles from './aopp.module.css'; type TProps = { children: ReactNode; -} +}; const Banner = ({ children }: TProps) => (
{children}
diff --git a/frontends/web/src/components/aopp/vasp.tsx b/frontends/web/src/components/aopp/vasp.tsx index 69a588fc94..cbcab8059f 100644 --- a/frontends/web/src/components/aopp/vasp.tsx +++ b/frontends/web/src/components/aopp/vasp.tsx @@ -19,6 +19,7 @@ import AOPPGroupLogo from '@/assets/exchanges/logos/aoppgroup.svg'; import BitcoinSuisseLogo from '@/assets/exchanges/logos/bitcoin_suisse.png'; import BittrLogo from '@/assets/exchanges/logos/bittr.png'; import BityLogo from '@/assets/exchanges/logos/bity.png'; +import CoinfinityLogo from '@/assets/exchanges/logos/coinfinity.svg'; import PocketBitcoinLogo from '@/assets/exchanges/logos/pocketbitcoin.svg'; import RelaiLogo from '@/assets/exchanges/logos/relai.svg'; @@ -27,17 +28,18 @@ type TVASPProps = { hostname: string; prominent?: boolean; withLogoText?: string; -} +}; type TVASPMap = { [hostname: string]: string; -} +}; const VASPLogoMap: TVASPMap = { 'demo.aopp.group': AOPPGroupLogo, 'testing.aopp.group': AOPPGroupLogo, 'bitcoinsuisse.com': BitcoinSuisseLogo, 'bity.com': BityLogo, + 'coinfinity.co': CoinfinityLogo, 'getbittr.com': BittrLogo, 'pocketbitcoin.com': PocketBitcoinLogo, 'relai.app': RelaiLogo, diff --git a/frontends/web/src/components/aopp/verifyaddress.tsx b/frontends/web/src/components/aopp/verifyaddress.tsx index 1151ac4dfe..297afe75df 100644 --- a/frontends/web/src/components/aopp/verifyaddress.tsx +++ b/frontends/web/src/components/aopp/verifyaddress.tsx @@ -21,10 +21,10 @@ import { Button } from '@/components/forms'; import { WaitDialog } from '@/components/wait-dialog/wait-dialog'; type TProps = { - accountCode: accountAPI.AccountCode; - address: string; - addressID: string; -} + accountCode: accountAPI.AccountCode; + address: string; + addressID: string; +}; export const VerifyAddress = ({ accountCode, address, addressID }: TProps) => { const [verifying, setVerifying] = useState(false); diff --git a/frontends/web/src/components/balance/balance.module.css b/frontends/web/src/components/balance/balance.module.css index f86c98f19f..2094f70785 100644 --- a/frontends/web/src/components/balance/balance.module.css +++ b/frontends/web/src/components/balance/balance.module.css @@ -41,7 +41,7 @@ } .balanceTable td { - font-size: var(--size-large-mobile); + font-size: var(--size-header); line-height: 1.5; } diff --git a/frontends/web/src/components/balance/balance.test.tsx b/frontends/web/src/components/balance/balance.test.tsx index 85f714b58e..7ce33749c0 100644 --- a/frontends/web/src/components/balance/balance.test.tsx +++ b/frontends/web/src/components/balance/balance.test.tsx @@ -18,7 +18,7 @@ import '../../../__mocks__/i18n'; import { useContext } from 'react'; import { Mock, afterEach, describe, expect, it, vi } from 'vitest'; import { render } from '@testing-library/react'; -import { IBalance } from '@/api/account'; +import { TBalance } from '@/api/account'; import { Balance } from './balance'; vi.mock('@/utils/request', () => ({ @@ -43,7 +43,7 @@ describe('components/balance/balance', () => { decimal: '.', group: ',' }); - const MOCK_BALANCE: IBalance = { + const MOCK_BALANCE: TBalance = { hasAvailable: true, hasIncoming: true, available: { @@ -117,7 +117,7 @@ describe('components/balance/balance', () => { group: ',' }); - const MOCK_BALANCE: IBalance = { + const MOCK_BALANCE: TBalance = { hasAvailable: true, hasIncoming: true, available: { diff --git a/frontends/web/src/components/balance/balance.tsx b/frontends/web/src/components/balance/balance.tsx index bdae8d0f0a..0e0d566586 100644 --- a/frontends/web/src/components/balance/balance.tsx +++ b/frontends/web/src/components/balance/balance.tsx @@ -16,16 +16,16 @@ */ import { useTranslation } from 'react-i18next'; -import { IBalance } from '@/api/account'; +import { TBalance } from '@/api/account'; import { Amount } from '@/components/amount/amount'; import { BalanceSkeleton } from '@/components/balance/balance-skeleton'; import { AmountWithUnit } from '@/components/amount/amount-with-unit'; import style from './balance.module.css'; type TProps = { - balance?: IBalance; + balance?: TBalance; noRotateFiat?: boolean; -} +}; export const Balance = ({ balance, diff --git a/frontends/web/src/components/banners/backup.tsx b/frontends/web/src/components/banners/backup.tsx index 9211e81a09..4e73b16d1f 100644 --- a/frontends/web/src/components/banners/backup.tsx +++ b/frontends/web/src/components/banners/backup.tsx @@ -31,7 +31,7 @@ import { LocalizationContext } from '@/contexts/localization-context'; type BackupReminderProps = { keystore: TKeystore; accountsBalanceSummary?: TAccountsBalanceSummary; -} +}; export const BackupReminder = ({ keystore, accountsBalanceSummary }: BackupReminderProps) => { const { t } = useTranslation(); diff --git a/frontends/web/src/components/banners/banner.tsx b/frontends/web/src/components/banners/banner.tsx index 570f0e638c..417bdc54c4 100644 --- a/frontends/web/src/components/banners/banner.tsx +++ b/frontends/web/src/components/banners/banner.tsx @@ -24,7 +24,7 @@ import style from './banner.module.css'; type TBannerProps = { msgKey: 'bitbox01' | 'bitbox02' | 'bitbox02nova'; -} +}; export const Banner = ({ msgKey }: TBannerProps) => { const { i18n, t } = useTranslation(); diff --git a/frontends/web/src/components/banners/index.tsx b/frontends/web/src/components/banners/index.tsx index 935a066b8e..1cebe882e4 100644 --- a/frontends/web/src/components/banners/index.tsx +++ b/frontends/web/src/components/banners/index.tsx @@ -26,7 +26,7 @@ import { SDCardWarning } from './sdcard'; type Props = { code?: AccountCode; devices: TDevices; -} +}; export const GlobalBanners = ({ code, diff --git a/frontends/web/src/components/banners/offline-error.tsx b/frontends/web/src/components/banners/offline-error.tsx index 2fd6aa639c..e67a6a6e01 100644 --- a/frontends/web/src/components/banners/offline-error.tsx +++ b/frontends/web/src/components/banners/offline-error.tsx @@ -22,7 +22,7 @@ import style from './offline-errors.module.css'; type Props = { error?: string | null; -} +}; export const OfflineError = ({ error, diff --git a/frontends/web/src/components/banners/sdcard.tsx b/frontends/web/src/components/banners/sdcard.tsx index 5abf4fd067..8af1f15bd6 100644 --- a/frontends/web/src/components/banners/sdcard.tsx +++ b/frontends/web/src/components/banners/sdcard.tsx @@ -25,7 +25,7 @@ import { Message } from '@/components/message/message'; type Props = { code?: AccountCode; devices: TDevices; -} +}; export const SDCardWarning = ({ code, diff --git a/frontends/web/src/components/bluetooth/bluetooth.tsx b/frontends/web/src/components/bluetooth/bluetooth.tsx index 32cc014615..62ee8e1a35 100644 --- a/frontends/web/src/components/bluetooth/bluetooth.tsx +++ b/frontends/web/src/components/bluetooth/bluetooth.tsx @@ -33,7 +33,7 @@ const isConnectedOrConnecting = (peripheral: TPeripheral) => { type Props = { peripheralContainerClassName?: string; -} +}; const BluetoothInner = ({ peripheralContainerClassName }: Props) => { const { t } = useTranslation(); diff --git a/frontends/web/src/components/bluetooth/connection-issues-dialog.tsx b/frontends/web/src/components/bluetooth/connection-issues-dialog.tsx index 958e2b1dee..0033e7c6bc 100644 --- a/frontends/web/src/components/bluetooth/connection-issues-dialog.tsx +++ b/frontends/web/src/components/bluetooth/connection-issues-dialog.tsx @@ -20,9 +20,9 @@ import { A } from '@/components/anchor/anchor'; import styles from './connection-issues-dialog.module.css'; type Props = { - dialogOpen: boolean - onClose: () => void -} + dialogOpen: boolean; + onClose: () => void; +}; export const ConnectionIssuesDialog = ({ dialogOpen, onClose }: Props) => { const { t } = useTranslation(); return ( diff --git a/frontends/web/src/components/bottom-navigation/bottom-navigation.module.css b/frontends/web/src/components/bottom-navigation/bottom-navigation.module.css index f087a3cdb4..f4a89df801 100644 --- a/frontends/web/src/components/bottom-navigation/bottom-navigation.module.css +++ b/frontends/web/src/components/bottom-navigation/bottom-navigation.module.css @@ -1,4 +1,5 @@ .container { + align-items: center; background-color: var(--background-secondary); bottom: 0; border-top: 2px solid var(--bottom-navigation-border-color); diff --git a/frontends/web/src/components/bottom-navigation/bottom-navigation.tsx b/frontends/web/src/components/bottom-navigation/bottom-navigation.tsx index e52ce9ab7f..ab561933dc 100644 --- a/frontends/web/src/components/bottom-navigation/bottom-navigation.tsx +++ b/frontends/web/src/components/bottom-navigation/bottom-navigation.tsx @@ -16,25 +16,25 @@ import { useTranslation } from 'react-i18next'; import { Link, useLocation } from 'react-router-dom'; +import type { TAccount } from '@/api/account'; +import type { TDevices } from '@/api/devices'; import { AccountIconSVG, MarketIconSVG, MoreIconSVG, PortfolioIconSVG } from '@/components/bottom-navigation/menu-icons'; -import type { Account } from '@/api/aopp'; import { useLoad } from '@/hooks/api'; import { getVersion } from '@/api/bitbox02'; import { RedDot } from '@/components/icon'; -import { TDevices } from '@/api/devices'; import styles from './bottom-navigation.module.css'; type Props = { - activeAccounts: Account[]; - devices: TDevices -} + activeAccounts: TAccount[]; + devices: TDevices; +}; export const BottomNavigation = ({ activeAccounts, devices }: Props) => { const { t } = useTranslation(); const { pathname } = useLocation(); const deviceID = Object.keys(devices)[0]; const isBitBox02 = deviceID && devices[deviceID] === 'bitbox02'; - const versionInfo = useLoad(isBitBox02 ? () => getVersion(deviceID) : null, [deviceID]); + const versionInfo = useLoad(isBitBox02 ? () => getVersion(deviceID) : null, [deviceID, isBitBox02]); const canUpgrade = versionInfo ? versionInfo.canUpgrade : false; const onlyHasOneAccount = activeAccounts.length === 1; diff --git a/frontends/web/src/components/confirm/Confirm.tsx b/frontends/web/src/components/confirm/Confirm.tsx index 30d606d310..8e84c57b59 100644 --- a/frontends/web/src/components/confirm/Confirm.tsx +++ b/frontends/web/src/components/confirm/Confirm.tsx @@ -28,11 +28,11 @@ type TCallback = (response: boolean) => void; */ export let confirmation: (message: string, callback: TCallback, customButtonText?: string) => void; -interface State { - active: boolean; - message?: string; - customButtonText?: string; -} +type State = { + active: boolean; + message?: string; + customButtonText?: string; +}; /** * Confirm alert that activates on confirmation module export call, diff --git a/frontends/web/src/components/contentwrapper/contentwrapper.tsx b/frontends/web/src/components/contentwrapper/contentwrapper.tsx index 7ebea1cc11..7f4c2bfaa1 100644 --- a/frontends/web/src/components/contentwrapper/contentwrapper.tsx +++ b/frontends/web/src/components/contentwrapper/contentwrapper.tsx @@ -18,9 +18,9 @@ import { ReactNode } from 'react'; import style from './contentwrapper.module.css'; type TProps = { - className?: string - children: ReactNode - } + className?: string; + children: ReactNode; +}; export const ContentWrapper = (({ className = '', children }: TProps) => { return ( diff --git a/frontends/web/src/components/copy/Copy.tsx b/frontends/web/src/components/copy/Copy.tsx index 949c87b53f..1a629a7c47 100644 --- a/frontends/web/src/components/copy/Copy.tsx +++ b/frontends/web/src/components/copy/Copy.tsx @@ -21,14 +21,14 @@ import { Check, Copy } from '@/components/icon/icon'; import style from './Copy.module.css'; type TProps = { - alignLeft?: boolean; - alignRight?: boolean; - borderLess?: boolean; - className?: string; - disabled?: boolean; - flexibleHeight?: boolean; - value: string; -} + alignLeft?: boolean; + alignRight?: boolean; + borderLess?: boolean; + className?: string; + disabled?: boolean; + flexibleHeight?: boolean; + value: string; +}; export const CopyableInput = ({ alignLeft, alignRight, borderLess, value, className, disabled, flexibleHeight }: TProps) => { const [success, setSuccess] = useState(false); diff --git a/frontends/web/src/components/devices/bitbox02bootloader/bitbox02bootloader.tsx b/frontends/web/src/components/devices/bitbox02bootloader/bitbox02bootloader.tsx index d89a04e23d..2c99b15341 100644 --- a/frontends/web/src/components/devices/bitbox02bootloader/bitbox02bootloader.tsx +++ b/frontends/web/src/components/devices/bitbox02bootloader/bitbox02bootloader.tsx @@ -29,7 +29,7 @@ import style from './bitbox02bootloader.module.css'; type TProps = { deviceID: string; -} +}; export const BitBox02Bootloader = ({ deviceID }: TProps) => { const { t } = useTranslation(); diff --git a/frontends/web/src/components/dialog/dialog-legacy.tsx b/frontends/web/src/components/dialog/dialog-legacy.tsx index a9f6d9ca27..a9e29a2b93 100644 --- a/frontends/web/src/components/dialog/dialog-legacy.tsx +++ b/frontends/web/src/components/dialog/dialog-legacy.tsx @@ -18,23 +18,24 @@ import React, { Component, createRef } from 'react'; import { CloseXDark, CloseXWhite } from '@/components/icon'; import style from './dialog-legacy.module.css'; -interface Props { - title?: string; - small?: boolean; - medium?: boolean; - large?: boolean; - slim?: boolean; - centered?: boolean; - disableEscape?: boolean; - onClose?: (e?: Event) => void; - disabledClose?: boolean; - children: React.ReactNode; -} -interface State { - active: boolean; - currentTab: number; -} +type Props = { + title?: string; + small?: boolean; + medium?: boolean; + large?: boolean; + slim?: boolean; + centered?: boolean; + disableEscape?: boolean; + onClose?: (e?: Event) => void; + disabledClose?: boolean; + children: React.ReactNode; +}; + +type State = { + active: boolean; + currentTab: number; +}; class DialogLegacy extends Component { private overlay = createRef(); @@ -216,9 +217,9 @@ class DialogLegacy extends Component { * ``` */ -interface DialogButtonsProps { - children: React.ReactNode; -} +type DialogButtonsProps = { + children: React.ReactNode; +}; const DialogButtons = ({ children }: DialogButtonsProps) => { return ( diff --git a/frontends/web/src/components/dialog/dialog.module.css b/frontends/web/src/components/dialog/dialog.module.css index 89a7efc18b..db39c49d4b 100644 --- a/frontends/web/src/components/dialog/dialog.module.css +++ b/frontends/web/src/components/dialog/dialog.module.css @@ -203,7 +203,7 @@ @media (max-width: 768px) { .header .title{ - font-size: var(--header-default-font-size); + font-size: var(--size-header); } .modal { diff --git a/frontends/web/src/components/dialog/dialog.tsx b/frontends/web/src/components/dialog/dialog.tsx index 36d238639b..d46f6284b2 100644 --- a/frontends/web/src/components/dialog/dialog.tsx +++ b/frontends/web/src/components/dialog/dialog.tsx @@ -93,7 +93,7 @@ export const Dialog = ({ // ESC closes dialog (fires onClose) useEsc(() => { - if (open) { + if (open && onClose) { deactivate(true); } }); @@ -109,6 +109,8 @@ export const Dialog = ({ if ( mouseDownTarget.current === e.currentTarget && e.target === e.currentTarget + && open + && onClose ) { deactivate(true); } @@ -117,8 +119,10 @@ export const Dialog = ({ // Close button handler const handleCloseClick = useCallback(() => { - deactivate(true); - }, [deactivate]); + if (open && onClose) { + deactivate(true); + } + }, [deactivate, onClose, open]); // Back button handler (mobile) const closeHandler = useCallback(() => { @@ -183,7 +187,7 @@ export const Dialog = ({ type DialogButtonsProps = { children: React.ReactNode; -} +}; /** * ### Container to place buttons in a dialog diff --git a/frontends/web/src/components/dropdown/mobile-fullscreen-selector.tsx b/frontends/web/src/components/dropdown/mobile-fullscreen-selector.tsx index b340587226..86f5506eb5 100644 --- a/frontends/web/src/components/dropdown/mobile-fullscreen-selector.tsx +++ b/frontends/web/src/components/dropdown/mobile-fullscreen-selector.tsx @@ -32,7 +32,7 @@ type Props = { isOpen?: boolean; onOpenChange?: (isOpen: boolean) => void; triggerComponent?: ReactNode | ((props: { onClick: () => void }) => ReactNode); -} +}; export const MobileFullscreenSelector = ({ title, diff --git a/frontends/web/src/components/forms/button.tsx b/frontends/web/src/components/forms/button.tsx index 0b3a18c1e5..cedd072fd2 100644 --- a/frontends/web/src/components/forms/button.tsx +++ b/frontends/web/src/components/forms/button.tsx @@ -23,19 +23,19 @@ type TButtonStyleProp = ({ danger: true } & Omit) | ({ primary: true } & Omit) | ({ secondary: true } & Omit) - | ({ transparent: true } & Omit) + | ({ transparent: true } & Omit); type TButtonStyleBase = { danger?: false; primary?: false; secondary?: false; transparent?: false; -} +}; type TProps = TButtonStyleProp & { disabled?: boolean; children: ReactNode; -} +}; type TButtonLink = LinkProps & TProps; diff --git a/frontends/web/src/components/forms/checkbox.tsx b/frontends/web/src/components/forms/checkbox.tsx index 4293c21e2a..37ea3e5d23 100644 --- a/frontends/web/src/components/forms/checkbox.tsx +++ b/frontends/web/src/components/forms/checkbox.tsx @@ -19,10 +19,10 @@ import { FunctionComponent } from 'react'; import styles from './checkbox.module.css'; type CheckboxProps = JSX.IntrinsicElements['input'] & { - label?: string; - id: string; - checkboxStyle?: 'default' | 'info' | 'warning' | 'success'; -} + label?: string; + id: string; + checkboxStyle?: 'default' | 'info' | 'warning' | 'success'; +}; export const Checkbox: FunctionComponent = ({ disabled = false, diff --git a/frontends/web/src/components/forms/input-with-dropdown.tsx b/frontends/web/src/components/forms/input-with-dropdown.tsx index 02a2105fe3..f2efaff11a 100644 --- a/frontends/web/src/components/forms/input-with-dropdown.tsx +++ b/frontends/web/src/components/forms/input-with-dropdown.tsx @@ -29,7 +29,7 @@ export type TInputWithDropdownProps = TBaseInputProps & { dropdownTitle?: string; isOptionDisabled?: (option: TOption) => boolean; renderOptions?: (option: TOption, isSelectedValue: boolean) => React.ReactNode; -} +}; export const InputWithDropdown = forwardRef>(({ id, diff --git a/frontends/web/src/components/forms/radio.tsx b/frontends/web/src/components/forms/radio.tsx index 87ca89142d..5df32ca0ac 100644 --- a/frontends/web/src/components/forms/radio.tsx +++ b/frontends/web/src/components/forms/radio.tsx @@ -17,11 +17,11 @@ import style from './radio.module.css'; -interface IRadioProps { - label?: string; -} +type IRadioProps = { + label?: string; +}; -type TRadioProps = IRadioProps & JSX.IntrinsicElements['input'] +type TRadioProps = IRadioProps & JSX.IntrinsicElements['input']; export const Radio = ({ disabled = false, diff --git a/frontends/web/src/components/forms/types.ts b/frontends/web/src/components/forms/types.ts index b50b010e51..08229d23b2 100644 --- a/frontends/web/src/components/forms/types.ts +++ b/frontends/web/src/components/forms/types.ts @@ -25,5 +25,5 @@ export type TBaseInputProps = { transparent?: boolean; labelSection?: JSX.Element | undefined; label?: string; -} & Omit, 'onInput'> +} & Omit, 'onInput'>; diff --git a/frontends/web/src/components/groupedaccountselector/groupedaccountselector.tsx b/frontends/web/src/components/groupedaccountselector/groupedaccountselector.tsx index 57bfad452f..10c6ad3a9f 100644 --- a/frontends/web/src/components/groupedaccountselector/groupedaccountselector.tsx +++ b/frontends/web/src/components/groupedaccountselector/groupedaccountselector.tsx @@ -17,7 +17,7 @@ import { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import Select, { components, SingleValueProps, OptionProps, SingleValue, DropdownIndicatorProps, GroupProps, GroupHeadingProps as ReactSelectGroupHeadingProps } from 'react-select'; -import { AccountCode, IAccount } from '@/api/account'; +import { AccountCode, TAccount } from '@/api/account'; import { Button } from '@/components/forms'; import { Logo } from '@/components/icon/logo'; import { AppContext } from '@/contexts/AppContext'; @@ -32,16 +32,16 @@ export type TGroupedOption = { label: string; connected: boolean; options: TOption[]; -} +}; export type TOption = { label: string; value: AccountCode; disabled: boolean; - coinCode?: IAccount['coinCode']; + coinCode?: TAccount['coinCode']; balance?: string; insured?: boolean; -} +}; type TAccountSelector = { title: string; @@ -49,8 +49,8 @@ type TAccountSelector = { selected?: string; onChange: (value: string) => void; onProceed: () => void; - accounts: IAccount[] -} + accounts: TAccount[]; +}; const SelectSingleValue = (props: SingleValueProps) => { const { hideAmounts } = useContext(AppContext); @@ -100,8 +100,8 @@ const Group = (props: GroupProps) => ( ); type GroupHeadingProps = { - customData: TGroupedOption -} & ReactSelectGroupHeadingProps + customData: TGroupedOption; +} & ReactSelectGroupHeadingProps; const GroupHeading = ( { customData, ...props }: GroupHeadingProps diff --git a/frontends/web/src/components/guide/entry.tsx b/frontends/web/src/components/guide/entry.tsx index c9859a85b7..b356cf563e 100644 --- a/frontends/web/src/components/guide/entry.tsx +++ b/frontends/web/src/components/guide/entry.tsx @@ -20,19 +20,19 @@ import { A } from '@/components/anchor/anchor'; import style from './guide.module.css'; export type TEntryProp = { - title: string; + title: string; + text: string; + link?: { + url?: string; text: string; - link?: { - url?: string; - text: string; - }; -} + }; +}; type TEntryProps = { - entry: TEntryProp; - shown?: boolean; - children?: ReactNode; -} + entry: TEntryProp; + shown?: boolean; + children?: ReactNode; +}; type TProps = TEntryProps; diff --git a/frontends/web/src/components/guide/guide.module.css b/frontends/web/src/components/guide/guide.module.css index 5010aa97b1..67cdacd66f 100644 --- a/frontends/web/src/components/guide/guide.module.css +++ b/frontends/web/src/components/guide/guide.module.css @@ -56,7 +56,7 @@ } .header h2 { - font-size: var(--header-default-font-size); + font-size: var(--size-header); font-weight: 400; margin: 0; } diff --git a/frontends/web/src/components/guide/guide.tsx b/frontends/web/src/components/guide/guide.tsx index 8b0874c836..daf5c7dcdb 100644 --- a/frontends/web/src/components/guide/guide.tsx +++ b/frontends/web/src/components/guide/guide.tsx @@ -25,9 +25,9 @@ import style from './guide.module.css'; export type TProps = { - children?: ReactNode; - title?: string -} + children?: ReactNode; + title?: string; +}; const Guide = ({ children, title = t('guide.title') }: TProps) => { const { guideShown, toggleGuide, setGuideExists } = useContext(AppContext); diff --git a/frontends/web/src/components/headerssync/headerssync.tsx b/frontends/web/src/components/headerssync/headerssync.tsx index 4191ad45a1..51d395521f 100644 --- a/frontends/web/src/components/headerssync/headerssync.tsx +++ b/frontends/web/src/components/headerssync/headerssync.tsx @@ -25,8 +25,8 @@ import { AsciiSpinner } from '@/components/spinner/ascii'; import style from './headerssync.module.css'; export type TProps = { - coinCode: CoinCode; -} + coinCode: CoinCode; +}; export const HeadersSync = ({ coinCode }: TProps) => { const { i18n, t } = useTranslation(); diff --git a/frontends/web/src/components/icon/icon.tsx b/frontends/web/src/components/icon/icon.tsx index d200a14f89..a6406ded9c 100644 --- a/frontends/web/src/components/icon/icon.tsx +++ b/frontends/web/src/components/icon/icon.tsx @@ -156,9 +156,9 @@ export const CaretDown = ({ className, ...props }: SVGProps) => ( ); -interface ExpandIconProps { - expand: boolean; -} +type ExpandIconProps = { + expand: boolean; +}; export const ExpandIcon = ({ expand = true, diff --git a/frontends/web/src/components/icon/logo.tsx b/frontends/web/src/components/icon/logo.tsx index 27aaa7e8ef..aeb73eb6d2 100644 --- a/frontends/web/src/components/icon/logo.tsx +++ b/frontends/web/src/components/icon/logo.tsx @@ -76,7 +76,7 @@ export const SwissMadeOpenSourceDark = ({ className, ...props }: ImgProps) => void } +type TProps = { onClick: () => void }; export const InfoButton = ({ onClick, ...props }: TProps) => { return (