From 0818e713fa0f9bb7a6472e34a05888896ffc3835 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Tue, 19 Nov 2024 07:49:44 +0100 Subject: [PATCH 01/47] Refactor: upgrade to Next 15 (#4485) * chore: update to nextjs 15 * chore: fix tsc lint errors * chore: fix eslint errors * chore: fixing failing unit tests * chore: remove yarn * chore: update eslint to v9 * chore: update github eslint action * chore: update cypress to v13.15.2 (#4490) * chore: update cypress to v13.15.2 * chore: update cypress github action to use v13.15.2 * chore: update mui to v6 (#4499) * chore: update mui to v6 Applied codemons to update the code * refactor: fix DatePicker with mui v6 fix: failing Date test * refactor: use ListItemButton instead of ListItem the selected and disabled props on ListItem have been removed in v6, but are available on the ListItemButton * chore: update snapshots * refactor: recoveryModal tests use snapshots * fix: failing bookmarks test with mui v6 * fix: missing DM-Sans font * fix: drawer was stealing clicks from rest of page * fix: failing tests * fix: fonts fail to load Read the comment in the file and have fun! * fix: prettier errors * fix: tsc errors --- .eslintrc.json | 45 - .github/workflows/cypress/action.yml | 4 +- .github/workflows/lint.yml | 3 +- eslint.config.mjs | 77 + jest.setup.js | 5 +- package.json | 58 +- public/fonts/fonts.css | 2 + .../address-book/AddressBookHeader/index.tsx | 19 +- .../address-book/AddressBookTable/index.tsx | 12 +- .../address-book/EntryDialog/index.tsx | 6 +- .../address-book/ExportDialog/index.tsx | 7 +- src/components/balances/AssetsTable/index.tsx | 15 +- .../balances/HiddenTokenButton/index.tsx | 6 +- .../balances/TokenListSelect/index.tsx | 11 +- src/components/balances/TokenMenu/index.tsx | 15 +- .../batch/BatchIndicator/BatchTooltip.tsx | 16 +- .../batch/BatchSidebar/BatchTxItem.tsx | 25 +- .../batch/BatchSidebar/EmptyBatch.tsx | 50 +- src/components/batch/BatchSidebar/index.tsx | 8 +- src/components/common/AddFunds/index.tsx | 54 +- src/components/common/AddressInput/index.tsx | 1 - .../common/AddressInputReadOnly/index.tsx | 8 +- .../common/BuyCryptoButton/index.tsx | 20 +- .../common/ChainIndicator/index.tsx | 8 +- src/components/common/Chip/index.tsx | 12 +- src/components/common/ChoiceButton/index.tsx | 24 +- .../common/ConnectWallet/AccountCenter.tsx | 10 +- .../common/CookieAndTermBanner/index.tsx | 54 +- .../common/CopyAddressButton/index.tsx | 8 +- .../common/CopyTooltip/ConfirmCopyModal.tsx | 24 +- src/components/common/Countdown/index.tsx | 28 +- .../common/DatePickerInput/index.tsx | 10 +- src/components/common/Disclaimer/index.tsx | 40 +- src/components/common/EnhancedTable/index.tsx | 1 - src/components/common/ErrorBoundary/index.tsx | 21 +- .../EthHashInfo/SrcEthHashInfo/index.tsx | 40 +- .../common/ExplorerButton/index.tsx | 16 +- src/components/common/ExternalLink/index.tsx | 9 +- src/components/common/FiatValue/index.tsx | 9 +- src/components/common/FileUpload/index.tsx | 64 +- .../common/LegalDisclaimerContent/index.tsx | 24 +- .../common/NetworkSelector/index.tsx | 63 +- src/components/common/Notifications/index.tsx | 7 +- .../common/OnboardingTooltip/index.tsx | 9 +- .../common/PageLayout/SideDrawer.tsx | 5 + .../common/PagePlaceholder/index.tsx | 10 +- src/components/common/PaginatedTxns/index.tsx | 28 +- .../__tests__/SafeTokenWidget.test.tsx | 2 +- .../common/SafeTokenWidget/index.tsx | 8 +- .../common/SpendingLimitLabel/index.tsx | 12 +- src/components/common/Sticky/index.tsx | 12 +- src/components/common/Table/DataTable.tsx | 6 +- .../common/TokenExplorerLink/index.tsx | 7 +- src/components/common/WalletInfo/index.tsx | 46 +- .../common/WalletOverview/index.tsx | 10 +- .../common/WidgetDisclaimer/index.tsx | 19 +- .../common/icons/CircularIcon/index.tsx | 8 +- src/components/dashboard/Assets/index.tsx | 54 +- src/components/dashboard/FirstSteps/index.tsx | 133 +- .../GovernanceSection/GovernanceSection.tsx | 67 +- .../dashboard/Overview/Overview.tsx | 53 +- .../PendingTxs/PendingRecoveryListItem.tsx | 13 +- .../PendingTxs/PendingTxListItem.tsx | 29 +- .../dashboard/PendingTxs/PendingTxsList.tsx | 29 +- .../SafeAppsDashboardSection.tsx | 11 +- .../dashboard/StakingBanner/index.tsx | 80 +- src/components/new-safe/CardStepper/index.tsx | 7 +- src/components/new-safe/OwnerRow/index.tsx | 20 +- .../new-safe/create/AdvancedCreateSafe.tsx | 35 +- .../new-safe/create/CreateSafeInfos/index.tsx | 8 +- .../new-safe/create/NetworkWarning/index.tsx | 12 +- .../create/NoWalletConnectedWarning/index.tsx | 10 +- .../new-safe/create/OverviewWidget/index.tsx | 16 +- src/components/new-safe/create/index.tsx | 35 +- .../steps/AdvancedOptionsStep/index.tsx | 53 +- .../create/steps/OwnerPolicyStep/index.tsx | 36 +- .../create/steps/ReviewStep/index.tsx | 59 +- .../create/steps/SetNameStep/index.tsx | 34 +- .../create/steps/StatusStep/StatusMessage.tsx | 39 +- .../create/steps/StatusStep/StatusStep.tsx | 17 +- .../create/steps/StatusStep/index.tsx | 29 +- src/components/new-safe/load/index.tsx | 26 +- .../load/steps/SafeOwnerStep/index.tsx | 9 +- .../load/steps/SafeReviewStep/index.tsx | 9 +- .../load/steps/SetAddressStep/index.tsx | 31 +- src/components/nfts/NftCollections/index.tsx | 2 - src/components/nfts/NftGrid/index.tsx | 44 +- src/components/nfts/NftSendForm/index.tsx | 43 +- .../NotificationCenter/index.tsx | 9 +- .../NotificationCenterItem/index.tsx | 10 +- .../NotificationCenterList/index.tsx | 8 +- .../safe-apps/AddCustomAppModal/CustomApp.tsx | 20 +- .../safe-apps/AddCustomAppModal/index.tsx | 30 +- .../safe-apps/AddCustomSafeAppCard/index.tsx | 10 +- .../AppFrame/TransactionQueueBar/index.tsx | 17 +- src/components/safe-apps/AppFrame/index.tsx | 6 +- .../safe-apps/AppFrame/useAppIsLoading.ts | 8 +- .../safe-apps/AppFrame/useFromAppAnalytics.ts | 2 +- .../AppFrame/useThirdPartyCookies.ts | 2 +- .../safe-apps/NativeSwapsCard/index.tsx | 17 +- .../safe-apps/PermissionsPrompt.tsx | 7 +- .../safe-apps/RemoveCustomAppModal.tsx | 7 +- .../safe-apps/SafeAppActionButtons/index.tsx | 11 +- .../safe-apps/SafeAppCard/index.tsx | 9 +- .../SafeAppLandingPage/AppActions.tsx | 29 +- .../SafeAppLandingPage/SafeAppDetails.tsx | 51 +- .../safe-apps/SafeAppLandingPage/TryDemo.tsx | 17 +- .../safe-apps/SafeAppLandingPage/index.tsx | 16 +- .../safe-apps/SafeAppPreviewDrawer/index.tsx | 31 +- .../SafeAppSocialLinksCard/index.tsx | 62 +- .../safe-apps/SafeAppTags/index.tsx | 9 +- .../safe-apps/SafeAppsFilters/index.tsx | 10 +- .../SafeAppsInfoModal/AllowedFeaturesList.tsx | 21 +- .../safe-apps/SafeAppsInfoModal/Slider.tsx | 8 +- .../safe-apps/SafeAppsInfoModal/index.tsx | 25 +- .../safe-apps/SafeAppsListHeader/index.tsx | 9 +- .../SafeAppsZeroResultsPlaceholder/index.tsx | 9 +- .../safe-messages/DecodedMsg/index.tsx | 11 +- .../safe-messages/InfoBox/index.tsx | 7 +- .../safe-messages/MsgDetails/index.tsx | 10 +- .../safe-messages/MsgSigners/index.tsx | 24 +- .../safe-messages/MsgSummary/index.tsx | 2 +- .../safe-messages/PaginatedMsgs/index.tsx | 36 +- .../settings/ContractVersion/index.tsx | 25 +- .../settings/DataManagement/ImportDialog.tsx | 6 +- .../settings/DataManagement/index.tsx | 15 +- .../settings/EnvironmentVariables/index.tsx | 39 +- .../FallbackHandler/__tests__/index.test.tsx | 20 +- .../settings/FallbackHandler/index.tsx | 23 +- .../GlobalPushNotifications.tsx | 2 +- .../useNotificationRegistrations.test.ts | 26 +- .../settings/PushNotifications/index.tsx | 37 +- .../settings/RequiredConfirmations/index.tsx | 29 +- .../settings/SafeAppsPermissions/index.tsx | 2 +- .../settings/SafeAppsSigningMethod/index.tsx | 14 +- src/components/settings/SafeModules/index.tsx | 2 +- .../settings/SecurityLogin/index.tsx | 9 +- .../settings/SecuritySettings/index.tsx | 14 +- .../SpendingLimits/NoSpendingLimits.tsx | 12 +- .../SpendingLimits/SpendingLimitsTable.tsx | 27 +- .../settings/SpendingLimits/index.tsx | 17 +- .../settings/TransactionGuards/index.tsx | 2 +- .../settings/owner/EditOwnerDialog/index.tsx | 13 +- .../settings/owner/OwnerList/index.tsx | 30 +- src/components/sidebar/DebugToggle/index.tsx | 7 +- .../sidebar/QrCodeButton/QrModal.tsx | 44 +- src/components/sidebar/Sidebar/index.tsx | 7 +- .../sidebar/SidebarHeader/index.tsx | 9 +- .../sidebar/SidebarNavigation/index.tsx | 43 +- .../transactions/BulkTxListGroup/index.tsx | 27 +- .../transactions/GroupedTxListItems/index.tsx | 32 +- .../HexEncodedData.test.tsx.snap | 43 +- .../transactions/MaliciousTxWarning/index.tsx | 12 +- .../SignedMessagesHelpLink/index.tsx | 15 +- .../transactions/TxConfirmations/index.tsx | 8 +- .../transactions/TxDetails/SafeTxGasForm.tsx | 19 +- .../TxDetails/Summary/TxDataRow/index.tsx | 8 +- .../transactions/TxDetails/Summary/index.tsx | 19 +- .../TxData/DecodedData/MethodCall.tsx | 13 +- .../DecodedData/MethodDetails/index.tsx | 25 +- .../DecodedData/SingleTxDecoded/index.tsx | 7 +- .../TxDetails/TxData/Rejection/index.tsx | 14 +- .../TxData/Transfer/TransferActions.tsx | 4 +- .../TxDetails/TxData/Transfer/index.tsx | 26 +- .../transactions/TxDetails/index.tsx | 8 +- .../TxFilterForm/TxFilterForm.test.tsx | 4 +- .../TxListItem/ExpandableTransactionItem.tsx | 7 +- .../transactions/TxSigners/index.tsx | 11 +- .../transactions/TxStatusChip/index.tsx | 12 +- .../transactions/TxStatusLabel/index.tsx | 2 +- .../transactions/TxSummary/QueueActions.tsx | 9 +- .../transactions/TxSummary/index.tsx | 74 +- src/components/tx-flow/SafeTxProvider.tsx | 6 +- src/components/tx-flow/TxInfoProvider.tsx | 4 +- .../tx-flow/common/OwnerList/index.tsx | 8 +- .../tx-flow/common/TxLayout/index.tsx | 28 +- .../tx-flow/common/TxNonce/index.tsx | 32 +- .../tx-flow/common/TxStatusWidget/index.tsx | 11 +- .../tx-flow/flows/AddOwner/ChooseOwner.tsx | 46 +- .../CancelRecoveryFlowReview.tsx | 7 +- .../CancelRecovery/CancelRecoveryOverview.tsx | 36 +- .../flows/ChangeThreshold/ChooseThreshold.tsx | 38 +- .../flows/ConfirmTx/ConfirmProposedTx.tsx | 8 +- .../NewSpendingLimit/CreateSpendingLimit.tsx | 8 +- .../NewSpendingLimit/ReviewSpendingLimit.tsx | 52 +- src/components/tx-flow/flows/NewTx/index.tsx | 45 +- .../flows/NftTransfer/ReviewNftBatch.tsx | 16 +- .../flows/NftTransfer/SendNftBatch.tsx | 68 +- .../RecoverAccountFlowReview.tsx | 22 +- .../RecoverAccountFlowSetup.tsx | 50 +- .../RecoveryAttempt/RecoveryAttemptReview.tsx | 7 +- .../tx-flow/flows/RejectTx/RejectTx.tsx | 20 +- .../flows/RemoveGuard/ReviewRemoveGuard.tsx | 8 +- .../flows/RemoveModule/ReviewRemoveModule.tsx | 14 +- .../flows/RemoveOwner/ReviewRemoveOwner.tsx | 24 +- .../flows/RemoveOwner/SetThreshold.tsx | 37 +- .../RemoveRecoveryFlowOverview.tsx | 12 +- .../RemoveSpendingLimit.tsx | 32 +- .../tx-flow/flows/ReplaceTx/DeleteTxModal.tsx | 51 +- .../tx-flow/flows/ReplaceTx/index.tsx | 33 +- .../flows/SignMessage/SignMessage.test.tsx | 10 +- .../tx-flow/flows/SignMessage/SignMessage.tsx | 75 +- .../tx-flow/flows/SignMessage/index.tsx | 15 +- .../ReviewSignMessageOnChain.test.tsx | 4 +- .../ReviewSignMessageOnChain.tsx | 38 +- .../flows/SuccessScreen/StatusMessage.tsx | 16 +- .../flows/SuccessScreen/StatusStepper.tsx | 28 +- .../SuccessScreen/statuses/DefaultStatus.tsx | 16 +- .../SuccessScreen/statuses/IndexingStatus.tsx | 16 +- .../statuses/ProcessingStatus.tsx | 23 +- .../TokenTransfer/CreateTokenTransfer.tsx | 19 +- .../TokenTransfer/ReviewSpendingLimitTx.tsx | 8 +- .../flows/TokenTransfer/SendAmountBlock.tsx | 16 +- .../flows/UpdateSafe/UpdateSafeReview.tsx | 27 +- .../UpsertRecoveryFlowIntro.tsx | 26 +- .../UpsertRecoveryFlowSettings.tsx | 32 +- .../UpsertRecovery/useRecoveryPeriods.ts | 2 +- src/components/tx-flow/index.tsx | 2 +- .../tx/AdvancedParams/AdvancedParamsForm.tsx | 15 +- .../tx/AdvancedParams/GasLimitInput.tsx | 2 +- src/components/tx/AdvancedParams/index.tsx | 2 +- .../AdvancedParams/{types.d.ts => types.ts} | 9 +- .../tx/ApprovalEditor/ApprovalEditorForm.tsx | 6 +- .../tx/ApprovalEditor/ApprovalItem.tsx | 16 +- .../tx/ApprovalEditor/Approvals.tsx | 7 +- .../ApprovalEditor/EditableApprovalItem.tsx | 17 +- .../tx/ApprovalEditor/SpenderField.tsx | 17 +- src/components/tx/ApprovalEditor/index.tsx | 19 +- src/components/tx/BalanceInfo/index.tsx | 7 +- .../ConfirmationOrderHeader.tsx | 70 +- src/components/tx/DecodedTx/index.tsx | 25 +- src/components/tx/ErrorMessage/index.tsx | 15 +- .../tx/ExecutionMethodSelector/index.tsx | 8 +- src/components/tx/FieldsGrid/index.tsx | 30 +- src/components/tx/GasParams/index.tsx | 17 +- src/components/tx/RemainingRelays/index.tsx | 10 +- src/components/tx/SendFromBlock/index.tsx | 17 +- .../tx/SignOrExecuteForm/BatchButton.tsx | 9 +- .../tx/SignOrExecuteForm/ExecuteForm.tsx | 17 +- .../ExecuteThroughRoleForm/index.tsx | 53 +- .../MigrateToL2Information.tsx | 21 +- .../tx/SignOrExecuteForm/SignForm.tsx | 10 +- .../SignOrExecuteForm/SignOrExecuteForm.tsx | 8 +- .../SignOrExecuteSkeleton.tsx | 10 +- .../__snapshots__/SignOrExecute.test.tsx.snap | 260 ++- src/components/tx/SuccessMessage/index.tsx | 7 +- .../BatchTransactions.test.tsx.snap | 147 +- .../ChangeThreshold.test.tsx.snap | 6 +- .../ChangeThreshold/index.tsx | 15 +- .../SettingsChange.test.tsx.snap | 96 +- .../SettingsChange/index.tsx | 12 +- .../ConfirmationView.test.tsx.snap | 286 ++- .../tx/confirmation-views/index.tsx | 3 +- src/components/tx/confirmation-views/utils.ts | 3 +- .../blockaid/BlockaidBalanceChange.tsx | 71 +- src/components/tx/security/blockaid/index.tsx | 46 +- .../tx/security/blockaid/useBlockaid.ts | 2 +- .../tx/security/shared/TxSecurityContext.tsx | 12 +- .../security/tenderly/__tests__/utils.test.ts | 2 +- src/components/tx/security/tenderly/index.tsx | 40 +- .../welcome/MyAccounts/AccountItem.tsx | 34 +- .../welcome/MyAccounts/MultiAccountItem.tsx | 64 +- .../welcome/MyAccounts/PaginatedSafeList.tsx | 31 +- .../welcome/MyAccounts/QueueActions.tsx | 25 +- .../welcome/MyAccounts/SubAccountItem.tsx | 34 +- src/components/welcome/MyAccounts/index.tsx | 16 +- src/components/welcome/NewSafe.tsx | 44 +- .../welcome/WelcomeLogin/WalletLogin.tsx | 29 +- src/components/welcome/WelcomeLogin/index.tsx | 23 +- .../counterfactual/ActivateAccountButton.tsx | 8 +- .../counterfactual/ActivateAccountFlow.tsx | 31 +- src/features/counterfactual/CheckBalance.tsx | 15 +- .../counterfactual/CounterfactualForm.tsx | 6 +- .../CounterfactualSuccessScreen.tsx | 33 +- src/features/counterfactual/FirstTxFlow.tsx | 10 +- .../counterfactual/PayNowPayLater.tsx | 23 +- .../__tests__/useDeployGasLimit.test.ts | 2 +- src/features/counterfactual/utils.ts | 2 +- .../components/CreateSafeOnNewChain/index.tsx | 26 +- .../InconsistentSignerSetupWarning.tsx | 31 +- .../components/DeleteProposerDialog.tsx | 8 +- .../components/EditProposerDialog.tsx | 4 +- .../ExecuteRecoveryButton/index.tsx | 4 +- .../GroupedRecoveryListItems/index.tsx | 30 +- .../RecoveryCards/RecoveryInProgressCard.tsx | 67 +- .../RecoveryCards/RecoveryProposalCard.tsx | 84 +- .../__tests__/RecoveryProposalCard.test.tsx | 6 +- .../RecoveryContext/useRecoveryPendingTxs.ts | 2 +- .../components/RecoveryDescription/index.tsx | 8 +- .../components/RecoveryHeader/index.test.tsx | 12 +- .../components/RecoveryHeader/index.tsx | 8 +- .../RecoveryList/LazyRecoveryList.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 9 + .../components/RecoveryModal/index.test.tsx | 34 +- .../components/RecoveryModal/index.tsx | 12 +- .../ChooseRecoveryMethodModal.tsx | 45 +- .../components/RecoverySettings/index.tsx | 22 +- .../components/RecoverySigners/index.tsx | 20 +- .../components/RecoverySummary/index.tsx | 38 +- .../useIsValidRecoveryExecution.test.ts | 12 +- .../components/EnableAccountBanner/index.tsx | 22 +- .../speedup/components/SpeedUpModal.tsx | 36 +- .../speedup/components/SpeedUpMonitor.tsx | 8 +- .../stake/components/StakePage/index.tsx | 18 +- .../StakingConfirmationTx/Deposit.tsx | 41 +- .../components/StakingConfirmationTx/Exit.tsx | 17 +- .../StakingConfirmationTx/Withdraw.tsx | 6 +- .../StakingTxDepositDetails/index.tsx | 12 +- .../components/StakingTxExitDetails/index.tsx | 10 +- .../StakingTxWithdrawDetails/index.tsx | 10 +- .../swap/components/OrderId/index.tsx | 6 +- .../swap/components/SwapOrder/index.tsx | 20 +- .../SwapOrder/rows/PartBuyAmount.tsx | 14 +- .../SwapOrder/rows/PartSellAmount.tsx | 14 +- .../SwapOrderConfirmationView/index.tsx | 9 +- .../swap/components/SwapTxInfo/SwapTx.tsx | 31 +- .../swap/helpers/__tests__/utils.test.ts | 1 - src/features/swap/helpers/fee.ts | 11 +- .../components/OutreachPopup/index.tsx | 47 +- .../__tests__/WalletConnectContext.test.tsx | 8 +- .../components/WcConnectionForm/index.tsx | 30 +- .../components/WcConnectionState/index.tsx | 8 +- .../components/WcErrorMessage/index.tsx | 9 +- .../components/WcHints/index.tsx | 8 +- .../components/WcLogoHeader/index.tsx | 10 +- .../WcProposalForm/CompatibilityWarning.tsx | 15 +- .../components/WcProposalForm/index.tsx | 28 +- .../components/WcSessionList/WcNoSessions.tsx | 19 +- src/hooks/__tests__/useChainId.test.ts | 2 +- src/hooks/__tests__/useDebounce.test.ts | 3 +- .../__tests__/useLoadSpendingLimits.test.ts | 2 +- .../__tests__/useSafeTokenAllocation.test.ts | 16 +- .../__tests__/useSafeMessageStatus.test.ts | 6 +- src/hooks/messages/useSafeMessageStatus.ts | 2 +- .../__tests__/useCategoryFilter.test.ts | 8 +- src/hooks/useTxNotifications.ts | 4 +- src/hooks/useVisibleBalances.ts | 15 +- src/hooks/wallets/consts.ts | 2 +- src/hooks/wallets/wallets.ts | 2 +- src/pages/_document.tsx | 1 - src/pages/_offline.tsx | 45 +- src/pages/apps/open.tsx | 9 +- src/pages/balances/index.tsx | 8 +- src/pages/balances/nfts.tsx | 20 +- src/pages/imprint.tsx | 63 +- src/pages/licenses.tsx | 53 +- src/pages/privacy.tsx | 1 - src/pages/settings/appearance.tsx | 32 +- src/pages/settings/cookies.tsx | 9 +- src/pages/settings/setup.tsx | 15 +- src/pages/share/safe-app.tsx | 8 +- src/pages/stake.tsx | 8 +- src/pages/swap.tsx | 8 +- src/pages/terms.tsx | 1 - src/pages/transactions/history.tsx | 8 +- src/pages/transactions/msg.tsx | 11 +- src/pages/transactions/queue.tsx | 7 +- src/pages/transactions/tx.tsx | 11 +- .../private-key-module/PkModulePopup.tsx | 14 +- .../push-notifications/preferences.ts | 2 +- src/services/safe-apps/AppCommunicator.ts | 4 +- .../useSafeWalletProvider.test.tsx | 22 +- .../useSafeWalletProvider.tsx | 4 +- .../security/modules/BlockaidModule/index.ts | 2 +- src/store/__tests__/txQueueSlice.test.ts | 2 +- src/styles/globals.css | 10 + src/tests/pages/apps-share.test.tsx | 7 +- src/tests/pages/apps.test.tsx | 36 +- src/tests/test-utils.tsx | 2 +- src/utils/__tests__/tokens.test.ts | 4 +- src/utils/__tests__/transactions.test.ts | 4 +- src/utils/transactions.ts | 2 +- tsconfig.json | 4 +- yarn.lock | 2072 ++++++++++------- 374 files changed, 7402 insertions(+), 2691 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 eslint.config.mjs rename src/components/tx/AdvancedParams/{types.d.ts => types.ts} (57%) create mode 100644 src/features/recovery/components/RecoveryModal/__snapshots__/index.test.tsx.snap diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 63839322a5..0000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "extends": [ - "next", - "prettier", - "plugin:prettier/recommended", - "plugin:storybook/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": ["./tsconfig.json"] - }, - "rules": { - "@next/next/no-img-element": "off", - "@next/next/google-font-display": "off", - "@next/next/google-font-preconnect": "off", - "@next/next/no-page-custom-font": "off", - "unused-imports/no-unused-imports-ts": "error", - "@typescript-eslint/consistent-type-imports": "error", - "@typescript-eslint/await-thenable": "error", - "no-constant-condition": "warn", - "no-unused-vars": ["error", { "varsIgnorePattern": "^_" }], - "react-hooks/exhaustive-deps": [ - "warn", - { - "additionalHooks": "useAsync" - } - ], - "no-only-tests/no-only-tests": "error", - "object-shorthand": ["error", "properties"], - "jsx-quotes": ["error", "prefer-double"], - "react/jsx-curly-brace-presence": ["error", { "props": "never", "children": "never" }] - }, - "ignorePatterns": [ - "node_modules/", - ".next/", - ".github/", - "cypress/", - "src/types/contracts/" - ], - "plugins": [ - "unused-imports", - "@typescript-eslint", - "no-only-tests" - ] -} diff --git a/.github/workflows/cypress/action.yml b/.github/workflows/cypress/action.yml index c549e1c5fa..337019affd 100644 --- a/.github/workflows/cypress/action.yml +++ b/.github/workflows/cypress/action.yml @@ -38,9 +38,9 @@ runs: curl -O 'https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb' sudo apt-get install ./google-chrome-stable_current_amd64.deb - - name: Install Cypress 13.13.1 + - name: Install Cypress 13.15.2 shell: bash - run: npm install cypress@13.13.1 --legacy-peer-deps + run: yarn add -D cypress@13.15.2 - uses: ./.github/workflows/build with: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bdbe5727fa..0bb639e056 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,10 +18,11 @@ jobs: - uses: ./.github/workflows/yarn - - uses: CatChen/eslint-suggestion-action@v2 + - uses: CatChen/eslint-suggestion-action@v4.1.7 with: request-changes: true # optional fail-check: true # optional github-token: ${{ secrets.GITHUB_TOKEN }} # optional directory: './' # optional targets: 'src' # optional + config-path: './eslint.config.mjs' diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..04caa5f0a2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,77 @@ +import unusedImports from 'eslint-plugin-unused-imports' +import typescriptEslint from '@typescript-eslint/eslint-plugin' +import noOnlyTests from 'eslint-plugin-no-only-tests' +import tsParser from '@typescript-eslint/parser' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}) + +export default [ + { + ignores: ['**/node_modules/', '**/.next/', '**/.github/', '**/cypress/', 'src/types/contracts/'], + }, + ...compat.extends('next', 'prettier', 'plugin:prettier/recommended', 'plugin:storybook/recommended'), + { + plugins: { + 'unused-imports': unusedImports, + '@typescript-eslint': typescriptEslint, + 'no-only-tests': noOnlyTests, + }, + + languageOptions: { + parser: tsParser, + ecmaVersion: 5, + sourceType: 'script', + + parserOptions: { + project: ['./tsconfig.json'], + }, + }, + + rules: { + '@next/next/no-img-element': 'off', + '@next/next/google-font-display': 'off', + '@next/next/google-font-preconnect': 'off', + '@next/next/no-page-custom-font': 'off', + 'unused-imports/no-unused-imports': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/await-thenable': 'error', + 'no-constant-condition': 'warn', + + 'unused-imports/no-unused-vars': [ + 'error', + { + varsIgnorePattern: '^_', + }, + ], + + 'react-hooks/exhaustive-deps': [ + 'warn', + { + additionalHooks: 'useAsync', + }, + ], + + 'no-only-tests/no-only-tests': 'error', + 'object-shorthand': ['error', 'properties'], + 'jsx-quotes': ['error', 'prefer-double'], + + 'react/jsx-curly-brace-presence': [ + 'error', + { + props: 'never', + children: 'never', + }, + ], + }, + }, +] diff --git a/jest.setup.js b/jest.setup.js index ec8ccaa216..d59f4f2592 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,9 +1,6 @@ -// Optional: configure or set up a testing framework before each test. -// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js` - // Used for __tests__/testing-library.js // Learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom/extend-expect' +import '@testing-library/jest-dom' import { TextEncoder, TextDecoder } from 'util' import { Request } from 'node-fetch' diff --git a/package.json b/package.json index 3592ae397b..5d621f26cd 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,9 @@ "@safe-global/safe-core-sdk-types/**/ethers": "^6.11.1", "@safe-global/protocol-kit/**/ethers": "^6.11.1", "@safe-global/api-kit/**/ethers": "^6.11.1", - "@gnosis.pm/zodiac/**/ethers": "^6.11.1" + "@gnosis.pm/zodiac/**/ethers": "^6.11.1", + "@types/react": "npm:types-react@19.0.0-rc.1", + "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" }, "dependencies": { "@cowprotocol/widget-react": "^0.10.0", @@ -48,17 +50,17 @@ "@emotion/cache": "^11.13.1", "@emotion/react": "^11.13.3", "@emotion/server": "^11.11.0", - "@emotion/styled": "^11.11.0", + "@emotion/styled": "^11.13.0", "@gnosis.pm/zodiac": "^4.0.3", - "@mui/icons-material": "^5.14.20", - "@mui/material": "^5.16.7", - "@mui/x-date-pickers": "^5.0.20", + "@mui/icons-material": "^6.1.6", + "@mui/material": "^6.1.6", + "@mui/x-date-pickers": "^7.22.1", "@reduxjs/toolkit": "^2.2.6", "@safe-global/api-kit": "^2.4.6", "@safe-global/protocol-kit": "^4.1.1", "@safe-global/safe-apps-sdk": "^9.1.0", + "@safe-global/safe-client-gateway-sdk": "v1.60.1", "@safe-global/safe-deployments": "1.37.12", - "@safe-global/safe-client-gateway-sdk": "1.60.1-next-069fa2b", "@safe-global/safe-gateway-typescript-sdk": "3.22.3-beta.15", "@safe-global/safe-modules-deployments": "^2.2.1", "@sentry/react": "^7.91.0", @@ -82,11 +84,11 @@ "idb-keyval": "^6.2.1", "js-cookie": "^3.0.1", "lodash": "^4.17.21", - "next": "^14.2.13", + "next": "15.0.2", "papaparse": "^5.3.2", "qrcode.react": "^3.1.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "19.0.0-rc-02c0e824-20241028", + "react-dom": "19.0.0-rc-02c0e824-20241028", "react-dropzone": "^14.2.3", "react-gtm-module": "^2.0.11", "react-hook-form": "7.41.1", @@ -98,11 +100,13 @@ "devDependencies": { "@chromatic-com/storybook": "^1.3.1", "@cowprotocol/app-data": "^2.1.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.14.0", "@faker-js/faker": "^9.0.3", "@mdx-js/loader": "^3.0.1", "@mdx-js/react": "^3.0.1", - "@next/bundle-analyzer": "^13.5.6", - "@next/mdx": "^14.2.11", + "@next/bundle-analyzer": "15.0.2", + "@next/mdx": "15.0.2", "@openzeppelin/contracts": "^4.9.6", "@safe-global/safe-core-sdk-types": "^5.0.1", "@sentry/types": "^7.74.0", @@ -117,10 +121,10 @@ "@storybook/react": "^8.0.6", "@storybook/test": "^8.0.6", "@svgr/webpack": "^6.3.1", - "@testing-library/cypress": "^8.0.7", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.3.0", - "@testing-library/user-event": "^14.4.2", + "@testing-library/cypress": "^10.0.2", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", "@typechain/ethers-v6": "^0.5.1", "@types/jest": "^29.5.4", "@types/js-cookie": "^3.0.6", @@ -128,30 +132,30 @@ "@types/mdx": "^2.0.13", "@types/node": "18.11.18", "@types/qrcode": "^1.5.5", - "@types/react": "^18.3.10", - "@types/react-dom": "^18.3.0", + "@types/react": "npm:types-react@19.0.0-rc.1", + "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "@types/react-gtm-module": "^2.0.3", "@types/semver": "^7.3.10", "@typescript-eslint/eslint-plugin": "^7.6.0", "@walletconnect/types": "^2.16.1", "cross-env": "^7.0.3", - "cypress": "^12.15.0", + "cypress": "^13.15.2", "cypress-file-upload": "^5.0.8", - "cypress-visual-regression": "^5.0.2", - "eslint": "^8.57.0", - "eslint-config-next": "^14.1.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-no-only-tests": "^3.1.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-storybook": "^0.8.0", - "eslint-plugin-unused-imports": "^2.0.0", + "cypress-visual-regression": "^5.2.2", + "eslint": "^9.14.0", + "eslint-config-next": "^15.0.2", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-no-only-tests": "^3.3.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-storybook": "^0.11.0", + "eslint-plugin-unused-imports": "^4.1.4", "fake-indexeddb": "^4.0.2", "gray-matter": "^4.0.3", "husky": "^9.0.11", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", "mockdate": "^3.0.5", - "prettier": "^2.7.0", + "prettier": "^3.3.3", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-heading-id": "^1.0.1", diff --git a/public/fonts/fonts.css b/public/fonts/fonts.css index 6dc970e07d..4c1a1dc164 100644 --- a/public/fonts/fonts.css +++ b/public/fonts/fonts.css @@ -2,6 +2,7 @@ font-family: 'DM Sans'; font-display: swap; font-weight: 400; + /** check that the font is loaded on the website. IDEs fail to find the file */ src: url('/fonts/DMSansRegular.woff2') format('woff2'); } @@ -9,5 +10,6 @@ font-family: 'DM Sans'; font-display: swap; font-weight: bold; + /** check that the font is loaded on the website. IDEs fail to find the file */ src: url('/fonts/DMSans700.woff2') format('woff2'); } diff --git a/src/components/address-book/AddressBookHeader/index.tsx b/src/components/address-book/AddressBookHeader/index.tsx index c1b861103a..13e389dbbf 100644 --- a/src/components/address-book/AddressBookHeader/index.tsx +++ b/src/components/address-book/AddressBookHeader/index.tsx @@ -55,7 +55,13 @@ function AddressBookHeader({ title="Address book" noBorder action={ - + - + Import diff --git a/src/components/address-book/AddressBookTable/index.tsx b/src/components/address-book/AddressBookTable/index.tsx index f1ee73aeec..6d0263e5ab 100644 --- a/src/components/address-book/AddressBookTable/index.tsx +++ b/src/components/address-book/AddressBookTable/index.tsx @@ -145,12 +145,16 @@ function AddressBookTable({ chain, setTxFlow }: AddressBookTableProps) { searchQuery={searchQuery} onSearchQueryChange={setSearchQuery} /> -
{filteredEntries.length > 0 ? ( ) : ( - + } text={`No entries found${chain ? ` on ${chain.chainName}` : ''}`} @@ -158,11 +162,8 @@ function AddressBookTable({ chain, setTxFlow }: AddressBookTableProps) { )}
- {open[ModalType.EXPORT] && } - {open[ModalType.IMPORT] && } - {open[ModalType.ENTRY] && ( )} - {open[ModalType.REMOVE] && } ) diff --git a/src/components/address-book/EntryDialog/index.tsx b/src/components/address-book/EntryDialog/index.tsx index 0d3ceb9852..e22428fd54 100644 --- a/src/components/address-book/EntryDialog/index.tsx +++ b/src/components/address-book/EntryDialog/index.tsx @@ -61,7 +61,11 @@ function EntryDialog({
- + diff --git a/src/components/address-book/ExportDialog/index.tsx b/src/components/address-book/ExportDialog/index.tsx index 15256feaae..9f089812de 100644 --- a/src/components/address-book/ExportDialog/index.tsx +++ b/src/components/address-book/ExportDialog/index.tsx @@ -71,7 +71,11 @@ function ExportDialog({ . - + - diff --git a/src/components/balances/AssetsTable/index.tsx b/src/components/balances/AssetsTable/index.tsx index 077bfed573..7b1a06f41b 100644 --- a/src/components/balances/AssetsTable/index.tsx +++ b/src/components/balances/AssetsTable/index.tsx @@ -157,7 +157,11 @@ const AssetsTable = ({ rawValue: rawFiatValue, collapsed: item.tokenInfo.address === hidingAsset, content: ( - + {rawFiatValue === 0 && ( @@ -185,7 +189,14 @@ const AssetsTable = ({ sticky: true, collapsed: item.tokenInfo.address === hidingAsset, content: ( - + <> diff --git a/src/components/balances/HiddenTokenButton/index.tsx b/src/components/balances/HiddenTokenButton/index.tsx index 18ab57dbc1..e0a30e3514 100644 --- a/src/components/balances/HiddenTokenButton/index.tsx +++ b/src/components/balances/HiddenTokenButton/index.tsx @@ -38,7 +38,11 @@ const HiddenTokenButton = ({ > <> - + {hiddenAssetCount === 0 ? 'Hide tokens' : `${hiddenAssetCount} hidden token${hiddenAssetCount > 1 ? 's' : ''}`}{' '} diff --git a/src/components/balances/TokenListSelect/index.tsx b/src/components/balances/TokenListSelect/index.tsx index e300bc4e16..beb7111e54 100644 --- a/src/components/balances/TokenListSelect/index.tsx +++ b/src/components/balances/TokenListSelect/index.tsx @@ -35,7 +35,6 @@ const TokenListSelect = () => { return ( Token list - { > - + {TokenListLabel.TRUSTED} - + {selectedAssetCount} {selectedAssetCount === 1 ? 'token' : 'tokens'} selected - + diff --git a/src/components/common/EnhancedTable/index.tsx b/src/components/common/EnhancedTable/index.tsx index 1be2541188..72086bed01 100644 --- a/src/components/common/EnhancedTable/index.tsx +++ b/src/components/common/EnhancedTable/index.tsx @@ -177,7 +177,6 @@ function EnhancedTable({ rows, headCells, mobileVariant }: EnhancedTableProps) { - {rows.length > pagedRows.length && ( { return (
- + Something went wrong,
please try again. @@ -25,7 +30,11 @@ const ErrorBoundary = ({ error, componentStack }: ErrorBoundaryProps) => { {IS_PRODUCTION ? ( - + In case the problem persists, please reach out to us via our{' '} Help Center @@ -36,7 +45,13 @@ const ErrorBoundary = ({ error, componentStack }: ErrorBoundaryProps) => { {componentStack} )} - + Go home
diff --git a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx index 39cb29ae58..bd1d15b8c7 100644 --- a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx @@ -81,11 +81,28 @@ const SrcEthHashInfo = ({ )}
)} - - + {name && ( - - + + {name} @@ -101,7 +118,14 @@ const SrcEthHashInfo = ({
{(!onlyName || !name) && ( - + {copyAddress ? ( {addressElement} @@ -117,7 +141,11 @@ const SrcEthHashInfo = ({ )} {hasExplorer && ExplorerButtonProps && ( - + )} diff --git a/src/components/common/ExplorerButton/index.tsx b/src/components/common/ExplorerButton/index.tsx index 4bc6db7373..d14c7e7df1 100644 --- a/src/components/common/ExplorerButton/index.tsx +++ b/src/components/common/ExplorerButton/index.tsx @@ -44,8 +44,20 @@ const ExplorerButton = ({ href={href} onClick={onClick} > - - + + View on explorer diff --git a/src/components/common/ExternalLink/index.tsx b/src/components/common/ExternalLink/index.tsx index 30b96958fc..9090c38177 100644 --- a/src/components/common/ExternalLink/index.tsx +++ b/src/components/common/ExternalLink/index.tsx @@ -15,7 +15,14 @@ const ExternalLink = ({ return ( - + {children} {!noIcon && } diff --git a/src/components/common/FiatValue/index.tsx b/src/components/common/FiatValue/index.tsx index d934a7b284..e7fb2daf3b 100644 --- a/src/components/common/FiatValue/index.tsx +++ b/src/components/common/FiatValue/index.tsx @@ -38,7 +38,14 @@ const FiatValue = ({ <> {whole} {decimals && ( - + {decimals} )} diff --git a/src/components/common/FileUpload/index.tsx b/src/components/common/FileUpload/index.tsx index 255971f78d..7ccd6315b0 100644 --- a/src/components/common/FileUpload/index.tsx +++ b/src/components/common/FileUpload/index.tsx @@ -23,8 +23,22 @@ const ColoredFileIcon = ({ color }: { color: SvgIconTypeMap['props']['color'] }) const UploadSummary = ({ fileInfo, onRemove }: { fileInfo: FileInfo; onRemove: (() => void) | MouseEventHandler }) => { return ( - - + + @@ -33,18 +47,40 @@ const UploadSummary = ({ fileInfo, onRemove }: { fileInfo: FileInfo; onRemove: ( {fileInfo.additionalInfo && ` - ${fileInfo.additionalInfo}`} - + - +
<> {fileInfo.summary.map((summaryItem, idx) => ( - + @@ -54,7 +90,14 @@ const UploadSummary = ({ fileInfo, onRemove }: { fileInfo: FileInfo; onRemove: ( ))} {fileInfo.error && ( - + @@ -105,8 +148,13 @@ const FileUpload = ({ }} > {getInputProps && } - - + ( +}): ReactElement => (
{withTitle && ( - + Disclaimer )}
- + You are now accessing {isSafeApps ? 'third-party apps' : 'a third-party app'}, which we do not own, control, maintain or audit. We are not liable for any loss you may suffer in connection with interacting with the{' '} {isSafeApps ? 'apps' : 'app'}, which is at your own risk. - + You must read our Terms, which contain more detailed provisions binding on you relating to the{' '} {isSafeApps ? 'apps' : 'app'}. diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index 0bdc3904d1..be1c268b38 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -133,7 +133,14 @@ const UndeployedNetworkMenuItem = ({ const NetworkSkeleton = () => { return ( - + @@ -143,7 +150,12 @@ const NetworkSkeleton = () => { const TestnetDivider = () => { return ( - + Testnets @@ -196,7 +208,14 @@ const UndeployedNetworks = ({ if (safeCreationLoading) { return ( - + ) @@ -204,7 +223,13 @@ const UndeployedNetworks = ({ const errorMessage = safeCreationDataError || (safeCreationData && noAvailableNetworks) ? ( - + {safeCreationDataError?.message && ( @@ -220,8 +245,19 @@ const UndeployedNetworks = ({ if (errorMessage) { return ( - - + + {errorMessage} @@ -241,8 +277,15 @@ const UndeployedNetworks = ({ return ( <> - +
Show all networks
+ {!safeCreationData ? ( - + diff --git a/src/components/common/Notifications/index.tsx b/src/components/common/Notifications/index.tsx index 6cf42948f4..6c90f8691f 100644 --- a/src/components/common/Notifications/index.tsx +++ b/src/components/common/Notifications/index.tsx @@ -75,7 +75,12 @@ const Toast = ({ {title && ( - + {title} )} diff --git a/src/components/common/OnboardingTooltip/index.tsx b/src/components/common/OnboardingTooltip/index.tsx index c5868791dc..3d2bdf88c9 100644 --- a/src/components/common/OnboardingTooltip/index.tsx +++ b/src/components/common/OnboardingTooltip/index.tsx @@ -39,7 +39,14 @@ export const OnboardingTooltip = ({ placement={placement} arrow title={ - +
{text}
)) ) : ( - - + + Connect your wallet to continue diff --git a/src/components/new-safe/create/index.tsx b/src/components/new-safe/create/index.tsx index 53409fb77a..52a29f56ec 100644 --- a/src/components/new-safe/create/index.tsx +++ b/src/components/new-safe/create/index.tsx @@ -183,13 +183,32 @@ const CreateSafe = () => { return ( - + - + Create new Safe Account - + { /> - + {activeStep < 2 && } {wallet?.address && } diff --git a/src/components/new-safe/create/steps/AdvancedOptionsStep/index.tsx b/src/components/new-safe/create/steps/AdvancedOptionsStep/index.tsx index 46aa38cba9..71314ad37b 100644 --- a/src/components/new-safe/create/steps/AdvancedOptionsStep/index.tsx +++ b/src/components/new-safe/create/steps/AdvancedOptionsStep/index.tsx @@ -101,7 +101,15 @@ const AdvancedOptionsStep = ({ onSubmit, onBack, data, setStep }: StepRenderProp - + Safe version - + Changes the used master copy and fallback handler of the Safe. - + Salt nonce - + Impacts the derived Safe address - + New Safe address {predictedSafeAddress ? ( @@ -178,7 +210,14 @@ const AdvancedOptionsStep = ({ onSubmit, onBack, data, setStep }: StepRenderProp - + diff --git a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx index 2504c4c0b2..ee8d0eea2c 100644 --- a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx +++ b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx @@ -63,17 +63,46 @@ const StatusMessage = ({ return ( <> - - + + {isError ? : } - + {stepInfo.description} - + {stepInfo.instruction && ( - + {stepInfo.instruction} )} diff --git a/src/components/new-safe/create/steps/StatusStep/StatusStep.tsx b/src/components/new-safe/create/steps/StatusStep/StatusStep.tsx index 56c2df1994..c05b4ca778 100644 --- a/src/components/new-safe/create/steps/StatusStep/StatusStep.tsx +++ b/src/components/new-safe/create/steps/StatusStep/StatusStep.tsx @@ -23,13 +23,18 @@ const StatusStep = ({ icon={} > (isLoading ? palette.border.main : palette.text.primary) }} + sx={{ + display: 'flex', + alignItems: 'center', + gap: 2, + color: ({ palette }) => (isLoading ? palette.border.main : palette.text.primary), + }} > - + {safeAddress && !isLoading ? ( ) : ( diff --git a/src/components/new-safe/create/steps/StatusStep/index.tsx b/src/components/new-safe/create/steps/StatusStep/index.tsx index 546ceaa94d..14e8e37ea9 100644 --- a/src/components/new-safe/create/steps/StatusStep/index.tsx +++ b/src/components/new-safe/create/steps/StatusStep/index.tsx @@ -101,24 +101,45 @@ export const CreateSafeStatus = ({ textAlign: 'center', }} > - + {counter && counter > SPEED_UP_THRESHOLD_IN_SECONDS && !isError && ( } sx={{ mt: 5 }}> - + Transaction is taking too long - + Try to speed it up with better gas parameters in your wallet. )} {isError && ( - + diff --git a/src/components/new-safe/load/steps/SafeReviewStep/index.tsx b/src/components/new-safe/load/steps/SafeReviewStep/index.tsx index 3ebd7ae2d7..e65efc693d 100644 --- a/src/components/new-safe/load/steps/SafeReviewStep/index.tsx +++ b/src/components/new-safe/load/steps/SafeReviewStep/index.tsx @@ -126,7 +126,14 @@ const SafeReviewStep = ({ data, onBack }: StepRenderProps) => - + diff --git a/src/components/new-safe/load/steps/SetAddressStep/index.tsx b/src/components/new-safe/load/steps/SetAddressStep/index.tsx index 9b85dfe8f9..47306854d8 100644 --- a/src/components/new-safe/load/steps/SetAddressStep/index.tsx +++ b/src/components/new-safe/load/steps/SetAddressStep/index.tsx @@ -101,7 +101,14 @@ const SetAddressStep = ({ data, onSubmit, onBack }: StepRenderProps - + - + @@ -141,7 +153,11 @@ const SetAddressStep = ({ data, onSubmit, onBack }: StepRenderProps - + By continuing you consent to the{' '} terms of use @@ -157,7 +173,14 @@ const SetAddressStep = ({ data, onSubmit, onBack }: StepRenderProps - + diff --git a/src/components/nfts/NftCollections/index.tsx b/src/components/nfts/NftCollections/index.tsx index 262691e0f7..02875bcf1a 100644 --- a/src/components/nfts/NftCollections/index.tsx +++ b/src/components/nfts/NftCollections/index.tsx @@ -70,7 +70,6 @@ const NftCollections = (): ReactElement => { {/* Batch send form */} - {/* NFTs table */} { )} - {/* NFT preview */} setPreviewNft(undefined)} nft={previewNft} /> diff --git a/src/components/nfts/NftGrid/index.tsx b/src/components/nfts/NftGrid/index.tsx index e35662b966..c44fc71bab 100644 --- a/src/components/nfts/NftGrid/index.tsx +++ b/src/components/nfts/NftGrid/index.tsx @@ -152,7 +152,14 @@ const NftGrid = ({ }} > {headCell.id === 'collection' ? ( - + {/* Collection name */} - + {item.imageUri ? activeNftIcon : inactiveNftIcon}
{item.tokenName || item.tokenSymbol} - + - {/* Token ID */} {item.name || `${item.tokenSymbol} #${item.id.slice(0, 20)}`} - {/* Links */} - + {linkTemplates?.map(({ title, logo, getUrl }) => ( {title} @@ -238,7 +262,6 @@ const NftGrid = ({ ))} - {/* Checkbox */} - + ))} diff --git a/src/components/nfts/NftSendForm/index.tsx b/src/components/nfts/NftSendForm/index.tsx index 9665dcd4e3..92948fd0b5 100644 --- a/src/components/nfts/NftSendForm/index.tsx +++ b/src/components/nfts/NftSendForm/index.tsx @@ -15,13 +15,46 @@ const NftSendForm = ({ selectedNfts }: NftSendFormProps): ReactElement => { return ( - - - - + + + + - + {`${selectedNfts.length} ${nftsText} selected`} diff --git a/src/components/notification-center/NotificationCenter/index.tsx b/src/components/notification-center/NotificationCenter/index.tsx index bd7c47371e..dd28e56d9c 100644 --- a/src/components/notification-center/NotificationCenter/index.tsx +++ b/src/components/notification-center/NotificationCenter/index.tsx @@ -108,7 +108,6 @@ const NotificationCenter = (): ReactElement => { - {
- + Notifications {hasUnread && ( diff --git a/src/components/notification-center/NotificationCenterItem/index.tsx b/src/components/notification-center/NotificationCenterItem/index.tsx index 4af8e6dd02..8fb0e70a78 100644 --- a/src/components/notification-center/NotificationCenterItem/index.tsx +++ b/src/components/notification-center/NotificationCenterItem/index.tsx @@ -49,7 +49,15 @@ const NotificationCenterItem = ({ const primaryText = ( <> - {title && {title}} + {title && ( + + {title} + + )} {message} ) diff --git a/src/components/notification-center/NotificationCenterList/index.tsx b/src/components/notification-center/NotificationCenterList/index.tsx index 8be6e28e82..a1f5875a30 100644 --- a/src/components/notification-center/NotificationCenterList/index.tsx +++ b/src/components/notification-center/NotificationCenterList/index.tsx @@ -19,7 +19,13 @@ const NotificationCenterList = ({ notifications, handleClose }: NotificationCent return (
- No notifications + + No notifications +
) } diff --git a/src/components/safe-apps/AddCustomAppModal/CustomApp.tsx b/src/components/safe-apps/AddCustomAppModal/CustomApp.tsx index 6247f0f3b7..fddf7cb567 100644 --- a/src/components/safe-apps/AddCustomAppModal/CustomApp.tsx +++ b/src/components/safe-apps/AddCustomAppModal/CustomApp.tsx @@ -22,15 +22,25 @@ const CustomApp = ({ safeApp, shareUrl }: CustomAppProps) => { return (
- - + {safeApp.name} - - + {safeApp.description} - {shareUrl ? ( - + {safeApp ? ( <> {isCustomAppInTheDefaultList ? ( - + - This Safe App is already registered + + This Safe App is already registered + ) : ( <> @@ -150,7 +166,13 @@ export const AddCustomAppModal = ({ open, onClose, onSave, safeAppsList }: Props
- Learn more about building + + Learn more about building + Safe Apps diff --git a/src/components/safe-apps/AddCustomSafeAppCard/index.tsx b/src/components/safe-apps/AddCustomSafeAppCard/index.tsx index aaf1cef170..77455963f5 100644 --- a/src/components/safe-apps/AddCustomSafeAppCard/index.tsx +++ b/src/components/safe-apps/AddCustomSafeAppCard/index.tsx @@ -15,7 +15,14 @@ const AddCustomSafeAppCard = ({ onSave, safeAppList }: Props) => { return ( <> - + {/* Add Custom Safe App Icon */} @@ -32,7 +39,6 @@ const AddCustomSafeAppCard = ({ onSave, safeAppList }: Props) => { - {/* Add Custom Safe App Modal */} - + {barTitle} @@ -81,7 +88,13 @@ const TransactionQueueBar = ({ - + diff --git a/src/components/safe-apps/AppFrame/index.tsx b/src/components/safe-apps/AppFrame/index.tsx index 1d429f5776..381c226f73 100644 --- a/src/components/safe-apps/AppFrame/index.tsx +++ b/src/components/safe-apps/AppFrame/index.tsx @@ -131,7 +131,11 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest, isNativeEm {`Safe Apps - Viewer - ${remoteApp ? remoteApp.name : UNKNOWN_APP_NAME}`} - + diff --git a/src/components/safe-apps/AppFrame/useAppIsLoading.ts b/src/components/safe-apps/AppFrame/useAppIsLoading.ts index ab1d74c94b..3fd8c2163c 100644 --- a/src/components/safe-apps/AppFrame/useAppIsLoading.ts +++ b/src/components/safe-apps/AppFrame/useAppIsLoading.ts @@ -5,7 +5,7 @@ const APP_SLOW_LOADING_WARNING_TIMEOUT = 15_000 const APP_LOAD_ERROR = 'There was an error loading the Safe App. There might be a problem with the Safe App provider.' type UseAppIsLoadingReturnType = { - iframeRef: React.RefObject + iframeRef: React.RefObject appIsLoading: boolean setAppIsLoading: (appIsLoading: boolean) => void isLoadingSlow: boolean @@ -16,9 +16,9 @@ const useAppIsLoading = (): UseAppIsLoadingReturnType => { const [isLoadingSlow, setIsLoadingSlow] = useState(false) const [, setAppLoadError] = useState(false) - const iframeRef = useRef(null) - const timer = useRef() - const errorTimer = useRef() + const iframeRef = useRef(null) + const timer = useRef(0) + const errorTimer = useRef(0) useEffect(() => { const clearTimeouts = () => { diff --git a/src/components/safe-apps/AppFrame/useFromAppAnalytics.ts b/src/components/safe-apps/AppFrame/useFromAppAnalytics.ts index 82f0c44676..12d3795ec8 100644 --- a/src/components/safe-apps/AppFrame/useFromAppAnalytics.ts +++ b/src/components/safe-apps/AppFrame/useFromAppAnalytics.ts @@ -16,7 +16,7 @@ const ALLOWED_DOMAINS: RegExp[] = [ /^https:\/\/safe-dao-governance\.dev\.5afe\.dev$/, ] -const useAnalyticsFromSafeApp = (iframeRef: RefObject): void => { +const useAnalyticsFromSafeApp = (iframeRef: RefObject): void => { const isValidMessage = useCallback( (msg: MessageEvent) => { if (!msg.data) return false diff --git a/src/components/safe-apps/AppFrame/useThirdPartyCookies.ts b/src/components/safe-apps/AppFrame/useThirdPartyCookies.ts index 2722f34e0b..b94c41dac8 100644 --- a/src/components/safe-apps/AppFrame/useThirdPartyCookies.ts +++ b/src/components/safe-apps/AppFrame/useThirdPartyCookies.ts @@ -24,7 +24,7 @@ type ThirdPartyCookiesType = { } const useThirdPartyCookies = (): ThirdPartyCookiesType => { - const iframeRef = useRef() + const iframeRef = useRef(null) const [thirdPartyCookiesDisabled, setThirdPartyCookiesDisabled] = useState(false) const messageHandler = useCallback((event: MessageEvent) => { diff --git a/src/components/safe-apps/NativeSwapsCard/index.tsx b/src/components/safe-apps/NativeSwapsCard/index.tsx index b95e687b72..5eecc9758e 100644 --- a/src/components/safe-apps/NativeSwapsCard/index.tsx +++ b/src/components/safe-apps/NativeSwapsCard/index.tsx @@ -30,17 +30,28 @@ const NativeSwapsCard = () => {
} /> - Native swaps are here! - + Experience seamless trading with better decoding and security in native swaps. - +
- + diff --git a/src/components/safe-apps/SafeAppsInfoModal/index.tsx b/src/components/safe-apps/SafeAppsInfoModal/index.tsx index 33b448aedd..16c76e02fa 100644 --- a/src/components/safe-apps/SafeAppsInfoModal/index.tsx +++ b/src/components/safe-apps/SafeAppsInfoModal/index.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo, useState } from 'react' +import { memo, type ReactElement, useMemo, useState } from 'react' import { alpha, Box } from '@mui/system' import { Grid, LinearProgress } from '@mui/material' @@ -31,7 +31,7 @@ const SafeAppsInfoModal = ({ isPermissionsReviewCompleted, isSafeAppInDefaultList, isFirstTimeAccessingApp, -}: SafeAppsInfoModalProps): JSX.Element => { +}: SafeAppsInfoModalProps): ReactElement => { const [hideWarning, setHideWarning] = useState(false) const [selectedFeatures, setSelectedFeatures] = useState( features.map((feature) => { @@ -110,7 +110,15 @@ const SafeAppsInfoModal = ({ const origin = useMemo(() => getOrigin(appUrl), [appUrl]) return ( - + ({ @@ -133,7 +141,16 @@ const SafeAppsInfoModal = ({ }, })} /> - + {!isConsentAccepted && } diff --git a/src/components/safe-apps/SafeAppsListHeader/index.tsx b/src/components/safe-apps/SafeAppsListHeader/index.tsx index 3257c51fa9..04e295a447 100644 --- a/src/components/safe-apps/SafeAppsListHeader/index.tsx +++ b/src/components/safe-apps/SafeAppsListHeader/index.tsx @@ -7,7 +7,14 @@ type SafeAppsListHeaderProps = { const SafeAppsListHeader = ({ title, amount }: SafeAppsListHeaderProps) => { return ( - + {title} ({amount || 0}) ) diff --git a/src/components/safe-apps/SafeAppsZeroResultsPlaceholder/index.tsx b/src/components/safe-apps/SafeAppsZeroResultsPlaceholder/index.tsx index bd3b411c78..43d453ba8b 100644 --- a/src/components/safe-apps/SafeAppsZeroResultsPlaceholder/index.tsx +++ b/src/components/safe-apps/SafeAppsZeroResultsPlaceholder/index.tsx @@ -7,7 +7,14 @@ const SafeAppsZeroResultsPlaceholder = ({ searchQuery }: { searchQuery: string } } text={ - + No Safe Apps found matching {searchQuery}. Connect to dApps that haven't yet been integrated with the {'Safe{Wallet}'} using WalletConnect. diff --git a/src/components/safe-messages/DecodedMsg/index.tsx b/src/components/safe-messages/DecodedMsg/index.tsx index 88c79f8ebb..4c5afe437f 100644 --- a/src/components/safe-messages/DecodedMsg/index.tsx +++ b/src/components/safe-messages/DecodedMsg/index.tsx @@ -19,14 +19,17 @@ const DecodedTypedObject = ({ displayedType, eip712Msg }: { displayedType: strin return ( ({ color: `${palette.border.main}` })} + sx={[ + { + textTransform: 'uppercase', + fontWeight: 700, + }, + ({ palette }) => ({ color: `${palette.border.main}` }), + ]} > {displayedType} - {Object.entries(displayedType === EIP712_DOMAIN_TYPE ? domain : msg).map((param, index) => { const [paramName, paramValue] = param const type = findType(paramName) || 'string' diff --git a/src/components/safe-messages/InfoBox/index.tsx b/src/components/safe-messages/InfoBox/index.tsx index 34bdff369f..4360710192 100644 --- a/src/components/safe-messages/InfoBox/index.tsx +++ b/src/components/safe-messages/InfoBox/index.tsx @@ -20,7 +20,12 @@ const InfoBox = ({
- + {title} {message} diff --git a/src/components/safe-messages/MsgDetails/index.tsx b/src/components/safe-messages/MsgDetails/index.tsx index fa2b1c7a78..784862daea 100644 --- a/src/components/safe-messages/MsgDetails/index.tsx +++ b/src/components/safe-messages/MsgDetails/index.tsx @@ -118,7 +118,15 @@ const MsgDetails = ({ msg }: { msg: SafeMessage }): ReactElement => {
{wallet && !isConfirmed && ( - + )} diff --git a/src/components/safe-messages/MsgSigners/index.tsx b/src/components/safe-messages/MsgSigners/index.tsx index a24c084ce4..96f64fd05d 100644 --- a/src/components/safe-messages/MsgSigners/index.tsx +++ b/src/components/safe-messages/MsgSigners/index.tsx @@ -106,7 +106,13 @@ export const MsgSigners = ({ - + {hideConfirmations ? 'Show all' : 'Hide all'} @@ -119,9 +125,21 @@ export const MsgSigners = ({ - + - + Confirmation #{idx + 1 + confirmationsSubmitted} diff --git a/src/components/safe-messages/MsgSummary/index.tsx b/src/components/safe-messages/MsgSummary/index.tsx index 7c8814263c..a8e1057cf1 100644 --- a/src/components/safe-messages/MsgSummary/index.tsx +++ b/src/components/safe-messages/MsgSummary/index.tsx @@ -63,7 +63,7 @@ const MsgSummary = ({ msg }: { msg: SafeMessage }): ReactElement => { display="flex" alignItems="center" gap={1} - color={({ palette }) => getStatusColor(msg.status, palette)} + sx={{ color: ({ palette }) => getStatusColor(msg.status, palette) }} > {isPending && } diff --git a/src/components/safe-messages/PaginatedMsgs/index.tsx b/src/components/safe-messages/PaginatedMsgs/index.tsx index b572223094..d140c9b876 100644 --- a/src/components/safe-messages/PaginatedMsgs/index.tsx +++ b/src/components/safe-messages/PaginatedMsgs/index.tsx @@ -19,13 +19,27 @@ const NoMessages = (): ReactElement => { } text={ - + Some applications allow you to interact with them via off-chain contract signatures (“messages“) that you can generate with your Safe Account. } > - + Learn more about off-chain messages{' '} @@ -45,15 +59,16 @@ const MsgPage = ({ return ( <> {page && page.results.length > 0 && } - {page?.results.length === 0 && } - {error && Error loading messages} - {loading && } - {page?.next && onNextPage && ( - + onNextPage(page.next!)} /> )} @@ -76,7 +91,12 @@ const PaginatedMsgs = (): ReactElement => { }, [safe.chainId, safeAddress]) return ( - + {pages.map((pageUrl, index) => ( ))} diff --git a/src/components/settings/ContractVersion/index.tsx b/src/components/settings/ContractVersion/index.tsx index 151548f3dc..935192a9bd 100644 --- a/src/components/settings/ContractVersion/index.tsx +++ b/src/components/settings/ContractVersion/index.tsx @@ -33,11 +33,23 @@ export const ContractVersion = () => { return ( <> - + Contract version - - + {safeLoaded ? ( <> {safe.version ?? 'Unsupported contract'} @@ -51,7 +63,6 @@ export const ContractVersion = () => { )} - {safeLoaded && safe.version && showUpdateDialog && ( { > New version is available: {latestSafeVersion} - + Update now to take advantage of new features and the highest security standards available. You will need to confirm this update just like any other transaction.{' '} GitHub diff --git a/src/components/settings/DataManagement/ImportDialog.tsx b/src/components/settings/DataManagement/ImportDialog.tsx index b6668e5c71..5b4f0ed385 100644 --- a/src/components/settings/DataManagement/ImportDialog.tsx +++ b/src/components/settings/DataManagement/ImportDialog.tsx @@ -88,7 +88,11 @@ export const ImportDialog = ({ {!jsonData || !fileName ? ( - + ) : ( diff --git a/src/components/settings/DataManagement/index.tsx b/src/components/settings/DataManagement/index.tsx index f753c119b2..a5c8b2bd1e 100644 --- a/src/components/settings/DataManagement/index.tsx +++ b/src/components/settings/DataManagement/index.tsx @@ -74,7 +74,12 @@ const DataManagement = () => { - + Data export @@ -105,11 +110,15 @@ const DataManagement = () => { - - + Data import diff --git a/src/components/settings/EnvironmentVariables/index.tsx b/src/components/settings/EnvironmentVariables/index.tsx index d2b45729ae..479da4ce10 100644 --- a/src/components/settings/EnvironmentVariables/index.tsx +++ b/src/components/settings/EnvironmentVariables/index.tsx @@ -70,21 +70,44 @@ const EnvironmentVariables = () => { return ( - + - + Environment variables - + You can override some of our default APIs here in case you need to. Proceed at your own risk.
- + RPC provider { fullWidth /> - + Tenderly { name: 'FallbackHandlerName', }, }, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const fbHandler = render(, { @@ -71,7 +71,7 @@ describe('FallbackHandler', () => { name: 'FallbackHandlerName', }, }, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const fbHandler = render(, { @@ -104,7 +104,7 @@ describe('FallbackHandler', () => { value: GOERLI_FALLBACK_HANDLER, }, }, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const fbHandler = render(, { @@ -125,7 +125,7 @@ describe('FallbackHandler', () => { version: '1.3.0', chainId: '5', }, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const fbHandler = render() @@ -150,7 +150,7 @@ describe('FallbackHandler', () => { version: '1.3.0', chainId: '5', }, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const fbHandler = render() @@ -178,7 +178,7 @@ describe('FallbackHandler', () => { value: '0x123', }, }, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const fbHandler = render() @@ -212,7 +212,7 @@ describe('FallbackHandler', () => { value: '0x123', }, }, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const fbHandler = render() @@ -232,7 +232,7 @@ describe('FallbackHandler', () => { version: '1.0.0', chainId: '5', }, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const fbHandler = render() @@ -251,7 +251,7 @@ describe('FallbackHandler', () => { value: TWAP_FALLBACK_HANDLER, }, }, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { getByText } = render() @@ -274,7 +274,7 @@ describe('FallbackHandler', () => { value: TWAP_FALLBACK_HANDLER, }, }, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { queryByText } = render() diff --git a/src/components/settings/FallbackHandler/index.tsx b/src/components/settings/FallbackHandler/index.tsx index 3bdd6f8ff2..1fa5e4f582 100644 --- a/src/components/settings/FallbackHandler/index.tsx +++ b/src/components/settings/FallbackHandler/index.tsx @@ -75,9 +75,21 @@ export const FallbackHandler = (): ReactElement | null => { return ( - + - + Fallback handler @@ -96,7 +108,12 @@ export const FallbackHandler = (): ReactElement | null => { sx={{ mt: 2 }} > {warning && ( - + {warning} )} diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index c0374ddb51..e50ee1e350 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -374,7 +374,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { if (totalNotifiableSafes === 0) { return ( - palette.primary.light}> + palette.primary.light }}> {address ? 'No owned Safes' : 'No wallet connected'} ) diff --git a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts index c0a6cb1068..5be1b4c375 100644 --- a/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts +++ b/src/components/settings/PushNotifications/hooks/__tests__/useNotificationRegistrations.test.ts @@ -35,7 +35,7 @@ describe('useNotificationRegistrations', () => { () => ({ label: 'MetaMask', - } as ConnectedWallet), + }) as ConnectedWallet, ) }) @@ -75,7 +75,7 @@ describe('useNotificationRegistrations', () => { () => ({ uuid: undefined, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { result } = renderHook(() => useNotificationRegistrations()) @@ -105,7 +105,7 @@ describe('useNotificationRegistrations', () => { ({ uuid: self.crypto.randomUUID(), createPreferences: createPreferencesMock, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { result } = renderHook(() => useNotificationRegistrations()) @@ -137,7 +137,7 @@ describe('useNotificationRegistrations', () => { ({ uuid: self.crypto.randomUUID(), createPreferences: createPreferencesMock, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { result } = renderHook(() => useNotificationRegistrations()) @@ -168,7 +168,7 @@ describe('useNotificationRegistrations', () => { ({ uuid: self.crypto.randomUUID(), createPreferences: createPreferencesMock, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const showNotificationSpy = jest.spyOn(notificationsSlice, 'showNotification') @@ -197,7 +197,7 @@ describe('useNotificationRegistrations', () => { () => ({ uuid: undefined, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { result } = renderHook(() => useNotificationRegistrations()) @@ -219,7 +219,7 @@ describe('useNotificationRegistrations', () => { ({ uuid, deletePreferences: deletePreferencesMock, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { result } = renderHook(() => useNotificationRegistrations()) @@ -245,7 +245,7 @@ describe('useNotificationRegistrations', () => { ({ uuid, deletePreferences: deletePreferencesMock, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { result } = renderHook(() => useNotificationRegistrations()) @@ -271,7 +271,7 @@ describe('useNotificationRegistrations', () => { ({ uuid, deletePreferences: deletePreferencesMock, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { result } = renderHook(() => useNotificationRegistrations()) @@ -295,7 +295,7 @@ describe('useNotificationRegistrations', () => { () => ({ uuid: undefined, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { result } = renderHook(() => useNotificationRegistrations()) @@ -317,7 +317,7 @@ describe('useNotificationRegistrations', () => { ({ uuid, deleteAllChainPreferences: deleteAllChainPreferencesMock, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { result } = renderHook(() => useNotificationRegistrations()) @@ -340,7 +340,7 @@ describe('useNotificationRegistrations', () => { ({ uuid, deleteAllChainPreferences: deleteAllChainPreferencesMock, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { result } = renderHook(() => useNotificationRegistrations()) @@ -363,7 +363,7 @@ describe('useNotificationRegistrations', () => { ({ uuid, deleteAllChainPreferences: deleteAllChainPreferencesMock, - } as unknown as ReturnType), + }) as unknown as ReturnType, ) const { result } = renderHook(() => useNotificationRegistrations()) diff --git a/src/components/settings/PushNotifications/index.tsx b/src/components/settings/PushNotifications/index.tsx index a03d43453f..4e8b50283e 100644 --- a/src/components/settings/PushNotifications/index.tsx +++ b/src/components/settings/PushNotifications/index.tsx @@ -90,13 +90,24 @@ export const PushNotifications = (): ReactElement => { - + Push notifications - + Enable push notifications for {safeLoaded ? 'this Safe Account' : 'your Safe Accounts'} in your browser with your signature. You will need to enable them again if you clear your browser cache. Learn more @@ -105,7 +116,13 @@ export const PushNotifications = (): ReactElement => { {shouldShowMacHelper && ( - + For macOS users @@ -161,7 +178,12 @@ export const PushNotifications = (): ReactElement => { - + Notification @@ -244,7 +266,12 @@ export const PushNotifications = (): ReactElement => { <> Confirmation requests {!preferences[WebhookType.CONFIRMATION_REQUEST] && ( - + {isOwner ? 'Requires your signature' : 'Only signers'} )} diff --git a/src/components/settings/RequiredConfirmations/index.tsx b/src/components/settings/RequiredConfirmations/index.tsx index 9b6df6aa5a..3701aafa2f 100644 --- a/src/components/settings/RequiredConfirmations/index.tsx +++ b/src/components/settings/RequiredConfirmations/index.tsx @@ -10,18 +10,39 @@ export const RequiredConfirmation = ({ threshold, owners }: { threshold: number; const { setTxFlow } = useContext(TxModalContext) return ( - + - + Required confirmations - Any transaction requires the confirmation of: + + Any transaction requires the confirmation of: + - + {threshold} out of {owners} signers. diff --git a/src/components/settings/SafeAppsPermissions/index.tsx b/src/components/settings/SafeAppsPermissions/index.tsx index f4c8d8fff7..efd312270d 100644 --- a/src/components/settings/SafeAppsPermissions/index.tsx +++ b/src/components/settings/SafeAppsPermissions/index.tsx @@ -101,7 +101,7 @@ const SafeAppsPermissions = (): ReactElement => {
{!domains.length && ( - palette.primary.light}> + palette.primary.light }}> There are no Safe Apps using permissions. )} diff --git a/src/components/settings/SafeAppsSigningMethod/index.tsx b/src/components/settings/SafeAppsSigningMethod/index.tsx index 4d900a6f6e..21e21f880b 100644 --- a/src/components/settings/SafeAppsSigningMethod/index.tsx +++ b/src/components/settings/SafeAppsSigningMethod/index.tsx @@ -19,13 +19,23 @@ export const SafeAppsSigningMethod = () => { - + Signing method - + This setting determines how the {'Safe{Wallet}'} will sign message requests from Safe Apps. Gasless, off-chain signing is used by default. Learn more about message signing{' '} here. diff --git a/src/components/settings/SafeModules/index.tsx b/src/components/settings/SafeModules/index.tsx index b53a933867..443894826f 100644 --- a/src/components/settings/SafeModules/index.tsx +++ b/src/components/settings/SafeModules/index.tsx @@ -16,7 +16,7 @@ import css from '../TransactionGuards/styles.module.css' const NoModules = () => { return ( - palette.primary.light}> + palette.primary.light }}> No modules enabled ) diff --git a/src/components/settings/SecurityLogin/index.tsx b/src/components/settings/SecurityLogin/index.tsx index d2ba01e986..1ba2421e76 100644 --- a/src/components/settings/SecurityLogin/index.tsx +++ b/src/components/settings/SecurityLogin/index.tsx @@ -11,9 +11,14 @@ const SecurityLogin = () => { const router = useRouter() return ( - + {isRecoverySupported && router.query.safe ? : null} - ) diff --git a/src/components/settings/SecuritySettings/index.tsx b/src/components/settings/SecuritySettings/index.tsx index 44759ba3f4..244dc212a2 100644 --- a/src/components/settings/SecuritySettings/index.tsx +++ b/src/components/settings/SecuritySettings/index.tsx @@ -10,13 +10,23 @@ const SecuritySettings = () => { - + Security - + Enabling this setting allows the signing of unreadable signature requests. Signing these messages can lead to unpredictable consequences, including the potential loss of funds or control over your account. diff --git a/src/components/settings/SpendingLimits/NoSpendingLimits.tsx b/src/components/settings/SpendingLimits/NoSpendingLimits.tsx index 41f37fb545..e67ac8ea79 100644 --- a/src/components/settings/SpendingLimits/NoSpendingLimits.tsx +++ b/src/components/settings/SpendingLimits/NoSpendingLimits.tsx @@ -6,7 +6,15 @@ import TimeIcon from '@/public/images/settings/spending-limit/time.svg' export const NoSpendingLimits = () => { return ( - + @@ -19,7 +27,6 @@ export const NoSpendingLimits = () => { Safe Account - @@ -29,7 +36,6 @@ export const NoSpendingLimits = () => { You can set allowances for any asset stored in your Safe Account - diff --git a/src/components/settings/SpendingLimits/SpendingLimitsTable.tsx b/src/components/settings/SpendingLimits/SpendingLimitsTable.tsx index d9f18a3b47..8d2d0307b6 100644 --- a/src/components/settings/SpendingLimits/SpendingLimitsTable.tsx +++ b/src/components/settings/SpendingLimits/SpendingLimitsTable.tsx @@ -20,7 +20,14 @@ const SKELETON_ROWS = new Array(3).fill('').map(() => { beneficiary: { rawValue: '0x', content: ( - +
@@ -36,7 +43,14 @@ const SKELETON_ROWS = new Array(3).fill('').map(() => { spent: { rawValue: '0', content: ( - + @@ -97,7 +111,14 @@ export const SpendingLimitsTable = ({ spent: { rawValue: spendingLimit.spent, content: ( - + {`${formattedSpent} of ${formattedAmount} ${spendingLimit.token.symbol}`} diff --git a/src/components/settings/SpendingLimits/index.tsx b/src/components/settings/SpendingLimits/index.tsx index 77c779a2d7..9f26cd8b61 100644 --- a/src/components/settings/SpendingLimits/index.tsx +++ b/src/components/settings/SpendingLimits/index.tsx @@ -20,9 +20,22 @@ const SpendingLimits = () => { return ( - + - + Spending limits diff --git a/src/components/settings/TransactionGuards/index.tsx b/src/components/settings/TransactionGuards/index.tsx index 470c7f4fc7..4fe1c357fb 100644 --- a/src/components/settings/TransactionGuards/index.tsx +++ b/src/components/settings/TransactionGuards/index.tsx @@ -15,7 +15,7 @@ import { RemoveGuardFlow } from '@/components/tx-flow/flows' const NoTransactionGuard = () => { return ( - palette.primary.light}> + palette.primary.light }}> No transaction guard set ) diff --git a/src/components/settings/owner/EditOwnerDialog/index.tsx b/src/components/settings/owner/EditOwnerDialog/index.tsx index b12aab771d..9a31183dff 100644 --- a/src/components/settings/owner/EditOwnerDialog/index.tsx +++ b/src/components/settings/owner/EditOwnerDialog/index.tsx @@ -58,16 +58,23 @@ export const EditOwnerDialog = ({ chainId, address, name }: { chainId: string; a - - + - + diff --git a/src/components/settings/owner/OwnerList/index.tsx b/src/components/settings/owner/OwnerList/index.tsx index d7c4871506..513cae19dc 100644 --- a/src/components/settings/owner/OwnerList/index.tsx +++ b/src/components/settings/owner/OwnerList/index.tsx @@ -90,16 +90,32 @@ export const OwnerList = () => { }, [safe.owners, safe.chainId, addressBook, setTxFlow]) return ( - + - + Members - + Signers @@ -107,7 +123,13 @@ export const OwnerList = () => { reject them. - + {(isOk) => ( diff --git a/src/components/sidebar/DebugToggle/index.tsx b/src/components/sidebar/DebugToggle/index.tsx index eb12300296..9bb058b8c1 100644 --- a/src/components/sidebar/DebugToggle/index.tsx +++ b/src/components/sidebar/DebugToggle/index.tsx @@ -25,7 +25,12 @@ const DebugToggle = (): ReactElement => { } return ( - + dispatch(setDarkMode(checked))} />} label="Dark mode" diff --git a/src/components/sidebar/QrCodeButton/QrModal.tsx b/src/components/sidebar/QrCodeButton/QrModal.tsx index 04ec8df5ba..b7357fe073 100644 --- a/src/components/sidebar/QrCodeButton/QrModal.tsx +++ b/src/components/sidebar/QrCodeButton/QrModal.tsx @@ -21,17 +21,47 @@ const QrModal = ({ onClose }: { onClose: () => void }): ReactElement => { return ( - + {chainName} network — only send {chainName} assets to this Safe Account. - + This is the address of your Safe Account. Deposit funds by scanning the QR code or copying the address below. Only send {nativeToken} and tokens (e.g. ERC20, ERC721) to this address. - - + + @@ -46,7 +76,11 @@ const QrModal = ({ onClose }: { onClose: () => void }): ReactElement => { } /> - + { {/* Nav menu */} - + @@ -54,7 +58,6 @@ const Sidebar = (): ReactElement => {
-
diff --git a/src/components/sidebar/SidebarHeader/index.tsx b/src/components/sidebar/SidebarHeader/index.tsx index 869733670e..11407a4c98 100644 --- a/src/components/sidebar/SidebarHeader/index.tsx +++ b/src/components/sidebar/SidebarHeader/index.tsx @@ -67,7 +67,13 @@ const SafeHeader = (): ReactElement => { )} - + {safe.deployed ? ( balances.fiatTotal ? ( @@ -113,7 +119,6 @@ const SafeHeader = (): ReactElement => {
-
) diff --git a/src/components/sidebar/SidebarNavigation/index.tsx b/src/components/sidebar/SidebarNavigation/index.tsx index 0c77c43cf6..a14541f566 100644 --- a/src/components/sidebar/SidebarNavigation/index.tsx +++ b/src/components/sidebar/SidebarNavigation/index.tsx @@ -1,6 +1,6 @@ import React, { useContext, useMemo, type ReactElement } from 'react' import { useRouter } from 'next/router' -import ListItem from '@mui/material/ListItem' +import { ListItemButton } from '@mui/material' import { ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' import { @@ -99,27 +99,30 @@ const Navigation = (): ReactElement => { key={item.href} arrow > - handleNavigationClick(item.href)} - key={item.href} - > - + handleNavigationClick(item.href)} + key={item.href} > - {item.icon && {item.icon}} - - - {item.label} - - {ItemTag} - - - + + {item.icon && {item.icon}} + + + {item.label} + + {ItemTag} + + + +
) })} diff --git a/src/components/transactions/BulkTxListGroup/index.tsx b/src/components/transactions/BulkTxListGroup/index.tsx index 935c2254c2..f20be867e2 100644 --- a/src/components/transactions/BulkTxListGroup/index.tsx +++ b/src/components/transactions/BulkTxListGroup/index.tsx @@ -39,22 +39,39 @@ const GroupedTxListItems = ({ } return ( - + - + {title} {groupedListItems.length} transactions - - + {groupedListItems.map((tx) => { const nonce = isMultisigExecutionInfo(tx.transaction.executionInfo) ? tx.transaction.executionInfo.nonce : '' return ( - + {nonce} diff --git a/src/components/transactions/GroupedTxListItems/index.tsx b/src/components/transactions/GroupedTxListItems/index.tsx index 424bcdcc89..a58334385a 100644 --- a/src/components/transactions/GroupedTxListItems/index.tsx +++ b/src/components/transactions/GroupedTxListItems/index.tsx @@ -31,15 +31,33 @@ const TxGroup = ({ groupedListItems }: { groupedListItems: Transaction[] }): Rea return ( - {nonce} - - + + {nonce} + + - - - - + + {groupedListItems.map((tx) => (

Data (hex-encoded)

@@ -32,19 +32,16 @@ exports[`HexEncodedData should not cut the text in case the limit option is high >

Some arbitrary data

@@ -94,25 +91,22 @@ exports[`HexEncodedData should not highlight the data if highlight option is fal > 0x10238476... diff --git a/src/components/transactions/MaliciousTxWarning/index.tsx b/src/components/transactions/MaliciousTxWarning/index.tsx index a77b14d725..b714e5b44b 100644 --- a/src/components/transactions/MaliciousTxWarning/index.tsx +++ b/src/components/transactions/MaliciousTxWarning/index.tsx @@ -4,12 +4,20 @@ import WarningIcon from '@/public/images/notifications/warning.svg' const MaliciousTxWarning = ({ withTooltip = true }: { withTooltip?: boolean }) => { return withTooltip ? ( - + ) : ( - + ) diff --git a/src/components/transactions/SignedMessagesHelpLink/index.tsx b/src/components/transactions/SignedMessagesHelpLink/index.tsx index 6c3cfafe14..3f0d7f94b8 100644 --- a/src/components/transactions/SignedMessagesHelpLink/index.tsx +++ b/src/components/transactions/SignedMessagesHelpLink/index.tsx @@ -14,10 +14,21 @@ const SignedMessagesHelpLink = () => { } return ( - + - + What are signed messages? diff --git a/src/components/transactions/TxConfirmations/index.tsx b/src/components/transactions/TxConfirmations/index.tsx index e8a843f14e..7eb7825276 100644 --- a/src/components/transactions/TxConfirmations/index.tsx +++ b/src/components/transactions/TxConfirmations/index.tsx @@ -17,8 +17,12 @@ const TxConfirmations = ({ return ( - - + {submittedConfirmations} out of {requiredConfirmations} diff --git a/src/components/transactions/TxDetails/SafeTxGasForm.tsx b/src/components/transactions/TxDetails/SafeTxGasForm.tsx index 67b4e7854b..319d4fb34f 100644 --- a/src/components/transactions/TxDetails/SafeTxGasForm.tsx +++ b/src/components/transactions/TxDetails/SafeTxGasForm.tsx @@ -62,15 +62,26 @@ const SafeTxGasForm = () => { const [editing, setEditing] = useState(false) return ( - + {safeTxGas} - {isEditable && ( - setEditing(true)} fontSize="small"> + setEditing(true)} + sx={{ + fontSize: 'small', + }} + > Edit )} - {editing && setEditing(false)} />} ) diff --git a/src/components/transactions/TxDetails/Summary/TxDataRow/index.tsx b/src/components/transactions/TxDetails/Summary/TxDataRow/index.tsx index f0b68a02b2..d8a93de5da 100644 --- a/src/components/transactions/TxDetails/Summary/TxDataRow/index.tsx +++ b/src/components/transactions/TxDetails/Summary/TxDataRow/index.tsx @@ -34,7 +34,13 @@ export const generateDataRowValue = ( ) case 'rawData': return ( - +
{value ? dataLength(value) : 0} bytes
diff --git a/src/components/transactions/TxDetails/Summary/index.tsx b/src/components/transactions/TxDetails/Summary/index.tsx index 7f1532e2b0..32bd2c1132 100644 --- a/src/components/transactions/TxDetails/Summary/index.tsx +++ b/src/components/transactions/TxDetails/Summary/index.tsx @@ -66,13 +66,11 @@ const Summary = ({ txDetails, defaultExpanded = false, hideDecodedData = false } {submittedAt ? dateString(submittedAt) : null} - {executedAt && ( {dateString(executedAt)} )} - {/* Advanced TxData */} {txData && ( <> @@ -89,9 +87,22 @@ const Summary = ({ txDetails, defaultExpanded = false, hideDecodedData = false } )} {expanded && ( - + {!isCustom && !hideDecodedData && ( - + )} diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/MethodCall.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/MethodCall.tsx index 7d78a51b9b..0ff9af87ac 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/MethodCall.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/MethodCall.tsx @@ -16,12 +16,14 @@ const MethodCall = ({ return ( <> Call - ) diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/MethodDetails/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/MethodDetails/index.tsx index 1818c49a51..232151beaa 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/MethodDetails/index.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/MethodDetails/index.tsx @@ -14,15 +14,27 @@ type MethodDetailsProps = { export const MethodDetails = ({ data, addressInfoIndex }: MethodDetailsProps): ReactElement => { if (!data.parameters?.length) { - return No parameters + return ( + + No parameters + + ) } return ( - + Parameters - {data.parameters?.map((param, index) => { const isArrayValueParam = isArrayParameter(param.type) || Array.isArray(param.value) const inlineType = isAddress(param.type) ? 'address' : isByte(param.type) ? 'bytes' : undefined @@ -31,7 +43,12 @@ export const MethodDetails = ({ data, addressInfoIndex }: MethodDetailsProps): R const title = ( <> {param.name}{' '} - + {param.type} diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx index dfe3114835..c09a20cf70 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx @@ -47,13 +47,16 @@ export const SingleTxDecoded = ({ tx, txData, actionTitle, variant, expanded, on
{actionTitle} - + {name ? name + ': ' : ''} {method}
- diff --git a/src/components/transactions/TxDetails/TxData/Rejection/index.tsx b/src/components/transactions/TxDetails/TxData/Rejection/index.tsx index f1c8f66036..748996e993 100644 --- a/src/components/transactions/TxDetails/TxData/Rejection/index.tsx +++ b/src/components/transactions/TxDetails/TxData/Rejection/index.tsx @@ -22,11 +22,21 @@ const RejectionTxInfo = ({ nonce, isTxExecuted }: Props) => { return ( <> - + {message} {!isTxExecuted && ( - + {title} diff --git a/src/components/transactions/TxDetails/TxData/Transfer/TransferActions.tsx b/src/components/transactions/TxDetails/TxData/Transfer/TransferActions.tsx index 3dac032a8b..dd16a19430 100644 --- a/src/components/transactions/TxDetails/TxData/Transfer/TransferActions.tsx +++ b/src/components/transactions/TxDetails/TxData/Transfer/TransferActions.tsx @@ -68,8 +68,8 @@ const TransferActions = ({ const amount = isNativeTokenTransfer(txInfo.transferInfo) ? safeFormatUnits(txInfo.transferInfo.value, ETHER) : isERC20Transfer(txInfo.transferInfo) - ? safeFormatUnits(txInfo.transferInfo.value, txInfo.transferInfo.decimals) - : undefined + ? safeFormatUnits(txInfo.transferInfo.value, txInfo.transferInfo.decimals) + : undefined const isOutgoingTx = isOutgoingTransfer(txInfo) const canSendAgain = diff --git a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx index 9971388af1..34d6a98664 100644 --- a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx +++ b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx @@ -21,7 +21,14 @@ const TransferTxInfoMain = ({ txInfo, txStatus, trusted, imitation }: TransferTx const { direction } = txInfo return ( - + {direction === TransferDirection.INCOMING ? 'Received' : isTxQueued(txStatus) ? 'Send' : 'Sent'}{' '} @@ -38,10 +45,21 @@ const TransferTxInfo = ({ txInfo, txStatus, trusted, imitation }: TransferTxInfo const address = txInfo.direction.toUpperCase() === TransferDirection.INCOMING ? txInfo.sender : txInfo.recipient return ( - + - - + )}
- {/* Signers */} {(!isUnsigned || isTxFromProposer) && (
@@ -140,7 +139,12 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement )} {isQueue && expiredSwap && ( - + This order has expired. Reject this transaction and try again. )} diff --git a/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx b/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx index fc9a83e9ae..32ce96e689 100644 --- a/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx +++ b/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import { screen, fireEvent } from '@testing-library/react' import { act, render } from '@/tests/test-utils' -import '@testing-library/jest-dom/extend-expect' +import '@testing-library/jest-dom' import TxFilterForm from './index' import { useRouter } from 'next/router' @@ -19,7 +19,7 @@ const toggleFilter = jest.fn() const fromDate = '20/01/2021' const toDate = '20/01/2020' -const placeholder = 'dd/mm/yyyy' +const placeholder = 'DD/MM/YYYY' const errorMsgFormat = 'Invalid address format' describe('TxFilterForm Component Tests', () => { diff --git a/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx b/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx index f94fb2b358..04e40ecd67 100644 --- a/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx +++ b/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx @@ -74,7 +74,12 @@ export const ExpandableTransactionItem = ({ export const TransactionSkeleton = () => ( <> - + diff --git a/src/components/transactions/TxSigners/index.tsx b/src/components/transactions/TxSigners/index.tsx index 27d03c7bf3..da3ad2705a 100644 --- a/src/components/transactions/TxSigners/index.tsx +++ b/src/components/transactions/TxSigners/index.tsx @@ -65,7 +65,7 @@ enum StepState { } const getStepColor = (state: StepState, palette: Palette): string => { - const colors: { [key in StepState]: string } = { + const colors: { [_key in StepState]: string } = { [StepState.CONFIRMED]: palette.primary.main, [StepState.ACTIVE]: palette.warning.dark, [StepState.DISABLED]: palette.border.main, @@ -202,7 +202,14 @@ export const TxSigners = ({ - + {hideConfirmations ? 'Show all' : 'Hide all'} diff --git a/src/components/transactions/TxStatusChip/index.tsx b/src/components/transactions/TxStatusChip/index.tsx index 339236fb75..2bffe6019a 100644 --- a/src/components/transactions/TxStatusChip/index.tsx +++ b/src/components/transactions/TxStatusChip/index.tsx @@ -17,11 +17,13 @@ const TxStatusChip = ({ children, color }: TxStatusChipProps): ReactElement => { label={ {children} diff --git a/src/components/transactions/TxStatusLabel/index.tsx b/src/components/transactions/TxStatusLabel/index.tsx index adb4abbda4..f284b162b3 100644 --- a/src/components/transactions/TxStatusLabel/index.tsx +++ b/src/components/transactions/TxStatusLabel/index.tsx @@ -34,7 +34,7 @@ const TxStatusLabel = ({ tx }: { tx: TransactionSummary }) => { display="flex" alignItems="center" gap={1} - color={({ palette }) => getStatusColor(tx, palette)} + sx={{ color: ({ palette }) => getStatusColor(tx, palette) }} data-testid="tx-status-label" > {isPending && } diff --git a/src/components/transactions/TxSummary/QueueActions.tsx b/src/components/transactions/TxSummary/QueueActions.tsx index df6075a6c0..c0726ad3b4 100644 --- a/src/components/transactions/TxSummary/QueueActions.tsx +++ b/src/components/transactions/TxSummary/QueueActions.tsx @@ -20,7 +20,14 @@ const QueueActions = ({ tx }: { tx: TransactionSummary }) => { } return ( - + {ExecutionComponent} {pendingTx && pendingTx.status === PendingStatus.PROCESSING && ( diff --git a/src/components/transactions/TxSummary/index.tsx b/src/components/transactions/TxSummary/index.tsx index af455a3878..a29e92db02 100644 --- a/src/components/transactions/TxSummary/index.tsx +++ b/src/components/transactions/TxSummary/index.tsx @@ -50,31 +50,57 @@ const TxSummary = ({ item, isConflictGroup, isBulkGroup }: TxSummaryProps): Reac id={tx.id} > {nonce !== undefined && !isConflictGroup && !isBulkGroup && ( - + {nonce} )} - {(isImitationTransaction || !isTrusted) && ( - + )} - - + - - + - - + - {isQueue && executionInfo && ( - + {executionInfo.confirmationsSubmitted > 0 || isPending ? ( )} - {isQueue && expiredSwap ? ( - + ) : !isQueue || isPending ? ( - + ) : ( '' )} - {isQueue && !expiredSwap && ( - + )} diff --git a/src/components/tx-flow/SafeTxProvider.tsx b/src/components/tx-flow/SafeTxProvider.tsx index f83c86663a..31e2070403 100644 --- a/src/components/tx-flow/SafeTxProvider.tsx +++ b/src/components/tx-flow/SafeTxProvider.tsx @@ -74,8 +74,10 @@ const SafeTxProvider = ({ children }: { children: ReactNode }): ReactElement => const recommendedSafeTxGas = useSafeTxGas(safeTx) // Priority to external nonce, then to the recommended one - const finalNonce = isSigned ? safeTx?.data.nonce : nonce ?? recommendedNonce ?? safeTx?.data.nonce - const finalSafeTxGas = isSigned ? safeTx?.data.safeTxGas : safeTxGas ?? recommendedSafeTxGas ?? safeTx?.data.safeTxGas + const finalNonce = isSigned ? safeTx?.data.nonce : (nonce ?? recommendedNonce ?? safeTx?.data.nonce) + const finalSafeTxGas = isSigned + ? safeTx?.data.safeTxGas + : (safeTxGas ?? recommendedSafeTxGas ?? safeTx?.data.safeTxGas) // Update the tx when the nonce or safeTxGas change useEffect(() => { diff --git a/src/components/tx-flow/TxInfoProvider.tsx b/src/components/tx-flow/TxInfoProvider.tsx index 567087c5c4..6187942349 100644 --- a/src/components/tx-flow/TxInfoProvider.tsx +++ b/src/components/tx-flow/TxInfoProvider.tsx @@ -1,4 +1,4 @@ -import { createContext } from 'react' +import { createContext, type ReactElement } from 'react' import { useSimulation, type UseSimulationReturn } from '@/components/tx/security/tenderly/useSimulation' import { FETCH_STATUS, type TenderlySimulation } from '@/components/tx/security/tenderly/types' @@ -40,7 +40,7 @@ export const TxInfoContext = createContext<{ }, }) -export const TxInfoProvider = ({ children }: { children: JSX.Element }) => { +export const TxInfoProvider = ({ children }: { children: ReactElement }) => { const simulation = useSimulation() const isLoading = simulation._simulationRequestStatus === FETCH_STATUS.LOADING diff --git a/src/components/tx-flow/common/OwnerList/index.tsx b/src/components/tx-flow/common/OwnerList/index.tsx index 9592d70588..5dfd23f363 100644 --- a/src/components/tx-flow/common/OwnerList/index.tsx +++ b/src/components/tx-flow/common/OwnerList/index.tsx @@ -19,7 +19,13 @@ export function OwnerList({ }): ReactElement { return ( - + {title ?? `New signer${owners.length > 1 ? 's' : ''}`} diff --git a/src/components/tx-flow/common/TxLayout/index.tsx b/src/components/tx-flow/common/TxLayout/index.tsx index 02d77dcee3..72a1e2afe8 100644 --- a/src/components/tx-flow/common/TxLayout/index.tsx +++ b/src/components/tx-flow/common/TxLayout/index.tsx @@ -32,18 +32,28 @@ const TxLayoutHeader = ({ return ( - + {icon && (
)} - + {subtitle}
- {!hideNonce && safe.deployed && nonceNeeded && }
) @@ -113,7 +123,13 @@ const TxLayout = ({ )} - + {/* Main content */}
@@ -121,8 +137,10 @@ const TxLayout = ({ data-testid="modal-title" variant="h3" component="div" - fontWeight="700" className={css.title} + sx={{ + fontWeight: '700', + }} > {title} diff --git a/src/components/tx-flow/common/TxNonce/index.tsx b/src/components/tx-flow/common/TxNonce/index.tsx index 5d3f243e39..154e5f3a73 100644 --- a/src/components/tx-flow/common/TxNonce/index.tsx +++ b/src/components/tx-flow/common/TxNonce/index.tsx @@ -42,7 +42,13 @@ const CustomPopper = function ({ const NonceFormHeader = memo(function NonceFormSubheader({ children, ...props }: ListSubheaderProps) { return ( - + {children} @@ -179,7 +185,13 @@ const TxNonceForm = ({ nonce, recommendedNonce }: { nonce: string; recommendedNo render={({ field, fieldState }) => { if (readOnly) { return ( - + {nonce} ) @@ -277,9 +289,21 @@ const TxNonce = () => { const { nonce, recommendedNonce } = useContext(SafeTxContext) return ( - + Nonce{' '} - + # {nonce === undefined || recommendedNonce === undefined ? ( diff --git a/src/components/tx-flow/common/TxStatusWidget/index.tsx b/src/components/tx-flow/common/TxStatusWidget/index.tsx index 11847b6ee9..ce16000d6e 100644 --- a/src/components/tx-flow/common/TxStatusWidget/index.tsx +++ b/src/components/tx-flow/common/TxStatusWidget/index.tsx @@ -47,7 +47,14 @@ const TxStatusWidget = ({ return (
- + {isMessage ? 'Message' : 'Transaction'} status @@ -56,9 +63,7 @@ const TxStatusWidget = ({
- -
diff --git a/src/components/tx-flow/flows/AddOwner/ChooseOwner.tsx b/src/components/tx-flow/flows/AddOwner/ChooseOwner.tsx index 2fa4aa625c..51c8e67486 100644 --- a/src/components/tx-flow/flows/AddOwner/ChooseOwner.tsx +++ b/src/components/tx-flow/flows/AddOwner/ChooseOwner.tsx @@ -83,12 +83,27 @@ export const ChooseOwner = ({ {params.removedOwner && ( <> - + {params.removedOwner && 'Review the signer you want to replace in the active Safe Account, then specify the new signer you want to replace it with:'} - - + + Current signer @@ -125,7 +140,13 @@ export const ChooseOwner = ({ {mode === ChooseOwnerMode.ADD && ( - + Threshold @@ -143,11 +164,24 @@ export const ChooseOwner = ({ - + Any transaction requires the confirmation of: - + - + All actions initiated by the Recoverer will be cancelled. The current signers will remain the signers of the Safe Account. - This transaction will initiate the cancellation of the{' '} {recovery.isMalicious ? 'malicious transaction' : 'recovery proposal'}. It requires other signer signatures in diff --git a/src/components/tx-flow/flows/CancelRecovery/CancelRecoveryOverview.tsx b/src/components/tx-flow/flows/CancelRecovery/CancelRecoveryOverview.tsx index 8e128c2871..3c882e5558 100644 --- a/src/components/tx-flow/flows/CancelRecovery/CancelRecoveryOverview.tsx +++ b/src/components/tx-flow/flows/CancelRecovery/CancelRecoveryOverview.tsx @@ -20,20 +20,48 @@ export function CancelRecoveryOverview({ onSubmit }: { onSubmit: () => void }): return ( - + {/* TODO: Replace with correct icon when provided */} - + Do you want to cancel the Account recovery? - + If it is an unwanted recovery proposal or you've noticed something suspicious, you can cancel it at any time. - + diff --git a/src/components/tx-flow/flows/ChangeThreshold/ChooseThreshold.tsx b/src/components/tx-flow/flows/ChangeThreshold/ChooseThreshold.tsx index 6d289b93ac..3daf2e9d25 100644 --- a/src/components/tx-flow/flows/ChangeThreshold/ChooseThreshold.tsx +++ b/src/components/tx-flow/flows/ChangeThreshold/ChooseThreshold.tsx @@ -41,7 +41,12 @@ export const ChooseThreshold = ({ return (
- + Threshold @@ -61,9 +66,12 @@ export const ChooseThreshold = ({ Any transaction will require the confirmation of:
- - + + {safe.owners.map((_, idx) => ( @@ -88,18 +103,25 @@ export const ChooseThreshold = ({ ))} - out of {safe.owners.length} signer(s) - {isError ? ( - + {fieldState.error?.message} ) : ( - + {fieldState.isDirty ? 'Previous policy was ' : 'Current policy is '} {safe.threshold} out of {safe.owners.length} diff --git a/src/components/tx-flow/flows/ConfirmTx/ConfirmProposedTx.tsx b/src/components/tx-flow/flows/ConfirmTx/ConfirmProposedTx.tsx index c62c776a3b..9451036981 100644 --- a/src/components/tx-flow/flows/ConfirmTx/ConfirmProposedTx.tsx +++ b/src/components/tx-flow/flows/ConfirmTx/ConfirmProposedTx.tsx @@ -40,7 +40,13 @@ const ConfirmProposedTx = ({ txSummary }: ConfirmProposedTxProps): ReactElement return ( - {text} + + {text} + ) } diff --git a/src/components/tx-flow/flows/NewSpendingLimit/CreateSpendingLimit.tsx b/src/components/tx-flow/flows/NewSpendingLimit/CreateSpendingLimit.tsx index 4d99ecfa03..0646c6197c 100644 --- a/src/components/tx-flow/flows/NewSpendingLimit/CreateSpendingLimit.tsx +++ b/src/components/tx-flow/flows/NewSpendingLimit/CreateSpendingLimit.tsx @@ -74,7 +74,13 @@ export const CreateSpendingLimit = ({ - + Reset Timer diff --git a/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx b/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx index e76464fca6..33c2a98580 100644 --- a/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx +++ b/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx @@ -97,10 +97,20 @@ export const ReviewSpendingLimit = ({ params }: { params: NewSpendingLimitFlowPr )} )} - - + - + Beneficiary @@ -115,10 +125,20 @@ export const ReviewSpendingLimit = ({ params }: { params: NewSpendingLimitFlowPr /> - - + - + Reset time @@ -133,16 +153,23 @@ export const ReviewSpendingLimit = ({ params }: { params: NewSpendingLimitFlowPr {oldResetTime} {' → '} )} - + {resetTime} @@ -161,7 +188,12 @@ export const ReviewSpendingLimit = ({ params }: { params: NewSpendingLimitFlowPr {existingSpendingLimit && ( - + You are about to replace an existing spending limit diff --git a/src/components/tx-flow/flows/NewTx/index.tsx b/src/components/tx-flow/flows/NewTx/index.tsx index b480243925..f6a79fafd8 100644 --- a/src/components/tx-flow/flows/NewTx/index.tsx +++ b/src/components/tx-flow/flows/NewTx/index.tsx @@ -22,16 +22,37 @@ const NewTxFlow = () => { return ( - + {/* Alignment of `TxLayout` */} - + - +
@@ -41,7 +62,15 @@ const NewTxFlow = () => {
- + Manage assets @@ -51,7 +80,13 @@ const NewTxFlow = () => { {txBuilder?.app && ( <> - + Interact with contracts diff --git a/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx b/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx index 74ac050502..e62924459b 100644 --- a/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx +++ b/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx @@ -40,9 +40,20 @@ const ReviewNftBatch = ({ params, onSubmit, txNonce }: ReviewNftBatchProps): Rea return ( - + - + Send @@ -51,7 +62,6 @@ const ReviewNftBatch = ({ params, onSubmit, txNonce }: ReviewNftBatchProps): Rea - ) diff --git a/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx b/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx index 9f2bb7977c..ab04d6f907 100644 --- a/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx +++ b/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx @@ -20,9 +20,21 @@ type SendNftBatchProps = { } const NftItem = ({ image, name, description }: { image: string; name: string; description?: string }) => ( - + - + - + {name} @@ -48,11 +67,13 @@ const NftItem = ({ image, name, description }: { image: string; name: string; de {description && ( {description} @@ -65,12 +86,14 @@ export const NftItems = ({ tokens }: { tokens: SafeCollectibleResponse[] }) => { return ( {tokens.map((token) => ( {
- + Selected NFTs diff --git a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx index 71db742fce..2da5365824 100644 --- a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx +++ b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx @@ -111,7 +111,11 @@ export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlo return ( <> - + This transaction will reset the Account setup, changing the signers {newThreshold !== safe.threshold ? ' and threshold' : ''}. @@ -120,8 +124,18 @@ export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlo - - + + After recovery, Safe Account transactions will require: @@ -135,9 +149,7 @@ export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlo - - <> diff --git a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowSetup.tsx b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowSetup.tsx index babd3d4a0a..7514b3f512 100644 --- a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowSetup.tsx +++ b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowSetup.tsx @@ -87,11 +87,22 @@ export function RecoverAccountFlowSetup({
- + Add signer(s) - + Set the new signer wallet(s) of this Safe Account and how many need to confirm a transaction before it can be executed. @@ -120,7 +131,15 @@ export function RecoverAccountFlowSetup({ /> - + {index > 0 && ( remove(index)}> @@ -143,7 +162,13 @@ export function RecoverAccountFlowSetup({
- + Threshold @@ -161,12 +186,25 @@ export function RecoverAccountFlowSetup({ - + After recovery, Safe Account transactions will require:
- + { return ( - + Execute this transaction to finalize the recovery. diff --git a/src/components/tx-flow/flows/RejectTx/RejectTx.tsx b/src/components/tx-flow/flows/RejectTx/RejectTx.tsx index 5e47cc5b50..1bbd9eb96e 100644 --- a/src/components/tx-flow/flows/RejectTx/RejectTx.tsx +++ b/src/components/tx-flow/flows/RejectTx/RejectTx.tsx @@ -20,15 +20,25 @@ const RejectTx = ({ txNonce }: RejectTxProps): ReactElement => { return ( - + To reject the transaction, a separate rejection transaction will be created to replace the original one. - - + Transaction nonce: {txNonce} - - + You will need to confirm the rejection transaction with your currently connected wallet. diff --git a/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx b/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx index 2b61d25a07..04b89061b4 100644 --- a/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx +++ b/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx @@ -28,10 +28,12 @@ export const ReviewRemoveGuard = ({ params }: { params: RemoveGuardFlowProps }) return ( ({ color: palette.primary.light })}>Transaction guard - - - + Once the transaction guard has been removed, checks by the transaction guard will not be conducted before or after any subsequent transactions. diff --git a/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx b/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx index 9df4ced258..e45e50994d 100644 --- a/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx +++ b/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx @@ -27,7 +27,13 @@ export const ReviewRemoveModule = ({ params }: { params: RemoveModuleFlowProps } return ( - + Module @@ -35,7 +41,11 @@ export const ReviewRemoveModule = ({ params }: { params: RemoveModuleFlowProps } - + After removing this module, any feature or app that uses this module might no longer work. If this Safe Account requires more then one signature, the module removal will have to be confirmed by other signers as well. diff --git a/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx b/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx index 4b15885928..2e13a1fa2e 100644 --- a/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx +++ b/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx @@ -35,7 +35,14 @@ export const ReviewRemoveOwner = ({ params }: { params: RemoveOwnerFlowProps }): return ( palette.warning.background, p: 2 }}> - + Selected signer @@ -48,10 +55,19 @@ export const ReviewRemoveOwner = ({ params }: { params: RemoveOwnerFlowProps }): /> - - - + + Any transaction requires the confirmation of: diff --git a/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx b/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx index f29129a3af..389d495a9e 100644 --- a/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx +++ b/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx @@ -36,16 +36,35 @@ export const SetThreshold = ({ return ( - - Review the signer you want to remove from the active Safe Account: + + + Review the signer you want to remove from the active Safe Account: + {/* TODO: Update the EthHashInfo style from the replace owner PR */} - - + + Threshold @@ -63,7 +82,15 @@ export const SetThreshold = ({ Any transaction requires the confirmation of: - + + {options?.map((owner) => ( + + + {!isOptionEnabled(owner) && ( + + Already signed + + )} + + ))} + + + + + ) +} diff --git a/src/components/tx/SignOrExecuteForm/SignerForm/styles.module.css b/src/components/tx/SignOrExecuteForm/SignerForm/styles.module.css new file mode 100644 index 0000000000..c4ebf3be80 --- /dev/null +++ b/src/components/tx/SignOrExecuteForm/SignerForm/styles.module.css @@ -0,0 +1,10 @@ +.signerForm :global .MuiOutlinedInput-notchedOutline { + border: 1px solid var(--color-border-light) !important; +} + +.disabledPill { + background-color: var(--color-border-light); + border-radius: 4px; + color: var(--color-text-primary); + padding: 4px 8px; +} diff --git a/src/components/tx/SignOrExecuteForm/__tests__/SignerForm.test.tsx b/src/components/tx/SignOrExecuteForm/__tests__/SignerForm.test.tsx new file mode 100644 index 0000000000..c96501f4e7 --- /dev/null +++ b/src/components/tx/SignOrExecuteForm/__tests__/SignerForm.test.tsx @@ -0,0 +1,131 @@ +import { useNestedSafeOwners } from '@/hooks/useNestedSafeOwners' +import useSafeInfo from '@/hooks/useSafeInfo' +import { render } from '@/tests/test-utils' +import { SignerForm } from '../SignerForm' +import { faker } from '@faker-js/faker' +import { extendedSafeInfoBuilder, addressExBuilder } from '@/tests/builders/safe' +import { generateRandomArray } from '@/tests/builders/utils' +import { type Eip1193Provider } from 'ethers' +import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' +import { type ReactElement, useState } from 'react' +import { WalletContext } from '@/components/common/WalletProvider' + +jest.mock('@/hooks/useNestedSafeOwners') +jest.mock('@/hooks/useSafeInfo') + +const TestWalletContextProvider = ({ + connectedWallet, + children, +}: { + connectedWallet: ConnectedWallet | null + children: ReactElement +}) => { + const [signerAddress, setSignerAddress] = useState() + + return ( + + {children} + + ) +} + +describe('SignerForm', () => { + const mockUseSafeInfo = useSafeInfo as jest.MockedFunction + const mockUseNestedSafeOwners = useNestedSafeOwners as jest.MockedFunction + + const safeAddress = faker.finance.ethereumAddress() + // Safe with 3 owners + const mockSafeInfo = { + safeAddress, + safe: extendedSafeInfoBuilder() + .with({ address: { value: safeAddress } }) + .with({ chainId: '1' }) + .with({ owners: generateRandomArray(() => addressExBuilder().build(), { min: 3, max: 3 }) }) + .build(), + safeLoaded: true, + safeLoading: false, + } + + const mockOwners = mockSafeInfo.safe.owners + + beforeAll(() => { + mockUseSafeInfo.mockReturnValue(mockSafeInfo) + }) + + it('should not render anything if no wallet is connected', () => { + const result = render( + + + , + ) + expect(result.queryByText('Sign with')).toBeNull() + }) + + it('should not render if there are no nested Safes', () => { + mockUseNestedSafeOwners.mockReturnValue([]) + + const result = render( + + + , + ) + + expect(result.queryByText('Sign with')).toBeNull() + }) + + it('should render sign form if there are nested Safes', () => { + mockUseNestedSafeOwners.mockReturnValue([mockOwners[0].value]) + const result = render( + + + , + ) + expect(result.queryByText('Sign with')).toBeVisible() + }) + + it('should render execution form if there are nested Safes', () => { + mockUseNestedSafeOwners.mockReturnValue([mockOwners[0].value]) + const result = render( + + + , + ) + expect(result.queryByText('Execute with')).toBeVisible() + }) +}) diff --git a/src/components/tx/SignOrExecuteForm/hooks.test.ts b/src/components/tx/SignOrExecuteForm/__tests__/hooks.test.ts similarity index 97% rename from src/components/tx/SignOrExecuteForm/hooks.test.ts rename to src/components/tx/SignOrExecuteForm/__tests__/hooks.test.ts index b6099d9877..c0e3f5cdff 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.test.ts +++ b/src/components/tx/SignOrExecuteForm/__tests__/hooks.test.ts @@ -17,11 +17,14 @@ import { useRecommendedNonce, useTxActions, useValidateNonce, -} from './hooks' +} from '../hooks' import * as recommendedNonce from '@/services/tx/tx-sender/recommendedNonce' import { defaultSafeInfo } from '@/store/safeInfoSlice' import { chainBuilder } from '@/tests/builders/chains' import * as useChains from '@/hooks/useChains' +import { MockEip1193Provider } from '@/tests/mocks/providers' +import { type SignerWallet } from '@/components/common/WalletProvider' +import { type NestedWallet } from '@/utils/nested-safe-wallet' const chainInfo = chainBuilder().with({ chainId: '1' }).build() @@ -49,11 +52,11 @@ describe('SignOrExecute hooks', () => { } as unknown as OnboardAPI) // Wallet - jest.spyOn(wallet, 'default').mockReturnValue({ + jest.spyOn(wallet, 'useSigner').mockReturnValue({ chainId: '1', - label: 'MetaMask', address: '0x1234567890000000000000000000000000000000', - } as unknown as ConnectedWallet) + provider: MockEip1193Provider, + } as unknown as NestedWallet) jest.spyOn(useChains, 'useCurrentChain').mockReturnValue(chainInfo) }) @@ -564,11 +567,11 @@ describe('SignOrExecute hooks', () => { describe('useAlreadySigned', () => { it('should return true if wallet already signed a tx', () => { // Wallet - jest.spyOn(wallet, 'default').mockReturnValue({ + jest.spyOn(wallet, 'useSigner').mockReturnValue({ chainId: '1', - label: 'MetaMask', address: '0x1234567890000000000000000000000000000000', - } as unknown as ConnectedWallet) + provider: MockEip1193Provider, + } as SignerWallet) const tx = createSafeTx() tx.addSignature({ @@ -584,11 +587,11 @@ describe('SignOrExecute hooks', () => { it('should return false if wallet has not signed a tx yet', () => { // Wallet - jest.spyOn(wallet, 'default').mockReturnValue({ + jest.spyOn(wallet, 'useSigner').mockReturnValue({ chainId: '1', - label: 'MetaMask', address: '0x1234567890000000000000000000000000000000', - } as unknown as ConnectedWallet) + provider: MockEip1193Provider, + } as SignerWallet) const tx = createSafeTx() tx.addSignature({ diff --git a/src/components/tx/SignOrExecuteForm/hooks.ts b/src/components/tx/SignOrExecuteForm/hooks.ts index 70df0774be..2474b104c6 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.ts @@ -1,9 +1,9 @@ -import { assertTx, assertWallet, assertOnboard, assertChainInfo } from '@/utils/helpers' +import { assertTx, assertOnboard, assertChainInfo, assertProvider } from '@/utils/helpers' import { useMemo } from 'react' import { type TransactionOptions, type SafeTransaction } from '@safe-global/safe-core-sdk-types' import { sameString } from '@safe-global/protocol-kit/dist/src/utils' import useSafeInfo from '@/hooks/useSafeInfo' -import useWallet from '@/hooks/wallets/useWallet' +import useWallet, { useSigner } from '@/hooks/wallets/useWallet' import useOnboard from '@/hooks/wallets/useOnboard' import { isSmartContractWallet } from '@/utils/wallets' import { @@ -42,8 +42,8 @@ type txDetails = AsyncResult export const useProposeTx = (safeTx?: SafeTransaction, txId?: string, origin?: string): txDetails => { const { safe } = useSafeInfo() - const wallet = useWallet() - const sender = wallet?.address || safe.owners?.[0]?.value + const signer = useSigner() + const sender = signer?.address || safe.owners?.[0]?.value return useAsync( async () => { @@ -61,6 +61,7 @@ export const useProposeTx = (safeTx?: SafeTransaction, txId?: string, origin?: s export const useTxActions = (): TxActions => { const { safe } = useSafeInfo() const onboard = useOnboard() + const signer = useSigner() const wallet = useWallet() const [addTxToBatch] = useUpdateBatch() const chain = useCurrentChain() @@ -87,48 +88,56 @@ export const useTxActions = (): TxActions => { const addToBatch: TxActions['addToBatch'] = async (safeTx, origin) => { assertTx(safeTx) - assertWallet(wallet) + assertProvider(signer?.provider) - const tx = await _propose(wallet.address, safeTx, undefined, origin) + const tx = await _propose(signer.address, safeTx, undefined, origin) await addTxToBatch(tx) return tx.txId } const signRelayedTx = async (safeTx: SafeTransaction, txId?: string): Promise => { assertTx(safeTx) - assertWallet(wallet) + assertProvider(signer?.provider) // Smart contracts cannot sign transactions off-chain - if (await isSmartContractWallet(wallet.chainId, wallet.address)) { + if (await isSmartContractWallet(signer.chainId, signer.address)) { throw new Error('Cannot relay an unsigned transaction from a smart contract wallet') } - return await dispatchTxSigning(safeTx, version, wallet.provider, txId) + return await dispatchTxSigning(safeTx, version, signer.provider, txId) } const signTx: TxActions['signTx'] = async (safeTx, txId, origin) => { assertTx(safeTx) - assertWallet(wallet) + assertProvider(signer?.provider) assertOnboard(onboard) // Smart contract wallets must sign via an on-chain tx - if (await isSmartContractWallet(wallet.chainId, wallet.address)) { + if (signer.isSafe || (await isSmartContractWallet(signer.chainId, signer.address))) { // If the first signature is a smart contract wallet, we have to propose w/o signatures // Otherwise the backend won't pick up the tx // The signature will be added once the on-chain signature is indexed - const id = txId || (await _propose(wallet.address, safeTx, txId, origin)).txId - await dispatchOnChainSigning(safeTx, id, wallet.provider, chainId, wallet.address, safeAddress) + const id = txId || (await _propose(signer.address, safeTx, txId, origin)).txId + await dispatchOnChainSigning( + safeTx, + id, + signer.provider, + chainId, + signer.address, + safeAddress, + Boolean(signer.isSafe), + ) return id } // Otherwise, sign off-chain - const signedTx = await dispatchTxSigning(safeTx, version, wallet.provider, txId) - const tx = await _propose(wallet.address, signedTx, txId, origin) + const signedTx = await dispatchTxSigning(safeTx, version, signer.provider, txId) + const tx = await _propose(signer.address, signedTx, txId, origin) return tx.txId } const signProposerTx: TxActions['signProposerTx'] = async (safeTx) => { assertTx(safeTx) - assertWallet(wallet) + assertProvider(wallet?.provider) assertOnboard(onboard) const signedTx = await dispatchProposerTxSigning(safeTx, wallet) @@ -139,7 +148,7 @@ export const useTxActions = (): TxActions => { const executeTx: TxActions['executeTx'] = async (txOptions, safeTx, txId, origin, isRelayed) => { assertTx(safeTx) - assertWallet(wallet) + assertProvider(signer?.provider) assertOnboard(onboard) assertChainInfo(chain) @@ -153,7 +162,7 @@ export const useTxActions = (): TxActions => { // Propose the tx if there's no id yet ("immediate execution") if (!txId || rePropose) { - tx = await _propose(wallet.address, safeTx, txId, origin) + tx = await _propose(signer.address, safeTx, txId, origin) txId = tx.txId } @@ -161,16 +170,15 @@ export const useTxActions = (): TxActions => { if (isRelayed) { await dispatchTxRelay(safeTx, safe, txId, chain, txOptions.gasLimit) } else { - const isSmartAccount = await isSmartContractWallet(wallet.chainId, wallet.address) - - await dispatchTxExecution(safeTx, txOptions, txId, wallet.provider, wallet.address, safeAddress, isSmartAccount) + const isSmartAccount = await isSmartContractWallet(signer.chainId, signer.address) + await dispatchTxExecution(safeTx, txOptions, txId, signer.provider, signer.address, safeAddress, isSmartAccount) } return txId } return { addToBatch, signTx, executeTx, signProposerTx, proposeTx } - }, [safe, wallet, addTxToBatch, onboard, chain]) + }, [safe, wallet, signer?.provider, signer?.address, signer?.chainId, signer?.isSafe, addTxToBatch, onboard, chain]) } export const useValidateNonce = (safeTx: SafeTransaction | undefined): boolean => { @@ -236,7 +244,7 @@ export const useSafeTxGas = (safeTx: SafeTransaction | undefined): string | unde } export const useAlreadySigned = (safeTx: SafeTransaction | undefined): boolean => { - const wallet = useWallet() + const wallet = useSigner() const hasSigned = safeTx && wallet && (safeTx.signatures.has(wallet.address.toLowerCase()) || safeTx.signatures.has(wallet.address)) return Boolean(hasSigned) diff --git a/src/components/tx/security/blockaid/__tests__/useBlockaid.test.ts b/src/components/tx/security/blockaid/__tests__/useBlockaid.test.ts index 0043375c3f..f8b3fc81d1 100644 --- a/src/components/tx/security/blockaid/__tests__/useBlockaid.test.ts +++ b/src/components/tx/security/blockaid/__tests__/useBlockaid.test.ts @@ -1,5 +1,4 @@ import * as useChains from '@/hooks/useChains' -import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import * as useWallet from '@/hooks/wallets/useWallet' import { SecuritySeverity } from '@/services/security/modules/types' import { eip712TypedDataBuilder } from '@/tests/builders/messages' @@ -12,6 +11,7 @@ import useSafeInfo from '@/hooks/useSafeInfo' import { safeInfoBuilder } from '@/tests/builders/safe' import { CLASSIFICATION_MAPPING, REASON_MAPPING } from '..' import { renderHook, waitFor } from '@/tests/test-utils' +import { type SignerWallet } from '@/components/common/WalletProvider' const setupFetchStub = (data: any) => () => { return Promise.resolve({ @@ -37,7 +37,7 @@ jest.mock('@/hooks/useSafeInfo') const mockUseSafeInfo = useSafeInfo as jest.MockedFunction describe.each([TEST_CASES.MESSAGE, TEST_CASES.TRANSACTION])('useBlockaid for %s', (testCase) => { - let mockUseWallet: jest.SpyInstance + let mockUseSigner: jest.SpyInstance const mockPayload = testCase === TEST_CASES.TRANSACTION ? safeTxBuilder().build() : eip712TypedDataBuilder().build() @@ -46,8 +46,8 @@ describe.each([TEST_CASES.MESSAGE, TEST_CASES.TRANSACTION])('useBlockaid for %s' beforeEach(() => { jest.resetAllMocks() jest.useFakeTimers() - mockUseWallet = jest.spyOn(useWallet, 'default') - mockUseWallet.mockImplementation(() => null) + mockUseSigner = jest.spyOn(useWallet, 'useSigner') + mockUseSigner.mockImplementation(() => null) mockUseSafeInfo.mockReturnValue({ safe: { ...mockSafeInfo, deployed: true }, safeAddress: mockSafeInfo.address.value, @@ -81,7 +81,7 @@ describe.each([TEST_CASES.MESSAGE, TEST_CASES.TRANSACTION])('useBlockaid for %s' it('should return undefined without feature enabled', async () => { const walletAddress = toBeHex('0x1', 20) - mockUseWallet.mockImplementation(() => ({ + mockUseSigner.mockImplementation(() => ({ address: walletAddress, chainId: '1', label: 'Testwallet', @@ -102,7 +102,7 @@ describe.each([TEST_CASES.MESSAGE, TEST_CASES.TRANSACTION])('useBlockaid for %s' it('should handle request errors', async () => { const walletAddress = toBeHex('0x1', 20) - mockUseWallet.mockImplementation(() => ({ + mockUseSigner.mockImplementation(() => ({ address: walletAddress, chainId: '1', label: 'Testwallet', @@ -126,7 +126,7 @@ describe.each([TEST_CASES.MESSAGE, TEST_CASES.TRANSACTION])('useBlockaid for %s' it('should handle failed simulations', async () => { const walletAddress = toBeHex('0x1', 20) - mockUseWallet.mockImplementation(() => ({ + mockUseSigner.mockImplementation(() => ({ address: walletAddress, chainId: '1', label: 'Testwallet', @@ -216,7 +216,7 @@ describe.each([TEST_CASES.MESSAGE, TEST_CASES.TRANSACTION])('useBlockaid for %s' }, } - mockUseWallet.mockImplementation(() => ({ + mockUseSigner.mockImplementation(() => ({ address: walletAddress, chainId: '1', label: 'Testwallet', diff --git a/src/components/tx/security/blockaid/useBlockaid.ts b/src/components/tx/security/blockaid/useBlockaid.ts index c328e97a92..f09a02fc0f 100644 --- a/src/components/tx/security/blockaid/useBlockaid.ts +++ b/src/components/tx/security/blockaid/useBlockaid.ts @@ -1,7 +1,7 @@ import useAsync, { type AsyncResult } from '@/hooks/useAsync' import { useHasFeature } from '@/hooks/useChains' import useSafeInfo from '@/hooks/useSafeInfo' -import useWallet from '@/hooks/wallets/useWallet' +import { useSigner } from '@/hooks/wallets/useWallet' import { MODALS_EVENTS, trackEvent } from '@/services/analytics' import type { SecurityResponse } from '@/services/security/modules/types' import { FEATURES } from '@/utils/chains' @@ -19,12 +19,12 @@ export const useBlockaid = ( data: SafeTransaction | EIP712TypedData | undefined, ): AsyncResult> => { const { safe, safeAddress } = useSafeInfo() - const wallet = useWallet() + const signer = useSigner() const isFeatureEnabled = useHasFeature(FEATURES.RISK_MITIGATION) const [blockaidPayload, blockaidErrors, blockaidLoading] = useAsync>( () => { - if (!isFeatureEnabled || !data || !wallet?.address) { + if (!isFeatureEnabled || !data || !signer?.address) { return } @@ -32,12 +32,11 @@ export const useBlockaid = ( chainId: Number(safe.chainId), data, safeAddress, - walletAddress: wallet.address, + walletAddress: signer.address, threshold: safe.threshold, }) }, - - [safe.chainId, safe.threshold, safeAddress, data, wallet?.address, isFeatureEnabled], + [safe.chainId, safe.threshold, safeAddress, data, signer?.address, isFeatureEnabled], false, ) diff --git a/src/components/tx/security/tenderly/index.tsx b/src/components/tx/security/tenderly/index.tsx index 7022849d28..9ad2f4c504 100644 --- a/src/components/tx/security/tenderly/index.tsx +++ b/src/components/tx/security/tenderly/index.tsx @@ -4,7 +4,7 @@ import { useContext, useEffect } from 'react' import type { ReactElement } from 'react' import useSafeInfo from '@/hooks/useSafeInfo' -import useWallet from '@/hooks/wallets/useWallet' +import { useSigner } from '@/hooks/wallets/useWallet' import CheckIcon from '@/public/images/common/check.svg' import CloseIcon from '@/public/images/common/close.svg' import { useDarkMode } from '@/hooks/useDarkMode' @@ -34,7 +34,7 @@ export type TxSimulationProps = { // TODO: Test this component const TxSimulationBlock = ({ transactions, disabled, gasLimit, executionOwner }: TxSimulationProps): ReactElement => { const { safe } = useSafeInfo() - const wallet = useWallet() + const signer = useSigner() const isSafeOwner = useIsSafeOwner() const isDarkMode = useDarkMode() const { safeTx } = useContext(SafeTxContext) @@ -44,14 +44,14 @@ const TxSimulationBlock = ({ transactions, disabled, gasLimit, executionOwner }: } = useContext(TxInfoContext) const handleSimulation = async () => { - if (!wallet) { + if (!signer) { return } simulateTransaction({ safe, // fall back to the first owner of the safe in case the transaction is created by a proposer - executionOwner: (executionOwner ?? isSafeOwner) ? wallet.address : safe.owners[0].value, + executionOwner: (executionOwner ?? isSafeOwner) ? signer.address : safe.owners[0].value, transactions, gasLimit, } as SimulationTxParams) diff --git a/src/features/myAccounts/hooks/useAllOwnedSafes.ts b/src/features/myAccounts/hooks/useAllOwnedSafes.ts index b96e7007cb..4af05cd4aa 100644 --- a/src/features/myAccounts/hooks/useAllOwnedSafes.ts +++ b/src/features/myAccounts/hooks/useAllOwnedSafes.ts @@ -1,40 +1,25 @@ import type { AllOwnedSafes } from '@safe-global/safe-gateway-typescript-sdk' -import { getAllOwnedSafes } from '@safe-global/safe-gateway-typescript-sdk' import type { AsyncResult } from '@/hooks/useAsync' -import useAsync from '@/hooks/useAsync' import useLocalStorage from '@/services/local-storage/useLocalStorage' import { useEffect } from 'react' +import { useGetAllOwnedSafesQuery } from '@/store/api/gateway' +import { asError } from '@/services/exceptions/utils' +import { skipToken } from '@reduxjs/toolkit/query' const CACHE_KEY = 'ownedSafesCache_' -type OwnedSafesPerAddress = { - address: string | undefined - ownedSafes: AllOwnedSafes -} - const useAllOwnedSafes = (address: string): AsyncResult => { const [cache, setCache] = useLocalStorage(CACHE_KEY + address) - const [data, error, isLoading] = useAsync(async () => { - if (!address) - return { - ownedSafes: {}, - address: undefined, - } - const ownedSafes = await getAllOwnedSafes(address) - return { - ownedSafes, - address, - } - }, [address]) + const { data, error, isLoading } = useGetAllOwnedSafesQuery(address === '' ? skipToken : { walletAddress: address }) useEffect(() => { - if (data?.ownedSafes != undefined && data.address === address) { - setCache(data.ownedSafes) + if (data != undefined) { + setCache(data) } }, [address, cache, data, setCache]) - return [cache, error, isLoading] + return [cache, asError(error), isLoading] } export default useAllOwnedSafes diff --git a/src/features/myAccounts/hooks/useAllSafes.ts b/src/features/myAccounts/hooks/useAllSafes.ts index 6990098fad..11e1a32bbf 100644 --- a/src/features/myAccounts/hooks/useAllSafes.ts +++ b/src/features/myAccounts/hooks/useAllSafes.ts @@ -8,6 +8,7 @@ import useWallet from '@/hooks/wallets/useWallet' import { selectAllAddressBooks, selectAllVisitedSafes, selectUndeployedSafes } from '@/store/slices' import { sameAddress } from '@/utils/addresses' import useAllOwnedSafes from './useAllOwnedSafes' + export type SafeItem = { chainId: string address: string diff --git a/src/features/recovery/hooks/__tests__/useIsValidExecution.test.ts b/src/features/recovery/hooks/__tests__/useIsValidExecution.test.ts index 4fb0e7c5f0..6ca079a063 100644 --- a/src/features/recovery/hooks/__tests__/useIsValidExecution.test.ts +++ b/src/features/recovery/hooks/__tests__/useIsValidExecution.test.ts @@ -52,7 +52,7 @@ describe('useIsValidExecution', () => { jest.resetAllMocks() jest.spyOn(web3, 'useWeb3ReadOnly').mockImplementation(() => mockReadOnlyProvider) - jest.spyOn(useWallet, 'default').mockReturnValue(mockWallet) + jest.spyOn(useWallet, 'useSigner').mockReturnValue(mockWallet) jest.spyOn(web3, 'createWeb3').mockImplementation(() => mockProvider) }) diff --git a/src/hooks/__tests__/useGasLimit.test.ts b/src/hooks/__tests__/useGasLimit.test.ts index bfdf20d472..c63d061381 100644 --- a/src/hooks/__tests__/useGasLimit.test.ts +++ b/src/hooks/__tests__/useGasLimit.test.ts @@ -30,7 +30,9 @@ describe('useGasLimit', () => { getContractManager: () => contractManager, } as unknown as Safe) - jest.spyOn(useWallet, 'default').mockReturnValue(connectedWalletBuilder().with({ address: walletAddress }).build()) + jest + .spyOn(useWallet, 'useSigner') + .mockReturnValue(connectedWalletBuilder().with({ address: walletAddress }).build()) jest.spyOn(useSafeInfo, 'default').mockReturnValue({ safe: { ...safeInfo, deployed: true }, safeAddress: safeInfo.address.value, @@ -49,7 +51,7 @@ describe('useGasLimit', () => { }) it('should return undefined if no owner is connected', async () => { - jest.spyOn(useWallet, 'default').mockReturnValue( + jest.spyOn(useWallet, 'useSigner').mockReturnValue( connectedWalletBuilder() .with({ address: undefined, diff --git a/src/hooks/__tests__/useNestedSafeOwners.test.ts b/src/hooks/__tests__/useNestedSafeOwners.test.ts new file mode 100644 index 0000000000..ed3d8b187e --- /dev/null +++ b/src/hooks/__tests__/useNestedSafeOwners.test.ts @@ -0,0 +1,54 @@ +import { useNestedSafeOwners } from '../useNestedSafeOwners' +import useSafeInfo from '@/hooks/useSafeInfo' +import { faker } from '@faker-js/faker' +import { addressExBuilder, extendedSafeInfoBuilder } from '@/tests/builders/safe' +import { renderHook } from '@/tests/test-utils' +import { generateRandomArray } from '@/tests/builders/utils' +import useOwnedSafes from '../useOwnedSafes' + +jest.mock('@/hooks/useOwnedSafes') +jest.mock('@/hooks/useSafeInfo') + +describe('useNestedSafeOwners', () => { + const mockUseSafeInfo = useSafeInfo as jest.MockedFunction + const mockUseOwnedSafes = useOwnedSafes as jest.MockedFunction + + const safeAddress = faker.finance.ethereumAddress() + // Safe with 3 owners + const mockSafeInfo = { + safeAddress, + safe: extendedSafeInfoBuilder() + .with({ address: { value: safeAddress } }) + .with({ chainId: '1' }) + .with({ owners: generateRandomArray(() => addressExBuilder().build(), { min: 3, max: 3 }) }) + .build(), + safeLoaded: true, + safeLoading: false, + } + + const mockOwners = mockSafeInfo.safe.owners + + beforeAll(() => { + mockUseSafeInfo.mockReturnValue(mockSafeInfo) + }) + + it('should return undefined without owned Safes', () => { + mockUseOwnedSafes.mockReturnValue({}) + const { result } = renderHook(() => useNestedSafeOwners()) + expect(result.current).toEqual(undefined) + }) + + it('should return empty list if no owned Safe is in the owners', () => { + mockUseOwnedSafes.mockReturnValue({ '1': [faker.finance.ethereumAddress()] }) + const { result } = renderHook(() => useNestedSafeOwners()) + expect(result.current).toEqual([]) + }) + + it('should return intersection of owners and owned Safes', () => { + mockUseOwnedSafes.mockReturnValue({ + '1': [faker.finance.ethereumAddress(), mockOwners[0].value, mockOwners[1].value, mockOwners[2].value], + }) + const { result } = renderHook(() => useNestedSafeOwners()) + expect(result.current).toEqual([mockOwners[0].value, mockOwners[1].value, mockOwners[2].value]) + }) +}) diff --git a/src/hooks/useGasLimit.ts b/src/hooks/useGasLimit.ts index cd6f1626b5..73fcde6b83 100644 --- a/src/hooks/useGasLimit.ts +++ b/src/hooks/useGasLimit.ts @@ -7,7 +7,7 @@ import useAsync from '@/hooks/useAsync' import useChainId from '@/hooks/useChainId' import { useWeb3ReadOnly } from '@/hooks/wallets/web3' import chains from '@/config/chains' -import useWallet from './wallets/useWallet' +import { useSigner } from './wallets/useWallet' import { useSafeSDK } from './coreSDK/safeCoreSDK' import useIsSafeOwner from './useIsSafeOwner' import { Errors, logError } from '@/services/exceptions' @@ -144,7 +144,7 @@ const useGasLimit = ( const { safe } = useSafeInfo() const safeAddress = safe.address.value const threshold = safe.threshold - const wallet = useWallet() + const wallet = useSigner() const walletAddress = wallet?.address const isOwner = useIsSafeOwner() const currentChainId = useChainId() diff --git a/src/hooks/useIsNestedSafeOwner.ts b/src/hooks/useIsNestedSafeOwner.ts new file mode 100644 index 0000000000..10a2c87fb0 --- /dev/null +++ b/src/hooks/useIsNestedSafeOwner.ts @@ -0,0 +1,7 @@ +import { useMemo } from 'react' +import { useNestedSafeOwners } from './useNestedSafeOwners' + +export const useIsNestedSafeOwner = () => { + const nestedOwners = useNestedSafeOwners() + return useMemo(() => nestedOwners && nestedOwners.length > 0, [nestedOwners]) +} diff --git a/src/hooks/useIsSafeOwner.ts b/src/hooks/useIsSafeOwner.ts index e1118c5818..4e6f73c8bd 100644 --- a/src/hooks/useIsSafeOwner.ts +++ b/src/hooks/useIsSafeOwner.ts @@ -1,12 +1,12 @@ import useSafeInfo from '@/hooks/useSafeInfo' -import useWallet from '@/hooks/wallets/useWallet' import { isOwner } from '@/utils/transaction-guards' +import { useSigner } from './wallets/useWallet' const useIsSafeOwner = () => { const { safe } = useSafeInfo() - const wallet = useWallet() + const signer = useSigner() - return isOwner(safe.owners, wallet?.address) + return isOwner(safe.owners, signer?.address) } export default useIsSafeOwner diff --git a/src/hooks/useIsValidExecution.ts b/src/hooks/useIsValidExecution.ts index e13194b007..0548d88c08 100644 --- a/src/hooks/useIsValidExecution.ts +++ b/src/hooks/useIsValidExecution.ts @@ -9,9 +9,11 @@ import { type JsonRpcProvider } from 'ethers' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import { getCurrentGnosisSafeContract } from '@/services/contracts/safeContracts' import useSafeInfo from '@/hooks/useSafeInfo' -import useWallet from '@/hooks/wallets/useWallet' +import { useSigner } from '@/hooks/wallets/useWallet' import { encodeSignatures } from '@/services/tx/encodeSignatures' import useIsSafeOwner from '@/hooks/useIsSafeOwner' +import { type NestedWallet } from '@/utils/nested-safe-wallet' +import { assertProvider } from '@/utils/helpers' const isContractError = (error: EthersError) => { if (!error.reason) return false @@ -22,10 +24,12 @@ const isContractError = (error: EthersError) => { // Monkey patch the signerProvider to proxy requests to the "readonly" provider if on the wrong chain // This is ONLY used to check the validity of a transaction in `useIsValidExecution` export const getPatchedSignerProvider = ( - wallet: ConnectedWallet, + wallet: ConnectedWallet | NestedWallet, chainId: SafeInfo['chainId'], readOnlyProvider: JsonRpcProvider, ) => { + assertProvider(wallet.provider) + const signerProvider = createWeb3(wallet.provider) if (wallet.chainId !== chainId) { @@ -57,7 +61,7 @@ const useIsValidExecution = ( executionValidationError?: Error isValidExecutionLoading: boolean } => { - const wallet = useWallet() + const wallet = useSigner() const { safe } = useSafeInfo() const readOnlyProvider = useWeb3ReadOnly() const isOwner = useIsSafeOwner() diff --git a/src/hooks/useNestedSafeOwners.tsx b/src/hooks/useNestedSafeOwners.tsx new file mode 100644 index 0000000000..efda33c46d --- /dev/null +++ b/src/hooks/useNestedSafeOwners.tsx @@ -0,0 +1,19 @@ +import useSafeInfo from '@/hooks/useSafeInfo' +import { useMemo } from 'react' +import useOwnedSafes from './useOwnedSafes' + +export const useNestedSafeOwners = () => { + const { safe, safeLoaded } = useSafeInfo() + const allOwned = useOwnedSafes() + + const nestedSafeOwner = useMemo(() => { + if (!safeLoaded) return null + + // Find an intersection of owned safes and the owners of the current safe + const ownerAddresses = safe?.owners.map((owner) => owner.value) + + return allOwned[safe.chainId]?.filter((ownedSafe) => ownerAddresses?.includes(ownedSafe)) + }, [allOwned, safe, safeLoaded]) + + return nestedSafeOwner +} diff --git a/src/hooks/useOwnedSafes.ts b/src/hooks/useOwnedSafes.ts index 66db1772b8..9a0f778ab1 100644 --- a/src/hooks/useOwnedSafes.ts +++ b/src/hooks/useOwnedSafes.ts @@ -1,12 +1,10 @@ -import { useEffect } from 'react' -import { getOwnedSafes, type OwnedSafes } from '@safe-global/safe-gateway-typescript-sdk' +import { useMemo } from 'react' +import { type OwnedSafes } from '@safe-global/safe-gateway-typescript-sdk' -import useLocalStorage from '@/services/local-storage/useLocalStorage' import useWallet from '@/hooks/wallets/useWallet' -import { Errors, logError } from '@/services/exceptions' import useChainId from './useChainId' - -const CACHE_KEY = 'ownedSafes' +import { useGetOwnedSafesQuery } from '@/store/slices' +import { skipToken } from '@reduxjs/toolkit/query' type OwnedSafesCache = { [walletAddress: string]: { @@ -17,36 +15,12 @@ type OwnedSafesCache = { const useOwnedSafes = (): OwnedSafesCache['walletAddress'] => { const chainId = useChainId() const { address: walletAddress } = useWallet() || {} - const [ownedSafesCache, setOwnedSafesCache] = useLocalStorage(CACHE_KEY) - - useEffect(() => { - if (!walletAddress || !chainId) return - let isCurrent = true - /** - * No useAsync in this case to avoid updating - * for a new chainId with stale data see https://github.com/safe-global/safe-wallet-web/pull/1760#discussion_r1133705349 - */ - getOwnedSafes(chainId, walletAddress) - .then( - (ownedSafes) => - isCurrent && - setOwnedSafesCache((prev) => ({ - ...prev, - [walletAddress]: { - ...(prev?.[walletAddress] || {}), - [chainId]: ownedSafes.safes, - }, - })), - ) - .catch((error: Error) => logError(Errors._610, error.message)) + const { data: ownedSafes } = useGetOwnedSafesQuery(walletAddress ? { chainId, walletAddress } : skipToken) - return () => { - isCurrent = false - } - }, [chainId, walletAddress, setOwnedSafesCache]) + const result = useMemo(() => ({ [chainId]: ownedSafes?.safes ?? [] }), [chainId, ownedSafes]) - return ownedSafesCache?.[walletAddress || ''] ?? {} + return result ?? {} } export default useOwnedSafes diff --git a/src/hooks/useTransactionStatus.ts b/src/hooks/useTransactionStatus.ts index ce79a694ae..1e539869d8 100644 --- a/src/hooks/useTransactionStatus.ts +++ b/src/hooks/useTransactionStatus.ts @@ -22,6 +22,7 @@ export const STATUS_LABELS: Record = { [PendingStatus.RELAYING]: 'Relaying', [PendingStatus.INDEXING]: 'Indexing', [PendingStatus.SIGNING]: 'Signing', + [PendingStatus.NESTED_SIGNING]: 'Signing', [ReplacedStatus]: 'Transaction will be replaced', } diff --git a/src/hooks/useTransactionType.tsx b/src/hooks/useTransactionType.tsx index 1955d01e72..fa2f74d48b 100644 --- a/src/hooks/useTransactionType.tsx +++ b/src/hooks/useTransactionType.tsx @@ -14,10 +14,9 @@ import BatchIcon from '@/public/images/common/multisend.svg' import { isCancellationTxInfo, - isExecTxInfo, isModuleExecutionInfo, isMultiSendTxInfo, - isOnChainConfirmationTxInfo, + isNestedConfirmationTxInfo, isOutgoingTransfer, isTxQueued, } from '@/utils/transaction-guards' @@ -133,7 +132,7 @@ export const getTransactionType = (tx: TransactionSummary, addressBook: AddressB } } - if (isOnChainConfirmationTxInfo(tx.txInfo) || isExecTxInfo(tx.txInfo)) { + if (isNestedConfirmationTxInfo(tx.txInfo)) { return { icon: , text: `Nested Safe${addressBookName ? `: ${addressBookName}` : ''}`, diff --git a/src/hooks/useTxPendingStatuses.ts b/src/hooks/useTxPendingStatuses.ts index 776fce6d4f..66897e17b3 100644 --- a/src/hooks/useTxPendingStatuses.ts +++ b/src/hooks/useTxPendingStatuses.ts @@ -233,6 +233,32 @@ const useTxPendingStatuses = (): void => { ) }) + const unsubNestedTx = txSubscribe(TxEvent.NESTED_SAFE_TX_CREATED, (detail) => { + const txId = detail.txId + const nonce = detail.nonce + + if (!txId || nonce === undefined) return + + // If we have future issues with statuses, we should refactor `useTxPendingStatuses` + // @see https://github.com/safe-global/safe-wallet-web/issues/1754 + const isIndexed = historicalTxs.some((tx) => tx.transaction.id === txId) + if (isIndexed) { + return + } + + dispatch( + setPendingTx({ + nonce, + chainId, + safeAddress, + txId, + status: PendingStatus.NESTED_SIGNING, + signerAddress: detail.parentSafeAddress, + txHashOrParentSafeTxHash: detail.txHashOrParentSafeTxHash, + }), + ) + }) + // All final states stop the watcher and clear the pending state const unsubFns = FINAL_PENDING_STATUSES.map((event) => txSubscribe(event, (detail) => { @@ -249,7 +275,14 @@ const useTxPendingStatuses = (): void => { }), ) - unsubFns.push(unsubProcessing, unsubSignatureProposing, unsubExecuting, unsubProcessed, unsubRelaying) + unsubFns.push( + unsubProcessing, + unsubSignatureProposing, + unsubExecuting, + unsubProcessed, + unsubRelaying, + unsubNestedTx, + ) return () => { unsubFns.forEach((unsub) => unsub()) diff --git a/src/hooks/wallets/useSelectAvailableSigner.ts b/src/hooks/wallets/useSelectAvailableSigner.ts new file mode 100644 index 0000000000..feef89fd90 --- /dev/null +++ b/src/hooks/wallets/useSelectAvailableSigner.ts @@ -0,0 +1,24 @@ +import { useCallback } from 'react' +import { useWalletContext } from './useWallet' +import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' +import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' +import { useNestedSafeOwners } from '../useNestedSafeOwners' +import { getAvailableSigners } from '@/utils/signers' + +/** + * + * @returns a function that sets a signer that can sign the given transaction in the given Safe + */ +export const useSelectAvailableSigner = () => { + const { connectedWallet: wallet, setSignerAddress } = useWalletContext() ?? {} + const nestedSafeOwners = useNestedSafeOwners() + + return useCallback( + (tx: SafeTransaction | undefined, safe: SafeInfo) => { + const availableSigners = getAvailableSigners(wallet, nestedSafeOwners, safe, tx) + + setSignerAddress?.(availableSigners[0]) + }, + [setSignerAddress, nestedSafeOwners, wallet], + ) +} diff --git a/src/hooks/wallets/useWallet.ts b/src/hooks/wallets/useWallet.ts index c3b0b3c10c..70e6fc476b 100644 --- a/src/hooks/wallets/useWallet.ts +++ b/src/hooks/wallets/useWallet.ts @@ -3,6 +3,14 @@ import { type ConnectedWallet } from './useOnboard' import { WalletContext } from '@/components/common/WalletProvider' const useWallet = (): ConnectedWallet | null => { + return useContext(WalletContext)?.connectedWallet ?? null +} + +export const useSigner = () => { + return useContext(WalletContext)?.signer ?? null +} + +export const useWalletContext = () => { return useContext(WalletContext) } diff --git a/src/services/analytics/events/modals.ts b/src/services/analytics/events/modals.ts index cba11054ec..eebafc3291 100644 --- a/src/services/analytics/events/modals.ts +++ b/src/services/analytics/events/modals.ts @@ -72,6 +72,21 @@ export const MODALS_EVENTS = { action: 'Swap', category: MODALS_CATEGORY, }, + CHANGE_SIGNER: { + action: 'Change tx signer', + category: MODALS_CATEGORY, + event: EventType.CLICK, + }, + OPEN_PARENT_TX: { + action: 'Open parent transaction', + category: MODALS_CATEGORY, + event: EventType.CLICK, + }, + OPEN_NESTED_TX: { + action: 'Open nested transaction', + category: MODALS_CATEGORY, + event: EventType.CLICK, + }, } export enum MODAL_NAVIGATION { diff --git a/src/services/analytics/events/transactions.ts b/src/services/analytics/events/transactions.ts index e2a0c12425..61b198035c 100644 --- a/src/services/analytics/events/transactions.ts +++ b/src/services/analytics/events/transactions.ts @@ -19,6 +19,7 @@ export enum TX_TYPES { batch = 'batch', rejection = 'rejection', typed_message = 'typed_message', + nested_safe = 'nested_safe', walletconnect = 'walletconnect', custom = 'custom', native_bridge = 'native_bridge', @@ -79,4 +80,29 @@ export const TX_EVENTS = { action: 'Execute via role', category: TX_CATEGORY, }, + CREATE_VIA_PARENT: { + event: EventType.TX_CREATED, + action: 'Create via parent', + category: TX_CATEGORY, + }, + CONFIRM_VIA_PARENT: { + event: EventType.TX_CREATED, + action: 'Confirm via parent', + category: TX_CATEGORY, + }, + EXECUTE_VIA_PARENT: { + event: EventType.TX_CREATED, + action: 'Execute via parent', + category: TX_CATEGORY, + }, + CONFIRM_IN_PARENT: { + event: EventType.TX_CONFIRMED, + action: 'Confirm in parent', + category: TX_CATEGORY, + }, + EXECUTE_IN_PARENT: { + event: EventType.TX_EXECUTED, + action: 'Execute in parent', + category: TX_CATEGORY, + }, } diff --git a/src/services/analytics/tx-tracking.ts b/src/services/analytics/tx-tracking.ts index e97da67293..fa2d20c913 100644 --- a/src/services/analytics/tx-tracking.ts +++ b/src/services/analytics/tx-tracking.ts @@ -9,6 +9,7 @@ import { isCancellationTxInfo, isSwapOrderTxInfo, isAnyStakingTxInfo, + isNestedConfirmationTxInfo, } from '@/utils/transaction-guards' import { BRIDGE_WIDGET_URL } from '@/features/bridge/components/BridgeWidget' @@ -75,6 +76,10 @@ export const getTransactionTrackingType = (details: TransactionDetails | undefin return TX_TYPES.batch } + if (isNestedConfirmationTxInfo(txInfo)) { + return TX_TYPES.nested_safe + } + return TX_TYPES.walletconnect } diff --git a/src/services/exceptions/ErrorCodes.ts b/src/services/exceptions/ErrorCodes.ts index 3a6e098b0e..b1767ab521 100644 --- a/src/services/exceptions/ErrorCodes.ts +++ b/src/services/exceptions/ErrorCodes.ts @@ -70,6 +70,7 @@ enum ErrorCodes { _814 = '814: Failed to speed up transaction', _815 = '815: Error executing a transaction through a role', _816 = '816: Error computing replay Safe creation data', + _817 = '817: Error sending a transaction through nested Safe provider', _900 = '900: Error loading Safe App', _901 = '901: Error processing Safe Apps SDK request', diff --git a/src/services/safe-wallet-provider/index.ts b/src/services/safe-wallet-provider/index.ts index 89c568d3b3..086d835285 100644 --- a/src/services/safe-wallet-provider/index.ts +++ b/src/services/safe-wallet-provider/index.ts @@ -93,7 +93,8 @@ export class SafeWalletProvider { return this.wallet_switchEthereumChain(...(params as [{ chainId: string }]), appInfo) } - case 'eth_accounts': { + case 'eth_accounts': + case 'eth_requestAccounts': { return this.eth_accounts() } diff --git a/src/services/tx/tx-sender/__tests__/ts-sender.test.ts b/src/services/tx/tx-sender/__tests__/ts-sender.test.ts index 40ce5714e3..190434c93d 100644 --- a/src/services/tx/tx-sender/__tests__/ts-sender.test.ts +++ b/src/services/tx/tx-sender/__tests__/ts-sender.test.ts @@ -43,12 +43,14 @@ const SIGNER_ADDRESS = '0x1234567890123456789012345678901234567890' const TX_HASH = '0x1234567890' // Mock getTransactionDetails jest.mock('@safe-global/safe-gateway-typescript-sdk', () => ({ + ...jest.requireActual('@safe-global/safe-gateway-typescript-sdk'), getTransactionDetails: jest.fn(), postSafeGasEstimation: jest.fn(() => Promise.resolve({ safeTxGas: 60000, recommendedNonce: 17 })), Operation: { CALL: 0, }, relayTransaction: jest.fn(() => Promise.resolve({ taskId: '0xdead1' })), + __esModule: true, })) // Mock extractTxInfo diff --git a/src/services/tx/tx-sender/dispatch.ts b/src/services/tx/tx-sender/dispatch.ts index a60ce9d11c..15d256440b 100644 --- a/src/services/tx/tx-sender/dispatch.ts +++ b/src/services/tx/tx-sender/dispatch.ts @@ -141,18 +141,22 @@ export const dispatchOnChainSigning = async ( chainId: SafeInfo['chainId'], signerAddress: string, safeAddress: string, + isNestedSafe: boolean, ) => { const sdk = await getSafeSDKWithSigner(provider) const safeTxHash = await sdk.getTransactionHash(safeTx) const eventParams = { txId, nonce: safeTx.data.nonce } const options = chainId === chains.zksync ? { gasLimit: ZK_SYNC_ON_CHAIN_SIGNATURE_GAS_LIMIT } : undefined - + let txHashOrParentSafeTxHash: string try { // TODO: This is a workaround until there is a fix for unchecked transactions in the protocol-kit const encodedApproveHashTx = await prepareApproveTxHash(safeTxHash, provider) - await provider.request({ + // Note: SafeWalletProvider returns transaction hash if it exists, otherwise the safeTxHash + // If the parent immediately executes, this will be the transaction hash of the approveHash + // otherwise the safeTxHash of it + txHashOrParentSafeTxHash = await provider.request({ method: 'eth_sendTransaction', params: [{ from: signerAddress, to: safeAddress, data: encodedApproveHashTx, gas: options?.gasLimit }], }) @@ -165,6 +169,14 @@ export const dispatchOnChainSigning = async ( txDispatch(TxEvent.ONCHAIN_SIGNATURE_SUCCESS, eventParams) + if (isNestedSafe) { + txDispatch(TxEvent.NESTED_SAFE_TX_CREATED, { + ...eventParams, + txHashOrParentSafeTxHash, + parentSafeAddress: signerAddress, + }) + } + // Until the on-chain signature is/has been executed, the safeTx is not // signed so we don't return it } diff --git a/src/services/tx/txEvents.ts b/src/services/tx/txEvents.ts index 562e52135c..9ecdb89778 100644 --- a/src/services/tx/txEvents.ts +++ b/src/services/tx/txEvents.ts @@ -12,6 +12,7 @@ export enum TxEvent { SIGNATURE_INDEXED = 'SIGNATURE_INDEXED', ONCHAIN_SIGNATURE_REQUESTED = 'ONCHAIN_SIGNATURE_REQUESTED', ONCHAIN_SIGNATURE_SUCCESS = 'ONCHAIN_SIGNATURE_SUCCESS', + NESTED_SAFE_TX_CREATED = 'NESTED_SAFE_TX_CREATED', EXECUTING = 'EXECUTING', PROCESSING = 'PROCESSING', PROCESSING_MODULE = 'PROCESSING_MODULE', @@ -38,6 +39,7 @@ interface TxEvents { [TxEvent.SIGNATURE_INDEXED]: { txId: string } [TxEvent.ONCHAIN_SIGNATURE_REQUESTED]: Id [TxEvent.ONCHAIN_SIGNATURE_SUCCESS]: Id + [TxEvent.NESTED_SAFE_TX_CREATED]: Id & { parentSafeAddress: string; txHashOrParentSafeTxHash: string } [TxEvent.EXECUTING]: Id [TxEvent.PROCESSING]: Id & { txHash: string diff --git a/src/store/__tests__/txQueueSlice.test.ts b/src/store/__tests__/txQueueSlice.test.ts index 7c92ce8f99..312bfbd7a3 100644 --- a/src/store/__tests__/txQueueSlice.test.ts +++ b/src/store/__tests__/txQueueSlice.test.ts @@ -17,6 +17,7 @@ import { txQueueListener, txQueueSlice } from '../txQueueSlice' import type { PendingTxsState } from '../pendingTxsSlice' import { PendingStatus } from '../pendingTxsSlice' import type { RootState } from '..' +import { faker } from '@faker-js/faker/.' describe('txQueueSlice', () => { const listenerMiddlewareInstance = createListenerMiddleware() @@ -71,6 +72,48 @@ describe('txQueueSlice', () => { expect(txDispatchSpy).toHaveBeenCalledWith(txEvents.TxEvent.SIGNATURE_INDEXED, { txId: '0x123' }) }) + it('should dispatch SIGNATURE_INDEXED event for Nested Signing state', () => { + const state = { + pendingTxs: { + '0x123': { + nonce: 1, + chainId: '5', + safeAddress: '0x0000000000000000000000000000000000000000', + status: PendingStatus.NESTED_SIGNING, + signerAddress: '0x456', + txHashOrParentSafeTxHash: faker.string.hexadecimal({ length: 64 }), + }, + } as PendingTxsState, + } as RootState + + const listenerApi = { + getState: jest.fn(() => state), + dispatch: jest.fn(), + } + + const transaction = { + type: TransactionListItemType.TRANSACTION, + transaction: { + id: '0x123', + executionInfo: { + type: DetailedExecutionInfoType.MULTISIG, + missingSigners: [], + }, + }, + } as unknown as TransactionListItem + + const action = txQueueSlice.actions.set({ + loading: false, + data: { + results: [transaction], + }, + }) + + listenerMiddlewareInstance.middleware(listenerApi)(jest.fn())(action) + + expect(txDispatchSpy).toHaveBeenCalledWith(txEvents.TxEvent.SIGNATURE_INDEXED, { txId: '0x123' }) + }) + it('should not dispatch an event if the queue slice is cleared', () => { const state = { pendingTxs: { diff --git a/src/store/api/gateway/index.ts b/src/store/api/gateway/index.ts index 8c8893b150..7e2fdccb18 100644 --- a/src/store/api/gateway/index.ts +++ b/src/store/api/gateway/index.ts @@ -1,7 +1,14 @@ import { proposerEndpoints } from '@/store/api/gateway/proposers' import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query/react' -import { getTransactionDetails, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' +import { + type AllOwnedSafes, + getAllOwnedSafes, + getTransactionDetails, + type TransactionDetails, + type OwnedSafes, + getOwnedSafes, +} from '@safe-global/safe-gateway-typescript-sdk' import { asError } from '@/services/exceptions/utils' import { safeOverviewEndpoints } from './safeOverviews' import { createSubmission, getSubmission } from '@safe-global/safe-client-gateway-sdk' @@ -29,6 +36,16 @@ export const gatewayApi = createApi({ return buildQueryFn(() => Promise.all(txIds.map((txId) => getTransactionDetails(chainId, txId)))) }, }), + getAllOwnedSafes: builder.query({ + queryFn({ walletAddress }) { + return buildQueryFn(() => getAllOwnedSafes(walletAddress)) + }, + }), + getOwnedSafes: builder.query({ + queryFn({ chainId, walletAddress }) { + return buildQueryFn(() => getOwnedSafes(chainId, walletAddress)) + }, + }), getSubmission: builder.query< getSubmission, { outreachId: number; chainId: string; safeAddress: string; signerAddress: string } @@ -72,4 +89,6 @@ export const { useCreateSubmissionMutation, useGetSafeOverviewQuery, useGetMultipleSafeOverviewsQuery, + useGetAllOwnedSafesQuery, + useGetOwnedSafesQuery, } = gatewayApi diff --git a/src/store/pendingTxsSlice.ts b/src/store/pendingTxsSlice.ts index 671c550a08..8eee001fdf 100644 --- a/src/store/pendingTxsSlice.ts +++ b/src/store/pendingTxsSlice.ts @@ -6,6 +6,7 @@ import { selectChainIdAndSafeAddress } from '@/store/common' export enum PendingStatus { SIGNING = 'SIGNING', + NESTED_SIGNING = 'NESTED_SIGNING', SUBMITTING = 'SUBMITTING', PROCESSING = 'PROCESSING', RELAYING = 'RELAYING', @@ -67,12 +68,19 @@ type PendingIndexingTx = PendingTxCommonProps & { txHash?: string } +type PendingNestedSigningTx = PendingTxCommonProps & { + signerAddress: string + txHashOrParentSafeTxHash: string + status: PendingStatus.NESTED_SIGNING +} + export type PendingTx = | PendingSigningTx | PendingSubmittingTx | PendingProcessingTx | PendingRelayingTx | PendingIndexingTx + | PendingNestedSigningTx export type PendingTxsState = { [txId: string]: PendingTx diff --git a/src/store/txQueueSlice.ts b/src/store/txQueueSlice.ts index 37ca6c0ae8..f5decbd539 100644 --- a/src/store/txQueueSlice.ts +++ b/src/store/txQueueSlice.ts @@ -8,6 +8,8 @@ import { PendingStatus, selectPendingTxs } from './pendingTxsSlice' import { sameAddress } from '@/utils/addresses' import { txDispatch, TxEvent } from '@/services/tx/txEvents' +const SIGNING_STATES = [PendingStatus.SIGNING, PendingStatus.NESTED_SIGNING] + const { slice, selector } = makeLoadableSlice('txQueue', undefined as TransactionListPage | undefined) export const txQueueSlice = slice @@ -45,7 +47,7 @@ export const txQueueListener = (listenerMiddleware: typeof listenerMiddlewareIns const txId = result.transaction.id const pendingTx = pendingTxs[txId] - if (!pendingTx || pendingTx.status !== PendingStatus.SIGNING) { + if (!pendingTx || !SIGNING_STATES.includes(pendingTx.status) || !('signerAddress' in pendingTx)) { continue } diff --git a/src/tests/builders/safeTx.ts b/src/tests/builders/safeTx.ts index 1e8d4eb2ca..449238592a 100644 --- a/src/tests/builders/safeTx.ts +++ b/src/tests/builders/safeTx.ts @@ -2,6 +2,15 @@ import { Builder, type IBuilder } from '@/tests/Builder' import { faker } from '@faker-js/faker' import { type SafeTransactionData, type SafeSignature, type SafeTransaction } from '@safe-global/safe-core-sdk-types' import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' +import { + type Custom, + DetailedExecutionInfoType, + type MultisigExecutionInfo, + type TransactionInfo, + TransactionInfoType, + type TransactionSummary, +} from '@safe-global/safe-gateway-typescript-sdk' +import { TransactionStatus } from '@safe-global/safe-apps-sdk' // TODO: Convert to builder export const createSafeTx = (data = '0x'): SafeTransaction => { @@ -65,3 +74,40 @@ export function safeSignatureBuilder(): IBuilder { data: faker.string.hexadecimal({ length: faker.number.int({ max: 500 }) }), }) } + +export function safeTxSummaryBuilder(): IBuilder { + return Builder.new().with({ + id: `multisig_${faker.string.hexadecimal({ length: 40 })}_${faker.string.hexadecimal({ length: 64 })}`, + executionInfo: executionInfoBuilder().build(), + txInfo: txInfoBuilder().build(), + txStatus: faker.helpers.enumValue(TransactionStatus), + }) +} + +export function executionInfoBuilder(): IBuilder { + const num1 = faker.number.int({ min: 1, max: 10 }) + const num2 = faker.number.int({ min: 1, max: 10 }) + + return Builder.new().with({ + nonce: faker.number.int(), + type: DetailedExecutionInfoType.MULTISIG, + confirmationsRequired: Math.max(num1, num2), + confirmationsSubmitted: Math.min(num1, num2), + missingSigners: Array.from({ length: Math.min(num1, num2) }).map(() => ({ + value: faker.finance.ethereumAddress(), + })), + }) +} + +export function txInfoBuilder(): IBuilder { + const mockData = faker.string.hexadecimal({ length: { min: 0, max: 128 } }) + return Builder.new().with({ + type: TransactionInfoType.CUSTOM, + actionCount: 1, + dataSize: mockData.length.toString(), + isCancellation: false, + methodName: faker.string.alpha(), + to: { value: faker.finance.ethereumAddress() }, + value: faker.number.bigInt({ min: 0, max: 10n ** 18n }).toString(), + }) +} diff --git a/src/utils/__tests__/signers.test.ts b/src/utils/__tests__/signers.test.ts new file mode 100644 index 0000000000..5ef249b3ed --- /dev/null +++ b/src/utils/__tests__/signers.test.ts @@ -0,0 +1,140 @@ +import { getAvailableSigners } from '../signers' +import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' +import { safeInfoBuilder } from '@/tests/builders/safe' +import { faker } from '@faker-js/faker' +import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' +import { checksumAddress } from '../addresses' + +describe('getAvailableSigners', () => { + const mockWallet = { + address: checksumAddress(faker.finance.ethereumAddress()), + } as ConnectedWallet + const parentSafe = checksumAddress(faker.finance.ethereumAddress()) + + const mockTx = { + signatures: new Map(), + } as SafeTransaction + + it('should return an empty array if wallet is null', () => { + const mockSafe = safeInfoBuilder() + .with({ owners: [{ value: mockWallet.address }] }) + .build() + + const result = getAvailableSigners(null, ['0xOwner1'], mockSafe, mockTx) + + expect(result).toEqual([]) + }) + + it('should return an empty array if nestedSafeOwners is null', () => { + const mockSafe = safeInfoBuilder() + .with({ owners: [{ value: mockWallet.address }] }) + .build() + const result = getAvailableSigners(mockWallet, null, mockSafe, mockTx) + + expect(result).toEqual([]) + }) + + it('should return an empty array if tx is undefined', () => { + const nestedOwners = [mockWallet.address] + const mockSafe = safeInfoBuilder() + .with({ owners: [{ value: mockWallet.address }, { value: parentSafe }] }) + .build() + + const result = getAvailableSigners(mockWallet, nestedOwners, mockSafe, undefined) + + expect(result).toEqual([]) + }) + + it('should include wallet address if wallet is a direct owner and has not signed', () => { + const nestedOwners = [checksumAddress(faker.finance.ethereumAddress())] + const mockSafe = safeInfoBuilder() + .with({ owners: [{ value: mockWallet.address }, { value: parentSafe }] }) + .build() + + const result = getAvailableSigners(mockWallet, nestedOwners, mockSafe, mockTx) + + expect(result).toEqual([nestedOwners[0], mockWallet.address]) + }) + + it('should not include wallet address if wallet is a direct owner and has already signed', () => { + const nestedOwners = [checksumAddress(faker.finance.ethereumAddress())] + const mockSafe = safeInfoBuilder() + .with({ owners: [{ value: mockWallet.address }, { value: parentSafe }], threshold: 2 }) + .build() + const signedTx = { + ...mockTx, + signatures: new Map([[mockWallet.address, 'mockWallet signature']]), + } as unknown as SafeTransaction + + const result = getAvailableSigners(mockWallet, nestedOwners, mockSafe, signedTx) + + expect(result).toEqual([nestedOwners[0]]) + }) + + it('should return only signers who have not signed if threshold is not met', () => { + const nestedOwners = [ + checksumAddress(faker.finance.ethereumAddress()), + checksumAddress(faker.finance.ethereumAddress()), + ] + const mockSafe = safeInfoBuilder() + .with({ owners: [{ value: mockWallet.address }, { value: parentSafe }] }) + .build() + const signedTx = { + ...mockTx, + signatures: new Map([[nestedOwners[0], 'nestedOwners[0] signature']]), + } as unknown as SafeTransaction + + const result = getAvailableSigners(mockWallet, nestedOwners, mockSafe, signedTx) + + expect(result).toEqual([nestedOwners[1], mockWallet.address]) + }) + + it('should return nestedSafeOwners if wallet is not a direct owner', () => { + const nestedOwners = [ + checksumAddress(faker.finance.ethereumAddress()), + checksumAddress(faker.finance.ethereumAddress()), + ] + const mockSafe = safeInfoBuilder() + .with({ owners: [{ value: parentSafe }] }) + .build() + const result = getAvailableSigners(mockWallet, nestedOwners, mockSafe, mockTx) + + expect(result).toEqual([nestedOwners[0], nestedOwners[1]]) + }) + + it('should return nested signers if the transaction has met the threshold', () => { + const nestedOwners = [checksumAddress(faker.finance.ethereumAddress())] + const mockSafe = safeInfoBuilder() + .with({ owners: [{ value: mockWallet.address }, { value: parentSafe }], threshold: 2 }) + .build() + const fullySignedTx = { + ...mockTx, + signatures: new Map([ + [checksumAddress(mockWallet.address), 'mockWallet signature'], + [checksumAddress(nestedOwners[0]), 'nestedOwners[0] signature'], + ]), + } as unknown as SafeTransaction + + const result = getAvailableSigners(mockWallet, nestedOwners, mockSafe, fullySignedTx) + + expect(result).toEqual([nestedOwners[0]]) + }) + + it('should handle case insensitivity in addresses', () => { + const nonChecksummedMockWallet = { address: mockWallet.address.toLowerCase() } as ConnectedWallet + const nestedOwners = [faker.finance.ethereumAddress().toUpperCase(), faker.finance.ethereumAddress().toLowerCase()] + const mockSafe = safeInfoBuilder() + .with({ + owners: [{ value: mockWallet.address }, { value: parentSafe }], + }) + .build() + + const result = getAvailableSigners(nonChecksummedMockWallet, nestedOwners, mockSafe, mockTx) + + expect(result).toEqual([ + checksumAddress(nestedOwners[0]), + checksumAddress(nestedOwners[1]), + checksumAddress(nonChecksummedMockWallet.address), + ]) + }) +}) diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index fd9f04b9ca..eac09b1a51 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -29,7 +29,7 @@ export function assertChainInfo(chainInfo: ChainInfo | undefined): asserts chain return invariant(chainInfo, 'No chain config available') } -export function assertProvider(provider: Eip1193Provider | undefined): asserts provider { +export function assertProvider(provider: Eip1193Provider | undefined | null): asserts provider { return invariant(provider, 'Provider not found') } diff --git a/src/utils/nested-safe-wallet.ts b/src/utils/nested-safe-wallet.ts new file mode 100644 index 0000000000..efe73dadb1 --- /dev/null +++ b/src/utils/nested-safe-wallet.ts @@ -0,0 +1,164 @@ +import { type Eip1193Provider, getAddress, type JsonRpcProvider } from 'ethers' +import { SafeWalletProvider, type WalletSDK } from '@/services/safe-wallet-provider' +import { getTransactionDetails, type SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { type NextRouter } from 'next/router' +import { AppRoutes } from '@/config/routes' +import proposeTx from '@/services/tx/proposeTransaction' +import { isSmartContractWallet } from '@/utils/wallets' +import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' +import { initSafeSDK } from '@/hooks/coreSDK/safeCoreSDK' +import { logError } from '@/services/exceptions' +import ErrorCodes from '@/services/exceptions/ErrorCodes' +import { tryOffChainTxSigning } from '@/services/tx/tx-sender/sdk' +import type { TransactionResult } from '@safe-global/safe-core-sdk-types' + +export type NestedWallet = { + address: string + chainId: string + provider: Eip1193Provider | null + isSafe: true +} + +export const getNestedWallet = ( + actualWallet: ConnectedWallet, + safeInfo: SafeInfo, + web3ReadOnly: JsonRpcProvider, + router: NextRouter, +): NestedWallet => { + let requestId = 0 + const nestedSafeSdk: WalletSDK = { + getBySafeTxHash(safeTxHash) { + return getTransactionDetails(safeInfo.chainId, safeTxHash) + }, + async switchChain() { + return Promise.reject('Switching chains is not supported yet') + }, + getCreateCallTransaction() { + throw new Error('Unsupported method') + }, + + async signMessage(): Promise<{ signature: string }> { + return Promise.reject('signMessage is not supported yet') + }, + + async proxy(method, params) { + return web3ReadOnly?.send(method, params ?? []) + }, + + async send(params) { + const safeCoreSDK = await initSafeSDK({ + provider: web3ReadOnly, + chainId: safeInfo.chainId, + address: safeInfo.address.value, + version: safeInfo.version, + implementationVersionState: safeInfo.implementationVersionState, + implementation: safeInfo.implementation.value, + }) + + const connectedSDK = await safeCoreSDK?.connect({ provider: actualWallet.provider }) + + if (!connectedSDK) { + return Promise.reject('Could not initialize core sdk') + } + + const transactions = params.txs.map(({ to, value, data }: any) => { + return { + to: getAddress(to), + value: BigInt(value).toString(), + data, + operation: 0, + } + }) + + const safeTx = await connectedSDK.createTransaction({ + transactions, + onlyCalls: true, + }) + + const safeTxHash = await connectedSDK.getTransactionHash(safeTx) + + let result: TransactionResult | null = null + + try { + if (await isSmartContractWallet(safeInfo.chainId, actualWallet.address)) { + // With the unchecked signer, the contract call resolves once the tx + // has been submitted in the wallet not when it has been executed + + // First we propose so the backend will pick it up + await proposeTx(safeInfo.chainId, safeInfo.address.value, actualWallet.address, safeTx, safeTxHash) + result = await connectedSDK.approveTransactionHash(safeTxHash) + } else { + // Sign off-chain + if (safeInfo.threshold === 1) { + // Always propose the tx so the resulting link to the parentTx does not error out + await proposeTx(safeInfo.chainId, safeInfo.address.value, actualWallet.address, safeTx, safeTxHash) + + // Directly execute the tx + result = await connectedSDK.executeTransaction(safeTx) + } else { + const signedTx = await tryOffChainTxSigning(safeTx, safeInfo.version, connectedSDK) + await proposeTx(safeInfo.chainId, safeInfo.address.value, actualWallet.address, signedTx, safeTxHash) + } + } + } catch (err) { + logError(ErrorCodes._817, err) + throw err + } + + return { + safeTxHash, + txHash: result?.hash, + } + }, + + setSafeSettings() { + throw new Error('setSafeSettings is not supported yet') + }, + + showTxStatus(safeTxHash) { + router.push({ + pathname: AppRoutes.transactions.tx, + query: { + safe: router.query.safe, + id: safeTxHash, + }, + }) + }, + + async signTypedMessage() { + return Promise.reject('signTypedMessage is not supported yet') + }, + } + + const nestedSafeProvider = new SafeWalletProvider( + { + chainId: Number(safeInfo.chainId), + safeAddress: safeInfo.address.value, + }, + nestedSafeSdk, + ) + + return { + provider: { + async request(request) { + const result = await nestedSafeProvider.request(requestId++, request, { + url: '', + description: '', + iconUrl: '', + name: 'Nested Safe', + }) + + if ('result' in result) { + return result.result + } + + if ('error' in result) { + throw new Error(result.error.message) + } + }, + }, + address: safeInfo.address.value, + chainId: safeInfo.chainId, + isSafe: true, + } +} diff --git a/src/utils/signers.ts b/src/utils/signers.ts new file mode 100644 index 0000000000..a8babd9767 --- /dev/null +++ b/src/utils/signers.ts @@ -0,0 +1,32 @@ +import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' +import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' +import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { checksumAddress } from './addresses' + +export const getAvailableSigners = ( + wallet: ConnectedWallet | null | undefined, + nestedSafeOwners: string[] | null, + safe: SafeInfo, + tx: SafeTransaction | undefined, +) => { + if (!wallet || !nestedSafeOwners || !tx) { + return [] + } + const walletAddress = checksumAddress(wallet.address) + + const isDirectOwner = safe.owners.map((owner) => checksumAddress(owner.value)).includes(walletAddress) + const isFullySigned = tx.signatures.size >= safe.threshold + const availableSigners = nestedSafeOwners ? nestedSafeOwners.map(checksumAddress) : [] + + const signers = Array.from(tx.signatures.keys()).map(checksumAddress) + + if (isDirectOwner && !signers.includes(walletAddress)) { + availableSigners.push(walletAddress) + } + + if (!isFullySigned) { + // Filter signers that already signed + return availableSigners.filter((signer) => !signers.includes(signer)) + } + return availableSigners +} diff --git a/src/utils/transaction-guards.ts b/src/utils/transaction-guards.ts index 3ee3e88b72..f126ebf5b0 100644 --- a/src/utils/transaction-guards.ts +++ b/src/utils/transaction-guards.ts @@ -443,3 +443,7 @@ export const isExecTxInfo = (info: TransactionInfo): info is Custom => { } return false } + +export const isNestedConfirmationTxInfo = (info: TransactionInfo): boolean => { + return isCustomTxInfo(info) && (isOnChainConfirmationTxInfo(info) || isExecTxInfo(info)) +} From a3cd8e43245dd1cb9aff354fdb33d9b5208fc3f0 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:39:55 +0100 Subject: [PATCH 35/47] Chore: fix lodash imports (#4580) --- src/components/new-safe/create/steps/ReviewStep/index.tsx | 2 +- src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx | 2 +- src/components/tx/ApprovalEditor/Approvals.tsx | 2 +- src/components/tx/ApprovalEditor/EditableApprovalItem.tsx | 2 +- src/features/multichain/utils/utils.ts | 2 +- src/features/myAccounts/hooks/useAllSafesGrouped.ts | 2 +- src/pages/index.tsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index d5a3c41d6d..1e388ad0ce 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -43,7 +43,7 @@ import { useMemo, useState } from 'react' import ChainIndicator from '@/components/common/ChainIndicator' import NetworkWarning from '../../NetworkWarning' import useAllSafes from '@/features/myAccounts/hooks/useAllSafes' -import { uniq } from 'lodash' +import uniq from 'lodash/uniq' import { selectRpc } from '@/store/settingsSlice' import { AppRoutes } from '@/config/routes' import { type ReplayedSafeProps } from '@/store/slices' diff --git a/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx b/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx index 0e2666258d..594e7f05cc 100644 --- a/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx +++ b/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx @@ -5,7 +5,7 @@ import type { ApprovalInfo } from './hooks/useApprovalInfos' import { useMemo } from 'react' import EditableApprovalItem from './EditableApprovalItem' -import { groupBy } from 'lodash' +import groupBy from 'lodash/groupBy' import { SpenderField } from './SpenderField' export type ApprovalEditorFormData = { diff --git a/src/components/tx/ApprovalEditor/Approvals.tsx b/src/components/tx/ApprovalEditor/Approvals.tsx index 1f4fea25d6..1d10bd7cc2 100644 --- a/src/components/tx/ApprovalEditor/Approvals.tsx +++ b/src/components/tx/ApprovalEditor/Approvals.tsx @@ -3,7 +3,7 @@ import { List, ListItem, Stack } from '@mui/material' import { type ApprovalInfo } from '@/components/tx/ApprovalEditor/hooks/useApprovalInfos' import css from './styles.module.css' import ApprovalItem from '@/components/tx/ApprovalEditor/ApprovalItem' -import { groupBy } from 'lodash' +import groupBy from 'lodash/groupBy' import { useMemo } from 'react' import { SpenderField } from './SpenderField' diff --git a/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx b/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx index f9492c44fa..6e63db2848 100644 --- a/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx +++ b/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx @@ -6,7 +6,7 @@ import { ApprovalValueField } from './ApprovalValueField' import Track from '@/components/common/Track' import { MODALS_EVENTS } from '@/services/analytics' import { useFormContext } from 'react-hook-form' -import { get } from 'lodash' +import get from 'lodash/get' import { EditOutlined } from '@mui/icons-material' import TokenIcon from '@/components/common/TokenIcon' import { useState } from 'react' diff --git a/src/features/multichain/utils/utils.ts b/src/features/multichain/utils/utils.ts index 05384ee149..a9d549d50b 100644 --- a/src/features/multichain/utils/utils.ts +++ b/src/features/multichain/utils/utils.ts @@ -7,7 +7,7 @@ import { Safe_proxy_factory__factory } from '@/types/contracts' import { keccak256, ethers, solidityPacked, getCreate2Address, type Provider } from 'ethers' import { extractCounterfactualSafeSetup } from '@/features/counterfactual/utils' import { encodeSafeSetupCall } from '@/components/new-safe/create/logic' -import { memoize } from 'lodash' +import memoize from 'lodash/memoize' import { FEATURES, hasFeature } from '@/utils/chains' import { type SafeItem } from '@/features/myAccounts/hooks/useAllSafes' import { type MultiChainSafeItem } from '@/features/myAccounts/hooks/useAllSafesGrouped' diff --git a/src/features/myAccounts/hooks/useAllSafesGrouped.ts b/src/features/myAccounts/hooks/useAllSafesGrouped.ts index 23f3a4663f..e6620532d5 100644 --- a/src/features/myAccounts/hooks/useAllSafesGrouped.ts +++ b/src/features/myAccounts/hooks/useAllSafesGrouped.ts @@ -1,4 +1,4 @@ -import { groupBy } from 'lodash' +import groupBy from 'lodash/groupBy' import useAllSafes, { type SafeItem, type SafeItems } from './useAllSafes' import { useMemo } from 'react' import { sameAddress } from '@/utils/addresses' diff --git a/src/pages/index.tsx b/src/pages/index.tsx index d861eead32..b20b74a1eb 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react' import type { NextPage } from 'next' import { useRouter } from 'next/router' import { AppRoutes } from '@/config/routes' -import { isEmpty } from 'lodash' +import isEmpty from 'lodash/isEmpty' import local from '@/services/local-storage/local' import { addedSafesSlice, type AddedSafesState } from '@/store/addedSafesSlice' From d5517fe5d8dc8ec5ae8b5d2ff4e468a45fcc4e94 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:13:35 +0100 Subject: [PATCH 36/47] Fix: replace "signer(s)" with "N signers" or "1 signer" (#4550) * Fix: replace "signer(s)" with "N signers" or "1 signer" * maybePlural * Use for remaining relays --- cypress/e2e/pages/owners.pages.js | 6 ++++-- cypress/support/constants.js | 2 +- .../balances/HiddenTokenButton/index.tsx | 3 ++- .../create/steps/OwnerPolicyStep/index.tsx | 5 ++++- .../new-safe/load/steps/SafeReviewStep/index.tsx | 3 ++- src/components/nfts/NftSendForm/index.tsx | 3 ++- .../PushNotifications/GlobalPushNotifications.tsx | 3 ++- src/components/transactions/TxInfo/index.tsx | 4 ++-- src/components/transactions/Warning/index.tsx | 3 ++- src/components/tx-flow/common/OwnerList/index.tsx | 3 ++- .../tx-flow/flows/AddOwner/ChooseOwner.tsx | 5 ++++- .../flows/ChangeThreshold/ChooseThreshold.tsx | 5 ++++- .../tx-flow/flows/ConfirmBatch/index.tsx | 4 ++-- .../RecoverAccount/RecoverAccountFlowSetup.tsx | 5 ++++- .../tx-flow/flows/RemoveOwner/SetThreshold.tsx | 5 ++++- src/components/tx/RemainingRelays/index.tsx | 4 +++- .../ChangeThreshold/ChangeThreshold.test.tsx | 2 +- .../__snapshots__/ChangeThreshold.test.tsx.snap | 5 +++-- .../confirmation-views/ChangeThreshold/index.tsx | 6 +++++- src/features/myAccounts/index.tsx | 3 ++- .../RecoveryCards/RecoveryProposalCard.tsx | 5 ++--- .../components/StakingConfirmationTx/Deposit.tsx | 4 ++-- .../stake/components/StakingTxExitInfo/index.tsx | 3 ++- src/services/tx/tx-sender/sdk.ts | 7 ++++--- src/utils/__tests__/formatters.test.ts | 15 +++++++++++++++ src/utils/date.ts | 7 ++++--- src/utils/formatters.ts | 5 +++++ 27 files changed, 89 insertions(+), 36 deletions(-) diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index 8d653bc327..1422cedef3 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -95,7 +95,7 @@ export function getThresholdOptions() { } export function verifyThresholdLimit(startValue, endValue) { - cy.get('p').contains(`out of ${endValue} signer(s)`) + cy.get('p').contains(`out of ${endValue} signer${endValue > 1 ? 's' : ''}`) clickOnThresholdDropdown() getThresholdOptions().eq(0).should('have.text', startValue).click() } @@ -248,7 +248,9 @@ export function verifyConfirmTransactionWindowDisplayed() { export function verifyThreshold(startValue, endValue) { main.verifyInputValue(thresholdInput, startValue) - cy.get('p').contains(`out of ${endValue} signer(s)`).should('be.visible') + cy.get('p') + .contains(`out of ${endValue} signer${endValue > 1 ? 's' : ''}`) + .should('be.visible') cy.get(thresholdInput).parent().click() cy.get(thresholdList).contains(endValue).should('be.visible') cy.get(thresholdList).find('li').should('have.length', endValue) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 4f3b928082..e48a822c4c 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -102,7 +102,7 @@ export const safeContractVersions = { } export const commonThresholds = { - oneOfOne: '1 out of 1 signer(s)', + oneOfOne: '1 out of 1 signer', } export const TXActionNames = { resetAllowance: 'resetAllowance', diff --git a/src/components/balances/HiddenTokenButton/index.tsx b/src/components/balances/HiddenTokenButton/index.tsx index e0a30e3514..e9a4ec09f8 100644 --- a/src/components/balances/HiddenTokenButton/index.tsx +++ b/src/components/balances/HiddenTokenButton/index.tsx @@ -7,6 +7,7 @@ import VisibilityOutlined from '@mui/icons-material/VisibilityOutlined' import Track from '@/components/common/Track' import css from './styles.module.css' +import { maybePlural } from '@/utils/formatters' const HiddenTokenButton = ({ toggleShowHiddenAssets, @@ -45,7 +46,7 @@ const HiddenTokenButton = ({ > {hiddenAssetCount === 0 ? 'Hide tokens' - : `${hiddenAssetCount} hidden token${hiddenAssetCount > 1 ? 's' : ''}`}{' '} + : `${hiddenAssetCount} hidden token${maybePlural(hiddenAssetCount)}`}{' '} diff --git a/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx b/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx index ce054a6f73..97754f3e80 100644 --- a/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx +++ b/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx @@ -16,6 +16,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack' import layoutCss from '@/components/new-safe/create/styles.module.css' import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' import OwnerRow from '@/components/new-safe/OwnerRow' +import { maybePlural } from '@/utils/formatters' enum OwnerPolicyStepFields { owners = 'owners', @@ -174,7 +175,9 @@ const OwnerPolicyStep = ({ /> - out of {ownerFields.length} signer(s) + + out of {ownerFields.length} signer{maybePlural(ownerFields)} + diff --git a/src/components/new-safe/load/steps/SafeReviewStep/index.tsx b/src/components/new-safe/load/steps/SafeReviewStep/index.tsx index e65efc693d..d613360304 100644 --- a/src/components/new-safe/load/steps/SafeReviewStep/index.tsx +++ b/src/components/new-safe/load/steps/SafeReviewStep/index.tsx @@ -17,6 +17,7 @@ import { LOAD_SAFE_EVENTS, OPEN_SAFE_LABELS, OVERVIEW_EVENTS, trackEvent } from import { AppRoutes } from '@/config/routes' import ReviewRow from '@/components/new-safe/ReviewRow' import { upsertAddressBookEntries } from '@/store/addressBookSlice' +import { maybePlural } from '@/utils/formatters' const SafeReviewStep = ({ data, onBack }: StepRenderProps) => { const chain = useCurrentChain() @@ -118,7 +119,7 @@ const SafeReviewStep = ({ data, onBack }: StepRenderProps) => name="Threshold" value={ - {data.threshold} out of {data.owners.length} signer(s) + {data.threshold} out of {data.owners.length} signer{maybePlural(data.owners)} } /> diff --git a/src/components/nfts/NftSendForm/index.tsx b/src/components/nfts/NftSendForm/index.tsx index 92948fd0b5..819f188493 100644 --- a/src/components/nfts/NftSendForm/index.tsx +++ b/src/components/nfts/NftSendForm/index.tsx @@ -4,13 +4,14 @@ import ArrowIcon from '@/public/images/common/arrow-nw.svg' import type { SafeCollectibleResponse } from '@safe-global/safe-gateway-typescript-sdk' import { Sticky } from '@/components/common/Sticky' import CheckWallet from '@/components/common/CheckWallet' +import { maybePlural } from '@/utils/formatters' type NftSendFormProps = { selectedNfts: SafeCollectibleResponse[] } const NftSendForm = ({ selectedNfts }: NftSendFormProps): ReactElement => { - const nftsText = `NFT${selectedNfts.length === 1 ? '' : 's'}` + const nftsText = `NFT${maybePlural(selectedNfts)}` const noSelected = selectedNfts.length === 0 return ( diff --git a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx index ecd23bf6cd..eb44de9c74 100644 --- a/src/components/settings/PushNotifications/GlobalPushNotifications.tsx +++ b/src/components/settings/PushNotifications/GlobalPushNotifications.tsx @@ -39,6 +39,7 @@ import css from './styles.module.css' import useAllOwnedSafes from '@/features/myAccounts/hooks/useAllOwnedSafes' import useWallet from '@/hooks/wallets/useWallet' import { selectAllAddedSafes, type AddedSafesState } from '@/store/addedSafesSlice' +import { maybePlural } from '@/utils/formatters' // UI logic @@ -391,7 +392,7 @@ export const GlobalPushNotifications = (): ReactElement | null => { {totalSignaturesRequired > 0 && ( We'll ask you to verify ownership of each Safe Account with your signature per chain{' '} - {totalSignaturesRequired} time{totalSignaturesRequired > 1 ? 's' : ''} + {totalSignaturesRequired} time{maybePlural(totalSignaturesRequired)} )} diff --git a/src/components/transactions/TxInfo/index.tsx b/src/components/transactions/TxInfo/index.tsx index c5ab73339f..3d4a9c4f4e 100644 --- a/src/components/transactions/TxInfo/index.tsx +++ b/src/components/transactions/TxInfo/index.tsx @@ -24,7 +24,7 @@ import { isStakingTxExitInfo, isStakingTxWithdrawInfo, } from '@/utils/transaction-guards' -import { ellipsis, shortenAddress } from '@/utils/formatters' +import { ellipsis, maybePlural, shortenAddress } from '@/utils/formatters' import { useCurrentChain } from '@/hooks/useChains' import { SwapTx } from '@/features/swap/components/SwapTxInfo/SwapTx' import StakingTxExitInfo from '@/features/stake/components/StakingTxExitInfo' @@ -104,7 +104,7 @@ const CreationTx = ({ info }: { info: Creation }): ReactElement => { const MultiSendTx = ({ info }: { info: MultiSend }): ReactElement => { return ( - {info.actionCount} {`action${info.actionCount > 1 ? 's' : ''}`} + {info.actionCount} {`action${maybePlural(info.actionCount)}`} ) } diff --git a/src/components/transactions/Warning/index.tsx b/src/components/transactions/Warning/index.tsx index 049ab9842a..16d199d3b3 100644 --- a/src/components/transactions/Warning/index.tsx +++ b/src/components/transactions/Warning/index.tsx @@ -6,6 +6,7 @@ import InfoOutlinedIcon from '@/public/images/notifications/info.svg' import css from './styles.module.css' import ExternalLink from '@/components/common/ExternalLink' import { HelpCenterArticle } from '@/config/constants' +import { maybePlural } from '@/utils/formatters' const Warning = ({ datatestid, @@ -55,7 +56,7 @@ export const DelegateCallWarning = ({ showWarning }: { showWarning: boolean }): } export const ApprovalWarning = ({ approvalTxCount }: { approvalTxCount: number }): ReactElement => ( - 1 ? 's' : ''}`} /> + ) export const ThresholdWarning = (): ReactElement => ( diff --git a/src/components/tx-flow/common/OwnerList/index.tsx b/src/components/tx-flow/common/OwnerList/index.tsx index 5dfd23f363..560c72671d 100644 --- a/src/components/tx-flow/common/OwnerList/index.tsx +++ b/src/components/tx-flow/common/OwnerList/index.tsx @@ -7,6 +7,7 @@ import PlusIcon from '@/public/images/common/plus.svg' import EthHashInfo from '@/components/common/EthHashInfo' import css from './styles.module.css' +import { maybePlural } from '@/utils/formatters' export function OwnerList({ title, @@ -27,7 +28,7 @@ export function OwnerList({ }} > - {title ?? `New signer${owners.length > 1 ? 's' : ''}`} + {title ?? `New signer${maybePlural(owners)}`}
{owners.map((newOwner) => ( @@ -203,7 +204,9 @@ export const ChooseOwner = ({ /> - out of {newNumberOfOwners} signer(s) + + out of {newNumberOfOwners} signer{maybePlural(newNumberOfOwners)} + diff --git a/src/components/tx-flow/flows/ChangeThreshold/ChooseThreshold.tsx b/src/components/tx-flow/flows/ChangeThreshold/ChooseThreshold.tsx index 3daf2e9d25..e54990b9b4 100644 --- a/src/components/tx-flow/flows/ChangeThreshold/ChooseThreshold.tsx +++ b/src/components/tx-flow/flows/ChangeThreshold/ChooseThreshold.tsx @@ -21,6 +21,7 @@ import InfoIcon from '@/public/images/notifications/info.svg' import { TOOLTIP_TITLES } from '@/components/tx-flow/common/constants' import commonCss from '@/components/tx-flow/common/styles.module.css' +import { maybePlural } from '@/utils/formatters' export const ChooseThreshold = ({ params, @@ -104,7 +105,9 @@ export const ChooseThreshold = ({ - out of {safe.owners.length} signer(s) + + out of {safe.owners.length} signer{maybePlural(safe.owners)} + {isError ? ( diff --git a/src/components/tx-flow/flows/ConfirmBatch/index.tsx b/src/components/tx-flow/flows/ConfirmBatch/index.tsx index a0159b21e8..dcb2476e55 100644 --- a/src/components/tx-flow/flows/ConfirmBatch/index.tsx +++ b/src/components/tx-flow/flows/ConfirmBatch/index.tsx @@ -8,6 +8,7 @@ import { OperationType } from '@safe-global/safe-core-sdk-types' import TxLayout from '../../common/TxLayout' import BatchIcon from '@/public/images/common/batch.svg' import { useDraftBatch } from '@/hooks/useDraftBatch' +import { maybePlural } from '@/utils/formatters' type ConfirmBatchProps = { onSubmit: () => void @@ -36,11 +37,10 @@ const ConfirmBatch = ({ onSubmit }: ConfirmBatchProps): ReactElement => { const ConfirmBatchFlow = (props: ConfirmBatchProps) => { const { length } = useDraftBatch() - return ( 1 ? 's' : ''}`} + subtitle={`This batch contains ${length} transaction${maybePlural(length)}`} icon={BatchIcon} step={0} isBatch diff --git a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowSetup.tsx b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowSetup.tsx index 7514b3f512..bc6865d513 100644 --- a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowSetup.tsx +++ b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowSetup.tsx @@ -28,6 +28,7 @@ import type { RecoverAccountFlowProps } from '.' import type { AddressEx } from '@safe-global/safe-gateway-typescript-sdk' import commonCss from '@/components/tx-flow/common/styles.module.css' +import { maybePlural } from '@/utils/formatters' export function _isSameSetup({ oldOwners, @@ -225,7 +226,9 @@ export function RecoverAccountFlowSetup({ - out of {fields.length} signer(s) + + out of {fields.length} signer{maybePlural(fields)} + diff --git a/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx b/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx index 389d495a9e..18de9819f6 100644 --- a/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx +++ b/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx @@ -11,6 +11,7 @@ import { TOOLTIP_TITLES } from '@/components/tx-flow/common/constants' import type { RemoveOwnerFlowProps } from '.' import commonCss from '@/components/tx-flow/common/styles.module.css' +import { maybePlural } from '@/utils/formatters' export const SetThreshold = ({ params, @@ -101,7 +102,9 @@ export const SetThreshold = ({ - out of {newNumberOfOwners} signer(s) + + out of {newNumberOfOwners} signer{maybePlural(newNumberOfOwners)} + diff --git a/src/components/tx/RemainingRelays/index.tsx b/src/components/tx/RemainingRelays/index.tsx index 117accdeb4..3a84ccaea7 100644 --- a/src/components/tx/RemainingRelays/index.tsx +++ b/src/components/tx/RemainingRelays/index.tsx @@ -3,10 +3,12 @@ import InfoIcon from '@/public/images/notifications/info.svg' import { MAX_DAY_RELAYS } from '@/hooks/useRemainingRelays' import css from '../BalanceInfo/styles.module.css' import type { RelayCountResponse } from '@safe-global/safe-gateway-typescript-sdk' +import { maybePlural } from '@/utils/formatters' const RemainingRelays = ({ relays, tooltip }: { relays?: RelayCountResponse; tooltip?: string }) => { if (!tooltip) { - tooltip = `${relays?.limit ?? MAX_DAY_RELAYS} transactions per day for free` + const limit = relays?.limit ?? MAX_DAY_RELAYS + tooltip = `${limit} transaction${maybePlural(limit)} per day for free` } return ( diff --git a/src/components/tx/confirmation-views/ChangeThreshold/ChangeThreshold.test.tsx b/src/components/tx/confirmation-views/ChangeThreshold/ChangeThreshold.test.tsx index 2ec8500f3a..e4f7a4c6d4 100644 --- a/src/components/tx/confirmation-views/ChangeThreshold/ChangeThreshold.test.tsx +++ b/src/components/tx/confirmation-views/ChangeThreshold/ChangeThreshold.test.tsx @@ -26,6 +26,6 @@ describe('ChangeThreshold', () => { ) expect(container).toMatchSnapshot() - expect(getByLabelText('threshold')).toHaveTextContent('3 out of 1 signer(s)') + expect(getByLabelText('threshold')).toHaveTextContent('3 out of 1 signer') }) }) diff --git a/src/components/tx/confirmation-views/ChangeThreshold/__snapshots__/ChangeThreshold.test.tsx.snap b/src/components/tx/confirmation-views/ChangeThreshold/__snapshots__/ChangeThreshold.test.tsx.snap index 9c91c0378c..86ded94068 100644 --- a/src/components/tx/confirmation-views/ChangeThreshold/__snapshots__/ChangeThreshold.test.tsx.snap +++ b/src/components/tx/confirmation-views/ChangeThreshold/__snapshots__/ChangeThreshold.test.tsx.snap @@ -15,10 +15,11 @@ exports[`ChangeThreshold should display the ChangeThreshold component with the n 3 - out of + out of + 1 - signer(s) + signer

diff --git a/src/components/tx/confirmation-views/ChangeThreshold/index.tsx b/src/components/tx/confirmation-views/ChangeThreshold/index.tsx index 0b52e50e31..0841eefdc7 100644 --- a/src/components/tx/confirmation-views/ChangeThreshold/index.tsx +++ b/src/components/tx/confirmation-views/ChangeThreshold/index.tsx @@ -7,6 +7,7 @@ import { ChangeThresholdReviewContext } from '@/components/tx-flow/flows/ChangeT import { ChangeSignerSetupWarning } from '@/features/multichain/components/SignerSetupWarning/ChangeSignerSetupWarning' import { type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import { isChangeThresholdView } from '../utils' +import { maybePlural } from '@/utils/formatters' interface ChangeThresholdProps { txDetails?: TransactionDetails @@ -32,7 +33,10 @@ function ChangeThreshold({ txDetails }: ChangeThresholdProps) {
- {newThreshold || threshold} out of {safe.owners.length} signer(s) + {newThreshold || threshold} out of{' '} + + {safe.owners.length} signer{maybePlural(safe.owners)} +
{/* Search results */} - Found {filteredSafes.length} result{filteredSafes.length === 1 ? '' : 's'} + Found {filteredSafes.length} result{maybePlural(filteredSafes)} diff --git a/src/features/recovery/components/RecoveryCards/RecoveryProposalCard.tsx b/src/features/recovery/components/RecoveryCards/RecoveryProposalCard.tsx index 5953417988..2f09d78b3a 100644 --- a/src/features/recovery/components/RecoveryCards/RecoveryProposalCard.tsx +++ b/src/features/recovery/components/RecoveryCards/RecoveryProposalCard.tsx @@ -16,6 +16,7 @@ import type { TxModalContextType } from '@/components/tx-flow' import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' import css from './styles.module.css' +import { maybePlural } from '@/utils/formatters' type Props = | { @@ -52,9 +53,7 @@ export function InternalRecoveryProposalCard({ /> ) const title = 'Recover this Account' - const desc = `The connected wallet was chosen as a trusted Recoverer. You can help the owner${ - safe.owners.length > 1 ? 's' : '' - } regain access by resetting the Account setup.` + const desc = `The connected wallet was chosen as a trusted Recoverer. You can help the owner${maybePlural(safe.owners)} regain access by resetting the Account setup.` const link = ( diff --git a/src/features/stake/components/StakingConfirmationTx/Deposit.tsx b/src/features/stake/components/StakingConfirmationTx/Deposit.tsx index 4de75c92b4..da3859f2b8 100644 --- a/src/features/stake/components/StakingConfirmationTx/Deposit.tsx +++ b/src/features/stake/components/StakingConfirmationTx/Deposit.tsx @@ -3,7 +3,7 @@ import FieldsGrid from '@/components/tx/FieldsGrid' import type { StakingTxDepositInfo } from '@safe-global/safe-gateway-typescript-sdk' import { type NativeStakingDepositConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' import ConfirmationOrderHeader from '@/components/tx/ConfirmationOrder/ConfirmationOrderHeader' -import { formatDurationFromMilliseconds, formatVisualAmount } from '@/utils/formatters' +import { formatDurationFromMilliseconds, formatVisualAmount, maybePlural } from '@/utils/formatters' import { formatCurrency } from '@/utils/formatNumber' import StakingStatus from '@/features/stake/components/StakingStatus' import { InfoTooltip } from '@/features/stake/components/InfoTooltip' @@ -86,7 +86,7 @@ const StakingConfirmationTxDeposit = ({ order, isTxDetails }: StakingOrderConfir borderRadius: 1, }} > - {order.numValidators} Ethereum validator{order.numValidators === 1 ? '' : 's'} + {order.numValidators} Ethereum validator{maybePlural(order.numValidators)}
) : ( diff --git a/src/features/stake/components/StakingTxExitInfo/index.tsx b/src/features/stake/components/StakingTxExitInfo/index.tsx index 989bb04d34..446c8abf73 100644 --- a/src/features/stake/components/StakingTxExitInfo/index.tsx +++ b/src/features/stake/components/StakingTxExitInfo/index.tsx @@ -1,9 +1,10 @@ +import { maybePlural } from '@/utils/formatters' import type { StakingTxExitInfo } from '@safe-global/safe-gateway-typescript-sdk' const StakingTxExitInfo = ({ info }: { info: StakingTxExitInfo }) => { return ( <> - {info.numValidators} Validator{info.numValidators > 1 ? 's' : ''} + {info.numValidators} Validator{maybePlural(info.numValidators)} ) } diff --git a/src/services/tx/tx-sender/sdk.ts b/src/services/tx/tx-sender/sdk.ts index ae00cebeb1..25253a134c 100644 --- a/src/services/tx/tx-sender/sdk.ts +++ b/src/services/tx/tx-sender/sdk.ts @@ -20,6 +20,7 @@ import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' import { asError } from '@/services/exceptions/utils' import { UncheckedJsonRpcSigner } from '@/utils/providers/UncheckedJsonRpcSigner' import get from 'lodash/get' +import { maybePlural } from '@/utils/formatters' export const getAndValidateSafeSDK = (): Safe => { const safeSDK = getSafeSDK() @@ -210,9 +211,9 @@ export const prepareTxExecution = async (safeTransaction: SafeTransaction, provi if (threshold > signedSafeTransaction.signatures.size) { const signaturesMissing = threshold - signedSafeTransaction.signatures.size throw new Error( - `There ${signaturesMissing > 1 ? 'are' : 'is'} ${signaturesMissing} signature${ - signaturesMissing > 1 ? 's' : '' - } missing`, + `There ${signaturesMissing > 1 ? 'are' : 'is'} ${signaturesMissing} signature${maybePlural( + signaturesMissing, + )} missing`, ) } diff --git a/src/utils/__tests__/formatters.test.ts b/src/utils/__tests__/formatters.test.ts index 4fc94b8a06..537fc985c8 100644 --- a/src/utils/__tests__/formatters.test.ts +++ b/src/utils/__tests__/formatters.test.ts @@ -101,4 +101,19 @@ describe('formatters', () => { expect(formatters.formatVisualAmount('1', 18, 18)).toEqual('0.000000000000000001') }) }) + + describe('maybePlural', () => { + const { maybePlural } = formatters + it('should add an "s" for more than 1', () => { + expect(maybePlural(2)).toEqual('s') + expect(maybePlural(10)).toEqual('s') + expect(maybePlural(0)).toEqual('') + expect(maybePlural(1)).toEqual('') + }) + + it('should work for arrays too', () => { + expect(maybePlural(['1', '2'])).toEqual('s') + expect(maybePlural(['1'])).toEqual('') + }) + }) }) diff --git a/src/utils/date.ts b/src/utils/date.ts index e17522f8d9..eb52304cc1 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -1,4 +1,5 @@ import { format, formatDistanceToNow, formatRelative } from 'date-fns' +import { maybePlural } from './formatters' export const currentMinutes = (): number => Math.floor(Date.now() / (1000 * 60)) @@ -40,14 +41,14 @@ export function getPeriod(seconds: number): string | undefined { const { days, hours, minutes } = getCountdown(seconds) if (days > 0) { - return `${days} day${days === 1 ? '' : 's'}` + return `${days} day${maybePlural(days)}` } if (hours > 0) { - return `${hours} hour${hours === 1 ? '' : 's'}` + return `${hours} hour${maybePlural(hours)}` } if (minutes > 0) { - return `${minutes} minute${minutes === 1 ? '' : 's'}` + return `${minutes} minute${maybePlural(minutes)}` } } diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index af5c88079c..065fa0c6af 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -103,3 +103,8 @@ export const formatDurationFromMilliseconds = ( const duration = intervalToDuration({ start: 0, end: seconds }) return formatDuration(duration, { format }) } + +export const maybePlural = (quantity: number | unknown[]) => { + quantity = Array.isArray(quantity) ? quantity.length : quantity + return quantity > 1 ? 's' : '' +} From b4c7d99df8a613960ba76afc6566a4a8b640cea6 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:28:16 +0100 Subject: [PATCH 37/47] Tests: Add using of mock safe address (#4581) --- cypress/e2e/prodhealthcheck/nfts.cy.js | 6 +++--- cypress/e2e/prodhealthcheck/spending_limits.cy.js | 5 +++-- cypress/e2e/regression/add_owner.cy.js | 6 ++---- cypress/e2e/regression/batch_tx.cy.js | 7 ++++--- cypress/e2e/regression/create_safe_simple.cy.js | 3 ++- cypress/e2e/regression/load_safe_2.cy.js | 5 +++-- cypress/e2e/regression/multichain_create_safe.cy.js | 12 +++--------- .../regression/multichain_create_safe_flow.cy.js | 10 ++-------- cypress/e2e/regression/multichain_network.cy.js | 3 ++- .../e2e/regression/multichain_networkswitch.cy.js | 3 ++- cypress/e2e/regression/multichain_setup.cy.js | 3 ++- cypress/e2e/regression/nfts.cy.js | 8 ++++---- cypress/e2e/regression/proposers.cy.js | 6 ++---- cypress/e2e/regression/proposers_2.cy.js | 5 +++-- cypress/e2e/regression/recovery.cy.js | 7 ++++--- cypress/e2e/regression/replace_owner.cy.js | 8 ++++---- cypress/e2e/regression/spending_limits.cy.js | 5 +++-- cypress/e2e/regression/twaps_history.cy.js | 9 +++------ cypress/e2e/safe-apps/drain_account.spec.cy.js | 13 ++++++------- cypress/e2e/safe-apps/tx-builder.2spec.cy.js | 4 ++-- cypress/e2e/safe-apps/tx-builder.spec.cy.js | 1 + cypress/support/utils/ethers.js | 5 +++++ 22 files changed, 65 insertions(+), 69 deletions(-) create mode 100644 cypress/support/utils/ethers.js diff --git a/cypress/e2e/prodhealthcheck/nfts.cy.js b/cypress/e2e/prodhealthcheck/nfts.cy.js index 9cd499d32f..4dd83652e4 100644 --- a/cypress/e2e/prodhealthcheck/nfts.cy.js +++ b/cypress/e2e/prodhealthcheck/nfts.cy.js @@ -3,6 +3,7 @@ import * as nfts from '../pages/nfts.pages' import * as createTx from '../pages/create_tx.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' +import { getMockAddress } from '../../support/utils/ethers.js' const multipleNFT = ['multiSend'] const multipleNFTAction = 'safeTransferFrom' @@ -32,14 +33,13 @@ describe('[PROD] NFTs tests', () => { nfts.waitForNftItems(2) }) - // TODO: Add Sign action it('Verify multipls NFTs can be selected and reviewed', () => { nfts.verifyInitialNFTData() nfts.selectNFTs(3) nfts.deselectNFTs([2], 3) nfts.sendNFT() nfts.verifyNFTModalData() - nfts.typeRecipientAddress(staticSafes.SEP_STATIC_SAFE_1) + nfts.typeRecipientAddress(getMockAddress()) nfts.clikOnNextBtn() nfts.verifyReviewModalData(2) }) @@ -48,7 +48,7 @@ describe('[PROD] NFTs tests', () => { nfts.verifyInitialNFTData() nfts.selectNFTs(2) nfts.sendNFT() - nfts.typeRecipientAddress(staticSafes.SEP_STATIC_SAFE_1) + nfts.typeRecipientAddress(getMockAddress()) nfts.clikOnNextBtn() nfts.verifyTxDetails(multipleNFT) nfts.verifyCountOfActions(2) diff --git a/cypress/e2e/prodhealthcheck/spending_limits.cy.js b/cypress/e2e/prodhealthcheck/spending_limits.cy.js index 9b57297de5..36ccd81dbd 100644 --- a/cypress/e2e/prodhealthcheck/spending_limits.cy.js +++ b/cypress/e2e/prodhealthcheck/spending_limits.cy.js @@ -4,6 +4,7 @@ import * as navigation from '../pages/navigation.page' import * as tx from '../pages/create_tx.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' +import { getMockAddress } from '../../support/utils/ethers.js' let staticSafes = [] const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) @@ -24,12 +25,12 @@ describe('[PROD] Spending limits tests', () => { //Assume that default reset time is set to One time wallet.connectSigner(signer) spendinglimit.clickOnNewSpendingLimitBtn() - spendinglimit.enterBeneficiaryAddress(staticSafes.SEP_STATIC_SAFE_6) + spendinglimit.enterBeneficiaryAddress(getMockAddress()) spendinglimit.enterSpendingLimitAmount(0.1) spendinglimit.clickOnNextBtn() spendinglimit.checkReviewData( tokenAmount, - staticSafes.SEP_STATIC_SAFE_6, + getMockAddress(), spendinglimit.timePeriodOptions.oneTime.split(' ').join('-'), ) }) diff --git a/cypress/e2e/regression/add_owner.cy.js b/cypress/e2e/regression/add_owner.cy.js index 13a60b3531..acf9296f6b 100644 --- a/cypress/e2e/regression/add_owner.cy.js +++ b/cypress/e2e/regression/add_owner.cy.js @@ -4,9 +4,7 @@ import * as owner from '../pages/owners.pages' import * as addressBook from '../pages/address_book.page' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' -import * as createTx from '../pages/create_tx.pages.js' -import * as navigation from '../pages/navigation.page' -import { getEvents, events, checkDataLayerEvents } from '../../support/utils/gtag.js' +import { getMockAddress } from '../../support/utils/ethers.js' let staticSafes = [] const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) @@ -58,7 +56,7 @@ describe('Add Owners tests', () => { it('Verify that Name field not mandatory', () => { wallet.connectSigner(signer) owner.openAddOwnerWindow() - owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) + owner.typeOwnerAddress(getMockAddress()) owner.clickOnNextBtn() owner.verifyConfirmTransactionWindowDisplayed() }) diff --git a/cypress/e2e/regression/batch_tx.cy.js b/cypress/e2e/regression/batch_tx.cy.js index 6c705b9bba..9c2748e336 100644 --- a/cypress/e2e/regression/batch_tx.cy.js +++ b/cypress/e2e/regression/batch_tx.cy.js @@ -6,6 +6,7 @@ import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' import * as ls from '../../support/localstorage_data.js' import * as navigation from '../pages/navigation.page.js' +import { getMockAddress } from '../../support/utils/ethers.js' const currentNonce = 3 const funds_first_tx = '0.001' @@ -29,13 +30,13 @@ describe('Batch transaction tests', { defaultCommandTimeout: 30000 }, () => { it('Verify the Add batch button is present in a transaction form', () => { //The "true" is to validate that the add to batch button is not visible if "Yes, execute" is selected - batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) + batch.addNewTransactionToBatch(getMockAddress(), currentNonce, funds_first_tx) }) it('Verify a second transaction can be added to the batch', () => { - batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) + batch.addNewTransactionToBatch(getMockAddress(), currentNonce, funds_first_tx) cy.wait(1000) - batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) + batch.addNewTransactionToBatch(getMockAddress(), currentNonce, funds_first_tx) batch.verifyBatchIconCount(2) batch.clickOnBatchCounter() batch.verifyAmountTransactionsInBatch(2) diff --git a/cypress/e2e/regression/create_safe_simple.cy.js b/cypress/e2e/regression/create_safe_simple.cy.js index 5aad0f0d26..e38a1520c2 100644 --- a/cypress/e2e/regression/create_safe_simple.cy.js +++ b/cypress/e2e/regression/create_safe_simple.cy.js @@ -4,6 +4,7 @@ import * as createwallet from '../pages/create_wallet.pages' import * as owner from '../pages/owners.pages' import * as ls from '../../support/localstorage_data.js' import * as wallet from '../../support/utils/wallet.js' +import { getMockAddress } from '../../support/utils/ethers.js' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY @@ -107,7 +108,7 @@ describe('Safe creation tests', () => { createwallet.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS, 1) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.ownerAdded) - createwallet.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS.toUpperCase(), 1) + createwallet.typeOwnerAddress(getMockAddress().replace('A', 'a'), 1) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) createwallet.typeOwnerAddress(constants.ENS_TEST_SEPOLIA_INVALID, 1) diff --git a/cypress/e2e/regression/load_safe_2.cy.js b/cypress/e2e/regression/load_safe_2.cy.js index e5750ec3d6..e49afa6d1c 100644 --- a/cypress/e2e/regression/load_safe_2.cy.js +++ b/cypress/e2e/regression/load_safe_2.cy.js @@ -5,6 +5,7 @@ import * as safe from '../pages/load_safe.pages' import * as ls from '../../support/localstorage_data.js' import * as owner from '../pages/owners.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import { getMockAddress } from '../../support/utils/ethers.js' let staticSafes, fundSafes = [] @@ -91,7 +92,7 @@ describe('Load Safe tests 2', () => { }) it('Verify a valid address can be entered', () => { - safe.inputAddress(staticSafes.SEP_STATIC_SAFE_13) + safe.inputAddress(getMockAddress()) safe.verifyAddresFormatIsValid() }) @@ -107,7 +108,7 @@ describe('Load Safe tests 2', () => { }) it('Verify that the wrong prefix is not allowed', () => { - safe.inputAddress(fundSafes.ETH_FUNDS_SAFE_13) + safe.inputAddress(`eth:${getMockAddress()}`) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.prefixMismatch) safe.verifyNextButtonStatus(constants.enabledStates.disabled) }) diff --git a/cypress/e2e/regression/multichain_create_safe.cy.js b/cypress/e2e/regression/multichain_create_safe.cy.js index c7d6823bd2..cf74dc4595 100644 --- a/cypress/e2e/regression/multichain_create_safe.cy.js +++ b/cypress/e2e/regression/multichain_create_safe.cy.js @@ -1,22 +1,16 @@ import * as constants from '../../support/constants.js' import * as main from '../pages/main.page.js' -import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' import * as createwallet from '../pages/create_wallet.pages' import * as createtx from '../pages/create_tx.pages.js' import * as tx from '../pages/transactions.page.js' import * as owner from '../pages/owners.pages' - -let staticSafes = [] +import { getMockAddress } from '../../support/utils/ethers.js' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Multichain safe creation tests', () => { - before(async () => { - staticSafes = await getSafes(CATEGORIES.static) - }) - beforeEach(() => { cy.visit(constants.welcomeUrl + '?chain=sep') cy.wait(2000) @@ -61,7 +55,7 @@ describe('Multichain safe creation tests', () => { createwallet.selectMultiNetwork(1, constants.networks.polygon.toLowerCase()) createwallet.clickOnNextBtn() owner.clickOnAddSignerBtn() - owner.typeOwnerAddressCreateSafeStep(1, constants.SEPOLIA_OWNER_2) + owner.typeOwnerAddressCreateSafeStep(1, getMockAddress()) owner.clickOnThresholdDropdown() owner.getThresholdOptions().eq(1).click() createwallet.clickOnNextBtn() @@ -79,7 +73,7 @@ describe('Multichain safe creation tests', () => { createwallet.selectMultiNetwork(1, constants.networks.polygon.toLowerCase()) createwallet.clickOnNextBtn() owner.clickOnAddSignerBtn() - owner.typeOwnerAddressCreateSafeStep(1, constants.SEPOLIA_OWNER_2) + owner.typeOwnerAddressCreateSafeStep(1, getMockAddress()) owner.clickOnThresholdDropdown() owner.getThresholdOptions().eq(0).click() createwallet.clickOnNextBtn() diff --git a/cypress/e2e/regression/multichain_create_safe_flow.cy.js b/cypress/e2e/regression/multichain_create_safe_flow.cy.js index da1ab474fb..6af934a157 100644 --- a/cypress/e2e/regression/multichain_create_safe_flow.cy.js +++ b/cypress/e2e/regression/multichain_create_safe_flow.cy.js @@ -1,20 +1,14 @@ import * as constants from '../../support/constants.js' import * as main from '../pages/main.page.js' -import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' import * as createwallet from '../pages/create_wallet.pages.js' import * as owner from '../pages/owners.pages.js' - -let staticSafes = [] +import { getMockAddress } from '../../support/utils/ethers.js' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Multichain safe creation flow tests', () => { - before(async () => { - staticSafes = await getSafes(CATEGORIES.static) - }) - beforeEach(() => { cy.visit(constants.welcomeUrl + '?chain=sep') cy.wait(2000) @@ -54,7 +48,7 @@ describe('Multichain safe creation flow tests', () => { createwallet.selectMultiNetwork(1, constants.networks.polygon.toLowerCase()) createwallet.clickOnNextBtn() owner.clickOnAddSignerBtn() - owner.typeOwnerAddressCreateSafeStep(1, constants.SEPOLIA_OWNER_2) + owner.typeOwnerAddressCreateSafeStep(1, getMockAddress()) createwallet.clickOnNextBtn() createwallet.clickOnReviewStepNextBtn() main.verifyElementsExist([createwallet.cfSafeActivationMsg, createwallet.cfSafeCreationSuccessMsg]) diff --git a/cypress/e2e/regression/multichain_network.cy.js b/cypress/e2e/regression/multichain_network.cy.js index 5edea28b86..16e104457f 100644 --- a/cypress/e2e/regression/multichain_network.cy.js +++ b/cypress/e2e/regression/multichain_network.cy.js @@ -10,7 +10,8 @@ let staticSafes = [] const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY -describe('Multichain add network tests', () => { +// Skip due to issues with Polygon +describe.skip('Multichain add network tests', () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) }) diff --git a/cypress/e2e/regression/multichain_networkswitch.cy.js b/cypress/e2e/regression/multichain_networkswitch.cy.js index 864f32c224..4e999b8fc3 100644 --- a/cypress/e2e/regression/multichain_networkswitch.cy.js +++ b/cypress/e2e/regression/multichain_networkswitch.cy.js @@ -14,7 +14,8 @@ const signer = walletCredentials.OWNER_4_PRIVATE_KEY // DO NOT use OWNER_2_PRIVATE_KEY for safe creation. Used for CF safes. const signer2 = walletCredentials.OWNER_2_PRIVATE_KEY -describe('Multichain header network switch tests', { defaultCommandTimeout: 30000 }, () => { +// Skip due to issues with Polygon +describe.skip('Multichain header network switch tests', { defaultCommandTimeout: 30000 }, () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) }) diff --git a/cypress/e2e/regression/multichain_setup.cy.js b/cypress/e2e/regression/multichain_setup.cy.js index 7cf54fb604..84fc77f15a 100644 --- a/cypress/e2e/regression/multichain_setup.cy.js +++ b/cypress/e2e/regression/multichain_setup.cy.js @@ -15,7 +15,8 @@ const signer = walletCredentials.OWNER_4_PRIVATE_KEY // DO NOT use OWNER_2_PRIVATE_KEY for safe creation. Used for CF safes. const signer2 = walletCredentials.OWNER_2_PRIVATE_KEY -describe('Multichain setup tests', { defaultCommandTimeout: 30000 }, () => { +// Skip due to issues with Polygon +describe.skip('Multichain setup tests', { defaultCommandTimeout: 30000 }, () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) }) diff --git a/cypress/e2e/regression/nfts.cy.js b/cypress/e2e/regression/nfts.cy.js index 5ce20ab641..fb848826f8 100644 --- a/cypress/e2e/regression/nfts.cy.js +++ b/cypress/e2e/regression/nfts.cy.js @@ -4,6 +4,7 @@ import * as navigation from '../pages/navigation.page' import * as createTx from '../pages/create_tx.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' +import { getMockAddress } from '../../support/utils/ethers.js' const singleNFT = ['safeTransferFrom'] const multipleNFT = ['multiSend'] @@ -35,14 +36,13 @@ describe('NFTs tests', () => { }) // Added to prod - // TODO: Add Sign action it('Verify multipls NFTs can be selected and reviewed', () => { nfts.verifyInitialNFTData() nfts.selectNFTs(3) nfts.deselectNFTs([2], 3) nfts.sendNFT() nfts.verifyNFTModalData() - nfts.typeRecipientAddress(staticSafes.SEP_STATIC_SAFE_1) + nfts.typeRecipientAddress(getMockAddress()) nfts.clikOnNextBtn() nfts.verifyReviewModalData(2) }) @@ -51,7 +51,7 @@ describe('NFTs tests', () => { nfts.verifyInitialNFTData() nfts.selectNFTs(1) nfts.sendNFT() - nfts.typeRecipientAddress(staticSafes.SEP_STATIC_SAFE_1) + nfts.typeRecipientAddress(getMockAddress()) nfts.clikOnNextBtn() nfts.verifyTxDetails(singleNFT) nfts.verifyCountOfActions(0) @@ -62,7 +62,7 @@ describe('NFTs tests', () => { nfts.verifyInitialNFTData() nfts.selectNFTs(2) nfts.sendNFT() - nfts.typeRecipientAddress(staticSafes.SEP_STATIC_SAFE_1) + nfts.typeRecipientAddress(getMockAddress()) nfts.clikOnNextBtn() nfts.verifyTxDetails(multipleNFT) nfts.verifyCountOfActions(2) diff --git a/cypress/e2e/regression/proposers.cy.js b/cypress/e2e/regression/proposers.cy.js index 611c4cbb14..fb966b0831 100644 --- a/cypress/e2e/regression/proposers.cy.js +++ b/cypress/e2e/regression/proposers.cy.js @@ -6,6 +6,7 @@ import * as wallet from '../../support/utils/wallet.js' import * as navigation from '../pages/navigation.page.js' import * as ls from '../../support/localstorage_data.js' import * as proposer from '../pages/proposers.pages.js' +import { getMockAddress } from '../../support/utils/ethers.js' let staticSafes = [] const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) @@ -50,10 +51,7 @@ describe('Proposers tests', () => { it('Verify that a proposer address must be checksummed', () => { proposer.clickOnAddProposerBtn() - proposer.enterProposerData( - staticSafes.SEP_STATIC_SAFE_31.substring(4).replace('E', 'e'), - main.generateRandomString(5), - ) + proposer.enterProposerData(getMockAddress().replace('A', 'a'), main.generateRandomString(5)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) }) diff --git a/cypress/e2e/regression/proposers_2.cy.js b/cypress/e2e/regression/proposers_2.cy.js index 810c73835d..794d4fe54b 100644 --- a/cypress/e2e/regression/proposers_2.cy.js +++ b/cypress/e2e/regression/proposers_2.cy.js @@ -5,6 +5,7 @@ import * as wallet from '../../support/utils/wallet.js' import * as proposer from '../pages/proposers.pages.js' import * as createtx from '../pages/create_tx.pages.js' import * as tx from '../pages/transactions.page.js' +import { getMockAddress } from '../../support/utils/ethers.js' let staticSafes = [] const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) @@ -26,7 +27,7 @@ describe('Proposers 2 tests', () => { wallet.connectSigner(signer) createtx.clickOnNewtransactionBtn() createtx.clickOnSendTokensBtn() - createtx.typeRecipientAddress(constants.EOA) + createtx.typeRecipientAddress(getMockAddress()) createtx.setSendValue(sendValue) createtx.clickOnNextBtn() tx.selectExecuteNow() @@ -41,7 +42,7 @@ describe('Proposers 2 tests', () => { wallet.connectSigner(signer2) createtx.clickOnNewtransactionBtn() createtx.clickOnSendTokensBtn() - createtx.typeRecipientAddress(constants.EOA) + createtx.typeRecipientAddress(getMockAddress()) createtx.setSendValue(sendValue) createtx.clickOnNextBtn() createtx.verifySubmitBtnIsEnabled() diff --git a/cypress/e2e/regression/recovery.cy.js b/cypress/e2e/regression/recovery.cy.js index 37d585cc90..4697bcd727 100644 --- a/cypress/e2e/regression/recovery.cy.js +++ b/cypress/e2e/regression/recovery.cy.js @@ -6,6 +6,7 @@ import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' import * as modules from '../pages/modules.page.js' import * as navigation from '../pages/navigation.page.js' +import { getMockAddress } from '../../support/utils/ethers.js' let recoverySafes, staticSafes = [] @@ -144,7 +145,7 @@ describe('Recovery regression tests', { defaultCommandTimeout: 50000 }, () => { recovery.clickOnSetupRecoveryBtn() recovery.clickOnSetupRecoveryModalBtn() recovery.clickOnNextBtn() - recovery.enterRecovererAddress(constants.SEPOLIA_OWNER_2) + recovery.enterRecovererAddress(getMockAddress()) recovery.agreeToTerms() recovery.clickOnNextBtn() navigation.clickOnModalCloseBtn(0) @@ -186,7 +187,7 @@ describe('Recovery regression tests', { defaultCommandTimeout: 50000 }, () => { recovery.enterRecovererAddress(main.generateRandomString(10), 1) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) - recovery.enterRecovererAddress(constants.DEFAULT_OWNER_ADDRESS.toUpperCase(), 1) + recovery.enterRecovererAddress(getMockAddress().replace('A', 'a'), 1) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) recovery.enterRecovererAddress(constants.ENS_TEST_SEPOLIA_INVALID, 1) @@ -206,7 +207,7 @@ describe('Recovery regression tests', { defaultCommandTimeout: 50000 }, () => { recovery.clickOnRecoverLaterBtn() cy.visit(constants.homeUrl + recoverySafes.SEP_RECOVERY_SAFE_4) recovery.clickOnStartRecoveryBtn() - recovery.enterRecovererAddress(constants.SEPOLIA_OWNER_2) + recovery.enterRecovererAddress(getMockAddress()) navigation.clickOnWalletExpandMoreIcon() navigation.clickOnDisconnectBtn() }) diff --git a/cypress/e2e/regression/replace_owner.cy.js b/cypress/e2e/regression/replace_owner.cy.js index 4f7db44a74..c9355ede96 100644 --- a/cypress/e2e/regression/replace_owner.cy.js +++ b/cypress/e2e/regression/replace_owner.cy.js @@ -6,11 +6,11 @@ import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' import * as ls from '../../support/localstorage_data.js' import { getEvents, events, checkDataLayerEvents } from '../../support/utils/gtag.js' +import { getMockAddress } from '../../support/utils/ethers.js' let staticSafes = [] const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY -const signer2 = walletCredentials.OWNER_1_PRIVATE_KEY const ownerName = 'Replacement Signer Name' @@ -51,7 +51,7 @@ describe('Replace Owners tests', () => { wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow(0) - owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) + owner.typeOwnerAddress(getMockAddress()) owner.clickOnNextBtn() owner.verifyConfirmTransactionWindowDisplayed() }) @@ -63,13 +63,13 @@ describe('Replace Owners tests', () => { owner.typeOwnerAddress(main.generateRandomString(10)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) - owner.typeOwnerAddress(constants.addresBookContacts.user1.address.toUpperCase()) + owner.typeOwnerAddress(getMockAddress().toUpperCase()) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) owner.typeOwnerAddress(staticSafes.SEP_STATIC_SAFE_4) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.ownSafe) - owner.typeOwnerAddress(constants.addresBookContacts.user1.address.replace('F', 'f')) + owner.typeOwnerAddress(getMockAddress().replace('A', 'a')) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) owner.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS) diff --git a/cypress/e2e/regression/spending_limits.cy.js b/cypress/e2e/regression/spending_limits.cy.js index 893f7871a4..7b8a33c71d 100644 --- a/cypress/e2e/regression/spending_limits.cy.js +++ b/cypress/e2e/regression/spending_limits.cy.js @@ -6,6 +6,7 @@ import * as tx from '../pages/create_tx.pages' import * as ls from '../../support/localstorage_data.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' +import { getMockAddress } from '../../support/utils/ethers.js' let staticSafes = [] const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) @@ -52,12 +53,12 @@ describe('Spending limits tests', () => { //Assume that default reset time is set to One time wallet.connectSigner(signer) spendinglimit.clickOnNewSpendingLimitBtn() - spendinglimit.enterBeneficiaryAddress(staticSafes.SEP_STATIC_SAFE_6) + spendinglimit.enterBeneficiaryAddress(getMockAddress()) spendinglimit.enterSpendingLimitAmount(0.1) spendinglimit.clickOnNextBtn() spendinglimit.checkReviewData( tokenAmount, - staticSafes.SEP_STATIC_SAFE_6, + getMockAddress(), spendinglimit.timePeriodOptions.oneTime.split(' ').join('-'), ) }) diff --git a/cypress/e2e/regression/twaps_history.cy.js b/cypress/e2e/regression/twaps_history.cy.js index 23cb87a543..0ef5854fd8 100644 --- a/cypress/e2e/regression/twaps_history.cy.js +++ b/cypress/e2e/regression/twaps_history.cy.js @@ -15,20 +15,17 @@ let iframeSelector const swapsHistory = swaps_data.type.history -// Blocked by a bug on UI -describe.skip('Twaps history tests', { defaultCommandTimeout: 30000 }, () => { +describe('Twaps history tests', { defaultCommandTimeout: 30000 }, () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) }) - beforeEach(() => { + // Blocked by bug on UI + it.skip('Verify order deails', () => { cy.visit(constants.swapUrl + staticSafes.SEP_STATIC_SAFE_27) main.waitForHistoryCallToComplete() wallet.connectSigner(signer) iframeSelector = `iframe[src*="${constants.swapWidget}"]` - }) - - it('Verify order deails', () => { swaps.acceptLegalDisclaimer() cy.wait(4000) main.getIframeBody(iframeSelector).within(() => { diff --git a/cypress/e2e/safe-apps/drain_account.spec.cy.js b/cypress/e2e/safe-apps/drain_account.spec.cy.js index 6f81403565..1698ae00d8 100644 --- a/cypress/e2e/safe-apps/drain_account.spec.cy.js +++ b/cypress/e2e/safe-apps/drain_account.spec.cy.js @@ -1,11 +1,10 @@ import 'cypress-file-upload' import * as constants from '../../support/constants' -import * as main from '../pages/main.page' import * as safeapps from '../pages/safeapps.pages' import * as navigation from '../pages/navigation.page' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' -import * as ls from '../../support/localstorage_data.js' import * as wallet from '../../support/utils/wallet.js' +import { getMockAddress } from '../../support/utils/ethers.js' let safeAppSafes = [] let iframeSelector @@ -31,7 +30,7 @@ describe('Drain Account tests', { defaultCommandTimeout: 40000 }, () => { it('Verify drain can be created', () => { wallet.connectSigner(signer) cy.enter(iframeSelector).then((getBody) => { - getBody().findByLabelText(safeapps.recipientStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) + getBody().findByLabelText(safeapps.recipientStr).type(getMockAddress()) getBody().findAllByText(safeapps.transferEverythingStr).click() }) cy.findByRole('button', { name: safeapps.testTransfer1 }) @@ -46,7 +45,7 @@ describe('Drain Account tests', { defaultCommandTimeout: 40000 }, () => { getBody().findByLabelText(safeapps.selectAllRowsChbxStr).click() getBody().findAllByLabelText(safeapps.selectRowChbxStr).eq(1).click() getBody().findAllByLabelText(safeapps.selectRowChbxStr).eq(2).click() - getBody().findByLabelText(safeapps.recipientStr).clear().type(safeAppSafes.SEP_SAFEAPP_SAFE_2) + getBody().findByLabelText(safeapps.recipientStr).clear().type(getMockAddress()) getBody().findAllByText(safeapps.transfer2AssetsStr).click() }) cy.findByRole('button', { name: safeapps.testTransfer2 }) @@ -67,7 +66,7 @@ describe('Drain Account tests', { defaultCommandTimeout: 40000 }, () => { it('Verify when cancelling a drain, previous data is preserved', () => { cy.enter(iframeSelector).then((getBody) => { - getBody().findByLabelText(safeapps.recipientStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) + getBody().findByLabelText(safeapps.recipientStr).type(getMockAddress()) getBody().findAllByText(safeapps.transferEverythingStr).click() }) navigation.clickOnModalCloseBtn(0) @@ -85,7 +84,7 @@ describe('Drain Account tests', { defaultCommandTimeout: 40000 }, () => { it('Verify a drain cannot be created with invalid recipient selected', () => { cy.enter(iframeSelector).then((getBody) => { - getBody().findByLabelText(safeapps.recipientStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2.substring(1)) + getBody().findByLabelText(safeapps.recipientStr).type(getMockAddress().substring(1)) getBody().findAllByText(safeapps.transferEverythingStr).click() getBody().findByText(safeapps.validRecipientAddressStr) }) @@ -94,7 +93,7 @@ describe('Drain Account tests', { defaultCommandTimeout: 40000 }, () => { it('Verify a drain cannot be created when no assets are selected', () => { cy.enter(iframeSelector).then((getBody) => { getBody().findByLabelText(safeapps.selectAllRowsChbxStr).click() - getBody().findByLabelText(safeapps.recipientStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) + getBody().findByLabelText(safeapps.recipientStr).type(getMockAddress()) getBody().findAllByText(safeapps.noTokensSelectedStr).should('be.visible') }) }) diff --git a/cypress/e2e/safe-apps/tx-builder.2spec.cy.js b/cypress/e2e/safe-apps/tx-builder.2spec.cy.js index 7f059a65ad..3353fe640e 100644 --- a/cypress/e2e/safe-apps/tx-builder.2spec.cy.js +++ b/cypress/e2e/safe-apps/tx-builder.2spec.cy.js @@ -6,8 +6,8 @@ import * as navigation from '../pages/navigation.page.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as ls from '../../support/localstorage_data.js' import { getEvents, events, checkDataLayerEvents } from '../../support/utils/gtag.js' -import * as wallet from '../../support/utils/wallet.js' import * as utils from '../../support/utils/checkers.js' +import { getMockAddress } from '../../support/utils/ethers.js' let safeAppSafes = [] let iframeSelector @@ -30,7 +30,7 @@ describe('Transaction Builder 2 tests', { defaultCommandTimeout: 20000 }, () => it('Verify a batch cannot be created without method data', () => { cy.enter(iframeSelector).then((getBody) => { - getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().findByLabelText(safeapps.enterAddressStr).type(getMockAddress()) getBody().findByText(safeapps.addTransactionStr).click() getBody() .findAllByText(safeapps.requiredStr) diff --git a/cypress/e2e/safe-apps/tx-builder.spec.cy.js b/cypress/e2e/safe-apps/tx-builder.spec.cy.js index bdd14fa43a..7c018d62ca 100644 --- a/cypress/e2e/safe-apps/tx-builder.spec.cy.js +++ b/cypress/e2e/safe-apps/tx-builder.spec.cy.js @@ -8,6 +8,7 @@ import * as ls from '../../support/localstorage_data.js' import { getEvents, events, checkDataLayerEvents } from '../../support/utils/gtag.js' import * as wallet from '../../support/utils/wallet.js' import * as utils from '../../support/utils/checkers.js' +import { getMockAddress } from '../../support/utils/ethers.js' let safeAppSafes = [] let iframeSelector diff --git a/cypress/support/utils/ethers.js b/cypress/support/utils/ethers.js new file mode 100644 index 0000000000..1e56b8634f --- /dev/null +++ b/cypress/support/utils/ethers.js @@ -0,0 +1,5 @@ +import { ethers } from 'ethers' + +export const getMockAddress = () => { + return ethers.getAddress('0x1234567890abcdef1234567890abcdef12345678') +} From c3120789d57d1e3642b65b94ad06309813a37b0a Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:43:39 +0100 Subject: [PATCH 38/47] Chore: extract terms version into a separate file for a smaller build (#4584) --- docs/update-terms.md | 4 ++-- mocks/terms.md.js | 6 ------ src/components/common/CookieAndTermBanner/index.tsx | 4 ++-- src/markdown/terms/terms.md | 5 ----- src/markdown/terms/terms.md.d.ts | 5 ----- src/markdown/terms/version.ts | 2 ++ src/store/cookiesAndTermsSlice.ts | 6 +++--- src/store/index.ts | 7 ++----- 8 files changed, 11 insertions(+), 28 deletions(-) delete mode 100644 mocks/terms.md.js delete mode 100644 src/markdown/terms/terms.md.d.ts create mode 100644 src/markdown/terms/version.ts diff --git a/docs/update-terms.md b/docs/update-terms.md index 5e69fac010..0e7d95832e 100644 --- a/docs/update-terms.md +++ b/docs/update-terms.md @@ -4,7 +4,7 @@ To update the terms and conditions, follow these steps: 1. Export the terms and conditions from Google Docs as a Markdown file. 2. Replace the content of the src/markdown/terms/terms.md file with the exported content. -3. Update the frontmatter of the file with the new version number and date. +3. If significant changes were made, update the version and last updated date in `version.ts` in the same folder. That’s it! @@ -13,7 +13,7 @@ will automatically appear for users who haven’t accepted the new terms. ## How does this work? -We rely on the version number from the frontmatter. When the Redux store is rehydrated, we check the version stored in +We rely on the version number from `version.ts`. When the Redux store is rehydrated, we check the version stored in the store against the version in the frontmatter. If they differ, we reset the accepted terms, forcing the user to accept the new version. diff --git a/mocks/terms.md.js b/mocks/terms.md.js deleted file mode 100644 index cadf08493b..0000000000 --- a/mocks/terms.md.js +++ /dev/null @@ -1,6 +0,0 @@ -export const metadata = { - version: 'test-version', - last_update_date: 'test-date', -} - -export default metadata diff --git a/src/components/common/CookieAndTermBanner/index.tsx b/src/components/common/CookieAndTermBanner/index.tsx index 43355279f5..1c74b4ea74 100644 --- a/src/components/common/CookieAndTermBanner/index.tsx +++ b/src/components/common/CookieAndTermBanner/index.tsx @@ -4,7 +4,7 @@ import type { CheckboxProps } from '@mui/material' import { Grid, Button, Checkbox, FormControlLabel, Typography, Paper, SvgIcon, Box } from '@mui/material' import WarningIcon from '@/public/images/notifications/warning.svg' import { useForm } from 'react-hook-form' -import { metadata } from '@/markdown/terms/terms.md' +import * as metadata from '@/markdown/terms/version' import { useAppDispatch, useAppSelector } from '@/store' import { @@ -104,7 +104,7 @@ export const CookieAndTermBanner = ({ > By browsing this page, you accept our{' '} Terms & Conditions (last updated{' '} - {metadata.last_update_date}) and the use of necessary cookies. By clicking "Accept all" you + {metadata.lastUpdated}) and the use of necessary cookies. By clicking "Accept all" you additionally agree to the use of Beamer and Analytics cookies as listed below.{' '} Cookie policy
diff --git a/src/markdown/terms/terms.md b/src/markdown/terms/terms.md index 2f3bfdb426..3ddf2c604c 100644 --- a/src/markdown/terms/terms.md +++ b/src/markdown/terms/terms.md @@ -1,8 +1,3 @@ ---- -version: 1.2 -last_update_date: September, 2024 ---- - # Terms and Conditions Last updated: September, 2024 diff --git a/src/markdown/terms/terms.md.d.ts b/src/markdown/terms/terms.md.d.ts deleted file mode 100644 index a89eca7930..0000000000 --- a/src/markdown/terms/terms.md.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { default } from '*.md' -export const metadata = { - version: string, - last_update_date: string, -} diff --git a/src/markdown/terms/version.ts b/src/markdown/terms/version.ts new file mode 100644 index 0000000000..3cfbbfd377 --- /dev/null +++ b/src/markdown/terms/version.ts @@ -0,0 +1,2 @@ +export const version = '1.2' +export const lastUpdated = 'September, 2024' diff --git a/src/store/cookiesAndTermsSlice.ts b/src/store/cookiesAndTermsSlice.ts index 7f9c2a6410..3177eeee81 100644 --- a/src/store/cookiesAndTermsSlice.ts +++ b/src/store/cookiesAndTermsSlice.ts @@ -1,7 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import type { RootState } from '.' -import { metadata } from '@/markdown/terms/terms.md' +import { version } from '@/markdown/terms/version' export enum CookieAndTermType { TERMS = 'terms', @@ -38,12 +38,12 @@ export const selectCookies = (state: RootState) => state[cookiesAndTermsSlice.na export const hasAcceptedTerms = (state: RootState): boolean => { const cookies = selectCookies(state) - return cookies[CookieAndTermType.TERMS] === true && cookies.termsVersion === metadata.version + return cookies[CookieAndTermType.TERMS] === true && cookies.termsVersion === version } export const hasConsentFor = (state: RootState, type: CookieAndTermType): boolean => { const cookies = selectCookies(state) - return cookies[type] === true && cookies.termsVersion === metadata.version + return cookies[type] === true && cookies.termsVersion === version } export const { saveCookieAndTermConsent } = cookiesAndTermsSlice.actions diff --git a/src/store/index.ts b/src/store/index.ts index 8617ee59da..c1f7783974 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -26,7 +26,7 @@ import * as slices from './slices' import * as hydrate from './useHydrateStore' import { ofacApi } from '@/store/api/ofac' import { safePassApi } from './api/safePass' -import { metadata } from '@/markdown/terms/terms.md' +import { version as termsVersion } from '@/markdown/terms/version' const rootReducer = combineReducers({ [slices.chainsSlice.name]: slices.chainsSlice.reducer, @@ -103,10 +103,7 @@ export const _hydrationReducer: typeof rootReducer = (state, action) => { const nextState = merge({}, state, action.payload) as RootState // Check if termsVersion matches - if ( - nextState[cookiesAndTermsSlice.name] && - nextState[cookiesAndTermsSlice.name].termsVersion !== metadata.version - ) { + if (nextState[cookiesAndTermsSlice.name] && nextState[cookiesAndTermsSlice.name].termsVersion !== termsVersion) { // Reset consent nextState[cookiesAndTermsSlice.name] = { ...cookiesAndTermsInitialState, From fb158730f0a0280b414dd45879446abdfa9c5e00 Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Mon, 2 Dec 2024 12:33:07 +0100 Subject: [PATCH 39/47] fix: add event labels to opening and (un-)pinning of Safe Apps (#4582) * Add event labels to opening and (un-)pinning of Safe Apps * Only prevent link from opening if never opened before --- .../SafeAppsDashboardSection.tsx | 11 +++++-- src/components/safe-apps/AppFrame/index.tsx | 16 +--------- .../safe-apps/SafeAppCard/index.tsx | 9 +++--- .../safe-apps/SafeAppList/index.tsx | 31 ++++++++++++------- .../safe-apps/SafeAppPreviewDrawer/index.tsx | 2 ++ src/hooks/safe-apps/useSafeApps.ts | 10 +++--- src/pages/apps/custom.tsx | 2 ++ src/pages/apps/index.tsx | 9 +++--- src/services/analytics/events/safeApps.ts | 9 ++++++ src/services/analytics/gtm.ts | 2 +- 10 files changed, 56 insertions(+), 45 deletions(-) diff --git a/src/components/dashboard/SafeAppsDashboardSection/SafeAppsDashboardSection.tsx b/src/components/dashboard/SafeAppsDashboardSection/SafeAppsDashboardSection.tsx index 6cc9c91608..a8cd748020 100644 --- a/src/components/dashboard/SafeAppsDashboardSection/SafeAppsDashboardSection.tsx +++ b/src/components/dashboard/SafeAppsDashboardSection/SafeAppsDashboardSection.tsx @@ -10,6 +10,7 @@ import SafeAppPreviewDrawer from '@/components/safe-apps/SafeAppPreviewDrawer' import SafeAppCard, { SafeAppCardContainer } from '@/components/safe-apps/SafeAppCard' import { AppRoutes } from '@/config/routes' import ExploreSafeAppsIcon from '@/public/images/apps/explore.svg' +import { SAFE_APPS_LABELS } from '@/services/analytics' import css from './styles.module.css' @@ -34,9 +35,13 @@ const SafeAppsDashboardSection = () => { togglePin(appId, SAFE_APPS_LABELS.dashboard)} isBookmarked={pinnedSafeAppIds.has(rankedSafeApp.id)} - onClickSafeApp={() => openPreviewDrawer(rankedSafeApp)} + onClickSafeApp={(e) => { + // Don't open link + e.preventDefault() + openPreviewDrawer(rankedSafeApp) + }} openPreviewDrawer={openPreviewDrawer} /> @@ -51,7 +56,7 @@ const SafeAppsDashboardSection = () => { safeApp={previewDrawerApp} isBookmarked={previewDrawerApp && pinnedSafeAppIds.has(previewDrawerApp.id)} onClose={closePreviewDrawer} - onBookmark={togglePin} + onBookmark={(appId) => togglePin(appId, SAFE_APPS_LABELS.apps_sidebar)} /> ) diff --git a/src/components/safe-apps/AppFrame/index.tsx b/src/components/safe-apps/AppFrame/index.tsx index 381c226f73..7e10c614cf 100644 --- a/src/components/safe-apps/AppFrame/index.tsx +++ b/src/components/safe-apps/AppFrame/index.tsx @@ -2,14 +2,12 @@ import useAddressBook from '@/hooks/useAddressBook' import useChainId from '@/hooks/useChainId' import { type AddressBookItem, Methods } from '@safe-global/safe-apps-sdk' import type { ReactElement } from 'react' -import { useMemo } from 'react' import { useCallback, useEffect } from 'react' import { Box, CircularProgress, Typography } from '@mui/material' import { useRouter } from 'next/router' import Head from 'next/head' import type { RequestId } from '@safe-global/safe-apps-sdk' import { trackSafeAppOpenCount } from '@/services/safe-apps/track-app-usage-count' -import { SAFE_APPS_EVENTS, trackSafeAppEvent } from '@/services/analytics' import useSafeInfo from '@/hooks/useSafeInfo' import { useSafeAppFromBackend } from '@/hooks/safe-apps/useSafeAppFromBackend' import { useSafePermissions } from '@/hooks/safe-apps/permissions' @@ -57,13 +55,12 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest, isNativeEm transactions, } = useTransactionQueueBarState() const queueBarVisible = transactions.results.length > 0 && !queueBarDismissed - const [remoteApp, , isBackendAppsLoading] = useSafeAppFromBackend(appUrl, safe.chainId) + const [remoteApp] = useSafeAppFromBackend(appUrl, safe.chainId) const { thirdPartyCookiesDisabled, setThirdPartyCookiesDisabled } = useThirdPartyCookies() const { iframeRef, appIsLoading, isLoadingSlow, setAppIsLoading } = useAppIsLoading() useAnalyticsFromSafeApp(iframeRef) const { permissionsRequest, setPermissionsRequest, confirmPermissionRequest, getPermissions, hasPermission } = useSafePermissions() - const appName = useMemo(() => (remoteApp ? remoteApp.name : appUrl), [appUrl, remoteApp]) const communicator = useCustomAppCommunicator(iframeRef, remoteApp || safeAppFromManifest, chain, { onGetPermissions: getPermissions, @@ -110,17 +107,6 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest, isNativeEm } }, [appUrl, iframeRef, setAppIsLoading, router, isNativeEmbed]) - useEffect(() => { - if (!isNativeEmbed && !appIsLoading && !isBackendAppsLoading) { - trackSafeAppEvent( - { - ...SAFE_APPS_EVENTS.OPEN_APP, - }, - appName, - ) - } - }, [appIsLoading, isBackendAppsLoading, appName, isNativeEmbed]) - if (!safeLoaded) { return
} diff --git a/src/components/safe-apps/SafeAppCard/index.tsx b/src/components/safe-apps/SafeAppCard/index.tsx index bc5fd0d51f..65419d9e3a 100644 --- a/src/components/safe-apps/SafeAppCard/index.tsx +++ b/src/components/safe-apps/SafeAppCard/index.tsx @@ -21,7 +21,7 @@ import css from './styles.module.css' type SafeAppCardProps = { safeApp: SafeAppData - onClickSafeApp?: () => void + onClickSafeApp?: (e: SyntheticEvent) => void isBookmarked?: boolean onBookmarkSafeApp?: (safeAppId: number) => void removeCustomApp?: (safeApp: SafeAppData) => void @@ -66,7 +66,7 @@ export const getSafeAppUrl = (router: NextRouter, safeAppUrl: string) => { type SafeAppCardViewProps = { safeApp: SafeAppData - onClickSafeApp?: () => void + onClickSafeApp?: (e: SyntheticEvent) => void safeAppUrl: string isBookmarked?: boolean onBookmarkSafeApp?: (safeAppId: number) => void @@ -137,7 +137,7 @@ const SafeAppCardGridView = ({ } type SafeAppCardContainerProps = { - onClickSafeApp?: () => void + onClickSafeApp?: (e: SyntheticEvent) => void safeAppUrl: string children: ReactNode height?: string @@ -153,8 +153,7 @@ export const SafeAppCardContainer = ({ }: SafeAppCardContainerProps) => { const handleClickSafeApp = (event: SyntheticEvent) => { if (onClickSafeApp) { - event.preventDefault() - onClickSafeApp() + onClickSafeApp(event) } } diff --git a/src/components/safe-apps/SafeAppList/index.tsx b/src/components/safe-apps/SafeAppList/index.tsx index 16ecb6431b..62b86f65ac 100644 --- a/src/components/safe-apps/SafeAppList/index.tsx +++ b/src/components/safe-apps/SafeAppList/index.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { type SyntheticEvent, useCallback } from 'react' import type { SafeAppData } from '@safe-global/safe-gateway-typescript-sdk' import SafeAppCard from '@/components/safe-apps/SafeAppCard' @@ -11,12 +11,14 @@ import css from './styles.module.css' import { Skeleton } from '@mui/material' import { useOpenedSafeApps } from '@/hooks/safe-apps/useOpenedSafeApps' import NativeSwapsCard from '@/components/safe-apps/NativeSwapsCard' +import { SAFE_APPS_EVENTS, SAFE_APPS_LABELS, trackSafeAppEvent } from '@/services/analytics' +import { useSafeApps } from '@/hooks/safe-apps/useSafeApps' type SafeAppListProps = { safeAppsList: SafeAppData[] safeAppsListLoading?: boolean bookmarkedSafeAppsId?: Set - onBookmarkSafeApp?: (safeAppId: number) => void + eventLabel: SAFE_APPS_LABELS addCustomApp?: (safeApp: SafeAppData) => void removeCustomApp?: (safeApp: SafeAppData) => void title: string @@ -29,7 +31,7 @@ const SafeAppList = ({ safeAppsList, safeAppsListLoading, bookmarkedSafeAppsId, - onBookmarkSafeApp, + eventLabel, addCustomApp, removeCustomApp, title, @@ -37,20 +39,25 @@ const SafeAppList = ({ isFiltered = false, showNativeSwapsCard = false, }: SafeAppListProps) => { + const { togglePin } = useSafeApps() const { isPreviewDrawerOpen, previewDrawerApp, openPreviewDrawer, closePreviewDrawer } = useSafeAppPreviewDrawer() const { openedSafeAppIds } = useOpenedSafeApps() const showZeroResultsPlaceholder = query && safeAppsList.length === 0 const handleSafeAppClick = useCallback( - (safeApp: SafeAppData) => { + (e: SyntheticEvent, safeApp: SafeAppData) => { const isCustomApp = safeApp.id < 1 - - if (isCustomApp || openedSafeAppIds.includes(safeApp.id)) return - - return () => openPreviewDrawer(safeApp) + if (!openedSafeAppIds.includes(safeApp.id) && !isCustomApp) { + // Don't open link + e.preventDefault() + openPreviewDrawer(safeApp) + } else { + // We only track if not previously opened as it is then tracked in preview drawer + trackSafeAppEvent({ ...SAFE_APPS_EVENTS.OPEN_APP, label: eventLabel }, safeApp.name) + } }, - [openPreviewDrawer, openedSafeAppIds], + [eventLabel, openPreviewDrawer, openedSafeAppIds], ) return ( @@ -82,9 +89,9 @@ const SafeAppList = ({ togglePin(safeApp.id, eventLabel)} removeCustomApp={removeCustomApp} - onClickSafeApp={handleSafeAppClick(safeApp)} + onClickSafeApp={(e) => handleSafeAppClick(e, safeApp)} openPreviewDrawer={openPreviewDrawer} /> @@ -100,7 +107,7 @@ const SafeAppList = ({ safeApp={previewDrawerApp} isBookmarked={previewDrawerApp && bookmarkedSafeAppsId?.has(previewDrawerApp.id)} onClose={closePreviewDrawer} - onBookmark={onBookmarkSafeApp} + onBookmark={(appId) => togglePin(appId, SAFE_APPS_LABELS.apps_sidebar)} /> ) diff --git a/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx b/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx index f295558ed4..a8526a902b 100644 --- a/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx +++ b/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx @@ -18,6 +18,7 @@ import SafeAppSocialLinksCard from '@/components/safe-apps/SafeAppSocialLinksCar import CloseIcon from '@/public/images/common/close.svg' import { useOpenedSafeApps } from '@/hooks/safe-apps/useOpenedSafeApps' import css from './styles.module.css' +import { SAFE_APPS_EVENTS, SAFE_APPS_LABELS, trackSafeAppEvent } from '@/services/analytics' type SafeAppPreviewDrawerProps = { safeApp?: SafeAppData @@ -35,6 +36,7 @@ const SafeAppPreviewDrawer = ({ isOpen, safeApp, isBookmarked, onClose, onBookma const onOpenSafe = () => { if (safeApp) { markSafeAppOpened(safeApp.id) + trackSafeAppEvent({ ...SAFE_APPS_EVENTS.OPEN_APP, label: SAFE_APPS_LABELS.apps_sidebar }, safeApp.name) } } diff --git a/src/hooks/safe-apps/useSafeApps.ts b/src/hooks/safe-apps/useSafeApps.ts index a9a299c06e..4e3b8f0635 100644 --- a/src/hooks/safe-apps/useSafeApps.ts +++ b/src/hooks/safe-apps/useSafeApps.ts @@ -5,7 +5,7 @@ import { useCustomSafeApps } from '@/hooks/safe-apps/useCustomSafeApps' import { usePinnedSafeApps } from '@/hooks/safe-apps/usePinnedSafeApps' import { useBrowserPermissions, useSafePermissions } from './permissions' import { useRankedSafeApps } from '@/hooks/safe-apps/useRankedSafeApps' -import { SAFE_APPS_EVENTS, trackSafeAppEvent } from '@/services/analytics' +import { SAFE_APPS_EVENTS, type SAFE_APPS_LABELS, trackSafeAppEvent } from '@/services/analytics' type ReturnType = { allSafeApps: SafeAppData[] @@ -18,7 +18,7 @@ type ReturnType = { customSafeAppsLoading: boolean remoteSafeAppsError?: Error addCustomApp: (app: SafeAppData) => void - togglePin: (appId: number) => void + togglePin: (appId: number, eventLabel: SAFE_APPS_LABELS) => void removeCustomApp: (appId: number) => void } @@ -61,17 +61,17 @@ const useSafeApps = (): ReturnType => { [updateCustomSafeApps, customSafeApps, removeSafePermissions, removeBrowserPermissions], ) - const togglePin = (appId: number) => { + const togglePin = (appId: number, eventLabel: SAFE_APPS_LABELS) => { const alreadyPinned = pinnedSafeAppIds.has(appId) const newSet = new Set(pinnedSafeAppIds) const appName = allSafeApps.find((app) => app.id === appId)?.name if (alreadyPinned) { newSet.delete(appId) - trackSafeAppEvent(SAFE_APPS_EVENTS.UNPIN, appName) + trackSafeAppEvent({ ...SAFE_APPS_EVENTS.UNPIN, label: eventLabel }, appName) } else { newSet.add(appId) - trackSafeAppEvent(SAFE_APPS_EVENTS.PIN, appName) + trackSafeAppEvent({ ...SAFE_APPS_EVENTS.PIN, label: eventLabel }, appName) } updatePinnedSafeApps(newSet) } diff --git a/src/pages/apps/custom.tsx b/src/pages/apps/custom.tsx index 5ff434e9de..eafcae0e1d 100644 --- a/src/pages/apps/custom.tsx +++ b/src/pages/apps/custom.tsx @@ -8,6 +8,7 @@ import SafeAppList from '@/components/safe-apps/SafeAppList' import SafeAppsSDKLink from '@/components/safe-apps/SafeAppsSDKLink' import { RemoveCustomAppModal } from '@/components/safe-apps/RemoveCustomAppModal' import type { SafeAppData } from '@safe-global/safe-gateway-typescript-sdk' +import { SAFE_APPS_LABELS } from '@/services/analytics' const CustomSafeApps: NextPage = () => { // TODO: create a custom hook instead of use useSafeApps @@ -42,6 +43,7 @@ const CustomSafeApps: NextPage = () => { safeAppsList={customSafeApps} addCustomApp={addCustomApp} removeCustomApp={openRemoveCustomAppModal} + eventLabel={SAFE_APPS_LABELS.apps_custom} /> diff --git a/src/pages/apps/index.tsx b/src/pages/apps/index.tsx index 1f9daa1d33..72252d4f67 100644 --- a/src/pages/apps/index.tsx +++ b/src/pages/apps/index.tsx @@ -14,10 +14,11 @@ import useSafeAppsFilters from '@/hooks/safe-apps/useSafeAppsFilters' import SafeAppsFilters from '@/components/safe-apps/SafeAppsFilters' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' +import { SAFE_APPS_LABELS } from '@/services/analytics' const SafeApps: NextPage = () => { const router = useRouter() - const { remoteSafeApps, remoteSafeAppsLoading, pinnedSafeApps, pinnedSafeAppIds, togglePin } = useSafeApps() + const { remoteSafeApps, remoteSafeAppsLoading, pinnedSafeApps, pinnedSafeAppIds } = useSafeApps() const { filteredApps, query, setQuery, setSelectedCategories, setOptimizedWithBatchFilter, selectedCategories } = useSafeAppsFilters(remoteSafeApps) const isFiltered = filteredApps.length !== remoteSafeApps.length @@ -72,7 +73,7 @@ const SafeApps: NextPage = () => { title="My pinned apps" safeAppsList={pinnedSafeApps} bookmarkedSafeAppsId={pinnedSafeAppIds} - onBookmarkSafeApp={togglePin} + eventLabel={SAFE_APPS_LABELS.apps_pinned} /> )} @@ -82,7 +83,7 @@ const SafeApps: NextPage = () => { title="Featured apps" safeAppsList={featuredSafeApps} bookmarkedSafeAppsId={pinnedSafeAppIds} - onBookmarkSafeApp={togglePin} + eventLabel={SAFE_APPS_LABELS.apps_featured} /> )} @@ -93,7 +94,7 @@ const SafeApps: NextPage = () => { safeAppsList={isFiltered ? filteredApps : nonPinnedApps} safeAppsListLoading={remoteSafeAppsLoading} bookmarkedSafeAppsId={pinnedSafeAppIds} - onBookmarkSafeApp={togglePin} + eventLabel={SAFE_APPS_LABELS.apps_all} query={query} showNativeSwapsCard /> diff --git a/src/services/analytics/events/safeApps.ts b/src/services/analytics/events/safeApps.ts index 7ae48c78d5..98b8c4bba7 100644 --- a/src/services/analytics/events/safeApps.ts +++ b/src/services/analytics/events/safeApps.ts @@ -70,3 +70,12 @@ export const SAFE_APPS_EVENTS = { action: 'SDK method call', }, } + +export enum SAFE_APPS_LABELS { + dashboard = 'dashboard', + apps_pinned = 'apps_pinned', + apps_featured = 'apps_featured', + apps_all = 'apps_all', + apps_custom = 'apps_custom', + apps_sidebar = 'apps_sidebar', +} diff --git a/src/services/analytics/gtm.ts b/src/services/analytics/gtm.ts index 3945b2d9ed..978d090f5e 100644 --- a/src/services/analytics/gtm.ts +++ b/src/services/analytics/gtm.ts @@ -159,7 +159,7 @@ export const normalizeAppName = (appName?: string): string => { } export const gtmTrackSafeApp = (eventData: AnalyticsEvent, appName?: string, sdkEventData?: SafeAppSDKEvent): void => { - if (!location.pathname.startsWith(AppRoutes.apps.index)) { + if (!location.pathname.startsWith(AppRoutes.apps.index) && !eventData.label) { return } From dc9327e89df056765f7a729561e5b6ee9f8647dd Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:13:20 +0000 Subject: [PATCH 40/47] Tests: Fix tests (#4586) * Fix tests --- cypress/e2e/happypath/recovery_hp_1.cy.js | 6 ------ cypress/e2e/pages/main.page.js | 10 ++++++++++ cypress/e2e/pages/nfts.pages.js | 2 +- cypress/e2e/pages/recovery.pages.js | 1 - cypress/e2e/pages/sidebar.pages.js | 2 +- cypress/e2e/smoke/load_safe.cy.js | 1 - cypress/support/e2e.js | 5 +++++ 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/cypress/e2e/happypath/recovery_hp_1.cy.js b/cypress/e2e/happypath/recovery_hp_1.cy.js index bb036e11fc..e542eead30 100644 --- a/cypress/e2e/happypath/recovery_hp_1.cy.js +++ b/cypress/e2e/happypath/recovery_hp_1.cy.js @@ -34,13 +34,7 @@ describe('Recovery happy path tests 1', () => { recovery.clickOnNextBtn() tx.executeFlow_1() recovery.verifyRecovererAdded([constants.SEPOLIA_OWNER_2_SHORT]) - recovery.clearRecoverers() - - // recovery.removeRecoverer(0, constants.SEPOLIA_OWNER_2) - // recovery.clickOnNextBtn() - // tx.executeFlow_1() - recovery.getSetupRecoveryBtn() }) }) diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index a7088c060f..229e7e963d 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -194,6 +194,16 @@ export function acceptCookies(index = 0) { }) } +export function acceptCookies2() { + cy.wait(2000) + cy.get('body').then(($body) => { + if ($body.find('button:contains(' + acceptSelection + ')').length > 0) { + cy.contains('button', acceptSelection).click() + cy.wait(500) + } + }) +} + export function verifyOwnerConnected(prefix = 'sep:') { cy.get(connectedOwnerBlock).should('contain', prefix) } diff --git a/cypress/e2e/pages/nfts.pages.js b/cypress/e2e/pages/nfts.pages.js index dcd566f383..84b51313f4 100644 --- a/cypress/e2e/pages/nfts.pages.js +++ b/cypress/e2e/pages/nfts.pages.js @@ -22,7 +22,7 @@ const txDetailsSummary = '[data-testid="decoded-tx-summary"]' const txAccordionDetails = '[data-testid="decoded-tx-details"]' const accordionActionItem = '[data-testid="action-item"]' -const noneNFTSelected = '0 NFTs selected' +const noneNFTSelected = /0 NFT[s]? selected/ const sendNFTStr = 'Send NFTs' const recipientAddressStr = 'Recipient address or ENS' const selectedNFTStr = 'Selected NFTs' diff --git a/cypress/e2e/pages/recovery.pages.js b/cypress/e2e/pages/recovery.pages.js index 9c84ea3fcc..4734e22da2 100644 --- a/cypress/e2e/pages/recovery.pages.js +++ b/cypress/e2e/pages/recovery.pages.js @@ -101,7 +101,6 @@ export function getSetupRecoveryBtn() { export function clickOnSetupRecoveryBtn() { getSetupRecoveryBtn().click() - cy.get(setupRecoveryModalBtn).should('be.visible') } export function clickOnSetupRecoveryModalBtn() { diff --git a/cypress/e2e/pages/sidebar.pages.js b/cypress/e2e/pages/sidebar.pages.js index a2bd1c0ad7..481e9bc923 100644 --- a/cypress/e2e/pages/sidebar.pages.js +++ b/cypress/e2e/pages/sidebar.pages.js @@ -349,7 +349,7 @@ export function checkSafeGroupBalance(index) { .find(groupBalance) .invoke('text') .should('include', '$') - .and('match', /\d+\.\d{2}/) + .and('match', /\$?\s?\d+(\.\d{1,3})?/) } export function checkSafeGroupAddress(index, address) { diff --git a/cypress/e2e/smoke/load_safe.cy.js b/cypress/e2e/smoke/load_safe.cy.js index ff05d98de6..b2665cc2ad 100644 --- a/cypress/e2e/smoke/load_safe.cy.js +++ b/cypress/e2e/smoke/load_safe.cy.js @@ -17,7 +17,6 @@ describe('[SMOKE] Load Safe tests', () => { beforeEach(() => { cy.visit(constants.loadNewSafeSepoliaUrl) - cy.wait(2000) }) it('[SMOKE] Verify a network can be selected in the Safe', () => { diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 5be72d9f02..d4394774d7 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -19,6 +19,8 @@ import './commands' import './safe-apps-commands' import * as constants from './constants' import * as ls from './localstorage_data' +import { acceptCookies2 } from '../e2e/pages/main.page' + // Alternatively you can use CommonJS syntax: // require('./commands') @@ -73,5 +75,8 @@ beforeEach(() => { constants.localStorageKeys.SAFE_v2__SafeApps__infoModal, ls.appPermissions(constants.safeTestAppurl).infoModalAccepted, ) + cy.wrap(window.localStorage).invoke('getItem', cookiesKey).should('equal', ls.cookies.acceptedCookies) }) + cy.visit(constants.setupUrl + 'sep:0xBb26E3717172d5000F87DeFd391994f789D80aEB') + acceptCookies2() }) From 374f5565745d822615598eaafafac5be5b96236c Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:45:46 +0100 Subject: [PATCH 41/47] 1.48.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9964334f1f..167165dde1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "safe-wallet-web", "homepage": "https://github.com/safe-global/safe-wallet-web", "license": "GPL-3.0", - "version": "1.47.2", + "version": "1.48.0", "type": "module", "scripts": { "dev": "next dev", From b30a74bd318bfbf895ec4cbcd41c4170f4fcb48b Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:13:20 +0000 Subject: [PATCH 42/47] Tests: Update twap tests (#4590) --- cypress/e2e/pages/swaps.pages.js | 16 ++++++++++++++++ cypress/e2e/regression/twaps.cy.js | 15 +++++++++++---- cypress/e2e/regression/twaps_2.cy.js | 22 +++++++++++++++------- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/cypress/e2e/pages/swaps.pages.js b/cypress/e2e/pages/swaps.pages.js index 8707e5d35d..84b226c356 100644 --- a/cypress/e2e/pages/swaps.pages.js +++ b/cypress/e2e/pages/swaps.pages.js @@ -14,6 +14,7 @@ export const assetsSwapBtn = '[data-testid="swap-btn"]' export const dashboardSwapBtn = '[data-testid="overview-swap-btn"]' export const customRecipient = 'div[id="recipient"]' const recipientToggle = 'button[id="toggle-recipient-mode-button"]' +const twapsAddressToggle = '[class*="Toggle__Wrapper"]' const orderTypeMenuItem = 'div[class*="MenuItem"]' const explorerBtn = '[data-testid="explorer-btn"]' const limitPriceFld = '[data-testid="limit-price"]' @@ -27,6 +28,7 @@ const groupedItems = '[data-testid="grouped-items"]' const inputCurrencyPreview = '[id="input-currency-preview"]' const outputCurrencyPreview = '[id="output-currency-preview"]' const reviewTwapBtn = '[id="do-trade-button"]' +export const unlockOrdersBtn = '[id="unlock-advanced-orders-btn"]' const swapStrBtn = 'Swap' const twapStrBtn = 'TWAP' @@ -105,6 +107,15 @@ export const swapTxs = { '&id=multisig_0x03042B890b99552b60A073F808100517fb148F60_0x5f08e05edb210a8990791e9df2f287a5311a8137815ec85856a2477a36552f1e', } +export function unlockTwapOrders(iframeSelector) { + main.getIframeBody(iframeSelector).then(($iframeBody) => { + if ($iframeBody.find(unlockOrdersBtn).length > 0) { + cy.wrap($iframeBody).find(unlockOrdersBtn).click() + cy.wait(500) + } + }) +} + export function clickOnAssetSwapBtn(index) { cy.get(assetsSwapBtn).eq(index).as('btn') cy.get('@btn').click() @@ -267,6 +278,10 @@ export function enableCustomRecipient(option) { if (!option) cy.get(recipientToggle).click() } +export function enableTwapCustomRecipient() { + cy.get(twapsAddressToggle).click() +} + export function disableCustomRecipient(option) { if (option) cy.get(recipientToggle).click() } @@ -349,6 +364,7 @@ export function switchToTwap() { cy.get('a').contains(swapStrBtn).click() cy.wait(1000) cy.get('a').contains(twapStrBtn).click() + cy.wait(1000) } export function checkTokenBalanceAndValue(tokenDirection, balance, value) { diff --git a/cypress/e2e/regression/twaps.cy.js b/cypress/e2e/regression/twaps.cy.js index ee918f2776..5b283a8515 100644 --- a/cypress/e2e/regression/twaps.cy.js +++ b/cypress/e2e/regression/twaps.cy.js @@ -10,15 +10,13 @@ const signer = walletCredentials.OWNER_4_PRIVATE_KEY let staticSafes = [] let iframeSelector -// Blocked by a bug on UI -describe.skip('Twaps tests', { defaultCommandTimeout: 30000 }, () => { +describe('Twaps tests', { defaultCommandTimeout: 30000 }, () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) }) beforeEach(() => { cy.visit(constants.swapUrl + staticSafes.SEP_STATIC_SAFE_27) - main.waitForHistoryCallToComplete() wallet.connectSigner(signer) iframeSelector = `iframe[src*="${constants.swapWidget}"]` }) @@ -37,8 +35,12 @@ describe.skip('Twaps tests', { defaultCommandTimeout: 30000 }, () => { swaps.acceptLegalDisclaimer() cy.wait(4000) + main.getIframeBody(iframeSelector).within(() => { swaps.switchToTwap() + }) + swaps.unlockTwapOrders(iframeSelector) + main.getIframeBody(iframeSelector).within(() => { swaps.clickOnTokenSelctor('input') swaps.checkTokenList(tokens) }) @@ -51,7 +53,9 @@ describe.skip('Twaps tests', { defaultCommandTimeout: 30000 }, () => { cy.wait(4000) main.getIframeBody(iframeSelector).within(() => { swaps.switchToTwap() - + }) + swaps.unlockTwapOrders(iframeSelector) + main.getIframeBody(iframeSelector).within(() => { swaps.selectInputCurrency(swaps.swapTokens.cow) swaps.setInputValue(500) swaps.selectOutputCurrency(swaps.swapTokens.dai) @@ -64,6 +68,9 @@ describe.skip('Twaps tests', { defaultCommandTimeout: 30000 }, () => { cy.wait(4000) main.getIframeBody(iframeSelector).within(() => { swaps.switchToTwap() + }) + swaps.unlockTwapOrders(iframeSelector) + main.getIframeBody(iframeSelector).within(() => { swaps.selectInputCurrency(swaps.swapTokens.cow) swaps.clickOnMaxBtn() swaps.checkInputValue('input', '750') diff --git a/cypress/e2e/regression/twaps_2.cy.js b/cypress/e2e/regression/twaps_2.cy.js index 84f1399de1..c37923b049 100644 --- a/cypress/e2e/regression/twaps_2.cy.js +++ b/cypress/e2e/regression/twaps_2.cy.js @@ -10,8 +10,7 @@ const signer = walletCredentials.OWNER_4_PRIVATE_KEY let staticSafes = [] let iframeSelector -// Blocked by a bug on UI -describe.skip('Twaps 2 tests', { defaultCommandTimeout: 30000 }, () => { +describe('Twaps 2 tests', { defaultCommandTimeout: 30000 }, () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) }) @@ -32,6 +31,9 @@ describe.skip('Twaps 2 tests', { defaultCommandTimeout: 30000 }, () => { cy.wait(4000) main.getIframeBody(iframeSelector).within(() => { swaps.switchToTwap() + }) + swaps.unlockTwapOrders(iframeSelector) + main.getIframeBody(iframeSelector).within(() => { swaps.selectInputCurrency(swaps.swapTokens.cow) swaps.setInputValue(2000) swaps.selectOutputCurrency(swaps.swapTokens.dai) @@ -49,6 +51,9 @@ describe.skip('Twaps 2 tests', { defaultCommandTimeout: 30000 }, () => { cy.wait(4000) main.getIframeBody(iframeSelector).within(() => { swaps.switchToTwap() + }) + swaps.unlockTwapOrders(iframeSelector) + main.getIframeBody(iframeSelector).within(() => { swaps.selectInputCurrency(swaps.swapTokens.cow) swaps.setInputValue(100) swaps.selectOutputCurrency(swaps.swapTokens.dai) @@ -74,12 +79,15 @@ describe.skip('Twaps 2 tests', { defaultCommandTimeout: 30000 }, () => { }) .within(() => { swaps.switchToTwap() - swaps.selectInputCurrency(swaps.swapTokens.cow) - swaps.clickOnSettingsBtnTwaps() - swaps.enableCustomRecipient(isCustomRecipientFound(swaps.customRecipient)) - swaps.clickOnSettingsBtnTwaps() - swaps.enterRecipient(swaps.blockedAddress) }) + swaps.unlockTwapOrders(iframeSelector) + main.getIframeBody(iframeSelector).within(() => { + swaps.selectInputCurrency(swaps.swapTokens.cow) + swaps.clickOnSettingsBtnTwaps() + swaps.enableTwapCustomRecipient() + swaps.clickOnSettingsBtnTwaps() + swaps.enterRecipient(swaps.blockedAddress) + }) cy.contains(swaps.blockedAddressStr) }, ) From 9e314183935f9e69b6932cbd08f9cba7cb28eea4 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:22:51 +0000 Subject: [PATCH 43/47] tests: Update test steps (#4591) --- cypress/e2e/regression/balances_pagination.cy.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cypress/e2e/regression/balances_pagination.cy.js b/cypress/e2e/regression/balances_pagination.cy.js index 8b81050e73..fdc658657b 100644 --- a/cypress/e2e/regression/balances_pagination.cy.js +++ b/cypress/e2e/regression/balances_pagination.cy.js @@ -5,12 +5,9 @@ import * as main from '../../e2e/pages/main.page' const ASSETS_LENGTH = 8 describe('Balance pagination tests', () => { - before(() => { + it('Verify a user can change rows per page and navigate to next and previous page', () => { cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_6) assets.selectTokenList(assets.tokenListOptions.allTokens) - }) - - it('Verify a user can change rows per page and navigate to next and previous page', () => { assets.verifyInitialTableState() assets.changeTo10RowsPerPage() assets.verifyTableHas10Rows() From 2bf05141dee3a8f4e46b9ff6671fda580451724f Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:40:51 +0100 Subject: [PATCH 44/47] Chore: speed up dev build (#4592) --- next.config.mjs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/next.config.mjs b/next.config.mjs index b867dc0049..b23c212339 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -47,7 +47,7 @@ const nextConfig = { '@gnosis.pm/zodiac', ], }, - webpack(config) { + webpack(config, { dev }) { config.module.rules.push({ test: /\.svg$/i, issuer: { and: [/\.(js|ts|md)x?$/] }, @@ -79,6 +79,21 @@ const nextConfig = { 'mainnet.json': path.resolve('./node_modules/@ethereumjs/common/dist.browser/genesisStates/mainnet.json'), } + if (dev) { + config.optimization.splitChunks = { + ...config.optimization.splitChunks, + cacheGroups: { + ...config.optimization.splitChunks.cacheGroups, + customModule: { + test: /[\\/]node_modules[\\/](@safe-global|ethers)[\\/]/, + name: 'protocol-kit-ethers', + chunks: 'all', + }, + }, + } + config.optimization.minimize = false + } + return config }, } From 7668262bae05663c319462e66ef30f778d564456 Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Tue, 3 Dec 2024 12:48:58 +0100 Subject: [PATCH 45/47] fix: do not duplicate hex prefixes in `wallet_getCallsStatus` receipts (#4594) --- .../safe-wallet-provider/index.test.ts | 55 ++++++++++++++++++- src/services/safe-wallet-provider/index.ts | 9 ++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/services/safe-wallet-provider/index.test.ts b/src/services/safe-wallet-provider/index.test.ts index cbb86f5c50..3aebe75d9c 100644 --- a/src/services/safe-wallet-provider/index.test.ts +++ b/src/services/safe-wallet-provider/index.test.ts @@ -619,7 +619,60 @@ describe('SafeWalletProvider', () => { }) describe('wallet_getCallsStatus', () => { - it('should return a confirmed transaction', async () => { + it('should return a confirmed transaction if blockNumber/gasUsed are hex', async () => { + const receipt: Pick = { + logs: [], + blockHash: faker.string.hexadecimal(), + // Typed as number/bigint; is hex + blockNumber: faker.string.hexadecimal() as unknown as number, + gasUsed: faker.string.hexadecimal() as unknown as bigint, + } + const sdk = { + getBySafeTxHash: jest.fn().mockResolvedValue({ + txStatus: 'SUCCESS', + txHash: '0x123', + txData: { + dataDecoded: { + parameters: [{ valueDecoded: [1] }], + }, + }, + }), + proxy: jest.fn().mockImplementation((method) => { + if (method === 'eth_getTransactionReceipt') { + return Promise.resolve(receipt) + } + return Promise.reject('Unknown method') + }), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const params = ['0x123'] + + const status = await safeWalletProvider.request(1, { method: 'wallet_getCallsStatus', params } as any, appInfo) + + expect(sdk.getBySafeTxHash).toHaveBeenCalledWith(params[0]) + expect(sdk.proxy).toHaveBeenCalledWith('eth_getTransactionReceipt', params) + expect(status).toStrictEqual({ + id: 1, + jsonrpc: '2.0', + result: { + receipts: [ + { + blockHash: receipt.blockHash, + blockNumber: receipt.blockNumber, + chainId: '0x1', + gasUsed: receipt.gasUsed, + logs: receipt.logs, + status: '0x1', + transactionHash: '0x123', + }, + ], + status: 'CONFIRMED', + }, + }) + }) + + it('should return a confirmed transaction if blockNumber/gasUsed are number/bigint', async () => { const receipt: Pick = { logs: [], blockHash: faker.string.hexadecimal(), diff --git a/src/services/safe-wallet-provider/index.ts b/src/services/safe-wallet-provider/index.ts index 086d835285..9598a23fc9 100644 --- a/src/services/safe-wallet-provider/index.ts +++ b/src/services/safe-wallet-provider/index.ts @@ -403,13 +403,18 @@ export class SafeWalletProvider { } const calls = tx.txData?.dataDecoded?.parameters?.[0].valueDecoded?.length ?? 1 + + // Typed as number; is hex + const blockNumber = Number(receipt.blockNumber) + const gasUsed = Number(receipt.gasUsed) + const receipts = Array.from({ length: calls }, () => ({ logs: receipt.logs, status: numberToHex(tx.txStatus === TransactionStatus.SUCCESS ? 1 : 0), chainId: numberToHex(this.safe.chainId), blockHash: receipt.blockHash as `0x${string}`, - blockNumber: numberToHex(receipt.blockNumber), - gasUsed: numberToHex(receipt.gasUsed), + blockNumber: numberToHex(blockNumber), + gasUsed: numberToHex(gasUsed), transactionHash: tx.txHash as `0x${string}`, })) From 26d95a3115043e8664d3864034fd8f5cdd22d839 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 3 Dec 2024 13:20:20 +0100 Subject: [PATCH 46/47] fix: Keep Sidebar drawer mounted --- src/components/sidebar/Sidebar/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/sidebar/Sidebar/index.tsx b/src/components/sidebar/Sidebar/index.tsx index 0423611e99..adce10f170 100644 --- a/src/components/sidebar/Sidebar/index.tsx +++ b/src/components/sidebar/Sidebar/index.tsx @@ -58,7 +58,13 @@ const Sidebar = (): ReactElement => {
- +
From d87151e83744d81fcd446bb7c7b0d6579cc9497a Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:58:47 +0100 Subject: [PATCH 47/47] Delay initial mount --- src/components/sidebar/Sidebar/index.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/sidebar/Sidebar/index.tsx b/src/components/sidebar/Sidebar/index.tsx index adce10f170..002a40bbe2 100644 --- a/src/components/sidebar/Sidebar/index.tsx +++ b/src/components/sidebar/Sidebar/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState, type ReactElement } from 'react' +import { useCallback, useEffect, useState, type ReactElement } from 'react' import { Box, Divider, Drawer } from '@mui/material' import ChevronRight from '@mui/icons-material/ChevronRight' @@ -14,6 +14,7 @@ import MyAccounts from '@/features/myAccounts' const Sidebar = (): ReactElement => { const [isDrawerOpen, setIsDrawerOpen] = useState(false) + const [modalProps, setModalProps] = useState({ keepMounted: false }) const onDrawerToggle = useCallback(() => { setIsDrawerOpen((isOpen) => { @@ -25,6 +26,13 @@ const Sidebar = (): ReactElement => { const closeDrawer = useCallback(() => setIsDrawerOpen(false), []) + // Mount the drawer 600ms after the page load, and keep it mounted + useEffect(() => { + setTimeout(() => { + setModalProps({ keepMounted: true }) + }, 600) + }, []) + return (
@@ -58,13 +66,7 @@ const Sidebar = (): ReactElement => {
- +