From e105a0bcd3dc57a255b344447a3d2fcc1db60c9b Mon Sep 17 00:00:00 2001 From: Martin Vere Cihlar Date: Mon, 16 Dec 2024 16:03:10 +0100 Subject: [PATCH 001/181] feat(e2e): Converts to PW test suite firmware.test.ts (#15983) Also move other firmware tests to this suite folder and activates this group of tests in CI --- .github/workflows/test-suite-desktop-e2e.yml | 4 +- .github/workflows/test-suite-web-e2e-pw.yml | 4 +- .../custom-firmware.test.ts | 2 +- .../tests/firmware/update-firmware.test.ts | 39 ++++++++++++ .../e2e/tests/firmware/firmware.test.ts | 61 ------------------- 5 files changed, 44 insertions(+), 66 deletions(-) rename packages/suite-desktop-core/e2e/tests/{settings => firmware}/custom-firmware.test.ts (95%) create mode 100644 packages/suite-desktop-core/e2e/tests/firmware/update-firmware.test.ts delete mode 100644 packages/suite-web/e2e/tests/firmware/firmware.test.ts diff --git a/.github/workflows/test-suite-desktop-e2e.yml b/.github/workflows/test-suite-desktop-e2e.yml index 49d9bc376d3c..2a91e22ee3c5 100644 --- a/.github/workflows/test-suite-desktop-e2e.yml +++ b/.github/workflows/test-suite-desktop-e2e.yml @@ -29,8 +29,8 @@ jobs: include: # - TEST_GROUP: "@group=suite" # CONTAINERS: "trezor-user-env-unix" - # - TEST_GROUP: "@group=device-management" - # CONTAINERS: "trezor-user-env-unix" + - TEST_GROUP: "@group=device-management" + CONTAINERS: "trezor-user-env-unix" - TEST_GROUP: "@group=settings" CONTAINERS: "trezor-user-env-unix electrum-regtest" # - TEST_GROUP: "@group=metadata" diff --git a/.github/workflows/test-suite-web-e2e-pw.yml b/.github/workflows/test-suite-web-e2e-pw.yml index 5c14ba1f5e9b..ec0f9f82f519 100644 --- a/.github/workflows/test-suite-web-e2e-pw.yml +++ b/.github/workflows/test-suite-web-e2e-pw.yml @@ -95,8 +95,8 @@ jobs: include: # - TEST_GROUP: "@group=suite" # CONTAINERS: "trezor-user-env-unix" - # - TEST_GROUP: "@group=device-management" - # CONTAINERS: "trezor-user-env-unix" + - TEST_GROUP: "@group=device-management" + CONTAINERS: "trezor-user-env-unix" - TEST_GROUP: "@group=settings" CONTAINERS: "trezor-user-env-unix" # - TEST_GROUP: "@group=metadata" diff --git a/packages/suite-desktop-core/e2e/tests/settings/custom-firmware.test.ts b/packages/suite-desktop-core/e2e/tests/firmware/custom-firmware.test.ts similarity index 95% rename from packages/suite-desktop-core/e2e/tests/settings/custom-firmware.test.ts rename to packages/suite-desktop-core/e2e/tests/firmware/custom-firmware.test.ts index ec13a1aa7811..2310b46caf87 100644 --- a/packages/suite-desktop-core/e2e/tests/settings/custom-firmware.test.ts +++ b/packages/suite-desktop-core/e2e/tests/firmware/custom-firmware.test.ts @@ -4,7 +4,7 @@ import { test, expect } from '../../support/fixtures'; const firmwarePath = path.join(__dirname, '../../fixtures/trezor-2.5.1.bin'); -test.describe('Custom firmware', { tag: ['@group=settings'] }, () => { +test.describe('Custom firmware', { tag: ['@group=device-management'] }, () => { test.use({ emulatorStartConf: { wipe: true } }); test.beforeEach(async ({ onboardingPage, settingsPage }) => { await onboardingPage.completeOnboarding(); diff --git a/packages/suite-desktop-core/e2e/tests/firmware/update-firmware.test.ts b/packages/suite-desktop-core/e2e/tests/firmware/update-firmware.test.ts new file mode 100644 index 000000000000..b7bcb9e0cbb4 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/firmware/update-firmware.test.ts @@ -0,0 +1,39 @@ +import { test, expect } from '../../support/fixtures'; + +// Skip due to minimal value of this test at current state +test.describe.skip('Firmware update', { tag: ['@group=device-management'] }, () => { + test.use({ emulatorStartConf: { wipe: true, model: 'T2T1', version: '2.5.2' } }); + test.beforeEach(async ({ onboardingPage, dashboardPage }) => { + await onboardingPage.completeOnboarding(); + await dashboardPage.discoveryShouldFinish(); + }); + + test('User triggers firmware update from a notification banner', async ({ page }) => { + await page.getByTestId('@notification/update-notification-banner').click(); + await page.getByTestId('@firmware/install-button').click(); + + await expect(page.getByTestId('@firmware-modal')).toBeVisible(); + // await expect(page.getByTestId('@firmware-modal')).toHaveScreenshot('check-seed.png'); + await page.getByTestId('@firmware/confirm-seed-checkbox').click(); + await page.getByTestId('@firmware/confirm-seed-button').click(); + + // we can't really test anything from this point since this https://github.com/trezor/trezor-suite/pull/12472 was merged + // in combination with not doing git lfs checkout in feature branches. Firmware will not be uploaded and an error is presented to user + // but only in feature branches, develop or production branches should display correct behavior. + + // one point to get over this would be to stub correct (bigger) firmware binary response here, but I don't know how to stub fetch that + // happens inside a nested iframe (connect-iframe). + // Update: Trezor env does not support bootloader atm https://github.com/trezor/trezor-user-env/issues/219 + + // reconnect in bootloader screen (disconnect) + // await expect(page.getByTestId('@firmware/disconnect-message')).toHaveText( + // 'Disconnect your Trezor', + // ); + // await trezorUserEnvLink.stopBridge(); + + // reconnect in bootloader screen (connect in bootloader) + // await expect(page.getByTestId('@firmware/connect-in-bootloader-message')).toHaveText( + // 'Swipe your finger across the touchscreen while connecting the USB cable.', + // ); + }); +}); diff --git a/packages/suite-web/e2e/tests/firmware/firmware.test.ts b/packages/suite-web/e2e/tests/firmware/firmware.test.ts deleted file mode 100644 index 59a686c33c2a..000000000000 --- a/packages/suite-web/e2e/tests/firmware/firmware.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -// @group_device-management -// @retry=2 - -describe('Firmware', () => { - beforeEach(() => { - // use portrait mode monitor to prevent scrolling in settings - cy.viewport('macbook-13').resetDb(); - }); - - it(`Firmware 2.5.2 outdated notification banner should open firmware update modal`, () => { - cy.task('startEmu', { wipe: true, model: 'T2T1', version: '2.5.2' }); - cy.task('setupEmu'); - cy.task('startBridge'); - cy.prefixedVisit('/'); - cy.passThroughInitialRun(); - cy.getTestElement('@notification/update-notification-banner').click(); - - // initial screen - cy.getTestElement('@firmware/install-button').click(); - - // check seed screen - cy.getTestElement('@modal/close-button').should('be.visible'); // modal is cancellable at this moment - cy.getTestElement('@firmware-modal').matchImageSnapshot('check-seed'); - cy.getTestElement('@firmware/confirm-seed-checkbox').click(); - cy.getTestElement('@firmware/confirm-seed-button').click(); - - // we can't really test anything from this point since this https://github.com/trezor/trezor-suite/pull/12472 was merged - // in combination with not doing git lfs checkout in feature branches. Firmware will not be uploaded and an error is presented to user - // but only in feature branches, develop or production branches should display correct behavior. - - // one point to get over this would be to stub correct (bigger) firmware binary response here, but I don't know how to stub fetch that - // happens inside a nested iframe (connect-iframe). - - // cy.prefixedVisit('/', { - // onBeforeLoad: (win: Window) => { - // cy.stub(win, 'fetch').callsFake( - // (uri: string, options: Parameters[1]) => { - // console.log('uri', uri); - // if (uri.includes('static/connect/data/firmware/t2t1/')) { - // return Promise.resolve( - // new Response(new ArrayBuffer(0), { status: 200 }), - // ); - // } - - // return fetch(uri, options); - // }, - // ); - // }, - // }); - - // // reconnect in bootloader screen (disconnect) - // cy.getTestElement('@firmware/disconnect-message', { timeout: 30000 }); - // cy.task('stopEmu'); - - // // reconnect in bootloader screen (connect in bootloader) - // cy.getTestElement('@firmware/connect-in-bootloader-message', { timeout: 20000 }); - // cy.log( - // 'And this is the end my friends. Emulator does not support bootloader, so we can not proceed with actual fw install', - // ); - }); -}); From 133970c9290c4e2b06d85b5507ec4e1fc036792b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Vytick=20Vytrhl=C3=ADk?= Date: Mon, 16 Dec 2024 13:16:54 +0100 Subject: [PATCH 002/181] fix(suite-native): do not show view only alert during passphrase flow --- .../src/screens/HomeScreen/useShowViewOnlyAlert.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/suite-native/module-home/src/screens/HomeScreen/useShowViewOnlyAlert.tsx b/suite-native/module-home/src/screens/HomeScreen/useShowViewOnlyAlert.tsx index 4d821ceb8a27..e954015e982e 100644 --- a/suite-native/module-home/src/screens/HomeScreen/useShowViewOnlyAlert.tsx +++ b/suite-native/module-home/src/screens/HomeScreen/useShowViewOnlyAlert.tsx @@ -23,6 +23,7 @@ import { } from '@suite-native/settings'; import { useToast } from '@suite-native/toasts'; import { TimerId } from '@trezor/type-utils'; +import { selectIsCreatingNewPassphraseWallet } from '@suite-native/device-authorization'; import viewOnlyLottie from '../../assets/view-only-lottie.json'; @@ -40,7 +41,7 @@ export const useShowViewOnlyAlert = () => { const viewOnlyCancelationTimestamp = useSelector(selectViewOnlyCancelationTimestamp); const isDeviceRemembered = useSelector(selectIsDeviceRemembered); const hasDiscovery = useSelector(selectHasDeviceDiscovery); - + const isCreatingNewPassphraseWallet = useSelector(selectIsCreatingNewPassphraseWallet); const [isAvailableBiometrics, setIsAvailableBiometrics] = useState(false); useEffect(() => { @@ -109,6 +110,7 @@ export const useShowViewOnlyAlert = () => { !isPortfolioTrackerDevice && !hasDiscovery && !viewOnlyCancelationTimestamp && + !isCreatingNewPassphraseWallet && (isBiometricsInitialSetupFinished || !isAvailableBiometrics); //show after a delay @@ -133,5 +135,6 @@ export const useShowViewOnlyAlert = () => { isPortfolioTrackerDevice, showViewOnlyAlert, viewOnlyCancelationTimestamp, + isCreatingNewPassphraseWallet, ]); }; From 9048aadbf29b45d6dacc9c09f28322287db9c0db Mon Sep 17 00:00:00 2001 From: Matej Kriz Date: Fri, 13 Dec 2024 10:57:58 +0100 Subject: [PATCH 003/181] chore(ci): retrieve APK url in github workflow --- .../release-suite-native-production.yml | 10 ++-- suite-native/app/eas-post-success.sh | 56 +------------------ 2 files changed, 7 insertions(+), 59 deletions(-) diff --git a/.github/workflows/release-suite-native-production.yml b/.github/workflows/release-suite-native-production.yml index 91f682b2f4c0..7fd205c5258b 100644 --- a/.github/workflows/release-suite-native-production.yml +++ b/.github/workflows/release-suite-native-production.yml @@ -95,9 +95,9 @@ jobs: - name: Install libs run: yarn workspaces focus @suite-native/app - name: Build on EAS Android - run: eas build - --platform android - --profile productionAPK - --non-interactive - --message ${{ github.sha }} + run: | + BUILD_ID=$(eas build --platform android --profile productionAPK --non-interactive --message ${{ github.sha }} --wait --json | jq -r '.[0].id') + echo "BUILD_ID: $BUILD_ID" + BUILD_URL=$(eas build:view "$BUILD_ID" --json | jq -r '.artifacts.buildUrl') + echo "BUILD_URL: $BUILD_URL" working-directory: suite-native/app diff --git a/suite-native/app/eas-post-success.sh b/suite-native/app/eas-post-success.sh index 15782eb51e24..c558ae61e9f4 100755 --- a/suite-native/app/eas-post-success.sh +++ b/suite-native/app/eas-post-success.sh @@ -16,60 +16,8 @@ distribute_develop_apk() { --release-notes "$release_notes" } -create_release_draft() { - suite_native_version=$(grep -E '^\s*"suiteNativeVersion":' package.json | awk -F ': ' '{print $2}' | tr -d ',"') - - # Create a GitHub release draft - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GITHUB_TREZOR_SUITE_TOKEN" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - -d '{ - "tag_name": "v'"$suite_native_version"'@mobile", - "name": "Suite Mobile v'"$suite_native_version"'", - "body": "'"$EAS_BUILD_GIT_COMMIT_HASH"'", - "draft": true, - "prerelease": false, - "generate_release_notes": false, - "make_latest": "false" - }' \ - https://api.github.com/repos/trezor/trezor-suite/releases > /tmp/release_response.json -} - -upload_production_apk() { - # Extract upload URL from the GitHub release draft response - upload_url=$(grep -o '"upload_url": *"[^"]*"' /tmp/release_response.json | grep -o '"[^"]*"$' | tr -d '"') - upload_url="${upload_url%\{*}" # Remove the curly braces and everything after them - upload_url="${upload_url}?name=Trezor-Suite-Lite-${suite_native_version}.apk" - - # Upload production APK to newly created GitHub release - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer $GITHUB_TREZOR_SUITE_TOKEN" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - -H "Content-Type: application/octet-stream" \ - "${upload_url}" \ - --data-binary "@$EAS_BUILD_WORKINGDIR/suite-native/app/android/app/build/outputs/apk/release/app-release.apk" > /tmp/asset_response.json - - # Wait until the response file is created and contains data - until [ -s /tmp/asset_response.json ]; do - sleep 1 - done - - cat /tmp/asset_response.json - - echo "APK uploaded" -} - -if [[ "$EAS_BUILD_PLATFORM" == "android" ]]; then - if [[ "$EAS_BUILD_PROFILE" == "develop" ]]; then - distribute_develop_apk - elif [[ "$EAS_BUILD_PROFILE" == "productionAPK" ]]; then - create_release_draft - upload_production_apk - fi +if [[ "$EAS_BUILD_PLATFORM" == "android" && "$EAS_BUILD_PROFILE" == "develop" ]]; then + distribute_develop_apk elif [[ "$EAS_BUILD_PLATFORM" == "ios" ]]; then exit 0 fi From 3b4f58223ef7315097fff37a51be360bb719563c Mon Sep 17 00:00:00 2001 From: Tomas Martykan Date: Mon, 16 Dec 2024 12:52:52 +0100 Subject: [PATCH 004/181] chore(suite): separate popup logic from `@suite-common/connect-init` --- .../suite-desktop-connect-popup/package.json | 27 +++++ .../suite-desktop-connect-popup/redux.d.ts | 7 ++ .../src/connectPopupThunks.ts | 99 +++++++++++++++++++ .../suite-desktop-connect-popup/src/index.ts | 1 + .../suite-desktop-connect-popup/tsconfig.json | 23 +++++ packages/suite/package.json | 1 + .../suite/src/actions/suite/initAction.ts | 3 +- .../middlewares/wallet/discoveryMiddleware.ts | 2 +- packages/suite/tsconfig.json | 3 + suite-common/connect-init/package.json | 1 - .../connect-init/src/connectInitThunks.ts | 98 +----------------- suite-common/connect-init/tsconfig.json | 3 - yarn.lock | 21 +++- 13 files changed, 186 insertions(+), 103 deletions(-) create mode 100644 packages/suite-desktop-connect-popup/package.json create mode 100644 packages/suite-desktop-connect-popup/redux.d.ts create mode 100644 packages/suite-desktop-connect-popup/src/connectPopupThunks.ts create mode 100644 packages/suite-desktop-connect-popup/src/index.ts create mode 100644 packages/suite-desktop-connect-popup/tsconfig.json diff --git a/packages/suite-desktop-connect-popup/package.json b/packages/suite-desktop-connect-popup/package.json new file mode 100644 index 000000000000..07c2e5002119 --- /dev/null +++ b/packages/suite-desktop-connect-popup/package.json @@ -0,0 +1,27 @@ +{ + "name": "@trezor/suite-desktop-connect-popup", + "version": "1.0.0", + "private": true, + "license": "See LICENSE.md in repo root", + "sideEffects": false, + "main": "src/index", + "scripts": { + "type-check": "yarn g:tsc --build tsconfig.json" + }, + "dependencies": { + "@reduxjs/toolkit": "1.9.5", + "@suite-common/redux-utils": "workspace:*", + "@suite-common/suite-types": "workspace:*", + "@suite-common/test-utils": "workspace:*", + "@suite-common/wallet-core": "workspace:*", + "@trezor/connect": "workspace:*", + "@trezor/env-utils": "workspace:^", + "@trezor/suite-desktop-api": "workspace:*", + "@trezor/urls": "workspace:*", + "@trezor/utils": "workspace:*" + }, + "devDependencies": { + "redux-mock-store": "^1.5.4", + "redux-thunk": "^2.4.2" + } +} diff --git a/packages/suite-desktop-connect-popup/redux.d.ts b/packages/suite-desktop-connect-popup/redux.d.ts new file mode 100644 index 000000000000..df9a0c3f969a --- /dev/null +++ b/packages/suite-desktop-connect-popup/redux.d.ts @@ -0,0 +1,7 @@ +import { AsyncThunkAction } from '@reduxjs/toolkit'; + +declare module 'redux' { + export interface Dispatch { + >(thunk: TThunk): ReturnType; + } +} diff --git a/packages/suite-desktop-connect-popup/src/connectPopupThunks.ts b/packages/suite-desktop-connect-popup/src/connectPopupThunks.ts new file mode 100644 index 000000000000..fb93d6a1f189 --- /dev/null +++ b/packages/suite-desktop-connect-popup/src/connectPopupThunks.ts @@ -0,0 +1,99 @@ +import { createThunk } from '@suite-common/redux-utils'; +import TrezorConnect, { ERRORS } from '@trezor/connect'; +import { createDeferred } from '@trezor/utils'; +import { selectSelectedDevice } from '@suite-common/wallet-core'; +import { desktopApi } from '@trezor/suite-desktop-api'; +import { serializeError } from '@trezor/connect/src/constants/errors'; + +const CONNECT_POPUP_MODULE = '@common/connect-popup'; + +export const connectPopupCallThunk = createThunk( + `${CONNECT_POPUP_MODULE}/callThunk`, + async ( + { + id, + method, + payload, + processName, + origin, + }: { + id: number; + method: string; + payload: any; + processName?: string; + origin?: string; + }, + { dispatch, getState, extra }, + ) => { + try { + const device = selectSelectedDevice(getState()); + + if (!device) { + console.error('Device not found'); + + // TODO: wait for device selection and continue + throw ERRORS.TypedError('Device_NotFound'); + } + + // @ts-expect-error: method is dynamic + const methodInfo = await TrezorConnect[method]({ + ...payload, + __info: true, + }); + if (!methodInfo.success) { + throw methodInfo; + } + + const confirmation = createDeferred(); + dispatch(extra.actions.lockDevice(true)); + dispatch( + extra.actions.openModal({ + type: 'connect-popup', + onCancel: () => confirmation.reject(ERRORS.TypedError('Method_Cancel')), + onConfirm: () => confirmation.resolve(), + method: methodInfo.payload.info, + processName, + origin, + }), + ); + await confirmation.promise; + dispatch(extra.actions.lockDevice(false)); + + // @ts-expect-error: method is dynamic + const response = await TrezorConnect[method]({ + device: { + path: device.path, + instance: device.instance, + state: device.state, + }, + ...payload, + }); + + dispatch(extra.actions.onModalCancel()); + + desktopApi.connectPopupResponse({ + ...response, + id, + }); + } catch (error) { + console.error('connectPopupCallThunk', error); + desktopApi.connectPopupResponse({ + success: false, + payload: serializeError(error), + id, + }); + } + }, +); + +export const connectPopupInitThunk = createThunk( + `${CONNECT_POPUP_MODULE}/initPopupThunk`, + async (_, { dispatch }) => { + if (desktopApi.available && (await desktopApi.connectPopupEnabled())) { + desktopApi.on('connect-popup/call', params => { + dispatch(connectPopupCallThunk(params)); + }); + desktopApi.connectPopupReady(); + } + }, +); diff --git a/packages/suite-desktop-connect-popup/src/index.ts b/packages/suite-desktop-connect-popup/src/index.ts new file mode 100644 index 000000000000..e56779802e7a --- /dev/null +++ b/packages/suite-desktop-connect-popup/src/index.ts @@ -0,0 +1 @@ +export * from './connectPopupThunks'; diff --git a/packages/suite-desktop-connect-popup/tsconfig.json b/packages/suite-desktop-connect-popup/tsconfig.json new file mode 100644 index 000000000000..9de034a7f700 --- /dev/null +++ b/packages/suite-desktop-connect-popup/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "libDev" }, + "references": [ + { + "path": "../../suite-common/redux-utils" + }, + { + "path": "../../suite-common/suite-types" + }, + { + "path": "../../suite-common/test-utils" + }, + { + "path": "../../suite-common/wallet-core" + }, + { "path": "../connect" }, + { "path": "../env-utils" }, + { "path": "../suite-desktop-api" }, + { "path": "../urls" }, + { "path": "../utils" } + ] +} diff --git a/packages/suite/package.json b/packages/suite/package.json index 4d98b750653a..811a8f404dc9 100644 --- a/packages/suite/package.json +++ b/packages/suite/package.json @@ -75,6 +75,7 @@ "@trezor/suite-analytics": "workspace:*", "@trezor/suite-data": "workspace:*", "@trezor/suite-desktop-api": "workspace:*", + "@trezor/suite-desktop-connect-popup": "workspace:*", "@trezor/suite-storage": "workspace:*", "@trezor/theme": "workspace:*", "@trezor/type-utils": "workspace:*", diff --git a/packages/suite/src/actions/suite/initAction.ts b/packages/suite/src/actions/suite/initAction.ts index 520723204eae..bf8bf0a9b916 100644 --- a/packages/suite/src/actions/suite/initAction.ts +++ b/packages/suite/src/actions/suite/initAction.ts @@ -9,6 +9,7 @@ import { } from '@suite-common/wallet-core'; import { periodicCheckTokenDefinitionsThunk } from '@suite-common/token-definitions'; import { desktopApi } from '@trezor/suite-desktop-api'; +import * as trezorConnectPopupActions from '@trezor/suite-desktop-connect-popup'; import { isDesktop } from '@trezor/env-utils'; import * as routerActions from 'src/actions/suite/routerActions'; @@ -115,7 +116,7 @@ export const init = () => async (dispatch: Dispatch, getState: GetState) => { // 14. init connect popup handler if (isDesktop()) { - dispatch(trezorConnectActions.connectPopupInitThunk()); + dispatch(trezorConnectPopupActions.connectPopupInitThunk()); } // 15. backend connected, suite is ready to use diff --git a/packages/suite/src/middlewares/wallet/discoveryMiddleware.ts b/packages/suite/src/middlewares/wallet/discoveryMiddleware.ts index 34ceb080ce8f..d537d18ee5f5 100644 --- a/packages/suite/src/middlewares/wallet/discoveryMiddleware.ts +++ b/packages/suite/src/middlewares/wallet/discoveryMiddleware.ts @@ -1,3 +1,4 @@ +import { connectPopupCallThunk } from '@trezor/suite-desktop-connect-popup'; import { authorizeDeviceThunk, deviceActions, @@ -15,7 +16,6 @@ import { UI } from '@trezor/connect'; import { isDeviceAcquired } from '@suite-common/suite-utils'; import { DiscoveryStatus } from '@suite-common/wallet-constants'; import { createMiddlewareWithExtraDeps } from '@suite-common/redux-utils'; -import { connectPopupCallThunk } from '@suite-common/connect-init'; import { SUITE, ROUTER, MODAL } from 'src/actions/suite/constants'; import * as walletSettingsActions from 'src/actions/settings/walletSettingsActions'; diff --git a/packages/suite/tsconfig.json b/packages/suite/tsconfig.json index 9d9fe32a3681..22418443b0a2 100644 --- a/packages/suite/tsconfig.json +++ b/packages/suite/tsconfig.json @@ -104,6 +104,9 @@ { "path": "../suite-analytics" }, { "path": "../suite-data" }, { "path": "../suite-desktop-api" }, + { + "path": "../suite-desktop-connect-popup" + }, { "path": "../suite-storage" }, { "path": "../theme" }, { "path": "../type-utils" }, diff --git a/suite-common/connect-init/package.json b/suite-common/connect-init/package.json index 67992798a112..ec1f66da017d 100644 --- a/suite-common/connect-init/package.json +++ b/suite-common/connect-init/package.json @@ -18,7 +18,6 @@ "@suite-common/wallet-core": "workspace:*", "@trezor/connect": "workspace:*", "@trezor/env-utils": "workspace:^", - "@trezor/suite-desktop-api": "workspace:*", "@trezor/urls": "workspace:*", "@trezor/utils": "workspace:*" }, diff --git a/suite-common/connect-init/src/connectInitThunks.ts b/suite-common/connect-init/src/connectInitThunks.ts index 5e32ca29f655..ccafbd2551e2 100644 --- a/suite-common/connect-init/src/connectInitThunks.ts +++ b/suite-common/connect-init/src/connectInitThunks.ts @@ -3,16 +3,13 @@ import TrezorConnect, { BLOCKCHAIN_EVENT, DEVICE, DEVICE_EVENT, - ERRORS, TRANSPORT_EVENT, UI_EVENT, } from '@trezor/connect'; import { DATA_URL } from '@trezor/urls'; -import { createDeferred, getSynchronize } from '@trezor/utils'; -import { deviceConnectThunks, selectSelectedDevice } from '@suite-common/wallet-core'; +import { getSynchronize } from '@trezor/utils'; +import { deviceConnectThunks } from '@suite-common/wallet-core'; import { isDesktop, isNative } from '@trezor/env-utils'; -import { desktopApi } from '@trezor/suite-desktop-api'; -import { serializeError } from '@trezor/connect/src/constants/errors'; import { cardanoConnectPatch } from './cardanoConnectPatch'; @@ -167,94 +164,3 @@ export const connectInitThunk = createThunk( } }, ); - -export const connectPopupCallThunk = createThunk( - `${CONNECT_INIT_MODULE}/callThunk`, - async ( - { - id, - method, - payload, - processName, - origin, - }: { - id: number; - method: string; - payload: any; - processName?: string; - origin?: string; - }, - { dispatch, getState, extra }, - ) => { - try { - const device = selectSelectedDevice(getState()); - - if (!device) { - console.error('Device not found'); - - // TODO: wait for device selection and continue - throw ERRORS.TypedError('Device_NotFound'); - } - - // @ts-expect-error: method is dynamic - const methodInfo = await TrezorConnect[method]({ - ...payload, - __info: true, - }); - if (!methodInfo.success) { - throw methodInfo; - } - - const confirmation = createDeferred(); - dispatch(extra.actions.lockDevice(true)); - dispatch( - extra.actions.openModal({ - type: 'connect-popup', - onCancel: () => confirmation.reject(ERRORS.TypedError('Method_Cancel')), - onConfirm: () => confirmation.resolve(), - method: methodInfo.payload.info, - processName, - origin, - }), - ); - await confirmation.promise; - dispatch(extra.actions.lockDevice(false)); - - // @ts-expect-error: method is dynamic - const response = await TrezorConnect[method]({ - device: { - path: device.path, - instance: device.instance, - state: device.state, - }, - ...payload, - }); - - dispatch(extra.actions.onModalCancel()); - - desktopApi.connectPopupResponse({ - ...response, - id, - }); - } catch (error) { - console.error('connectPopupCallThunk', error); - desktopApi.connectPopupResponse({ - success: false, - payload: serializeError(error), - id, - }); - } - }, -); - -export const connectPopupInitThunk = createThunk( - `${CONNECT_INIT_MODULE}/initPopupThunk`, - async (_, { dispatch }) => { - if (desktopApi.available && (await desktopApi.connectPopupEnabled())) { - desktopApi.on('connect-popup/call', params => { - dispatch(connectPopupCallThunk(params)); - }); - desktopApi.connectPopupReady(); - } - }, -); diff --git a/suite-common/connect-init/tsconfig.json b/suite-common/connect-init/tsconfig.json index a64595e6886f..cb4b183f706c 100644 --- a/suite-common/connect-init/tsconfig.json +++ b/suite-common/connect-init/tsconfig.json @@ -8,9 +8,6 @@ { "path": "../wallet-core" }, { "path": "../../packages/connect" }, { "path": "../../packages/env-utils" }, - { - "path": "../../packages/suite-desktop-api" - }, { "path": "../../packages/urls" }, { "path": "../../packages/utils" } ] diff --git a/yarn.lock b/yarn.lock index aa7f16f56fd3..0e144f83ea9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9431,7 +9431,6 @@ __metadata: "@suite-common/wallet-core": "workspace:*" "@trezor/connect": "workspace:*" "@trezor/env-utils": "workspace:^" - "@trezor/suite-desktop-api": "workspace:*" "@trezor/urls": "workspace:*" "@trezor/utils": "workspace:*" redux-mock-store: "npm:^1.5.4" @@ -12374,6 +12373,25 @@ __metadata: languageName: unknown linkType: soft +"@trezor/suite-desktop-connect-popup@workspace:*, @trezor/suite-desktop-connect-popup@workspace:packages/suite-desktop-connect-popup": + version: 0.0.0-use.local + resolution: "@trezor/suite-desktop-connect-popup@workspace:packages/suite-desktop-connect-popup" + dependencies: + "@reduxjs/toolkit": "npm:1.9.5" + "@suite-common/redux-utils": "workspace:*" + "@suite-common/suite-types": "workspace:*" + "@suite-common/test-utils": "workspace:*" + "@suite-common/wallet-core": "workspace:*" + "@trezor/connect": "workspace:*" + "@trezor/env-utils": "workspace:^" + "@trezor/suite-desktop-api": "workspace:*" + "@trezor/urls": "workspace:*" + "@trezor/utils": "workspace:*" + redux-mock-store: "npm:^1.5.4" + redux-thunk: "npm:^2.4.2" + languageName: unknown + linkType: soft + "@trezor/suite-desktop-core@workspace:packages/suite-desktop-core": version: 0.0.0-use.local resolution: "@trezor/suite-desktop-core@workspace:packages/suite-desktop-core" @@ -12600,6 +12618,7 @@ __metadata: "@trezor/suite-analytics": "workspace:*" "@trezor/suite-data": "workspace:*" "@trezor/suite-desktop-api": "workspace:*" + "@trezor/suite-desktop-connect-popup": "workspace:*" "@trezor/suite-storage": "workspace:*" "@trezor/theme": "workspace:*" "@trezor/type-utils": "workspace:*" From 620b360de2f111c5a415be36dc53c67918d44f8d Mon Sep 17 00:00:00 2001 From: Jan Komarek Date: Mon, 16 Dec 2024 17:53:20 +0100 Subject: [PATCH 005/181] fix(suite): change p to div to fix DOM nesting validation --- .../TransactionsGroup/PendingGroupHeader.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/PendingGroupHeader.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/PendingGroupHeader.tsx index 50283e447c48..b9313b308f30 100644 --- a/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/PendingGroupHeader.tsx +++ b/packages/suite/src/views/wallet/transactions/TransactionList/TransactionsGroup/PendingGroupHeader.tsx @@ -1,4 +1,4 @@ -import { InfoSegments, Paragraph } from '@trezor/components'; +import { InfoSegments, Text } from '@trezor/components'; import { Translation } from 'src/components/suite'; @@ -6,11 +6,11 @@ type PendingGroupHeaderProps = { txsCount: number }; export const PendingGroupHeader = ({ txsCount }: PendingGroupHeaderProps) => { return ( - + {txsCount} - + ); }; From aa7ff5e8658252839c7084e3ca969ef86b805cc9 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Mon, 2 Dec 2024 12:49:32 +0100 Subject: [PATCH 006/181] feat(node-utils): checkSocks5Proxy util --- packages/node-utils/src/checkSocks5Proxy.ts | 35 +++++++++++++++++++++ packages/node-utils/src/index.ts | 1 + 2 files changed, 36 insertions(+) create mode 100644 packages/node-utils/src/checkSocks5Proxy.ts diff --git a/packages/node-utils/src/checkSocks5Proxy.ts b/packages/node-utils/src/checkSocks5Proxy.ts new file mode 100644 index 000000000000..e6af1f4e138e --- /dev/null +++ b/packages/node-utils/src/checkSocks5Proxy.ts @@ -0,0 +1,35 @@ +import net from 'net'; + +export const checkSocks5Proxy = (host: string, port: number): Promise => { + return new Promise((resolve, reject) => { + const socket = new net.Socket(); + + socket.setTimeout(2_000); + + socket.on('connect', () => { + // Version 5, 1 method, no authentication + const handshakeRequest = Buffer.from([0x05, 0x01, 0x00]); + socket.write(handshakeRequest); + }); + + socket.on('data', data => { + if (data[0] === 0x05 && data[1] === 0x00) { + resolve(true); + } else { + resolve(false); + } + socket.destroy(); + }); + + socket.on('error', err => { + reject(err); + }); + + socket.on('timeout', () => { + socket.destroy(); + reject(new Error('Connection timed out')); + }); + + socket.connect(port, host); + }); +}; diff --git a/packages/node-utils/src/index.ts b/packages/node-utils/src/index.ts index 0201677b904a..951c43c5d5ec 100644 --- a/packages/node-utils/src/index.ts +++ b/packages/node-utils/src/index.ts @@ -12,3 +12,4 @@ export { type Response, } from './http'; export { checkFileExists } from './checkFileExists'; +export { checkSocks5Proxy } from './checkSocks5Proxy'; From c456866d2e3da32c5ae7f8aa3928998052b7b537 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Mon, 2 Dec 2024 12:49:59 +0100 Subject: [PATCH 007/181] test(node-utils): checkSocks5Proxy tests --- .../src/tests/checkSocks5Proxy.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 packages/node-utils/src/tests/checkSocks5Proxy.test.ts diff --git a/packages/node-utils/src/tests/checkSocks5Proxy.test.ts b/packages/node-utils/src/tests/checkSocks5Proxy.test.ts new file mode 100644 index 000000000000..7a90e28ae788 --- /dev/null +++ b/packages/node-utils/src/tests/checkSocks5Proxy.test.ts @@ -0,0 +1,62 @@ +import net from 'net'; + +import { checkSocks5Proxy } from '../checkSocks5Proxy'; + +jest.mock('net'); + +describe('checkSocks5Proxy', () => { + const host = '127.0.0.1'; + const port = 9050; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return true for a valid SOCKS5 proxy', async () => { + const mockSocket = { + connect: jest.fn(), + write: jest.fn(), + on: jest.fn((event, callback) => { + if (event === 'connect') { + callback(); + } + if (event === 'data') { + // Valid SOCKS5 response. + callback(Buffer.from([0x05, 0x00])); + } + }), + setTimeout: jest.fn(), + destroy: jest.fn(), + }; + + // @ts-expect-error + net.Socket.mockImplementation(() => mockSocket); + + const result = await checkSocks5Proxy(host, port); + expect(result).toBe(true); + }); + + it('should return false for an invalid SOCKS5 proxy', async () => { + const mockSocket = { + connect: jest.fn(), + write: jest.fn(), + on: jest.fn((event, callback) => { + if (event === 'connect') { + callback(); + } + if (event === 'data') { + // Not valid SOCKS5 response + callback(Buffer.from([0x05, 0x01])); + } + }), + setTimeout: jest.fn(), + destroy: jest.fn(), + }; + + // @ts-expect-error + net.Socket.mockImplementation(() => mockSocket); + + const result = await checkSocks5Proxy(host, port); + expect(result).toBe(false); + }); +}); From ba6e82d8fc6fd7f4cc1e174a33280c8cd3c50a9d Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Mon, 2 Dec 2024 12:52:03 +0100 Subject: [PATCH 008/181] feat(request-manager): support external Tor --- packages/request-manager/src/controller.ts | 97 ++++++++----------- .../request-manager/src/controllerExternal.ts | 87 +++++++++++++++++ packages/request-manager/src/index.ts | 1 + packages/request-manager/src/types.ts | 6 ++ 4 files changed, 136 insertions(+), 55 deletions(-) create mode 100644 packages/request-manager/src/controllerExternal.ts diff --git a/packages/request-manager/src/controller.ts b/packages/request-manager/src/controller.ts index ac42aeb48c85..4073bf1bedce 100644 --- a/packages/request-manager/src/controller.ts +++ b/packages/request-manager/src/controller.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'events'; import path from 'path'; -import { createTimeoutPromise } from '@trezor/utils'; +import { ScheduleActionParams, ScheduledAction, scheduleAction } from '@trezor/utils'; import { checkFileExists } from '@trezor/node-utils'; import { TorControlPort } from './torControlPort'; @@ -13,15 +13,15 @@ import { } from './types'; import { bootstrapParser, BOOTSTRAP_EVENT_PROGRESS } from './events/bootstrap'; +const WAITING_TIME = 1000; +const MAX_TRIES_WAITING = 200; +const BOOTSTRAP_SLOW_TRESHOLD = 1000 * 5; // 5 seconds. + export class TorController extends EventEmitter { options: TorConnectionOptions; controlPort: TorControlPort; bootstrapSlownessChecker?: NodeJS.Timeout; status: TorControllerStatus = TOR_CONTROLLER_STATUS.Stopped; - // Configurations - waitingTime = 1000; - maxTriesWaiting = 200; - bootstrapSlowThreshold = 1000 * 5; // 5 seconds. constructor(options: TorConnectionOptions) { super(); @@ -57,14 +57,32 @@ export class TorController extends EventEmitter { if (this.bootstrapSlownessChecker) { clearTimeout(this.bootstrapSlownessChecker); } - // When Bootstrap starts we wait time defined in bootstrapSlowThreshold and if after that time, + // When Bootstrap starts we wait time defined in BOOTSTRAP_SLOW_TRESHOLD and if after that time, // it has not being finalized, then we send slow event. We know that Bootstrap is going on since // we received, at least, first Bootstrap events from ControlPort. this.bootstrapSlownessChecker = setTimeout(() => { this.emit('bootstrap/event', { type: 'slow', }); - }, this.bootstrapSlowThreshold); + }, BOOTSTRAP_SLOW_TRESHOLD); + } + + private onMessageReceived(message: string) { + const bootstrap: BootstrapEvent[] = bootstrapParser(message); + bootstrap.forEach(event => { + if (event.type !== 'progress') return; + if (event.progress && !this.getIsBootstrapping()) { + // We consider that bootstrap has started when we receive any bootstrap event and + // Tor is not bootstrapping yet. + // If we do not receive any bootstrapping event, we can consider there is something going wrong and + // an error will be thrown when `MAX_TRIES_WAITING` is reached in `waitUntilAlive`. + this.startBootstrap(); + } + if (event.progress === BOOTSTRAP_EVENT_PROGRESS.Done) { + this.successfullyBootstrapped(); + } + this.emit('bootstrap/event', event); + }); } public async getTorConfiguration( @@ -173,59 +191,28 @@ export class TorController extends EventEmitter { return config; } - public onMessageReceived(message: string) { - const bootstrap: BootstrapEvent[] = bootstrapParser(message); - bootstrap.forEach(event => { - if (event.type !== 'progress') return; - if (event.progress && !this.getIsBootstrapping()) { - // We consider that bootstrap has started when we receive any bootstrap event and - // Tor is not bootstrapping yet. - // If we do not receive any bootstrapping event, we can consider there is something going wrong and - // an error will be thrown when `maxTriesWaiting` is reached in `waitUntilAlive`. - this.startBootstrap(); - } - if (event.progress === BOOTSTRAP_EVENT_PROGRESS.Done) { - this.successfullyBootstrapped(); - } - this.emit('bootstrap/event', event); - }); - } - - public waitUntilAlive(): Promise { - const errorMessages: string[] = []; + public async waitUntilAlive(): Promise { this.status = TOR_CONTROLLER_STATUS.Bootstrapping; - const waitUntilResponse = async (triesCount: number): Promise => { - if (this.getIsStopped()) { - // If TOR is starting and we want to cancel it. - return; - } - if (triesCount >= this.maxTriesWaiting) { - throw new Error( - `Timeout waiting for TOR control port: \n${errorMessages.join('\n')}`, - ); - } - try { - const isConnected = await this.controlPort.connect(); - const isAlive = this.controlPort.ping(); - if (isConnected && isAlive && this.getIsCircuitEstablished()) { - // It is running so let's not wait anymore. - return; - } - } catch (error) { - // Some error here is expected when waiting but - // we do not want to throw until maxTriesWaiting is reach. - // Instead we want to log it to know what causes the error. - if (error && error.message) { - console.warn('request-manager:', error.message); - errorMessages.push(error.message); - } + + const checkConnection: ScheduledAction = async () => { + if (this.getIsStopped()) return false; + const isConnected = await this.controlPort.connect(); + const isAlive = this.controlPort.ping(); + const isCircuitEstablished = this.getIsCircuitEstablished(); + // It is running so let's not wait anymore. + if (isConnected && isAlive && isCircuitEstablished) { + return true; } - await createTimeoutPromise(this.waitingTime); + throw new Error('Tor not alive'); + }; - return waitUntilResponse(triesCount + 1); + const params: ScheduleActionParams = { + attempts: MAX_TRIES_WAITING, + timeout: WAITING_TIME, + gap: WAITING_TIME, }; - return waitUntilResponse(1); + await scheduleAction(checkConnection, params); } public getStatus(): Promise { diff --git a/packages/request-manager/src/controllerExternal.ts b/packages/request-manager/src/controllerExternal.ts new file mode 100644 index 000000000000..e020e3e5f6c6 --- /dev/null +++ b/packages/request-manager/src/controllerExternal.ts @@ -0,0 +1,87 @@ +import { EventEmitter } from 'events'; + +import { ScheduleActionParams, ScheduledAction, scheduleAction } from '@trezor/utils'; +import { checkSocks5Proxy } from '@trezor/node-utils'; + +import { TOR_CONTROLLER_STATUS, TorControllerStatus, TorExternalConnectionOptions } from './types'; + +const WAITING_TIME = 1_000; +const MAX_TRIES_WAITING = 200; + +export class TorControllerExternal extends EventEmitter { + status: TorControllerStatus = TOR_CONTROLLER_STATUS.Stopped; + options: TorExternalConnectionOptions; + + constructor(options: TorExternalConnectionOptions) { + super(); + this.options = options; + } + + private getIsStopped() { + return this.status === TOR_CONTROLLER_STATUS.Stopped; + } + + private async getIsExternalTorRunning() { + let isSocks5ProxyPort = false; + try { + isSocks5ProxyPort = await checkSocks5Proxy(this.options.host, this.options.port); + } catch { + // Ignore errors. + } + + return isSocks5ProxyPort; + } + + private startBootstrap() { + this.status = TOR_CONTROLLER_STATUS.Bootstrapping; + } + + private successfullyBootstrapped() { + this.status = TOR_CONTROLLER_STATUS.ExternalTorRunning; + } + + public getTorConfiguration() { + return ''; + } + + public async waitUntilAlive() { + this.startBootstrap(); + + const checkConnection: ScheduledAction = async () => { + if (this.getIsStopped()) return false; + const isRunning = await this.getIsExternalTorRunning(); + if (isRunning) { + this.successfullyBootstrapped(); + + return true; + } + + throw new Error('Tor external not alive'); + }; + + const params: ScheduleActionParams = { + attempts: MAX_TRIES_WAITING, + timeout: WAITING_TIME, + gap: WAITING_TIME, + }; + + await scheduleAction(checkConnection, params); + } + + public async getStatus() { + const isExternalTorRunning = await this.getIsExternalTorRunning(); + if (isExternalTorRunning) { + return TOR_CONTROLLER_STATUS.ExternalTorRunning; + } + + return TOR_CONTROLLER_STATUS.Stopped; + } + + public closeActiveCircuits() { + // Do nothing. Not possible in External Tor without ControlPort. + } + + public stop() { + this.status = TOR_CONTROLLER_STATUS.Stopped; + } +} diff --git a/packages/request-manager/src/index.ts b/packages/request-manager/src/index.ts index aef9257d3391..2e198e5fe49c 100644 --- a/packages/request-manager/src/index.ts +++ b/packages/request-manager/src/index.ts @@ -1,4 +1,5 @@ export { TorController } from './controller'; +export { TorControllerExternal } from './controllerExternal'; export { createInterceptor } from './interceptor'; export type { InterceptedEvent, BootstrapEvent, TorControllerStatus } from './types'; export { TOR_CONTROLLER_STATUS } from './types'; diff --git a/packages/request-manager/src/types.ts b/packages/request-manager/src/types.ts index ac9fff309854..c567100c4457 100644 --- a/packages/request-manager/src/types.ts +++ b/packages/request-manager/src/types.ts @@ -6,6 +6,11 @@ export interface TorConnectionOptions { snowflakeBinaryPath: string; } +export interface TorExternalConnectionOptions { + host: string; + port: number; +} + export type TorCommandResponse = | { success: true; @@ -72,5 +77,6 @@ export const TOR_CONTROLLER_STATUS = { Bootstrapping: 'Bootstrapping', Stopped: 'Stopped', CircuitEstablished: 'CircuitEstablished', + ExternalTorRunning: 'ExternalTorRunning', } as const; export type TorControllerStatus = keyof typeof TOR_CONTROLLER_STATUS; From 3751a241c9db234a18ef4dddfb63877eca00fc1a Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Mon, 2 Dec 2024 12:53:02 +0100 Subject: [PATCH 009/181] feat(suite-desktop-core): support external Tor --- packages/suite-desktop-core/src/index.d.ts | 5 +- .../src/libs/processes/TorExternalProcess.ts | 46 +++++ .../src/libs/processes/TorProcess.ts | 10 +- packages/suite-desktop-core/src/libs/store.ts | 3 + .../suite-desktop-core/src/modules/tor.ts | 159 +++++++++++++----- .../src/modules/trezor-connect.ts | 11 +- 6 files changed, 184 insertions(+), 50 deletions(-) create mode 100644 packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts diff --git a/packages/suite-desktop-core/src/index.d.ts b/packages/suite-desktop-core/src/index.d.ts index 3c39c93cfb37..64d3b1b6a4ab 100644 --- a/packages/suite-desktop-core/src/index.d.ts +++ b/packages/suite-desktop-core/src/index.d.ts @@ -100,8 +100,11 @@ declare type UpdateSettings = { declare type TorSettings = { running: boolean; // Tor should be enabled host: string; // Hostname of the tor process through which traffic is routed - port: number; // Port of the tor process through which traffic is routed + port: number; // Port of the Tor process through which traffic is routed + controlPort: number; // Port of the Tor Control Port + torDataDir: string; // Path of tor data directory snowflakeBinaryPath: string; // Path in user system to the snowflake binary + useExternalTor: boolean; // Tor should use external daemon instead of the one built-in suite. }; declare type BridgeSettings = { diff --git a/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts b/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts new file mode 100644 index 000000000000..76d8fb152ae0 --- /dev/null +++ b/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts @@ -0,0 +1,46 @@ +import { TOR_CONTROLLER_STATUS, TorControllerExternal } from '@trezor/request-manager'; + +import { Status } from './BaseProcess'; + +export type TorProcessStatus = Status & { isBootstrapping?: boolean }; + +const DEFAULT_TOR_EXTERNAL_HOST = '127.0.0.1'; +const DEFAULT_TOR_EXTERNAL_PORT = 9050; + +export class TorExternalProcess { + isStopped = true; + torController: TorControllerExternal; + port = DEFAULT_TOR_EXTERNAL_PORT; + host = DEFAULT_TOR_EXTERNAL_HOST; + constructor() { + this.torController = new TorControllerExternal({ host: this.host, port: this.port }); + } + + public setTorConfig(_torConfig: { useExternalTor: boolean; snowflakeBinaryPath: string }) { + // Do nothing + } + + public getPort() { + return this.port; + } + + public async status(): Promise { + const torControllerStatus = await this.torController.getStatus(); + + return { + service: torControllerStatus === TOR_CONTROLLER_STATUS.ExternalTorRunning, + process: torControllerStatus === TOR_CONTROLLER_STATUS.ExternalTorRunning, + isBootstrapping: false, // For Tor external we fake bootstrap process. + }; + } + + public async start(): Promise { + this.isStopped = false; + await this.torController.waitUntilAlive(); + } + + public stop() { + // We should not stop External Tor Process but ignore it. + this.isStopped = true; + } +} diff --git a/packages/suite-desktop-core/src/libs/processes/TorProcess.ts b/packages/suite-desktop-core/src/libs/processes/TorProcess.ts index 6ac1d5b56225..97b7183f5a81 100644 --- a/packages/suite-desktop-core/src/libs/processes/TorProcess.ts +++ b/packages/suite-desktop-core/src/libs/processes/TorProcess.ts @@ -31,11 +31,15 @@ export class TorProcess extends BaseProcess { }); } - setTorConfig(torConfig: Pick) { + public setTorConfig(torConfig: Pick) { this.snowflakeBinaryPath = torConfig.snowflakeBinaryPath; } - async status(): Promise { + public getPort() { + return this.port; + } + + public async status(): Promise { const torControllerStatus = await this.torController.getStatus(); return { @@ -45,7 +49,7 @@ export class TorProcess extends BaseProcess { }; } - async start(): Promise { + public async start(): Promise { const electronProcessId = process.pid; const torConfiguration = await this.torController.getTorConfiguration( electronProcessId, diff --git a/packages/suite-desktop-core/src/libs/store.ts b/packages/suite-desktop-core/src/libs/store.ts index f8a5cd9e02d0..67700c2b90f0 100644 --- a/packages/suite-desktop-core/src/libs/store.ts +++ b/packages/suite-desktop-core/src/libs/store.ts @@ -63,8 +63,11 @@ export class Store { return this.store.get('torSettings', { running: false, port: 9050, + controlPort: 9051, host: '127.0.0.1', snowflakeBinaryPath: '', + useExternalTor: false, + torDataDir: '', }); } diff --git a/packages/suite-desktop-core/src/modules/tor.ts b/packages/suite-desktop-core/src/modules/tor.ts index 5c3ca1b5c3cc..cde6a58003b8 100644 --- a/packages/suite-desktop-core/src/modules/tor.ts +++ b/packages/suite-desktop-core/src/modules/tor.ts @@ -12,28 +12,52 @@ import { getFreePort } from '@trezor/node-utils'; import { validateIpcMessage } from '@trezor/ipc-proxy'; import { TorProcess, TorProcessStatus } from '../libs/processes/TorProcess'; +import { TorExternalProcess } from '../libs/processes/TorExternalProcess'; import { app, ipcMain } from '../typed-electron'; import type { Dependencies } from './index'; const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) => { const { logger } = global; - const host = '127.0.0.1'; - const port = await getFreePort(); - const controlPort = await getFreePort(); - const torDataDir = path.join(app.getPath('userData'), 'tor'); const initialSettings = store.getTorSettings(); - store.setTorSettings({ ...initialSettings, host, port }); - - const tor = new TorProcess({ - host, - port, - controlPort, - torDataDir, - snowflakeBinaryPath: initialSettings.snowflakeBinaryPath, + store.setTorSettings({ + ...initialSettings, + port: await getFreePort(), + controlPort: await getFreePort(), + torDataDir: path.join(app.getPath('userData'), 'tor'), }); + const settings = store.getTorSettings(); + + const processes = [ + { + type: 'tor', + process: new TorProcess({ + host: settings.host, + port: settings.port, + controlPort: settings.controlPort, + torDataDir: settings.torDataDir, + snowflakeBinaryPath: settings.snowflakeBinaryPath, + }), + }, + { + type: 'tor-external', + process: new TorExternalProcess(), + }, + ]; + + const getTarget = () => { + const { useExternalTor } = store.getTorSettings(); + const currentTarget = useExternalTor ? 'tor-external' : 'tor'; + + return processes.find(process => process.type === currentTarget)!.process; + }; + + const updateTorPort = (port: number) => { + store.setTorSettings({ ...store.getTorSettings(), port }); + }; + const setProxy = (rule: string) => { logger.info('tor', `Setting proxy rules to "${rule}"`); // Including network session of electron auto-updater in the Tor proxy. @@ -44,17 +68,26 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) }); }; - const getProxySettings = (shouldEnableTor: boolean) => - shouldEnableTor ? { proxy: `socks://${host}:${port}` } : { proxy: '' }; + const getProxySettings = (shouldEnableTor: boolean) => { + const { useExternalTor, port, host } = store.getTorSettings(); + return shouldEnableTor + ? { + proxy: `socks://${host}:${useExternalTor ? 9050 : port}`, + } + : { proxy: '' }; + }; const handleTorProcessStatus = (status: TorProcessStatus) => { + const { useExternalTor, running } = store.getTorSettings(); let type: TorStatus; if (!status.process) { type = TorStatus.Disabled; } else if (status.isBootstrapping) { type = TorStatus.Enabling; - } else if (status.service) { + } else if (status.service && !useExternalTor) { + type = TorStatus.Enabled; + } else if (useExternalTor && running) { type = TorStatus.Enabled; } else { type = TorStatus.Disabled; @@ -90,20 +123,52 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) } }; + const createFakeBootstrapProcess = () => { + let progress = 0; + const duration = 3_000; + // update progress every 300ms. + const interval = 300; + + const increment = (100 / duration) * interval; + const intervalId = setInterval(() => { + progress += increment; + if (progress >= 100) { + progress = 100; + clearInterval(intervalId); + } + handleBootstrapEvent({ + type: 'progress', + progress: `${progress}`, + summary: 'Using External Tor fake progress', + }); + }, interval); + }; + const setupTor = async (shouldEnableTor: boolean) => { - const isTorRunning = (await tor.status()).process; - const { snowflakeBinaryPath } = store.getTorSettings(); + const { useExternalTor, snowflakeBinaryPath } = store.getTorSettings(); + + const isTorRunning = (await getTarget().status()).process; - if (shouldEnableTor === isTorRunning) { + if (shouldEnableTor === isTorRunning && !useExternalTor) { return; } if (shouldEnableTor === true) { - setProxy(`socks5://${host}:${port}`); - tor.torController.on('bootstrap/event', handleBootstrapEvent); + const { host } = store.getTorSettings(); + const port = getTarget().getPort(); + const proxyRule = `socks5://${host}:${port}`; + setProxy(proxyRule); + getTarget().torController.on('bootstrap/event', handleBootstrapEvent); + try { - tor.setTorConfig({ snowflakeBinaryPath }); - await tor.start(); + getTarget().setTorConfig({ snowflakeBinaryPath, useExternalTor }); + updateTorPort(port); + if (useExternalTor) { + await getTarget().start(); + createFakeBootstrapProcess(); + } else { + await getTarget().start(); + } } catch (error) { mainWindowProxy.getInstance()?.webContents.send('tor/bootstrap', { type: 'error', @@ -111,18 +176,19 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) }); // When there is error does not mean that the process is stop, // so we make sure to stop it so we are able to restart it. - tor.stop(); + getTarget().stop(); + throw error; } finally { - tor.torController.removeAllListeners(); + getTarget().torController.removeAllListeners(); } } else { mainWindowProxy.getInstance()?.webContents.send('tor/status', { type: TorStatus.Disabling, }); setProxy(''); - tor.torController.stop(); - await tor.stop(); + getTarget().torController.stop(); + await getTarget().stop(); } store.setTorSettings({ ...store.getTorSettings(), running: shouldEnableTor }); @@ -130,15 +196,20 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) ipcMain.handle( 'tor/change-settings', - (ipcEvent, { snowflakeBinaryPath }: { snowflakeBinaryPath: string }) => { + ( + ipcEvent, + { + snowflakeBinaryPath, + useExternalTor, + }: { snowflakeBinaryPath: string; useExternalTor: boolean }, + ) => { validateIpcMessage(ipcEvent); try { store.setTorSettings({ - running: store.getTorSettings().running, - host, - port, + ...store.getTorSettings(), snowflakeBinaryPath, + useExternalTor, }); return { success: true }; @@ -175,6 +246,7 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) // correctly set in module trezor-connect-ipc. const proxySettings = getProxySettings(shouldEnableTor); + // Proxy is also set in packages/suite-desktop-core/src/modules/trezor-connect.ts await TrezorConnect.setProxy(proxySettings); logger.info( @@ -201,7 +273,8 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) } // Once Tor is toggled it renderer should know the new status. - const status = await tor.status(); + const status = await getTarget().status(); + handleTorProcessStatus(status); return { success: true }; @@ -211,11 +284,16 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) let lastCircuitResetTime = 0; const socksTimeout = 30000; // this value reflects --SocksTimeout flag set by TorController config mainThreadEmitter.on('module/reset-tor-circuits', event => { + if (store.getTorSettings().useExternalTor) { + logger.debug('tor', `Ignore circuit reset. Running External Tor without Control Port.`); + + return; + } const lastResetDiff = Date.now() - lastCircuitResetTime; if (lastResetDiff > socksTimeout) { logger.debug('tor', `Close active circuits. Triggered by identity ${event.identity}`); lastCircuitResetTime = Date.now(); - tor.torController.closeActiveCircuits(); + getTarget().torController.closeActiveCircuits(); } else { logger.debug( 'tor', @@ -225,8 +303,9 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) }); ipcMain.on('tor/get-status', async () => { - logger.debug('tor', `Getting status (${store.getTorSettings().running ? 'ON' : 'OFF'})`); - const status = await tor.status(); + const { running } = store.getTorSettings(); + logger.debug('tor', `Getting status (${running ? 'ON' : 'OFF'})`); + const status = await getTarget().status(); handleTorProcessStatus(status); }); @@ -235,7 +314,7 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) store.setTorSettings({ ...store.getTorSettings(), running: true }); } - return tor; + return getTarget; }; type TorModule = (dependencies: Dependencies) => { @@ -245,24 +324,24 @@ type TorModule = (dependencies: Dependencies) => { export const init: TorModule = dependencies => { let loaded = false; - let tor: TorProcess | undefined; + let getTarget: any; const onLoad = async () => { if (loaded) return { shouldRunTor: false }; loaded = true; - tor = await load(dependencies); - const torSettings = dependencies.store.getTorSettings(); + getTarget = await load(dependencies); + const { running } = dependencies.store.getTorSettings(); return { - shouldRunTor: torSettings.running, + shouldRunTor: running, }; }; const onQuit = async () => { const { logger } = global; logger.info('tor', 'Stopping (app quit)'); - await tor?.stop(); + await getTarget()?.stop(); }; return { onLoad, onQuit }; diff --git a/packages/suite-desktop-core/src/modules/trezor-connect.ts b/packages/suite-desktop-core/src/modules/trezor-connect.ts index eb8c1d375cf3..393e13896dc4 100644 --- a/packages/suite-desktop-core/src/modules/trezor-connect.ts +++ b/packages/suite-desktop-core/src/modules/trezor-connect.ts @@ -11,11 +11,10 @@ export const initBackground: ModuleInitBackground = ({ mainThreadEmitter, store const { logger } = global; logger.info(SERVICE_NAME, `Starting service`); - const setProxy = (ifRunning = false) => { - const tor = store.getTorSettings(); - if (ifRunning && !tor.running) return Promise.resolve(); - const payload = tor.running ? { proxy: `socks://${tor.host}:${tor.port}` } : { proxy: '' }; - logger.info(SERVICE_NAME, `${tor.running ? 'Enable' : 'Disable'} proxy ${payload.proxy}`); + const setProxy = () => { + const { running, host, port } = store.getTorSettings(); + const payload = running ? { proxy: `socks://${host}:${port}` } : { proxy: '' }; + logger.info(SERVICE_NAME, `${running ? 'Enable' : 'Disable'} proxy ${payload.proxy}`); return TrezorConnect.setProxy(payload); }; @@ -26,7 +25,7 @@ export const initBackground: ModuleInitBackground = ({ mainThreadEmitter, store logger.debug(SERVICE_NAME, `call ${method}`); if (method === 'init') { const response = await TrezorConnect[method](...params); - await setProxy(true); + await setProxy(); return response; } From 8424a21c1884cd566fcb440f8049a081a817c14c Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Mon, 2 Dec 2024 12:53:28 +0100 Subject: [PATCH 010/181] feat(suite-desktop-api): support external Tor --- packages/suite-desktop-api/src/messages.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/suite-desktop-api/src/messages.ts b/packages/suite-desktop-api/src/messages.ts index 6eeb54c95628..24f0cc420598 100644 --- a/packages/suite-desktop-api/src/messages.ts +++ b/packages/suite-desktop-api/src/messages.ts @@ -55,6 +55,7 @@ export type HandshakeTorModule = { export type TorSettings = { snowflakeBinaryPath: string; + useExternalTor: boolean; }; export type TraySettings = { From 3c34e5e3db661741b01063721d7633ad4657e4fe Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Mon, 2 Dec 2024 12:53:58 +0100 Subject: [PATCH 011/181] feat(suite): external-tor experimental feature --- .../suite/src/constants/suite/experimental.ts | 17 ++++++++++++++++- packages/suite/src/support/messages.ts | 9 +++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/suite/src/constants/suite/experimental.ts b/packages/suite/src/constants/suite/experimental.ts index 45e6721e889f..1c96d8b7fd81 100644 --- a/packages/suite/src/constants/suite/experimental.ts +++ b/packages/suite/src/constants/suite/experimental.ts @@ -5,7 +5,7 @@ import { Route } from '@suite-common/suite-types'; import { Dispatch } from '../../types/suite'; -export type ExperimentalFeature = 'password-manager' | 'tor-snowflake'; +export type ExperimentalFeature = 'password-manager' | 'tor-snowflake' | 'tor-external'; export type ExperimentalFeatureConfig = { title: TranslationKey; @@ -39,4 +39,19 @@ export const EXPERIMENTAL_FEATURES: Record { + const result = await desktopApi.getTorSettings(); + if (result.success && result.payload.useExternalTor !== newValue) { + await desktopApi.changeTorSettings({ + ...result.payload, + useExternalTor: newValue, + }); + } + }, + }, }; diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index a58109ce8d22..81efcb62f2b1 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -5010,6 +5010,15 @@ export default defineMessages({ defaultMessage: 'Access censored websites and apps using Tor Snowflake, a system designed to bypass restrictions.', }, + TR_EXPERIMENTAL_TOR_EXTERNAL: { + id: 'TR_EXPERIMENTAL_TOR_EXTERNAL', + defaultMessage: 'Tor external', + }, + TR_EXPERIMENTAL_TOR_EXTERNAL_DESCRIPTION: { + id: 'TR_EXPERIMENTAL_TOR_EXTERNAL_DESCRIPTION', + defaultMessage: + 'Allows you to use Tor daemon running in a external process on port 9050 instead of the one bundled with Trezor Suite.', + }, TR_EARLY_ACCESS: { id: 'TR_EARLY_ACCESS', defaultMessage: 'Early Access Program', From 1b192a582f3c7d27996d6b6376e7dde35c82dffb Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Fri, 13 Dec 2024 08:59:28 +0100 Subject: [PATCH 012/181] chore(request-manager): remove snowflake direct support --- .../request-manager/e2e/identities-stress.ts | 3 +- .../request-manager/e2e/interceptor.test.ts | 6 +-- .../e2e/torControlPort.test.ts | 3 -- packages/request-manager/src/controller.ts | 41 +------------------ packages/request-manager/src/types.ts | 1 - 5 files changed, 4 insertions(+), 50 deletions(-) diff --git a/packages/request-manager/e2e/identities-stress.ts b/packages/request-manager/e2e/identities-stress.ts index 5f1289c8c53e..faad13c90c0a 100644 --- a/packages/request-manager/e2e/identities-stress.ts +++ b/packages/request-manager/e2e/identities-stress.ts @@ -37,9 +37,8 @@ const intervalBetweenRequests = 1000 * 20; port, controlPort, torDataDir, - snowflakeBinaryPath: '', }); - const torParams = await torController.getTorConfiguration(processId); + const torParams = torController.getTorConfiguration(processId); // Starting Tor process from binary. torRunner({ torParams, diff --git a/packages/request-manager/e2e/interceptor.test.ts b/packages/request-manager/e2e/interceptor.test.ts index dd12649c1412..5186a783a831 100644 --- a/packages/request-manager/e2e/interceptor.test.ts +++ b/packages/request-manager/e2e/interceptor.test.ts @@ -10,7 +10,6 @@ const hostIp = '127.0.0.1'; const port = 38835; const controlPort = 35527; const processId = process.pid; -const snowflakeBinaryPath = ''; // 1 minute before timeout, because Tor might be slow to start. jest.setTimeout(60000); @@ -29,7 +28,7 @@ describe('Interceptor', () => { let torController: TorController; let torIdentities: TorIdentities; - const torSettings = { running: true, host: hostIp, port, snowflakeBinaryPath }; + const torSettings = { running: true, host: hostIp, port }; const INTERCEPTOR = { handler: () => {}, @@ -45,9 +44,8 @@ describe('Interceptor', () => { port, controlPort, torDataDir, - snowflakeBinaryPath, }); - const torParams = await torController.getTorConfiguration(processId); + const torParams = torController.getTorConfiguration(processId); // Starting Tor process from binary. torProcess = torRunner({ torParams, diff --git a/packages/request-manager/e2e/torControlPort.test.ts b/packages/request-manager/e2e/torControlPort.test.ts index d341d062f13e..864764c11ad0 100644 --- a/packages/request-manager/e2e/torControlPort.test.ts +++ b/packages/request-manager/e2e/torControlPort.test.ts @@ -18,7 +18,6 @@ const controlAuthCookiePath = `${torDataDir}/control_auth_cookie`; const host = 'localhost'; const port = 9998; const controlPort = 9999; -const snowflakeBinaryPath = ''; describe('TorControlPort', () => { beforeAll(async () => { @@ -40,7 +39,6 @@ describe('TorControlPort', () => { port, controlPort, torDataDir, - snowflakeBinaryPath, }; const fakeListener = () => {}; const torControlPort = new TorControlPort(options, fakeListener); @@ -105,7 +103,6 @@ describe('TorControlPort', () => { port, controlPort, torDataDir, - snowflakeBinaryPath, }; const fakeListener = () => {}; const torControlPort = new TorControlPort(options, fakeListener); diff --git a/packages/request-manager/src/controller.ts b/packages/request-manager/src/controller.ts index 4073bf1bedce..ee58e4af0bf1 100644 --- a/packages/request-manager/src/controller.ts +++ b/packages/request-manager/src/controller.ts @@ -2,7 +2,6 @@ import { EventEmitter } from 'events'; import path from 'path'; import { ScheduleActionParams, ScheduledAction, scheduleAction } from '@trezor/utils'; -import { checkFileExists } from '@trezor/node-utils'; import { TorControlPort } from './torControlPort'; import { @@ -85,13 +84,9 @@ export class TorController extends EventEmitter { }); } - public async getTorConfiguration( - processId: number, - snowflakeBinaryPath?: string, - ): Promise { + public getTorConfiguration(processId: number): string[] { const { torDataDir } = this.options; const controlAuthCookiePath = path.join(torDataDir, 'control_auth_cookie'); - const snowflakeLogPath = path.join(torDataDir, 'snowflake.log'); // https://github.com/torproject/tor/blob/bf30943cb75911d70367106af644d4273baaa85d/doc/man/tor.1.txt const config: string[] = [ @@ -154,40 +149,6 @@ export class TorController extends EventEmitter { this.options.torDataDir, ]; - let existsSnowflakeBinary = false; - if (snowflakeBinaryPath && snowflakeBinaryPath.trim() !== '') { - // If provided snowflake file does not exists, do not use it. - existsSnowflakeBinary = await checkFileExists(snowflakeBinaryPath); - } - - if (existsSnowflakeBinary) { - // Snowflake is a WebRTC pluggable transport for Tor (client) - // More info: - // https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/tree/main/client - // https://packages.debian.org/bookworm/snowflake-client - - const SNOWFLAKE_PLUGIN = 'snowflake exec'; - const SNOWFLAKE_SERVER = 'snowflake 192.0.2.3:80'; - const SNOWFLAKE_FINGERPRINT = '2B280B23E1107BB62ABFC40DDCC8824814F80A72'; - const SNOWFLAKE_URL = 'https://snowflake-broker.torproject.net.global.prod.fastly.net/'; - const SNOWFLAKE_FRONT = 'fronts=foursquare.com,github.githubassets.com'; - const SNOWFLAKE_ICE = - 'ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478'; - const SNOWFLAKE_UTLS = 'utls-imitate=hellorandomizedalpn'; - - const snowflakeCommand = `${SNOWFLAKE_PLUGIN} ${snowflakeBinaryPath} -log ${snowflakeLogPath}`; - const snowflakeBridge = `${SNOWFLAKE_SERVER} ${SNOWFLAKE_FINGERPRINT} fingerprint=${SNOWFLAKE_FINGERPRINT} url=${SNOWFLAKE_URL} ${SNOWFLAKE_FRONT} ${SNOWFLAKE_ICE} ${SNOWFLAKE_UTLS}`; - - config.push( - '--UseBridges', - '1', - '--ClientTransportPlugin', - snowflakeCommand, - '--Bridge', - snowflakeBridge, - ); - } - return config; } diff --git a/packages/request-manager/src/types.ts b/packages/request-manager/src/types.ts index c567100c4457..052a84251d8f 100644 --- a/packages/request-manager/src/types.ts +++ b/packages/request-manager/src/types.ts @@ -3,7 +3,6 @@ export interface TorConnectionOptions { port: number; controlPort: number; torDataDir: string; - snowflakeBinaryPath: string; } export interface TorExternalConnectionOptions { From d529c1c480801eb9b63ca1b2e16109da34a49fac Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Fri, 13 Dec 2024 09:00:04 +0100 Subject: [PATCH 013/181] chore(urls): remove snowflake urls --- packages/urls/src/urls.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/urls/src/urls.ts b/packages/urls/src/urls.ts index 5aad9f142a22..ecf0998b97f6 100644 --- a/packages/urls/src/urls.ts +++ b/packages/urls/src/urls.ts @@ -128,8 +128,6 @@ export const CHROME_UPDATE_URL: Url = 'https://support.google.com/chrome/answer/ export const CHROME_ANDROID_URL: Url = 'https://play.google.com/store/apps/details?id=com.android.chrome'; export const TOR_PROJECT_URL: Url = 'https://www.torproject.org/'; -export const TOR_SNOWFLAKE_PROJECT_URL: Url = 'https://snowflake.torproject.org/'; -export const TOR_SNOWFLAKE_KB_URL: Url = 'https://trezor.io/learn/a/tor-snowflake-in-trezor-suite'; export const EXPERIMENTAL_FEATURES_KB_URL: Url = 'https://trezor.io/learn/a/experimental-features-in-trezor-suite'; export const EXPERIMENTAL_PASSWORD_MANAGER_KB_URL: Url = From c813b359564e4900609b939ff9ff8cb2d45d5421 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Fri, 13 Dec 2024 09:00:36 +0100 Subject: [PATCH 014/181] chore(suite-desktop-core): remove snowflake --- packages/suite-desktop-core/src/index.d.ts | 1 - .../src/libs/processes/TorExternalProcess.ts | 4 -- .../src/libs/processes/TorProcess.ts | 12 +----- packages/suite-desktop-core/src/libs/store.ts | 1 - .../suite-desktop-core/src/modules/tor.ts | 43 +++++++------------ 5 files changed, 16 insertions(+), 45 deletions(-) diff --git a/packages/suite-desktop-core/src/index.d.ts b/packages/suite-desktop-core/src/index.d.ts index 64d3b1b6a4ab..3ea4b0fb9a4b 100644 --- a/packages/suite-desktop-core/src/index.d.ts +++ b/packages/suite-desktop-core/src/index.d.ts @@ -103,7 +103,6 @@ declare type TorSettings = { port: number; // Port of the Tor process through which traffic is routed controlPort: number; // Port of the Tor Control Port torDataDir: string; // Path of tor data directory - snowflakeBinaryPath: string; // Path in user system to the snowflake binary useExternalTor: boolean; // Tor should use external daemon instead of the one built-in suite. }; diff --git a/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts b/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts index 76d8fb152ae0..a2e30fe459e7 100644 --- a/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts +++ b/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts @@ -16,10 +16,6 @@ export class TorExternalProcess { this.torController = new TorControllerExternal({ host: this.host, port: this.port }); } - public setTorConfig(_torConfig: { useExternalTor: boolean; snowflakeBinaryPath: string }) { - // Do nothing - } - public getPort() { return this.port; } diff --git a/packages/suite-desktop-core/src/libs/processes/TorProcess.ts b/packages/suite-desktop-core/src/libs/processes/TorProcess.ts index 97b7183f5a81..2a543a7c5f8b 100644 --- a/packages/suite-desktop-core/src/libs/processes/TorProcess.ts +++ b/packages/suite-desktop-core/src/libs/processes/TorProcess.ts @@ -11,7 +11,6 @@ export class TorProcess extends BaseProcess { controlPort: number; torHost: string; torDataDir: string; - snowflakeBinaryPath: string; constructor(options: TorConnectionOptions) { super('tor', 'tor'); @@ -20,21 +19,15 @@ export class TorProcess extends BaseProcess { this.controlPort = options.controlPort; this.torHost = options.host; this.torDataDir = options.torDataDir; - this.snowflakeBinaryPath = ''; this.torController = new TorController({ host: this.torHost, port: this.port, controlPort: this.controlPort, torDataDir: this.torDataDir, - snowflakeBinaryPath: this.snowflakeBinaryPath, }); } - public setTorConfig(torConfig: Pick) { - this.snowflakeBinaryPath = torConfig.snowflakeBinaryPath; - } - public getPort() { return this.port; } @@ -51,10 +44,7 @@ export class TorProcess extends BaseProcess { public async start(): Promise { const electronProcessId = process.pid; - const torConfiguration = await this.torController.getTorConfiguration( - electronProcessId, - this.snowflakeBinaryPath, - ); + const torConfiguration = this.torController.getTorConfiguration(electronProcessId); await super.start(torConfiguration); diff --git a/packages/suite-desktop-core/src/libs/store.ts b/packages/suite-desktop-core/src/libs/store.ts index 67700c2b90f0..42cd31a25af9 100644 --- a/packages/suite-desktop-core/src/libs/store.ts +++ b/packages/suite-desktop-core/src/libs/store.ts @@ -65,7 +65,6 @@ export class Store { port: 9050, controlPort: 9051, host: '127.0.0.1', - snowflakeBinaryPath: '', useExternalTor: false, torDataDir: '', }); diff --git a/packages/suite-desktop-core/src/modules/tor.ts b/packages/suite-desktop-core/src/modules/tor.ts index cde6a58003b8..3d2af751064b 100644 --- a/packages/suite-desktop-core/src/modules/tor.ts +++ b/packages/suite-desktop-core/src/modules/tor.ts @@ -30,28 +30,23 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) const settings = store.getTorSettings(); - const processes = [ - { - type: 'tor', - process: new TorProcess({ - host: settings.host, - port: settings.port, - controlPort: settings.controlPort, - torDataDir: settings.torDataDir, - snowflakeBinaryPath: settings.snowflakeBinaryPath, - }), - }, - { - type: 'tor-external', - process: new TorExternalProcess(), - }, - ]; + const bundledTorProcess = new TorProcess({ + host: settings.host, + port: settings.port, + controlPort: settings.controlPort, + torDataDir: settings.torDataDir, + }); + + const externalTorProcess = new TorExternalProcess(); const getTarget = () => { const { useExternalTor } = store.getTorSettings(); - const currentTarget = useExternalTor ? 'tor-external' : 'tor'; - return processes.find(process => process.type === currentTarget)!.process; + if (useExternalTor) { + return externalTorProcess; + } + + return bundledTorProcess; }; const updateTorPort = (port: number) => { @@ -145,7 +140,7 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) }; const setupTor = async (shouldEnableTor: boolean) => { - const { useExternalTor, snowflakeBinaryPath } = store.getTorSettings(); + const { useExternalTor } = store.getTorSettings(); const isTorRunning = (await getTarget().status()).process; @@ -161,7 +156,6 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) getTarget().torController.on('bootstrap/event', handleBootstrapEvent); try { - getTarget().setTorConfig({ snowflakeBinaryPath, useExternalTor }); updateTorPort(port); if (useExternalTor) { await getTarget().start(); @@ -196,19 +190,12 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) ipcMain.handle( 'tor/change-settings', - ( - ipcEvent, - { - snowflakeBinaryPath, - useExternalTor, - }: { snowflakeBinaryPath: string; useExternalTor: boolean }, - ) => { + (ipcEvent, { useExternalTor }: { useExternalTor: boolean }) => { validateIpcMessage(ipcEvent); try { store.setTorSettings({ ...store.getTorSettings(), - snowflakeBinaryPath, useExternalTor, }); From 97081749a609371b65090d8086005f2edb52f070 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Fri, 13 Dec 2024 09:35:32 +0100 Subject: [PATCH 015/181] chore(suite-desktop-api): remove snwoflake --- packages/suite-desktop-api/src/factory.ts | 2 +- packages/suite-desktop-api/src/messages.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/suite-desktop-api/src/factory.ts b/packages/suite-desktop-api/src/factory.ts index 17269e44fa17..baa14bfd23fb 100644 --- a/packages/suite-desktop-api/src/factory.ts +++ b/packages/suite-desktop-api/src/factory.ts @@ -125,7 +125,7 @@ export const factory = >( getTorSettings: () => ipcRenderer.invoke('tor/get-settings'), changeTorSettings: payload => { - if (validation.isObject({ snowflakeBinaryPath: 'string' }, payload)) { + if (validation.isObject({ useExternalTor: 'boolean' }, payload)) { return ipcRenderer.invoke('tor/change-settings', payload); } diff --git a/packages/suite-desktop-api/src/messages.ts b/packages/suite-desktop-api/src/messages.ts index 24f0cc420598..b8e5bc6cf2e1 100644 --- a/packages/suite-desktop-api/src/messages.ts +++ b/packages/suite-desktop-api/src/messages.ts @@ -54,7 +54,6 @@ export type HandshakeTorModule = { }; export type TorSettings = { - snowflakeBinaryPath: string; useExternalTor: boolean; }; From 147a20fbe60796075ceb82207a4bad8dcb600150 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Fri, 13 Dec 2024 09:36:07 +0100 Subject: [PATCH 016/181] feat(suite): remove snowflakes experimental feature --- .../suite/src/constants/suite/experimental.ts | 22 +-- packages/suite/src/support/messages.ts | 30 ----- packages/suite/src/types/suite/index.ts | 5 - .../SettingsGeneral/SettingsGeneral.tsx | 6 - .../settings/SettingsGeneral/TorSnowflake.tsx | 125 ------------------ 5 files changed, 4 insertions(+), 184 deletions(-) delete mode 100644 packages/suite/src/views/settings/SettingsGeneral/TorSnowflake.tsx diff --git a/packages/suite/src/constants/suite/experimental.ts b/packages/suite/src/constants/suite/experimental.ts index 1c96d8b7fd81..adbd887b33dd 100644 --- a/packages/suite/src/constants/suite/experimental.ts +++ b/packages/suite/src/constants/suite/experimental.ts @@ -1,11 +1,12 @@ import { TranslationKey } from '@suite-common/intl-types'; import { desktopApi } from '@trezor/suite-desktop-api'; -import { EXPERIMENTAL_PASSWORD_MANAGER_KB_URL, TOR_SNOWFLAKE_KB_URL, Url } from '@trezor/urls'; +import { EXPERIMENTAL_PASSWORD_MANAGER_KB_URL, Url } from '@trezor/urls'; import { Route } from '@suite-common/suite-types'; +import { isDesktop } from '@trezor/env-utils'; import { Dispatch } from '../../types/suite'; -export type ExperimentalFeature = 'password-manager' | 'tor-snowflake' | 'tor-external'; +export type ExperimentalFeature = 'password-manager' | 'tor-external'; export type ExperimentalFeatureConfig = { title: TranslationKey; @@ -23,27 +24,12 @@ export const EXPERIMENTAL_FEATURES: Record { - if (!newValue) { - const result = await desktopApi.getTorSettings(); - if (result.success && result.payload.snowflakeBinaryPath !== '') { - await desktopApi.changeTorSettings({ - ...result.payload, - snowflakeBinaryPath: '', - }); - } - } - }, - }, 'tor-external': { title: 'TR_EXPERIMENTAL_TOR_EXTERNAL', description: 'TR_EXPERIMENTAL_TOR_EXTERNAL_DESCRIPTION', // TODO: create knowledge base page for this! // knowledgeBaseUrl: TOR_EXTERNAL_KNOWLEDGE_BASE, + isDisabled: () => !isDesktop(), onToggle: async ({ newValue }) => { const result = await desktopApi.getTorSettings(); if (result.success && result.payload.useExternalTor !== newValue) { diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 81efcb62f2b1..789fb20e11ef 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -4052,27 +4052,6 @@ export default defineMessages({ id: 'TR_ONION_LINKS_TITLE', defaultMessage: 'Open trezor.io links as .onion links', }, - TR_TOR_CONFIG_SNOWFLAKE_TITLE: { - id: 'TR_TOR_CONFIG_SNOWFLAKE_TITLE', - defaultMessage: 'Tor Snowflake Binary Path', - }, - TR_TOR_CONFIG_SNOWFLAKE_DESCRIPTION: { - id: 'TR_TOR_CONFIG_SNOWFLAKE_DESCRIPTION', - defaultMessage: - 'Enter the path to the Tor Snowflake binary on your system. Make sure Tor is disabled before making this change.', - }, - TR_TOR_CONFIG_SNOWFLAKE_ERROR_PATH: { - id: 'TR_TOR_CONFIG_SNOWFLAKE_ERROR_PATH', - defaultMessage: 'Must be a valid full path.', - }, - TR_TOR_CONFIG_SNOWFLAKE_UPDATE_LABEL: { - id: 'TR_TOR_CONFIG_SNOWFLAKE_UPDATE_LABEL', - defaultMessage: 'Update path', - }, - TR_TOR_CONFIG_SNOWFLAKE_DISABLE_LABEL: { - id: 'TR_TOR_CONFIG_SNOWFLAKE_DISABLE_LABEL', - defaultMessage: 'Disable Tor Snowflake', - }, TR_TOR_ENABLE_TITLE: { id: 'TR_TOR_ENABLE_TITLE', defaultMessage: 'Enable Tor', @@ -5001,15 +4980,6 @@ export default defineMessages({ defaultMessage: 'Use this utility to retrieve passwords stored on Dropbox and secured by Trezor. Designed for former users of the Trezor Password Manager Chrome extension.', }, - TR_EXPERIMENTAL_TOR_SNOWFLAKE: { - id: 'TR_EXPERIMENTAL_TOR_SNOWFLAKE', - defaultMessage: 'Tor Snowflake', - }, - TR_EXPERIMENTAL_TOR_SNOWFLAKE_DESCRIPTION: { - id: 'TR_EXPERIMENTAL_TOR_SNOWFLAKE_DESCRIPTION', - defaultMessage: - 'Access censored websites and apps using Tor Snowflake, a system designed to bypass restrictions.', - }, TR_EXPERIMENTAL_TOR_EXTERNAL: { id: 'TR_EXPERIMENTAL_TOR_EXTERNAL', defaultMessage: 'Tor external', diff --git a/packages/suite/src/types/suite/index.ts b/packages/suite/src/types/suite/index.ts index b8a8cc4e2331..d5739278aec0 100644 --- a/packages/suite/src/types/suite/index.ts +++ b/packages/suite/src/types/suite/index.ts @@ -117,11 +117,6 @@ export interface TorBootstrap { isSlow?: boolean; } -export type TorConfig = { - enableSnowflake: boolean; - snowflakeBinaryPath: string; -}; - export enum DisplayMode { CHUNKS = 1, PAGINATED_TEXT, diff --git a/packages/suite/src/views/settings/SettingsGeneral/SettingsGeneral.tsx b/packages/suite/src/views/settings/SettingsGeneral/SettingsGeneral.tsx index d13a60e79edf..2972d683f3f7 100644 --- a/packages/suite/src/views/settings/SettingsGeneral/SettingsGeneral.tsx +++ b/packages/suite/src/views/settings/SettingsGeneral/SettingsGeneral.tsx @@ -5,7 +5,6 @@ import { SettingsLayout, SettingsSection } from 'src/components/settings'; import { Translation } from 'src/components/suite'; import { useLayoutSize, useSelector } from 'src/hooks/suite'; import { - selectHasExperimentalFeature, selectIsSettingsDesktopAppPromoBannerShown, selectTorState, } from 'src/reducers/suite/suiteReducer'; @@ -30,7 +29,6 @@ import { DesktopSuiteBanner } from './DesktopSuiteBanner'; import { AddressDisplay } from './AddressDisplay'; import { EnableViewOnly } from './EnableViewOnly'; import { Experimental } from './Experimental'; -import { TorSnowflake } from './TorSnowflake'; import { AutomaticUpdate } from './AutomaticUpdate'; import { AutoStart } from './AutoStart'; import { ShowOnTray } from './ShowOnTray'; @@ -45,9 +43,6 @@ export const SettingsGeneral = () => { const desktopUpdate = useSelector(state => state.desktopUpdate); const metadata = useSelector(state => state.metadata); const { isMobileLayout } = useLayoutSize(); - const torSnowflakeExperimentalFeature = useSelector( - selectHasExperimentalFeature('tor-snowflake'), - ); const hasBitcoinNetworks = enabledNetworks.some(symbol => { const networkFeatures = getNetwork(symbol).features; @@ -85,7 +80,6 @@ export const SettingsGeneral = () => { } icon="torBrowser"> {isDesktop() && } {isTorEnabled && } - {isDesktop() && torSnowflakeExperimentalFeature && } )} diff --git a/packages/suite/src/views/settings/SettingsGeneral/TorSnowflake.tsx b/packages/suite/src/views/settings/SettingsGeneral/TorSnowflake.tsx deleted file mode 100644 index acb15cff5dd1..000000000000 --- a/packages/suite/src/views/settings/SettingsGeneral/TorSnowflake.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { ChangeEventHandler, useEffect, useState } from 'react'; - -import styled from 'styled-components'; - -import { TorSettings } from '@trezor/suite-desktop-api/src/messages'; -import { TOR_SNOWFLAKE_KB_URL } from '@trezor/urls'; -import { breakpointMediaQueries } from '@trezor/styles'; -import { desktopApi } from '@trezor/suite-desktop-api'; -import { Button, Input } from '@trezor/components'; -import { isFullPath } from '@trezor/utils'; -import { spacingsPx } from '@trezor/theme'; - -import { selectTorState } from 'src/reducers/suite/suiteReducer'; -import { useSelector, useTranslation } from 'src/hooks/suite'; -import { ActionColumn, SectionItem, TextColumn, Translation } from 'src/components/suite'; - -const Container = styled.div` - display: flex; - flex-direction: column; - align-items: center; - gap: ${spacingsPx.sm}; - min-width: 200px; - - ${breakpointMediaQueries.below_sm} { - min-width: 100%; - } -`; - -export const TorSnowflake = () => { - const { isTorEnabled } = useSelector(selectTorState); - const [torSettings, setTorSettings] = useState(null); - const [hasPathChanged, setHasPathChanged] = useState(false); - const [error, setError] = useState(null); - const { translationString } = useTranslation(); - - useEffect(() => { - const fetchTorSettings = async () => { - const result = await desktopApi.getTorSettings(); - if (result.success) { - setTorSettings(result.payload); - } else { - setError(result.error); - } - }; - - fetchTorSettings(); - - const handleTorSettingsChange = (settings: TorSettings) => setTorSettings(settings); - desktopApi.on('tor/settings', handleTorSettingsChange); - - return () => { - desktopApi.removeAllListeners('tor/settings'); - }; - }, []); - - const handleChange: ChangeEventHandler = ({ target: { value } }) => { - if (!torSettings) return; - - setHasPathChanged(true); - if (!isFullPath(value) && value !== '') { - setError(translationString('TR_TOR_CONFIG_SNOWFLAKE_ERROR_PATH')); - } else { - setError(null); - } - setTorSettings(prevSettings => ({ - ...prevSettings!, - snowflakeBinaryPath: value, - })); - }; - - const handleClick = async () => { - if (!torSettings || error) return; - - await desktopApi.changeTorSettings({ - ...torSettings, - snowflakeBinaryPath: torSettings.snowflakeBinaryPath, - }); - setHasPathChanged(false); - }; - - const isUpdateDisabled = - !torSettings || - !!error || - (!isFullPath(torSettings.snowflakeBinaryPath) && torSettings.snowflakeBinaryPath !== '') || - isTorEnabled || - !hasPathChanged; - - if (!torSettings) return null; - - const buttonTranslationId = - torSettings.snowflakeBinaryPath === '' && hasPathChanged - ? 'TR_TOR_CONFIG_SNOWFLAKE_DISABLE_LABEL' - : 'TR_TOR_CONFIG_SNOWFLAKE_UPDATE_LABEL'; - - return ( - - } - description={} - buttonLink={TOR_SNOWFLAKE_KB_URL} - /> - - - - - - - - ); -}; From 7bb79010ece71536b36b3aab1ab1be7c436ce64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20V=C3=A1clav=C3=ADk?= Date: Mon, 16 Dec 2024 21:42:35 +0100 Subject: [PATCH 017/181] fix(suite): Fix labeling issue on switch device (#15909) * fix(suite): Fix labeling issue on switch device * fix(e2e): Fixed labelling e2e test --------- Co-authored-by: Ondrej Hajek --- .../support/pageObjects/switchDeviceObject.ts | 22 ++++++++++++++++--- .../MetadataLabeling/MetadataLabeling.tsx | 1 + 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/suite-web/e2e/support/pageObjects/switchDeviceObject.ts b/packages/suite-web/e2e/support/pageObjects/switchDeviceObject.ts index 3afbf47cf146..84a9035defaf 100644 --- a/packages/suite-web/e2e/support/pageObjects/switchDeviceObject.ts +++ b/packages/suite-web/e2e/support/pageObjects/switchDeviceObject.ts @@ -13,7 +13,10 @@ class SwitchDevice { clickAddLabel(index: number) { cy.wait(2000); // TODO fix waiting for animation - this.hoverOverWallet(index); + this.hoverAndCheck( + index, + `${this.walletSelectorBeginPart}[data-testid$=":${index + 1}/add-label-button"]`, + ); cy.get( `${this.walletSelectorBeginPart}[data-testid$=":${index + 1}/add-label-button"]`, ).click(); @@ -21,7 +24,10 @@ class SwitchDevice { clickEditLabel(index: number) { cy.wait(2000); // TODO fix waiting for animation - this.hoverOverWallet(index); + this.hoverAndCheck( + index, + `${this.walletSelectorBeginPart}[data-testid$=":${index + 1}/edit-label-button"]`, + ); cy.get( `${this.walletSelectorBeginPart}[data-testid$=":${index + 1}/edit-label-button"]`, ).click(); @@ -33,10 +39,20 @@ class SwitchDevice { cy.get('@metadataInput').type('{enter}'); } - private hoverOverWallet(index: number) { + private hoverAndCheck(index: number, checkSelector: string, retryCount = 2) { + if (retryCount === 0) { + throw new Error(`Failed to make the ${checkSelector} visible`); + } + cy.get( `${this.walletSelectorBeginPart}[data-testid$=":${index + 1}/hover-container"]`, ).realHover(); + cy.get(checkSelector).then($el => { + if (!$el.is(':visible')) { + cy.log(`Retrying hover and check for index ${index}`); + this.hoverAndCheck(index, checkSelector, retryCount - 1); + } + }); } } diff --git a/packages/suite/src/components/suite/labeling/MetadataLabeling/MetadataLabeling.tsx b/packages/suite/src/components/suite/labeling/MetadataLabeling/MetadataLabeling.tsx index fbd9f60bedb4..d9c439a5e1ac 100644 --- a/packages/suite/src/components/suite/labeling/MetadataLabeling/MetadataLabeling.tsx +++ b/packages/suite/src/components/suite/labeling/MetadataLabeling/MetadataLabeling.tsx @@ -65,6 +65,7 @@ const ActionButton = styled(Button)<{ $isValueVisible?: boolean; $isVisible?: bo margin-left: ${({ $isValueVisible, $isVisible, isLoading }) => $isValueVisible || !$isVisible || isLoading ? '12px' : '4px'}; visibility: ${({ $isVisible }) => ($isVisible ? 'visible' : 'hidden')}; + width: ${({ $isVisible }) => ($isVisible ? 'auto' : '1px')}; `; // @TODO this shouldn't be Button From 44ad7bc06636e1d0e447479432788e2bcc14518a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20V=C3=A1clav=C3=ADk?= Date: Mon, 16 Dec 2024 22:12:14 +0100 Subject: [PATCH 018/181] feat(suite): Disable switch wallet on discovery (#15984) * feat(suite): Disable switch wallet on discovery * fix(e2e): Fixed waiting for discovery in metadata lifecycle test --------- Co-authored-by: Ondrej Hajek --- .../tests/metadata/metadata-lifecycle.test.ts | 2 +- .../DeviceSelector/DeviceSelector.tsx | 69 ++++++++++++------- .../DeviceSelector/DeviceStatus.tsx | 2 +- 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/packages/suite-web/e2e/tests/metadata/metadata-lifecycle.test.ts b/packages/suite-web/e2e/tests/metadata/metadata-lifecycle.test.ts index d66a202611e8..64dbd77eeab9 100644 --- a/packages/suite-web/e2e/tests/metadata/metadata-lifecycle.test.ts +++ b/packages/suite-web/e2e/tests/metadata/metadata-lifecycle.test.ts @@ -54,11 +54,11 @@ describe('Metadata - cancel metadata on device', () => { cy.task('pressNo'); // set wallet to remembered + cy.discoveryShouldFinish(); cy.getTestElement('@menu/switch-device').click(); cy.getTestElement('@viewOnlyStatus/disabled').click(); cy.getTestElement('@viewOnly/radios/enabled').click(); cy.safeReload(); - cy.discoveryShouldFinish(); // no dialogue, metadata keys survive together with remembered wallet! // but when user tries to add another wallet, there is enable labeling dialogue again cy.getTestElement('@menu/switch-device').click(); diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx index 2a10e176917c..dea528384aa5 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx @@ -6,16 +6,17 @@ import { selectDevicesCount, selectSelectedDevice } from '@suite-common/wallet-c import type { TimerId } from '@trezor/type-utils'; import { borders, spacingsPx } from '@trezor/theme'; import { focusStyleTransition, getFocusShadowStyle } from '@trezor/components/src/utils/utils'; -import { Icon } from '@trezor/components'; +import { Icon, Tooltip } from '@trezor/components'; import { SHAKE } from 'src/support/suite/styles/animations'; import { goto } from 'src/actions/suite/routerActions'; -import { useDispatch, useSelector } from 'src/hooks/suite'; +import { useDiscovery, useDispatch, useSelector } from 'src/hooks/suite'; import { ViewOnlyTooltip } from 'src/views/view-only/ViewOnlyTooltip'; import { SidebarDeviceStatus } from './SidebarDeviceStatus'; import { ExpandedSidebarOnly } from '../Sidebar/ExpandedSidebarOnly'; import { useResponsiveContext } from '../../../../../support/suite/ResponsiveContext'; +import { Translation } from '../../../Translation'; const CaretContainer = styled.div` background: transparent; @@ -57,15 +58,16 @@ const Wrapper = styled.div<{ $isAnimationTriggered?: boolean; $isSidebarCollapse `} `; -const InnerContainer = styled.div` +const InnerContainer = styled.div<{ $isDisabled?: boolean }>` position: relative; width: 100%; display: flex; align-items: center; - cursor: pointer; gap: ${spacingsPx.md}; min-height: 42px; -webkit-app-region: no-drag; + + cursor: ${({ $isDisabled }) => ($isDisabled ? 'not-allowed' : 'pointer')}; `; export const DeviceSelector = () => { @@ -73,6 +75,9 @@ export const DeviceSelector = () => { const deviceCount = useSelector(selectDevicesCount); const dispatch = useDispatch(); + const { getDiscoveryStatus } = useDiscovery(); + const discoveryStatus = getDiscoveryStatus(); + const discoveryInProgress = discoveryStatus && discoveryStatus.status === 'loading'; const [localCount, setLocalCount] = useState(null); const [isAnimationTriggered, setIsAnimationTriggered] = useState(false); @@ -108,14 +113,17 @@ export const DeviceSelector = () => { } }, [countChanged]); - const handleSwitchDeviceClick = () => - dispatch( - goto('suite-switch-device', { - params: { - cancelable: true, - }, - }), - ); + const handleSwitchDeviceClick = () => { + if (!discoveryInProgress) { + dispatch( + goto('suite-switch-device', { + params: { + cancelable: true, + }, + }), + ); + } + }; const { isSidebarCollapsed } = useResponsiveContext(); @@ -125,21 +133,30 @@ export const DeviceSelector = () => { $isSidebarCollapsed={isSidebarCollapsed} > - + ) : undefined + } > - - - - {selectedDevice && selectedDevice.state && ( - - - - )} - - + + + + + {selectedDevice && selectedDevice.state && ( + + + + )} + + + ); diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx index f95ed6f132b4..6a9b7454a668 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx @@ -67,7 +67,7 @@ export const DeviceStatus = ({ {content} ) : ( - + {image} )} From e8bd8b65fa8a5e766a3572ca5ee5b21c30a6fe30 Mon Sep 17 00:00:00 2001 From: Adam Havel Date: Mon, 16 Dec 2024 15:10:37 +0100 Subject: [PATCH 019/181] fix(suite): device animation aspect ratio --- .../components/animations/DeviceAnimation.tsx | 10 +- .../ConfirmOnDevice/ConfirmOnDevice.tsx | 11 +- .../ConfirmOnDeviceContent.tsx | 142 +++++------------- .../RotateDeviceImage/RotateDeviceImage.tsx | 8 +- .../DeviceSelector/DeviceStatus.tsx | 2 - 5 files changed, 48 insertions(+), 125 deletions(-) diff --git a/packages/components/src/components/animations/DeviceAnimation.tsx b/packages/components/src/components/animations/DeviceAnimation.tsx index aea1970c5da2..8af5844ee09d 100644 --- a/packages/components/src/components/animations/DeviceAnimation.tsx +++ b/packages/components/src/components/animations/DeviceAnimation.tsx @@ -10,8 +10,8 @@ import { AnimationWrapper, Shape } from './AnimationPrimitives'; import { resolveStaticPath } from '../../utils/resolveStaticPath'; const StyledVideo = styled.video` - width: 100%; - height: 100%; + max-width: 100%; + max-height: 100%; `; export type AnimationDeviceType = @@ -80,8 +80,6 @@ export const DeviceAnimation = forwardRef` - display: flex; - width: 300px; - height: 62px; - padding: 0 ${spacingsPx.md} 0 ${spacingsPx.xxl}; +const Wrapper = styled.div<{ $animation?: AnimationDirection; $isCancelable?: boolean }>` + padding: ${spacingsPx.sm} ${spacingsPx.sm} ${spacingsPx.sm} ${spacingsPx.xxl}; border-radius: ${borders.radii.full}; background: ${({ theme }) => theme.backgroundSurfaceElevation0}; box-shadow: ${({ theme }) => theme.boxShadowBase}; - align-items: center; + + ${({ $isCancelable }) => !$isCancelable && `padding-right: ${spacingsPx.xxl};`} ${({ $animation }) => $animation === AnimationDirection.Up && @@ -70,6 +68,7 @@ export interface ConfirmOnDeviceProps { export const ConfirmOnDevice = ({ isConfirmed, ...rest }: ConfirmOnDeviceProps) => ( e.stopPropagation()} > diff --git a/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDeviceContent.tsx b/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDeviceContent.tsx index f3f09898b19e..fa9086e37e1b 100644 --- a/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDeviceContent.tsx +++ b/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDeviceContent.tsx @@ -1,82 +1,17 @@ import { ReactNode } from 'react'; -import styled, { css, useTheme } from 'styled-components'; +import styled, { css } from 'styled-components'; -import { Icon, useElevation } from '@trezor/components'; +import { Column, Row, IconButton, Text } from '@trezor/components'; import { DeviceModelInternal } from '@trezor/connect'; -import { - Elevation, - borders, - mapElevationToBackground, - spacingsPx, - typography, -} from '@trezor/theme'; +import { borders, spacings, spacingsPx } from '@trezor/theme'; import { RotateDeviceImage } from '../RotateDeviceImage/RotateDeviceImage'; -const Column = styled.div` - display: flex; -`; - -const Title = styled.div` - display: flex; - justify-content: center; - ${typography.body}; - color: ${({ theme }) => theme.textDefault}; -`; - -const Left = styled(Column)``; - -const Middle = styled(Column)` - flex: 1; - justify-content: center; - flex-direction: column; -`; - -const Right = styled(Column)``; - -const Steps = styled.div` - display: flex; - margin-top: ${spacingsPx.sm}; - max-width: 200px; - padding: 0 ${spacingsPx.sm}; - justify-content: center; -`; - -const CloseWrapper = styled.div` - margin-left: ${spacingsPx.xs}; -`; - -const Close = styled.div<{ $elevation: Elevation }>` - border-radius: 100%; - cursor: pointer; - background: ${mapElevationToBackground}; - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - transition: opacity 0.1s; - - :hover { - opacity: 0.7; - } -`; - -const Success = styled.div` - display: flex; - flex: 1; - ${typography.callout} - color: ${({ theme }) => theme.textPrimaryDefault}; - text-align: center; - justify-content: center; -`; - const Step = styled.div<{ $isActive: boolean }>` - width: 18px; - height: 4px; + flex: 1; + height: ${spacingsPx.xxs}; border-radius: ${borders.radii.xxs}; - margin-right: ${spacingsPx.xxs}; background: ${({ theme }) => theme.backgroundNeutralSubdued}; ${({ $isActive }) => @@ -86,10 +21,6 @@ const Step = styled.div<{ $isActive: boolean }>` `} `; -const StyledRotateDeviceImage = styled(RotateDeviceImage)` - height: 34px; -`; - const isStepActive = (index: number, activeStep?: number) => { if (!activeStep) { return false; @@ -114,39 +45,38 @@ export interface ConfirmOnDeviceProps { export const ConfirmOnDeviceContent = ({ title, - steps, + steps = 3, activeStep, onCancel, successText, deviceModelInternal, deviceUnitColor, }: ConfirmOnDeviceProps) => { - const { elevation } = useElevation(); const hasSteps = steps && activeStep !== undefined; - const theme = useTheme(); return ( - <> - - - + + - - {title} + + {title} {successText && hasSteps && activeStep > steps && ( - + {successText} - + )} {hasSteps && activeStep <= steps && ( - + {Array.from(Array(steps).keys()).map((step, index) => ( ))} - + )} - - - - - {onCancel && ( - - - - )} - - - + + + {onCancel && ( + + )} + ); }; diff --git a/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx b/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx index 7c6407a3dc0b..e9f49c0014bd 100644 --- a/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx +++ b/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx @@ -48,7 +48,13 @@ export const RotateDeviceImage = ({ width={animationWidth} /> ) : ( - + )} ); diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx index 6a9b7454a668..0423e483085c 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx @@ -24,7 +24,6 @@ type DeviceStatusProps = { const DeviceWrapper = styled.div<{ $isLowerOpacity: boolean }>` display: flex; - width: 24px; opacity: ${({ $isLowerOpacity }) => $isLowerOpacity && 0.4}; `; @@ -44,7 +43,6 @@ export const DeviceStatus = ({ deviceModel={deviceModel} deviceColor={device?.features?.unit_color} animationHeight="34px" - animationWidth="24px" /> ); From 5913011798f2330058d8f4d9629ef48fed681a8d Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Tue, 10 Dec 2024 07:27:59 +0100 Subject: [PATCH 020/181] feat(utils): add resolveAfter util --- packages/utils/src/index.ts | 1 + packages/utils/src/resolveAfter.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 packages/utils/src/resolveAfter.ts diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 789c79473143..083999cf3bea 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -49,3 +49,4 @@ export * from './throttler'; export * from './extractUrlsFromText'; export * from './isFullPath'; export * from './asciiUtils'; +export * from './resolveAfter'; diff --git a/packages/utils/src/resolveAfter.ts b/packages/utils/src/resolveAfter.ts new file mode 100644 index 000000000000..a3eed1445080 --- /dev/null +++ b/packages/utils/src/resolveAfter.ts @@ -0,0 +1,11 @@ +import { createDeferred } from './createDeferred'; + +export const resolveAfter = (msec: number, value?: T) => { + const { promise, reject, resolve } = createDeferred(); + const timeout = setTimeout(resolve, msec, value); + + return { + promise: promise.finally(() => clearTimeout(timeout)), + reject, + }; +}; From 89fffd0fab2d2ae4555461dc68bbe65324642988 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Tue, 10 Dec 2024 07:28:30 +0100 Subject: [PATCH 021/181] test(utils): test resolveAfter utils --- packages/utils/tests/resolveAfter.test.ts | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/utils/tests/resolveAfter.test.ts diff --git a/packages/utils/tests/resolveAfter.test.ts b/packages/utils/tests/resolveAfter.test.ts new file mode 100644 index 000000000000..e4d9bc53545d --- /dev/null +++ b/packages/utils/tests/resolveAfter.test.ts @@ -0,0 +1,24 @@ +import { resolveAfter } from '../src/resolveAfter'; + +describe('resolveAfter', () => { + jest.useFakeTimers(); + + it('resolves after specified time', async () => { + const { promise } = resolveAfter(200, 'foo'); + + jest.advanceTimersByTime(200); + + await expect(promise).resolves.toBe('foo'); + }); + + it('rejects if the promise is rejected', async () => { + const { promise, reject } = resolveAfter(200); + + // Reject the promise after 100ms + setTimeout(() => reject(new Error('bar')), 100); + + jest.advanceTimersByTime(100); + + await expect(promise).rejects.toThrow('bar'); + }); +}); From 0f8a86379bcb00bce86b888c3f43458e01da19f3 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Ortiz karliatto Date: Tue, 10 Dec 2024 07:29:14 +0100 Subject: [PATCH 022/181] chore(connect): use resolveAfter from @trezor/utilsf --- packages/connect/src/api/composeTransaction.ts | 2 +- packages/connect/src/api/getAccountInfo.ts | 3 ++- packages/connect/src/device/DeviceList.ts | 2 +- packages/connect/src/utils/promiseUtils.ts | 13 ------------- 4 files changed, 4 insertions(+), 16 deletions(-) delete mode 100644 packages/connect/src/utils/promiseUtils.ts diff --git a/packages/connect/src/api/composeTransaction.ts b/packages/connect/src/api/composeTransaction.ts index f72749c222d0..818e6b65b1a2 100644 --- a/packages/connect/src/api/composeTransaction.ts +++ b/packages/connect/src/api/composeTransaction.ts @@ -1,6 +1,7 @@ // origin: https://github.com/trezor/connect/blob/develop/src/js/core/methods/ComposeTransaction.js import { BigNumber } from '@trezor/utils/src/bigNumber'; +import { resolveAfter } from '@trezor/utils/src/resolveAfter'; import type { ComposeOutput, TransactionInputOutputSortingStrategy } from '@trezor/utxo-lib'; import { AbstractMethod } from '../core/AbstractMethod'; @@ -9,7 +10,6 @@ import { UI, createUiMessage } from '../events'; import { Discovery } from './common/Discovery'; import { validateParams, getFirmwareRange } from './common/paramsValidator'; import * as pathUtils from '../utils/pathUtils'; -import { resolveAfter } from '../utils/promiseUtils'; import { formatAmount } from '../utils/formatUtils'; import { getBitcoinNetwork, fixCoinInfoNetwork } from '../data/coinInfo'; import { isBackendSupported, initBlockchain } from '../backend/BlockchainLink'; diff --git a/packages/connect/src/api/getAccountInfo.ts b/packages/connect/src/api/getAccountInfo.ts index b317bb6c7b21..099117af6172 100644 --- a/packages/connect/src/api/getAccountInfo.ts +++ b/packages/connect/src/api/getAccountInfo.ts @@ -1,11 +1,12 @@ // origin: https://github.com/trezor/connect/blob/develop/src/js/core/methods/GetAccountInfo.js +import { resolveAfter } from '@trezor/utils/src/resolveAfter'; + import { AbstractMethod, MethodReturnType, DEFAULT_FIRMWARE_RANGE } from '../core/AbstractMethod'; import { Discovery } from './common/Discovery'; import { validateParams, getFirmwareRange } from './common/paramsValidator'; import { validatePath, getSerializedPath } from '../utils/pathUtils'; import { getAccountLabel, isUtxoBased } from '../utils/accountUtils'; -import { resolveAfter } from '../utils/promiseUtils'; import { getCoinInfo } from '../data/coinInfo'; import { PROTO, ERRORS } from '../constants'; import { UI, createUiMessage } from '../events'; diff --git a/packages/connect/src/device/DeviceList.ts b/packages/connect/src/device/DeviceList.ts index 81e8e34bd7a2..aca31c954dd2 100644 --- a/packages/connect/src/device/DeviceList.ts +++ b/packages/connect/src/device/DeviceList.ts @@ -11,6 +11,7 @@ import { isTransportInstance, } from '@trezor/transport'; import { Descriptor, PathPublic } from '@trezor/transport/src/types'; +import { resolveAfter } from '@trezor/utils/src/resolveAfter'; import { ERRORS } from '../constants'; import { DEVICE, TransportInfo } from '../events'; @@ -23,7 +24,6 @@ import { } from '../types'; import { getBridgeInfo } from '../data/transportInfo'; import { initLog } from '../utils/debug'; -import { resolveAfter } from '../utils/promiseUtils'; import { typedObjectKeys } from '../types/utils'; // custom log diff --git a/packages/connect/src/utils/promiseUtils.ts b/packages/connect/src/utils/promiseUtils.ts deleted file mode 100644 index 9361ecdc93b1..000000000000 --- a/packages/connect/src/utils/promiseUtils.ts +++ /dev/null @@ -1,13 +0,0 @@ -// origin: https://github.com/trezor/connect/blob/develop/src/js/utils/promiseUtils.js - -import { createDeferred } from '@trezor/utils'; - -export const resolveAfter = (msec: number, value?: T) => { - const { promise, reject, resolve } = createDeferred(); - const timeout = setTimeout(resolve, msec, value); - - return { - promise: promise.finally(() => clearTimeout(timeout)), - reject, - }; -}; From 0427f46754bb29ab34ea95e2e31dfa0aeb529654 Mon Sep 17 00:00:00 2001 From: Albina Nikiforova Date: Mon, 16 Dec 2024 15:04:27 +0100 Subject: [PATCH 023/181] feat(suite): contract address error for Ethereum mainnet accounts --- .../src/components/form/BottomText.tsx | 2 +- .../src/components/wallet/Fees/CustomFee.tsx | 2 +- .../src/components/wallet/InputError.tsx | 24 +-- packages/suite/src/support/messages.ts | 8 + .../src/views/wallet/send/Outputs/Address.tsx | 152 +++++++++++------- packages/urls/src/urls.ts | 2 + .../wallet-utils/src/validationUtils.ts | 28 ++++ 7 files changed, 145 insertions(+), 73 deletions(-) diff --git a/packages/components/src/components/form/BottomText.tsx b/packages/components/src/components/form/BottomText.tsx index 63be3fe3e4a2..77f0a950e385 100644 --- a/packages/components/src/components/form/BottomText.tsx +++ b/packages/components/src/components/form/BottomText.tsx @@ -60,7 +60,7 @@ export const BottomText = ({ (iconName && ( ))} - + {children} diff --git a/packages/suite/src/components/wallet/Fees/CustomFee.tsx b/packages/suite/src/components/wallet/Fees/CustomFee.tsx index 80557bce6d36..34acaaa41bee 100644 --- a/packages/suite/src/components/wallet/Fees/CustomFee.tsx +++ b/packages/suite/src/components/wallet/Fees/CustomFee.tsx @@ -166,7 +166,7 @@ export const CustomFee = ({ feeLimitError?.message ? ( ) : null } diff --git a/packages/suite/src/components/wallet/InputError.tsx b/packages/suite/src/components/wallet/InputError.tsx index 3d0c1e717292..31cce366e101 100644 --- a/packages/suite/src/components/wallet/InputError.tsx +++ b/packages/suite/src/components/wallet/InputError.tsx @@ -7,23 +7,23 @@ import { spacings } from '@trezor/theme'; import { LearnMoreButton } from '../suite/LearnMoreButton'; type ButtonProps = { onClick: MouseEventHandler; text: string }; -type LinkProps = { url: Url }; export type InputErrorProps = { - button?: ButtonProps | LinkProps; + buttonProps?: ButtonProps; + learnMoreUrl?: Url; message?: string; }; -export const InputError = ({ button, message }: InputErrorProps) => ( +export const InputError = ({ buttonProps, learnMoreUrl, message }: InputErrorProps) => ( - {message} - {button && - ('url' in button ? ( - - ) : ( - - ))} + + {message} + {learnMoreUrl && } + + {buttonProps?.text && ( + + )} ); diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 789fb20e11ef..57879dbe8cf5 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -2036,6 +2036,14 @@ export default defineMessages({ defaultMessage: 'Unable to verify address history. Check that the address is correct.', id: 'TR_ETH_ADDRESS_CANT_VERIFY_HISTORY', }, + TR_EVM_ADDRESS_IS_CONTRACT: { + defaultMessage: 'You are sending funds to a contract address.', + id: 'TR_EVM_ADDRESS_IS_CONTRACT', + }, + TR_I_UNDERSTAND_THE_RISK: { + defaultMessage: 'I understand', + id: 'TR_I_UNDERSTAND_THE_RISK', + }, TR_NEEDS_ATTENTION_BOOTLOADER: { defaultMessage: 'Trezor is in Bootloader mode.', id: 'TR_NEEDS_ATTENTION_BOOTLOADER', diff --git a/packages/suite/src/views/wallet/send/Outputs/Address.tsx b/packages/suite/src/views/wallet/send/Outputs/Address.tsx index 4b554a0b89dc..a83817606916 100644 --- a/packages/suite/src/views/wallet/send/Outputs/Address.tsx +++ b/packages/suite/src/views/wallet/send/Outputs/Address.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { checkAddressCheckSum, toChecksumAddress } from 'web3-utils'; import styled from 'styled-components'; @@ -16,11 +16,15 @@ import { isTaprootAddress, isBech32AddressUppercase, getInputState, + checkIsAddressNotUsedNotChecksummed, } from '@suite-common/wallet-utils'; import { getNetworkSymbolForProtocol } from '@suite-common/suite-utils'; -import { HELP_CENTER_EVM_ADDRESS_CHECKSUM } from '@trezor/urls'; import { spacings } from '@trezor/theme'; import { CoinLogo } from '@trezor/product-components'; +import { + HELP_CENTER_EVM_ADDRESS_CHECKSUM, + HELP_CENTER_EVM_SEND_TO_CONTRACT_URL, +} from '@trezor/urls'; import { scanOrRequestSendFormThunk } from 'src/actions/wallet/send/sendFormThunks'; import { useSendFormContext } from 'src/hooks/wallet'; @@ -59,6 +63,7 @@ export const Address = ({ output, outputId, outputsCount }: AddressProps) => { const [addressDeprecatedUrl, setAddressDeprecatedUrl] = useState>(undefined); const [hasAddressChecksummed, setHasAddressChecksummed] = useState(); + const contractAddressWarningDismissed = useRef(false); const dispatch = useDispatch(); const { device } = useDevice(); const { @@ -72,6 +77,7 @@ export const Address = ({ output, outputId, outputsCount }: AddressProps) => { metadataEnabled, watch, setDraftSaveRequest, + trigger, } = useSendFormContext(); const { translationString } = useTranslation(); const { descriptor, networkType, symbol } = account; @@ -89,9 +95,14 @@ export const Address = ({ output, outputId, outputsCount }: AddressProps) => { const options = getDefaultValue('options', []); const broadcastEnabled = options.includes('broadcast'); const isOnline = useSelector(state => state.suite.online); + + useEffect(() => { + contractAddressWarningDismissed.current = false; + }, [address]); + const getInputErrorState = () => { - if (hasAddressChecksummed) { - return 'primary'; + if (hasAddressChecksummed && !addressError) { + return 'default'; } if (addressError) { return getInputState(addressError); @@ -158,39 +169,56 @@ export const Address = ({ output, outputId, outputsCount }: AddressProps) => { } }, [amountInputName, composeTransaction, dispatch, inputName, setValue, symbol]); - const getValidationButtonProps = (): InputErrorProps['button'] => { + const getInputErrorProps = (): { + learnMoreUrl?: InputErrorProps['learnMoreUrl']; + buttonProps?: InputErrorProps['buttonProps']; + } => { switch (addressError?.type) { case 'deprecated': - if (addressDeprecatedUrl) { + return { + learnMoreUrl: addressDeprecatedUrl ? URLS[addressDeprecatedUrl] : undefined, + }; + case 'evmchecks': + if (!checkAddressCheckSum(address)) { return { - url: URLS[addressDeprecatedUrl], + buttonProps: { + onClick: () => { + setValue(inputName, toChecksumAddress(address), { + shouldValidate: true, + }); + + setHasAddressChecksummed(true); + }, + text: translationString('TR_CONVERT_TO_CHECKSUM_ADDRESS'), + }, + }; + } + if (!contractAddressWarningDismissed.current) { + return { + buttonProps: { + onClick: () => { + contractAddressWarningDismissed.current = true; + trigger(inputName); + }, + text: translationString('TR_I_UNDERSTAND_THE_RISK'), + }, + learnMoreUrl: HELP_CENTER_EVM_SEND_TO_CONTRACT_URL, }; } - return undefined; - - case 'checksum': - return { - onClick: () => { - setValue(inputName, toChecksumAddress(address), { - shouldValidate: true, - }); - - setHasAddressChecksummed(true); - }, - text: translationString('TR_CONVERT_TO_CHECKSUM_ADDRESS'), - }; - + return {}; case 'uppercase': return { - onClick: () => - setValue(inputName, address.toLowerCase(), { - shouldValidate: true, - }), - text: translationString('TR_CONVERT_TO_LOWERCASE'), + buttonProps: { + onClick: () => + setValue(inputName, address.toLowerCase(), { + shouldValidate: true, + }), + text: translationString('TR_CONVERT_TO_LOWERCASE'), + }, }; default: - return undefined; + return {}; } }; @@ -230,38 +258,46 @@ export const Address = ({ output, outputId, outputsCount }: AddressProps) => { return translationString('RECIPIENT_IS_NOT_VALID'); } }, - // Eth addresses are valid without checksum but Trezor displays them as checksummed. - checksum: async (address: string) => { - if (networkType === 'ethereum' && !checkAddressCheckSum(address)) { - if (isOnline) { - const params = { - descriptor: address, - coin: symbol, - }; - // 1. If the address is used but unchecksummed, then Suite will automatically - // convert the address to the correct checksummed form and inform the user as described in the OP. - const result = await TrezorConnect.getAccountInfo(params); - - if (result.success) { - const hasHistory = result.payload.history.total !== 0; - if (hasHistory) { - setValue(inputName, toChecksumAddress(address), { - shouldValidate: true, - }); - setHasAddressChecksummed(true); + evmchecks: async (address: string) => { + if (networkType === 'ethereum') { + if (!isOnline) { + return translationString('TR_ETH_ADDRESS_CANT_VERIFY_HISTORY'); + } + const params = { + descriptor: address, + coin: symbol, + }; + const result = await TrezorConnect.getAccountInfo(params); - return; - } + if (!result.success) { + return translationString('TR_ETH_ADDRESS_CANT_VERIFY_HISTORY'); + } - // 2. If the address is not checksummed at all and not found in blockbook - // offer to checksum it with a button. - if (!hasHistory && address === address.toLowerCase()) { - return translationString('TR_ETH_ADDRESS_NOT_USED_NOT_CHECKSUMMED'); - } + const { payload } = result; + + // 1. Validate address checksum. + // Eth addresses are valid without checksum but Trezor displays them as checksummed. + if (!checkAddressCheckSum(address)) { + const checksumAndUsageValidationResult = + checkIsAddressNotUsedNotChecksummed( + address, + payload.history, + inputName, + setValue, + setHasAddressChecksummed, + ); + if (checksumAndUsageValidationResult) { + return translationString('TR_ETH_ADDRESS_NOT_USED_NOT_CHECKSUMMED'); } } - return translationString('TR_ETH_ADDRESS_CANT_VERIFY_HISTORY'); + //2. Check if address is a contract address + if (!contractAddressWarningDismissed.current && symbol === 'eth') { + const isContract = payload.misc?.contractInfo; + if (isContract) { + return translationString('TR_EVM_ADDRESS_IS_CONTRACT'); + } + } } }, rippleToSelf: (value: string) => { @@ -284,7 +320,7 @@ export const Address = ({ output, outputId, outputsCount }: AddressProps) => { const addressBottomText = isAddressWithLabel ? addressLabelComponent : null; const getBottomText = () => { - if (hasAddressChecksummed) { + if (hasAddressChecksummed && !addressError) { return ( { ); } if (addressError) { - return ( - - ); + return ; } return addressBottomText; }; const getBottomTextIconComponent = () => { - if (hasAddressChecksummed) { + if (hasAddressChecksummed && !addressError) { return ; } diff --git a/packages/urls/src/urls.ts b/packages/urls/src/urls.ts index ecf0998b97f6..308cbddf7d6a 100644 --- a/packages/urls/src/urls.ts +++ b/packages/urls/src/urls.ts @@ -107,6 +107,8 @@ export const HELP_CENTER_TRANSACTION_FEES_URL: Url = 'https://trezor.io/learn/a/transaction-fees-in-trezor-suite'; export const HELP_CENTER_EVM_ADDRESS_CHECKSUM: Url = 'https://trezor.io/learn/a/evm-address-checksum-in-trezor-suite'; +export const HELP_CENTER_EVM_SEND_TO_CONTRACT_URL = + 'https://trezor.io/support/a/where-is-my-ethereum'; export const HELP_CENTER_FIRMWARE_REVISION_CHECK: Url = 'https://trezor.io/learn/a/trezor-firmware-revision-check'; export const HELP_CENTER_REPLACE_BY_FEE: Url = diff --git a/suite-common/wallet-utils/src/validationUtils.ts b/suite-common/wallet-utils/src/validationUtils.ts index 5d5df5f59eb2..428d4952366a 100644 --- a/suite-common/wallet-utils/src/validationUtils.ts +++ b/suite-common/wallet-utils/src/validationUtils.ts @@ -1,6 +1,11 @@ +import { UseFormSetValue } from 'react-hook-form'; + +import { toChecksumAddress } from 'web3-utils'; + import addressValidator from '@trezor/address-validator'; import { getTestnetSymbols } from '@suite-common/wallet-config'; import { Account } from '@suite-common/wallet-types'; +import { AccountInfo } from '@trezor/blockchain-link-types'; const getNetworkType = (symbol: Account['symbol']) => { if (symbol === 'regtest') return symbol; @@ -87,3 +92,26 @@ export const isHexValid = (value: string, prefix?: string) => { return true; }; + +export const checkIsAddressNotUsedNotChecksummed = ( + address: string, + history: AccountInfo['history'], + inputName: string, + setValue: UseFormSetValue, + setHasAddressChecksummed: (value: boolean) => void, +) => { + const hasHistory = history.total !== 0; + + if (hasHistory) { + setValue(inputName, toChecksumAddress(address), { shouldValidate: true }); + setHasAddressChecksummed(true); + + return false; + } + + if (!hasHistory && address === address.toLowerCase()) { + return true; + } + + return false; +}; From cfb7ac2b17f7fdecfc22f06b88fa124bca516cbd Mon Sep 17 00:00:00 2001 From: Petr Knetl Date: Mon, 16 Dec 2024 09:45:11 +0100 Subject: [PATCH 024/181] fix(suite-native): ios e2e tests --- .../app/e2e/pageObjects/onboardingActions.ts | 4 +- .../app/e2e/tests/accountManagement.test.ts | 4 +- .../e2e/tests/bitcoinAccountsImport.test.ts | 15 +++-- .../app/e2e/tests/onboardAndConnect.test.ts | 60 ++++++++++--------- suite-native/app/e2e/utils.ts | 7 ++- .../feature-flags/src/featureFlagsSlice.ts | 2 +- 6 files changed, 54 insertions(+), 38 deletions(-) diff --git a/suite-native/app/e2e/pageObjects/onboardingActions.ts b/suite-native/app/e2e/pageObjects/onboardingActions.ts index c16b779fb3aa..a60d953d08ff 100644 --- a/suite-native/app/e2e/pageObjects/onboardingActions.ts +++ b/suite-native/app/e2e/pageObjects/onboardingActions.ts @@ -1,5 +1,7 @@ import { expect as detoxExpect } from 'detox'; +import { onAlertSheet } from './alertSheetActions'; + const platform = device.getPlatform(); class OnOnboardingActions { @@ -26,7 +28,7 @@ class OnOnboardingActions { await element(by.id('@onboarding/UserDataConsent/allow')).tap(); try { - await element(by.id('reject-biometrics')).tap(); + await onAlertSheet.tapSecondaryButton(); } catch { // Android emulator does not support biometrics, so the sheet is not displayed at all. console.warn( diff --git a/suite-native/app/e2e/tests/accountManagement.test.ts b/suite-native/app/e2e/tests/accountManagement.test.ts index f62a2f1d3e99..304c8e40649f 100644 --- a/suite-native/app/e2e/tests/accountManagement.test.ts +++ b/suite-native/app/e2e/tests/accountManagement.test.ts @@ -42,14 +42,14 @@ describe('Account management', () => { }); it('Import account and remove it', async () => { - const accountName = 'BTC Taproot'; + const accountName = 'BTC Legacy SegWit'; await onTabBar.navigateToMyAssets(); await onMyAssets.addAccount(); await onAccountImport.importAccount({ networkSymbol: 'btc', - xpub: xpubs.btc.taproot, + xpub: xpubs.btc.legacySegwit, accountName, }); diff --git a/suite-native/app/e2e/tests/bitcoinAccountsImport.test.ts b/suite-native/app/e2e/tests/bitcoinAccountsImport.test.ts index 95b813af3b02..76593a899cf9 100644 --- a/suite-native/app/e2e/tests/bitcoinAccountsImport.test.ts +++ b/suite-native/app/e2e/tests/bitcoinAccountsImport.test.ts @@ -32,13 +32,16 @@ describe('Import Bitcoin network accounts.', () => { }); }); - it('Import BTC Taproot account', async () => { - await onAccountImport.importAccount({ - networkSymbol: 'btc', - xpub: xpubs.btc.taproot, - accountName: 'BTC Taproot', + // This test is skipped for iOS, because detox runner is unable to input single quote (') correctly and inputs quotation mark (’) instead. + // Since the quotation mark is invalid character in terms of taproot xpub, the test always fails on iOS. + if (device.getPlatform() !== 'ios') + it('Import BTC Taproot account', async () => { + await onAccountImport.importAccount({ + networkSymbol: 'btc', + xpub: xpubs.btc.taproot, + accountName: 'BTC Taproot', + }); }); - }); it('Import BTC Legacy account', async () => { await onAccountImport.importAccount({ diff --git a/suite-native/app/e2e/tests/onboardAndConnect.test.ts b/suite-native/app/e2e/tests/onboardAndConnect.test.ts index 389adc0d1755..90582d040897 100644 --- a/suite-native/app/e2e/tests/onboardAndConnect.test.ts +++ b/suite-native/app/e2e/tests/onboardAndConnect.test.ts @@ -1,6 +1,8 @@ // `expect` keyword is already used by jest. import { expect as detoxExpect } from 'detox'; +import { conditionalDescribe } from '@suite-common/test-utils'; + import { onAlertSheet } from '../pageObjects/alertSheetActions'; import { disconnectTrezorUserEnv, openApp, prepareTrezorEmulator } from '../utils'; import { onOnboarding } from '../pageObjects/onboardingActions'; @@ -8,39 +10,43 @@ import { onCoinEnablingInit } from '../pageObjects/coinEnablingActions'; const platform = device.getPlatform(); -describe('Go through onboarding and connect Trezor.', () => { - beforeAll(async () => { - await prepareTrezorEmulator(); +conditionalDescribe( + device.getPlatform() !== 'android', + 'Go through onboarding and connect Trezor.', + () => { + beforeAll(async () => { + await prepareTrezorEmulator(); - await openApp({ newInstance: true }); - }); + await openApp({ newInstance: true }); + }); - afterAll(async () => { - disconnectTrezorUserEnv(); - await device.terminateApp(); - }); + afterAll(async () => { + disconnectTrezorUserEnv(); + await device.terminateApp(); + }); - it('Navigate to dashboard', async () => { - await onOnboarding.finishOnboarding(); + it('Navigate to dashboard', async () => { + await onOnboarding.finishOnboarding(); - if (platform === 'android') { - await waitFor(element(by.id('@screen/CoinEnablingInit'))) - .toBeVisible() - .withTimeout(10000); + if (platform === 'android') { + await waitFor(element(by.id('@screen/CoinEnablingInit'))) + .toBeVisible() + .withTimeout(10000); - await onCoinEnablingInit.waitForScreen(); + await onCoinEnablingInit.waitForScreen(); - await onCoinEnablingInit.enableNetwork('btc'); - await onCoinEnablingInit.enableNetwork('eth'); + await onCoinEnablingInit.enableNetwork('btc'); + await onCoinEnablingInit.enableNetwork('eth'); - await onCoinEnablingInit.clickOnConfirmButton(); + await onCoinEnablingInit.clickOnConfirmButton(); - await onAlertSheet.skipViewOnlyMode(); + await onAlertSheet.skipViewOnlyMode(); - await detoxExpect(element(by.id('@home/portfolio/header'))); - } else { - await detoxExpect(element(by.text('Hi there!'))).toBeVisible(); - await detoxExpect(element(by.text('Get started'))).toBeVisible(); - } - }); -}); + await detoxExpect(element(by.id('@home/portfolio/header'))); + } else { + await detoxExpect(element(by.text('Hi there!'))).toBeVisible(); + await detoxExpect(element(by.text('Get started'))).toBeVisible(); + } + }); + }, +); diff --git a/suite-native/app/e2e/utils.ts b/suite-native/app/e2e/utils.ts index b7091bf13c3c..aac0dc6cfdc7 100644 --- a/suite-native/app/e2e/utils.ts +++ b/suite-native/app/e2e/utils.ts @@ -3,14 +3,19 @@ import { expect as detoxExpect } from 'detox'; import { MNEMONICS, TrezorUserEnvLink } from '@trezor/trezor-user-env-link'; +const platform = device.getPlatform(); + const APP_LAUNCH_ARGS = { // Do not synchronize communication with the trezor bridge and metro server running on localhost. Since the trezor // bridge is exchanging messages with the app all the time, the test runner would wait forever otherwise. detoxURLBlacklistRegex: '\\("^.*127.0.0.1.*",".*localhost.*","^*clients3\\.google\\.com*"\\)', + + // Main loop synchronization is infinitely blocking iOS tests while is the graph displayed, so we need to disable it. + // Not sure about the cause of it yet. + DTXDisableMainRunLoopSync: platform === 'ios', }; const TREZOR_DEVICE_LABEL = 'Trezor T - Tester'; -const platform = device.getPlatform(); export const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)); diff --git a/suite-native/feature-flags/src/featureFlagsSlice.ts b/suite-native/feature-flags/src/featureFlagsSlice.ts index 551d2b6a91fd..abe3a98869d0 100644 --- a/suite-native/feature-flags/src/featureFlagsSlice.ts +++ b/suite-native/feature-flags/src/featureFlagsSlice.ts @@ -20,7 +20,7 @@ export type FeatureFlagsRootState = { }; export const featureFlagsInitialState: FeatureFlagsState = { - [FeatureFlag.IsDeviceConnectEnabled]: isAndroid() || isDebugEnv(), + [FeatureFlag.IsDeviceConnectEnabled]: isAndroid() || (isDebugEnv() && !isDetoxTestBuild()), [FeatureFlag.IsCardanoSendEnabled]: isAndroid() && isDevelopOrDebugEnv(), [FeatureFlag.IsRegtestEnabled]: isDebugEnv() || isDetoxTestBuild(), [FeatureFlag.IsSolanaEnabled]: false, From 9055e1ea2bdc3ec8fb3e97eaee863a3286635418 Mon Sep 17 00:00:00 2001 From: Petr Knetl Date: Mon, 16 Dec 2024 09:50:47 +0100 Subject: [PATCH 025/181] refactor(suite-native): conditional jest test condition negated --- suite-common/test-utils/src/conditionalDescribe.ts | 12 ++++-------- suite-native/app/e2e/tests/deeplinkPopup.test.ts | 2 +- suite-native/app/e2e/tests/deviceSettings.test.ts | 2 +- suite-native/app/e2e/tests/onboardAndConnect.test.ts | 2 +- suite-native/app/e2e/tests/send.test.ts | 2 +- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/suite-common/test-utils/src/conditionalDescribe.ts b/suite-common/test-utils/src/conditionalDescribe.ts index ee70ed3fa6ce..a7e98be17277 100644 --- a/suite-common/test-utils/src/conditionalDescribe.ts +++ b/suite-common/test-utils/src/conditionalDescribe.ts @@ -1,11 +1,7 @@ -export const conditionalDescribe = ( - skipCondition: boolean, - title: string, - fn: jest.EmptyFunction, -) => { - if (skipCondition) { - describe.skip(title, fn); - } else { +export const conditionalDescribe = (condition: boolean, title: string, fn: jest.EmptyFunction) => { + if (condition) { describe(title, fn); + } else { + describe.skip(title, fn); } }; diff --git a/suite-native/app/e2e/tests/deeplinkPopup.test.ts b/suite-native/app/e2e/tests/deeplinkPopup.test.ts index e2c264b98ff7..9f54024938e6 100644 --- a/suite-native/app/e2e/tests/deeplinkPopup.test.ts +++ b/suite-native/app/e2e/tests/deeplinkPopup.test.ts @@ -38,7 +38,7 @@ const openUriScheme = (url: string, platformToOpen: 'android') => { }); }; -conditionalDescribe(device.getPlatform() !== 'android', 'Deeplink connect popup.', () => { +conditionalDescribe(device.getPlatform() === 'android', 'Deeplink connect popup.', () => { beforeAll(async () => { await new Promise(resolve => { server = http.createServer((req, res) => { diff --git a/suite-native/app/e2e/tests/deviceSettings.test.ts b/suite-native/app/e2e/tests/deviceSettings.test.ts index 161c71aa8306..f916fdd51090 100644 --- a/suite-native/app/e2e/tests/deviceSettings.test.ts +++ b/suite-native/app/e2e/tests/deviceSettings.test.ts @@ -16,7 +16,7 @@ import { restartApp, } from '../utils'; -conditionalDescribe(device.getPlatform() !== 'android', 'Device settings', () => { +conditionalDescribe(device.getPlatform() === 'android', 'Device settings', () => { beforeAll(async () => { await prepareTrezorEmulator(); await openApp({ newInstance: true }); diff --git a/suite-native/app/e2e/tests/onboardAndConnect.test.ts b/suite-native/app/e2e/tests/onboardAndConnect.test.ts index 90582d040897..05f1558e9795 100644 --- a/suite-native/app/e2e/tests/onboardAndConnect.test.ts +++ b/suite-native/app/e2e/tests/onboardAndConnect.test.ts @@ -11,7 +11,7 @@ import { onCoinEnablingInit } from '../pageObjects/coinEnablingActions'; const platform = device.getPlatform(); conditionalDescribe( - device.getPlatform() !== 'android', + device.getPlatform() === 'android', 'Go through onboarding and connect Trezor.', () => { beforeAll(async () => { diff --git a/suite-native/app/e2e/tests/send.test.ts b/suite-native/app/e2e/tests/send.test.ts index 91a42cb342b5..fc781b311a5f 100644 --- a/suite-native/app/e2e/tests/send.test.ts +++ b/suite-native/app/e2e/tests/send.test.ts @@ -66,7 +66,7 @@ const signTransactionAndSendIt = async () => { await onSendOutputsReview.clickSendTransaction(); }; -conditionalDescribe(device.getPlatform() !== 'android', 'Send transaction flow.', () => { +conditionalDescribe(device.getPlatform() === 'android', 'Send transaction flow.', () => { beforeAll(async () => { await prepareTrezorEmulator(); await openApp({ newInstance: true }); From a2d37657d419bb6d1cfafc9a148aced5125ada36 Mon Sep 17 00:00:00 2001 From: Matej Kriz Date: Mon, 16 Dec 2024 10:52:25 +0100 Subject: [PATCH 026/181] chore(ci): temporary workaround to fix EAS builds - https://github.com/trezor/trezor-suite/pull/14690 introduced dependency on @trezor/suite-desktop-api in @suite-common/connect-init - The ultimate solution would be to split that logic to a new package and remove the dependency on desktop-api from connect-init --- .easignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.easignore b/.easignore index cf5bf1eca0e6..6c8abd06ca81 100644 --- a/.easignore +++ b/.easignore @@ -8,7 +8,6 @@ packages/suite-analytics/ packages/suite-build/ packages/suite-data/ packages/suite-desktop/ -packages/suite-desktop-api/ packages/suite-desktop-core/ packages/suite-desktop-ui/ packages/suite-storage/ From fb4ae53b3da6d99d400a1de2384552e89dce22be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20H=C3=A1jek?= Date: Tue, 17 Dec 2024 12:56:39 +0100 Subject: [PATCH 027/181] ci: Added workflow to check project assignment (#15911) --- .../workflows/check-project-assignment.yml | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/check-project-assignment.yml diff --git a/.github/workflows/check-project-assignment.yml b/.github/workflows/check-project-assignment.yml new file mode 100644 index 000000000000..72856f1d12bf --- /dev/null +++ b/.github/workflows/check-project-assignment.yml @@ -0,0 +1,61 @@ +name: "[Check] Project/Issue Assignment" + +on: + pull_request: + types: + - opened + - ready_for_review + - labeled + - synchronize + +jobs: + check-project-or-issue: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check if PR is assigned to a project or an issue + env: + GITHUB_TOKEN: ${{ secrets.TREZOR_BOT_TOKEN }} + run: | + # Fetch PR labels + PR_LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') + + # Check for "no-project" label + if echo "$PR_LABELS" | grep -q "^no-project$"; then + echo "Pass: The PR has the 'no-project' label." + exit 0 + fi + + # Check for linked issues using GraphQL + LINKED_ISSUES=$(gh api graphql -f query=' + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + closingIssuesReferences(first: 10) { + nodes { + id + } + } + } + } + }' -F owner=${{ github.repository_owner }} -F repo=${{ github.event.repository.name }} -F number=${{ github.event.pull_request.number }} --jq '.data.repository.pullRequest.closingIssuesReferences.nodes | length') + + if [ "$LINKED_ISSUES" -gt 0 ]; then + echo "Pass: The PR is linked to $LINKED_ISSUES issue(s)." + exit 0 + fi + + # Check for associated projects + PROJECT_COUNT=$(gh pr view ${{ github.event.pull_request.number }} --json projectItems --jq '.projectItems | length') + + if [ "$PROJECT_COUNT" -gt 0 ]; then + echo "Pass: This PR is assigned to a project." + exit 0 + fi + + # If no condition passes + echo "Error: This PR is not assigned to any project, not linked to a valid issue, and does not have the 'no-project' label." + echo "Please assign the PR to a project or link it to an issue. Alternatively, add the 'no-project' label if not applicable." + exit 1 From b03b6c346123586ff52ef036b9135f6094cab3bf Mon Sep 17 00:00:00 2001 From: yanas Date: Sat, 14 Dec 2024 16:30:04 +0100 Subject: [PATCH 028/181] feat(suite-native): use filled icons for focused items in tab bar --- suite-common/icons/generateIconFont.ts | 3 + .../iconFontsMobile/TrezorSuiteIcons.json | 102 +++++++++--------- .../iconFontsMobile/TrezorSuiteIcons.ttf | Bin 20796 -> 21468 bytes suite-native/app/app.config.ts | 2 +- .../app/src/navigation/enhanceTabOption.ts | 3 + suite-native/app/src/navigation/routes.ts | 4 + .../navigation/src/components/TabBar.tsx | 4 +- .../navigation/src/components/TabBarItem.tsx | 12 ++- suite-native/navigation/src/types.tsx | 1 + 9 files changed, 78 insertions(+), 53 deletions(-) diff --git a/suite-common/icons/generateIconFont.ts b/suite-common/icons/generateIconFont.ts index 88f67015002a..e858556d750d 100644 --- a/suite-common/icons/generateIconFont.ts +++ b/suite-common/icons/generateIconFont.ts @@ -51,6 +51,7 @@ const usedIcons = [ 'database', 'detective', 'discover', + 'discoverFilled', 'eye', 'eyeSlash', 'facebookLogo', @@ -60,9 +61,11 @@ const usedIcons = [ 'flag', 'flagCheckered', 'gear', + 'gearFilled', 'githubLogo', 'handPalm', 'house', + 'houseFilled', 'image', 'info', 'lifebuoy', diff --git a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json index 9a8aae0bf23c..2a8841a2ad40 100644 --- a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json +++ b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json @@ -44,53 +44,57 @@ "lifebuoy": 61739, "info": 61740, "image": 61741, - "house": 61742, - "handPalm": 61743, - "githubLogo": 61744, - "gear": 61745, - "flagCheckered": 61746, - "flag": 61747, - "fingerprintSimple": 61748, - "fingerprint": 61749, - "filePdf": 61750, - "facebookLogo": 61751, - "eyeSlash": 61752, - "eye": 61753, - "discover": 61754, - "detective": 61755, - "database": 61756, - "cpu": 61757, - "copy": 61758, - "coins": 61759, - "coinVerticalCheck": 61760, - "clockClockwise": 61761, - "circleDashed": 61762, - "checks": 61763, - "checkCircleFilled": 61764, - "checkCircle": 61765, - "check": 61766, - "chatCircle": 61767, - "change": 61768, - "caretUpFilled": 61769, - "caretUpDown": 61770, - "caretUp": 61771, - "caretRight": 61772, - "caretLeft": 61773, - "caretDownFilled": 61774, - "caretDown": 61775, - "caretCircleRight": 61776, - "calendar": 61777, - "bugBeetle": 61778, - "bookmarkSimple": 61779, - "backspace": 61780, - "arrowsCounterClockwise": 61781, - "arrowUpRight": 61782, - "arrowUp": 61783, - "arrowURightDown": 61784, - "arrowSquareOut": 61785, - "arrowRight": 61786, - "arrowLineUpRight": 61787, - "arrowLineUp": 61788, - "arrowLineDown": 61789, - "arrowDown": 61790 + "houseFilled": 61742, + "house": 61743, + "handPalm": 61744, + "githubLogo": 61745, + "gearFilled": 61746, + "gear": 61747, + "flagCheckered": 61748, + "flag": 61749, + "fingerprintSimple": 61750, + "fingerprint": 61751, + "filePdf": 61752, + "facebookLogo": 61753, + "eyeSlash": 61754, + "eye": 61755, + "discoverFilled": 61756, + "discover": 61757, + "detective": 61758, + "database": 61759, + "cpu": 61760, + "copy": 61761, + "coins": 61762, + "coinVerticalCheck": 61763, + "clockClockwise": 61764, + "circleDashed": 61765, + "checks": 61766, + "checkCircleFilled": 61767, + "checkCircle": 61768, + "check": 61769, + "chatCircle": 61770, + "change": 61771, + "caretUpFilled": 61772, + "caretUpDown": 61773, + "caretUp": 61774, + "caretRight": 61775, + "caretLeft": 61776, + "caretDownFilled": 61777, + "caretDown": 61778, + "caretCircleRight": 61779, + "calendar": 61780, + "bugBeetle": 61781, + "bookmarkSimple": 61782, + "backspace": 61783, + "arrowsCounterClockwise": 61784, + "arrowUpRight": 61785, + "arrowUp": 61786, + "arrowURightDown": 61787, + "arrowSquareOut": 61788, + "arrowRight": 61789, + "arrowLineUpRight": 61790, + "arrowLineUp": 61791, + "arrowLineDownFilled": 61792, + "arrowLineDown": 61793, + "arrowDown": 61794 } diff --git a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.ttf b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.ttf index c752a59087745b27568609c4df8327fdb5d536de..6c7d41c0222857d81b2ac96ff9e3b97422f3af17 100644 GIT binary patch delta 922 zcmXw1U1(ER9RL6CJ@=zGYf_W+rY1M{#zb$iP2)_mtkBkNu8PCfTCt;!cJ0lFO-4RS zTWcL-qF^V(fqI33Pf`@64>GyK2kH8tFCticopi&&o`zWZS{Ux1raR~S&j0Ioe&>U; zvV`mR@p|myj{tZL048%4YkK7jcN>6T01#KF$`{TiHy+&ra8dcQ{$jz(kKVd7NWmLI zrbr2H6t9!NLfBiZ%q`3ue*d2I-qtnCwVXBh$M`z{rJL%{E7rm^&cj2hwNV_YT9v~6 zzkPkak2_&tH9uI5@OUzY%^=$v~#08#c|b+v7pWNF7S) zdVP8(C3+K*ws-Nt->4Nyi0Q zI4&`EnwR$*dhXy$DFNS&gYTpm6%4lI0bC`E}2Vdhs zSFv_(ws5pmE*J8`wl5d0YW}2Et|(Kbx#GF(Ky9j~Ockt|2Fd<#Xw>7*mu7Rd`NB@l bxjOVS`hC{SOzr$Ysaoi-ov-e&jp5LLs(jFy delta 441 zcmcb!oN>=0#tF*xZ43;IJq!#C3CX#M1$y@xw=gg;XD~3ZwWa4&rU`U#?qy)$+Q7hI zZ<3Lkm=ZB%fiF<)10Xib017Zgfc5SH@>Mc&ODb|F{r?X%?+p;w zNO?%LNL`S6C9NdwB%L5#Cp}Gii}VkfCRrv~E!i^JdvYvtI&vv;tK=@pbI9Atm&niB z#N8{m`J-kF)8<@VAFj>37Fxoar+d3G1HE|9Z{Ox6{cLhpKULP2-SuAKe { slug: appSlugs[buildType], owner: appOwners[buildType], version: suiteNativeVersion, - runtimeVersion: '20', + runtimeVersion: '21', ...(buildType === 'production' ? {} : { diff --git a/suite-native/app/src/navigation/enhanceTabOption.ts b/suite-native/app/src/navigation/enhanceTabOption.ts index 47d7ec9b0e35..3bd2059c6384 100644 --- a/suite-native/app/src/navigation/enhanceTabOption.ts +++ b/suite-native/app/src/navigation/enhanceTabOption.ts @@ -4,6 +4,7 @@ import { AppTabsParamList } from '@suite-native/navigation'; type TabOption = { routeName: RouteName; iconName: IconName; + focusedIconName: IconName; label: string; params?: ParamList[RouteName]; }; @@ -14,12 +15,14 @@ export const enhanceTabOption = < >({ routeName, iconName, + focusedIconName, label, params, }: TabOption) => ({ [routeName]: { routeName, iconName, + focusedIconName, label, params, }, diff --git a/suite-native/app/src/navigation/routes.ts b/suite-native/app/src/navigation/routes.ts index c60ab2659480..00da16f02a6e 100644 --- a/suite-native/app/src/navigation/routes.ts +++ b/suite-native/app/src/navigation/routes.ts @@ -5,12 +5,14 @@ import { enhanceTabOption } from './enhanceTabOption'; const homeStack = enhanceTabOption({ routeName: AppTabsRoutes.HomeStack, iconName: 'house', + focusedIconName: 'houseFilled', label: 'Home', }); const accountsStack = enhanceTabOption({ routeName: AppTabsRoutes.AccountsStack, iconName: 'discover', + focusedIconName: 'discoverFilled', label: 'My assets', params: { screen: AccountsStackRoutes.Accounts, @@ -21,11 +23,13 @@ const receiveStack = enhanceTabOption({ routeName: AppTabsRoutes.ReceiveStack, label: 'Receive', iconName: 'arrowLineDown', + focusedIconName: 'arrowLineDown', }); const settingsStack = enhanceTabOption({ routeName: AppTabsRoutes.SettingsStack, iconName: 'gear', + focusedIconName: 'gearFilled', label: 'Settings', }); diff --git a/suite-native/navigation/src/components/TabBar.tsx b/suite-native/navigation/src/components/TabBar.tsx index 5a65d00e9f60..983837bd85b1 100644 --- a/suite-native/navigation/src/components/TabBar.tsx +++ b/suite-native/navigation/src/components/TabBar.tsx @@ -43,7 +43,8 @@ export const TabBar = ({ state, navigation, tabItemOptions }: TabBarProps) => { > {state.routes.map((route, index) => { const isFocused = state.index === index; - const { routeName, iconName, label, params } = tabItemOptions[route.name]; + const { routeName, iconName, focusedIconName, label, params } = + tabItemOptions[route.name]; const handleTabBarItemPress = () => { const event = navigation.emit({ @@ -62,6 +63,7 @@ export const TabBar = ({ state, navigation, tabItemOptions }: TabBarProps) => { key={route.key} isFocused={isFocused} iconName={iconName} + focusedIconName={focusedIconName} title={label} onPress={handleTabBarItemPress} testID={route.name} diff --git a/suite-native/navigation/src/components/TabBarItem.tsx b/suite-native/navigation/src/components/TabBarItem.tsx index 340dc774f046..87de25db0d54 100644 --- a/suite-native/navigation/src/components/TabBarItem.tsx +++ b/suite-native/navigation/src/components/TabBarItem.tsx @@ -8,6 +8,7 @@ type TabBarItemProps = { isFocused: boolean; onPress: () => void; iconName: IconName; + focusedIconName: IconName; title?: string; testID: string; }; @@ -26,7 +27,14 @@ const tabBarItemContainerStyle = prepareNativeStyle(utils => ({ const TAB_BAR_ITEM_HORIZONTAL_HIT_SLOP = 15; -export const TabBarItem = ({ isFocused, onPress, iconName, title, testID }: TabBarItemProps) => { +export const TabBarItem = ({ + isFocused, + onPress, + iconName, + focusedIconName, + title, + testID, +}: TabBarItemProps) => { const { applyStyle } = useNativeStyles(); return ( @@ -49,7 +57,7 @@ export const TabBarItem = ({ isFocused, onPress, iconName, title, testID }: TabB > diff --git a/suite-native/navigation/src/types.tsx b/suite-native/navigation/src/types.tsx index 5b1fe91bcd8c..2d32b6a022d5 100644 --- a/suite-native/navigation/src/types.tsx +++ b/suite-native/navigation/src/types.tsx @@ -64,6 +64,7 @@ export type TabsOptions = { [routeName: string]: { routeName: string; iconName: IconName; + focusedIconName: IconName; label: string; params?: Record; }; From 4b010cf161183c168648574ce885b4313c95eb22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20H=C3=A1jek?= Date: Tue, 17 Dec 2024 16:37:39 +0100 Subject: [PATCH 029/181] ci: Removed settings group from nightly cypress Suite web tests (#16001) --- .github/workflows/test-suite-web-nightly.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test-suite-web-nightly.yml b/.github/workflows/test-suite-web-nightly.yml index ca6235488d24..6f4014cdd586 100644 --- a/.github/workflows/test-suite-web-nightly.yml +++ b/.github/workflows/test-suite-web-nightly.yml @@ -21,9 +21,6 @@ jobs: - TEST_GROUP: "@group_device-management" CONTAINERS: "trezor-user-env-unix" CYPRESS_USE_TREZOR_USER_ENV_BRIDGE: "1" - - TEST_GROUP: "@group_settings" - CONTAINERS: "trezor-user-env-unix" - CYPRESS_USE_TREZOR_USER_ENV_BRIDGE: "1" - TEST_GROUP: "@group_metadata" CONTAINERS: "trezor-user-env-unix" CYPRESS_USE_TREZOR_USER_ENV_BRIDGE: "1" From c03f9119d001353fcc6e88f1b9fb24b33ba6b7bf Mon Sep 17 00:00:00 2001 From: Marek Polak Date: Tue, 17 Dec 2024 17:20:15 +0100 Subject: [PATCH 030/181] fix(suite): forget draft on token row send --- .../views/wallet/tokens/common/TokensTable/TokenRow.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/suite/src/views/wallet/tokens/common/TokensTable/TokenRow.tsx b/packages/suite/src/views/wallet/tokens/common/TokensTable/TokenRow.tsx index 485d94e95789..f849aa133466 100644 --- a/packages/suite/src/views/wallet/tokens/common/TokensTable/TokenRow.tsx +++ b/packages/suite/src/views/wallet/tokens/common/TokensTable/TokenRow.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { selectSelectedDevice } from '@suite-common/wallet-core'; +import { selectSelectedDevice, sendFormActions } from '@suite-common/wallet-core'; import { Account, TokenAddress } from '@suite-common/wallet-types'; import { Network, getCoingeckoId } from '@suite-common/wallet-config'; import { @@ -494,6 +494,11 @@ export const TokenRow = ({ type: SUITE.SET_SEND_FORM_PREFILL, payload: token.contract, }); + dispatch( + sendFormActions.removeDraft({ + accountKey: account.key, + }), + ); goToWithAnalytics('wallet-send', { params: { symbol: account.symbol, From 7c25b380a52f5514c199877652ec95372388b69a Mon Sep 17 00:00:00 2001 From: Adam Havel Date: Thu, 12 Dec 2024 09:39:25 +0100 Subject: [PATCH 031/181] chore(suite): refactor updates modals --- .../src/components/IconCircle/IconCircle.tsx | 35 ++---- .../src/components/IconCircle/types.tsx | 8 -- .../src/components/IconCircle/utils.tsx | 25 +--- .../components/NewModal/NewModal.stories.tsx | 15 --- .../src/components/NewModal/NewModal.tsx | 21 ++-- .../loaders/ProgressBar/ProgressBar.tsx | 30 ++--- packages/components/src/index.ts | 1 - .../src/support/DesktopUpdater/Available.tsx | 91 ++++---------- .../support/DesktopUpdater/Downloading.tsx | 65 ++++------ .../DesktopUpdater/EarlyAccessDisable.tsx | 66 ++++------- .../DesktopUpdater/EarlyAccessEnable.tsx | 112 ++++++++---------- .../support/DesktopUpdater/JustUpdated.tsx | 36 +++--- .../src/support/DesktopUpdater/Ready.tsx | 33 +++--- .../DesktopUpdater/changelogComponents.tsx | 20 ---- .../suite/TorLoader/TorProgressBar.tsx | 2 +- .../TxDetailModal/BasicTxDetails.tsx | 24 ++-- 16 files changed, 199 insertions(+), 385 deletions(-) delete mode 100644 packages/suite-desktop-ui/src/support/DesktopUpdater/changelogComponents.tsx diff --git a/packages/components/src/components/IconCircle/IconCircle.tsx b/packages/components/src/components/IconCircle/IconCircle.tsx index 96fd3e6672bf..9478d84a00a2 100644 --- a/packages/components/src/components/IconCircle/IconCircle.tsx +++ b/packages/components/src/components/IconCircle/IconCircle.tsx @@ -1,13 +1,8 @@ import styled, { css } from 'styled-components'; -import { ExclusiveColorOrVariant, Icon, IconName, IconSize, getIconSize } from '../Icon/Icon'; +import { Icon, IconName, IconSize, getIconSize } from '../Icon/Icon'; import { TransientProps } from '../../utils/transientProps'; -import { - IconCircleExclusiveColorOrVariant, - IconCircleVariant, - IconCircleColors, - IconCirclePaddingType, -} from './types'; +import { IconCircleVariant, IconCirclePaddingType } from './types'; import { mapVariantToIconBackground, mapVariantToIconBorderColor, @@ -23,12 +18,11 @@ import { export const allowedIconCircleFrameProps = ['margin'] as const satisfies FramePropsKeys[]; type AllowedFrameProps = Pick; -type IconCircleWrapperProps = TransientProps< - IconCircleExclusiveColorOrVariant & AllowedFrameProps -> & { +type IconCircleWrapperProps = TransientProps & { $size: number; $hasBorder: boolean; $paddingType: IconCirclePaddingType; + $variant: IconCircleVariant; }; const IconCircleWrapper = styled.div` @@ -58,26 +52,17 @@ export type IconCircleProps = { size?: IconSize | number; paddingType?: IconCirclePaddingType; hasBorder?: boolean; -} & IconCircleExclusiveColorOrVariant & - AllowedFrameProps; + variant?: IconCircleVariant; +} & AllowedFrameProps; export const IconCircle = ({ name, size = 60, hasBorder = true, paddingType = 'large', - iconColor, - variant, + variant = 'primary', ...rest }: IconCircleProps) => { - const wrapperColorOrVariant: TransientProps = - iconColor === undefined ? { $variant: variant ?? 'primary' } : { $iconColor: iconColor }; - - const iconColorOrVariant: ExclusiveColorOrVariant = - iconColor === undefined - ? { variant: variant ?? 'primary' } - : { color: iconColor.foreground }; - const iconSize = getIconSize(size); const frameProps = pickAndPrepareFrameProps(rest, allowedIconCircleFrameProps); @@ -86,12 +71,12 @@ export const IconCircle = ({ $size={iconSize} $paddingType={paddingType} $hasBorder={hasBorder} - {...wrapperColorOrVariant} + $variant={variant} {...frameProps} > - + ); }; -export type { IconCircleVariant, IconCircleColors }; +export type { IconCircleVariant }; diff --git a/packages/components/src/components/IconCircle/types.tsx b/packages/components/src/components/IconCircle/types.tsx index e480c159cc86..d91dab335b98 100644 --- a/packages/components/src/components/IconCircle/types.tsx +++ b/packages/components/src/components/IconCircle/types.tsx @@ -1,5 +1,3 @@ -import { CSSColor } from '@trezor/theme'; - import { UIVariant, UISize } from '../../config/types'; export const iconCircleVariants = [ @@ -12,11 +10,5 @@ export const iconCircleVariants = [ export type IconCircleVariant = Extract; -export type IconCircleColors = { foreground: CSSColor; background: CSSColor }; - -export type IconCircleExclusiveColorOrVariant = - | { variant?: IconCircleVariant; iconColor?: undefined } - | { variant?: undefined; iconColor?: IconCircleColors }; - export const iconCirclePaddingTypes = ['small', 'medium', 'large'] as const; export type IconCirclePaddingType = Extract; diff --git a/packages/components/src/components/IconCircle/utils.tsx b/packages/components/src/components/IconCircle/utils.tsx index 9d5721ff9498..e5ab966542bd 100644 --- a/packages/components/src/components/IconCircle/utils.tsx +++ b/packages/components/src/components/IconCircle/utils.tsx @@ -2,32 +2,20 @@ import { DefaultTheme } from 'styled-components'; import { Color, CSSColor } from '@trezor/theme'; -import { - IconCircleVariant, - IconCircleExclusiveColorOrVariant, - IconCirclePaddingType, -} from './types'; -import { TransientProps } from '../../utils/transientProps'; +import { IconCircleVariant, IconCirclePaddingType } from './types'; type VariantMapArgs = { theme: DefaultTheme; $hasBorder: boolean; -} & TransientProps; + $variant: IconCircleVariant; +}; type PaddingTypeMap = { $paddingType: IconCirclePaddingType; $size: number; }; -export const mapVariantToIconBorderColor = ({ - $variant, - theme, - $iconColor, -}: VariantMapArgs): CSSColor => { - if ($variant === undefined) { - return $iconColor?.foreground ?? 'transparent'; - } - +export const mapVariantToIconBorderColor = ({ $variant, theme }: VariantMapArgs): CSSColor => { const colorMap: Record = { primary: 'backgroundPrimarySubtleOnElevation0', warning: 'backgroundAlertYellowSubtleOnElevation0', @@ -42,13 +30,8 @@ export const mapVariantToIconBorderColor = ({ export const mapVariantToIconBackground = ({ theme, $hasBorder, - $iconColor, $variant, }: VariantMapArgs): CSSColor => { - if ($variant === undefined) { - return $iconColor?.background ?? 'transparent'; - } - const noBorderColorMap: Record = { primary: 'backgroundPrimarySubtleOnElevation0', warning: 'backgroundAlertYellowSubtleOnElevation0', diff --git a/packages/components/src/components/NewModal/NewModal.stories.tsx b/packages/components/src/components/NewModal/NewModal.stories.tsx index 3da32c7bfcf2..912ea8b357d5 100644 --- a/packages/components/src/components/NewModal/NewModal.stories.tsx +++ b/packages/components/src/components/NewModal/NewModal.stories.tsx @@ -8,7 +8,6 @@ import { NewModalProps, variables, intermediaryTheme, - IconCircle, } from '../../index'; import { newModalVariants, newModalSizes } from './types'; import { getFramePropsStory } from '../../utils/frameProps'; @@ -50,7 +49,6 @@ export const NewModal: StoryObj = { args: { variant: 'primary', iconName: undefined, - iconComponent: undefined, heading: 'Modal heading', description: 'Modal description', children: @@ -69,19 +67,6 @@ export const NewModal: StoryObj = { }, options: [...newModalVariants, undefined], }, - iconComponent: { - options: ['nothing', 'purple'], - mapping: { - nothing: undefined, - purple: ( - - ), - }, - }, size: { control: { type: 'select', diff --git a/packages/components/src/components/NewModal/NewModal.tsx b/packages/components/src/components/NewModal/NewModal.tsx index d130561b60ef..b8afceecaf72 100644 --- a/packages/components/src/components/NewModal/NewModal.tsx +++ b/packages/components/src/components/NewModal/NewModal.tsx @@ -20,7 +20,7 @@ import { ElevationContext, ElevationUp, useElevation } from '../ElevationContext import { useScrollShadow } from '../../utils/useScrollShadow'; import { IconCircle } from '../IconCircle/IconCircle'; import { IconName } from '../Icon/Icon'; -import { Row } from '../Flex/Flex'; +import { Box } from '../Box/Box'; import { NewModalButton } from './NewModalButton'; import { NewModalContext } from './NewModalContext'; import { NewModalBackdrop } from './NewModalBackdrop'; @@ -99,10 +99,6 @@ const Footer = styled.footer` border-top: 1px solid ${({ theme }) => theme.borderElevation0}; `; -type ExclusiveIconNameOrComponent = - | { iconName?: IconName; iconComponent?: undefined } - | { iconName?: undefined; iconComponent?: ReactNode }; - type NewModalProps = AllowedFrameProps & { variant?: NewModalVariant; children?: ReactNode; @@ -114,8 +110,9 @@ type NewModalProps = AllowedFrameProps & { isBackdropCancelable?: boolean; alignment?: NewModalAlignment; size?: NewModalSize; + iconName?: IconName; 'data-testid'?: string; -} & ExclusiveIconNameOrComponent; +}; const InnerNewModalBase = ({ children, @@ -125,7 +122,6 @@ const InnerNewModalBase = ({ description, bottomContent, iconName, - iconComponent, onBackClick, onCancel, isBackdropCancelable, @@ -193,18 +189,15 @@ const InnerNewModalBase = ({ - {(iconComponent || iconName) && ( - - {iconComponent ?? - (iconName && ( - - ))} - + + )} {children} diff --git a/packages/components/src/components/loaders/ProgressBar/ProgressBar.tsx b/packages/components/src/components/loaders/ProgressBar/ProgressBar.tsx index 96b610b273e5..4def1c20ed5b 100644 --- a/packages/components/src/components/loaders/ProgressBar/ProgressBar.tsx +++ b/packages/components/src/components/loaders/ProgressBar/ProgressBar.tsx @@ -1,41 +1,37 @@ import styled from 'styled-components'; -import { borders } from '@trezor/theme'; +import { borders, spacingsPx } from '@trezor/theme'; const Wrapper = styled.div` background: ${({ theme }) => theme.backgroundNeutralSubdued}; width: 100%; border-radius: ${borders.radii.full}; + overflow: hidden; `; type ValueProps = { $max: number; $value: number; - $isRed: boolean; }; -const Value = styled.div.attrs(({ $max, $value }) => ({ - style: { - width: `${(100 / $max) * $value}%`, - }, -}))` - background: ${({ theme, $isRed }) => ($isRed ? theme.borderAlertRed : theme.borderSecondary)}; - height: 5px; +const Value = styled.div` + background: ${({ theme }) => theme.iconPrimaryDefault}; + height: ${spacingsPx.xs}; max-width: 100%; - transition: width 0.5s; + width: 1%; + transform: ${({ $max, $value }) => `scaleX(${(100 / $max) * $value})`}; + transform-origin: left; + transition: transform 0.5s; `; -export interface ProgressBarProps { +export type ProgressBarProps = { max?: number; value: number; - isRed?: boolean; -} +}; -// HTML progress element is not used because styling is browser-dependent (no consistent way to override styles -// from parent component, no straightforward way to add width transition in Firefox) -export const ProgressBar = ({ max = 100, value, isRed = false, ...props }: ProgressBarProps) => ( +export const ProgressBar = ({ max = 100, value, ...props }: ProgressBarProps) => ( - + ); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 2c6b485f642f..29f149d06471 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -52,7 +52,6 @@ export { IconCircle, type IconCircleProps, type IconCircleVariant, - type IconCircleColors, } from './components/IconCircle/IconCircle'; export { InfoSegments, type InfoSegmentsProps } from './components/InfoSegments/InfoSegments'; export { InfoItem, type InfoItemProps } from './components/InfoItem/InfoItem'; diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/Available.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/Available.tsx index 8837ab00e8e4..a3538d94093e 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/Available.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/Available.tsx @@ -1,46 +1,15 @@ -import styled from 'styled-components'; - -import { - Card, - Checkbox, - Column, - Icon, - Markdown, - NewModal, - Paragraph, - Row, - Text, -} from '@trezor/components'; +import { Card, Checkbox, Column, Markdown, NewModal, Paragraph, H4 } from '@trezor/components'; import { desktopApi, UpdateInfo } from '@trezor/suite-desktop-api'; -import { borders, spacings, spacingsPx } from '@trezor/theme'; +import { spacings } from '@trezor/theme'; -import { FormattedDate, Translation } from 'src/components/suite'; +import { Translation } from 'src/components/suite'; import { useDispatch, useSelector } from 'src/hooks/suite'; import { download } from 'src/actions/suite/desktopUpdateActions'; import { selectSuiteFlags } from 'src/reducers/suite/suiteReducer'; import { setFlag } from 'src/actions/suite/suiteActions'; -import { Changelog } from './changelogComponents'; import { getVersionName } from './getVersionName'; -const GreenTag = styled.div` - display: flex; - align-items: center; - gap: ${spacingsPx.xxs}; - border-radius: ${borders.radii.full}; - background-color: ${({ theme }) => theme.backgroundPrimarySubtleOnElevation0}; - padding: ${spacingsPx.xxxs} ${spacingsPx.xs}; -`; - -const NewTag = () => ( - - - - - - -); - interface AvailableProps { onCancel: () => void; latest: UpdateInfo | undefined; @@ -85,47 +54,33 @@ export const Available = ({ onCancel, latest }: AvailableProps) => { } > - -
- - - - - - -
- - + +

+ +

+ + + + {latest?.changelog ? ( {latest?.changelog} ) : ( )} -
- - - {latest?.releaseDate && ( - - - - )} - - - - - - - - -
+ + + + + + ); }; diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/Downloading.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/Downloading.tsx index 475e61bfc522..104115f053f7 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/Downloading.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/Downloading.tsx @@ -1,34 +1,12 @@ import { useEffect, useState } from 'react'; -import styled from 'styled-components'; - import { UpdateProgress } from '@trezor/suite-desktop-api'; import { bytesToHumanReadable } from '@trezor/utils'; -import { H2, NewModal, ProgressBar, variables, Row, Column } from '@trezor/components'; +import { NewModal, ProgressBar, Paragraph, Column, Text, H3 } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { Translation } from 'src/components/suite'; -const DownloadProgress = styled.span` - font-size: 20px; - font-weight: ${variables.FONT_WEIGHT.DEMI_BOLD}; - color: ${({ theme }) => theme.legacy.TYPE_LIGHT_GREY}; -`; - -const ReceivedData = styled.span` - color: ${({ theme }) => theme.legacy.TYPE_GREEN}; -`; - -const TotalData = styled.span` - color: ${({ theme }) => theme.legacy.TYPE_LIGHT_GREY}; -`; - -// eslint-disable-next-line local-rules/no-override-ds-component -const Text = styled(H2)` - color: ${({ theme }) => theme.legacy.TYPE_DARK_GREY}; - font-weight: ${variables.FONT_WEIGHT.MEDIUM}; -`; - interface DownloadingProps { hideWindow: () => void; progress?: UpdateProgress; @@ -52,30 +30,27 @@ export const Downloading = ({ hideWindow, progress }: DownloadingProps) => { } + iconName="download" > - - - {progress?.verifying ? ( - - - {ellipsisArray.filter((_, k) => k < step)} - - ) : ( - <> - - - - - - {bytesToHumanReadable(progress?.transferred || 0)} - - /{bytesToHumanReadable(progress?.total || 0)} - - - )} - - +

+ {progress?.verifying ? ( + <> + + {ellipsisArray.filter((_, k) => k < step)} + + ) : ( + + )} +

+ + + + {bytesToHumanReadable(progress?.transferred || 0)} + + {' / '} + {bytesToHumanReadable(progress?.total || 0)} + ); diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessDisable.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessDisable.tsx index 10ca9f2cec21..fb89e6626018 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessDisable.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessDisable.tsx @@ -1,18 +1,9 @@ import { useCallback, useState } from 'react'; -import { useTheme } from 'styled-components'; - import { SUITE_URL } from '@trezor/urls'; import { analytics, EventType } from '@trezor/suite-analytics'; import { desktopApi } from '@trezor/suite-desktop-api'; -import { - Button, - Column, - NewModal, - Paragraph, - IconCircleColors, - IconCircle, -} from '@trezor/components'; +import { NewModal, Paragraph, H3, Column } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { Translation, TrezorLink } from 'src/components/suite'; @@ -24,8 +15,6 @@ interface EarlyAccessDisableProps { export const EarlyAccessDisable = ({ hideWindow }: EarlyAccessDisableProps) => { const [enabled, setEnabled] = useState(true); - const theme = useTheme(); - const allowPrerelease = useCallback(() => { analytics.report({ type: EventType.SettingsGeneralEarlyAccess, @@ -37,63 +26,58 @@ export const EarlyAccessDisable = ({ hideWindow }: EarlyAccessDisableProps) => { setEnabled(false); }, []); - const purpleModalColorBranding: IconCircleColors = { - foreground: theme.iconAlertPurple, - background: theme.backgroundAlertPurpleSubtleOnElevationNegative, - }; - - const eapIconComponent = ( - - ); - return enabled ? ( } bottomContent={ <> - - + } > - - + +

+ +

+ - - +
) : ( } bottomContent={ <> - + - + } > - - + +

+ +

+ - - +
diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessEnable.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessEnable.tsx index 4b2a33af6188..176c70414a83 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessEnable.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessEnable.tsx @@ -1,19 +1,8 @@ import { useCallback, useState } from 'react'; -import { useTheme } from 'styled-components'; - import { analytics, EventType } from '@trezor/suite-analytics'; import { desktopApi } from '@trezor/suite-desktop-api'; -import { - Button, - Paragraph, - Tooltip, - NewModal, - Card, - Column, - IconCircleColors, - IconCircle, -} from '@trezor/components'; +import { Paragraph, Tooltip, NewModal, H3, Column, Card } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { CheckItem, Translation } from 'src/components/suite'; @@ -26,8 +15,6 @@ export const EarlyAccessEnable = ({ hideWindow }: EarlyAccessEnableProps) => { const [understood, setUnderstood] = useState(false); const [enabled, setEnabled] = useState(false); - const theme = useTheme(); - const allowPrerelease = useCallback(() => { analytics.report({ type: EventType.SettingsGeneralEarlyAccess, @@ -41,78 +28,83 @@ export const EarlyAccessEnable = ({ hideWindow }: EarlyAccessEnableProps) => { const checkForUpdates = useCallback(() => desktopApi.checkForUpdates(true), []); - const purpleModalColorBranding: IconCircleColors = { - foreground: theme.iconAlertPurple, - background: theme.backgroundAlertPurpleSubtleOnElevationNegative, - }; - - const eapIconComponent = ( - - ); - return enabled ? ( } + iconName="eap" + variant="info" onCancel={hideWindow} bottomContent={ <> - - + } > - + +

+ +

+ + + +
) : ( } + iconName="eap" + variant="info" onCancel={hideWindow} bottomContent={ - - } - > - - + + + +
+ + + + } > - - + +

+ +

+ - - +
- - - } - description="" - isChecked={understood} - onClick={() => setUnderstood(!understood)} - /> -
+ + } + description="" + isChecked={understood} + onClick={() => setUnderstood(!understood)} + /> + ); }; diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/JustUpdated.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/JustUpdated.tsx index 7cd285da93c4..01f1702be7cc 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/JustUpdated.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/JustUpdated.tsx @@ -1,12 +1,9 @@ import { useState, useCallback, useEffect } from 'react'; -import { Column, Markdown, NewModal, Paragraph } from '@trezor/components'; -import { spacings } from '@trezor/theme'; +import { Markdown, NewModal, Card } from '@trezor/components'; import { Translation } from 'src/components/suite'; -import { Changelog } from './changelogComponents'; - interface AvailableProps { onCancel: () => void; } @@ -29,14 +26,11 @@ export const JustUpdated = ({ onCancel }: AvailableProps) => { return ( - -
+ } - description={} onCancel={onCancel} bottomContent={ <> @@ -46,15 +40,17 @@ export const JustUpdated = ({ onCancel }: AvailableProps) => { } > - - - {changelog !== null ? ( - {changelog} - ) : ( - - )} - - + } + > + {changelog !== null ? ( + {changelog} + ) : ( + + )} + ); }; diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/Ready.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/Ready.tsx index 7f2d0a92a8ce..f8ac2c403cad 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/Ready.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/Ready.tsx @@ -1,4 +1,4 @@ -import { Button, NewModal, Paragraph, Row } from '@trezor/components'; +import { NewModal, Paragraph, H3, Column } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { Translation } from 'src/components/suite'; @@ -20,25 +20,28 @@ export const Ready = ({ hideWindow }: ReadyProps) => { return ( } onCancel={installOnQuit} + iconName="download" bottomContent={ - - - - + + + + + } > - - - - - - + +

+ +

+ + {' '} + + +
); }; diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/changelogComponents.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/changelogComponents.tsx deleted file mode 100644 index ecc7a2be2b8d..000000000000 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/changelogComponents.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { ReactNode } from 'react'; - -import styled from 'styled-components'; - -import { borders, Elevation, mapElevationToBackground, spacingsPx } from '@trezor/theme'; -import { useElevation } from '@trezor/components'; - -const ChangelogWrapper = styled.div<{ $elevation: Elevation }>` - background-color: ${({ theme, $elevation }) => mapElevationToBackground({ theme, $elevation })}; - border-radius: ${borders.radii.md}; - max-height: 400px; - overflow-y: auto; - padding: ${spacingsPx.md} ${spacingsPx.xl}; -`; - -export const Changelog = ({ children }: { children: ReactNode }) => { - const { elevation } = useElevation(); - - return {children}; -}; diff --git a/packages/suite/src/components/suite/TorLoader/TorProgressBar.tsx b/packages/suite/src/components/suite/TorLoader/TorProgressBar.tsx index 3e617e22492c..6e388a414c64 100644 --- a/packages/suite/src/components/suite/TorLoader/TorProgressBar.tsx +++ b/packages/suite/src/components/suite/TorLoader/TorProgressBar.tsx @@ -123,7 +123,7 @@ export const TorProgressBar = ({ - + {isTorError ? ( diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx index 0723cf992628..ce407ab5b443 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx @@ -1,8 +1,8 @@ -import styled, { useTheme } from 'styled-components'; +import styled from 'styled-components'; import { fromWei } from 'web3-utils'; import { - IconCircle, + Icon, Text, H3, useElevation, @@ -31,10 +31,13 @@ const IconWrapper = styled.div<{ $elevation: Elevation }>` border-radius: ${borders.radii.full}; `; -const NestedIconWrapper = styled.div` +const NestedIconWrapper = styled.div<{ $elevation: Elevation }>` position: absolute; top: -${spacingsPx.xxs}; right: -${spacingsPx.xxs}; + background: ${mapElevationToBorder}; + border-radius: ${borders.radii.full}; + padding: ${spacingsPx.xxxs}; `; const Item = ({ label, iconName, children }: Partial) => ( @@ -67,7 +70,6 @@ export const BasicTxDetails = ({ explorerUrlQueryString, }: BasicTxDetailsProps) => { const { elevation } = useElevation(); - const theme = useTheme(); // all solana txs which are fetched are already confirmed const isConfirmed = confirmations > 0 || tx.solanaSpecific?.status === 'confirmed'; @@ -76,16 +78,10 @@ export const BasicTxDetails = ({ - - + From 50584f40d07607f94d0e19722f7bb90a7d4163be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20V=C3=A1clav=C3=ADk?= Date: Tue, 17 Dec 2024 17:02:02 +0100 Subject: [PATCH 032/181] fix(suite): revert labeling and fix eject wallet --- .../MetadataLabeling/MetadataLabeling.tsx | 1 - .../DeviceItem/WalletInstance.tsx | 190 ++++++++++-------- 2 files changed, 104 insertions(+), 87 deletions(-) diff --git a/packages/suite/src/components/suite/labeling/MetadataLabeling/MetadataLabeling.tsx b/packages/suite/src/components/suite/labeling/MetadataLabeling/MetadataLabeling.tsx index d9c439a5e1ac..fbd9f60bedb4 100644 --- a/packages/suite/src/components/suite/labeling/MetadataLabeling/MetadataLabeling.tsx +++ b/packages/suite/src/components/suite/labeling/MetadataLabeling/MetadataLabeling.tsx @@ -65,7 +65,6 @@ const ActionButton = styled(Button)<{ $isValueVisible?: boolean; $isVisible?: bo margin-left: ${({ $isValueVisible, $isVisible, isLoading }) => $isValueVisible || !$isVisible || isLoading ? '12px' : '4px'}; visibility: ${({ $isVisible }) => ($isVisible ? 'visible' : 'hidden')}; - width: ${({ $isVisible }) => ($isVisible ? 'auto' : '1px')}; `; // @TODO this shouldn't be Button diff --git a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx index 554438a52f22..a27ec4ed3d5c 100644 --- a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx @@ -7,7 +7,7 @@ import { selectSelectedDevice, createDiscoveryThunk, } from '@suite-common/wallet-core'; -import { Card, Icon, Tooltip, Row, Column, Text, Divider } from '@trezor/components'; +import { Card, Icon, Tooltip, Row, Column, Text, Divider, Box } from '@trezor/components'; import { getAllAccounts, getTotalFiatBalance } from '@suite-common/wallet-utils'; import { spacings, negativeSpacings } from '@trezor/theme'; @@ -102,93 +102,111 @@ export const WalletInstance = ({ contentType === 'disabling-view-only-ejects-wallet'; return ( - - - - - {discoveryProcess ? ( - - {!instance.useEmptyPassphrase && ( - } - > - - - )} - {instance.state?.staticSessionId ? ( - - ) : ( - - )} - - ) : ( - - )} - - - + + + + + + {discoveryProcess ? ( + + {!instance.useEmptyPassphrase && ( + + } + > + + + )} + {instance.state?.staticSessionId ? ( + + ) : ( + + )} + + ) : ( + + )} + + + + + - - + + - {(isViewOnlyRendered || - isEjectConfirmationRendered || - isDisablingViewOnlyEjectsWalletRendered) && ( - - )} + {(isViewOnlyRendered || + isEjectConfirmationRendered || + isDisablingViewOnlyEjectsWalletRendered) && ( + + )} - {isViewOnlyRendered && } - {isEjectConfirmationRendered && ( - - )} - {isDisablingViewOnlyEjectsWalletRendered && ( - - )} - + {isViewOnlyRendered && ( + + )} + {isEjectConfirmationRendered && ( + + )} + {isDisablingViewOnlyEjectsWalletRendered && ( + + )} + + ); }; From e0fd625d9969d46f3fd5b5befcfc12efe0efc355 Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Tue, 17 Dec 2024 13:16:12 +0100 Subject: [PATCH 033/181] chore: add TREZOR_PRE_COMMIT_ESLINT_SKIP global flax to skip pre-commit hook for ESLint --- .husky/eslint.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.husky/eslint.sh b/.husky/eslint.sh index 4a29193f3622..e2fb5cba2bda 100644 --- a/.husky/eslint.sh +++ b/.husky/eslint.sh @@ -1,5 +1,13 @@ #!/bin/bash +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +if [ "$TREZOR_PRE_COMMIT_ESLINT_SKIP" == "true" ]; then + echo "Skipping eslint pre-commit hook, do: 'export TREZOR_PRE_COMMIT_ESLINT_SKIP=false' to re-enable it." + exit 0 +fi + # This check will get all staged files that are *.js(x)/*.ts(x) files # and run eslint on them and tries to fix them and re-add them to be committed. # If auto-fix is possible it shall be transparent for the user. @@ -7,11 +15,12 @@ STAGED_FILES=$(git diff --cached --name-only --diff-filter=d | grep '(\.js\|\.jsx$\|\.ts\|\.tsx)$') +echo -e "${GREEN}Running Eslint pre-commit hook, to disable it do: 'export TREZOR_PRE_COMMIT_ESLINT_SKIP=true'.${NC}" + # Exit if no files. Passing no arguments would trigger eslint for whole repo if [ -z "$STAGED_FILES" ]; then echo "No staged JavaScript/TypeScript files to lint." else - echo "Linting JavaScript/TypeScript files..." echo "$STAGED_FILES" echo "" From d035575255667c6f4ed0737cf040021d17795fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Vytick=20Vytrhl=C3=ADk?= Date: Fri, 13 Dec 2024 12:38:04 +0100 Subject: [PATCH 034/181] chore(suite-native): remove unused thunk --- .../module-send/src/sendFormThunks.ts | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/suite-native/module-send/src/sendFormThunks.ts b/suite-native/module-send/src/sendFormThunks.ts index 5db9e2cde00e..57671a2e225b 100644 --- a/suite-native/module-send/src/sendFormThunks.ts +++ b/suite-native/module-send/src/sendFormThunks.ts @@ -4,10 +4,8 @@ import { D, pipe } from '@mobily/ts-belt'; import { createThunk } from '@suite-common/redux-utils'; import { getNetwork } from '@suite-common/wallet-config'; import { - PushTransactionError, deviceActions, enhancePrecomposedTransactionThunk, - pushSendFormTransactionThunk, selectAccountByKey, selectSelectedDevice, selectSendFormDraftByKey, @@ -19,7 +17,6 @@ import { SignTransactionError, } from '@suite-common/wallet-core'; import { - Account, AccountKey, FormState, GeneralPrecomposedTransactionFinal, @@ -122,29 +119,6 @@ export const cleanupSendFormThunk = createThunk( }, ); -export const sendTransactionAndCleanupSendFormThunk = createThunk< - { txid: string }, - { account: Account }, - { rejectValue: PushTransactionError } ->( - `${SEND_MODULE_PREFIX}/sendTransactionAndCleanupSendFormThunk`, - async ({ account }, { dispatch, rejectWithValue }) => { - const response = await dispatch( - pushSendFormTransactionThunk({ - selectedAccount: account, - }), - ); - - if (isRejected(response)) { - return rejectWithValue(response.payload!); - } - - dispatch(cleanupSendFormThunk({ accountKey: account.key })); - - return response.payload.payload; - }, -); - export const removeSendFormDraftsSupportingAmountUnitThunk = createThunk( `${SEND_MODULE_PREFIX}/removeSendFormDraftsSupportingAmountUnitThunk`, (_, { dispatch, getState }) => { From e79fee1cd1dd09df320a98c8a76bad13b44d5079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Vytick=20Vytrhl=C3=ADk?= Date: Fri, 13 Dec 2024 12:40:09 +0100 Subject: [PATCH 035/181] feat(suite-native): register for solana blockhash also on address screen --- .../useSubscribeForSolanaBlockUpdates.ts | 24 +++++++++++++++++++ .../src/screens/SendFeesScreen.tsx | 21 ---------------- .../src/screens/SendOutputsScreen.tsx | 6 ++++- 3 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 suite-native/module-send/src/hooks/useSubscribeForSolanaBlockUpdates.ts diff --git a/suite-native/module-send/src/hooks/useSubscribeForSolanaBlockUpdates.ts b/suite-native/module-send/src/hooks/useSubscribeForSolanaBlockUpdates.ts new file mode 100644 index 000000000000..94c3e3ba0369 --- /dev/null +++ b/suite-native/module-send/src/hooks/useSubscribeForSolanaBlockUpdates.ts @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; + +import { Account } from '@suite-common/wallet-types'; +import TrezorConnect from '@trezor/connect'; + +export const useSubscribeForSolanaBlockUpdates = (account: Account | null) => { + useEffect(() => { + // Subscribe to blocks for Solana, since they are not fetched globally + // this is needed for correct Solana fee estimation + if (account && account.networkType === 'solana') { + TrezorConnect.blockchainSubscribe({ + coin: account.symbol, + blocks: true, + }); + + return () => { + TrezorConnect.blockchainUnsubscribe({ + coin: account.symbol, + blocks: true, + }); + }; + } + }, [account]); +}; diff --git a/suite-native/module-send/src/screens/SendFeesScreen.tsx b/suite-native/module-send/src/screens/SendFeesScreen.tsx index 5510cbee4b52..01a9e8941d0d 100644 --- a/suite-native/module-send/src/screens/SendFeesScreen.tsx +++ b/suite-native/module-send/src/screens/SendFeesScreen.tsx @@ -1,9 +1,7 @@ import { useSelector } from 'react-redux'; -import { useEffect } from 'react'; import { SendStackParamList, SendStackRoutes, StackProps } from '@suite-native/navigation'; import { AccountsRootState, selectAccountByKey } from '@suite-common/wallet-core'; -import TrezorConnect from '@trezor/connect'; import { SendFeesForm } from '../components/SendFeesForm'; import { SendScreen } from '../components/SendScreen'; @@ -17,25 +15,6 @@ export const SendFeesScreen = ({ const account = useSelector((state: AccountsRootState) => selectAccountByKey(state, accountKey), ); - - useEffect(() => { - // Subscribe to blocks for Solana, since they are not fetched globally - // this is needed for correct Solana fee estimation - if (account && account.networkType === 'solana') { - TrezorConnect.blockchainSubscribe({ - coin: account.symbol, - blocks: true, - }); - - return () => { - TrezorConnect.blockchainUnsubscribe({ - coin: account.symbol, - blocks: true, - }); - }; - } - }, [account]); - if (!account) return; return ( diff --git a/suite-native/module-send/src/screens/SendOutputsScreen.tsx b/suite-native/module-send/src/screens/SendOutputsScreen.tsx index a82b7008a006..89a9bcb0bc35 100644 --- a/suite-native/module-send/src/screens/SendOutputsScreen.tsx +++ b/suite-native/module-send/src/screens/SendOutputsScreen.tsx @@ -44,6 +44,7 @@ import { calculateFeeLevelsMaxAmountThunk } from '../sendFormThunks'; import { constructFormDraft } from '../utils'; import { FeeLevelsMaxAmount } from '../types'; import { storeFeeLevels } from '../sendFormSlice'; +import { useSubscribeForSolanaBlockUpdates } from '../hooks/useSubscribeForSolanaBlockUpdates'; const buttonWrapperStyle = prepareNativeStyle(utils => ({ width: '100%', @@ -97,6 +98,8 @@ export const SendOutputsScreen = ({ selectSendFormDraftByKey(state, accountKey, tokenContract), ); + useSubscribeForSolanaBlockUpdates(account); + const deviceUnavailableCapabilities = useSelector(selectDeviceUnavailableCapabilities); const network = account ? getNetwork(account.symbol) : null; @@ -210,7 +213,8 @@ export const SendOutputsScreen = ({ setValue('rippleDestinationTag', sendFormDraft.rippleDestinationTag, { shouldTouch: true, }); - await calculateNormalFeeMaxAmount(); + // The max amount is equal to the total token balance for tokens. (fee is paid in mainnet currency) + if (!tokenContract) await calculateNormalFeeMaxAmount(); trigger(); } }; From f1f29c7a8b3ad74b7c28287827262f142cf56a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20V=C3=A1clav=C3=ADk?= Date: Tue, 17 Dec 2024 22:19:50 +0100 Subject: [PATCH 036/181] fix(suite): Fix sidebar mouseup bug (#16000) --- .../components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx index 5cc54d9d3821..f9528efc2b2a 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx @@ -39,7 +39,7 @@ const Content = styled.div` export const Sidebar = () => { const [closedNotificationDevice, setClosedNotificationDevice] = useState(false); const [closedNotificationSuite, setClosedNotificationSuite] = useState(false); - const { isSidebarCollapsed } = useResponsiveContext(); + const { isSidebarCollapsed, setSidebarWidth, sidebarWidth } = useResponsiveContext(); const { elevation } = useElevation(); const { updateStatusDevice, updateStatusSuite } = useUpdateStatus(); @@ -48,9 +48,8 @@ export const Sidebar = () => { setSidebarWidth: (width: number) => setSidebarWidthInRedux({ width }), }); - const { setSidebarWidth, sidebarWidth } = useResponsiveContext(); - const handleSidebarWidthChanged = (width: number) => { + setSidebarWidth(width); actions.setSidebarWidth(width); }; const handleSidebarWidthUpdate = (width: number) => { From e55ae3d8cea4ef00efb0356e1728aaf0836ebf20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20V=C3=A1clav=C3=ADk?= Date: Tue, 17 Dec 2024 22:19:58 +0100 Subject: [PATCH 037/181] fix(suite): Fix skeletons in collapsed sidebar (#15999) --- .../AccountsMenu/AccountItemSkeleton.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemSkeleton.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemSkeleton.tsx index b2f3e9e48906..5bfc2917292d 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemSkeleton.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemSkeleton.tsx @@ -3,8 +3,24 @@ import { spacings } from '@trezor/theme'; import { useLoadingSkeleton } from 'src/hooks/suite'; +import { useResponsiveContext } from '../../../../support/suite/ResponsiveContext'; + export const AccountItemSkeleton = () => { const { shouldAnimate } = useLoadingSkeleton(); + const { isSidebarCollapsed } = useResponsiveContext(); + + if (isSidebarCollapsed) { + return ( + + + + ); + } return ( Date: Tue, 17 Dec 2024 22:20:03 +0100 Subject: [PATCH 038/181] fix(suite): Fix button text in reveal address (#15998) --- .../ReduxModal/UserContextModal/ConfirmUnverifiedModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConfirmUnverifiedModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConfirmUnverifiedModal.tsx index 80cf302b1ed4..7ed151df01ac 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConfirmUnverifiedModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConfirmUnverifiedModal.tsx @@ -83,7 +83,7 @@ export const ConfirmUnverifiedModal = ({ )} } From e9b82b5b9cb5e47c379ae171c0c311394e151137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20V=C3=A1clav=C3=ADk?= Date: Tue, 17 Dec 2024 22:20:07 +0100 Subject: [PATCH 039/181] fix(suite): Fix button color in dashboard (#15997) --- .../views/dashboard/PortfolioCard/PortfolioCardException.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/suite/src/views/dashboard/PortfolioCard/PortfolioCardException.tsx b/packages/suite/src/views/dashboard/PortfolioCard/PortfolioCardException.tsx index 37dd40957473..6d92a8532dd7 100644 --- a/packages/suite/src/views/dashboard/PortfolioCard/PortfolioCardException.tsx +++ b/packages/suite/src/views/dashboard/PortfolioCard/PortfolioCardException.tsx @@ -55,7 +55,7 @@ const Container = ({ title, description, cta, dataTestBase }: ContainerProps) => {actions.map(a => (