From 0b41172cbae00869c0fcc7c6c24c03974671bdbd Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Tue, 14 May 2024 15:39:29 +0200 Subject: [PATCH 001/154] Tests: fix failing tests (#3704) --- .../e2e/happypath/sendfunds_connected_wallet.cy.js | 2 +- cypress/e2e/smoke/create_tx.cy.js | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js index 330119f9f5..c465ead4b1 100644 --- a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js +++ b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js @@ -14,7 +14,7 @@ import { contracts, abi_qtrust, abi_nft_pc2 } from '../../support/api/contracts' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' const safeBalanceEth = 305220000000000000n -const qtrustBanance = 93000000000000000025n +const qtrustBanance = 95000000000000000025n const transferAmount = '1' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) diff --git a/cypress/e2e/smoke/create_tx.cy.js b/cypress/e2e/smoke/create_tx.cy.js index fda40e71fd..ebc2735dee 100644 --- a/cypress/e2e/smoke/create_tx.cy.js +++ b/cypress/e2e/smoke/create_tx.cy.js @@ -57,19 +57,25 @@ describe('[SMOKE] Create transactions tests', () => { createtx.verifyTooltipMessage(constants.nonceTooltipMsg.muchHigherThanRecommended) }) - it.skip('[SMOKE] Verify advance parameters gas limit input', () => { + it('[SMOKE] Verify advance parameters gas limit input', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_6) + createtx.clickOnNewtransactionBtn() + createtx.clickOnSendTokensBtn() happyPathToStepTwo() - createtx.changeNonce(currentNonce) + createtx.changeNonce('1') createtx.selectCurrentWallet() createtx.openExecutionParamsModal() createtx.verifyAndSubmitExecutionParams() }) - it.skip('[SMOKE] Verify a transaction shows relayer and addToBatch button', () => { + it('[SMOKE] Verify a transaction shows relayer and addToBatch button', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_6) + createtx.clickOnNewtransactionBtn() + createtx.clickOnSendTokensBtn() happyPathToStepTwo() createtx.verifySubmitBtnIsEnabled() createtx.verifyNativeTokenTransfer() - createtx.changeNonce(currentNonce) + createtx.changeNonce('1') createtx.verifyConfirmTransactionData() createtx.verifyRelayerAttemptsAvailable() createtx.selectCurrentWallet() From 1dae49292157e6ee9c2cddea84a85fe5a47f1946 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Tue, 14 May 2024 15:59:54 +0200 Subject: [PATCH 002/154] refactor: Remove safe token paused logic (#3703) --- .../TokenTransfer/CreateTokenTransfer.tsx | 29 ++--------- .../__tests__/CreateTokenTransfer.test.tsx | 48 ++----------------- src/hooks/useIsSafeTokenPaused.ts | 34 ------------- 3 files changed, 7 insertions(+), 104 deletions(-) delete mode 100644 src/hooks/useIsSafeTokenPaused.ts diff --git a/src/components/tx-flow/flows/TokenTransfer/CreateTokenTransfer.tsx b/src/components/tx-flow/flows/TokenTransfer/CreateTokenTransfer.tsx index f426f72876..4b5c18b3f0 100644 --- a/src/components/tx-flow/flows/TokenTransfer/CreateTokenTransfer.tsx +++ b/src/components/tx-flow/flows/TokenTransfer/CreateTokenTransfer.tsx @@ -1,16 +1,11 @@ import { useTokenAmount, useVisibleTokens } from '@/components/tx-flow/flows/TokenTransfer/utils' -import madProps from '@/utils/mad-props' import { type ReactElement, useContext, useEffect } from 'react' import { type TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { useSafeTokenAddress } from '@/components/common/SafeTokenWidget' -import useIsSafeTokenPaused from '@/hooks/useIsSafeTokenPaused' import useIsOnlySpendingLimitBeneficiary from '@/hooks/useIsOnlySpendingLimitBeneficiary' import { FormProvider, useForm } from 'react-hook-form' -import { sameAddress } from '@/utils/addresses' -import { Box, Button, CardActions, Divider, FormControl, Grid, SvgIcon, Typography } from '@mui/material' +import { Button, CardActions, Divider, FormControl, Grid, Typography } from '@mui/material' import TokenIcon from '@/components/common/TokenIcon' import AddressBookInput from '@/components/common/AddressBookInput' -import InfoIcon from '@/public/images/notifications/info.svg' import SpendingLimitRow from '@/components/tx-flow/flows/TokenTransfer/SpendingLimitRow' import { TokenTransferFields, type TokenTransferParams, TokenTransferType } from '.' import TxCard from '../../common/TxCard' @@ -38,14 +33,10 @@ export const AutocompleteItem = (item: { tokenInfo: TokenInfo; balance: string } export const CreateTokenTransfer = ({ params, onSubmit, - isSafeTokenPaused, - safeTokenAddress, txNonce, }: { params: TokenTransferParams onSubmit: (data: TokenTransferParams) => void - isSafeTokenPaused: ReturnType - safeTokenAddress?: ReturnType txNonce?: number }): ReactElement => { const disableSpendingLimit = txNonce !== undefined @@ -92,8 +83,6 @@ export const CreateTokenTransfer = ({ const maxAmount = isSpendingLimitType && totalAmount > spendingLimitAmount ? spendingLimitAmount : totalAmount - const isSafeTokenSelected = sameAddress(safeTokenAddress, tokenAddress) - const isDisabled = isSafeTokenSelected && isSafeTokenPaused const isAddressValid = !!recipient && !errors[TokenTransferFields.recipient] useEffect(() => { @@ -110,15 +99,6 @@ export const CreateTokenTransfer = ({ - {isDisabled && ( - - - - $SAFE is currently non-transferable. - - - )} - {!disableSpendingLimit && spendingLimitAmount > 0n && ( @@ -128,7 +108,7 @@ export const CreateTokenTransfer = ({ - @@ -138,7 +118,4 @@ export const CreateTokenTransfer = ({ ) } -export default madProps(CreateTokenTransfer, { - safeTokenAddress: useSafeTokenAddress, - isSafeTokenPaused: useIsSafeTokenPaused, -}) +export default CreateTokenTransfer diff --git a/src/components/tx-flow/flows/TokenTransfer/__tests__/CreateTokenTransfer.test.tsx b/src/components/tx-flow/flows/TokenTransfer/__tests__/CreateTokenTransfer.test.tsx index 01c52de3bd..aa5bf407ec 100644 --- a/src/components/tx-flow/flows/TokenTransfer/__tests__/CreateTokenTransfer.test.tsx +++ b/src/components/tx-flow/flows/TokenTransfer/__tests__/CreateTokenTransfer.test.tsx @@ -17,69 +17,29 @@ describe('CreateTokenTransfer', () => { }) it('should display a token amount input', () => { - const { getByText } = render( - , - ) + const { getByText } = render() expect(getByText('Amount')).toBeInTheDocument() }) it('should display a recipient input', () => { - const { getAllByText } = render( - , - ) + const { getAllByText } = render() expect(getAllByText('Recipient address')[0]).toBeInTheDocument() }) - it('should disable the submit button and display a warning if $SAFE token is selected and not transferable', () => { - const { getByText } = render( - , - ) - - const button = getByText('Next') - - expect(getByText('$SAFE is currently non-transferable.')).toBeInTheDocument() - expect(button).toBeDisabled() - }) - - it('should enable the submit button if $SAFE token is selected and transferable', () => { - const { queryByText, getByText } = render( - , - ) - - const button = getByText('Next') - - expect(queryByText('$SAFE is currently non-transferable.')).not.toBeInTheDocument() - expect(button).not.toBeDisabled() - }) - it('should display a type selection if a spending limit token is selected', () => { jest .spyOn(tokenUtils, 'useTokenAmount') .mockReturnValue({ totalAmount: BigInt(1000), spendingLimitAmount: BigInt(500) }) - const { getByText } = render( - , - ) + const { getByText } = render() expect(getByText('Send as')).toBeInTheDocument() }) it('should not display a type selection if there is a txNonce', () => { - const { queryByText } = render( - , - ) + const { queryByText } = render() expect(queryByText('Send as')).not.toBeInTheDocument() }) diff --git a/src/hooks/useIsSafeTokenPaused.ts b/src/hooks/useIsSafeTokenPaused.ts deleted file mode 100644 index 5f6b2b3412..0000000000 --- a/src/hooks/useIsSafeTokenPaused.ts +++ /dev/null @@ -1,34 +0,0 @@ -import useChainId from '@/hooks/useChainId' -import { getSafeTokenAddress } from '@/components/common/SafeTokenWidget' -import { useWeb3ReadOnly } from '@/hooks/wallets/web3' -import useAsync from '@/hooks/useAsync' -import { Contract, Interface } from 'ethers' - -const PAUSED_ABI = 'function paused() public view virtual returns (bool)' - -// TODO: Remove this hook after the safe token has been unpaused -const useIsSafeTokenPaused = () => { - const chainId = useChainId() - const provider = useWeb3ReadOnly() - - const [isSafeTokenPaused] = useAsync(async () => { - const safeTokenAddress = getSafeTokenAddress(chainId) - - if (!safeTokenAddress) return false - - const safeTokenContract = new Contract(safeTokenAddress, new Interface([PAUSED_ABI]), provider) - - let isPaused: boolean - try { - isPaused = await safeTokenContract.paused() - } catch (err) { - isPaused = false - } - - return isPaused - }, [chainId, provider]) - - return isSafeTokenPaused -} - -export default useIsSafeTokenPaused From be9294b3c8146d22925b90ffc3e25d103e02eace Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Tue, 14 May 2024 19:51:55 +0200 Subject: [PATCH 003/154] Chore: Slim Down Docker Image (#3707) When playing around with the Safe web interface I noticed that the Docker image that it produces is **over 6GB in size**! This PR slims down the docker image significantly by using a multistage build where the first stage builds the static website, and the actual image just hosts the static webside (here using BusyBox `httpd` which is nice and lightweight). This gets the image down to around 72MB, around 1% of the original image size _(sizes computed from Docker images built on amd64 Linux)_. Note that there is one weird detail is that static HTTP servers typically don't have support for automatically adding `.html` endings to URL paths. It was worked around here with symlinks. See comment in the Dockerfile for more details. ``` $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE localhost/safe-web small 2bb37df44d9f 14 minutes ago 71.9 MB localhost/safe-web big ae2940e99e19 3 hours ago 6.52 GB ``` --- Dockerfile | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 101be17060..e3e53fcde3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM node:18-alpine +FROM node:18-alpine AS build + RUN apk add --no-cache libc6-compat git python3 py3-pip make g++ libusb-dev eudev-dev linux-headers WORKDIR /app COPY . . @@ -17,8 +18,22 @@ ENV NODE_ENV production # Uncomment the following line in case you want to disable telemetry during the build. ENV NEXT_TELEMETRY_DISABLED 1 +RUN yarn build + +FROM alpine + +RUN apk add busybox-extras tini +WORKDIR /www +COPY --from=build /app/out /www + +# Next.js automatically adds `.html` extensions to URLs, but static HTTP file +# servers don't generally do this. You can work around this by creating symbolic +# links from `my-page.html` to either `my-page/index.html` or `my-page` +# depending on whether or not a `my-page` directory exists. +RUN find /www -name '*.html' -exec sh -c 'f="{}"; b="${f%.*}"; [ -d "$b" ] && ln -s "$f" "$b/index.html" || ln -s "$f" "$b"' ';' + EXPOSE 3000 ENV PORT 3000 -CMD ["yarn", "static-serve"] +CMD ["tini", "--", "busybox-extras", "httpd", "-fvv", "-h", "/www", "-p", "0.0.0.0:3000"] From d4b77b4742c2b16161b77a3d00c32b1f72ecaedd Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Wed, 15 May 2024 16:04:04 +0200 Subject: [PATCH 004/154] Tests: update message tests (#3715) * tests: update message tests * tests: disable eslint for localstorage file * tests: update test titles --- cypress/e2e/pages/create_tx.pages.js | 2 +- cypress/e2e/pages/main.page.js | 4 ++ cypress/e2e/pages/messages.pages.js | 14 ++++ cypress/e2e/pages/modals.page.js | 2 + .../modals/message_confirmation.pages.js | 46 ++++++++++++ cypress/e2e/pages/safeapps.pages.js | 11 +++ .../e2e/regression/messages_offchain.cy.js | 16 ++++- cypress/e2e/regression/messages_popup.cy.js | 72 +++++++++++++++++++ cypress/support/constants.js | 5 ++ cypress/support/localstorage_data.js | 21 ++++++ cypress/support/utils/checkers.js | 4 ++ .../safe-messages/InfoBox/index.tsx | 2 +- .../tx-flow/flows/SignMessage/SignMessage.tsx | 6 +- 13 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 cypress/e2e/pages/messages.pages.js create mode 100644 cypress/e2e/pages/modals/message_confirmation.pages.js create mode 100644 cypress/e2e/regression/messages_popup.cy.js create mode 100644 cypress/support/utils/checkers.js diff --git a/cypress/e2e/pages/create_tx.pages.js b/cypress/e2e/pages/create_tx.pages.js index 553c77861c..49dd8f99eb 100644 --- a/cypress/e2e/pages/create_tx.pages.js +++ b/cypress/e2e/pages/create_tx.pages.js @@ -31,7 +31,7 @@ const spamTokenWarningIcon = '[data-testid="warning"]' const untrustedTokenWarningModal = '[data-testid="untrusted-token-warning"]' const sendTokensBtn = '[data-testid="send-tokens-btn"]' export const replacementNewSigner = '[data-testid="new-owner"]' -const messageItem = '[data-testid="message-item"]' +export const messageItem = '[data-testid="message-item"]' const viewTransactionBtn = 'View transaction' const transactionDetailsTitle = 'Transaction details' diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index 9494cb9a8c..b8547b1318 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -300,3 +300,7 @@ export function verifyTextVisibility(stringsArray) { cy.contains(string).should('be.visible') }) } + +export function getIframeBody(iframe) { + return cy.get(iframe).its('0.contentDocument.body').should('not.be.empty').then(cy.wrap) +} diff --git a/cypress/e2e/pages/messages.pages.js b/cypress/e2e/pages/messages.pages.js new file mode 100644 index 0000000000..bc6052b32d --- /dev/null +++ b/cypress/e2e/pages/messages.pages.js @@ -0,0 +1,14 @@ +import { messageItem } from './create_tx.pages' +const onchainMsgInput = 'input[placeholder*="Message"]' + +export function enterOnchainMessage(msg) { + cy.get(onchainMsgInput).type(msg) +} + +export function clickOnMessageSignBtn(index) { + cy.get(messageItem) + .eq(index) + .within(() => { + cy.get('button').contains('Sign').click() + }) +} diff --git a/cypress/e2e/pages/modals.page.js b/cypress/e2e/pages/modals.page.js index 1e8063405a..d2b99a68e2 100644 --- a/cypress/e2e/pages/modals.page.js +++ b/cypress/e2e/pages/modals.page.js @@ -5,6 +5,8 @@ export const modalTitiles = { editEntry: 'Edit entry', deleteEntry: 'Delete entry', dataImport: 'Data import', + confirmTx: 'Confirm transaction', + confirmMsg: 'Confirm message', } export function verifyModalTitle(title) { diff --git a/cypress/e2e/pages/modals/message_confirmation.pages.js b/cypress/e2e/pages/modals/message_confirmation.pages.js new file mode 100644 index 0000000000..67d78c4c56 --- /dev/null +++ b/cypress/e2e/pages/modals/message_confirmation.pages.js @@ -0,0 +1,46 @@ +import * as modal from '../modals.page' +import * as checkers from '../../../support/utils/checkers' +import * as main from '../main.page' + +const messageHash = '[data-testid="message-hash"]' +const messageDetails = '[data-testid="message-details"]' +const messageInfobox = '[data-testid="message-infobox"]' + +const messageInfoBoxData = [ + 'Collect all the confirmations', + 'Confirmations (1 of 2)', + 'The signature will be submitted to the Safe App when the message is fully signed', +] + +export function verifyConfirmationWindowTitle(title) { + cy.get(modal.modalTitle).should('contain', title) +} + +export function verifyMessagePresent(msg) { + cy.get('textarea').should('contain', msg) +} + +export function verifySafeAppInPopupWindow(safeApp) { + cy.contains(safeApp) +} + +export function verifyOffchainMessageHash(index) { + cy.get(messageHash) + .eq(index) + .invoke('text') + .then((text) => { + if (!checkers.startsWith0x(text)) { + throw new Error(`Message at index ${index} does not start with '0x': ${text}`) + } + }) +} + +export function checkMessageInfobox() { + cy.get(messageInfobox).within(() => { + main.verifyTextVisibility(messageInfoBoxData) + }) +} + +export function clickOnMessageDetails() { + cy.get(messageDetails).click() +} diff --git a/cypress/e2e/pages/safeapps.pages.js b/cypress/e2e/pages/safeapps.pages.js index b7d1238f5d..9c9c3e84d4 100644 --- a/cypress/e2e/pages/safeapps.pages.js +++ b/cypress/e2e/pages/safeapps.pages.js @@ -89,6 +89,8 @@ export const transferStr = 'Transfer' export const successStr = 'Success' export const failedStr = 'Failed' +export const dummyTxStr = 'Trigger dummy tx (safe.txs.send)' +export const signOnchainMsgStr = 'Sign message (on-chain)' export const pinWalletConnectStr = /pin walletconnect/i export const transactionBuilderStr = 'Transaction Builder' export const testAddressValueStr = 'testAddressValue' @@ -126,6 +128,14 @@ export const permissionCheckboxNames = { fullscreen: 'Fullscreen', } +export function triggetOffChainTx() { + cy.contains(dummyTxStr).click() +} + +export function triggetOnChainTx() { + cy.contains(signOnchainMsgStr).click() +} + export function verifyWarningDefaultAppMsgIsDisplayed() { cy.get('p').contains(warningDefaultAppStr).should('be.visible') cy.wait(1000) @@ -145,6 +155,7 @@ export function verifyLinkName(name) { export function clickOnApp(app) { cy.contains(app).click() + cy.wait(2000) } export function verifyNoAppsTextPresent() { diff --git a/cypress/e2e/regression/messages_offchain.cy.js b/cypress/e2e/regression/messages_offchain.cy.js index 62077ced58..28bfacda76 100644 --- a/cypress/e2e/regression/messages_offchain.cy.js +++ b/cypress/e2e/regression/messages_offchain.cy.js @@ -3,8 +3,12 @@ import * as main from '../pages/main.page.js' import * as createTx from '../pages/create_tx.pages.js' import * as msg_data from '../../fixtures/txmessages_data.json' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as modal from '../pages/modals.page' +import * as messages from '../pages/messages.pages.js' +import * as msg_confirmation_modal from '../pages/modals/message_confirmation.pages.js' let staticSafes = [] +const offchainMessage = 'Test message 2 off-chain' const typeMessagesGeneral = msg_data.type.general const typeMessagesOffchain = msg_data.type.offChain @@ -46,7 +50,7 @@ describe('Offchain Messages tests', () => { ) }) - it('Verify exapanded details for simple off-chain message', () => { + it('Verify exapanded details for EIP 191 off-chain message', () => { createTx.clickOnTransactionItemByIndex(2) cy.contains(typeMessagesOffchain.message2).should('be.visible') }) @@ -71,4 +75,14 @@ describe('Offchain Messages tests', () => { main.verifyTextVisibility(values) }) + + it('Verify confirmation window is displayed for unsigned message', () => { + messages.clickOnMessageSignBtn(2) + msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmMsg) + msg_confirmation_modal.verifyMessagePresent(offchainMessage) + msg_confirmation_modal.clickOnMessageDetails() + msg_confirmation_modal.verifyOffchainMessageHash(0) + msg_confirmation_modal.verifyOffchainMessageHash(1) + msg_confirmation_modal.checkMessageInfobox() + }) }) diff --git a/cypress/e2e/regression/messages_popup.cy.js b/cypress/e2e/regression/messages_popup.cy.js new file mode 100644 index 0000000000..399d41dc29 --- /dev/null +++ b/cypress/e2e/regression/messages_popup.cy.js @@ -0,0 +1,72 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as modal from '../pages/modals.page.js' +import * as apps from '../pages/safeapps.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as ls from '../../support/localstorage_data.js' +import * as messages from '../pages/messages.pages.js' +import * as msg_confirmation_modal from '../pages/modals/message_confirmation.pages.js' + +let staticSafes = [] +const safeApp = 'Safe Test App' +const onchainMessage = 'Message 1' +let iframeSelector + +describe('Messages popup window tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.appsCustomUrl + staticSafes.SEP_STATIC_SAFE_10) + main.acceptCookies() + iframeSelector = `iframe[id="iframe-${constants.safeTestAppurl}"]` + }) + + it('Verify off-chain message popup window can be triggered', () => { + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__customSafeApps_11155111, + ls.customApps(constants.safeTestAppurl).safeTestApp, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__browserPermissions, + ls.appPermissions(constants.safeTestAppurl).grantedPermissions, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__infoModal, + ls.appPermissions(constants.safeTestAppurl).infoModalAccepted, + ) + cy.reload() + apps.clickOnApp(safeApp) + main.getIframeBody(iframeSelector).within(() => { + apps.triggetOffChainTx() + }) + msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmTx) + msg_confirmation_modal.verifySafeAppInPopupWindow(safeApp) + }) + + it('Verify on-chain message popup window can be triggered', () => { + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__customSafeApps_11155111, + ls.customApps(constants.safeTestAppurl).safeTestApp, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__browserPermissions, + ls.appPermissions(constants.safeTestAppurl).grantedPermissions, + ) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__infoModal, + ls.appPermissions(constants.safeTestAppurl).infoModalAccepted, + ) + cy.reload() + apps.clickOnApp(safeApp) + main.getIframeBody(iframeSelector).within(() => { + messages.enterOnchainMessage(onchainMessage) + apps.triggetOnChainTx() + }) + msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmMsg) + msg_confirmation_modal.verifySafeAppInPopupWindow(safeApp) + msg_confirmation_modal.verifyMessagePresent(onchainMessage) + }) +}) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index dc60748283..1b9fec42e6 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -38,11 +38,13 @@ export const goerlySafeName = /g(ö|oe)rli-safe/ export const sepoliaSafeName = 'sepolia-safe' export const goerliToken = /G(ö|oe)rli Ether/ +export const safeTestAppurl = 'https://safe-apps-test-app.pages.dev' export const TX_Builder_url = 'https://safe-apps.dev.5afe.dev/tx-builder' export const drainAccount_url = 'https://safe-apps.dev.5afe.dev/drain-safe' export const testAppUrl = 'https://safe-test-app.com' export const addressBookUrl = '/address-book?safe=' export const appsUrlGeneral = '/apps?=safe=' +export const appsCustomUrl = 'apps/custom?safe=' export const BALANCE_URL = '/balances?safe=' export const balanceNftsUrl = '/balances/nfts?safe=' export const transactionQueueUrl = '/transactions/queue?safe=' @@ -230,6 +232,9 @@ export const localStorageKeys = { SAFE_v2__safeApps: 'SAFE_v2__safeApps', SAFE_v2__cookies: 'SAFE_v2__cookies', SAFE_v2__tokenlist_onboarding: 'SAFE_v2__tokenlist_onboarding', + SAFE_v2__customSafeApps_11155111: 'SAFE_v2__customSafeApps-11155111', + SAFE_v2__SafeApps__browserPermissions: 'SAFE_v2__SafeApps__browserPermissions', + SAFE_v2__SafeApps__infoModal: 'SAFE_v2__SafeApps__infoModal', } export const connectWalletNames = { diff --git a/cypress/support/localstorage_data.js b/cypress/support/localstorage_data.js index 6eb629a971..7972fa2db0 100644 --- a/cypress/support/localstorage_data.js +++ b/cypress/support/localstorage_data.js @@ -1,3 +1,4 @@ +/* eslint-disable */ export const batchData = { entry0: { 11155111: { @@ -631,6 +632,26 @@ export const pinnedApps = { transactionBuilder: { 11155111: { pinned: [24], opened: [] } }, } +export const customApps = (url) => ({ + safeTestApp: [{ url: url }], + grantedPermissions: { + [url]: [ + { feature: 'camera', status: 'granted' }, + { feature: 'microphone', status: 'granted' }, + ], + }, +}) + +export const appPermissions = (url) => ({ + grantedPermissions: { + [url]: [ + { feature: 'camera', status: 'granted' }, + { feature: 'microphone', status: 'granted' }, + ], + }, + infoModalAccepted: { 11155111: { consentsAccepted: true, warningCheckedCustomApps: [] } }, +}) + export const cookies = { acceptedCookies: { necessary: true, updates: true, analytics: true }, acceptedTokenListOnboarding: true, diff --git a/cypress/support/utils/checkers.js b/cypress/support/utils/checkers.js new file mode 100644 index 0000000000..112e11b096 --- /dev/null +++ b/cypress/support/utils/checkers.js @@ -0,0 +1,4 @@ +export function startsWith0x(str) { + const pattern = /^0x/ + return pattern.test(str) +} diff --git a/src/components/safe-messages/InfoBox/index.tsx b/src/components/safe-messages/InfoBox/index.tsx index 21a22a5c6c..cc7b3d5fa0 100644 --- a/src/components/safe-messages/InfoBox/index.tsx +++ b/src/components/safe-messages/InfoBox/index.tsx @@ -16,7 +16,7 @@ const InfoBox = ({ className?: string }): ReactElement => { return ( -
+
diff --git a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx index 0149fbb2df..e0aebcf1cb 100644 --- a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx +++ b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx @@ -79,7 +79,7 @@ const MessageHashField = ({ label, hashValue }: { label: string; hashValue: stri {label}: - + @@ -302,7 +302,9 @@ const SignMessage = ({ message, safeAppId, requestId }: ProposeProps | ConfirmPr - }>SafeMessage details + }> + SafeMessage details + From b6f7ac2053b5e4cdf41435e6e1b401993c47efb9 Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Wed, 15 May 2024 18:28:45 +0200 Subject: [PATCH 005/154] Revert Slim Down Docker Image (#3707) (#3709) This reverts PR #3707 as the CI build appears to be failing. I will investigate and try to make a follow up PR that fixes the issue. --- Dockerfile | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index e3e53fcde3..101be17060 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ -FROM node:18-alpine AS build - +FROM node:18-alpine RUN apk add --no-cache libc6-compat git python3 py3-pip make g++ libusb-dev eudev-dev linux-headers WORKDIR /app COPY . . @@ -18,22 +17,8 @@ ENV NODE_ENV production # Uncomment the following line in case you want to disable telemetry during the build. ENV NEXT_TELEMETRY_DISABLED 1 -RUN yarn build - -FROM alpine - -RUN apk add busybox-extras tini -WORKDIR /www -COPY --from=build /app/out /www - -# Next.js automatically adds `.html` extensions to URLs, but static HTTP file -# servers don't generally do this. You can work around this by creating symbolic -# links from `my-page.html` to either `my-page/index.html` or `my-page` -# depending on whether or not a `my-page` directory exists. -RUN find /www -name '*.html' -exec sh -c 'f="{}"; b="${f%.*}"; [ -d "$b" ] && ln -s "$f" "$b/index.html" || ln -s "$f" "$b"' ';' - EXPOSE 3000 ENV PORT 3000 -CMD ["tini", "--", "busybox-extras", "httpd", "-fvv", "-h", "/www", "-p", "0.0.0.0:3000"] +CMD ["yarn", "static-serve"] From 830cd7a4b5f0d36dfeb017572cadb1b9c4ba38d1 Mon Sep 17 00:00:00 2001 From: 0xhappyleow <58267901+happyleow@users.noreply.github.com> Date: Thu, 16 May 2024 15:10:30 +0800 Subject: [PATCH 006/154] chore: upgrade-safe-deployments (#3719) --- package.json | 8 +++---- yarn.lock | 61 +++++++++++++++++----------------------------------- 2 files changed, 24 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 7b136c7170..91bd6c5564 100644 --- a/package.json +++ b/package.json @@ -56,10 +56,10 @@ "@mui/material": "^5.14.20", "@mui/x-date-pickers": "^5.0.20", "@reduxjs/toolkit": "^1.9.5", - "@safe-global/api-kit": "^2.3.0", - "@safe-global/protocol-kit": "^3.1.0", + "@safe-global/api-kit": "^2.3.2", + "@safe-global/protocol-kit": "^3.1.1", "@safe-global/safe-apps-sdk": "^9.0.0-next.1", - "@safe-global/safe-deployments": "^1.35.0", + "@safe-global/safe-deployments": "^1.36.0", "@safe-global/safe-gateway-typescript-sdk": "3.21.1", "@safe-global/safe-modules-deployments": "^1.2.0", "@sentry/react": "^7.91.0", @@ -106,7 +106,7 @@ "@faker-js/faker": "^8.1.0", "@next/bundle-analyzer": "^13.5.6", "@openzeppelin/contracts": "^4.9.2", - "@safe-global/safe-core-sdk-types": "^4.0.2", + "@safe-global/safe-core-sdk-types": "^4.1.1", "@sentry/types": "^7.74.0", "@storybook/addon-designs": "^8.0.0", "@storybook/addon-essentials": "^8.0.6", diff --git a/yarn.lock b/yarn.lock index 119fabd656..740cbdb140 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4237,37 +4237,23 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9" integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== -"@safe-global/api-kit@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@safe-global/api-kit/-/api-kit-2.3.0.tgz#fc7e741a25cb5983ce7e7ccf0f2fd2ef64bbe704" - integrity sha512-Mh9HH9+TK4dz5tiuPLn/GK42cOf1lKGvcmH/JQK8b7gD+YJugnXdrsirs3XahvwdG0rG7xfL9tf4mcW7zWzuXQ== +"@safe-global/api-kit@^2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@safe-global/api-kit/-/api-kit-2.3.2.tgz#37cab964faf79f98eb5f1a86004a56c0fb0eef21" + integrity sha512-pnbP23t3XQOWZv5Y35CoDpBAajSeB0ZY9ahajruNYt6pyPYxleQVuRueCQ+FDZHNDGS0SK7dlUYyUScs/GsTZQ== dependencies: - "@safe-global/protocol-kit" "^3.0.2" - "@safe-global/safe-core-sdk-types" "^4.0.2" + "@safe-global/protocol-kit" "^3.1.1" + "@safe-global/safe-core-sdk-types" "^4.1.1" ethers "^6.7.1" node-fetch "^2.7.0" -"@safe-global/protocol-kit@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@safe-global/protocol-kit/-/protocol-kit-3.0.2.tgz#cfa4c5e890c101aa89e11768d07b2bc5455f72fb" - integrity sha512-Jxvvfu4yqEdWeOuY3VWOOs/q5f27om3tctL2guOCDbAuSx3Vd1peaKRwLiREkvrrqKEW0tfmzUSsqtmlJExfBw== - dependencies: - "@noble/hashes" "^1.3.3" - "@safe-global/safe-deployments" "^1.34.0" - ethereumjs-util "^7.1.5" - ethers "^6.7.1" - semver "^7.5.4" - web3 "^1.10.3" - web3-core "^1.10.3" - web3-utils "^1.10.3" - -"@safe-global/protocol-kit@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@safe-global/protocol-kit/-/protocol-kit-3.1.0.tgz#429f3f11b3b2c60ed31568f853393155f316316b" - integrity sha512-PUZgTohIoQ1KN7RYE2IcQL8lj/LAb4WRgkDwB+1Vv7AoOLTI1U0Whajfe6Ur9w35BrQwr/x2HAoQvVSzH4FZ3Q== +"@safe-global/protocol-kit@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@safe-global/protocol-kit/-/protocol-kit-3.1.1.tgz#eec3cee3432cd1ad8b159e1911527d93ecc502f9" + integrity sha512-ai/N1DI6U53CsC46Do8eOyb6IkgVhjoQFhbFIh5rFSAKiuw3B0hTF7nrVRb0jw4NFlNHCcWDAER/uNH0Qy2Pkg== dependencies: "@noble/hashes" "^1.3.3" - "@safe-global/safe-deployments" "^1.35.0" + "@safe-global/safe-deployments" "^1.36.0" ethereumjs-util "^7.1.5" ethers "^6.7.1" semver "^7.5.4" @@ -4283,27 +4269,20 @@ "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" viem "^1.6.0" -"@safe-global/safe-core-sdk-types@^4.0.2": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@safe-global/safe-core-sdk-types/-/safe-core-sdk-types-4.0.2.tgz#70a99d8d61b83db458124c263391d86e5d6d5057" - integrity sha512-3I60xV/BLPiBtc3nGp2itgiDL+YbMI9OwaANvnJL2AwSS1kc2kH3/SsCwAW3s4Usr3b0lE08aO7I9ropyxFHhA== +"@safe-global/safe-core-sdk-types@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@safe-global/safe-core-sdk-types/-/safe-core-sdk-types-4.1.1.tgz#b75e15a2e6fa50f131629ca6f6f3a98fd6037d30" + integrity sha512-5NIWG7OjV+C5iquux0yPcu8SHUzg1qJXJ/jAQcPwXGTC7ZVsFawnR43/l2Vg9zEwf0RF0xTm3W8DXkaBYORiEQ== dependencies: - "@safe-global/safe-deployments" "^1.34.0" + "@safe-global/safe-deployments" "^1.36.0" ethers "^6.7.1" web3-core "^1.10.3" web3-utils "^1.10.3" -"@safe-global/safe-deployments@^1.34.0": - version "1.34.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-deployments/-/safe-deployments-1.34.0.tgz#5eef33012a4af55c4440036b1c0cfdb2245c6e49" - integrity sha512-J55iHhB1tiNoPeVQ5s943zrfeKRYPqBtnz/EM7d878WzUmmDlTGKHN98qPYKBxkRKP1UjEWuQDrZxy80lx1rJw== - dependencies: - semver "^7.3.7" - -"@safe-global/safe-deployments@^1.35.0": - version "1.35.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-deployments/-/safe-deployments-1.35.0.tgz#6930a86a006526a9791ebd2a11cf8f5d8358563b" - integrity sha512-Of8WQEcvL5Fm+xxnCDjah6Hkw+sNdzcApQnzr+OsPBxYtZL0RRtbmesypj36oOD8BQmyrH54V8DVN+pYjrfJ9g== +"@safe-global/safe-deployments@^1.36.0": + version "1.36.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-deployments/-/safe-deployments-1.36.0.tgz#7e5cd470cc1042182d47f65b59831a64ed8feff1" + integrity sha512-9MbDJveRR64AbmzjIpuUqmDBDtOZpXpvkyhTUs+5UOPT3WgSO375/ZTO7hZpywP7+EmxnjkGc9EoxjGcC4TAyw== dependencies: semver "^7.6.0" From ecde7fd6185b495ccd0dc8cf50638da1952a89be Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Thu, 16 May 2024 13:16:12 +0200 Subject: [PATCH 007/154] fix: always fetch message info when signing (#3724) --------- Co-authored-by: katspaugh <381895+katspaugh@users.noreply.github.com> --- .../flows/SignMessage/SignMessage.test.tsx | 139 ++++++++++++++---- .../tx-flow/flows/SignMessage/index.tsx | 5 +- src/hooks/messages/useSafeMessage.ts | 16 +- .../messages/useSyncSafeMessageSigner.ts | 2 +- 4 files changed, 130 insertions(+), 32 deletions(-) diff --git a/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx b/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx index 9922e32a65..7a16ad5081 100644 --- a/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx +++ b/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx @@ -8,7 +8,6 @@ import * as useIsWrongChainHook from '@/hooks/useIsWrongChain' import * as useIsSafeOwnerHook from '@/hooks/useIsSafeOwner' import * as useWalletHook from '@/hooks/wallets/useWallet' import * as useSafeInfoHook from '@/hooks/useSafeInfo' -import * as useAsyncHook from '@/hooks/useAsync' import * as useChainsHook from '@/hooks/useChains' import * as sender from '@/services/safe-messages/safeMsgSender' import * as onboard from '@/hooks/wallets/useOnboard' @@ -220,8 +219,7 @@ describe('SignMessage', () => { it('proposes a message if not already proposed', async () => { jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => true) jest.spyOn(onboard, 'default').mockReturnValue(mockOnboard) - - jest.spyOn(useAsyncHook, 'default').mockReturnValue([undefined, new Error('SafeMessage not found'), false]) + ;(getSafeMessage as jest.Mock).mockRejectedValue(new Error('SafeMessage not found')) const { getByText, baseElement } = render( { mockUseSafeMessages.mockReturnValue(msgs) jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => true) - - jest.spyOn(useAsyncHook, 'default').mockReturnValue([undefined, new Error('SafeMessage not found'), false]) + ;(getSafeMessage as jest.Mock).mockRejectedValue(new Error('SafeMessage not found')) const proposalSpy = jest .spyOn(sender, 'dispatchSafeMsgProposal') @@ -553,6 +550,38 @@ describe('SignMessage', () => { it('displays an error if the message could not be confirmed', async () => { jest.spyOn(onboard, 'default').mockReturnValue(mockOnboard) jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => true) + jest.spyOn(useWalletHook, 'default').mockImplementation( + () => + ({ + address: zeroPadValue('0x03', 20), + } as ConnectedWallet), + ) + + const messageText = 'Hello world!' + const messageHash = generateSafeMessageHash( + { + version: '1.3.0', + address: { + value: zeroPadValue('0x01', 20), + }, + chainId: '5', + } as SafeInfo, + messageText, + ) + const msg = { + type: SafeMessageListItemType.MESSAGE, + messageHash, + confirmations: [ + { + owner: { + value: zeroPadValue('0x02', 20), + }, + }, + ], + confirmationsRequired: 2, + confirmationsSubmitted: 1, + } as unknown as SafeMessage + ;(getSafeMessage as jest.Mock).mockResolvedValue(msg) const msgs: { page?: SafeMessageListPage @@ -560,7 +589,7 @@ describe('SignMessage', () => { loading: boolean } = { page: { - results: [], + results: [msg], }, error: undefined, loading: false, @@ -568,38 +597,94 @@ describe('SignMessage', () => { mockUseSafeMessages.mockReturnValue(msgs) - jest - .spyOn(useAsyncHook, 'default') - .mockReturnValue([ - { confirmations: [] as SafeMessage['confirmations'] } as SafeMessage, - new Error('SafeMessage not found'), - false, - ]) - - const confirmationSpy = jest - .spyOn(sender, 'dispatchSafeMsgProposal') - .mockImplementation(() => Promise.reject(new Error('Test error'))) - const { getByText } = render( - , + , ) + await act(async () => { + Promise.resolve() + }) + + const confirmationSpy = jest + .spyOn(sender, 'dispatchSafeMsgConfirmation') + .mockImplementation(() => Promise.reject(new Error('Error confirming'))) + const button = getByText('Sign') + expect(button).toBeEnabled() + await act(() => { fireEvent.click(button) }) - expect(confirmationSpy).toHaveBeenCalled() - await waitFor(() => { + expect(confirmationSpy).toHaveBeenCalled() expect(getByText('Error confirming the message. Please try again.')).toBeInTheDocument() }) }) + + it('shows all signatures and success message if message has already been signed', async () => { + jest.spyOn(onboard, 'default').mockReturnValue(mockOnboard) + jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => true) + jest.spyOn(useWalletHook, 'default').mockImplementation( + () => + ({ + address: zeroPadValue('0x03', 20), + } as ConnectedWallet), + ) + + const messageText = 'Hello world!' + const messageHash = generateSafeMessageHash( + { + version: '1.3.0', + address: { + value: zeroPadValue('0x01', 20), + }, + chainId: '5', + } as SafeInfo, + messageText, + ) + const msg = { + type: SafeMessageListItemType.MESSAGE, + messageHash, + confirmations: [ + { + owner: { + value: zeroPadValue('0x02', 20), + }, + }, + { + owner: { + value: zeroPadValue('0x03', 20), + }, + }, + ], + confirmationsRequired: 2, + confirmationsSubmitted: 2, + preparedSignature: '0x678', + } as unknown as SafeMessage + + const msgs: { + page?: SafeMessageListPage + error?: string + loading: boolean + } = { + page: { + results: [], + }, + error: undefined, + loading: false, + } + + mockUseSafeMessages.mockReturnValue(msgs) + ;(getSafeMessage as jest.Mock).mockResolvedValue(msg) + + const { getByText } = render( + , + ) + + await waitFor(() => { + expect(getByText('Message successfully signed')).toBeInTheDocument() + }) + }) }) diff --git a/src/components/tx-flow/flows/SignMessage/index.tsx b/src/components/tx-flow/flows/SignMessage/index.tsx index 6c54f8e290..3b9383bfee 100644 --- a/src/components/tx-flow/flows/SignMessage/index.tsx +++ b/src/components/tx-flow/flows/SignMessage/index.tsx @@ -5,6 +5,7 @@ import { selectSwapParams } from '@/features/swap/store/swapParamsSlice' import { useAppSelector } from '@/store' import { Box, Typography } from '@mui/material' import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard' +import { ErrorBoundary } from '@sentry/react' const APP_LOGO_FALLBACK_IMAGE = '/images/apps/apps-icon.svg' const APP_NAME_FALLBACK = 'Sign message' @@ -36,7 +37,9 @@ const SignMessageFlow = ({ ...props }: ProposeProps | ConfirmProps) => { hideNonce isMessage > - + Error signing message
}> + + ) } diff --git a/src/hooks/messages/useSafeMessage.ts b/src/hooks/messages/useSafeMessage.ts index b4742cc78e..fe5ec4eeb0 100644 --- a/src/hooks/messages/useSafeMessage.ts +++ b/src/hooks/messages/useSafeMessage.ts @@ -1,20 +1,30 @@ import { isSafeMessageListItem } from '@/utils/safe-message-guards' -import type { SafeMessage } from '@safe-global/safe-gateway-typescript-sdk' +import { type SafeMessage } from '@safe-global/safe-gateway-typescript-sdk' import { useState, useEffect } from 'react' import useSafeMessages from './useSafeMessages' +import useAsync from '../useAsync' +import useSafeInfo from '../useSafeInfo' +import { fetchSafeMessage } from './useSyncSafeMessageSigner' const useSafeMessage = (safeMessageHash: string) => { const [safeMessage, setSafeMessage] = useState() + const { safe } = useSafeInfo() + const messages = useSafeMessages() const ongoingMessage = messages.page?.results ?.filter(isSafeMessageListItem) .find((msg) => msg.messageHash === safeMessageHash) + const [updatedMessage] = useAsync(async () => { + return fetchSafeMessage(safeMessageHash, safe.chainId) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [safeMessageHash, safe.chainId, safe.messagesTag]) + useEffect(() => { - setSafeMessage(ongoingMessage) - }, [ongoingMessage]) + setSafeMessage(updatedMessage ?? ongoingMessage) + }, [ongoingMessage, updatedMessage]) return [safeMessage, setSafeMessage] as const } diff --git a/src/hooks/messages/useSyncSafeMessageSigner.ts b/src/hooks/messages/useSyncSafeMessageSigner.ts index 8fa28e42ef..baab16603d 100644 --- a/src/hooks/messages/useSyncSafeMessageSigner.ts +++ b/src/hooks/messages/useSyncSafeMessageSigner.ts @@ -14,7 +14,7 @@ import useOnboard from '../wallets/useOnboard' const HIDE_DELAY = 3000 -const fetchSafeMessage = async (safeMessageHash: string, chainId: string) => { +export const fetchSafeMessage = async (safeMessageHash: string, chainId: string) => { let message: SafeMessage | undefined try { // fetchedMessage does not have a type because it is explicitly a message From 33aad08e7cee3bc46c6d5cbab412e72731ef3ca3 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Thu, 16 May 2024 13:55:37 +0200 Subject: [PATCH 008/154] Fix: make custom Safe Apps' id an int (#3722) --- src/components/safe-apps/AddCustomAppModal/index.tsx | 2 -- src/components/safe-apps/utils.ts | 2 +- src/services/safe-apps/manifest.ts | 3 ++- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/safe-apps/AddCustomAppModal/index.tsx b/src/components/safe-apps/AddCustomAppModal/index.tsx index eeae0cc239..1e0ed6160d 100644 --- a/src/components/safe-apps/AddCustomAppModal/index.tsx +++ b/src/components/safe-apps/AddCustomAppModal/index.tsx @@ -1,5 +1,4 @@ import { useCallback } from 'react' -import { useRouter } from 'next/router' import type { SubmitHandler } from 'react-hook-form' import { useForm } from 'react-hook-form' import { @@ -52,7 +51,6 @@ const INVALID_URL_ERROR = 'The url is invalid' export const AddCustomAppModal = ({ open, onClose, onSave, safeAppsList }: Props) => { const currentChain = useCurrentChain() - const router = useRouter() const { register, diff --git a/src/components/safe-apps/utils.ts b/src/components/safe-apps/utils.ts index c28e9835c9..8a4265a40f 100644 --- a/src/components/safe-apps/utils.ts +++ b/src/components/safe-apps/utils.ts @@ -65,7 +65,7 @@ export const getLegacyChainName = (chainName: string, chainId: string): string = export const getEmptySafeApp = (url = '', appData?: SafeAppData): SafeAppDataWithPermissions => { return { - id: Math.random(), + id: Math.round(Math.random() * 1e9 + 1e6), url, name: 'unknown', iconUrl: '/images/apps/apps-icon.svg', diff --git a/src/services/safe-apps/manifest.ts b/src/services/safe-apps/manifest.ts index 543e82b638..b4fa97fdb2 100644 --- a/src/services/safe-apps/manifest.ts +++ b/src/services/safe-apps/manifest.ts @@ -99,7 +99,8 @@ const fetchSafeAppFromManifest = async ( const iconUrl = getAppLogoUrl(normalizedAppUrl, appManifest) return { - id: Math.random(), + // Must satisfy https://docs.djangoproject.com/en/5.0/ref/models/fields/#positiveintegerfield + id: Math.round(Math.random() * 1e9 + 1e6), url: normalizedAppUrl, name: appManifest.name, description: appManifest.description, From 657c070333a16383c81e458a1c3c7b6f12423f0f Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Fri, 17 May 2024 15:16:44 +0200 Subject: [PATCH 009/154] Tests: fix open safe app test (#3730) * tests: fix open safe app test --- cypress/e2e/pages/safeapps.pages.js | 8 ++++++-- cypress/e2e/regression/messages_popup.cy.js | 2 ++ cypress/e2e/safe-apps/info_modal.cy.js | 1 + src/components/safe-apps/SafeAppPreviewDrawer/index.tsx | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/pages/safeapps.pages.js b/cypress/e2e/pages/safeapps.pages.js index 9c9c3e84d4..1810656cb1 100644 --- a/cypress/e2e/pages/safeapps.pages.js +++ b/cypress/e2e/pages/safeapps.pages.js @@ -9,6 +9,7 @@ export const downloadBatchBtn = 'button[title="Download batch"]' export const deleteBatchBtn = 'button[title="Delete Batch"]' const appModal = '[data-testid="app-info-modal"]' export const safeAppsList = '[data-testid="apps-list"]' +const openSafeAppBtn = '[data-testid="open-safe-app-btn"]' const addBtnStr = /add/i const noAppsStr = /no Safe Apps found/i @@ -216,8 +217,11 @@ export function verifyAppDescription(descr) { } export function clickOnOpenSafeAppBtn() { - cy.findByRole('link', { name: openSafeAppBtnStr }).click() - cy.wait(500) + cy.get(openSafeAppBtn).click() + cy.wait(2000) +} + +export function verifyDisclaimerIsDisplayed() { verifyDisclaimerIsVisible() cy.wait(500) } diff --git a/cypress/e2e/regression/messages_popup.cy.js b/cypress/e2e/regression/messages_popup.cy.js index 399d41dc29..441debb121 100644 --- a/cypress/e2e/regression/messages_popup.cy.js +++ b/cypress/e2e/regression/messages_popup.cy.js @@ -39,6 +39,7 @@ describe('Messages popup window tests', () => { ) cy.reload() apps.clickOnApp(safeApp) + apps.clickOnOpenSafeAppBtn() main.getIframeBody(iframeSelector).within(() => { apps.triggetOffChainTx() }) @@ -61,6 +62,7 @@ describe('Messages popup window tests', () => { ) cy.reload() apps.clickOnApp(safeApp) + apps.clickOnOpenSafeAppBtn() main.getIframeBody(iframeSelector).within(() => { messages.enterOnchainMessage(onchainMessage) apps.triggetOnChainTx() diff --git a/cypress/e2e/safe-apps/info_modal.cy.js b/cypress/e2e/safe-apps/info_modal.cy.js index f89febd702..3a361ebbf5 100644 --- a/cypress/e2e/safe-apps/info_modal.cy.js +++ b/cypress/e2e/safe-apps/info_modal.cy.js @@ -21,6 +21,7 @@ describe('Info modal tests', () => { it('Verify the disclaimer is displayed when a Safe App is opened', () => { safeapps.clickOnApp(safeapps.transactionBuilderStr) safeapps.clickOnOpenSafeAppBtn() + safeapps.verifyDisclaimerIsDisplayed() }) // Skip tests due to changed logic diff --git a/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx b/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx index 8f64e2e339..f1e63e4b17 100644 --- a/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx +++ b/src/components/safe-apps/SafeAppPreviewDrawer/index.tsx @@ -91,6 +91,7 @@ const SafeAppPreviewDrawer = ({ isOpen, safeApp, isBookmarked, onClose, onBookma {/* Open Safe App button */} + @@ -96,8 +96,8 @@ const ActivityRewardsSection = () => { How it works
- - + +
From ef107fcef7fd7a14af76aee9039dd275e7ac6ee8 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Wed, 22 May 2024 12:31:59 +0200 Subject: [PATCH 011/154] chore: bump package version (#3736) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fac18b15d1..655c5d8368 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.36.4", + "version": "1.36.5", "type": "module", "scripts": { "dev": "next dev", From c60895fdada6c816ea91cf55ccdbfd74b330b80d Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Fri, 24 May 2024 09:47:22 +0200 Subject: [PATCH 012/154] fix: Hide signed transaction from untrusted pending queue (#3710) * fix: Hide signed transaction from untrusted pending queue * fix: Revert label adjustment, extract logic from hook and write tests * refactor: Filter items first --- src/components/transactions/TxList/index.tsx | 8 +- src/hooks/__tests__/usePendingTxs.test.ts | 196 +++++++++++++------ src/hooks/usePendingTxs.ts | 57 +++--- 3 files changed, 178 insertions(+), 83 deletions(-) diff --git a/src/components/transactions/TxList/index.tsx b/src/components/transactions/TxList/index.tsx index 1b92a9860b..ac816b689b 100644 --- a/src/components/transactions/TxList/index.tsx +++ b/src/components/transactions/TxList/index.tsx @@ -1,10 +1,10 @@ -import type { ReactElement, ReactNode } from 'react' -import { useMemo } from 'react' +import GroupedTxListItems from '@/components/transactions/GroupedTxListItems' +import { groupConflictingTxs } from '@/utils/tx-list' import { Box } from '@mui/material' import type { TransactionListPage } from '@safe-global/safe-gateway-typescript-sdk' +import type { ReactElement, ReactNode } from 'react' +import { useMemo } from 'react' import TxListItem from '../TxListItem' -import GroupedTxListItems from '@/components/transactions/GroupedTxListItems' -import { groupConflictingTxs } from '@/utils/tx-list' import css from './styles.module.css' type TxListProps = { diff --git a/src/hooks/__tests__/usePendingTxs.test.ts b/src/hooks/__tests__/usePendingTxs.test.ts index 4ed501db8f..5765fa1c68 100644 --- a/src/hooks/__tests__/usePendingTxs.test.ts +++ b/src/hooks/__tests__/usePendingTxs.test.ts @@ -6,39 +6,131 @@ import { type Transaction, getTransactionQueue, TransactionListItemType, + DetailedExecutionInfoType, + LabelValue, + type TransactionListPage, + type ExecutionInfo, + type TransactionSummary, + ConflictType, } from '@safe-global/safe-gateway-typescript-sdk' import * as useSafeInfoHook from '@/hooks/useSafeInfo' -import { useHasPendingTxs, usePendingTxsQueue } from '../usePendingTxs' +import { filterUntrustedQueue, useHasPendingTxs, usePendingTxsQueue } from '../usePendingTxs' + +const mockQueue = { + next: undefined, + previous: undefined, + results: [ + { + type: TransactionListItemType.LABEL, + label: LabelValue.Next, + }, + { + type: 'TRANSACTION', + transaction: { + id: 'multisig_123', + executionInfo: { + confirmationsSubmitted: 0, + type: DetailedExecutionInfoType.MULTISIG, + }, + }, + }, + { + type: 'TRANSACTION', + transaction: { + id: 'multisig_456', + }, + }, + ], +} + +const mockQueueWithConflictHeaders = { + next: undefined, + previous: undefined, + results: [ + { + type: TransactionListItemType.LABEL, + label: LabelValue.Next, + }, + { + type: TransactionListItemType.CONFLICT_HEADER, + nonce: 2, + }, + { + type: 'TRANSACTION', + transaction: { + id: 'multisig_123', + executionInfo: { + confirmationsSubmitted: 0, + type: DetailedExecutionInfoType.MULTISIG, + }, + }, + }, + { + type: 'TRANSACTION', + transaction: { + id: 'multisig_456', + }, + }, + ], +} // Mock getTransactionQueue jest.mock('@safe-global/safe-gateway-typescript-sdk', () => ({ ...jest.requireActual('@safe-global/safe-gateway-typescript-sdk'), - getTransactionQueue: jest.fn(() => - Promise.resolve({ - next: null, - previous: null, - results: [ - { - type: 'LABEL', - label: 'Next', - }, - { - type: 'TRANSACTION', - transaction: { - id: 'multisig_123', - }, - }, - { - type: 'TRANSACTION', - transaction: { - id: 'multisig_456', - }, - }, - ], - }), - ), + getTransactionQueue: jest.fn(() => Promise.resolve(mockQueue)), })) +describe('filterUntrustedQueue', () => { + it('should remove transactions that are not pending', () => { + const mockPendingIds = ['multisig_123'] + + const result = filterUntrustedQueue(mockQueue, mockPendingIds) + + expect(result?.results.length).toEqual(2) + }) + + it('should rename the first label to Pending', () => { + const mockPendingIds = ['multisig_123'] + + const result = filterUntrustedQueue(mockQueue, mockPendingIds) + + expect(result?.results[0]).toEqual({ type: 'LABEL', label: 'Pending' }) + }) + + it('should remove all conflict headers', () => { + const mockPendingIds = ['multisig_123'] + + const result = filterUntrustedQueue(mockQueueWithConflictHeaders, mockPendingIds) + + expect(result?.results[0]).toEqual({ type: 'LABEL', label: 'Pending' }) + expect(result?.results.length).toEqual(2) + expect(result?.results[1].type).not.toEqual(TransactionListItemType.CONFLICT_HEADER) + }) + + it('should remove all transactions that are signed', () => { + const mockPendingIds = ['multisig_123', 'multisig_789'] + const mockQueueWithSignedTxs = { ...mockQueue } + + mockQueueWithSignedTxs.results.push({ + type: TransactionListItemType.TRANSACTION, + transaction: { + id: 'multisig_789', + executionInfo: { + confirmationsSubmitted: 1, + confirmationsRequired: 1, + type: DetailedExecutionInfoType.MULTISIG, + } as ExecutionInfo, + } as unknown as TransactionSummary, + conflictType: ConflictType.NONE, + }) + + const result = filterUntrustedQueue(mockQueueWithSignedTxs, mockPendingIds) + + expect(result?.results.length).toEqual(2) + expect(result?.results[2]).not.toEqual(mockQueueWithSignedTxs.results[2]) + }) +}) + describe('usePendingTxsQueue', () => { beforeEach(() => { jest.clearAllMocks() @@ -59,7 +151,7 @@ describe('usePendingTxsQueue', () => { })) }) - it('should return the pending txs queue', async () => { + it('should return the pending txs queue for unsigned transactions', async () => { const { result } = renderHook(() => usePendingTxsQueue(), { initialReduxState: { pendingTxs: { @@ -106,35 +198,29 @@ describe('usePendingTxsQueue', () => { expect(result?.current.page).toBeUndefined() }) - it('should remove conflicting header if only one of the conflicting txs is pending', async () => { - ;(getTransactionQueue as jest.Mock).mockImplementation(() => - Promise.resolve({ - next: null, - previous: null, - results: [ - { - type: 'LABEL', - label: 'Next', - }, - { - type: TransactionListItemType.CONFLICT_HEADER, - nonce: 2, - }, - { - type: 'TRANSACTION', - transaction: { - id: 'multisig_123', - }, - }, - { - type: 'TRANSACTION', - transaction: { - id: 'multisig_456', - }, - }, - ], - }), - ) + it('should return undefined if none of the pending txs are unsigned', async () => { + const { result } = renderHook(() => usePendingTxsQueue(), { + initialReduxState: { + pendingTxs: { + multisig_456: { + chainId: '5', + safeAddress: '0x0000000000000000000000000000000000000001', + txHash: 'tx567', + } as PendingTx, + }, + }, + }) + + expect(result?.current.loading).toBe(true) + + await act(() => Promise.resolve(true)) + + expect(result?.current.loading).toBe(false) + expect(result?.current.page).toBeUndefined() + }) + + it('should remove all conflict headers', async () => { + ;(getTransactionQueue as jest.Mock).mockImplementation(() => Promise.resolve(mockQueueWithConflictHeaders)) const { result } = renderHook(() => usePendingTxsQueue(), { initialReduxState: { diff --git a/src/hooks/usePendingTxs.ts b/src/hooks/usePendingTxs.ts index f7e95e2683..db7a46c623 100644 --- a/src/hooks/usePendingTxs.ts +++ b/src/hooks/usePendingTxs.ts @@ -8,7 +8,12 @@ import { import { useAppSelector } from '@/store' import { selectPendingTxIdsBySafe } from '@/store/pendingTxsSlice' import useAsync from './useAsync' -import { isConflictHeaderListItem, isLabelListItem, isTransactionListItem } from '@/utils/transaction-guards' +import { + isConflictHeaderListItem, + isLabelListItem, + isMultisigExecutionInfo, + isTransactionListItem, +} from '@/utils/transaction-guards' import useSafeInfo from './useSafeInfo' const usePendingTxIds = (): Array => { @@ -31,6 +36,32 @@ export const useShowUnsignedQueue = (): boolean => { return safe.threshold === 1 && hasPending } +export const filterUntrustedQueue = ( + untrustedQueue: TransactionListPage, + pendingIds: Array, +) => { + // Only keep labels and pending unsigned transactions + const results = untrustedQueue.results + .filter((item) => !isTransactionListItem(item) || pendingIds.includes(item.transaction.id)) + .filter((item) => !isConflictHeaderListItem(item)) + .filter( + (item) => + !isTransactionListItem(item) || + (isTransactionListItem(item) && + isMultisigExecutionInfo(item.transaction.executionInfo) && + item.transaction.executionInfo.confirmationsSubmitted === 0), + ) + + // Adjust the first label ("Next" -> "Pending") + if (results[0] && isLabelListItem(results[0])) { + results[0].label = 'Pending' as LabelValue + } + + const transactions = results.filter((item) => isTransactionListItem(item)) + + return transactions.length ? { results } : undefined +} + export const usePendingTxsQueue = (): { page?: TransactionListPage error?: string @@ -53,29 +84,7 @@ export const usePendingTxsQueue = (): { const pendingTxPage = useMemo(() => { if (!untrustedQueue || !pendingIds.length) return - // Find the pending txs in the "untrusted" queue by id - // Keep labels too - const results = untrustedQueue.results.filter( - (item) => !isTransactionListItem(item) || pendingIds.includes(item.transaction.id), - ) - - // Adjust the first label ("Next" -> "Pending") - if (results[0] && isLabelListItem(results[0])) { - results[0].label = 'Pending' as LabelValue - - if (results.filter((item) => isTransactionListItem(item)).length === 0) { - results.splice(0, 1) - } - } - - if (results[1] && isConflictHeaderListItem(results[1])) { - // Check if we both conflicting txs are still pending - if (results.filter((item) => isTransactionListItem(item)).length <= 1) { - results.splice(1, 1) - } - } - - return results.length ? { results } : undefined + return filterUntrustedQueue(untrustedQueue, pendingIds) }, [untrustedQueue, pendingIds]) return useMemo( From 4f34136399cca6ef7a8d3e2fc3981e1e798e79e9 Mon Sep 17 00:00:00 2001 From: James Mealy Date: Fri, 24 May 2024 10:10:03 +0200 Subject: [PATCH 013/154] Fix: Show loading state in WC input between pairing and receiving proposal (#3712) * fix: show loading state in WC widget between pairing and receiving a proposal * fix: clear loading state on failed paring * empty commit for deployment * disable loading state for approval form * fix typo * add 30 second timeout for proposal * Only show timeout error when waiting on proposal --- .../components/WalletConnectUi/index.tsx | 2 +- .../components/WcInput/index.tsx | 29 ++++++++++++------- .../index.tsx | 3 +- 3 files changed, 21 insertions(+), 13 deletions(-) rename src/features/walletconnect/components/{WcSessionMananger => WcSessionManager}/index.tsx (97%) diff --git a/src/features/walletconnect/components/WalletConnectUi/index.tsx b/src/features/walletconnect/components/WalletConnectUi/index.tsx index c78c211514..dbd5bdb678 100644 --- a/src/features/walletconnect/components/WalletConnectUi/index.tsx +++ b/src/features/walletconnect/components/WalletConnectUi/index.tsx @@ -5,7 +5,7 @@ import useWalletConnectSessions from '@/features/walletconnect/hooks/useWalletCo import { WalletConnectContext } from '@/features/walletconnect/WalletConnectContext' import useWcUri from '../../hooks/useWcUri' import WcHeaderWidget from '../WcHeaderWidget' -import WcSessionManager from '../WcSessionMananger' +import WcSessionManager from '../WcSessionManager' import { WalletConnectProvider } from '../WalletConnectProvider' const WalletConnectWidget = () => { diff --git a/src/features/walletconnect/components/WcInput/index.tsx b/src/features/walletconnect/components/WcInput/index.tsx index 2b6018dfb4..ff40714c80 100644 --- a/src/features/walletconnect/components/WcInput/index.tsx +++ b/src/features/walletconnect/components/WcInput/index.tsx @@ -10,6 +10,8 @@ import { getClipboard, isClipboardSupported } from '@/utils/clipboard' import { Button, CircularProgress, InputAdornment, TextField } from '@mui/material' import { useCallback, useContext, useEffect, useState } from 'react' +const PROPOSAL_TIMEOUT = 30_000 + const useTrackErrors = (error?: Error) => { const debouncedErrorMessage = useDebounce(error?.message, 1000) @@ -22,10 +24,10 @@ const useTrackErrors = (error?: Error) => { } const WcInput = ({ uri }: { uri: string }) => { - const { walletConnect, isLoading, setIsLoading } = useContext(WalletConnectContext) + const { walletConnect, isLoading, setIsLoading, setError } = useContext(WalletConnectContext) const [value, setValue] = useState('') - const [error, setError] = useState() - useTrackErrors(error) + const [inputError, setInputError] = useState() + useTrackErrors(inputError) const onInput = useCallback( async (val: string) => { @@ -34,11 +36,11 @@ const WcInput = ({ uri }: { uri: string }) => { setValue(val) if (val && !isPairingUri(val)) { - setError(new Error('Invalid pairing code')) + setInputError(new Error('Invalid pairing code')) return } - setError(undefined) + setInputError(undefined) if (!val) return @@ -47,12 +49,17 @@ const WcInput = ({ uri }: { uri: string }) => { try { await walletConnect.connect(val) } catch (e) { - setError(asError(e)) + setInputError(asError(e)) + setIsLoading(undefined) } - - setIsLoading(undefined) + setTimeout(() => { + if (isLoading && isLoading !== WCLoadingState.APPROVE) { + setIsLoading(undefined) + setError(new Error('Connection timed out')) + } + }, PROPOSAL_TIMEOUT) }, - [setIsLoading, walletConnect], + [isLoading, setError, setIsLoading, walletConnect], ) // Insert a pre-filled uri @@ -77,8 +84,8 @@ const WcInput = ({ uri }: { uri: string }) => { autoComplete="off" autoFocus disabled={!!isLoading} - error={!!error} - label={error ? error.message : 'Pairing code'} + error={!!inputError} + label={inputError ? inputError.message : 'Pairing code'} placeholder="wc:" spellCheck={false} InputProps={{ diff --git a/src/features/walletconnect/components/WcSessionMananger/index.tsx b/src/features/walletconnect/components/WcSessionManager/index.tsx similarity index 97% rename from src/features/walletconnect/components/WcSessionMananger/index.tsx rename to src/features/walletconnect/components/WcSessionManager/index.tsx index b5d140c5b1..dbc611d422 100644 --- a/src/features/walletconnect/components/WcSessionMananger/index.tsx +++ b/src/features/walletconnect/components/WcSessionManager/index.tsx @@ -87,8 +87,9 @@ const WcSessionManager = ({ sessions, uri }: WcSessionManagerProps) => { } setProposal(proposalData) + setIsLoading(undefined) }) - }, [walletConnect, setError, autoApprove, onApprove, chainId]) + }, [autoApprove, chainId, onApprove, setError, setIsLoading, walletConnect]) // Track errors useEffect(() => { From 1733f18dbc099203fb6c6bf4a43c08debb3a534b Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 24 May 2024 10:18:40 +0200 Subject: [PATCH 014/154] Fix: move rhino.fi to warned bridges (#3740) --- src/features/walletconnect/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/walletconnect/constants.ts b/src/features/walletconnect/constants.ts index da63f69eb6..1f43d957a2 100644 --- a/src/features/walletconnect/constants.ts +++ b/src/features/walletconnect/constants.ts @@ -46,7 +46,6 @@ export const BlockedBridges = [ 'zksync-era.l2scan.co', 'www.portalbridge.com', 'wallet.polygon.technology', - 'app.rhino.fi', // Unsupported chain bridges 'bridge.zora.energy', @@ -67,6 +66,7 @@ export const WarnedBridges = [ 'core.app', 'across.to', // doesn't send their URL in the session proposal 'app.allbridge.io', + 'app.rhino.fi', 'bridge.arbitrum.io', 'bridge.base.org', 'bridge.linea.build', From 72a2018150159170affcd2257eb0e66938e12ea6 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 24 May 2024 11:08:08 +0200 Subject: [PATCH 015/154] Chore: update release action (#3739) --- .github/workflows/deploy-production.yml | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 66ca16f382..139049865c 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -51,21 +51,17 @@ jobs: # Update the GitHub release with a checksummed archive - name: Upload archive - uses: actions/upload-release-asset@v1 + - uses: Shopify/upload-to-release@v2.0.0 with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ${{ env.ARCHIVE_NAME }}.tar.gz - asset_name: ${{ env.ARCHIVE_NAME }}.tar.gz - asset_content_type: application/gzip - env: - GITHUB_TOKEN: ${{ github.token }} + path: ${{ env.ARCHIVE_NAME }}.tar.gz + name: ${{ env.ARCHIVE_NAME }}.tar.gz + content-type: application/gzip + repo-token: ${{ github.token }} - name: Upload checksum - uses: actions/upload-release-asset@v1 + uses: Shopify/upload-to-release@v2.0.0 with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt - asset_name: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt - asset_content_type: text/plain - env: - GITHUB_TOKEN: ${{ github.token }} + path: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt + name: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt + content-type: text/plain + repo-token: ${{ github.token }} From 98e879ac4d2f84d0fd69844a7c4d42469fc05a72 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 24 May 2024 11:42:06 +0200 Subject: [PATCH 016/154] Fix: update social login disclaimer (#3741) * Fix: update social login disclaimer * Prettier --- .../common/SocialLoginDeprecation/index.tsx | 2 +- src/components/common/SocialSigner/index.tsx | 2 +- .../welcome/MyAccounts/styles.module.css | 16 ++++++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/common/SocialLoginDeprecation/index.tsx b/src/components/common/SocialLoginDeprecation/index.tsx index e484a6b7c0..da8d9a9b38 100644 --- a/src/components/common/SocialLoginDeprecation/index.tsx +++ b/src/components/common/SocialLoginDeprecation/index.tsx @@ -24,7 +24,7 @@ const SocialLoginDeprecation = () => { return ( - The Social Login wallet is deprecated and will be removed on 01.05.2024. + The Social Login wallet is deprecated and will be removed on 01.07.2024.
Please{' '} diff --git a/src/components/common/SocialSigner/index.tsx b/src/components/common/SocialSigner/index.tsx index 2f21e51168..e75afd413c 100644 --- a/src/components/common/SocialSigner/index.tsx +++ b/src/components/common/SocialSigner/index.tsx @@ -134,7 +134,7 @@ export const SocialSigner = ({ socialWalletService, wallet, onLogin, onRequirePa - From 01.05.2024 we will no longer support account creation and login with Google. + We're discontinuing support for Google login. Please export your credentials till 01.07.2024. ) diff --git a/src/components/welcome/MyAccounts/styles.module.css b/src/components/welcome/MyAccounts/styles.module.css index 5aa0edf2b1..60854d02b8 100644 --- a/src/components/welcome/MyAccounts/styles.module.css +++ b/src/components/welcome/MyAccounts/styles.module.css @@ -64,10 +64,18 @@ 'a c d'; } - .safeLink :nth-child(1) { grid-area: a; } - .safeLink :nth-child(2) { grid-area: b; } - .safeLink :nth-child(3) { grid-area: c; } - .safeLink :nth-child(4) { grid-area: d; } + .safeLink :nth-child(1) { + grid-area: a; + } + .safeLink :nth-child(2) { + grid-area: b; + } + .safeLink :nth-child(3) { + grid-area: c; + } + .safeLink :nth-child(4) { + grid-area: d; + } } .safeAddress { From 433bb16257ae46f1d3485f9fd137f4933790aea1 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Fri, 24 May 2024 17:41:34 +0200 Subject: [PATCH 017/154] fix: (WC) do not return Safe as available on all optional Namespaces (#3725) --- .../services/WalletConnectWallet.ts | 9 ++--- .../__tests__/WalletConnectWallet.test.ts | 35 ++++--------------- 2 files changed, 8 insertions(+), 36 deletions(-) diff --git a/src/features/walletconnect/services/WalletConnectWallet.ts b/src/features/walletconnect/services/WalletConnectWallet.ts index d497e78642..bb2036e55d 100644 --- a/src/features/walletconnect/services/WalletConnectWallet.ts +++ b/src/features/walletconnect/services/WalletConnectWallet.ts @@ -82,15 +82,10 @@ class WalletConnectWallet { } private getNamespaces(proposal: Web3WalletTypes.SessionProposal, currentChainId: string, safeAddress: string) { - // Most dApps require mainnet, but we aren't always on mainnet - // As workaround, we pretend include all required and optional chains with the Safe chainId + // As workaround, we pretend to support all the required chains plus the current Safe's chain const requiredChains = proposal.params.requiredNamespaces[EIP155]?.chains || [] - const optionalChains = proposal.params.optionalNamespaces[EIP155]?.chains || [] - const supportedChainIds = [currentChainId].concat( - requiredChains.map(stripEip155Prefix), - optionalChains.map(stripEip155Prefix), - ) + const supportedChainIds = [currentChainId].concat(requiredChains.map(stripEip155Prefix)) const eip155ChainIds = supportedChainIds.map(getEip155ChainId) const eip155Accounts = eip155ChainIds.map((eip155ChainId) => `${eip155ChainId}:${safeAddress}`) diff --git a/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts b/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts index ce86f0dbbb..643a2f873c 100644 --- a/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts +++ b/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts @@ -155,21 +155,13 @@ describe('WalletConnectWallet', () => { await wallet.approveSession( proposal, - '69420', // Not in proposal, therefore not supported + '43114', // Not in proposal, therefore not supported toBeHex('0x123', 20), ) const namespaces = { eip155: { - chains: [ - 'eip155:1', - 'eip155:43114', - 'eip155:42161', - 'eip155:8453', - 'eip155:100', - 'eip155:137', - 'eip155:1101', - ], + chains: ['eip155:1', 'eip155:43114'], methods: [ 'eth_sendTransaction', 'personal_sign', @@ -180,15 +172,7 @@ describe('WalletConnectWallet', () => { 'wallet_switchEthereumChain', ], events: ['chainChanged', 'accountsChanged'], - accounts: [ - `eip155:1:${toBeHex('0x123', 20)}`, - `eip155:43114:${toBeHex('0x123', 20)}`, - `eip155:42161:${toBeHex('0x123', 20)}`, - `eip155:8453:${toBeHex('0x123', 20)}`, - `eip155:100:${toBeHex('0x123', 20)}`, - `eip155:137:${toBeHex('0x123', 20)}`, - `eip155:1101:${toBeHex('0x123', 20)}`, - ], + accounts: [`eip155:1:${toBeHex('0x123', 20)}`, `eip155:43114:${toBeHex('0x123', 20)}`], }, } @@ -226,23 +210,16 @@ describe('WalletConnectWallet', () => { await wallet.approveSession( proposal, - '69420', // Not in proposal, therefore not supported + '43114', // Not in proposal, therefore not supported toBeHex('0x123', 20), ) const namespaces = { eip155: { - chains: ['eip155:43114', 'eip155:42161', 'eip155:8453', 'eip155:100', 'eip155:137', 'eip155:1101'], + chains: ['eip155:43114'], methods: ['eth_accounts', 'personal_sign', 'eth_sendTransaction'], events: ['chainChanged', 'accountsChanged'], - accounts: [ - `eip155:43114:${toBeHex('0x123', 20)}`, - `eip155:42161:${toBeHex('0x123', 20)}`, - `eip155:8453:${toBeHex('0x123', 20)}`, - `eip155:100:${toBeHex('0x123', 20)}`, - `eip155:137:${toBeHex('0x123', 20)}`, - `eip155:1101:${toBeHex('0x123', 20)}`, - ], + accounts: [`eip155:43114:${toBeHex('0x123', 20)}`], }, } From 548bf6aa6902aae0ca4e73316a42c09bc1532b0f Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Sat, 25 May 2024 09:03:24 +0200 Subject: [PATCH 018/154] Fix: memoize Safe List data (#3729) * Fix: memoize Safe List data * Adjust types --- .../welcome/MyAccounts/useSafeOverviews.ts | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/components/welcome/MyAccounts/useSafeOverviews.ts b/src/components/welcome/MyAccounts/useSafeOverviews.ts index bfe33c3eb4..3101b54c48 100644 --- a/src/components/welcome/MyAccounts/useSafeOverviews.ts +++ b/src/components/welcome/MyAccounts/useSafeOverviews.ts @@ -1,26 +1,44 @@ +import { useMemo } from 'react' import { useTokenListSetting } from '@/hooks/loadables/useLoadBalances' -import useAsync from '@/hooks/useAsync' +import useAsync, { type AsyncResult } from '@/hooks/useAsync' import useWallet from '@/hooks/wallets/useWallet' import { useAppSelector } from '@/store' import { selectCurrency } from '@/store/settingsSlice' -import { getSafeOverviews } from '@safe-global/safe-gateway-typescript-sdk' +import { type SafeOverview, getSafeOverviews } from '@safe-global/safe-gateway-typescript-sdk' -function useSafeOverviews(safes: Array<{ address: string; chainId: string }>) { +const _cache: Record = {} + +type SafeParams = { + address: string + chainId: string +} + +// EIP155 address format +const makeSafeId = ({ chainId, address }: SafeParams) => `${chainId}:${address}` as `${number}:0x${string}` + +function useSafeOverviews(safes: Array): AsyncResult { const excludeSpam = useTokenListSetting() || false const currency = useAppSelector(selectCurrency) const wallet = useWallet() const walletAddress = wallet?.address + const safesIds = useMemo(() => safes.map(makeSafeId), [safes]) - return useAsync(async () => { - const safesStrings = safes.map((safe) => `${safe.chainId}:${safe.address}` as `${number}:0x${string}`) - - return await getSafeOverviews(safesStrings, { + const [data, error, isLoading] = useAsync(async () => { + return await getSafeOverviews(safesIds, { trusted: true, exclude_spam: excludeSpam, currency, wallet_address: walletAddress, }) - }, [safes, excludeSpam, currency, walletAddress]) + }, [safesIds, excludeSpam, currency, walletAddress]) + + const cacheKey = safesIds.join() + const result = data ?? _cache[cacheKey] + + // Cache until the next page load + _cache[cacheKey] = result + + return useMemo(() => [result, error, isLoading], [result, error, isLoading]) } export default useSafeOverviews From ae506eea79739e947fdf99a6443017669028629b Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Tue, 28 May 2024 09:44:48 +0200 Subject: [PATCH 019/154] fix: Safe stats tracking for owned Safes (#3708) --- src/components/welcome/MyAccounts/index.tsx | 2 +- .../welcome/MyAccounts/useAllOwnedSafes.ts | 25 +++++++++++++---- .../welcome/MyAccounts/useAllSafes.ts | 8 +++--- .../MyAccounts/useTrackedSafesCount.ts | 27 +++++++++++++++---- src/services/analytics/events/overview.ts | 2 ++ 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/components/welcome/MyAccounts/index.tsx b/src/components/welcome/MyAccounts/index.tsx index 717de2940a..6bf3e19f3e 100644 --- a/src/components/welcome/MyAccounts/index.tsx +++ b/src/components/welcome/MyAccounts/index.tsx @@ -28,7 +28,7 @@ const AccountsList = ({ safes, onLinkClick }: AccountsListProps) => { const ownedSafes = useMemo(() => safes?.filter(({ isWatchlist }) => !isWatchlist), [safes]) const watchlistSafes = useMemo(() => safes?.filter(({ isWatchlist }) => isWatchlist), [safes]) - useTrackSafesCount(ownedSafes, watchlistSafes) + useTrackSafesCount(ownedSafes, watchlistSafes, wallet) const isLoginPage = router.pathname === AppRoutes.welcome.accounts const trackingLabel = isLoginPage ? OVERVIEW_LABELS.login_page : OVERVIEW_LABELS.sidebar diff --git a/src/components/welcome/MyAccounts/useAllOwnedSafes.ts b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts index a14c362eff..b96e7007cb 100644 --- a/src/components/welcome/MyAccounts/useAllOwnedSafes.ts +++ b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts @@ -7,17 +7,32 @@ import { useEffect } from 'react' 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 {} - return getAllOwnedSafes(address) + const [data, error, isLoading] = useAsync(async () => { + if (!address) + return { + ownedSafes: {}, + address: undefined, + } + const ownedSafes = await getAllOwnedSafes(address) + return { + ownedSafes, + address, + } }, [address]) useEffect(() => { - if (data != undefined) setCache(data) - }, [data, setCache]) + if (data?.ownedSafes != undefined && data.address === address) { + setCache(data.ownedSafes) + } + }, [address, cache, data, setCache]) return [cache, error, isLoading] } diff --git a/src/components/welcome/MyAccounts/useAllSafes.ts b/src/components/welcome/MyAccounts/useAllSafes.ts index c5dc62f6a5..84c3a40020 100644 --- a/src/components/welcome/MyAccounts/useAllSafes.ts +++ b/src/components/welcome/MyAccounts/useAllSafes.ts @@ -8,7 +8,6 @@ import useChains from '@/hooks/useChains' import useWallet from '@/hooks/wallets/useWallet' import { selectUndeployedSafes } from '@/store/slices' import { sameAddress } from '@/utils/addresses' - export type SafeItem = { chainId: string address: string @@ -37,12 +36,15 @@ export const useHasSafes = () => { const useAllSafes = (): SafeItems | undefined => { const { address: walletAddress = '' } = useWallet() || {} - const [allOwned] = useAllOwnedSafes(walletAddress) + const [allOwned, , allOwnedLoading] = useAllOwnedSafes(walletAddress) const allAdded = useAddedSafes() const { configs } = useChains() const undeployedSafes = useAppSelector(selectUndeployedSafes) return useMemo(() => { + if (walletAddress && (allOwned === undefined || allOwnedLoading)) { + return undefined + } const chains = uniq(Object.keys(allAdded).concat(Object.keys(allOwned || {}))) return chains.flatMap((chainId) => { @@ -64,7 +66,7 @@ const useAllSafes = (): SafeItems | undefined => { } }) }) - }, [allAdded, allOwned, configs, undeployedSafes, walletAddress]) + }, [allAdded, allOwned, allOwnedLoading, configs, undeployedSafes, walletAddress]) } export default useAllSafes diff --git a/src/components/welcome/MyAccounts/useTrackedSafesCount.ts b/src/components/welcome/MyAccounts/useTrackedSafesCount.ts index 69bd25229b..e22d788aca 100644 --- a/src/components/welcome/MyAccounts/useTrackedSafesCount.ts +++ b/src/components/welcome/MyAccounts/useTrackedSafesCount.ts @@ -3,20 +3,37 @@ import { OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' import { useRouter } from 'next/router' import { useEffect } from 'react' import type { SafeItems } from './useAllSafes' +import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' -let isTracked = false +let isOwnedSafesTracked = false +let isWatchlistTracked = false -const useTrackSafesCount = (ownedSafes: SafeItems | undefined, watchlistSafes: SafeItems | undefined) => { +const useTrackSafesCount = ( + ownedSafes: SafeItems | undefined, + watchlistSafes: SafeItems | undefined, + wallet: ConnectedWallet | null, +) => { const router = useRouter() const isLoginPage = router.pathname === AppRoutes.welcome.accounts + // Reset tracking for new wallet useEffect(() => { - if (watchlistSafes && ownedSafes && isLoginPage && !isTracked) { + isOwnedSafesTracked = false + }, [wallet?.address]) + + useEffect(() => { + if (!isOwnedSafesTracked && ownedSafes && ownedSafes.length > 0 && isLoginPage) { trackEvent({ ...OVERVIEW_EVENTS.TOTAL_SAFES_OWNED, label: ownedSafes.length }) + isOwnedSafesTracked = true + } + }, [isLoginPage, ownedSafes]) + + useEffect(() => { + if (watchlistSafes && isLoginPage && watchlistSafes.length > 0 && !isWatchlistTracked) { trackEvent({ ...OVERVIEW_EVENTS.TOTAL_SAFES_WATCHLIST, label: watchlistSafes.length }) - isTracked = true + isWatchlistTracked = true } - }, [isLoginPage, ownedSafes, watchlistSafes]) + }, [isLoginPage, watchlistSafes]) } export default useTrackSafesCount diff --git a/src/services/analytics/events/overview.ts b/src/services/analytics/events/overview.ts index 094d9ef7e6..b24f4d733a 100644 --- a/src/services/analytics/events/overview.ts +++ b/src/services/analytics/events/overview.ts @@ -38,10 +38,12 @@ export const OVERVIEW_EVENTS = { TOTAL_SAFES_OWNED: { action: 'Total Safes owned', category: OVERVIEW_CATEGORY, + event: EventType.META, }, TOTAL_SAFES_WATCHLIST: { action: 'Total Safes watchlist', category: OVERVIEW_CATEGORY, + event: EventType.META, }, SIDEBAR: { action: 'Sidebar', From 6e794e4f869dd66bdadbd4554538fcf41f3b468d Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Tue, 28 May 2024 12:23:27 +0200 Subject: [PATCH 020/154] Chore: remove batch reordering (#3754) --- package.json | 1 - .../batch/BatchSidebar/BatchReorder.tsx | 33 ------------------- .../batch/BatchSidebar/BatchTxItem.tsx | 25 ++------------ src/components/batch/BatchSidebar/index.tsx | 8 ++--- src/hooks/useDraftBatch.ts | 19 ++--------- src/services/analytics/events/batching.ts | 5 --- src/store/batchSlice.ts | 19 ++--------- yarn.lock | 21 ------------ 8 files changed, 10 insertions(+), 121 deletions(-) delete mode 100644 src/components/batch/BatchSidebar/BatchReorder.tsx diff --git a/package.json b/package.json index 655c5d8368..30af401943 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,6 @@ "ethers": "^6.11.1", "exponential-backoff": "^3.1.0", "firebase": "^10.3.1", - "framer-motion": "^10.13.1", "fuse.js": "^6.6.2", "idb-keyval": "^6.2.1", "js-cookie": "^3.0.1", diff --git a/src/components/batch/BatchSidebar/BatchReorder.tsx b/src/components/batch/BatchSidebar/BatchReorder.tsx deleted file mode 100644 index 1f7ee41fde..0000000000 --- a/src/components/batch/BatchSidebar/BatchReorder.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Reorder } from 'framer-motion' -import type { DraftBatchItem } from '@/store/batchSlice' -import BatchTxItem from './BatchTxItem' -import { useState } from 'react' - -const BatchReorder = ({ - txItems, - onDelete, - onReorder, -}: { - txItems: DraftBatchItem[] - onDelete?: (id: string) => void - onReorder: (items: DraftBatchItem[]) => void -}) => { - const [dragging, setDragging] = useState(false) - - return ( - - {txItems.map((item, index) => ( - setDragging(true)} - onDragEnd={() => setDragging(false)} - > - - - ))} - - ) -} - -export default BatchReorder diff --git a/src/components/batch/BatchSidebar/BatchTxItem.tsx b/src/components/batch/BatchSidebar/BatchTxItem.tsx index e9161b4def..a9b27f44ca 100644 --- a/src/components/batch/BatchSidebar/BatchTxItem.tsx +++ b/src/components/batch/BatchSidebar/BatchTxItem.tsx @@ -6,7 +6,6 @@ import { type DraftBatchItem } from '@/store/batchSlice' import TxType from '@/components/transactions/TxType' import TxInfo from '@/components/transactions/TxInfo' import DeleteIcon from '@/public/images/common/delete.svg' -import DragIcon from '@/public/images/common/drag.svg' import TxData from '@/components/transactions/TxDetails/TxData' import { MethodDetails } from '@/components/transactions/TxDetails/TxData/DecodedData/MethodDetails' import { TxDataRow } from '@/components/transactions/TxDetails/Summary/TxDataRow' @@ -17,19 +16,9 @@ type BatchTxItemProps = DraftBatchItem & { id: string count: number onDelete?: (id: string) => void - draggable?: boolean - dragging?: boolean } -const BatchTxItem = ({ - id, - count, - timestamp, - txDetails, - onDelete, - dragging = false, - draggable = false, -}: BatchTxItemProps) => { +const BatchTxItem = ({ id, count, timestamp, txDetails, onDelete }: BatchTxItemProps) => { const txSummary = useMemo( () => ({ timestamp, @@ -61,18 +50,8 @@ const BatchTxItem = ({
{count}
- } disabled={dragging} className={css.accordion}> + } className={css.accordion}> - {draggable && ( - e.stopPropagation()} - /> - )} - diff --git a/src/components/batch/BatchSidebar/index.tsx b/src/components/batch/BatchSidebar/index.tsx index 5c882a2e7f..d1f89ac6d2 100644 --- a/src/components/batch/BatchSidebar/index.tsx +++ b/src/components/batch/BatchSidebar/index.tsx @@ -1,6 +1,5 @@ import { type SyntheticEvent, useEffect } from 'react' import { useCallback, useContext } from 'react' -import dynamic from 'next/dynamic' import { Button, Divider, Drawer, IconButton, SvgIcon, Typography } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' import { useDraftBatch, useUpdateBatch } from '@/hooks/useDraftBatch' @@ -13,13 +12,12 @@ import { BATCH_EVENTS } from '@/services/analytics' import CheckWallet from '@/components/common/CheckWallet' import PlusIcon from '@/public/images/common/plus.svg' import EmptyBatch from './EmptyBatch' - -const BatchReorder = dynamic(() => import('./BatchReorder')) +import BatchTxList from './BatchTxList' const BatchSidebar = ({ isOpen, onToggle }: { isOpen: boolean; onToggle: (open: boolean) => void }) => { const { txFlow, setTxFlow } = useContext(TxModalContext) const batchTxs = useDraftBatch() - const [, deleteTx, onReorder] = useUpdateBatch() + const [, deleteTx] = useUpdateBatch() const closeSidebar = useCallback(() => { onToggle(false) @@ -73,7 +71,7 @@ const BatchSidebar = ({ isOpen, onToggle }: { isOpen: boolean; onToggle: (open: {batchTxs.length ? ( <>
- +
diff --git a/src/hooks/useDraftBatch.ts b/src/hooks/useDraftBatch.ts index 5dcf1f536e..a4794a24e6 100644 --- a/src/hooks/useDraftBatch.ts +++ b/src/hooks/useDraftBatch.ts @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from '@/store' import useChainId from './useChainId' import useSafeAddress from './useSafeAddress' import type { DraftBatchItem } from '@/store/batchSlice' -import { selectBatchBySafe, addTx, removeTx, setBatch } from '@/store/batchSlice' +import { selectBatchBySafe, addTx, removeTx } from '@/store/batchSlice' import { type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import { BATCH_EVENTS, trackEvent } from '@/services/analytics' import { txDispatch, TxEvent } from '@/services/tx/txEvents' @@ -43,22 +43,7 @@ export const useUpdateBatch = () => { [dispatch, chainId, safeAddress], ) - const onSet = useCallback( - (items: DraftBatchItem[]) => { - dispatch( - setBatch({ - chainId, - safeAddress, - items, - }), - ) - - trackEvent({ ...BATCH_EVENTS.BATCH_REORDER }) - }, - [dispatch, chainId, safeAddress], - ) - - return [onAdd, onDelete, onSet] as const + return [onAdd, onDelete] as const } export const useDraftBatch = (): DraftBatchItem[] => { diff --git a/src/services/analytics/events/batching.ts b/src/services/analytics/events/batching.ts index 91c42ebe90..e6f312a390 100644 --- a/src/services/analytics/events/batching.ts +++ b/src/services/analytics/events/batching.ts @@ -16,11 +16,6 @@ export const BATCH_EVENTS = { action: 'Tx added to batch', category, }, - // On reorder of batch items - BATCH_REORDER: { - action: 'Batch reorder', - category, - }, // When batch item details are expanded BATCH_EXPAND_TX: { action: 'Expand batched tx', diff --git a/src/store/batchSlice.ts b/src/store/batchSlice.ts index 6a796b4526..4591aaad26 100644 --- a/src/store/batchSlice.ts +++ b/src/store/batchSlice.ts @@ -20,21 +20,6 @@ export const batchSlice = createSlice({ name: 'batch', initialState, reducers: { - // Set a batch (used for reordering) - setBatch: ( - state, - action: PayloadAction<{ - chainId: string - safeAddress: string - items: DraftBatchItem[] - }>, - ) => { - const { chainId, safeAddress, items } = action.payload - state[chainId] = state[chainId] || {} - // @ts-expect-error - state[chainId][safeAddress] = items - }, - // Add a tx to the batch addTx: ( state, @@ -47,9 +32,11 @@ export const batchSlice = createSlice({ const { chainId, safeAddress, txDetails } = action.payload state[chainId] = state[chainId] || {} state[chainId][safeAddress] = state[chainId][safeAddress] || [] + // @ts-expect-error state[chainId][safeAddress].push({ id: Math.random().toString(36).slice(2), timestamp: Date.now(), + // @ts-expect-error txDetails, }) }, @@ -71,7 +58,7 @@ export const batchSlice = createSlice({ }, }) -export const { setBatch, addTx, removeTx } = batchSlice.actions +export const { addTx, removeTx } = batchSlice.actions const selectAllBatches = (state: RootState): BatchTxsState => { return state[batchSlice.name] || initialState diff --git a/yarn.lock b/yarn.lock index 740cbdb140..3f5715a29c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1346,13 +1346,6 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== -"@emotion/is-prop-valid@^0.8.2": - version "0.8.8" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" - integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== - dependencies: - "@emotion/memoize" "0.7.4" - "@emotion/is-prop-valid@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc" @@ -1360,11 +1353,6 @@ dependencies: "@emotion/memoize" "^0.8.1" -"@emotion/memoize@0.7.4": - version "0.7.4" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" - integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== - "@emotion/memoize@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" @@ -12173,15 +12161,6 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -framer-motion@^10.13.1: - version "10.16.4" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-10.16.4.tgz#30279ef5499b8d85db3a298ee25c83429933e9f8" - integrity sha512-p9V9nGomS3m6/CALXqv6nFGMuFOxbWsmaOrdmhyQimMIlLl3LC7h7l86wge/Js/8cRu5ktutS/zlzgR7eBOtFA== - dependencies: - tslib "^2.4.0" - optionalDependencies: - "@emotion/is-prop-valid" "^0.8.2" - fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" From 53162c664baa640fe11c4dd7fb5174484b21b148 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Tue, 28 May 2024 14:50:08 +0200 Subject: [PATCH 021/154] Tests: add component tests for tx history filter (#3758) * tests: add component tests for tx history filter * tests: update tests --- .../TxFilterForm/TxFilterForm.test.tsx | 227 ++++++++++++++++++ .../transactions/TxFilterForm/index.tsx | 9 +- 2 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 src/components/transactions/TxFilterForm/TxFilterForm.test.tsx diff --git a/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx b/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx new file mode 100644 index 0000000000..fc9a83e9ae --- /dev/null +++ b/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx @@ -0,0 +1,227 @@ +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 TxFilterForm from './index' +import { useRouter } from 'next/router' + +jest.mock('next/router', () => ({ + useRouter: jest.fn(), +})) + +const mockRouter = { + query: {}, + pathname: '', + push: jest.fn(), +} + +const toggleFilter = jest.fn() + +const fromDate = '20/01/2021' +const toDate = '20/01/2020' +const placeholder = 'dd/mm/yyyy' +const errorMsgFormat = 'Invalid address format' + +describe('TxFilterForm Component Tests', () => { + beforeEach(() => { + ;(useRouter as jest.Mock).mockReturnValue(mockRouter) + }) + + const renderComponent = () => render() + + it('Verify that when an end date is behind a start date, there are validation rules applied', async () => { + renderComponent() + + const errorMsgEndDate = 'Must be after "From" date' + const errorMsgStartDate = 'Must be before "To" date' + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + }) + + expect(fromDateInput).toHaveValue(fromDate) + expect(toDateInput).toHaveValue(toDate) + + expect(await screen.findByText(errorMsgEndDate, { selector: 'label' })).toBeInTheDocument() + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: '' } }) + fireEvent.change(toDateInput, { target: { value: '' } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + }) + + expect(toDateInput).toHaveValue(toDate) + expect(fromDateInput).toHaveValue(fromDate) + + expect(await screen.findByText(errorMsgStartDate, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify there is error when start and end date contain far future dates', async () => { + renderComponent() + + const futureDate = '20/01/2036' + const errorMsgFutureDate = 'Date cannot be in the future' + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + fireEvent.change(toDateInput, { target: { value: futureDate } }) + }) + + expect(await screen.findByText(errorMsgFutureDate, { selector: 'label' })).toBeInTheDocument() + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: futureDate } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + }) + + expect(await screen.findByText(errorMsgFutureDate, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify that when entering invalid characters in token filed shows an error message', async () => { + renderComponent() + + const token = '694urt5' + + const tokenInput = screen.getByTestId('token-input').querySelector('input') as HTMLInputElement + + expect(tokenInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(tokenInput, { target: { value: token } }) + }) + + expect(await screen.findByText(errorMsgFormat, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify there is error when 0 is entered in amount field', async () => { + renderComponent() + + const errorMsgZero = 'The value must be greater than 0' + const amountInput = screen.getByTestId('amount-input').querySelector('input') as HTMLInputElement + + expect(amountInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(amountInput, { target: { value: '0' } }) + }) + + expect(await screen.findByText(errorMsgZero, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify that entering negative numbers and a non-numeric value in the amount filter is not allowed', async () => { + renderComponent() + + const amountInput = screen.getByTestId('amount-input').querySelector('input') as HTMLInputElement + + expect(amountInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(amountInput, { target: { value: '-1' } }) + }) + expect(amountInput).toHaveValue('1') + await act(async () => { + fireEvent.change(amountInput, { target: { value: 'hrtyu' } }) + }) + expect(amountInput).toHaveValue('') + }) + + it('Verify that characters and negative numbers cannot be entered in nonce filed', async () => { + renderComponent() + + const outgoingRadio = screen.getByLabelText('Outgoing') + fireEvent.click(outgoingRadio) + + const nonceInput = screen.getByTestId('nonce-input').querySelector('input') as HTMLInputElement + + expect(nonceInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(nonceInput, { target: { value: '-1' } }) + }) + expect(nonceInput).toHaveValue('1') + await act(async () => { + fireEvent.change(nonceInput, { target: { value: 'hrtyu' } }) + }) + expect(nonceInput).toHaveValue('') + }) + + it('Verify that entering random characters in module field shows error', async () => { + renderComponent() + + const outgoingRadio = screen.getByLabelText('Module-based') + fireEvent.click(outgoingRadio) + + const addressInput = screen.getByTestId('address-item').querySelector('input') as HTMLInputElement + + expect(addressInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(addressInput, { target: { value: 'hrtyu' } }) + }) + expect(await screen.findByText(errorMsgFormat, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify when filter is cleared, the filter modal is still displayed', async () => { + renderComponent() + + const fromDate1 = '20/01/2021' + const toDate1 = '20/01/2022' + + const clearButton = screen.getByTestId('clear-btn') + const modal = screen.getByTestId('filter-modal') + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate1 } }) + fireEvent.change(toDateInput, { target: { value: toDate1 } }) + }) + + expect(fromDateInput).toHaveValue(fromDate1) + expect(toDateInput).toHaveValue(toDate1) + + await act(async () => { + fireEvent.click(clearButton) + }) + + expect(fromDateInput).toHaveValue('') + expect(toDateInput).toHaveValue('') + expect(modal).toBeInTheDocument() + }) + + it('Verify when filter is applied, it disappears from the view', async () => { + renderComponent() + + const fromDate = '20/01/2020' + const toDate = '20/01/2021' + + const applyButton = screen.getByTestId('apply-btn') + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + }) + + expect(fromDateInput).toHaveValue(fromDate) + expect(toDateInput).toHaveValue(toDate) + + await act(async () => { + fireEvent.click(applyButton) + }) + + // Check that toggleFilter hide filter trigger has been called + expect(toggleFilter).toHaveBeenCalled() + }) +}) diff --git a/src/components/transactions/TxFilterForm/index.tsx b/src/components/transactions/TxFilterForm/index.tsx index dc07cb4c9e..2d31eb0232 100644 --- a/src/components/transactions/TxFilterForm/index.tsx +++ b/src/components/transactions/TxFilterForm/index.tsx @@ -120,7 +120,7 @@ const TxFilterForm = ({ toggleFilter }: { toggleFilter: () => void }): ReactElem
- + palette.primary.light }}>Transaction type @@ -185,6 +185,7 @@ const TxFilterForm = ({ toggleFilter }: { toggleFilter: () => void }): ReactElem }} render={({ field, fieldState }) => ( void }): ReactElem {isIncomingFilter && ( void }): ReactElem }} render={({ field, fieldState }) => ( void }): ReactElem - - From e1156b620761cc6c3be6cde53787d59fc65093c2 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Tue, 28 May 2024 16:27:53 +0200 Subject: [PATCH 022/154] Fix: rearrange Settings tabs (#3751) --- cypress/e2e/pages/owners.pages.js | 2 +- cypress/support/constants.js | 2 +- .../common/SocialLoginDeprecation/index.tsx | 2 +- src/components/common/WalletInfo/index.tsx | 2 +- .../settings/ContractVersion/index.tsx | 67 ++++++++++--------- .../settings/RequiredConfirmations/index.tsx | 25 ++++--- src/components/settings/SafeModules/index.tsx | 2 +- .../settings/SecurityLogin/index.tsx | 7 +- .../settings/SettingsHeader/index.test.tsx | 8 +-- .../sidebar/SidebarNavigation/config.tsx | 8 +-- .../tx-flow/flows/SignMessage/SignMessage.tsx | 4 +- src/config/routes.ts | 4 +- .../{security-login.tsx => security.tsx} | 6 +- src/pages/settings/setup.tsx | 5 +- 14 files changed, 74 insertions(+), 70 deletions(-) rename src/pages/settings/{security-login.tsx => security.tsx} (69%) diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index 9f1a38a52d..1f6a142fc1 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -242,5 +242,5 @@ export function verifyThreshold(startValue, endValue) { cy.get(thresholdInput).parent().click() cy.get(thresholdList).contains(endValue).should('be.visible') cy.get(thresholdList).find('li').should('have.length', endValue) - cy.get('body').click() + cy.get('body').click({ force: true }) } diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 1b9fec42e6..8b883c0224 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -62,7 +62,7 @@ export const getPermissionsUrl = '/get-permissions' export const appSettingsUrl = '/settings/safe-apps' export const setupUrl = '/settings/setup?safe=' export const dataSettingsUrl = '/settings/data?safe=' -export const securityUrl = '/settings/security-login?safe=' +export const securityUrl = '/settings/setup?safe=' export const invalidAppUrl = 'https://my-invalid-custom-app.com/manifest.json' export const validAppUrlJson = 'https://my-valid-custom-app.com/manifest.json' export const validAppUrl = 'https://my-valid-custom-app.com' diff --git a/src/components/common/SocialLoginDeprecation/index.tsx b/src/components/common/SocialLoginDeprecation/index.tsx index da8d9a9b38..4147386b40 100644 --- a/src/components/common/SocialLoginDeprecation/index.tsx +++ b/src/components/common/SocialLoginDeprecation/index.tsx @@ -18,7 +18,7 @@ const SocialLoginDeprecation = () => { } const settingsPage = { - pathname: AppRoutes.settings.securityLogin, + pathname: AppRoutes.settings.security, query: router.query, } diff --git a/src/components/common/WalletInfo/index.tsx b/src/components/common/WalletInfo/index.tsx index f083e57895..31c929be03 100644 --- a/src/components/common/WalletInfo/index.tsx +++ b/src/components/common/WalletInfo/index.tsx @@ -71,7 +71,7 @@ export const WalletInfo = ({ {socialWalletService && !socialWalletService.isMFAEnabled() && ( - + - )} - - - ) : ( - - Latest version - - ) - ) : null} - + + 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 + + + + {(isOk) => ( + + )} + + + )} ) } diff --git a/src/components/settings/RequiredConfirmations/index.tsx b/src/components/settings/RequiredConfirmations/index.tsx index 61db4d9900..2ccc9fc094 100644 --- a/src/components/settings/RequiredConfirmations/index.tsx +++ b/src/components/settings/RequiredConfirmations/index.tsx @@ -19,23 +19,22 @@ export const RequiredConfirmation = ({ threshold, owners }: { threshold: number; - Any transaction requires the confirmation of: - + Any transaction requires the confirmation of: + + {threshold} out of {owners} signers. {owners > 1 && ( - - - {(isOk) => ( - - - - )} - - + + {(isOk) => ( + + + + )} + )} diff --git a/src/components/settings/SafeModules/index.tsx b/src/components/settings/SafeModules/index.tsx index 88c85b9bea..204448ce3e 100644 --- a/src/components/settings/SafeModules/index.tsx +++ b/src/components/settings/SafeModules/index.tsx @@ -65,7 +65,7 @@ const SafeModules = () => { - Safe Account modules + Safe modules diff --git a/src/components/settings/SecurityLogin/index.tsx b/src/components/settings/SecurityLogin/index.tsx index 944df3bac7..6b921dc0c6 100644 --- a/src/components/settings/SecurityLogin/index.tsx +++ b/src/components/settings/SecurityLogin/index.tsx @@ -3,7 +3,6 @@ import SocialSignerMFA from './SocialSignerMFA' import SocialSignerExport from './SocialSignerExport' import useWallet from '@/hooks/wallets/useWallet' import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import SpendingLimits from '../SpendingLimits' import dynamic from 'next/dynamic' import { useIsRecoverySupported } from '@/features/recovery/hooks/useIsRecoverySupported' import SecuritySettings from '../SecuritySettings' @@ -19,6 +18,8 @@ const SecurityLogin = () => { {isRecoverySupported && } + + {isSocialLogin && ( <> @@ -48,10 +49,6 @@ const SecurityLogin = () => { )} - - - - ) } diff --git a/src/components/settings/SettingsHeader/index.test.tsx b/src/components/settings/SettingsHeader/index.test.tsx index f50dc917fc..978ac05a90 100644 --- a/src/components/settings/SettingsHeader/index.test.tsx +++ b/src/components/settings/SettingsHeader/index.test.tsx @@ -33,13 +33,13 @@ describe('SettingsHeader', () => { expect(result.getByText('Notifications')).toBeInTheDocument() }) - it('displays Security & Login if connected wallet is a social signer', () => { + it('displays Security tab if connected wallet is a social signer', () => { const mockWallet = connectedWalletBuilder().with({ label: ONBOARD_MPC_MODULE_LABEL }).build() jest.spyOn(wallet, 'default').mockReturnValue(mockWallet) const result = render() - expect(result.getByText('Security & Login')).toBeInTheDocument() + expect(result.getByText('Security')).toBeInTheDocument() }) }) @@ -66,13 +66,13 @@ describe('SettingsHeader', () => { expect(result.getByText('Notifications')).toBeInTheDocument() }) - it('displays Security & Login if connected wallet is a social signer', () => { + it('displays Security if connected wallet is a social signer', () => { const mockWallet = connectedWalletBuilder().with({ label: ONBOARD_MPC_MODULE_LABEL }).build() jest.spyOn(wallet, 'default').mockReturnValue(mockWallet) const result = render() - expect(result.getByText('Security & Login')).toBeInTheDocument() + expect(result.getByText('Security')).toBeInTheDocument() }) }) }) diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index 27cc371aeb..6797127c73 100644 --- a/src/components/sidebar/SidebarNavigation/config.tsx +++ b/src/components/sidebar/SidebarNavigation/config.tsx @@ -90,8 +90,8 @@ export const settingsNavItems = [ href: AppRoutes.settings.appearance, }, { - label: 'Security & Login', - href: AppRoutes.settings.securityLogin, + label: 'Security', + href: AppRoutes.settings.security, }, { label: 'Notifications', @@ -129,8 +129,8 @@ export const generalSettingsNavItems = [ href: AppRoutes.settings.notifications, }, { - label: 'Security & Login', - href: AppRoutes.settings.securityLogin, + label: 'Security', + href: AppRoutes.settings.security, }, { label: 'Data', diff --git a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx index e0aebcf1cb..7531d2a3dc 100644 --- a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx +++ b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx @@ -166,7 +166,7 @@ const BlindSigningWarning = ({ return ( This request involves{' '} - + blind signing , which can lead to unpredictable outcomes. @@ -176,7 +176,7 @@ const BlindSigningWarning = ({ ) : ( <> If you wish to proceed, you must first{' '} - + enable blind signing . diff --git a/src/config/routes.ts b/src/config/routes.ts index 1c2a1ad022..314ddfda74 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -2,12 +2,12 @@ export const AppRoutes = { '404': '/404', wc: '/wc', terms: '/terms', + swap: '/swap', privacy: '/privacy', licenses: '/licenses', index: '/', imprint: '/imprint', home: '/home', - swap: '/swap', cookie: '/cookie', addressBook: '/address-book', addOwner: '/addOwner', @@ -28,7 +28,7 @@ export const AppRoutes = { }, settings: { setup: '/settings/setup', - securityLogin: '/settings/security-login', + security: '/settings/security', notifications: '/settings/notifications', modules: '/settings/modules', index: '/settings', diff --git a/src/pages/settings/security-login.tsx b/src/pages/settings/security.tsx similarity index 69% rename from src/pages/settings/security-login.tsx rename to src/pages/settings/security.tsx index 856af7d8a1..1931fc7145 100644 --- a/src/pages/settings/security-login.tsx +++ b/src/pages/settings/security.tsx @@ -4,11 +4,11 @@ import Head from 'next/head' import SettingsHeader from '@/components/settings/SettingsHeader' import SecurityLogin from '@/components/settings/SecurityLogin' -const SecurityLoginPage: NextPage = () => { +const SecurityPage: NextPage = () => { return ( <> - {'Safe{Wallet} – Settings – Security & Login'} + {'Safe{Wallet} – Settings – Security'} @@ -20,4 +20,4 @@ const SecurityLoginPage: NextPage = () => { ) } -export default SecurityLoginPage +export default SecurityPage diff --git a/src/pages/settings/setup.tsx b/src/pages/settings/setup.tsx index 3229762803..92da73d9e2 100644 --- a/src/pages/settings/setup.tsx +++ b/src/pages/settings/setup.tsx @@ -8,6 +8,7 @@ import { RequiredConfirmation } from '@/components/settings/RequiredConfirmation import useSafeInfo from '@/hooks/useSafeInfo' import SettingsHeader from '@/components/settings/SettingsHeader' import DelegatesList from '@/components/settings/DelegatesList' +import SpendingLimits from '@/components/settings/SpendingLimits' const Setup: NextPage = () => { const { safe, safeLoaded } = useSafeInfo() @@ -57,12 +58,14 @@ const Setup: NextPage = () => { - + + + From f1bad1f3bf0ebed71faa41b8b64e584f307f7fe4 Mon Sep 17 00:00:00 2001 From: James Mealy Date: Wed, 29 May 2024 09:37:21 +0100 Subject: [PATCH 023/154] fix: do not show nonce conflict warning for batch transactions. (#3608) From 01878b514fbeb555259c64111b65c6378b30e370 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Wed, 29 May 2024 10:44:43 +0200 Subject: [PATCH 024/154] Chore: remove social login (#3753) --- README.md | 2 - cypress/e2e/pages/create_wallet.pages.js | 16 - .../e2e/regression/create_safe_google.cy.js | 53 -- package.json | 3 - .../common/ConnectWallet/ConnectionCenter.tsx | 80 +- .../common/ConnectWallet/WalletDetails.tsx | 9 +- .../__tests__/ConnectionCenter.test.tsx | 13 +- .../common/ConnectWallet/styles.module.css | 4 - .../common/NetworkSelector/index.tsx | 15 +- src/components/common/PageLayout/index.tsx | 3 - .../common/SocialLoginDeprecation/index.tsx | 46 -- .../common/SocialLoginInfo/index.tsx | 75 -- .../common/SocialLoginInfo/styles.module.css | 33 - .../common/SocialSigner/PasswordRecovery.tsx | 115 --- .../__tests__/PasswordRecovery.test.tsx | 48 -- .../__tests__/SocialSignerLogin.test.tsx | 172 ---- src/components/common/SocialSigner/index.tsx | 146 ---- .../common/SocialSigner/styles.module.css | 34 - .../common/WalletInfo/index.test.tsx | 122 --- src/components/common/WalletInfo/index.tsx | 83 +- .../common/WalletOverview/index.tsx | 18 - .../common/WalletOverview/styles.module.css | 4 - src/components/new-safe/create/index.tsx | 11 +- .../create/steps/ReviewStep/index.test.tsx | 20 +- .../create/steps/ReviewStep/index.tsx | 61 +- .../ExportMPCAccountModal.tsx | 147 ---- .../SocialSignerExport/index.tsx | 47 -- .../SocialSignerExport/styles.module.css | 10 - .../SocialSignerMFA/PasswordInput.tsx | 44 - .../SocialSignerMFA/index.test.tsx | 45 - .../SecurityLogin/SocialSignerMFA/index.tsx | 298 ------- .../SocialSignerMFA/styles.module.css | 76 -- .../settings/SecurityLogin/index.tsx | 38 +- .../settings/SettingsHeader/index.test.tsx | 21 - src/components/welcome/NewSafeSocial.tsx | 81 -- .../welcome/WelcomeLogin/WalletLogin.tsx | 5 +- src/components/welcome/WelcomeLogin/index.tsx | 22 +- src/config/routes.ts | 1 - .../counterfactual/ActivateAccountFlow.tsx | 6 +- src/hooks/useIsSidebarRoute.ts | 1 - .../wallets/__tests__/useOnboard.test.ts | 133 +-- src/hooks/wallets/consts.ts | 2 - .../wallets/mpc/__tests__/useMPC.test.ts | 172 ---- src/hooks/wallets/mpc/useMPC.ts | 87 -- .../wallets/mpc/useRehydrateSocialWallet.ts | 73 -- src/hooks/wallets/mpc/useSocialWallet.ts | 19 - src/hooks/wallets/useOnboard.ts | 14 +- src/hooks/wallets/wallets.ts | 11 - src/pages/_app.tsx | 5 - src/pages/welcome/social-login.tsx | 17 - src/service-workers/index.ts | 3 - src/service-workers/mpc-core-kit-sw.ts | 325 -------- src/services/analytics/events/mpcWallet.ts | 61 -- src/services/mpc/PasswordRecoveryModal.tsx | 42 - src/services/mpc/SocialLoginModule.ts | 138 ---- src/services/mpc/SocialWalletService.ts | 207 ----- .../mpc/__mocks__/SocialWalletService.ts | 74 -- .../mpc/__tests__/SocialWalletService.test.ts | 340 -------- src/services/mpc/__tests__/module.test.ts | 125 --- src/services/mpc/config.ts | 40 - src/services/mpc/icon.ts | 12 - src/services/mpc/interfaces.ts | 52 -- .../mpc/recovery/DeviceShareRecovery.ts | 59 -- .../mpc/recovery/SecurityQuestionRecovery.ts | 48 -- src/tests/builders/wallet.ts | 2 +- src/utils/chains.ts | 1 - src/utils/wallets.ts | 6 +- yarn.lock | 778 +----------------- 68 files changed, 66 insertions(+), 4808 deletions(-) delete mode 100644 cypress/e2e/regression/create_safe_google.cy.js delete mode 100644 src/components/common/SocialLoginDeprecation/index.tsx delete mode 100644 src/components/common/SocialLoginInfo/index.tsx delete mode 100644 src/components/common/SocialLoginInfo/styles.module.css delete mode 100644 src/components/common/SocialSigner/PasswordRecovery.tsx delete mode 100644 src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx delete mode 100644 src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx delete mode 100644 src/components/common/SocialSigner/index.tsx delete mode 100644 src/components/common/SocialSigner/styles.module.css delete mode 100644 src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx delete mode 100644 src/components/settings/SecurityLogin/SocialSignerExport/index.tsx delete mode 100644 src/components/settings/SecurityLogin/SocialSignerExport/styles.module.css delete mode 100644 src/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput.tsx delete mode 100644 src/components/settings/SecurityLogin/SocialSignerMFA/index.test.tsx delete mode 100644 src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx delete mode 100644 src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css delete mode 100644 src/components/welcome/NewSafeSocial.tsx delete mode 100644 src/hooks/wallets/mpc/__tests__/useMPC.test.ts delete mode 100644 src/hooks/wallets/mpc/useMPC.ts delete mode 100644 src/hooks/wallets/mpc/useRehydrateSocialWallet.ts delete mode 100644 src/hooks/wallets/mpc/useSocialWallet.ts delete mode 100644 src/pages/welcome/social-login.tsx delete mode 100644 src/service-workers/mpc-core-kit-sw.ts delete mode 100644 src/services/analytics/events/mpcWallet.ts delete mode 100644 src/services/mpc/PasswordRecoveryModal.tsx delete mode 100644 src/services/mpc/SocialLoginModule.ts delete mode 100644 src/services/mpc/SocialWalletService.ts delete mode 100644 src/services/mpc/__mocks__/SocialWalletService.ts delete mode 100644 src/services/mpc/__tests__/SocialWalletService.test.ts delete mode 100644 src/services/mpc/__tests__/module.test.ts delete mode 100644 src/services/mpc/config.ts delete mode 100644 src/services/mpc/icon.ts delete mode 100644 src/services/mpc/interfaces.ts delete mode 100644 src/services/mpc/recovery/DeviceShareRecovery.ts delete mode 100644 src/services/mpc/recovery/SecurityQuestionRecovery.ts diff --git a/README.md b/README.md index 6e426486de..9a2b592618 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,6 @@ Here's the list of all the environment variables: | `NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION` | FCM vapid key on production | `NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING` | FCM `initializeApp` options on staging | `NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING` | FCM vapid key on staging -| `NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION` | Web3Auth and Google credentials (production) -| `NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING` | Web3Auth and Google credentials (staging) | `NEXT_PUBLIC_SPINDL_SDK_KEY` | [Spindl](http://spindl.xyz) SDK key If you don't provide some of the variables, the corresponding features will be disabled in the UI. diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index ba859cbb02..342330fa96 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -87,22 +87,6 @@ export function verifySponsorMessageIsPresent() { cy.get(networkFeeSection).contains(sponsorStr).should('exist') } -export function verifyGoogleConnectBtnIsDisabled() { - cy.get(googleConnectBtn).should('be.disabled') -} - -export function verifyGoogleConnectBtnIsEnabled() { - cy.get(googleConnectBtn).should('not.be.disabled') -} - -export function verifyGoogleSignin() { - return cy.get(googleSignedinBtn).should('exist') -} - -export function verifyGoogleAccountInfoInHeader() { - return cy.get(accountInfoHeader).should('exist') -} - export function verifyPolicy1_1() { cy.contains(policy1_2).should('exist') // TOD: Need data-cy for containers diff --git a/cypress/e2e/regression/create_safe_google.cy.js b/cypress/e2e/regression/create_safe_google.cy.js deleted file mode 100644 index 4a946f39ea..0000000000 --- a/cypress/e2e/regression/create_safe_google.cy.js +++ /dev/null @@ -1,53 +0,0 @@ -import * as constants from '../../support/constants' -import * as main from '../pages/main.page' -import * as createwallet from '../pages/create_wallet.pages' -import * as owner from '../pages/owners.pages' -import * as navigation from '../pages/navigation.page' - -describe('Safe creation Google tests', () => { - beforeEach(() => { - cy.visit(constants.welcomeUrl + '?chain=sep') - cy.clearLocalStorage() - main.acceptCookies() - // TODO: Need credentials to finish API Google login - // createwallet.loginGoogleAPI() - }) - - // TODO: Clarify requirements - it.skip('Verify that "Connect with Google" option is disabled for the networks without Relay on the Welcome page', () => { - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() - createwallet.selectNetwork(constants.networks.polygon) - createwallet.verifyGoogleConnectBtnIsDisabled() - }) - - it.skip('Verify a successful connection with google', () => { - createwallet.verifyGoogleSignin() - }) - - it.skip('Verify Google account info in the header after account connection', () => { - createwallet.verifyGoogleAccountInfoInHeader() - }) - - it.skip('Verify a successful safe creation with a Google account', { defaultCommandTimeout: 90000 }, () => { - createwallet.verifyGoogleSignin().click() - createwallet.clickOnContinueWithWalletBtn() - createwallet.verifyOwnerInfoIsPresent() - createwallet.clickOnReviewStepNextBtn() - createwallet.verifySafeIsBeingCreated() - createwallet.verifySafeCreationIsComplete() - }) - - it.skip('Verify a successful transaction creation with Google account', { defaultCommandTimeout: 90000 }, () => { - createwallet.verifyGoogleSignin().click() - createwallet.clickOnContinueWithWalletBtn() - createwallet.clickOnReviewStepNextBtn() - createwallet.verifySafeCreationIsComplete() - navigation.clickOnSideNavigation(navigation.sideNavSettingsIcon) - owner.openAddOwnerWindow() - owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) - owner.clickOnNextBtn() - main.clickOnExecuteBtn() - owner.verifyOwnerTransactionComplted() - }) -}) diff --git a/package.json b/package.json index 30af401943..9c94ff2e24 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "@safe-global/safe-modules-deployments": "^1.2.0", "@sentry/react": "^7.91.0", "@spindl-xyz/attribution-lite": "^1.4.0", - "@tkey-mpc/common-types": "^8.2.2", "@truffle/hdwallet-provider": "^2.1.4", "@walletconnect/utils": "^2.11.3", "@walletconnect/web3wallet": "^1.10.3", @@ -75,9 +74,7 @@ "@web3-onboard/ledger": "2.3.2", "@web3-onboard/trezor": "^2.4.2", "@web3-onboard/walletconnect": "^2.5.4", - "@web3auth/mpc-core-kit": "^1.1.3", "blo": "^1.1.1", - "bn.js": "^5.2.1", "classnames": "^2.3.1", "date-fns": "^2.30.0", "ethers": "^6.11.1", diff --git a/src/components/common/ConnectWallet/ConnectionCenter.tsx b/src/components/common/ConnectWallet/ConnectionCenter.tsx index 1af6af79d8..9c81d6f3c0 100644 --- a/src/components/common/ConnectWallet/ConnectionCenter.tsx +++ b/src/components/common/ConnectWallet/ConnectionCenter.tsx @@ -1,80 +1,14 @@ import ConnectWalletButton from '@/components/common/ConnectWallet/ConnectWalletButton' -import { useHasFeature } from '@/hooks/useChains' -import { FEATURES } from '@/utils/chains' -import madProps from '@/utils/mad-props' -import { Popover, ButtonBase, Typography, Paper, Box } from '@mui/material' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import ExpandLessIcon from '@mui/icons-material/ExpandLess' -import classnames from 'classnames' -import { useState, type MouseEvent, type ReactElement } from 'react' - -import KeyholeIcon from '@/components/common/icons/KeyholeIcon' -import WalletDetails from '@/components/common/ConnectWallet/WalletDetails' - +import { Box } from '@mui/material' +import { type ReactElement } from 'react' import css from '@/components/common/ConnectWallet/styles.module.css' -export const ConnectionCenter = ({ isSocialLoginEnabled }: { isSocialLoginEnabled: boolean }): ReactElement => { - const [anchorEl, setAnchorEl] = useState(null) - const open = !!anchorEl - - const handleClick = (event: MouseEvent) => { - setAnchorEl(event.currentTarget) - } - - const handleClose = () => { - setAnchorEl(null) - } - - const ExpandIcon = open ? ExpandLessIcon : ExpandMoreIcon - - if (!isSocialLoginEnabled) { - return ( - - - - ) - } - +const ConnectionCenter = (): ReactElement => { return ( - <> - - - - - Not connected - palette.error.main }}> - Connect wallet - - - - - - - - - - - - + + + ) } -const useIsSocialLoginEnabled = () => useHasFeature(FEATURES.SOCIAL_LOGIN) - -export default madProps(ConnectionCenter, { - isSocialLoginEnabled: useIsSocialLoginEnabled, -}) +export default ConnectionCenter diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx index 414a93a4e2..8bab108de3 100644 --- a/src/components/common/ConnectWallet/WalletDetails.tsx +++ b/src/components/common/ConnectWallet/WalletDetails.tsx @@ -1,13 +1,8 @@ -import { Box, Divider, Skeleton, SvgIcon, Typography } from '@mui/material' -import dynamic from 'next/dynamic' +import { Box, Divider, SvgIcon, Typography } from '@mui/material' import type { ReactElement } from 'react' import LockIcon from '@/public/images/common/lock.svg' -const SocialSigner = dynamic(() => import('@/components/common/SocialSigner'), { - loading: () => , -}) - import WalletLogin from '@/components/welcome/WelcomeLogin/WalletLogin' const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement => { @@ -26,8 +21,6 @@ const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement = or - - ) } diff --git a/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx b/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx index 5ca16f4512..844eb8a112 100644 --- a/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx +++ b/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx @@ -1,16 +1,9 @@ -import { ConnectionCenter } from '@/components/common/ConnectWallet/ConnectionCenter' +import ConnectionCenter from '@/components/common/ConnectWallet/ConnectionCenter' import { render } from '@/tests/test-utils' describe('ConnectionCenter', () => { - it('displays a Connect wallet button if the social login feature is enabled', () => { - const { getByText, queryByText } = render() - - expect(getByText('Connect wallet')).toBeInTheDocument() - expect(queryByText('Connect')).not.toBeInTheDocument() - }) - - it('displays the ConnectWalletButton if the social login feature is disabled', () => { - const { getByText, queryByText } = render() + it('displays the ConnectWalletButton', () => { + const { getByText, queryByText } = render() expect(queryByText('Connect wallet')).not.toBeInTheDocument() expect(getByText('Connect')).toBeInTheDocument() diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index dcff10dfc8..483c0bf6c5 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -80,10 +80,6 @@ } @media (max-width: 599.95px) { - .socialLoginInfo > div > div { - display: none; - } - .notConnected { display: none; } diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index 553cd6292f..960ddc4e79 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -15,12 +15,8 @@ import { type ReactElement, useMemo } from 'react' import { useCallback } from 'react' import { AppRoutes } from '@/config/routes' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' -import useWallet from '@/hooks/wallets/useWallet' -import { isSocialWalletEnabled } from '@/hooks/wallets/wallets' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => { - const wallet = useWallet() const isDarkMode = useDarkMode() const theme = useTheme() const { configs } = useChains() @@ -64,24 +60,17 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => } } - const isSocialLogin = isSocialLoginWallet(wallet?.label) - const renderMenuItem = useCallback( (value: string, chain: ChainInfo) => { return ( - + ) }, - [getNetworkLink, isSocialLogin, props.onChainSelect], + [getNetworkLink, props.onChainSelect], ) return configs.length ? ( diff --git a/src/components/common/PageLayout/index.tsx b/src/components/common/PageLayout/index.tsx index a3eac5d5a7..e0f00b8e4f 100644 --- a/src/components/common/PageLayout/index.tsx +++ b/src/components/common/PageLayout/index.tsx @@ -9,7 +9,6 @@ import SideDrawer from './SideDrawer' import { useIsSidebarRoute } from '@/hooks/useIsSidebarRoute' import { TxModalContext } from '@/components/tx-flow' import BatchSidebar from '@/components/batch/BatchSidebar' -import SocialLoginDeprecation from '@/components/common/SocialLoginDeprecation' const PageLayout = ({ pathname, children }: { pathname: string; children: ReactElement }): ReactElement => { const [isSidebarRoute, isAnimated] = useIsSidebarRoute(pathname) @@ -36,8 +35,6 @@ const PageLayout = ({ pathname, children }: { pathname: string; children: ReactE })} >
- - {children}
diff --git a/src/components/common/SocialLoginDeprecation/index.tsx b/src/components/common/SocialLoginDeprecation/index.tsx deleted file mode 100644 index 4147386b40..0000000000 --- a/src/components/common/SocialLoginDeprecation/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Alert } from '@mui/material' -import Link from 'next/link' -import useWallet from '@/hooks/wallets/useWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import { AppRoutes } from '@/config/routes' -import { useRouter } from 'next/router' - -const SocialLoginDeprecation = () => { - const router = useRouter() - const wallet = useWallet() - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - if (!isSocialLogin) return null - - const ownersPage = { - pathname: AppRoutes.settings.setup, - query: router.query, - } - - const settingsPage = { - pathname: AppRoutes.settings.security, - query: router.query, - } - - return ( - - The Social Login wallet is deprecated and will be removed on 01.07.2024. -
- Please{' '} - - - swap the signer - - {' '} - to a different wallet, or{' '} - - - export your private key - - {' '} - to avoid losing access to your Safe Account. -
- ) -} - -export default SocialLoginDeprecation diff --git a/src/components/common/SocialLoginInfo/index.tsx b/src/components/common/SocialLoginInfo/index.tsx deleted file mode 100644 index 00423fccba..0000000000 --- a/src/components/common/SocialLoginInfo/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import WalletBalance from '@/components/common/WalletBalance' -import { Badge, Box, Typography } from '@mui/material' -import css from './styles.module.css' -import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' -import CopyAddressButton from '@/components/common/CopyAddressButton' -import ExplorerButton from '@/components/common/ExplorerButton' -import { getBlockExplorerLink } from '@/utils/chains' -import { useAppSelector } from '@/store' -import { selectSettings } from '@/store/settingsSlice' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -const SocialLoginInfo = ({ - wallet, - chainInfo, - hideActions = false, - size = 28, - balance, - showBalance, -}: { - wallet: ConnectedWallet - chainInfo?: ChainInfo - hideActions?: boolean - size?: number - balance?: string - showBalance?: boolean -}) => { - const socialWalletService = useSocialWallet() - const userInfo = socialWalletService?.getUserInfo() - const prefix = chainInfo?.shortName - const link = chainInfo ? getBlockExplorerLink(chainInfo, wallet.address) : undefined - const settings = useAppSelector(selectSettings) - - if (!userInfo) return <> - - return ( - - - Profile Image - {!socialWalletService?.isMFAEnabled() && } - -
- - {userInfo.name} - - {showBalance ? ( - - - - ) : ( - - {userInfo.email} - - )} -
- {!hideActions && ( -
- - - - -
- )} -
- ) -} - -export default SocialLoginInfo diff --git a/src/components/common/SocialLoginInfo/styles.module.css b/src/components/common/SocialLoginInfo/styles.module.css deleted file mode 100644 index 0acd0bcdcf..0000000000 --- a/src/components/common/SocialLoginInfo/styles.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.profileImg { - border-radius: var(--space-2); - display: block; -} - -.bubble { - content: ''; - position: absolute; - right: 3px; - bottom: 3px; -} - -.bubble span { - outline: 1px solid var(--color-background-paper); -} - -.profileData { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 2px; -} - -.text { - font-size: 12px; - line-height: 14px; -} - -.actionButtons { - display: flex; - justify-self: flex-end; - margin-left: auto; -} diff --git a/src/components/common/SocialSigner/PasswordRecovery.tsx b/src/components/common/SocialSigner/PasswordRecovery.tsx deleted file mode 100644 index d1f09b634d..0000000000 --- a/src/components/common/SocialSigner/PasswordRecovery.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { - Typography, - FormControlLabel, - Checkbox, - Button, - Box, - Divider, - Grid, - LinearProgress, - FormControl, -} from '@mui/material' -import { useState } from 'react' -import Track from '@/components/common/Track' -import { FormProvider, useForm } from 'react-hook-form' -import PasswordInput from '@/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput' -import ErrorMessage from '@/components/tx/ErrorMessage' - -import css from './styles.module.css' - -type PasswordFormData = { - password: string -} - -export const PasswordRecovery = ({ - recoverFactorWithPassword, - onSuccess, -}: { - recoverFactorWithPassword: (password: string, storeDeviceFactor: boolean) => Promise - onSuccess?: (() => void) | undefined -}) => { - const [storeDeviceFactor, setStoreDeviceFactor] = useState(false) - - const formMethods = useForm({ - mode: 'all', - defaultValues: { - password: '', - }, - }) - - const { handleSubmit, formState } = formMethods - - const [error, setError] = useState() - - const onSubmit = async (data: PasswordFormData) => { - setError(undefined) - try { - await recoverFactorWithPassword(data.password, storeDeviceFactor) - - onSuccess?.() - } catch (e) { - setError('Incorrect password') - } - } - - const isDisabled = formState.isSubmitting - - return ( - - - - - - Verify your account - - - - - - Enter security password - - - This browser is not registered with your Account yet. Please enter your recovery password to restore - access to this Account. - - - - - - - - setStoreDeviceFactor((prev) => !prev)} /> - } - label="Do not ask again on this device" - /> - {error && {error}} - - - - - - - - - - - - - - ) -} diff --git a/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx b/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx deleted file mode 100644 index 488a33cccf..0000000000 --- a/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { fireEvent, render } from '@/tests/test-utils' -import { PasswordRecovery } from '@/components/common/SocialSigner/PasswordRecovery' -import { act, waitFor } from '@testing-library/react' - -describe('PasswordRecovery', () => { - it('displays an error if password is wrong', async () => { - const mockRecoverWithPassword = jest.fn(() => Promise.reject()) - const mockOnSuccess = jest.fn() - - const { getByText, getByLabelText } = render( - , - ) - - const passwordField = getByLabelText('Recovery password') - const submitButton = getByText('Submit') - - act(() => { - fireEvent.change(passwordField, { target: { value: 'somethingwrong' } }) - submitButton.click() - }) - - await waitFor(() => { - expect(mockOnSuccess).not.toHaveBeenCalled() - expect(getByText('Incorrect password')).toBeInTheDocument() - }) - }) - - it('calls onSuccess if password is correct', async () => { - const mockRecoverWithPassword = jest.fn(() => Promise.resolve()) - const mockOnSuccess = jest.fn() - - const { getByText, getByLabelText } = render( - , - ) - - const passwordField = getByLabelText('Recovery password') - const submitButton = getByText('Submit') - - act(() => { - fireEvent.change(passwordField, { target: { value: 'somethingCorrect' } }) - submitButton.click() - }) - - await waitFor(() => { - expect(mockOnSuccess).toHaveBeenCalled() - }) - }) -}) diff --git a/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx b/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx deleted file mode 100644 index 6c3c33a710..0000000000 --- a/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { act, render, waitFor } from '@/tests/test-utils' - -import { SocialSigner, _getSupportedChains } from '@/components/common/SocialSigner' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { COREKIT_STATUS, type UserInfo, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' -import SocialWalletService from '@/services/mpc/SocialWalletService' -import { TxModalProvider } from '@/components/tx-flow' -import { fireEvent } from '@testing-library/react' -import { type ISocialWalletService } from '@/services/mpc/interfaces' -import { connectedWalletBuilder } from '@/tests/builders/wallet' -import { chainBuilder } from '@/tests/builders/chains' -import PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal' - -jest.mock('@/services/mpc/SocialWalletService') - -const mockWallet = connectedWalletBuilder().with({ chainId: '5', label: ONBOARD_MPC_MODULE_LABEL }).build() - -describe('SocialSignerLogin', () => { - let mockSocialWalletService: ISocialWalletService - - beforeEach(() => { - jest.resetAllMocks() - - mockSocialWalletService = new SocialWalletService({} as unknown as Web3AuthMPCCoreKit) - }) - - it('should render continue with connected account when on gnosis chain', async () => { - const mockOnLogin = jest.fn() - - const result = render( - - - - , - ) - - await waitFor(() => { - expect(result.findByText('Continue as Test Testermann')).resolves.toBeDefined() - }) - - // We do not automatically invoke the callback as the user did not actively connect - expect(mockOnLogin).not.toHaveBeenCalled() - - const button = await result.findByRole('button') - button.click() - - expect(mockOnLogin).toHaveBeenCalled() - }) - - it('should render google login button if no wallet is connected on gnosis chain', async () => { - const mockOnLogin = jest.fn() - - const result = render( - - - - , - ) - - await waitFor(async () => { - expect(result.findByText('Continue with Google')).resolves.toBeDefined() - expect(await result.findByRole('button')).toBeEnabled() - }) - }) - - it('should display a Continue as button and call onLogin when clicked', () => { - const mockOnLogin = jest.fn() - mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.LOGGED_IN)) - - const result = render( - - - - , - ) - - expect(result.getByText('Continue as Test Testermann')).toBeInTheDocument() - - const button = result.getByRole('button') - button.click() - - expect(mockOnLogin).toHaveBeenCalled() - }) - - it('should display Password Recovery form and display a Continue as button when login succeeds', async () => { - const mockOnLogin = jest.fn() - mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.REQUIRED_SHARE)) - mockSocialWalletService.getUserInfo = jest.fn().mockReturnValue(undefined) - mockSocialWalletService.recoverAccountWithPassword = jest.fn(() => Promise.resolve(true)) - - const result = render( - - - - , - ) - - await waitFor(() => { - expect(result.findByText('Continue with Google')).resolves.toBeDefined() - }) - - // We do not automatically invoke the callback as the user did not actively connect - expect(mockOnLogin).not.toHaveBeenCalled() - - const button = await result.findByRole('button') - - act(() => { - button.click() - }) - - await waitFor(() => { - expect(result.findByText('Enter security password')).resolves.toBeDefined() - }) - - const passwordField = await result.findByLabelText('Recovery password') - const submitButton = await result.findByText('Submit') - - act(() => { - fireEvent.change(passwordField, { target: { value: 'Test1234!' } }) - submitButton.click() - }) - - mockSocialWalletService.getUserInfo = jest.fn().mockReturnValue({ - email: 'test@testermann.com', - name: 'Test Testermann', - profileImage: 'test.testermann.local/profile.png', - } as unknown as UserInfo) - - result.rerender( - - - - , - ) - - await waitFor(() => { - expect(result.getByText('Continue as Test Testermann')).toBeInTheDocument() - }) - }) - - describe('getSupportedChains', () => { - const mockEthereumChain = chainBuilder() - .with({ - chainId: '1', - chainName: 'Ethereum', - disabledWallets: ['socialSigner'], - }) - .build() - const mockGnosisChain = chainBuilder() - .with({ chainId: '100', chainName: 'Gnosis Chain', disabledWallets: ['Coinbase'] }) - .build() - it('returns chain names where social login is enabled', () => { - const mockGoerliChain = chainBuilder().with({ chainId: '5', chainName: 'Goerli', disabledWallets: [] }).build() - - const mockChains = [mockEthereumChain, mockGnosisChain, mockGoerliChain] - const result = _getSupportedChains(mockChains) - - expect(result).toEqual(['Gnosis Chain', 'Goerli']) - }) - - it('returns an empty array if social login is not enabled on any chain', () => { - const mockGoerliChain = chainBuilder() - .with({ chainId: '5', chainName: 'Goerli', disabledWallets: ['socialSigner'] }) - .build() - - const mockChains = [mockEthereumChain, mockGoerliChain] - const result = _getSupportedChains(mockChains) - - expect(result).toEqual([]) - }) - }) -}) diff --git a/src/components/common/SocialSigner/index.tsx b/src/components/common/SocialSigner/index.tsx deleted file mode 100644 index e75afd413c..0000000000 --- a/src/components/common/SocialSigner/index.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' -import { type ISocialWalletService } from '@/services/mpc/interfaces' -import { Alert, Box, Button, LinearProgress, SvgIcon, Typography } from '@mui/material' -import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' -import { useState } from 'react' -import GoogleLogo from '@/public/images/welcome/logo-google.svg' - -import css from './styles.module.css' -import useWallet from '@/hooks/wallets/useWallet' -import Track from '@/components/common/Track' -import { CREATE_SAFE_EVENTS } from '@/services/analytics' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import { CGW_NAMES } from '@/hooks/wallets/consts' -import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import madProps from '@/utils/mad-props' -import { asError } from '@/services/exceptions/utils' -import ErrorMessage from '@/components/tx/ErrorMessage' -import { open } from '@/services/mpc/PasswordRecoveryModal' - -export const _getSupportedChains = (chains: ChainInfo[]) => { - return chains - .filter((chain) => CGW_NAMES.SOCIAL_LOGIN && !chain.disabledWallets.includes(CGW_NAMES.SOCIAL_LOGIN)) - .map((chainConfig) => chainConfig.chainName) -} - -type SocialSignerLoginProps = { - socialWalletService: ISocialWalletService | undefined - wallet: ReturnType - onLogin?: () => void - onRequirePassword?: () => void -} - -export const SocialSigner = ({ socialWalletService, wallet, onLogin, onRequirePassword }: SocialSignerLoginProps) => { - const [loginPending, setLoginPending] = useState(false) - const [loginError, setLoginError] = useState(undefined) - const userInfo = socialWalletService?.getUserInfo() - const isDisabled = loginPending - - const isWelcomePage = !!onLogin - - const login = async () => { - if (!socialWalletService) return - - setLoginPending(true) - setLoginError(undefined) - try { - const status = await socialWalletService.loginAndCreate() - - if (status === COREKIT_STATUS.LOGGED_IN) { - setLoginPending(false) - return - } - - if (status === COREKIT_STATUS.REQUIRED_SHARE) { - onRequirePassword?.() - open(() => setLoginPending(false)) - return - } - } catch (err) { - const error = asError(err) - setLoginError(error.message) - } finally { - setLoginPending(false) - onLogin?.() - } - } - - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - return ( - <> - - {isSocialLogin && userInfo ? ( - - - - ) : ( - - - - )} - {loginError && {loginError}} - - - - We're discontinuing support for Google login. Please export your credentials till 01.07.2024. - - - ) -} - -export default madProps(SocialSigner, { - socialWalletService: useSocialWallet, - wallet: useWallet, -}) diff --git a/src/components/common/SocialSigner/styles.module.css b/src/components/common/SocialSigner/styles.module.css deleted file mode 100644 index 2b81df624a..0000000000 --- a/src/components/common/SocialSigner/styles.module.css +++ /dev/null @@ -1,34 +0,0 @@ -.profileImg { - border-radius: var(--space-2); - width: 32px; - height: 32px; -} - -.profileData { - display: flex; - flex-direction: column; - align-items: flex-start; -} - -.loginProgress { - margin-bottom: -16px; - top: 0; - width: 100%; - height: 2px; - position: absolute; - border-top-left-radius: 6px; - border-top-right-radius: 6px; -} - -.passwordWrapper { - padding: var(--space-4) var(--space-4) var(--space-2) var(--space-4); - display: flex; - flex-direction: column; - align-items: baseline; - gap: var(--space-1); -} - -.loginError { - width: 100%; - margin: 0; -} diff --git a/src/components/common/WalletInfo/index.test.tsx b/src/components/common/WalletInfo/index.test.tsx index 9375bb4ecc..3b435d6dd9 100644 --- a/src/components/common/WalletInfo/index.test.tsx +++ b/src/components/common/WalletInfo/index.test.tsx @@ -1,13 +1,7 @@ import { render } from '@/tests/test-utils' import { WalletInfo } from '@/components/common/WalletInfo/index' import { type EIP1193Provider, type OnboardAPI } from '@web3-onboard/core' -import { type NextRouter } from 'next/router' -import * as mpcModule from '@/services/mpc/SocialLoginModule' -import * as constants from '@/config/constants' -import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' import { act } from '@testing-library/react' -import SocialWalletService from '@/services/mpc/SocialWalletService' -import type { ISocialWalletService } from '@/services/mpc/interfaces' const mockWallet = { address: '0x1234567890123456789012345678901234567890', @@ -16,32 +10,21 @@ const mockWallet = { provider: null as unknown as EIP1193Provider, } -const mockRouter = { - query: {}, - pathname: '', -} as NextRouter - const mockOnboard = { connectWallet: jest.fn(), disconnectWallet: jest.fn(), setChain: jest.fn(), } as unknown as OnboardAPI -jest.mock('@/services/mpc/SocialWalletService') - describe('WalletInfo', () => { - let socialWalletService: ISocialWalletService beforeEach(() => { jest.resetAllMocks() - socialWalletService = new SocialWalletService({} as unknown as Web3AuthMPCCoreKit) }) it('should display the wallet address', () => { const { getByText } = render( { const { getByText } = render( { const { getByText } = render( { expect(mockOnboard.disconnectWallet).toHaveBeenCalled() }) - - it('should display a Delete Account button on dev for social login', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => false) - - const { getByText } = render( - , - ) - - expect(getByText('Delete account')).toBeInTheDocument() - }) - - it('should not display a Delete Account on prod', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => true) - - const { queryByText } = render( - , - ) - - expect(queryByText('Delete account')).not.toBeInTheDocument() - }) - - it('should not display a Delete Account if not social login', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(false) - jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => false) - - const { queryByText } = render( - , - ) - - expect(queryByText('Delete account')).not.toBeInTheDocument() - }) - - it('should display an enable mfa button if mfa is not enabled', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - - const { getByText } = render( - , - ) - - expect(getByText('Add multifactor authentication')).toBeInTheDocument() - }) - - it('should not display an enable mfa button if mfa is already enabled', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - - // Mock that MFA is enabled - socialWalletService.enableMFA('', '') - - const { queryByText } = render( - , - ) - - expect(queryByText('Add multifactor authentication')).not.toBeInTheDocument() - }) }) diff --git a/src/components/common/WalletInfo/index.tsx b/src/components/common/WalletInfo/index.tsx index 31c929be03..23c1af8cbd 100644 --- a/src/components/common/WalletInfo/index.tsx +++ b/src/components/common/WalletInfo/index.tsx @@ -2,21 +2,13 @@ import WalletBalance from '@/components/common/WalletBalance' import { WalletIdenticon } from '@/components/common/WalletOverview' import { Box, Button, Typography } from '@mui/material' import css from './styles.module.css' -import SocialLoginInfo from '@/components/common/SocialLoginInfo' -import Link from 'next/link' -import { AppRoutes } from '@/config/routes' -import LockIcon from '@/public/images/common/lock-small.svg' import EthHashInfo from '@/components/common/EthHashInfo' import ChainSwitcher from '@/components/common/ChainSwitcher' -import { IS_PRODUCTION } from '@/config/constants' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import useOnboard, { type ConnectedWallet, switchWallet } from '@/hooks/wallets/useOnboard' -import { useRouter } from 'next/router' import useAddressBook from '@/hooks/useAddressBook' import { useAppSelector } from '@/store' import { selectChainById } from '@/store/chainsSlice' import madProps from '@/utils/mad-props' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew' import useChainId from '@/hooks/useChainId' @@ -24,23 +16,12 @@ type WalletInfoProps = { wallet: ConnectedWallet balance?: string | bigint currentChainId: ReturnType - socialWalletService: ReturnType - router: ReturnType onboard: ReturnType addressBook: ReturnType handleClose: () => void } -export const WalletInfo = ({ - wallet, - balance, - currentChainId, - socialWalletService, - router, - onboard, - addressBook, - handleClose, -}: WalletInfoProps) => { +export const WalletInfo = ({ wallet, balance, currentChainId, onboard, addressBook, handleClose }: WalletInfoProps) => { const chainInfo = useAppSelector((state) => selectChainById(state, wallet.chainId)) const prefix = chainInfo?.shortName @@ -51,8 +32,6 @@ export const WalletInfo = ({ } } - const resetAccount = () => socialWalletService?.__deleteAccount() - const handleDisconnect = () => { onboard?.disconnectWallet({ label: wallet.label, @@ -61,48 +40,22 @@ export const WalletInfo = ({ handleClose() } - const isSocialLogin = isSocialLoginWallet(wallet.label) - return ( <> - {isSocialLogin ? ( - - - - {socialWalletService && !socialWalletService.isMFAEnabled() && ( - - - - )} - - ) : ( - <> - - - - - - )} + + + + + @@ -148,20 +101,12 @@ export const WalletInfo = ({ > Disconnect - - {!IS_PRODUCTION && isSocialLogin && ( - - )} ) } export default madProps(WalletInfo, { - socialWalletService: useSocialWallet, - router: useRouter, onboard: useOnboard, addressBook: useAddressBook, currentChainId: useChainId, diff --git a/src/components/common/WalletOverview/index.tsx b/src/components/common/WalletOverview/index.tsx index c4339f536e..f60bfc830c 100644 --- a/src/components/common/WalletOverview/index.tsx +++ b/src/components/common/WalletOverview/index.tsx @@ -11,8 +11,6 @@ import { selectChainById } from '@/store/chainsSlice' import WalletBalance from '@/components/common/WalletBalance' import css from './styles.module.css' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import SocialLoginInfo from '@/components/common/SocialLoginInfo' export const WalletIdenticon = ({ wallet, size = 32 }: { wallet: ConnectedWallet; size?: number }) => { return ( @@ -39,22 +37,6 @@ const WalletOverview = ({ const walletChain = useAppSelector((state) => selectChainById(state, wallet.chainId)) const prefix = walletChain?.shortName - const isSocialLogin = isSocialLoginWallet(wallet.label) - - if (isSocialLogin) { - return ( -
- -
- ) - } - return ( diff --git a/src/components/common/WalletOverview/styles.module.css b/src/components/common/WalletOverview/styles.module.css index 6cf83c89fd..14dd5f668f 100644 --- a/src/components/common/WalletOverview/styles.module.css +++ b/src/components/common/WalletOverview/styles.module.css @@ -45,8 +45,4 @@ width: 22px; height: auto; } - - .socialLoginInfo > div > div:last-child { - display: none; - } } diff --git a/src/components/new-safe/create/index.tsx b/src/components/new-safe/create/index.tsx index ba9d43b84f..9a14bad22d 100644 --- a/src/components/new-safe/create/index.tsx +++ b/src/components/new-safe/create/index.tsx @@ -18,8 +18,6 @@ import CreateSafeInfos from '@/components/new-safe/create/CreateSafeInfos' import { type ReactElement, useMemo, useState } from 'react' import ExternalLink from '@/components/common/ExternalLink' import { HelpCenterArticle } from '@/config/constants' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import { useMnemonicSafeName } from '@/hooks/useMnemonicName' export type NewSafeFormData = { name: string @@ -151,14 +149,9 @@ const CreateSafe = () => { const staticHint = useMemo(() => staticHints[activeStep], [activeStep]) - const mnemonicSafeName = useMnemonicSafeName() - - // Jump to review screen when using social login - const isSocialLogin = isSocialLoginWallet(wallet?.label) - const initialStep = isSocialLogin ? 2 : 0 - + const initialStep = 0 const initialData: NewSafeFormData = { - name: isSocialLogin ? mnemonicSafeName : '', + name: '', owners: [], threshold: 1, saltNonce: Date.now(), diff --git a/src/components/new-safe/create/steps/ReviewStep/index.test.tsx b/src/components/new-safe/create/steps/ReviewStep/index.test.tsx index 8ebfc9bdaa..6455eef21d 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.test.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.test.tsx @@ -7,7 +7,6 @@ import { render } from '@/tests/test-utils' import ReviewStep, { NetworkFee } from '@/components/new-safe/create/steps/ReviewStep/index' import * as useWallet from '@/hooks/wallets/useWallet' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' -import * as socialLogin from '@/services/mpc/SocialLoginModule' import { act, fireEvent } from '@testing-library/react' const mockChainInfo = { @@ -20,29 +19,13 @@ const mockChainInfo = { } as ChainInfo describe('NetworkFee', () => { - it('should display the total fee if not social login', () => { + it('should display the total fee', () => { jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'MetaMask' } as unknown as ConnectedWallet) const mockTotalFee = '0.0123' const result = render() expect(result.getByText(`≈ ${mockTotalFee} ${mockChainInfo.nativeCurrency.symbol}`)).toBeInTheDocument() }) - - it('displays a sponsored by message for social login', () => { - jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'Social Login' } as unknown as ConnectedWallet) - const result = render() - - expect(result.getByText(/Your account is sponsored by Gnosis/)).toBeInTheDocument() - }) - - it('displays an error message for social login if there are no relays left', () => { - jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'Social Login' } as unknown as ConnectedWallet) - const result = render() - - expect( - result.getByText(/You have used up your 5 free transactions per hour. Please try again later/), - ).toBeInTheDocument() - }) }) describe('ReviewStep', () => { @@ -129,7 +112,6 @@ describe('ReviewStep', () => { } jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) jest.spyOn(relay, 'hasRemainingRelays').mockReturnValue(true) - jest.spyOn(socialLogin, 'isSocialLoginWallet').mockReturnValue(false) const { getByText } = render( , diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index 15801600bf..85141ffaec 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -14,30 +14,27 @@ import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCre import ReviewRow from '@/components/new-safe/ReviewRow' import ErrorMessage from '@/components/tx/ErrorMessage' import { ExecutionMethod, ExecutionMethodSelector } from '@/components/tx/ExecutionMethodSelector' -import { RELAY_SPONSORS } from '@/components/tx/SponsoredBy' import { LATEST_SAFE_VERSION } from '@/config/constants' import PayNowPayLater, { PayMethod } from '@/features/counterfactual/PayNowPayLater' import { createCounterfactualSafe } from '@/features/counterfactual/utils' import { useCurrentChain, useHasFeature } from '@/hooks/useChains' import useGasPrice from '@/hooks/useGasPrice' import useIsWrongChain from '@/hooks/useIsWrongChain' -import { MAX_HOUR_RELAYS, useLeastRemainingRelays } from '@/hooks/useRemainingRelays' +import { useLeastRemainingRelays } from '@/hooks/useRemainingRelays' import useWalletCanPay from '@/hooks/useWalletCanPay' import useWallet from '@/hooks/wallets/useWallet' import { useWeb3 } from '@/hooks/wallets/web3' import { CREATE_SAFE_CATEGORY, CREATE_SAFE_EVENTS, OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' import { gtmSetSafeAddress } from '@/services/analytics/gtm' import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeContracts' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import { useAppDispatch } from '@/store' import { FEATURES } from '@/utils/chains' import { hasRemainingRelays } from '@/utils/relaying' import ArrowBackIcon from '@mui/icons-material/ArrowBack' -import { Alert, Box, Button, CircularProgress, Divider, Grid, Typography } from '@mui/material' +import { Box, Button, CircularProgress, Divider, Grid, Typography } from '@mui/material' import { type DeploySafeProps } from '@safe-global/protocol-kit' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import classnames from 'classnames' -import Image from 'next/image' import { useRouter } from 'next/router' import { useMemo, useState } from 'react' import { usePendingSafe } from '../StatusStep/usePendingSafe' @@ -55,45 +52,14 @@ export const NetworkFee = ({ }) => { const wallet = useWallet() - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - if (!isSocialLogin) { - return ( - - - - ≈ {totalFee} {chain?.nativeCurrency.symbol} - - - - ) - } - - if (willRelay) { - const sponsor = RELAY_SPONSORS[chain?.chainId || ''] || RELAY_SPONSORS.default - return ( - <> - Free - - Your account is sponsored by - {sponsor.name}{' '} - {sponsor.name} - - - ) - } - return ( - - You have used up your {MAX_HOUR_RELAYS} free transactions per hour. Please try again later. - + + + + ≈ {totalFee} {chain?.nativeCurrency.symbol} + + + ) } @@ -239,8 +205,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps @@ -254,7 +219,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps - {canRelay && !isSocialLogin && payMethod === PayMethod.PayNow && ( + {canRelay && payMethod === PayMethod.PayNow && ( - {canRelay && !isSocialLogin && ( + {canRelay && ( - {!willRelay && !isSocialLogin && ( + {!willRelay && ( You will have to confirm a transaction with your connected wallet. diff --git a/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx b/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx deleted file mode 100644 index bacce6a38f..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import CopyButton from '@/components/common/CopyButton' -import ModalDialog from '@/components/common/ModalDialog' -import { trackEvent } from '@/services/analytics' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { Box, Button, DialogContent, DialogTitle, IconButton, TextField, Typography } from '@mui/material' -import { useState } from 'react' -import { useForm } from 'react-hook-form' -import { Visibility, VisibilityOff, Close } from '@mui/icons-material' -import css from '@/components/settings/SecurityLogin/SocialSignerExport/styles.module.css' -import ErrorCodes from '@/services/exceptions/ErrorCodes' -import { logError } from '@/services/exceptions' -import ErrorMessage from '@/components/tx/ErrorMessage' -import { asError } from '@/services/exceptions/utils' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -enum ExportFieldNames { - password = 'password', - pk = 'pk', -} - -type ExportFormData = { - [ExportFieldNames.password]: string - [ExportFieldNames.pk]: string | undefined -} - -const ExportMPCAccountModal = ({ onClose, open }: { onClose: () => void; open: boolean }) => { - const socialWalletService = useSocialWallet() - const [error, setError] = useState() - - const [showPassword, setShowPassword] = useState(false) - const formMethods = useForm({ - mode: 'all', - defaultValues: { - [ExportFieldNames.password]: '', - }, - }) - const { register, formState, handleSubmit, setValue, watch, reset } = formMethods - - const exportedKey = watch(ExportFieldNames.pk) - - const onSubmit = async (data: ExportFormData) => { - if (!socialWalletService) { - return - } - try { - setError(undefined) - const pk = await socialWalletService.exportSignerKey(data[ExportFieldNames.password]) - trackEvent(MPC_WALLET_EVENTS.EXPORT_PK_SUCCESS) - setValue(ExportFieldNames.pk, pk) - } catch (err) { - logError(ErrorCodes._305, err) - trackEvent(MPC_WALLET_EVENTS.EXPORT_PK_ERROR) - setError(asError(err).message) - } - } - - const handleClose = () => { - setError(undefined) - reset() - onClose() - } - - const toggleShowPK = () => { - trackEvent(MPC_WALLET_EVENTS.SEE_PK) - setShowPassword((prev) => !prev) - } - - const onCopy = () => { - trackEvent(MPC_WALLET_EVENTS.COPY_PK) - } - - return ( - - Export your account - - - - - -
- - For security reasons you have to enter your password to reveal your account key. - - {exportedKey ? ( - - - - {showPassword ? : } - - - - ), - }} - {...register(ExportFieldNames.pk)} - /> - - ) : ( - <> - - - )} - {error && {error}} - - - - {exportedKey === undefined && ( - - )} - - -
-
-
- ) -} - -export default ExportMPCAccountModal diff --git a/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx b/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx deleted file mode 100644 index 088e484af2..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Track from '@/components/common/Track' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { Alert, Box, Button, Tooltip, Typography } from '@mui/material' -import { useState } from 'react' -import ExportMPCAccountModal from '@/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -const SocialSignerExport = () => { - const [isModalOpen, setIsModalOpen] = useState(false) - - const socialWalletService = useSocialWallet() - - const isPasswordSet = socialWalletService?.isRecoveryPasswordSet() ?? false - - return ( - <> - - - Signers created via Google can be exported and imported to any non-custodial wallet outside of Safe. - - - Never disclose your keys or seed phrase to anyone. If someone gains access to them, they have full access over - your social login signer. - - - - - - - - - - - setIsModalOpen(false)} open={isModalOpen} /> - - ) -} - -export default SocialSignerExport diff --git a/src/components/settings/SecurityLogin/SocialSignerExport/styles.module.css b/src/components/settings/SecurityLogin/SocialSignerExport/styles.module.css deleted file mode 100644 index c818925ecd..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerExport/styles.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.close { - position: absolute; - right: var(--space-1); - top: var(--space-1); -} - -.modalError { - width: 100%; - margin: 0; -} diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput.tsx deleted file mode 100644 index 40e78010b2..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useState } from 'react' -import { IconButton, TextField, type TextFieldProps } from '@mui/material' -import { Visibility, VisibilityOff } from '@mui/icons-material' -import { useFormContext, type Validate } from 'react-hook-form' - -const PasswordInput = ({ - name, - validate, - required = false, - ...props -}: Omit & { - name: string - validate?: Validate - required?: boolean -}) => { - const [showPassword, setShowPassword] = useState(false) - - const { register, formState } = useFormContext() || {} - - return ( - setShowPassword((prev) => !prev)} - edge="end" - > - {showPassword ? : } - - ), - }} - {...register(name, { - required, - validate, - })} - /> - ) -} - -export default PasswordInput diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/index.test.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/index.test.tsx deleted file mode 100644 index 4a2bde113d..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/index.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { _getPasswordStrength, PasswordStrength } from '@/components/settings/SecurityLogin/SocialSignerMFA/index' - -describe('_getPasswordStrength', () => { - it('should return weak if the value has fewer than 9 characters', () => { - const result = _getPasswordStrength('Testpw1!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has no uppercase letter', () => { - const result = _getPasswordStrength('testpassword1!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has no number', () => { - const result = _getPasswordStrength('Testpassword!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has no special character', () => { - const result = _getPasswordStrength('Testpassword123') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has 12 or more characters but no uppercase letter', () => { - const result = _getPasswordStrength('testpassword123!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return medium if the value has 9 or more characters, uppercase, number and special character', () => { - const result = _getPasswordStrength('Testpw123!') - - expect(result).toEqual(PasswordStrength.medium) - }) - - it('should return strong if the value has 12 or more characters, uppercase, number and special character', () => { - const result = _getPasswordStrength('Testpassword123!') - - expect(result).toEqual(PasswordStrength.strong) - }) -}) diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx deleted file mode 100644 index cfb4ec0464..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import Track from '@/components/common/Track' -import { - Typography, - Button, - Box, - Accordion, - AccordionSummary, - AccordionDetails, - Grid, - FormControl, - SvgIcon, - Divider, - Alert, -} from '@mui/material' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { useRouter } from 'next/router' -import { useState, useMemo, type ChangeEvent } from 'react' -import { FormProvider, useForm } from 'react-hook-form' -import CheckIcon from '@/public/images/common/check-filled.svg' -import LockWarningIcon from '@/public/images/common/lock-warning.svg' -import PasswordInput from '@/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput' -import css from '@/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css' -import BarChartIcon from '@/public/images/common/bar-chart.svg' -import ShieldIcon from '@/public/images/common/shield.svg' -import ShieldOffIcon from '@/public/images/common/shield-off.svg' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -enum PasswordFieldNames { - currentPassword = 'currentPassword', - newPassword = 'newPassword', - confirmPassword = 'confirmPassword', -} - -type PasswordFormData = { - [PasswordFieldNames.currentPassword]: string | undefined - [PasswordFieldNames.newPassword]: string - [PasswordFieldNames.confirmPassword]: string -} - -export enum PasswordStrength { - strong, - medium, - weak, -} - -// At least 12 characters, one lowercase, one uppercase, one number, one symbol -const strongPassword = new RegExp('(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{12,})') -// At least 9 characters, one lowercase, one uppercase, one number, one symbol -const mediumPassword = new RegExp('((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{9,}))') - -export const _getPasswordStrength = (value: string): PasswordStrength | undefined => { - if (value === '') return undefined - - if (strongPassword.test(value)) { - return PasswordStrength.strong - } - - if (mediumPassword.test(value)) { - return PasswordStrength.medium - } - - return PasswordStrength.weak -} - -const passwordStrengthMap = { - [PasswordStrength.strong]: { - label: 'Strong', - className: 'strongPassword', - }, - [PasswordStrength.medium]: { - label: 'Medium', - className: 'mediumPassword', - }, - [PasswordStrength.weak]: { - label: 'Weak', - className: 'weakPassword', - }, -} as const - -const SocialSignerMFA = () => { - const router = useRouter() - const socialWalletService = useSocialWallet() - const [passwordStrength, setPasswordStrength] = useState() - const [submitError, setSubmitError] = useState() - const [open, setOpen] = useState(false) - - const formMethods = useForm({ - mode: 'all', - defaultValues: { - [PasswordFieldNames.confirmPassword]: '', - [PasswordFieldNames.currentPassword]: undefined, - [PasswordFieldNames.newPassword]: '', - }, - }) - - const { formState, handleSubmit, reset, watch } = formMethods - - const isPasswordSet = useMemo(() => { - if (!socialWalletService) { - return false - } - return socialWalletService.isRecoveryPasswordSet() - }, [socialWalletService]) - - const onSubmit = async (data: PasswordFormData) => { - if (!socialWalletService) return - - try { - await socialWalletService.enableMFA( - data[PasswordFieldNames.currentPassword], - data[PasswordFieldNames.newPassword], - ) - onReset() - setOpen(false) - - // This is a workaround so that the isPasswordSet and isMFAEnabled state update - router.reload() - } catch (e) { - setSubmitError('The password you entered is incorrect. Please try again.') - } - } - - const onReset = () => { - reset() - setPasswordStrength(undefined) - setSubmitError(undefined) - } - - const toggleAccordion = () => { - setOpen((prev) => !prev) - } - - const confirmPassword = watch(PasswordFieldNames.confirmPassword) - const newPassword = watch(PasswordFieldNames.newPassword) - const passwordsEmpty = confirmPassword === '' && newPassword === '' - const passwordsMatch = newPassword === confirmPassword - - const isSubmitDisabled = - !passwordsMatch || - passwordStrength === PasswordStrength.weak || - formState.isSubmitting || - !formMethods.formState.isValid - - return ( - -
- - - Protect your social login signer with a password. It will be used to restore access in another browser or on - another device. - - - }> - - - Password - - - - - - {isPasswordSet && ( - <> - - - - - - )} - - - ) => { - const value = event.target.value - setPasswordStrength(_getPasswordStrength(value)) - }, - }} - /> - - - {passwordStrength !== undefined - ? `${passwordStrengthMap[passwordStrength].label} password` - : 'Password strength'} - - - Include at least 9 or more characters, a number, an uppercase letter and a symbol - - - - - - - {passwordsEmpty ? ( - <> - Passwords should match - - ) : passwordsMatch ? ( - <> - Passwords match - - ) : ( - <> - Passwords don't match - - )} - - - - {submitError && {submitError}} - - - - - - - - - `1px solid ${theme.palette.border.light}` }} - > - - - - You won't be able to restore this password - -
    - - You will have to input this password if you login with this social login signer in another - browser or on another device. - - - We suggest to use a password manager or to write the password down on a piece of paper and - secure it. - -
-
-
-
-
-
-
-
-
- ) -} - -export default SocialSignerMFA diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css b/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css deleted file mode 100644 index b41b19e58f..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css +++ /dev/null @@ -1,76 +0,0 @@ -.list { - list-style: none; - counter-reset: item; - padding: 0; - margin: var(--space-2) 0; -} - -.list li { - counter-increment: item; - margin-bottom: var(--space-2); - display: flex; -} - -.list li:before { - margin-right: var(--space-1); - content: counter(item); - background: var(--color-primary-main); - border-radius: 100%; - color: var(--color-background-paper); - width: 20px; - height: 20px; - line-height: 20px; - font-size: 12px; - text-align: center; - flex-shrink: 0; -} - -.list li:last-child { - margin-bottom: 0; -} - -.defaultPassword, -.passwordsShouldMatch { - color: var(--color-border-main); -} - -.passwordsShouldMatch svg path { - stroke: var(--color-border-main); -} - -.defaultPassword svg path { - stroke: var(--color-border-main); -} - -.weakPassword { - color: var(--color-error-main); -} - -.weakPassword svg path:first-child { - stroke: var(--color-error-main); -} - -.mediumPassword { - color: var(--color-warning-main); -} - -.mediumPassword svg path:first-child, -.mediumPassword svg path:nth-child(2) { - stroke: var(--color-warning-main); -} - -.strongPassword { - color: var(--color-success-main); -} - -.strongPassword svg path { - stroke: var(--color-success-main); -} - -.passwordsMatch { - color: var(--color-success-main); -} - -.passwordsNoMatch { - color: var(--color-error-main); -} diff --git a/src/components/settings/SecurityLogin/index.tsx b/src/components/settings/SecurityLogin/index.tsx index 6b921dc0c6..0fb33dc324 100644 --- a/src/components/settings/SecurityLogin/index.tsx +++ b/src/components/settings/SecurityLogin/index.tsx @@ -1,8 +1,4 @@ -import { Box, Grid, Paper, Typography } from '@mui/material' -import SocialSignerMFA from './SocialSignerMFA' -import SocialSignerExport from './SocialSignerExport' -import useWallet from '@/hooks/wallets/useWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' +import { Box } from '@mui/material' import dynamic from 'next/dynamic' import { useIsRecoverySupported } from '@/features/recovery/hooks/useIsRecoverySupported' import SecuritySettings from '../SecuritySettings' @@ -11,44 +7,12 @@ const RecoverySettings = dynamic(() => import('@/features/recovery/components/Re const SecurityLogin = () => { const isRecoverySupported = useIsRecoverySupported() - const wallet = useWallet() - const isSocialLogin = isSocialLoginWallet(wallet?.label) return ( {isRecoverySupported && } - - {isSocialLogin && ( - <> - - - - - Multi-factor Authentication - - - - - - - - - - - - - Social login signer export - - - - - - - - - )} ) } diff --git a/src/components/settings/SettingsHeader/index.test.tsx b/src/components/settings/SettingsHeader/index.test.tsx index 978ac05a90..b8ce692c58 100644 --- a/src/components/settings/SettingsHeader/index.test.tsx +++ b/src/components/settings/SettingsHeader/index.test.tsx @@ -1,9 +1,6 @@ import SettingsHeader from '@/components/settings/SettingsHeader/index' import * as safeAddress from '@/hooks/useSafeAddress' import * as feature from '@/hooks/useChains' -import * as wallet from '@/hooks/wallets/useWallet' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { connectedWalletBuilder } from '@/tests/builders/wallet' import { render } from '@/tests/test-utils' import { faker } from '@faker-js/faker' @@ -32,15 +29,6 @@ describe('SettingsHeader', () => { expect(result.getByText('Notifications')).toBeInTheDocument() }) - - it('displays Security tab if connected wallet is a social signer', () => { - const mockWallet = connectedWalletBuilder().with({ label: ONBOARD_MPC_MODULE_LABEL }).build() - jest.spyOn(wallet, 'default').mockReturnValue(mockWallet) - - const result = render() - - expect(result.getByText('Security')).toBeInTheDocument() - }) }) describe('No safe is open', () => { @@ -65,14 +53,5 @@ describe('SettingsHeader', () => { expect(result.getByText('Notifications')).toBeInTheDocument() }) - - it('displays Security if connected wallet is a social signer', () => { - const mockWallet = connectedWalletBuilder().with({ label: ONBOARD_MPC_MODULE_LABEL }).build() - jest.spyOn(wallet, 'default').mockReturnValue(mockWallet) - - const result = render() - - expect(result.getByText('Security')).toBeInTheDocument() - }) }) }) diff --git a/src/components/welcome/NewSafeSocial.tsx b/src/components/welcome/NewSafeSocial.tsx deleted file mode 100644 index b74e4501a7..0000000000 --- a/src/components/welcome/NewSafeSocial.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react' -import { Box, Button, Grid, Typography } from '@mui/material' -import css from './styles.module.css' -import Link from 'next/link' - -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' -import WelcomeLogin from './WelcomeLogin' -import GnosisChainLogo from '@/public/images/common/gnosis-chain-logo.png' -import Image from 'next/image' - -const BulletListItem = ({ text }: { text: string }) => ( -
  • - - {text} - -
  • -) - -const MarqueeItem = () => { - return ( -
  • - Free on - Gnosis Chain logo - Gnosis Chain -
  • - ) -} - -const NewSafeSocial = () => { - return ( - <> - - - - - -
    - - - Get the most secure web3 account in {'<'}30 seconds - - -
      - - - -
    - - - - -
    - -
    -
      - - - -
    - -
    -
    -
    -
    - - ) -} - -export default NewSafeSocial diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx index cec4771cea..97a5d78e01 100644 --- a/src/components/welcome/WelcomeLogin/WalletLogin.tsx +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -1,6 +1,5 @@ import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' import useWallet from '@/hooks/wallets/useWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import { Box, Button, Typography } from '@mui/material' import EthHashInfo from '@/components/common/EthHashInfo' import WalletIcon from '@/components/common/WalletIcon' @@ -14,9 +13,7 @@ const WalletLogin = ({ onLogin }: { onLogin: () => void }) => { onLogin() } - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - if (wallet !== null && !isSocialLogin) { + if (wallet !== null) { return (
    diff --git a/src/components/dashboard/index.tsx b/src/components/dashboard/index.tsx index e0a56f277a..3c72bf6be8 100644 --- a/src/components/dashboard/index.tsx +++ b/src/components/dashboard/index.tsx @@ -14,6 +14,8 @@ import { CREATION_MODAL_QUERY_PARM } from '../new-safe/create/logic' import useRecovery from '@/features/recovery/hooks/useRecovery' import { useIsRecoverySupported } from '@/features/recovery/hooks/useIsRecoverySupported' import ActivityRewardsSection from '@/components/dashboard/ActivityRewardsSection' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' const RecoveryHeader = dynamic(() => import('@/features/recovery/components/RecoveryHeader')) const RecoveryWidget = dynamic(() => import('@/features/recovery/components/RecoveryWidget')) @@ -21,7 +23,7 @@ const Dashboard = (): ReactElement => { const router = useRouter() const { safe } = useSafeInfo() const { [CREATION_MODAL_QUERY_PARM]: showCreationModal = '' } = router.query - + const showSafeApps = useHasFeature(FEATURES.SAFE_APPS) const supportsRecovery = useIsRecoverySupported() const [recovery] = useRecovery() const showRecoveryWidget = supportsRecovery && !recovery @@ -53,13 +55,17 @@ const Dashboard = (): ReactElement => { ) : null} - - - + {showSafeApps && ( + + + + )} - - - + {showSafeApps && ( + + + + )} @@ -67,6 +73,7 @@ const Dashboard = (): ReactElement => { )} + {showCreationModal ? : null} ) diff --git a/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts b/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts index 47c7e4ce9d..b0fae21862 100644 --- a/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts +++ b/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts @@ -23,7 +23,7 @@ import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' import { waitForCreateSafeTx } from '@/services/tx/txMonitor' import useGasPrice from '@/hooks/useGasPrice' import { hasFeature } from '@/utils/chains' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES } from '@/utils/chains' import type { DeploySafeProps } from '@safe-global/protocol-kit' import { usePendingSafe } from './usePendingSafe' diff --git a/src/components/notification-center/NotificationCenter/index.tsx b/src/components/notification-center/NotificationCenter/index.tsx index d756721dde..ebce3aee22 100644 --- a/src/components/notification-center/NotificationCenter/index.tsx +++ b/src/components/notification-center/NotificationCenter/index.tsx @@ -25,6 +25,8 @@ import SettingsIcon from '@/public/images/sidebar/settings.svg' import css from './styles.module.css' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' import SvgIcon from '@mui/icons-material/ExpandLess' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' const NOTIFICATION_CENTER_LIMIT = 4 @@ -33,7 +35,7 @@ const NotificationCenter = (): ReactElement => { const [showAll, setShowAll] = useState(false) const [anchorEl, setAnchorEl] = useState(null) const open = Boolean(anchorEl) - + const hasPushNotifications = useHasFeature(FEATURES.PUSH_NOTIFICATIONS) const dispatch = useAppDispatch() const notifications = useAppSelector(selectNotifications) @@ -144,9 +146,11 @@ const NotificationCenter = (): ReactElement => { )}
    +
    +
    {canExpand && ( <> @@ -166,18 +170,21 @@ const NotificationCenter = (): ReactElement => { )} - - - Push notifications settings - - + + {hasPushNotifications && ( + + + Push notifications settings + + + )}
    diff --git a/src/components/settings/SettingsHeader/index.test.tsx b/src/components/settings/SettingsHeader/index.test.tsx index b8ce692c58..32574c41fb 100644 --- a/src/components/settings/SettingsHeader/index.test.tsx +++ b/src/components/settings/SettingsHeader/index.test.tsx @@ -1,9 +1,11 @@ -import SettingsHeader from '@/components/settings/SettingsHeader/index' +import { SettingsHeader } from '@/components/settings/SettingsHeader/index' +import { CONFIG_SERVICE_CHAINS } from '@/tests/mocks/chains' import * as safeAddress from '@/hooks/useSafeAddress' -import * as feature from '@/hooks/useChains' import { render } from '@/tests/test-utils' import { faker } from '@faker-js/faker' +import { FEATURES } from '@/utils/chains' +import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' describe('SettingsHeader', () => { beforeEach(() => { @@ -17,15 +19,21 @@ describe('SettingsHeader', () => { }) it('displays safe specific preferences if on a safe', () => { - const result = render() + const result = render() expect(result.getByText('Setup')).toBeInTheDocument() }) it('displays Notifications if feature is enabled', () => { - jest.spyOn(feature, 'useHasFeature').mockReturnValue(true) - - const result = render() + const result = render( + , + ) expect(result.getByText('Notifications')).toBeInTheDocument() }) @@ -38,7 +46,7 @@ describe('SettingsHeader', () => { }) it('displays general preferences if no safe is open', () => { - const result = render() + const result = render() expect(result.getByText('Cookies')).toBeInTheDocument() expect(result.getByText('Appearance')).toBeInTheDocument() @@ -47,9 +55,15 @@ describe('SettingsHeader', () => { }) it('displays Notifications if feature is enabled', () => { - jest.spyOn(feature, 'useHasFeature').mockReturnValue(true) - - const result = render() + const result = render( + , + ) expect(result.getByText('Notifications')).toBeInTheDocument() }) diff --git a/src/components/settings/SettingsHeader/index.tsx b/src/components/settings/SettingsHeader/index.tsx index 5bb856f21a..f20def13f1 100644 --- a/src/components/settings/SettingsHeader/index.tsx +++ b/src/components/settings/SettingsHeader/index.tsx @@ -5,11 +5,20 @@ import PageHeader from '@/components/common/PageHeader' import { generalSettingsNavItems, settingsNavItems } from '@/components/sidebar/SidebarNavigation/config' import css from '@/components/common/PageHeader/styles.module.css' import useSafeAddress from '@/hooks/useSafeAddress' +import { useCurrentChain } from '@/hooks/useChains' +import { isRouteEnabled } from '@/utils/chains' +import madProps from '@/utils/mad-props' -const SettingsHeader = (): ReactElement => { - const safeAddress = useSafeAddress() - - const navItems = safeAddress ? settingsNavItems : generalSettingsNavItems +export const SettingsHeader = ({ + safeAddress, + chain, +}: { + safeAddress: ReturnType + chain: ReturnType +}): ReactElement => { + const navItems = safeAddress + ? settingsNavItems.filter((route) => isRouteEnabled(route.href, chain)) + : generalSettingsNavItems return ( { ) } -export default SettingsHeader +export default madProps(SettingsHeader, { + safeAddress: useSafeAddress, + chain: useCurrentChain, +}) diff --git a/src/components/sidebar/SidebarNavigation/index.tsx b/src/components/sidebar/SidebarNavigation/index.tsx index b247e31b6a..91b08bf290 100644 --- a/src/components/sidebar/SidebarNavigation/index.tsx +++ b/src/components/sidebar/SidebarNavigation/index.tsx @@ -1,7 +1,7 @@ import React, { useMemo, type ReactElement } from 'react' import { useRouter } from 'next/router' import ListItem from '@mui/material/ListItem' -import { type ChainInfo, ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' +import { ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' import { SidebarList, @@ -15,7 +15,7 @@ import useSafeInfo from '@/hooks/useSafeInfo' import { AppRoutes } from '@/config/routes' import { useQueuedTxsLength } from '@/hooks/useTxQueue' import { useCurrentChain } from '@/hooks/useChains' -import { FeatureRoutes, hasFeature } from '@/utils/chains' +import { isRouteEnabled } from '@/utils/chains' import { trackEvent } from '@/services/analytics' import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' @@ -24,13 +24,6 @@ const getSubdirectory = (pathname: string): string => { return pathname.split('/')[1] } -const isRouteEnabled = (route: string, chain?: ChainInfo) => { - if (!chain) return false - - const featureRoute = FeatureRoutes[route] - return !featureRoute || hasFeature(chain, featureRoute) -} - const Navigation = (): ReactElement => { const chain = useCurrentChain() const router = useRouter() diff --git a/src/components/tx-flow/common/TxButton.tsx b/src/components/tx-flow/common/TxButton.tsx index 214f796660..265b1a0e7d 100644 --- a/src/components/tx-flow/common/TxButton.tsx +++ b/src/components/tx-flow/common/TxButton.tsx @@ -8,6 +8,8 @@ import Track from '@/components/common/Track' import { MODALS_EVENTS } from '@/services/analytics' import { useContext } from 'react' import { TxModalContext } from '..' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' const buttonSx = { height: '58px', @@ -27,6 +29,9 @@ export const SendTokensButton = ({ onClick, sx }: { onClick: () => void; sx?: Bu export const SendNFTsButton = () => { const router = useRouter() const { setTxFlow } = useContext(TxModalContext) + const isEnabled = useHasFeature(FEATURES.ERC721) + + if (!isEnabled) return null const isNftPage = router.pathname === AppRoutes.balances.nfts const onClick = isNftPage ? () => setTxFlow(undefined) : undefined diff --git a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx index fb47366b97..e4fd4c17b6 100644 --- a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx +++ b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx @@ -1,6 +1,6 @@ import { CircularProgress, Typography, Button, CardActions, Divider, Alert } from '@mui/material' import useAsync from '@/hooks/useAsync' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES } from '@/utils/chains' import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import { getReadOnlyMultiSendCallOnlyContract } from '@/services/contracts/safeContracts' import { useCurrentChain } from '@/hooks/useChains' diff --git a/src/features/counterfactual/ActivateAccountFlow.tsx b/src/features/counterfactual/ActivateAccountFlow.tsx index 3b397bbd89..c568ab5864 100644 --- a/src/features/counterfactual/ActivateAccountFlow.tsx +++ b/src/features/counterfactual/ActivateAccountFlow.tsx @@ -29,7 +29,7 @@ import { hasFeature } from '@/utils/chains' import { hasRemainingRelays } from '@/utils/relaying' import { Box, Button, CircularProgress, Divider, Grid, Typography } from '@mui/material' import type { DeploySafeProps } from '@safe-global/protocol-kit' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES } from '@/utils/chains' import React, { useContext, useState } from 'react' const useActivateAccount = () => { diff --git a/src/pages/transactions/messages.tsx b/src/pages/transactions/messages.tsx index 952ad618f8..01ec4892af 100644 --- a/src/pages/transactions/messages.tsx +++ b/src/pages/transactions/messages.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' import Head from 'next/head' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES } from '@/utils/chains' import { useRouter } from 'next/router' import type { NextPage } from 'next' diff --git a/src/tests/mocks/chains.ts b/src/tests/mocks/chains.ts index 795f466b90..b7285b8b52 100644 --- a/src/tests/mocks/chains.ts +++ b/src/tests/mocks/chains.ts @@ -43,7 +43,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ ], disabledWallets: ['lattice'], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.DOMAIN_LOOKUP, FEATURES.EIP1559, FEATURES.ERC721, @@ -94,7 +93,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.EIP1559, FEATURES.ERC721, FEATURES.SAFE_APPS, @@ -150,7 +148,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.EIP1559, FEATURES.ERC721, FEATURES.SAFE_APPS, @@ -203,7 +200,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL, @@ -253,7 +249,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.DOMAIN_LOOKUP, FEATURES.ERC721, FEATURES.SAFE_APPS, @@ -301,13 +296,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'trust', 'walletLink', ], - features: [ - FEATURES.CONTRACT_INTERACTION, - FEATURES.ERC721, - FEATURES.SAFE_APPS, - FEATURES.SAFE_TX_GAS_OPTIONAL, - FEATURES.TX_SIMULATION, - ], + features: [FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL, FEATURES.TX_SIMULATION], }, { transactionService: 'https://safe-transaction.aurora.gnosis.io', @@ -398,7 +387,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'trust', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.EIP1559, FEATURES.ERC721, FEATURES.SAFE_APPS, @@ -447,13 +435,7 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'trust', 'walletLink', ], - features: [ - FEATURES.CONTRACT_INTERACTION, - FEATURES.ERC721, - FEATURES.SAFE_APPS, - FEATURES.SAFE_TX_GAS_OPTIONAL, - FEATURES.TX_SIMULATION, - ], + features: [FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL, FEATURES.TX_SIMULATION], }, { transactionService: 'https://safe-transaction.goerli.gnosis.io/', @@ -496,7 +478,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.DOMAIN_LOOKUP, FEATURES.EIP1559, FEATURES.ERC721, @@ -537,7 +518,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ gasPrice: [{ type: GAS_PRICE_TYPE.FIXED, weiValue: '24000000000' }], disabledWallets: ['fortmatic', 'lattice', 'tally'], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.DOMAIN_LOOKUP, FEATURES.EIP1559, FEATURES.ERC721, @@ -589,7 +569,6 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [ - FEATURES.CONTRACT_INTERACTION, FEATURES.DOMAIN_LOOKUP, FEATURES.ERC721, FEATURES.SAFE_APPS, diff --git a/src/utils/__tests__/chains.test.ts b/src/utils/__tests__/chains.test.ts index 8a12c47cd6..ed8a0cf156 100644 --- a/src/utils/__tests__/chains.test.ts +++ b/src/utils/__tests__/chains.test.ts @@ -4,7 +4,7 @@ import { CONFIG_SERVICE_CHAINS } from '@/tests/mocks/chains' describe('chains', () => { describe('hasFeature', () => { it('returns true for a feature that exists', () => { - expect(hasFeature(CONFIG_SERVICE_CHAINS[0], FEATURES.CONTRACT_INTERACTION)).toBe(true) + expect(hasFeature(CONFIG_SERVICE_CHAINS[0], FEATURES.ERC721)).toBe(true) }) it("returns false for a feature that doesn't exists", () => { diff --git a/src/utils/chains.ts b/src/utils/chains.ts index c2fe11a859..755561596a 100644 --- a/src/utils/chains.ts +++ b/src/utils/chains.ts @@ -5,7 +5,6 @@ import { getExplorerLink } from './gateway' export enum FEATURES { ERC721 = 'ERC721', SAFE_APPS = 'SAFE_APPS', - CONTRACT_INTERACTION = 'CONTRACT_INTERACTION', DOMAIN_LOOKUP = 'DOMAIN_LOOKUP', SPENDING_LIMIT = 'SPENDING_LIMIT', EIP1559 = 'EIP1559', @@ -28,6 +27,8 @@ export enum FEATURES { export const FeatureRoutes = { [AppRoutes.apps.index]: FEATURES.SAFE_APPS, [AppRoutes.swap]: FEATURES.NATIVE_SWAPS, + [AppRoutes.balances.nfts]: FEATURES.ERC721, + [AppRoutes.settings.notifications]: FEATURES.PUSH_NOTIFICATIONS, } export const hasFeature = (chain: ChainInfo, feature: FEATURES): boolean => { @@ -42,3 +43,9 @@ export const getBlockExplorerLink = ( return getExplorerLink(address, chain.blockExplorerUriTemplate) } } + +export const isRouteEnabled = (route: string, chain?: ChainInfo) => { + if (!chain) return false + const featureRoute = FeatureRoutes[route] + return !featureRoute || hasFeature(chain, featureRoute) +} diff --git a/src/utils/safe-messages.ts b/src/utils/safe-messages.ts index c6e3764e1c..27cd99d585 100644 --- a/src/utils/safe-messages.ts +++ b/src/utils/safe-messages.ts @@ -11,8 +11,8 @@ import { type SafeMessage, type EIP712TypedData, type ChainInfo, - FEATURES, } from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES } from '@/utils/chains' import { hasFeature } from './chains' import { asError } from '@/services/exceptions/utils' From 5fe39f14d695c584f209203ca7b8b1b0167cd822 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Wed, 29 May 2024 11:57:11 +0200 Subject: [PATCH 026/154] Fix: back button style (#3742) --- src/components/tx-flow/common/TxLayout/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/tx-flow/common/TxLayout/index.tsx b/src/components/tx-flow/common/TxLayout/index.tsx index 872f5e6a9c..de243b6d85 100644 --- a/src/components/tx-flow/common/TxLayout/index.tsx +++ b/src/components/tx-flow/common/TxLayout/index.tsx @@ -1,6 +1,7 @@ import useSafeInfo from '@/hooks/useSafeInfo' import { type ComponentType, type ReactElement, type ReactNode, useContext, useEffect, useState } from 'react' import { Box, Container, Grid, Typography, Button, Paper, SvgIcon, IconButton, useMediaQuery } from '@mui/material' +import ArrowBackIcon from '@mui/icons-material/ArrowBack' import { useTheme } from '@mui/material/styles' import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk' import classnames from 'classnames' @@ -146,9 +147,10 @@ const TxLayout = ({ {onBack && step > 0 && ( From 1e781a90cff0cb925c39faf16acc0246e5932397 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 11:57:58 +0200 Subject: [PATCH 027/154] chore(deps): bump next from 14.1.0 to 14.1.1 (#3681) Bumps [next](https://github.com/vercel/next.js) from 14.1.0 to 14.1.1. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v14.1.0...v14.1.1) --- updated-dependencies: - dependency-name: next dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 155 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 91 insertions(+), 66 deletions(-) diff --git a/package.json b/package.json index 9c94ff2e24..5e9277abb2 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "idb-keyval": "^6.2.1", "js-cookie": "^3.0.1", "lodash": "^4.17.21", - "next": "^14.1.0", + "next": "^14.1.1", "papaparse": "^5.3.2", "qrcode.react": "^3.1.0", "react": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index 2bfd94ad66..864e29157a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3746,10 +3746,10 @@ dependencies: webpack-bundle-analyzer "4.7.0" -"@next/env@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.0.tgz#43d92ebb53bc0ae43dcc64fb4d418f8f17d7a341" - integrity sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw== +"@next/env@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.1.tgz#80150a8440eb0022a73ba353c6088d419b908bac" + integrity sha512-7CnQyD5G8shHxQIIg3c7/pSeYFeMhsNbpU/bmvH7ZnDql7mNRgg8O2JZrhrc/soFnfBnKP4/xXNiiSIPn2w8gA== "@next/eslint-plugin-next@14.1.0": version "14.1.0" @@ -3758,50 +3758,50 @@ dependencies: glob "10.3.10" -"@next/swc-darwin-arm64@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz#70a57c87ab1ae5aa963a3ba0f4e59e18f4ecea39" - integrity sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ== - -"@next/swc-darwin-x64@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz#0863a22feae1540e83c249384b539069fef054e9" - integrity sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g== - -"@next/swc-linux-arm64-gnu@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz#893da533d3fce4aec7116fe772d4f9b95232423c" - integrity sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ== - -"@next/swc-linux-arm64-musl@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz#d81ddcf95916310b8b0e4ad32b637406564244c0" - integrity sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g== - -"@next/swc-linux-x64-gnu@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz#18967f100ec19938354332dcb0268393cbacf581" - integrity sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ== - -"@next/swc-linux-x64-musl@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz#77077cd4ba8dda8f349dc7ceb6230e68ee3293cf" - integrity sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg== - -"@next/swc-win32-arm64-msvc@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz#5f0b8cf955644104621e6d7cc923cad3a4c5365a" - integrity sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ== - -"@next/swc-win32-ia32-msvc@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz#21f4de1293ac5e5a168a412b139db5d3420a89d0" - integrity sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw== - -"@next/swc-win32-x64-msvc@14.1.0": - version "14.1.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz#e561fb330466d41807123d932b365cf3d33ceba2" - integrity sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg== +"@next/swc-darwin-arm64@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.1.tgz#b74ba7c14af7d05fa2848bdeb8ee87716c939b64" + integrity sha512-yDjSFKQKTIjyT7cFv+DqQfW5jsD+tVxXTckSe1KIouKk75t1qZmj/mV3wzdmFb0XHVGtyRjDMulfVG8uCKemOQ== + +"@next/swc-darwin-x64@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.1.tgz#82c3e67775e40094c66e76845d1a36cc29c9e78b" + integrity sha512-KCQmBL0CmFmN8D64FHIZVD9I4ugQsDBBEJKiblXGgwn7wBCSe8N4Dx47sdzl4JAg39IkSN5NNrr8AniXLMb3aw== + +"@next/swc-linux-arm64-gnu@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.1.tgz#4f4134457b90adc5c3d167d07dfb713c632c0caa" + integrity sha512-YDQfbWyW0JMKhJf/T4eyFr4b3tceTorQ5w2n7I0mNVTFOvu6CGEzfwT3RSAQGTi/FFMTFcuspPec/7dFHuP7Eg== + +"@next/swc-linux-arm64-musl@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.1.tgz#594bedafaeba4a56db23a48ffed2cef7cd09c31a" + integrity sha512-fiuN/OG6sNGRN/bRFxRvV5LyzLB8gaL8cbDH5o3mEiVwfcMzyE5T//ilMmaTrnA8HLMS6hoz4cHOu6Qcp9vxgQ== + +"@next/swc-linux-x64-gnu@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.1.tgz#cb4e75f1ff2b9bcadf2a50684605928ddfc58528" + integrity sha512-rv6AAdEXoezjbdfp3ouMuVqeLjE1Bin0AuE6qxE6V9g3Giz5/R3xpocHoAi7CufRR+lnkuUjRBn05SYJ83oKNQ== + +"@next/swc-linux-x64-musl@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.1.tgz#15f26800df941b94d06327f674819ab64b272e25" + integrity sha512-YAZLGsaNeChSrpz/G7MxO3TIBLaMN8QWMr3X8bt6rCvKovwU7GqQlDu99WdvF33kI8ZahvcdbFsy4jAFzFX7og== + +"@next/swc-win32-arm64-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.1.tgz#060c134fa7fa843666e3e8574972b2b723773dd9" + integrity sha512-1L4mUYPBMvVDMZg1inUYyPvFSduot0g73hgfD9CODgbr4xiTYe0VOMTZzaRqYJYBA9mana0x4eaAaypmWo1r5A== + +"@next/swc-win32-ia32-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.1.tgz#5c06889352b1f77e3807834a0d0afd7e2d2d1da2" + integrity sha512-jvIE9tsuj9vpbbXlR5YxrghRfMuG0Qm/nZ/1KDHc+y6FpnZ/apsgh+G6t15vefU0zp3WSpTMIdXRUsNl/7RSuw== + +"@next/swc-win32-x64-msvc@14.1.1": + version "14.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.1.tgz#d38c63a8f9b7f36c1470872797d3735b4a9c5c52" + integrity sha512-S6K6EHDU5+1KrBDLko7/c1MNy/Ya73pIAmvKeFwsF4RmBFJSO7/7YeD4FnZ4iBdzE69PpQ4sOMU9ORKeNuxe8A== "@ngraveio/bc-ur@^1.0.0", "@ngraveio/bc-ur@^1.1.5": version "1.1.6" @@ -14781,12 +14781,12 @@ next-tick@1, next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -next@^14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/next/-/next-14.1.0.tgz#b31c0261ff9caa6b4a17c5af019ed77387174b69" - integrity sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q== +next@^14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/next/-/next-14.1.1.tgz#92bd603996c050422a738e90362dff758459a171" + integrity sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww== dependencies: - "@next/env" "14.1.0" + "@next/env" "14.1.1" "@swc/helpers" "0.5.2" busboy "1.6.0" caniuse-lite "^1.0.30001579" @@ -14794,15 +14794,15 @@ next@^14.1.0: postcss "8.4.31" styled-jsx "5.1.1" optionalDependencies: - "@next/swc-darwin-arm64" "14.1.0" - "@next/swc-darwin-x64" "14.1.0" - "@next/swc-linux-arm64-gnu" "14.1.0" - "@next/swc-linux-arm64-musl" "14.1.0" - "@next/swc-linux-x64-gnu" "14.1.0" - "@next/swc-linux-x64-musl" "14.1.0" - "@next/swc-win32-arm64-msvc" "14.1.0" - "@next/swc-win32-ia32-msvc" "14.1.0" - "@next/swc-win32-x64-msvc" "14.1.0" + "@next/swc-darwin-arm64" "14.1.1" + "@next/swc-darwin-x64" "14.1.1" + "@next/swc-linux-arm64-gnu" "14.1.1" + "@next/swc-linux-arm64-musl" "14.1.1" + "@next/swc-linux-x64-gnu" "14.1.1" + "@next/swc-linux-x64-musl" "14.1.1" + "@next/swc-win32-arm64-msvc" "14.1.1" + "@next/swc-win32-ia32-msvc" "14.1.1" + "@next/swc-win32-x64-msvc" "14.1.1" no-case@^3.0.4: version "3.0.4" @@ -17584,7 +17584,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -17672,7 +17681,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -19893,7 +19909,7 @@ workbox-window@7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -19911,6 +19927,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 050e58b2ea3a5e4ae4e6223d14664b4ba3dd203e Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 29 May 2024 12:21:47 +0200 Subject: [PATCH 028/154] 1.37.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e9277abb2..3b870a826a 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.36.5", + "version": "1.37.0", "type": "module", "scripts": { "dev": "next dev", From 3da9a4e1573b1e42cb94fb7aa607f7e0c4e56db4 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Wed, 29 May 2024 18:00:29 +0200 Subject: [PATCH 029/154] feat: add new label to swaps button [SWAP-86] (#3763) * feat: add new label to swaps button --- .../sidebar/SidebarNavigation/config.tsx | 3 +++ .../sidebar/SidebarNavigation/index.tsx | 15 +++++++-------- src/components/theme/darkPalette.ts | 2 +- src/components/theme/safeTheme.ts | 11 +++++++++++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index 6797127c73..64ed08785d 100644 --- a/src/components/sidebar/SidebarNavigation/config.tsx +++ b/src/components/sidebar/SidebarNavigation/config.tsx @@ -9,11 +9,13 @@ import AppsIcon from '@/public/images/apps/apps-icon.svg' import SettingsIcon from '@/public/images/sidebar/settings.svg' import SwapIcon from '@/public/images/common/swap.svg' import { SvgIcon } from '@mui/material' +import Chip from '@mui/material/Chip' export type NavItem = { label: string icon?: ReactElement href: string + tag?: ReactElement } export const navItems: NavItem[] = [ @@ -31,6 +33,7 @@ export const navItems: NavItem[] = [ label: 'Swap', icon: , href: AppRoutes.swap, + tag: , }, { label: 'Transactions', diff --git a/src/components/sidebar/SidebarNavigation/index.tsx b/src/components/sidebar/SidebarNavigation/index.tsx index 91b08bf290..7c6fd9eba1 100644 --- a/src/components/sidebar/SidebarNavigation/index.tsx +++ b/src/components/sidebar/SidebarNavigation/index.tsx @@ -49,13 +49,6 @@ const Navigation = (): ReactElement => { } } - const getCounter = (item: NavItem) => { - // Indicate qeueued txs - if (item.href === AppRoutes.transactions.history) { - return queueSize - } - } - // Route Transactions to Queue if there are queued txs, otherwise to History const getRoute = (href: string) => { if (href === AppRoutes.transactions.history && queueSize) { @@ -75,6 +68,12 @@ const Navigation = (): ReactElement => { {enabledNavItems.map((item) => { const isSelected = currentSubdirectory === getSubdirectory(item.href) + let ItemTag = item.tag ? item.tag : null + + if (item.href === AppRoutes.transactions.history) { + ItemTag = queueSize ? : null + } + return ( { {item.label} - + {ItemTag} diff --git a/src/components/theme/darkPalette.ts b/src/components/theme/darkPalette.ts index 8ecf64ed9b..46818fd506 100644 --- a/src/components/theme/darkPalette.ts +++ b/src/components/theme/darkPalette.ts @@ -12,7 +12,7 @@ const darkPalette = { secondary: { dark: '#636669', main: '#FFFFFF', - light: '#12FF80', + light: '#B0FFC9', background: '#1B2A22', }, border: { diff --git a/src/components/theme/safeTheme.ts b/src/components/theme/safeTheme.ts index 766b4c96c7..e9108c3258 100644 --- a/src/components/theme/safeTheme.ts +++ b/src/components/theme/safeTheme.ts @@ -17,6 +17,7 @@ declare module '@mui/material/styles' { backdrop: Palette['primary'] static: Palette['primary'] } + export interface PaletteOptions { border: PaletteOptions['primary'] logo: PaletteOptions['primary'] @@ -33,6 +34,7 @@ declare module '@mui/material/styles' { export interface PaletteColor { background?: string } + export interface SimplePaletteColorOptions { background?: string } @@ -52,6 +54,7 @@ declare module '@mui/material/Button' { export interface ButtonPropsColorOverrides { background: true } + export interface ButtonPropsVariantOverrides { danger: true } @@ -279,6 +282,14 @@ const createSafeTheme = (mode: PaletteMode): Theme => { }, }, }, + MuiChip: { + styleOverrides: { + colorSuccess: ({ theme }) => ({ + backgroundColor: theme.palette.secondary.light, + height: '24px', + }), + }, + }, MuiAlert: { styleOverrides: { standardError: ({ theme }) => ({ From 4f57526194d666f1d4fd7ad56cd9e3ca4d4edb12 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Thu, 30 May 2024 14:58:19 +0200 Subject: [PATCH 030/154] fix: Add network to Rabby if unknown (#3776) --- next-env.d.ts | 1 + .../common/BuyCryptoButton/index.tsx | 2 +- .../common/SafeTokenWidget/index.tsx | 2 +- src/components/tx-flow/index.tsx | 2 +- src/hooks/useIsSidebarRoute.ts | 2 +- src/services/tx/tx-sender/sdk.ts | 38 +++++++++++-------- tsconfig.json | 36 +++++++++++++++--- 7 files changed, 57 insertions(+), 26 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03dc6..fd36f9494e 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/src/components/common/BuyCryptoButton/index.tsx b/src/components/common/BuyCryptoButton/index.tsx index 77d9e8f28c..c843586f39 100644 --- a/src/components/common/BuyCryptoButton/index.tsx +++ b/src/components/common/BuyCryptoButton/index.tsx @@ -21,7 +21,7 @@ const useOnrampAppUrl = (): string | undefined => { const useBuyCryptoHref = (): LinkProps['href'] | undefined => { const query = useSearchParams() - const safe = query.get('safe') + const safe = query?.get('safe') const appUrl = useOnrampAppUrl() return useMemo(() => { diff --git a/src/components/common/SafeTokenWidget/index.tsx b/src/components/common/SafeTokenWidget/index.tsx index d930a116e7..79c4aa10fb 100644 --- a/src/components/common/SafeTokenWidget/index.tsx +++ b/src/components/common/SafeTokenWidget/index.tsx @@ -54,7 +54,7 @@ const SafeTokenWidget = () => { const url = { pathname: AppRoutes.apps.open, - query: { safe: query.get('safe'), appUrl: GOVERNANCE_APP_URL }, + query: { safe: query?.get('safe'), appUrl: GOVERNANCE_APP_URL }, } const canRedeemSep5 = canRedeemSep5Airdrop(allocationData) diff --git a/src/components/tx-flow/index.tsx b/src/components/tx-flow/index.tsx index 46b98a6d60..5927f3796e 100644 --- a/src/components/tx-flow/index.tsx +++ b/src/components/tx-flow/index.tsx @@ -32,7 +32,7 @@ export const TxModalProvider = ({ children }: { children: ReactNode }): ReactEle const safeId = useChainId() + useSafeAddress() const prevSafeId = useRef(safeId ?? '') const pathname = usePathname() - const prevPathname = useRef(pathname) + const prevPathname = useRef(pathname) const handleModalClose = useCallback(() => { if (shouldWarn.current && !confirmClose()) { diff --git a/src/hooks/useIsSidebarRoute.ts b/src/hooks/useIsSidebarRoute.ts index a915b09fd6..280eb6fa9b 100644 --- a/src/hooks/useIsSidebarRoute.ts +++ b/src/hooks/useIsSidebarRoute.ts @@ -25,7 +25,7 @@ const TOGGLE_SIDEBAR_ROUTES = [AppRoutes.apps.open] */ export function useIsSidebarRoute(pathname?: string): [boolean, boolean] { const clientPathname = usePathname() - const route = pathname || clientPathname + const route = pathname || clientPathname || '' const noSidebar = NO_SIDEBAR_ROUTES.includes(route) const toggledSidebar = TOGGLE_SIDEBAR_ROUTES.includes(route) const router = useRouter() diff --git a/src/services/tx/tx-sender/sdk.ts b/src/services/tx/tx-sender/sdk.ts index 9379b42b37..44ac9ee94f 100644 --- a/src/services/tx/tx-sender/sdk.ts +++ b/src/services/tx/tx-sender/sdk.ts @@ -15,6 +15,7 @@ import { type OnboardAPI } from '@web3-onboard/core' import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' import { asError } from '@/services/exceptions/utils' import { UncheckedJsonRpcSigner } from '@/utils/providers/UncheckedJsonRpcSigner' +import get from 'lodash/get' export const getAndValidateSafeSDK = (): Safe => { const safeSDK = getSafeSDK() @@ -36,24 +37,29 @@ async function switchOrAddChain(walletProvider: ConnectedWallet['provider'], cha params: [{ chainId: hexChainId }], }) } catch (error) { - if ((error as Error & { code: number }).code !== UNKNOWN_CHAIN_ERROR_CODE) { - throw error + const errorCode = get(error, 'code') as number | undefined + + // Rabby emits the same error code as MM, but it is nested + const nestedErrorCode = get(error, 'data.originalError.code') as number | undefined + + if (errorCode === UNKNOWN_CHAIN_ERROR_CODE || nestedErrorCode === UNKNOWN_CHAIN_ERROR_CODE) { + const chain = await getChainConfig(chainId) + + return walletProvider.request({ + method: 'wallet_addEthereumChain', + params: [ + { + chainId: hexChainId, + chainName: chain.chainName, + nativeCurrency: chain.nativeCurrency, + rpcUrls: [chain.publicRpcUri.value], + blockExplorerUrls: [new URL(chain.blockExplorerUriTemplate.address).origin], + }, + ], + }) } - const chain = await getChainConfig(chainId) - - return walletProvider.request({ - method: 'wallet_addEthereumChain', - params: [ - { - chainId: hexChainId, - chainName: chain.chainName, - nativeCurrency: chain.nativeCurrency, - rpcUrls: [chain.publicRpcUri.value], - blockExplorerUrls: [new URL(chain.blockExplorerUriTemplate.address).origin], - }, - ], - }) + throw error } } diff --git a/tsconfig.json b/tsconfig.json index 1fc4f56929..d7d47b0276 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "es2020", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -16,11 +20,31 @@ "incremental": true, "baseUrl": ".", "paths": { - "@/*": ["./src/*"], - "@/public/*": ["./public/*"] + "@/*": [ + "./src/*" + ], + "@/public/*": [ + "./public/*" + ] }, - "plugins": [{ "name": "typescript-plugin-css-modules" }] + "plugins": [ + { + "name": "typescript-plugin-css-modules" + }, + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "src/definitions.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "src/types/contracts"] + "include": [ + "next-env.d.ts", + "src/definitions.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules", + "src/types/contracts" + ] } From b4528286aad1c9615486e75bb204501433638392 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Thu, 30 May 2024 16:16:30 +0200 Subject: [PATCH 031/154] Chore: use separate Infura tokens for dev (#3773) --- .github/workflows/build/action.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index 843ad309f7..fd3d7998f2 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -16,7 +16,19 @@ inputs: runs: using: 'composite' + steps: + - name: Set environment variables + shell: bash + run: | + if [ "${{ inputs.prod }}" = "true" ]; then + echo "NEXT_PUBLIC_INFURA_TOKEN=${{ fromJSON(inputs.secrets).NEXT_PUBLIC_INFURA_TOKEN }}" >> $GITHUB_ENV + echo "NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN=${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN }}" >> $GITHUB_ENV + else + echo "NEXT_PUBLIC_INFURA_TOKEN=${{ fromJSON(inputs.secrets).NEXT_PUBLIC_INFURA_TOKEN_DEVSTAGING }}" >> $GITHUB_ENV + echo "NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN=${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN_DEVSTAGING }}" >> $GITHUB_ENV + fi + - name: Build shell: bash run: yarn build @@ -31,8 +43,6 @@ runs: NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID }} NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LATEST_AUTH: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LATEST_AUTH }} NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LIVE_AUTH: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LIVE_AUTH }} - NEXT_PUBLIC_INFURA_TOKEN: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_INFURA_TOKEN }} - NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN }} NEXT_PUBLIC_SENTRY_DSN: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SENTRY_DSN }} NEXT_PUBLIC_TENDERLY_ORG_NAME: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_TENDERLY_ORG_NAME }} NEXT_PUBLIC_TENDERLY_PROJECT_NAME: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_TENDERLY_PROJECT_NAME }} From 146b394b7222720ba55533da71a66ea0698c27c9 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Thu, 30 May 2024 16:34:01 +0200 Subject: [PATCH 032/154] Tests: add tx filter tests (#3781) --- .../happypath/tx_history_filter_hp_1.cy.js | 136 +++++++++ .../happypath/tx_history_filter_hp_2.cy.js | 30 ++ cypress/e2e/pages/create_tx.pages.js | 76 +++++ cypress/e2e/pages/owners.pages.js | 2 +- cypress/e2e/smoke/tx_history_filter.cy.js | 277 ++++++++++++++++++ cypress/support/utils/txquery.js | 41 +++ .../transactions/TxFilterForm/index.tsx | 4 +- 7 files changed, 563 insertions(+), 3 deletions(-) create mode 100644 cypress/e2e/happypath/tx_history_filter_hp_1.cy.js create mode 100644 cypress/e2e/happypath/tx_history_filter_hp_2.cy.js create mode 100644 cypress/e2e/smoke/tx_history_filter.cy.js create mode 100644 cypress/support/utils/txquery.js diff --git a/cypress/e2e/happypath/tx_history_filter_hp_1.cy.js b/cypress/e2e/happypath/tx_history_filter_hp_1.cy.js new file mode 100644 index 0000000000..454ac5e403 --- /dev/null +++ b/cypress/e2e/happypath/tx_history_filter_hp_1.cy.js @@ -0,0 +1,136 @@ +/* eslint-disable */ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as createTx from '../pages/create_tx.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let staticSafes = [] +const startDate = '01/12/2023' +const endDate = '01/12/2023' +const startDate2 = '20/12/2023' +const endDate2 = '20/12/2023' + +describe('Tx history happy path tests 1', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_7) + main.acceptCookies() + }) + + it('Verify a user can filter incoming transactions by dates, amount and token address', () => { + const uiDate = 'Dec 1, 2023' + const uiDate2 = 'Dec 1, 2023 - 8:05:00 AM' + const uiDate3 = 'Dec 1, 2023 - 7:52:36 AM' + const uiDate4 = 'Dec 15, 2023 - 10:33:00 AM' + const amount = '0.001' + const token = '0x7CB180dE9BE0d8935EbAAc9b4fc533952Df128Ae' + + // date and amount + createTx.clickOnFilterBtn() + createTx.setTxType(createTx.filterTypes.incoming) + createTx.fillFilterForm({ endDate: endDate, amount: amount }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(2) + createTx.checkTxItemDate(0, uiDate) + createTx.checkTxItemDate(1, uiDate) + + // combined filters + createTx.clickOnFilterBtn() + createTx.fillFilterForm({ startDate: startDate }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(2) + createTx.checkTxItemDate(0, uiDate) + createTx.checkTxItemDate(1, uiDate) + + // reset txs + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.verifyNumberOfTransactions(25) + + // chronological order + createTx.fillFilterForm({ startDate: startDate, endDate: endDate }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(7) + createTx.checkTxItemDate(5, uiDate2) + createTx.checkTxItemDate(6, uiDate3) + + // token + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.fillFilterForm({ token: token }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(1) + createTx.checkTxItemDate(0, uiDate4) + + // no txs + createTx.clickOnFilterBtn() + createTx.fillFilterForm({ startDate: startDate2, endDate: endDate2 }) + createTx.clickOnApplyBtn() + createTx.verifyNoTxDisplayed('incoming') + }) + + it('Verify a user can filter outgoing transactions by dates, nonce, amount and recipient', () => { + const uiDate = 'Nov 30, 2023 - 11:06:00 AM' + const uiDate2 = 'Dec 1, 2023 - 7:54:36 AM' + const uiDate3 = 'Dec 1, 2023 - 7:37:24 AM' + const uiDate4 = 'Nov 30, 2023 - 11:02:12 AM' + const amount = '0.000000000001' + const recipient = 'sep:0x06373d5e45AD31BD354CeBfA8dB4eD2c75B8708e' + + // date and recipient + createTx.clickOnFilterBtn() + createTx.setTxType(createTx.filterTypes.outgoing) + + createTx.fillFilterForm({ endDate: endDate, recipient: recipient }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(1) + createTx.checkTxItemDate(0, uiDate4) + + // combined filters + createTx.clickOnFilterBtn() + createTx.fillFilterForm({ startDate: startDate }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(0) + + // reset txs + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(14) + + // chronological order + createTx.clickOnFilterBtn() + createTx.fillFilterForm({ startDate: startDate, endDate: endDate }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(2) + createTx.checkTxItemDate(0, uiDate2) + createTx.checkTxItemDate(1, uiDate3) + + // nonce + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.fillFilterForm({ nonce: '1' }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(1) + createTx.checkTxItemDate(0, uiDate) + + // amount + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.fillFilterForm({ amount: amount }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(1) + createTx.checkTxItemDate(0, uiDate4) + + // no txs + createTx.clickOnFilterBtn() + createTx.clickOnClearBtn() + createTx.fillFilterForm({ startDate: startDate2, endDate: endDate2 }) + createTx.clickOnApplyBtn() + createTx.verifyNoTxDisplayed('outgoing') + }) +}) diff --git a/cypress/e2e/happypath/tx_history_filter_hp_2.cy.js b/cypress/e2e/happypath/tx_history_filter_hp_2.cy.js new file mode 100644 index 0000000000..98e0f5d983 --- /dev/null +++ b/cypress/e2e/happypath/tx_history_filter_hp_2.cy.js @@ -0,0 +1,30 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as createTx from '../pages/create_tx.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let staticSafes = [] + +describe('Tx history happy path tests 2', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_8) + main.acceptCookies() + }) + + it('Verify a user can filter outgoing transactions by module', () => { + const moduleAddress = 'sep:0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134' + const uiDate = 'Jan 30, 2024 - 10:53:48 AM' + + createTx.clickOnFilterBtn() + createTx.setTxType(createTx.filterTypes.module) + createTx.fillFilterForm({ address: moduleAddress }) + createTx.clickOnApplyBtn() + createTx.verifyNumberOfTransactions(1) + createTx.checkTxItemDate(0, uiDate) + }) +}) diff --git a/cypress/e2e/pages/create_tx.pages.js b/cypress/e2e/pages/create_tx.pages.js index 49dd8f99eb..53ec026d18 100644 --- a/cypress/e2e/pages/create_tx.pages.js +++ b/cypress/e2e/pages/create_tx.pages.js @@ -32,6 +32,15 @@ const untrustedTokenWarningModal = '[data-testid="untrusted-token-warning"]' const sendTokensBtn = '[data-testid="send-tokens-btn"]' export const replacementNewSigner = '[data-testid="new-owner"]' export const messageItem = '[data-testid="message-item"]' +const filterStartDateInput = '[data-testid="start-date"]' +const filterEndDateInput = '[data-testid="end-date"]' +const filterAmountInput = '[data-testid="amount-input"]' +const filterTokenInput = '[data-testid="token-input"]' +const filterNonceInput = '[data-testid="nonce-input"]' +const filterApplyBtn = '[data-testid="apply-btn"]' +const filterClearBtn = '[data-testid="clear-btn"]' +const addressItem = '[data-testid="address-item"]' +const radioSelector = 'div[role="radiogroup"]' const viewTransactionBtn = 'View transaction' const transactionDetailsTitle = 'Transaction details' @@ -52,6 +61,73 @@ const signBtnStr = 'Sign' const expandAllBtnStr = 'Expand all' const collapseAllBtnStr = 'Collapse all' export const messageNestedStr = `"nestedString": "Test message 3 off-chain"` +const noTxFoundStr = (type) => `0 ${type} transactions found` + +export const filterTypes = { + incoming: 'Incoming', + outgoing: 'Outgoing', + module: 'Module-based', +} + +export function setTxType(type) { + cy.get(radioSelector).find('label').contains(type).click() +} + +export function verifyNoTxDisplayed(type) { + cy.get(transactionItem) + .should('have.length', 0) + .then(($items) => { + main.verifyElementsCount($items, 0) + }) + + cy.contains(noTxFoundStr(type)).should('be.visible') +} + +export function clickOnApplyBtn() { + cy.get(filterApplyBtn).click() +} + +export function clickOnClearBtn() { + cy.get(filterClearBtn).click() +} + +export function fillFilterForm({ address, startDate, endDate, amount, token, nonce, recipient } = {}) { + const inputMap = { + address: { selector: addressItem, findInput: true }, + startDate: { selector: filterStartDateInput, findInput: true }, + endDate: { selector: filterEndDateInput, findInput: true }, + amount: { selector: filterAmountInput, findInput: true }, + token: { selector: filterTokenInput, findInput: true }, + nonce: { selector: filterNonceInput, findInput: true }, + recipient: { selector: addressItem, findInput: true }, + } + + Object.entries({ address, startDate, endDate, amount, token, nonce, recipient }).forEach(([key, value]) => { + if (value !== undefined) { + const { selector, findInput } = inputMap[key] + const element = findInput ? cy.get(selector).find('input') : cy.get(selector) + element.clear().type(value) + } + }) +} + +export function clickOnFilterBtn() { + cy.get('button').then((buttons) => { + const filterButton = [...buttons].find((button) => { + return ['Filter', 'Incoming', 'Outgoing', 'Module-based'].includes(button.innerText) + }) + + if (filterButton) { + cy.wrap(filterButton).click() + } else { + throw new Error('No filter button found') + } + }) +} + +export function checkTxItemDate(index, date) { + cy.get(txDate).eq(index).should('contain', date) +} export function clickOnSendTokensBtn() { cy.get(sendTokensBtn).click() diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index 1f6a142fc1..9dd97666b9 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -242,5 +242,5 @@ export function verifyThreshold(startValue, endValue) { cy.get(thresholdInput).parent().click() cy.get(thresholdList).contains(endValue).should('be.visible') cy.get(thresholdList).find('li').should('have.length', endValue) - cy.get('body').click({ force: true }) + cy.get('body').click(0, 0) } diff --git a/cypress/e2e/smoke/tx_history_filter.cy.js b/cypress/e2e/smoke/tx_history_filter.cy.js new file mode 100644 index 0000000000..97cf73cc47 --- /dev/null +++ b/cypress/e2e/smoke/tx_history_filter.cy.js @@ -0,0 +1,277 @@ +/* eslint-disable */ +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import { buildQueryUrl } from '../../support/utils/txquery.js' +import * as constants from '../../support/constants.js' + +let staticSafes = [] +let safeAddress +const success = constants.transactionStatus.success.toUpperCase() +const txType_outgoing = 'multisig' +const txType_incoming = 'incoming' + +describe('[SMOKE] API Tx history filter tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + safeAddress = staticSafes.SEP_STATIC_SAFE_7.substring(4) + }) + + const chainId = constants.networkKeys.sepolia + + // incoming tx + it('Verify that when date range is set with 1 date, correct data is returned', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-12-14T23:00:00.000Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length).to.eq(1) + const txType = results.filter((tx) => tx.transaction.txStatus === success) + const txdirection = results.filter( + (tx) => tx.transaction.txInfo.direction === params.transactionType.toUpperCase(), + ) + expect(txType.length, 'Number of successful transactions').to.eq(1) + expect(txdirection.length, 'Number of incoming transactions').to.eq(1) + }) + }) + + it('Verify that when a large amount is set in the amount field, error is returned', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-12-14T23:00:00.000Z', + value: '893748237489328479823749823748723984728734000000000000000000', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request({ + url: url, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(400) + }) + }) + + it('Verify that applying a token for which no transaction exist returns no results', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-12-14T23:00:00.000Z', + token_address: constants.RECIPIENT_ADDRESS, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify that when the date range filter is set to only one day with no transactions, it returns no results', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-12-31T23:00:00.000Z', + token_address: constants.RECIPIENT_ADDRESS, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify setting non-existent amount with valid data range returns no results', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-11-30T23:00:00.000Z', + endDate: '2023-12-01T22:59:59.999Z', + value: '20000000000000000000', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify timestamps are within the expected range for incoming transactions', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-11-29T23:00:00.000Z', + endDate: '2023-12-15T22:59:59.999Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + results.forEach((tx) => { + const timestamp = tx.transaction.timestamp + expect(timestamp, 'Transaction timestamp').to.be.within( + new Date(params.startDate).getTime(), + new Date(params.endDate).getTime(), + ) + }) + }) + }) + + it('Verify sender and recipient addresses for incoming transactions', () => { + const params = { + transactionType: txType_incoming, + startDate: '2023-12-14T23:00:00.000Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + results.forEach((tx) => { + expect(tx.transaction.txInfo.sender.value, 'Sender address').to.match(/^0x[0-9a-fA-F]{40}$/) + expect(tx.transaction.txInfo.recipient.value, 'Recipient address').to.eq(safeAddress) + }) + }) + }) + + // outgoing tx + it('Verify that when date range is set with 1 date, correct data is returned', () => { + const params = { + transactionType: txType_outgoing, + endDate: '2023-11-30T22:59:59.999Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + const txType = results.filter((tx) => tx.transaction.txStatus === success) + expect(txType.length, 'Number of successful transactions').to.eq(11) + }) + }) + + it('Verify that when a large amount is set in the amount field, error is returned', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-12-14T23:00:00.000Z', + value: '893748237489328479823749823748723984728734000000000000000000', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request({ + url: url, + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(400) + }) + }) + + it('Verify that applying a recipient for which no transaction exist returns no results', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-12-14T23:00:00.000Z', + to: constants.RECIPIENT_ADDRESS, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify that when the date range filter is set to only one day with no transactions, it returns no results', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-12-31T23:00:00.000Z', + token_address: constants.RECIPIENT_ADDRESS, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify setting existent amount with invalid data range returns no results', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-12-15T23:00:00.000Z', + endDate: '2023-12-20T22:59:59.999Z', + value: '10000000000000000000', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify setting existent nonce with invalid end date returns no results', () => { + const params = { + transactionType: txType_outgoing, + endDate: '2023-11-28T22:59:59.999Z', + nonce: 10, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) + + it('Verify timestamps are within the expected range for transactions', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-11-29T00:00:00.000Z', + endDate: '2023-11-30T22:59:59.999Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + results.forEach((tx) => { + const timestamp = tx.transaction.timestamp + expect(timestamp, 'Transaction timestamp').to.be.within( + new Date(params.startDate).getTime(), + new Date(params.endDate).getTime(), + ) + }) + }) + }) + + it('Verify sender and recipient addresses for transactions', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-11-30T22:59:59.999Z', + endDate: '2023-11-30T22:59:59.999Z', + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + results.forEach((tx) => { + expect(tx.transaction.txInfo.sender.value, 'Sender address').to.eq(safeAddress) + expect(tx.transaction.txInfo.recipient.value, 'Recipient address').to.match(/^0x[0-9a-fA-F]{40}$/) + }) + }) + }) + + it('Verify that setting a non-existent token for transactions returns no results', () => { + const params = { + transactionType: txType_outgoing, + startDate: '2023-12-01T00:00:00.000Z', + endDate: '2023-12-01T23:59:59.999Z', + to: constants.RECIPIENT_ADDRESS, + } + const url = buildQueryUrl({ chainId, safeAddress, ...params }) + + cy.request(url).then((response) => { + const results = response.body.results + expect(results.length, 'Number of transactions').to.eq(0) + }) + }) +}) diff --git a/cypress/support/utils/txquery.js b/cypress/support/utils/txquery.js new file mode 100644 index 0000000000..c4845de5ff --- /dev/null +++ b/cypress/support/utils/txquery.js @@ -0,0 +1,41 @@ +/* eslint-disable */ +import { stagingCGWUrlv1 } from '../constants' +function buildQueryUrl({ chainId, safeAddress, transactionType, ...params }) { + const baseUrlMap = { + incoming: `${stagingCGWUrlv1}/chains/${chainId}/safes/${safeAddress}/incoming-transfers/`, + multisig: `${stagingCGWUrlv1}/chains/${chainId}/safes/${safeAddress}/multisig-transactions/`, + module: `${stagingCGWUrlv1}/chains/${chainId}/safes/${safeAddress}/module-transactions/`, + } + + const defaultParams = { + safe: `sep:${safeAddress}`, + timezone_offset: '7200000', + trusted: 'false', + } + + const paramMap = { + startDate: 'execution_date__gte', + endDate: 'execution_date__lte', + value: 'value', + tokenAddress: 'token_address', + to: 'to', + nonce: 'nonce', + module: 'module', + } + + const baseUrl = baseUrlMap[transactionType] + if (!baseUrl) { + throw new Error(`Unsupported transaction type: ${transactionType}`) + } + + const mergedParams = { ...defaultParams, ...params } + const queryString = Object.entries(mergedParams) + .map(([key, value]) => `${paramMap[key] || key}=${value}`) + .join('&') + + return baseUrl + '?' + queryString +} + +export default { + buildQueryUrl, +} diff --git a/src/components/transactions/TxFilterForm/index.tsx b/src/components/transactions/TxFilterForm/index.tsx index 2d31eb0232..9fb166b661 100644 --- a/src/components/transactions/TxFilterForm/index.tsx +++ b/src/components/transactions/TxFilterForm/index.tsx @@ -146,7 +146,7 @@ const TxFilterForm = ({ toggleFilter }: { toggleFilter: () => void }): ReactElem {!isModuleFilter && ( <> - + void }): ReactElem }} /> - + Date: Thu, 30 May 2024 16:34:32 +0200 Subject: [PATCH 033/154] Analytics: differentiate bulk executions from batches (#3779) --- src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx | 2 +- src/services/analytics/events/transactions.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx index e4fd4c17b6..819198da7b 100644 --- a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx +++ b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx @@ -141,7 +141,7 @@ export const ReviewBatch = ({ params }: { params: ExecuteBatchFlowProps }) => { return } - trackEvent({ ...TX_EVENTS.EXECUTE, label: TX_TYPES.batch }) + trackEvent({ ...TX_EVENTS.EXECUTE, label: TX_TYPES.bulk_execute }) } const submitDisabled = loading || !isSubmittable || !gasPrice diff --git a/src/services/analytics/events/transactions.ts b/src/services/analytics/events/transactions.ts index 48a456f7ee..131e527e6a 100644 --- a/src/services/analytics/events/transactions.ts +++ b/src/services/analytics/events/transactions.ts @@ -22,6 +22,7 @@ export enum TX_TYPES { walletconnect = 'walletconnect', custom = 'custom', native_swap = 'native_swap', + bulk_execute = 'bulk_execute', // Counterfactual activate_without_tx = 'activate_without_tx', From d5f5adbf13669507b52068d734d53350487d70ac Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Thu, 30 May 2024 17:07:48 +0200 Subject: [PATCH 034/154] Chore: fix branch conditionals (#3782) --- .github/workflows/deploy-dev.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index aa986eed3d..367059656e 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -41,7 +41,7 @@ jobs: - uses: ./.github/workflows/build with: secrets: ${{ toJSON(secrets) }} - prod: ${{ github.ref == 'refs/heads/main' }} + prod: ${{ github.base_ref == 'main' }} # PRs to main are Release Candidates and must be tested with prod settings - uses: ./.github/workflows/build-storybook @@ -53,14 +53,14 @@ jobs: # Staging - name: Deploy to the staging S3 - if: github.ref == 'refs/heads/main' + if: startsWith(github.ref, 'refs/heads/main') env: BUCKET: s3://${{ secrets.AWS_STAGING_BUCKET_NAME }}/current run: bash ./scripts/github/s3_upload.sh # Dev - name: Deploy to the dev S3 - if: github.ref == 'refs/heads/dev' + if: startsWith(github.ref, 'refs/heads/dev') env: BUCKET: s3://${{ secrets.AWS_DEVELOPMENT_BUCKET_NAME }} run: bash ./scripts/github/s3_upload.sh From 7537a6268ed24e9a8e3205600d2779382010c0ed Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Fri, 31 May 2024 10:02:04 +0200 Subject: [PATCH 035/154] fix: counterfactual safe tracking (#3783) --- src/components/welcome/MyAccounts/useTrackedSafesCount.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/welcome/MyAccounts/useTrackedSafesCount.ts b/src/components/welcome/MyAccounts/useTrackedSafesCount.ts index e22d788aca..06289dac02 100644 --- a/src/components/welcome/MyAccounts/useTrackedSafesCount.ts +++ b/src/components/welcome/MyAccounts/useTrackedSafesCount.ts @@ -22,11 +22,11 @@ const useTrackSafesCount = ( }, [wallet?.address]) useEffect(() => { - if (!isOwnedSafesTracked && ownedSafes && ownedSafes.length > 0 && isLoginPage) { + if (wallet && !isOwnedSafesTracked && ownedSafes && ownedSafes.length > 0 && isLoginPage) { trackEvent({ ...OVERVIEW_EVENTS.TOTAL_SAFES_OWNED, label: ownedSafes.length }) isOwnedSafesTracked = true } - }, [isLoginPage, ownedSafes]) + }, [isLoginPage, ownedSafes, wallet]) useEffect(() => { if (watchlistSafes && isLoginPage && watchlistSafes.length > 0 && !isWatchlistTracked) { From 1f6e2b6874998dfe4147907c3b4c367510c8be73 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 31 May 2024 16:04:18 +0200 Subject: [PATCH 036/154] Fix: remove the New chip from Recovery settings (#3785) --- src/features/recovery/components/RecoverySettings/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/features/recovery/components/RecoverySettings/index.tsx b/src/features/recovery/components/RecoverySettings/index.tsx index fcbeb6a5ea..0aa340ddec 100644 --- a/src/features/recovery/components/RecoverySettings/index.tsx +++ b/src/features/recovery/components/RecoverySettings/index.tsx @@ -4,7 +4,6 @@ import { RECOVERY_EVENTS } from '@/services/analytics/events/recovery' import { Box, Button, Grid, Paper, SvgIcon, Tooltip, Typography } from '@mui/material' import { type ReactElement, useMemo, useState } from 'react' -import { Chip } from '@/components/common/Chip' import ExternalLink from '@/components/common/ExternalLink' import { DelayModifierRow } from './DelayModifierRow' import useRecovery from '@/features/recovery/hooks/useRecovery' @@ -118,8 +117,6 @@ function RecoverySettings(): ReactElement { Account recovery - - From be5d93107c3a5555ac91c0cd271e8ad2f10b1b2e Mon Sep 17 00:00:00 2001 From: katspaugh Date: Mon, 3 Jun 2024 09:41:58 +0200 Subject: [PATCH 037/154] 1.37.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b870a826a..bfffed21ed 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.37.0", + "version": "1.37.1", "type": "module", "scripts": { "dev": "next dev", From f0ecdd8675e01380bed8291cdbb519d498f4e0ce Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:22:19 +0200 Subject: [PATCH 038/154] Chore: prod build on staging (main) (#3787) --- .github/workflows/deploy-dev.yml | 2 +- .github/workflows/deploy-production.yml | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 367059656e..a56a220e14 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -41,7 +41,7 @@ jobs: - uses: ./.github/workflows/build with: secrets: ${{ toJSON(secrets) }} - prod: ${{ github.base_ref == 'main' }} # PRs to main are Release Candidates and must be tested with prod settings + if: startsWith(github.ref, 'refs/heads/main') - uses: ./.github/workflows/build-storybook diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 139049865c..19968fff12 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -10,9 +10,12 @@ jobs: id-token: write runs-on: ubuntu-latest + name: Deploy release + env: ARCHIVE_NAME: ${{ github.event.repository.name }}-${{ github.event.release.tag_name }} + steps: - uses: actions/checkout@v4 @@ -36,14 +39,15 @@ jobs: aws-region: ${{ secrets.AWS_REGION }} # Script to upload release files - - name: 'Upload release build files for production' + - name: Upload release build files for production env: BUCKET: s3://${{ secrets.AWS_STAGING_BUCKET_NAME }}/releases/${{ github.event.release.tag_name }} CHECKSUM_FILE: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt run: bash ./scripts/github/s3_upload.sh # Script to prepare production deployments - - run: bash ./scripts/github/prepare_production_deployment.sh + - name: Prepare deployment + run: bash ./scripts/github/prepare_production_deployment.sh env: PROD_DEPLOYMENT_HOOK_TOKEN: ${{ secrets.PROD_DEPLOYMENT_HOOK_TOKEN }} PROD_DEPLOYMENT_HOOK_URL: ${{ secrets.PROD_DEPLOYMENT_HOOK_URL }} @@ -51,7 +55,7 @@ jobs: # Update the GitHub release with a checksummed archive - name: Upload archive - - uses: Shopify/upload-to-release@v2.0.0 + uses: Shopify/upload-to-release@v2.0.0 with: path: ${{ env.ARCHIVE_NAME }}.tar.gz name: ${{ env.ARCHIVE_NAME }}.tar.gz From f4d5157e91dc5f74fc172846d7e1915bfafe5334 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Mon, 3 Jun 2024 10:47:59 +0200 Subject: [PATCH 039/154] Chore: change release trigger --- .github/workflows/deploy-production.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 19968fff12..067d363ff9 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -2,7 +2,7 @@ name: Release on: release: - types: [published] + types: [released] jobs: release: From 0d3c33b56cac0684741f8a552cc201f3d73427dc Mon Sep 17 00:00:00 2001 From: katspaugh Date: Mon, 3 Jun 2024 10:51:59 +0200 Subject: [PATCH 040/154] Chore: temporarily remove checksum uploading --- .github/workflows/deploy-production.yml | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 067d363ff9..84a0e6ba61 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -2,7 +2,7 @@ name: Release on: release: - types: [released] + types: [published] jobs: release: @@ -52,20 +52,3 @@ jobs: PROD_DEPLOYMENT_HOOK_TOKEN: ${{ secrets.PROD_DEPLOYMENT_HOOK_TOKEN }} PROD_DEPLOYMENT_HOOK_URL: ${{ secrets.PROD_DEPLOYMENT_HOOK_URL }} VERSION_TAG: ${{ github.event.release.tag_name }} - - # Update the GitHub release with a checksummed archive - - name: Upload archive - uses: Shopify/upload-to-release@v2.0.0 - with: - path: ${{ env.ARCHIVE_NAME }}.tar.gz - name: ${{ env.ARCHIVE_NAME }}.tar.gz - content-type: application/gzip - repo-token: ${{ github.token }} - - - name: Upload checksum - uses: Shopify/upload-to-release@v2.0.0 - with: - path: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt - name: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt - content-type: text/plain - repo-token: ${{ github.token }} From e9ed505fc104d01cb098c7c2f69d58f297d7643f Mon Sep 17 00:00:00 2001 From: katspaugh Date: Mon, 3 Jun 2024 10:52:32 +0200 Subject: [PATCH 041/154] 1.37.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bfffed21ed..ea0a77454e 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.37.1", + "version": "1.37.2", "type": "module", "scripts": { "dev": "next dev", From f20bcb53f6a905c06610e55085a1dd0cc2f135c6 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:21:27 +0200 Subject: [PATCH 042/154] Fix: Error initializing the Safe Core SDK -> Error connecting to the blockchain (#3789) --- src/hooks/coreSDK/useInitSafeCoreSDK.ts | 2 +- src/services/exceptions/ErrorCodes.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/coreSDK/useInitSafeCoreSDK.ts b/src/hooks/coreSDK/useInitSafeCoreSDK.ts index 012502131e..0ca0a8fdd2 100644 --- a/src/hooks/coreSDK/useInitSafeCoreSDK.ts +++ b/src/hooks/coreSDK/useInitSafeCoreSDK.ts @@ -43,7 +43,7 @@ export const useInitSafeCoreSDK = () => { const e = asError(_e) dispatch( showNotification({ - message: 'Please try connecting your wallet again.', + message: 'Error connecting to the blockchain. Please try reloading the page.', groupKey: 'core-sdk-init-error', variant: 'error', detailedMessage: e.message, diff --git a/src/services/exceptions/ErrorCodes.ts b/src/services/exceptions/ErrorCodes.ts index 95c50e7639..31e9aae072 100644 --- a/src/services/exceptions/ErrorCodes.ts +++ b/src/services/exceptions/ErrorCodes.ts @@ -11,7 +11,7 @@ enum ErrorCodes { _101 = '101: Failed to resolve the address', _103 = '103: Error creating a SafeTransaction', _104 = '104: Invalid chain short name in the URL', - _105 = '105: Error initializing the Safe Core SDK', + _105 = '105: Error connecting to the blockchain', _106 = '106: Failed to get connected wallet', _302 = '302: Error connecting to the wallet', From 0e20e87bcd3cb0c076cb60d519c8f5b0771eac97 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Mon, 3 Jun 2024 18:35:14 +0200 Subject: [PATCH 043/154] Refactor: Replace onboard with Eip1193Provider in dispatch calls (#3745) * fix: Replace onboard with Eip1193Provider in dispatch calls * fix: Remove dangling console.log --- .../flows/ExecuteBatch/ReviewBatch.tsx | 12 +- .../RecoverAccountFlowReview.tsx | 8 +- .../tx-flow/flows/ReplaceTx/DeleteTxModal.tsx | 12 +- .../flows/SafeAppsTx/ReviewSafeAppsTx.tsx | 8 +- .../flows/SignMessage/SignMessage.test.tsx | 150 +++++------------- .../ReviewSignMessageOnChain.tsx | 8 +- .../TokenTransfer/ReviewSpendingLimitTx.tsx | 8 +- src/components/tx/SignOrExecuteForm/hooks.ts | 14 +- .../counterfactual/CounterfactualForm.tsx | 4 +- .../__tests__/useDeployGasLimit.test.ts | 16 ++ .../counterfactual/hooks/useDeployGasLimit.ts | 12 +- src/features/counterfactual/utils.ts | 25 ++- .../components/CancelRecoveryButton/index.tsx | 10 +- .../ExecuteRecoveryButton/index.tsx | 10 +- .../recovery/services/recovery-sender.ts | 48 +++--- .../speedup/components/SpeedUpModal.tsx | 10 +- .../messages/useSyncSafeMessageSigner.ts | 13 +- .../__tests__/safeMsgSender.test.ts | 59 +------ src/services/safe-messages/safeMsgSender.ts | 14 +- .../tx/tx-sender/__tests__/ts-sender.test.ts | 62 ++------ src/services/tx/tx-sender/dispatch.ts | 101 +++++------- src/services/tx/tx-sender/sdk.ts | 22 ++- src/utils/helpers.ts | 5 + 23 files changed, 270 insertions(+), 361 deletions(-) diff --git a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx index 819198da7b..5c3d9bb5fb 100644 --- a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx +++ b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx @@ -1,3 +1,5 @@ +import useWallet from '@/hooks/wallets/useWallet' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { CircularProgress, Typography, Button, CardActions, Divider, Alert } from '@mui/material' import useAsync from '@/hooks/useAsync' import { FEATURES } from '@/utils/chains' @@ -60,6 +62,7 @@ export const ReviewBatch = ({ params }: { params: ExecuteBatchFlowProps }) => { const canRelay = hasRemainingRelays(relays) const willRelay = canRelay && executionMethod === ExecutionMethod.RELAY const onboard = useOnboard() + const wallet = useWallet() const [txsWithDetails, error, loading] = useAsync(() => { if (!chain?.chainId) return @@ -87,7 +90,8 @@ export const ReviewBatch = ({ params }: { params: ExecuteBatchFlowProps }) => { }, [txsWithDetails, multiSendTxs]) const onExecute = async () => { - if (!userNonce || !onboard || !multiSendTxData || !multiSendContract || !txsWithDetails || !gasPrice) return + if (!userNonce || !onboard || !wallet || !multiSendTxData || !multiSendContract || !txsWithDetails || !gasPrice) + return const overrides: Overrides = isEIP1559 ? { maxFeePerGas: maxFeePerGas?.toString(), maxPriorityFeePerGas: maxPriorityFeePerGas?.toString() } @@ -95,12 +99,14 @@ export const ReviewBatch = ({ params }: { params: ExecuteBatchFlowProps }) => { overrides.nonce = userNonce + await assertWalletChain(onboard, safe.chainId) + await dispatchBatchExecution( txsWithDetails, multiSendContract, multiSendTxData, - onboard, - safe.chainId, + wallet.provider, + wallet.address, safe.address.value, overrides as Overrides & { nonce: number }, ) diff --git a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx index 4d78409375..1926c32cc8 100644 --- a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx +++ b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx @@ -1,5 +1,6 @@ import { trackEvent } from '@/services/analytics' import { RECOVERY_EVENTS } from '@/services/analytics/events/recovery' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { CardActions, Button, Typography, Divider, Box, CircularProgress } from '@mui/material' import { useContext, useEffect, useState } from 'react' import type { ReactElement } from 'react' @@ -70,7 +71,7 @@ export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlo // On modal submit const onSubmit = async () => { - if (!recovery || !onboard || !safeTx) { + if (!recovery || !onboard || !wallet || !safeTx) { return } @@ -79,11 +80,14 @@ export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlo setIsRejectedByUser(false) try { + await assertWalletChain(onboard, safe.chainId) + await dispatchRecoveryProposal({ - onboard, + provider: wallet.provider, safe, safeTx, delayModifierAddress: recovery.address, + signerAddress: wallet.address, }) trackEvent({ ...RECOVERY_EVENTS.SUBMIT_RECOVERY_ATTEMPT }) } catch (_err) { diff --git a/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx b/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx index 9800ac634b..f08b4395e2 100644 --- a/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx +++ b/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx @@ -1,3 +1,4 @@ +import useWallet from '@/hooks/wallets/useWallet' import { useState } from 'react' import { Dialog, @@ -14,7 +15,6 @@ import { } from '@mui/material' import { Close } from '@mui/icons-material' import madProps from '@/utils/mad-props' -import useOnboard from '@/hooks/wallets/useOnboard' import useChainId from '@/hooks/useChainId' import useSafeAddress from '@/hooks/useSafeAddress' import { deleteTx } from '@/utils/gateway' @@ -32,12 +32,12 @@ type DeleteTxModalProps = { safeTxHash: string onClose: () => void onSuccess: () => void - onboard: ReturnType + wallet: ReturnType chainId: ReturnType safeAddress: ReturnType } -const _DeleteTxModal = ({ safeTxHash, onSuccess, onClose, onboard, safeAddress, chainId }: DeleteTxModalProps) => { +const _DeleteTxModal = ({ safeTxHash, onSuccess, onClose, wallet, safeAddress, chainId }: DeleteTxModalProps) => { const [error, setError] = useState() const [isLoading, setIsLoading] = useState(false) @@ -46,7 +46,7 @@ const _DeleteTxModal = ({ safeTxHash, onSuccess, onClose, onboard, safeAddress, setIsLoading(true) trackEvent(REJECT_TX_EVENTS.DELETE_CONFIRM) - if (!onboard || !safeAddress || !chainId || !safeTxHash) { + if (!wallet?.provider || !safeAddress || !chainId || !safeTxHash) { setIsLoading(false) setError(new Error('Please connect your wallet first')) trackEvent(REJECT_TX_EVENTS.DELETE_FAIL) @@ -54,7 +54,7 @@ const _DeleteTxModal = ({ safeTxHash, onSuccess, onClose, onboard, safeAddress, } try { - const signer = await getAssertedChainSigner(onboard, chainId) + const signer = await getAssertedChainSigner(wallet.provider) await deleteTx({ safeTxHash, @@ -145,7 +145,7 @@ const _DeleteTxModal = ({ safeTxHash, onSuccess, onClose, onboard, safeAddress, } const DeleteTxModal = madProps(_DeleteTxModal, { - onboard: useOnboard, + wallet: useWallet, chainId: useChainId, safeAddress: useSafeAddress, }) diff --git a/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx b/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx index 0429faab79..4d1138ce0b 100644 --- a/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx +++ b/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx @@ -1,4 +1,6 @@ import { SWAP_TITLE } from '@/features/swap' +import useWallet from '@/hooks/wallets/useWallet' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { useContext, useEffect, useMemo } from 'react' import type { ReactElement } from 'react' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' @@ -26,6 +28,7 @@ const ReviewSafeAppsTx = ({ }: ReviewSafeAppsTxProps): ReactElement => { const { safe } = useSafeInfo() const onboard = useOnboard() + const wallet = useWallet() const { safeTx, setSafeTx, safeTxError, setSafeTxError } = useContext(SafeTxContext) useHighlightHiddenTab() @@ -48,12 +51,13 @@ const ReviewSafeAppsTx = ({ }, [txs, setSafeTx, setSafeTxError, params]) const handleSubmit = async (txId: string) => { - if (!safeTx || !onboard) return + if (!safeTx || !onboard || !wallet?.provider) return trackSafeAppTxCount(Number(appId)) let safeTxHash = '' try { - safeTxHash = await dispatchSafeAppsTx(safeTx, requestId, onboard, safe.chainId, txId) + await assertWalletChain(onboard, safe.chainId) + safeTxHash = await dispatchSafeAppsTx(safeTx, requestId, wallet.provider, txId) } catch (error) { setSafeTxError(asError(error)) } diff --git a/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx b/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx index 7a16ad5081..ca6d202f98 100644 --- a/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx +++ b/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx @@ -1,9 +1,9 @@ import { extendedSafeInfoBuilder } from '@/tests/builders/safe' import { hexlify, zeroPadValue, toUtf8Bytes } from 'ethers' -import type { SafeInfo, SafeMessage, SafeMessageListPage } from '@safe-global/safe-gateway-typescript-sdk' +import type { SafeInfo, SafeMessage } from '@safe-global/safe-gateway-typescript-sdk' import { SafeMessageListItemType } from '@safe-global/safe-gateway-typescript-sdk' - import SignMessage from './SignMessage' + import * as useIsWrongChainHook from '@/hooks/useIsWrongChain' import * as useIsSafeOwnerHook from '@/hooks/useIsSafeOwner' import * as useWalletHook from '@/hooks/wallets/useWallet' @@ -11,12 +11,13 @@ import * as useSafeInfoHook from '@/hooks/useSafeInfo' import * as useChainsHook from '@/hooks/useChains' import * as sender from '@/services/safe-messages/safeMsgSender' import * as onboard from '@/hooks/wallets/useOnboard' +import * as sdk from '@/services/tx/tx-sender/sdk' +import * as useSafeMessage from '@/hooks/messages/useSafeMessage' import { render, act, fireEvent, waitFor } from '@/tests/test-utils' import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' import type { EIP1193Provider, WalletState, AppState, OnboardAPI } from '@web3-onboard/core' import { generateSafeMessageHash } from '@/utils/safe-messages' import { getSafeMessage } from '@safe-global/safe-gateway-typescript-sdk' -import useSafeMessages from '@/hooks/messages/useSafeMessages' import { chainBuilder } from '@/tests/builders/chains' jest.mock('@safe-global/safe-gateway-typescript-sdk', () => ({ @@ -24,17 +25,6 @@ jest.mock('@safe-global/safe-gateway-typescript-sdk', () => ({ getSafeMessage: jest.fn(), })) -jest.mock('@/hooks/messages/useSafeMessages', () => ({ - __esModule: true, - default: jest.fn(() => ({ - page: { - results: [], - }, - error: undefined, - loading: false, - })), -})) - let mockProvider = { request: jest.fn, } as unknown as EIP1193Provider @@ -80,6 +70,17 @@ const mockOnboard = { }, } as unknown as OnboardAPI +const extendedSafeInfo = { + ...extendedSafeInfoBuilder().build(), + version: '1.3.0', + address: { + value: zeroPadValue('0x01', 20), + }, + chainId: '5', + threshold: 2, + deployed: true, +} + describe('SignMessage', () => { beforeAll(() => { jest.useFakeTimers() @@ -89,20 +90,8 @@ describe('SignMessage', () => { jest.useRealTimers() }) - const mockUseSafeMessages = useSafeMessages as jest.Mock - const extendedSafeInfo = { - ...extendedSafeInfoBuilder().build(), - version: '1.3.0', - address: { - value: zeroPadValue('0x01', 20), - }, - chainId: '5', - threshold: 2, - deployed: true, - } - beforeEach(() => { - jest.clearAllMocks() + jest.resetAllMocks() jest.spyOn(useSafeInfoHook, 'default').mockImplementation(() => ({ safe: extendedSafeInfo, @@ -113,6 +102,8 @@ describe('SignMessage', () => { })) jest.spyOn(useIsWrongChainHook, 'default').mockImplementation(() => false) + + jest.spyOn(sdk, 'assertWalletChain').mockImplementation(jest.fn()) }) describe('EIP-191 messages', () => { @@ -219,6 +210,7 @@ describe('SignMessage', () => { it('proposes a message if not already proposed', async () => { jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => true) jest.spyOn(onboard, 'default').mockReturnValue(mockOnboard) + jest.spyOn(useWalletHook, 'default').mockReturnValue({} as ConnectedWallet) ;(getSafeMessage as jest.Mock).mockRejectedValue(new Error('SafeMessage not found')) const { getByText, baseElement } = render( @@ -273,12 +265,7 @@ describe('SignMessage', () => { it('confirms the message if already proposed', async () => { jest.spyOn(onboard, 'default').mockReturnValue(mockOnboard) jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => true) - jest.spyOn(useWalletHook, 'default').mockImplementation( - () => - ({ - address: zeroPadValue('0x03', 20), - } as ConnectedWallet), - ) + jest.spyOn(useWalletHook, 'default').mockReturnValue({ provider: mockProvider } as unknown as ConnectedWallet) const messageText = 'Hello world!' const messageHash = generateSafeMessageHash( @@ -305,36 +292,20 @@ describe('SignMessage', () => { confirmationsSubmitted: 1, } as unknown as SafeMessage - const msgs: { - page?: SafeMessageListPage - error?: string - loading: boolean - } = { - page: { - results: [msg], - }, - error: undefined, - loading: false, - } + jest.spyOn(useSafeMessage, 'default').mockReturnValueOnce([msg, jest.fn]) - mockUseSafeMessages.mockReturnValue(msgs) - - const { getByText } = render( + const { getByText, rerender } = render( , ) - await act(async () => { - Promise.resolve() - }) - const confirmationSpy = jest .spyOn(sender, 'dispatchSafeMsgConfirmation') .mockImplementation(() => Promise.resolve()) const button = getByText('Sign') - expect(button).toBeEnabled() - ;(getSafeMessage as jest.Mock).mockResolvedValue({ + + const newMsg = { ...msg, confirmations: [ { @@ -351,26 +322,35 @@ describe('SignMessage', () => { confirmationsRequired: 2, confirmationsSubmitted: 2, preparedSignature: '0x789', - }) + } as unknown as SafeMessage + + ;(getSafeMessage as jest.Mock).mockResolvedValue(newMsg) await act(() => { fireEvent.click(button) }) + jest.spyOn(useSafeMessage, 'default').mockReturnValue([newMsg, jest.fn]) + + rerender() + expect(confirmationSpy).toHaveBeenCalledWith( expect.objectContaining({ safe: extendedSafeInfo, message: 'Hello world!', - onboard: expect.anything(), + provider: expect.anything(), }), ) - expect(getByText('Message successfully signed')).toBeInTheDocument() + await waitFor(() => { + expect(getByText('Message successfully signed')).toBeInTheDocument() + }) }) it('displays an error if no wallet is connected', () => { jest.spyOn(useWalletHook, 'default').mockReturnValue(null) jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => false) + jest.spyOn(useSafeMessage, 'default').mockImplementation(() => [undefined, jest.fn()]) const { getByText } = render( { jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => true) jest.spyOn(useIsWrongChainHook, 'default').mockImplementation(() => true) jest.spyOn(useChainsHook, 'useCurrentChain').mockReturnValue(chainBuilder().build()) + jest.spyOn(useSafeMessage, 'default').mockImplementation(() => [undefined, jest.fn()]) const { getByText } = render( { } as ConnectedWallet), ) jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => false) + jest.spyOn(useSafeMessage, 'default').mockImplementation(() => [undefined, jest.fn()]) const { getByText } = render( { confirmationsSubmitted: 1, } as unknown as SafeMessage - const msgs: { - page?: SafeMessageListPage - error?: string - loading: boolean - } = { - page: { - results: [msg], - }, - error: undefined, - loading: false, - } - - mockUseSafeMessages.mockReturnValue(msgs) + jest.spyOn(useSafeMessage, 'default').mockReturnValue([msg, jest.fn]) const { getByText } = render( , @@ -503,19 +473,7 @@ describe('SignMessage', () => { } as ConnectedWallet), ) - const msgs: { - page?: SafeMessageListPage - error?: string - loading: boolean - } = { - page: { - results: [], - }, - error: undefined, - loading: false, - } - - mockUseSafeMessages.mockReturnValue(msgs) + jest.spyOn(useSafeMessage, 'default').mockReturnValue([undefined, jest.fn()]) jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => true) ;(getSafeMessage as jest.Mock).mockRejectedValue(new Error('SafeMessage not found')) @@ -583,19 +541,7 @@ describe('SignMessage', () => { } as unknown as SafeMessage ;(getSafeMessage as jest.Mock).mockResolvedValue(msg) - const msgs: { - page?: SafeMessageListPage - error?: string - loading: boolean - } = { - page: { - results: [msg], - }, - error: undefined, - loading: false, - } - - mockUseSafeMessages.mockReturnValue(msgs) + jest.spyOn(useSafeMessage, 'default').mockReturnValue([msg, jest.fn()]) const { getByText } = render( , @@ -664,19 +610,7 @@ describe('SignMessage', () => { preparedSignature: '0x678', } as unknown as SafeMessage - const msgs: { - page?: SafeMessageListPage - error?: string - loading: boolean - } = { - page: { - results: [], - }, - error: undefined, - loading: false, - } - - mockUseSafeMessages.mockReturnValue(msgs) + jest.spyOn(useSafeMessage, 'default').mockReturnValue([msg, jest.fn()]) ;(getSafeMessage as jest.Mock).mockResolvedValue(msg) const { getByText } = render( diff --git a/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx b/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx index 25bc17b513..74c54a9fd5 100644 --- a/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx +++ b/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx @@ -1,3 +1,5 @@ +import useWallet from '@/hooks/wallets/useWallet' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import type { ReactElement } from 'react' import { useContext, useEffect, useState } from 'react' import { useMemo } from 'react' @@ -41,6 +43,7 @@ const ReviewSignMessageOnChain = ({ message, method, requestId }: SignMessageOnC const chainId = useChainId() const { safe } = useSafeInfo() const onboard = useOnboard() + const wallet = useWallet() const { safeTx, setSafeTx, setSafeTxError } = useContext(SafeTxContext) useHighlightHiddenTab() @@ -108,10 +111,11 @@ const ReviewSignMessageOnChain = ({ message, method, requestId }: SignMessageOnC ]) const handleSubmit = async () => { - if (!safeTx || !onboard) return + if (!safeTx || !onboard || !wallet) return try { - await dispatchSafeAppsTx(safeTx, requestId, onboard, safe.chainId) + await assertWalletChain(onboard, safe.chainId) + await dispatchSafeAppsTx(safeTx, requestId, wallet.provider) } catch (error) { setSafeTxError(asError(error)) } diff --git a/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx b/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx index b78d655289..86318c1361 100644 --- a/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx +++ b/src/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx.tsx @@ -1,3 +1,5 @@ +import useWallet from '@/hooks/wallets/useWallet' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import type { ReactElement, SyntheticEvent } from 'react' import { useContext, useMemo, useState } from 'react' import { type BigNumberish, type BytesLike, parseUnits } from 'ethers' @@ -51,6 +53,7 @@ const ReviewSpendingLimitTx = ({ const { setTxFlow } = useContext(TxModalContext) const currentChain = useCurrentChain() const onboard = useOnboard() + const wallet = useWallet() const { safe, safeAddress } = useSafeInfo() const { balances } = useBalances() const token = balances.items.find((item) => item.tokenInfo.address === params.tokenAddress) @@ -83,7 +86,7 @@ const ReviewSpendingLimitTx = ({ const handleSubmit = async (e: SyntheticEvent) => { e.preventDefault() - if (!onboard) return + if (!onboard || !wallet) return trackEvent(MODALS_EVENTS.USE_SPENDING_LIMIT) @@ -94,7 +97,8 @@ const ReviewSpendingLimitTx = ({ const txOptions = getTxOptions(advancedParams, currentChain) try { - await dispatchSpendingLimitTxExecution(txParams, txOptions, onboard, safe.chainId, safeAddress) + await assertWalletChain(onboard, safe.chainId) + await dispatchSpendingLimitTxExecution(txParams, txOptions, wallet.provider, safe.chainId, safeAddress) onSubmit('', true) setTxFlow(undefined) } catch (_err) { diff --git a/src/components/tx/SignOrExecuteForm/hooks.ts b/src/components/tx/SignOrExecuteForm/hooks.ts index c7a4669ea9..dd84f9a6f9 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.ts @@ -1,3 +1,4 @@ +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { assertTx, assertWallet, assertOnboard } from '@/utils/helpers' import { useMemo } from 'react' import { type TransactionOptions, type SafeTransaction } from '@safe-global/safe-core-sdk-types' @@ -64,13 +65,12 @@ export const useTxActions = (): TxActions => { const signRelayedTx = async (safeTx: SafeTransaction, txId?: string): Promise => { assertTx(safeTx) assertWallet(wallet) - assertOnboard(onboard) // Smart contracts cannot sign transactions off-chain if (await isSmartContractWallet(wallet.chainId, wallet.address)) { throw new Error('Cannot relay an unsigned transaction from a smart contract wallet') } - return await dispatchTxSigning(safeTx, version, onboard, chainId, txId) + return await dispatchTxSigning(safeTx, version, wallet.provider, txId) } const signTx: TxActions['signTx'] = async (safeTx, txId, origin) => { @@ -78,18 +78,20 @@ export const useTxActions = (): TxActions => { assertWallet(wallet) assertOnboard(onboard) + await assertWalletChain(onboard, chainId) + // Smart contract wallets must sign via an on-chain tx if (await isSmartContractWallet(wallet.chainId, wallet.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 proposeTx(wallet.address, safeTx, txId, origin)).txId - await dispatchOnChainSigning(safeTx, id, onboard, chainId) + await dispatchOnChainSigning(safeTx, id, wallet.provider, chainId) return id } // Otherwise, sign off-chain - const signedTx = await dispatchTxSigning(safeTx, version, onboard, chainId, txId) + const signedTx = await dispatchTxSigning(safeTx, version, wallet.provider, txId) const tx = await proposeTx(wallet.address, signedTx, txId, origin) return tx.txId } @@ -99,6 +101,8 @@ export const useTxActions = (): TxActions => { assertWallet(wallet) assertOnboard(onboard) + await assertWalletChain(onboard, chainId) + let tx: TransactionDetails | undefined // Relayed transactions must be fully signed, so request a final signature if needed if (isRelayed && safeTx.signatures.size < safe.threshold) { @@ -122,7 +126,7 @@ export const useTxActions = (): TxActions => { if (isRelayed) { await dispatchTxRelay(safeTx, safe, txId, txOptions.gasLimit) } else { - await dispatchTxExecution(safeTx, txOptions, txId, onboard, chainId, safeAddress) + await dispatchTxExecution(safeTx, txOptions, txId, wallet.provider, wallet.address, safeAddress) } return txId diff --git a/src/features/counterfactual/CounterfactualForm.tsx b/src/features/counterfactual/CounterfactualForm.tsx index 8f613d0b4e..3097f5f87d 100644 --- a/src/features/counterfactual/CounterfactualForm.tsx +++ b/src/features/counterfactual/CounterfactualForm.tsx @@ -8,6 +8,7 @@ import useOnboard from '@/hooks/wallets/useOnboard' import useWallet from '@/hooks/wallets/useWallet' import { OVERVIEW_EVENTS, trackEvent, WALLET_EVENTS } from '@/services/analytics' import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import madProps from '@/utils/mad-props' import React, { type ReactElement, type SyntheticEvent, useContext, useState } from 'react' import { CircularProgress, Box, Button, CardActions, Divider, Alert } from '@mui/material' @@ -81,7 +82,8 @@ export const CounterfactualForm = ({ try { trackEvent({ ...OVERVIEW_EVENTS.PROCEED_WITH_TX, label: TX_TYPES.activate_with_tx }) - await deploySafeAndExecuteTx(txOptions, chainId, wallet, safeTx, onboard) + onboard && (await assertWalletChain(onboard, chainId)) + await deploySafeAndExecuteTx(txOptions, wallet, safeTx, wallet?.provider) trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.activate_with_tx }) trackEvent({ ...TX_EVENTS.EXECUTE, label: TX_TYPES.activate_with_tx }) diff --git a/src/features/counterfactual/__tests__/useDeployGasLimit.test.ts b/src/features/counterfactual/__tests__/useDeployGasLimit.test.ts index 6f6c7005d9..0579fc2a6f 100644 --- a/src/features/counterfactual/__tests__/useDeployGasLimit.test.ts +++ b/src/features/counterfactual/__tests__/useDeployGasLimit.test.ts @@ -1,5 +1,7 @@ import useDeployGasLimit from '@/features/counterfactual/hooks/useDeployGasLimit' +import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' import * as onboard from '@/hooks/wallets/useOnboard' +import * as useWallet from '@/hooks/wallets/useWallet' import * as sdk from '@/services/tx/tx-sender/sdk' import { safeTxBuilder } from '@/tests/builders/safeTx' import * as protocolKit from '@safe-global/protocol-kit' @@ -13,6 +15,13 @@ import { faker } from '@faker-js/faker' import type { CompatibilityFallbackHandlerContract, SimulateTxAccessorContract } from '@safe-global/safe-core-sdk-types' describe('useDeployGasLimit hook', () => { + beforeEach(() => { + jest.resetAllMocks() + + jest.spyOn(useWallet, 'default').mockReturnValue({} as ConnectedWallet) + jest.spyOn(sdk, 'assertWalletChain').mockImplementation(jest.fn()) + }) + it('returns undefined in onboard is not initialized', () => { jest.spyOn(onboard, 'default').mockReturnValue(undefined) const { result } = renderHook(() => useDeployGasLimit()) @@ -20,6 +29,13 @@ describe('useDeployGasLimit hook', () => { expect(result.current.gasLimit).toBeUndefined() }) + it('returns undefined in there is no wallet connected', () => { + jest.spyOn(useWallet, 'default').mockReturnValue(null) + const { result } = renderHook(() => useDeployGasLimit()) + + expect(result.current.gasLimit).toBeUndefined() + }) + it('returns safe deployment gas estimation', async () => { const mockGas = '100' const mockOnboard = {} as OnboardAPI diff --git a/src/features/counterfactual/hooks/useDeployGasLimit.ts b/src/features/counterfactual/hooks/useDeployGasLimit.ts index 499adb9232..45e4061008 100644 --- a/src/features/counterfactual/hooks/useDeployGasLimit.ts +++ b/src/features/counterfactual/hooks/useDeployGasLimit.ts @@ -1,7 +1,8 @@ import useAsync from '@/hooks/useAsync' import useChainId from '@/hooks/useChainId' import useOnboard from '@/hooks/wallets/useOnboard' -import { getSafeSDKWithSigner } from '@/services/tx/tx-sender/sdk' +import useWallet from '@/hooks/wallets/useWallet' +import { assertWalletChain, getSafeSDKWithSigner } from '@/services/tx/tx-sender/sdk' import { estimateSafeDeploymentGas, estimateTxBaseGas } from '@safe-global/protocol-kit' import type Safe from '@safe-global/protocol-kit' @@ -21,11 +22,14 @@ type DeployGasLimitProps = { const useDeployGasLimit = (safeTx?: SafeTransaction) => { const onboard = useOnboard() + const wallet = useWallet() const chainId = useChainId() const [gasLimit, gasLimitError, gasLimitLoading] = useAsync(async () => { - if (!onboard) return - const sdk = await getSafeSDKWithSigner(onboard, chainId) + if (!wallet || !onboard) return + + await assertWalletChain(onboard, chainId) + const sdk = await getSafeSDKWithSigner(wallet.provider) const [baseGas, batchTxGas, safeDeploymentGas] = await Promise.all([ safeTx ? estimateTxBaseGas(sdk, safeTx) : '0', @@ -37,7 +41,7 @@ const useDeployGasLimit = (safeTx?: SafeTransaction) => { const safeTxGas = totalGas - BigInt(safeDeploymentGas) return { safeTxGas, safeDeploymentGas, totalGas } - }, [onboard, chainId, safeTx]) + }, [onboard, wallet, chainId, safeTx]) return { gasLimit, gasLimitError, gasLimitLoading } } diff --git a/src/features/counterfactual/utils.ts b/src/features/counterfactual/utils.ts index 3260aef775..5812ba6eed 100644 --- a/src/features/counterfactual/utils.ts +++ b/src/features/counterfactual/utils.ts @@ -8,14 +8,14 @@ import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import { createWeb3, getWeb3ReadOnly } from '@/hooks/wallets/web3' import { asError } from '@/services/exceptions/utils' import ExternalStore from '@/services/ExternalStore' -import { assertWalletChain, getUncheckedSafeSDK, tryOffChainTxSigning } from '@/services/tx/tx-sender/sdk' +import { getUncheckedSafeSDK, tryOffChainTxSigning } from '@/services/tx/tx-sender/sdk' import { getRelayTxStatus, TaskState } from '@/services/tx/txMonitor' import type { AppDispatch } from '@/store' import { addOrUpdateSafe } from '@/store/addedSafesSlice' import { upsertAddressBookEntry } from '@/store/addressBookSlice' import { defaultSafeInfo } from '@/store/safeInfoSlice' import { didRevert, type EthersError } from '@/utils/ethers-utils' -import { assertOnboard, assertTx, assertWallet } from '@/utils/helpers' +import { assertProvider, assertTx, assertWallet } from '@/utils/helpers' import type { DeploySafeProps, PredictedSafeProps } from '@safe-global/protocol-kit' import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' import type { SafeTransaction, SafeVersion, TransactionOptions } from '@safe-global/safe-core-sdk-types' @@ -23,11 +23,9 @@ import { type ChainInfo, ImplementationVersionState, type SafeBalanceResponse, - type SafeInfo, TokenType, } from '@safe-global/safe-gateway-typescript-sdk' -import type { OnboardAPI } from '@web3-onboard/core' -import type { BrowserProvider, ContractTransactionResponse, Provider } from 'ethers' +import type { BrowserProvider, ContractTransactionResponse, Eip1193Provider, Provider } from 'ethers' import type { NextRouter } from 'next/router' export const getUndeployedSafeInfo = (undeployedSafe: PredictedSafeProps, address: string, chainId: string) => { @@ -50,19 +48,17 @@ export const CF_TX_GROUP_KEY = 'cf-tx' export const dispatchTxExecutionAndDeploySafe = async ( safeTx: SafeTransaction, txOptions: TransactionOptions, - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, ) => { - const sdkUnchecked = await getUncheckedSafeSDK(onboard, chainId) + const sdkUnchecked = await getUncheckedSafeSDK(provider) const eventParams = { groupKey: CF_TX_GROUP_KEY } let result: ContractTransactionResponse | undefined try { const signedTx = await tryOffChainTxSigning(safeTx, await sdkUnchecked.getContractVersion(), sdkUnchecked) - const wallet = await assertWalletChain(onboard, chainId) - const provider = createWeb3(wallet.provider) - const signer = await provider.getSigner() + const browserProvider = createWeb3(provider) + const signer = await browserProvider.getSigner() const deploymentTx = await sdkUnchecked.wrapSafeTransactionIntoDeploymentBatch(signedTx, txOptions) @@ -83,16 +79,15 @@ export const dispatchTxExecutionAndDeploySafe = async ( export const deploySafeAndExecuteTx = async ( txOptions: TransactionOptions, - chainId: string, wallet: ConnectedWallet | null, safeTx?: SafeTransaction, - onboard?: OnboardAPI, + provider?: Eip1193Provider, ) => { assertTx(safeTx) assertWallet(wallet) - assertOnboard(onboard) + assertProvider(provider) - return dispatchTxExecutionAndDeploySafe(safeTx, txOptions, onboard, chainId) + return dispatchTxExecutionAndDeploySafe(safeTx, txOptions, provider) } export const { getStore: getNativeBalance, setStore: setNativeBalance } = new ExternalStore(0n) diff --git a/src/features/recovery/components/CancelRecoveryButton/index.tsx b/src/features/recovery/components/CancelRecoveryButton/index.tsx index 23c683311b..0bb427872c 100644 --- a/src/features/recovery/components/CancelRecoveryButton/index.tsx +++ b/src/features/recovery/components/CancelRecoveryButton/index.tsx @@ -1,5 +1,7 @@ +import useWallet from '@/hooks/wallets/useWallet' import { trackEvent } from '@/services/analytics' import { RECOVERY_EVENTS } from '@/services/analytics/events/recovery' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { Button } from '@mui/material' import { useContext } from 'react' import type { SyntheticEvent, ReactElement } from 'react' @@ -29,6 +31,7 @@ export function CancelRecoveryButton({ const { isExpired, isPending } = useRecoveryTxState(recovery) const { setTxFlow } = useContext(TxModalContext) const onboard = useOnboard() + const wallet = useWallet() const { safe } = useSafeInfo() const onClick = async (e: SyntheticEvent) => { @@ -38,13 +41,16 @@ export function CancelRecoveryButton({ trackEvent(RECOVERY_EVENTS.CANCEL_RECOVERY) if (isOwner) { setTxFlow() - } else if (onboard) { + } else if (onboard && wallet) { try { + await assertWalletChain(onboard, safe.chainId) + await dispatchRecoverySkipExpired({ - onboard, + provider: wallet.provider, chainId: safe.chainId, delayModifierAddress: recovery.address, recoveryTxHash: recovery.args.txHash, + signerAddress: wallet.address, }) } catch (_err) { const err = asError(_err) diff --git a/src/features/recovery/components/ExecuteRecoveryButton/index.tsx b/src/features/recovery/components/ExecuteRecoveryButton/index.tsx index 400b404a39..9776b89413 100644 --- a/src/features/recovery/components/ExecuteRecoveryButton/index.tsx +++ b/src/features/recovery/components/ExecuteRecoveryButton/index.tsx @@ -1,3 +1,5 @@ +import useWallet from '@/hooks/wallets/useWallet' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { Button, Tooltip } from '@mui/material' import { useContext } from 'react' import type { SyntheticEvent, ReactElement } from 'react' @@ -22,22 +24,26 @@ export function ExecuteRecoveryButton({ const { setSubmitError } = useContext(RecoveryListItemContext) const { isExecutable, isNext, isPending } = useRecoveryTxState(recovery) const onboard = useOnboard() + const wallet = useWallet() const { safe } = useSafeInfo() const onClick = async (e: SyntheticEvent) => { e.stopPropagation() e.preventDefault() - if (!onboard) { + if (!onboard || !wallet) { return } try { + await assertWalletChain(onboard, safe.chainId) + await dispatchRecoveryExecution({ - onboard, + provider: wallet.provider, chainId: safe.chainId, args: recovery.args, delayModifierAddress: recovery.address, + signerAddress: wallet.address, }) } catch (_err) { const err = asError(_err) diff --git a/src/features/recovery/services/recovery-sender.ts b/src/features/recovery/services/recovery-sender.ts index 3f2b48503c..20192febff 100644 --- a/src/features/recovery/services/recovery-sender.ts +++ b/src/features/recovery/services/recovery-sender.ts @@ -1,37 +1,34 @@ import { getModuleInstance, KnownContracts } from '@gnosis.pm/zodiac' import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' -import type { OnboardAPI } from '@web3-onboard/core' import type { TransactionAddedEvent } from '@gnosis.pm/zodiac/dist/cjs/types/Delay' -import type { TransactionResponse } from 'ethers' +import type { Eip1193Provider, TransactionResponse } from 'ethers' import { createWeb3 } from '@/hooks/wallets/web3' import { didReprice, didRevert } from '@/utils/ethers-utils' import { recoveryDispatch, RecoveryEvent, RecoveryTxType } from './recoveryEvents' import { asError } from '@/services/exceptions/utils' -import { assertWalletChain } from '../../../services/tx/tx-sender/sdk' import { isSmartContractWallet } from '@/utils/wallets' import { UncheckedJsonRpcSigner } from '@/utils/providers/UncheckedJsonRpcSigner' async function getDelayModifierContract({ - onboard, + provider, chainId, delayModifierAddress, + signerAddress, }: { - onboard: OnboardAPI + provider: Eip1193Provider chainId: string delayModifierAddress: string + signerAddress: string }) { - // Switch signer to chain of Safe - const wallet = await assertWalletChain(onboard, chainId) + const browserProvider = createWeb3(provider) + const isSmartContract = await isSmartContractWallet(chainId, signerAddress) - const provider = createWeb3(wallet.provider) - const isSmartContract = await isSmartContractWallet(wallet.chainId, wallet.address) - - const originalSigner = await provider.getSigner() + const originalSigner = await browserProvider.getSigner() // Use unchecked signer for smart contract wallets as transactions do not necessarily immediately execute const signer = isSmartContract - ? new UncheckedJsonRpcSigner(provider, await originalSigner.getAddress()) + ? new UncheckedJsonRpcSigner(browserProvider, await originalSigner.getAddress()) : originalSigner const delayModifier = getModuleInstance(KnownContracts.DELAY, delayModifierAddress, signer).connect(signer) @@ -80,20 +77,23 @@ function waitForRecoveryTx({ } export async function dispatchRecoveryProposal({ - onboard, + provider, safe, safeTx, delayModifierAddress, + signerAddress, }: { - onboard: OnboardAPI + provider: Eip1193Provider safe: SafeInfo safeTx: SafeTransaction delayModifierAddress: string + signerAddress: string }) { const { delayModifier, isUnchecked } = await getDelayModifierContract({ - onboard, + provider, chainId: safe.chainId, delayModifierAddress, + signerAddress, }) const txType = RecoveryTxType.PROPOSAL @@ -143,20 +143,23 @@ export async function dispatchRecoveryProposal({ } export async function dispatchRecoveryExecution({ - onboard, + provider, chainId, args, delayModifierAddress, + signerAddress, }: { - onboard: OnboardAPI + provider: Eip1193Provider chainId: string args: TransactionAddedEvent.Log['args'] delayModifierAddress: string + signerAddress: string }) { const { delayModifier, isUnchecked } = await getDelayModifierContract({ - onboard, + provider, chainId, delayModifierAddress, + signerAddress, }) const txType = RecoveryTxType.EXECUTION @@ -192,20 +195,23 @@ export async function dispatchRecoveryExecution({ } export async function dispatchRecoverySkipExpired({ - onboard, + provider, chainId, delayModifierAddress, recoveryTxHash, + signerAddress, }: { - onboard: OnboardAPI + provider: Eip1193Provider chainId: string delayModifierAddress: string recoveryTxHash: string + signerAddress: string }) { const { delayModifier, isUnchecked } = await getDelayModifierContract({ - onboard, + provider, chainId, delayModifierAddress, + signerAddress, }) const txType = RecoveryTxType.SKIP_EXPIRED diff --git a/src/features/speedup/components/SpeedUpModal.tsx b/src/features/speedup/components/SpeedUpModal.tsx index 8705f4bec8..d4a054a93e 100644 --- a/src/features/speedup/components/SpeedUpModal.tsx +++ b/src/features/speedup/components/SpeedUpModal.tsx @@ -1,5 +1,6 @@ import useGasPrice from '@/hooks/useGasPrice' import ModalDialog from '@/components/common/ModalDialog' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import DialogContent from '@mui/material/DialogContent' import { Box, Button, SvgIcon, Tooltip, Typography } from '@mui/material' import RocketSpeedup from '@/public/images/common/ic-rocket-speedup.svg' @@ -89,12 +90,15 @@ export const SpeedUpModal = ({ try { setWaitingForConfirmation(true) + await assertWalletChain(onboard, chainInfo.chainId) + if (pendingTx.txType === PendingTxType.SAFE_TX) { await dispatchSafeTxSpeedUp( txOptions as Omit & { nonce: number }, txId, - onboard, + wallet.provider, chainInfo.chainId, + wallet.address, safeAddress, ) const txType = await getTransactionTrackingType(chainInfo.chainId, txId) @@ -105,8 +109,8 @@ export const SpeedUpModal = ({ txId, pendingTx.to, pendingTx.data, - onboard, - chainInfo?.chainId, + wallet.provider, + wallet.address, safeAddress, ) // Currently all custom txs are batch executes diff --git a/src/hooks/messages/useSyncSafeMessageSigner.ts b/src/hooks/messages/useSyncSafeMessageSigner.ts index baab16603d..a22a2014b9 100644 --- a/src/hooks/messages/useSyncSafeMessageSigner.ts +++ b/src/hooks/messages/useSyncSafeMessageSigner.ts @@ -1,7 +1,9 @@ +import useWallet from '@/hooks/wallets/useWallet' import { Errors, logError } from '@/services/exceptions' import { asError } from '@/services/exceptions/utils' import { dispatchPreparedSignature } from '@/services/safe-messages/safeMsgNotifications' import { dispatchSafeMsgProposal, dispatchSafeMsgConfirmation } from '@/services/safe-messages/safeMsgSender' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { getSafeMessage, SafeMessageListItemType, @@ -38,6 +40,7 @@ const useSyncSafeMessageSigner = ( ) => { const [submitError, setSubmitError] = useState() const onboard = useOnboard() + const wallet = useWallet() const { safe } = useSafeInfo() // If the message gets updated in the messageSlice we dispatch it if the signature is complete @@ -51,16 +54,18 @@ const useSyncSafeMessageSigner = ( const onSign = useCallback(async () => { // Error is shown when no wallet is connected, this appeases TypeScript - if (!onboard) { + if (!onboard || !wallet) { return } setSubmitError(undefined) try { + await assertWalletChain(onboard, safe.chainId) + // When collecting the first signature if (!message) { - await dispatchSafeMsgProposal({ onboard, safe, message: decodedMessage, safeAppId }) + await dispatchSafeMsgProposal({ provider: wallet.provider, safe, message: decodedMessage, safeAppId }) // Fetch updated message const updatedMsg = await fetchSafeMessage(safeMessageHash, safe.chainId) @@ -71,7 +76,7 @@ const useSyncSafeMessageSigner = ( } return updatedMsg } else { - await dispatchSafeMsgConfirmation({ onboard, safe, message: decodedMessage }) + await dispatchSafeMsgConfirmation({ provider: wallet.provider, safe, message: decodedMessage }) // No requestID => we are in the confirm message dialog and do not need to leave the window open if (!requestId) { @@ -86,7 +91,7 @@ const useSyncSafeMessageSigner = ( } catch (e) { setSubmitError(asError(e)) } - }, [onboard, requestId, message, safe, decodedMessage, safeAppId, safeMessageHash, onClose]) + }, [onboard, wallet, safe, message, decodedMessage, safeAppId, safeMessageHash, onClose, requestId]) return { submitError, onSign } } diff --git a/src/services/safe-messages/__tests__/safeMsgSender.test.ts b/src/services/safe-messages/__tests__/safeMsgSender.test.ts index 744bd3e762..57e3e273ac 100644 --- a/src/services/safe-messages/__tests__/safeMsgSender.test.ts +++ b/src/services/safe-messages/__tests__/safeMsgSender.test.ts @@ -1,3 +1,4 @@ +import { MockEip1193Provider } from '@/tests/mocks/providers' import * as gateway from '@safe-global/safe-gateway-typescript-sdk' import type { JsonRpcSigner } from 'ethers' import { zeroPadBytes } from 'ethers' @@ -7,7 +8,6 @@ import * as utils from '@/utils/safe-messages' import * as events from '@/services/safe-messages/safeMsgEvents' import * as sdk from '@/services/tx/tx-sender/sdk' import { zeroPadValue } from 'ethers' -import type { EIP1193Provider, OnboardAPI, WalletState, AppState } from '@web3-onboard/core' jest.mock('@safe-global/safe-gateway-typescript-sdk', () => ({ ...jest.requireActual('@safe-global/safe-gateway-typescript-sdk'), @@ -15,51 +15,6 @@ jest.mock('@safe-global/safe-gateway-typescript-sdk', () => ({ confirmSafeMessage: jest.fn(), })) -let mockProvider = { - request: jest.fn, -} as unknown as EIP1193Provider - -const mockOnboardState = { - chains: [], - walletModules: [], - wallets: [ - { - label: 'Wallet 1', - icon: '', - provider: mockProvider, - chains: [{ id: '0x5' }], - accounts: [ - { - address: '0x1234567890123456789012345678901234567890', - ens: null, - balance: null, - }, - ], - }, - ] as WalletState[], - accountCenter: { - enabled: true, - }, -} as unknown as AppState - -const mockOnboard = { - connectWallet: jest.fn(), - disconnectWallet: jest.fn(), - setChain: jest.fn(), - state: { - select: (key: keyof AppState) => ({ - subscribe: (next: any) => { - next(mockOnboardState[key]) - - return { - unsubscribe: jest.fn(), - } - }, - }), - get: () => mockOnboardState, - }, -} as unknown as OnboardAPI - const mockValidSignature = `${zeroPadBytes('0x0456', 64)}1c` const mockSignatureWithInvalidV = `${zeroPadBytes('0x0456', 64)}01` describe('safeMsgSender', () => { @@ -90,7 +45,7 @@ describe('safeMsgSender', () => { const message = 'Hello world' const safeAppId = 1 - await dispatchSafeMsgProposal({ onboard: mockOnboard, safe, message, safeAppId }) + await dispatchSafeMsgProposal({ provider: MockEip1193Provider, safe, message, safeAppId }) expect(proposeSafeMessageSpy).toHaveBeenCalledWith('5', zeroPadValue('0x0789', 20), { message, @@ -135,7 +90,7 @@ describe('safeMsgSender', () => { } const safeAppId = 1 - await dispatchSafeMsgProposal({ onboard: mockOnboard, safe, message, safeAppId }) + await dispatchSafeMsgProposal({ provider: MockEip1193Provider, safe, message, safeAppId }) // Normalize message manually message.types['EIP712Domain'] = [ @@ -172,7 +127,7 @@ describe('safeMsgSender', () => { const message = 'Hello world' const safeAppId = 1 - await dispatchSafeMsgProposal({ onboard: mockOnboard, safe, message, safeAppId }) + await dispatchSafeMsgProposal({ provider: MockEip1193Provider, safe, message, safeAppId }) expect(proposeSafeMessageSpy).toHaveBeenCalledWith('5', zeroPadValue('0x0789', 20), { message, @@ -203,7 +158,7 @@ describe('safeMsgSender', () => { const safeAppId = 1 try { - await dispatchSafeMsgProposal({ onboard: mockOnboard, safe, message, safeAppId }) + await dispatchSafeMsgProposal({ provider: MockEip1193Provider, safe, message, safeAppId }) } catch (e) { expect((e as Error).message).toBe('Example error') @@ -237,7 +192,7 @@ describe('safeMsgSender', () => { } as unknown as gateway.SafeInfo const message = 'Hello world' - await dispatchSafeMsgConfirmation({ onboard: mockOnboard, safe, message }) + await dispatchSafeMsgConfirmation({ provider: MockEip1193Provider, safe, message }) expect(confirmSafeMessageSpy).toHaveBeenCalledWith('5', '0x0123', { signature: mockValidSignature, @@ -264,7 +219,7 @@ describe('safeMsgSender', () => { const message = 'Hello world' try { - await dispatchSafeMsgConfirmation({ onboard: mockOnboard, safe, message }) + await dispatchSafeMsgConfirmation({ provider: MockEip1193Provider, safe, message }) } catch (e) { expect((e as Error).message).toBe('Example error') diff --git a/src/services/safe-messages/safeMsgSender.ts b/src/services/safe-messages/safeMsgSender.ts index 8e00d1b9d9..5cedd7a468 100644 --- a/src/services/safe-messages/safeMsgSender.ts +++ b/src/services/safe-messages/safeMsgSender.ts @@ -1,6 +1,6 @@ import { proposeSafeMessage, confirmSafeMessage } from '@safe-global/safe-gateway-typescript-sdk' import type { SafeInfo, SafeMessage } from '@safe-global/safe-gateway-typescript-sdk' -import type { OnboardAPI } from '@web3-onboard/core' +import type { Eip1193Provider } from 'ethers' import { safeMsgDispatch, SafeMsgEvent } from './safeMsgEvents' import { generateSafeMessageHash, isEIP712TypedData, tryOffChainMsgSigning } from '@/utils/safe-messages' @@ -9,12 +9,12 @@ import { getAssertedChainSigner } from '@/services/tx/tx-sender/sdk' import { asError } from '../exceptions/utils' export const dispatchSafeMsgProposal = async ({ - onboard, + provider, safe, message, safeAppId, }: { - onboard: OnboardAPI + provider: Eip1193Provider safe: SafeInfo message: SafeMessage['message'] safeAppId?: number @@ -22,7 +22,7 @@ export const dispatchSafeMsgProposal = async ({ const messageHash = generateSafeMessageHash(safe, message) try { - const signer = await getAssertedChainSigner(onboard, safe.chainId) + const signer = await getAssertedChainSigner(provider) const signature = await tryOffChainMsgSigning(signer, safe, message) let normalizedMessage = message @@ -50,18 +50,18 @@ export const dispatchSafeMsgProposal = async ({ } export const dispatchSafeMsgConfirmation = async ({ - onboard, + provider, safe, message, }: { - onboard: OnboardAPI + provider: Eip1193Provider safe: SafeInfo message: SafeMessage['message'] }): Promise => { const messageHash = generateSafeMessageHash(safe, message) try { - const signer = await getAssertedChainSigner(onboard, safe.chainId) + const signer = await getAssertedChainSigner(provider) const signature = await tryOffChainMsgSigning(signer, safe, message) await confirmSafeMessage(safe.chainId, messageHash, { 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 dd5149468e..f961694bd6 100644 --- a/src/services/tx/tx-sender/__tests__/ts-sender.test.ts +++ b/src/services/tx/tx-sender/__tests__/ts-sender.test.ts @@ -33,7 +33,6 @@ const setupFetchStub = (data: any) => (_url: string) => { ok: true, }) } -import type { OnboardAPI, WalletState, AppState } from '@web3-onboard/core' import { toBeHex } from 'ethers' import { generatePreValidatedSignature } from '@safe-global/protocol-kit/dist/src/utils/signatures' import { createMockSafeTransaction } from '@/tests/transactions' @@ -67,47 +66,6 @@ jest.mock('../../proposeTransaction', () => ({ default: jest.fn(() => Promise.resolve({ txId: '123' })), })) -const mockOnboardState = { - chains: [], - walletModules: [], - wallets: [ - { - label: 'Wallet 1', - icon: '', - provider: MockEip1193Provider, - chains: [{ id: '0x5' }], - accounts: [ - { - address: SIGNER_ADDRESS, - ens: null, - balance: null, - }, - ], - }, - ] as WalletState[], - accountCenter: { - enabled: true, - }, -} as unknown as AppState - -const mockOnboard = { - connectWallet: jest.fn(), - disconnectWallet: jest.fn(), - setChain: jest.fn(), - state: { - select: (key: keyof AppState) => ({ - subscribe: (next: any) => { - next(mockOnboardState[key]) - - return { - unsubscribe: jest.fn(), - } - }, - }), - get: () => mockOnboardState, - }, -} as unknown as OnboardAPI - // Mock Safe SDK const mockSafeSDK = { createTransaction: jest.fn(() => ({ @@ -323,7 +281,7 @@ describe('txSender', () => { nonce: 1, }) - const signedTx = await dispatchTxSigning(tx, '1.3.0', mockOnboard, '5', '0x345') + const signedTx = await dispatchTxSigning(tx, '1.3.0', MockEip1193Provider, '0x345') expect(mockSafeSDK.createTransaction).toHaveBeenCalled() @@ -344,7 +302,7 @@ describe('txSender', () => { nonce: 1, }) - const signedTx = await dispatchTxSigning(tx, '1.0.0', mockOnboard, '5', '0x345') + const signedTx = await dispatchTxSigning(tx, '1.0.0', MockEip1193Provider, '0x345') expect(mockSafeSDK.createTransaction).toHaveBeenCalledTimes(1) @@ -365,7 +323,7 @@ describe('txSender', () => { nonce: 1, }) - const signedTx = await dispatchTxSigning(tx, null, mockOnboard, '5', '0x345') + const signedTx = await dispatchTxSigning(tx, null, MockEip1193Provider, '0x345') expect(mockSafeSDK.createTransaction).toHaveBeenCalledTimes(1) @@ -388,7 +346,7 @@ describe('txSender', () => { nonce: 1, }) - const signedTx = await dispatchTxSigning(tx, '1.3.0', mockOnboard, '5', '0x345') + const signedTx = await dispatchTxSigning(tx, '1.3.0', MockEip1193Provider, '0x345') expect(mockSafeSDK.createTransaction).toHaveBeenCalledTimes(1) @@ -414,7 +372,7 @@ describe('txSender', () => { let signedTx try { - signedTx = await dispatchTxSigning(tx, '1.3.0', mockOnboard, '5', '0x345') + signedTx = await dispatchTxSigning(tx, '1.3.0', MockEip1193Provider, '0x345') } catch (error) { expect(mockSafeSDK.createTransaction).toHaveBeenCalledTimes(1) @@ -448,7 +406,7 @@ describe('txSender', () => { let signedTx try { - signedTx = await dispatchTxSigning(tx, '1.3.0', mockOnboard, '5', '0x345') + signedTx = await dispatchTxSigning(tx, '1.3.0', MockEip1193Provider, '0x345') } catch (error) { expect(mockSafeSDK.createTransaction).toHaveBeenCalledTimes(1) @@ -484,7 +442,7 @@ describe('txSender', () => { nonce: 1, }) - await dispatchTxExecution(safeTx, { nonce: 1 }, txId, mockOnboard, '5', safeAddress) + await dispatchTxExecution(safeTx, { nonce: 1 }, txId, MockEip1193Provider, SIGNER_ADDRESS, safeAddress) expect(mockSafeSDK.executeTransaction).toHaveBeenCalled() expect(txEvents.txDispatch).toHaveBeenCalledWith('EXECUTING', { txId }) @@ -512,7 +470,9 @@ describe('txSender', () => { nonce: 1, }) - await expect(dispatchTxExecution(safeTx, {}, txId, mockOnboard, '5', safeAddress)).rejects.toThrow('error') + await expect(dispatchTxExecution(safeTx, {}, txId, MockEip1193Provider, '5', safeAddress)).rejects.toThrow( + 'error', + ) expect(mockSafeSDK.executeTransaction).toHaveBeenCalled() expect(txEvents.txDispatch).toHaveBeenCalledWith('FAILED', { txId, error: new Error('error') }) @@ -531,7 +491,7 @@ describe('txSender', () => { nonce: 1, }) - await dispatchTxExecution(safeTx, { nonce: 1 }, txId, mockOnboard, '5', '0x123') + await dispatchTxExecution(safeTx, { nonce: 1 }, txId, MockEip1193Provider, SIGNER_ADDRESS, '0x123') expect(mockSafeSDK.executeTransaction).toHaveBeenCalled() expect(txEvents.txDispatch).toHaveBeenCalledWith('EXECUTING', { txId }) diff --git a/src/services/tx/tx-sender/dispatch.ts b/src/services/tx/tx-sender/dispatch.ts index 3f9038aa7b..76d6510a1d 100644 --- a/src/services/tx/tx-sender/dispatch.ts +++ b/src/services/tx/tx-sender/dispatch.ts @@ -4,21 +4,14 @@ import { didRevert } from '@/utils/ethers-utils' import type { MultiSendCallOnlyEthersContract } from '@safe-global/protocol-kit' import { type SpendingLimitTxParams } from '@/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx' import { getSpendingLimitContract } from '@/services/contracts/spendingLimitContracts' -import type { ContractTransactionResponse, Overrides, TransactionResponse } from 'ethers' +import type { ContractTransactionResponse, Eip1193Provider, Overrides, TransactionResponse } from 'ethers' import type { RequestId } from '@safe-global/safe-apps-sdk' import proposeTx from '../proposeTransaction' import { txDispatch, TxEvent } from '../txEvents' import { waitForRelayedTx, waitForTx } from '@/services/tx/txMonitor' import { getReadOnlyCurrentGnosisSafeContract } from '@/services/contracts/safeContracts' -import { - getAndValidateSafeSDK, - getSafeSDKWithSigner, - getUncheckedSafeSDK, - assertWalletChain, - tryOffChainTxSigning, -} from './sdk' +import { getAndValidateSafeSDK, getSafeSDKWithSigner, getUncheckedSafeSDK, tryOffChainTxSigning } from './sdk' import { createWeb3, getUserNonce, getWeb3ReadOnly } from '@/hooks/wallets/web3' -import { type OnboardAPI } from '@web3-onboard/core' import { asError } from '@/services/exceptions/utils' import chains from '@/config/chains' import { LATEST_SAFE_VERSION } from '@/config/constants' @@ -76,11 +69,10 @@ export const dispatchTxProposal = async ({ export const dispatchTxSigning = async ( safeTx: SafeTransaction, safeVersion: SafeInfo['version'], - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, txId?: string, ): Promise => { - const sdk = await getSafeSDKWithSigner(onboard, chainId) + const sdk = await getSafeSDKWithSigner(provider) let signedTx: SafeTransaction | undefined try { @@ -106,10 +98,10 @@ const ZK_SYNC_ON_CHAIN_SIGNATURE_GAS_LIMIT = 4_500_000 export const dispatchOnChainSigning = async ( safeTx: SafeTransaction, txId: string, - onboard: OnboardAPI, + provider: Eip1193Provider, chainId: SafeInfo['chainId'], ) => { - const sdkUnchecked = await getUncheckedSafeSDK(onboard, chainId) + const sdkUnchecked = await getUncheckedSafeSDK(provider) const safeTxHash = await sdkUnchecked.getTransactionHash(safeTx) const eventParams = { txId } @@ -135,13 +127,13 @@ export const dispatchOnChainSigning = async ( export const dispatchSafeTxSpeedUp = async ( txOptions: Omit & { nonce: number }, txId: string, - onboard: OnboardAPI, + provider: Eip1193Provider, chainId: SafeInfo['chainId'], + signerAddress: string, safeAddress: string, ) => { - const sdkUnchecked = await getUncheckedSafeSDK(onboard, chainId) + const sdkUnchecked = await getUncheckedSafeSDK(provider) const eventParams = { txId } - const wallet = await assertWalletChain(onboard, chainId) const signerNonce = txOptions.nonce // Execute the tx @@ -158,17 +150,17 @@ export const dispatchSafeTxSpeedUp = async ( txDispatch(TxEvent.PROCESSING, { ...eventParams, txHash: result.hash, - signerAddress: wallet.address, + signerAddress, signerNonce, gasLimit: txOptions.gasLimit, txType: 'SafeTx', }) - const provider = getWeb3ReadOnly() + const readOnlyProvider = getWeb3ReadOnly() - if (provider) { + if (readOnlyProvider) { // don't await as we don't want to block - waitForTx(provider, [txId], result.hash, safeAddress, wallet.address, signerNonce) + waitForTx(readOnlyProvider, [txId], result.hash, safeAddress, signerAddress, signerNonce) } return result.hash @@ -179,15 +171,14 @@ export const dispatchCustomTxSpeedUp = async ( txId: string, to: string, data: string, - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, + signerAddress: string, safeAddress: string, ) => { const eventParams = { txId } - const wallet = await assertWalletChain(onboard, chainId) const signerNonce = txOptions.nonce - const web3Provider = createWeb3(wallet.provider) - const signer = await web3Provider.getSigner() + const browserProvider = createWeb3(provider) + const signer = await browserProvider.getSigner() // Execute the tx let result: TransactionResponse | undefined @@ -201,7 +192,7 @@ export const dispatchCustomTxSpeedUp = async ( txDispatch(TxEvent.PROCESSING, { txHash: result.hash, - signerAddress: wallet.address, + signerAddress, signerNonce, data, to, @@ -209,11 +200,11 @@ export const dispatchCustomTxSpeedUp = async ( txType: 'Custom', }) - const provider = getWeb3ReadOnly() + const readOnlyProvider = getWeb3ReadOnly() - if (provider) { + if (readOnlyProvider) { // don't await as we don't want to block - waitForTx(provider, [txId], result.hash, safeAddress, wallet.address, signerNonce) + waitForTx(readOnlyProvider, [txId], result.hash, safeAddress, signerAddress, signerNonce) } return result.hash @@ -226,15 +217,14 @@ export const dispatchTxExecution = async ( safeTx: SafeTransaction, txOptions: TransactionOptions, txId: string, - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, + signerAddress: string, safeAddress: string, ): Promise => { - const sdkUnchecked = await getUncheckedSafeSDK(onboard, chainId) + const sdkUnchecked = await getUncheckedSafeSDK(provider) const eventParams = { txId } - const wallet = await assertWalletChain(onboard, chainId) - const signerNonce = txOptions.nonce ?? (await getUserNonce(wallet.address)) + const signerNonce = txOptions.nonce ?? (await getUserNonce(signerAddress)) // Execute the tx let result: TransactionResult | undefined @@ -249,18 +239,18 @@ export const dispatchTxExecution = async ( txDispatch(TxEvent.PROCESSING, { ...eventParams, txHash: result.hash, - signerAddress: wallet.address, + signerAddress, signerNonce, gasLimit: txOptions.gasLimit, txType: 'SafeTx', }) - const provider = getWeb3ReadOnly() + const readOnlyProvider = getWeb3ReadOnly() // Asynchronously watch the tx to be mined/validated - if (provider) { + if (readOnlyProvider) { // don't await as we don't want to block - waitForTx(provider, [txId], result.hash, safeAddress, wallet.address, signerNonce) + waitForTx(readOnlyProvider, [txId], result.hash, safeAddress, signerAddress, signerNonce) } return result.hash @@ -270,8 +260,8 @@ export const dispatchBatchExecution = async ( txs: TransactionDetails[], multiSendContract: MultiSendCallOnlyEthersContract, multiSendTxData: string, - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, + signerAddress: string, safeAddress: string, overrides: Omit & { nonce: number }, ) => { @@ -279,18 +269,17 @@ export const dispatchBatchExecution = async ( let result: ContractTransactionResponse | undefined const txIds = txs.map((tx) => tx.txId) - let signerAddress: string | undefined = undefined let signerNonce = overrides.nonce let txData = multiSendContract.encode('multiSend', [multiSendTxData]) - const wallet = await assertWalletChain(onboard, chainId) try { - signerAddress = wallet.address if (signerNonce === undefined || signerNonce === null) { signerNonce = await getUserNonce(signerAddress) } - const provider = createWeb3(wallet.provider) - result = await multiSendContract.contract.connect(await provider.getSigner()).multiSend(multiSendTxData, overrides) + const browserProvider = createWeb3(provider) + result = await multiSendContract.contract + .connect(await browserProvider.getSigner()) + .multiSend(multiSendTxData, overrides) txIds.forEach((txId) => { txDispatch(TxEvent.EXECUTING, { txId, groupKey }) @@ -309,18 +298,18 @@ export const dispatchBatchExecution = async ( txHash: result!.hash, groupKey, signerNonce, - signerAddress: wallet.address, + signerAddress, txType: 'Custom', data: txData, to: txTo, }) }) - const provider = getWeb3ReadOnly() + const readOnlyProvider = getWeb3ReadOnly() - if (provider) { + if (readOnlyProvider) { // don't await as we don't want to block - waitForTx(provider, txIds, result.hash, safeAddress, signerAddress, signerNonce) + waitForTx(readOnlyProvider, txIds, result.hash, safeAddress, signerAddress, signerNonce) } return result!.hash @@ -329,7 +318,7 @@ export const dispatchBatchExecution = async ( export const dispatchSpendingLimitTxExecution = async ( txParams: SpendingLimitTxParams, txOptions: TransactionOptions, - onboard: OnboardAPI, + provider: Eip1193Provider, chainId: SafeInfo['chainId'], safeAddress: string, ) => { @@ -337,9 +326,8 @@ export const dispatchSpendingLimitTxExecution = async ( let result: ContractTransactionResponse | undefined try { - const wallet = await assertWalletChain(onboard, chainId) - const provider = createWeb3(wallet.provider) - const contract = getSpendingLimitContract(chainId, await provider.getSigner()) + const browserProvider = createWeb3(provider) + const contract = getSpendingLimitContract(chainId, await browserProvider.getSigner()) result = await contract.executeAllowanceTransfer( txParams.safeAddress, @@ -387,11 +375,10 @@ export const dispatchSpendingLimitTxExecution = async ( export const dispatchSafeAppsTx = async ( safeTx: SafeTransaction, safeAppRequestId: RequestId, - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], + provider: Eip1193Provider, txId?: string, ): Promise => { - const sdk = await getSafeSDKWithSigner(onboard, chainId) + const sdk = await getSafeSDKWithSigner(provider) const safeTxHash = await sdk.getTransactionHash(safeTx) txDispatch(TxEvent.SAFE_APPS_REQUEST, { safeAppRequestId, safeTxHash, txId }) return safeTxHash diff --git a/src/services/tx/tx-sender/sdk.ts b/src/services/tx/tx-sender/sdk.ts index 44ac9ee94f..df3f5c339b 100644 --- a/src/services/tx/tx-sender/sdk.ts +++ b/src/services/tx/tx-sender/sdk.ts @@ -1,7 +1,7 @@ import { getSafeSDK } from '@/hooks/coreSDK/safeCoreSDK' import type Safe from '@safe-global/protocol-kit' import { EthersAdapter, SigningMethod } from '@safe-global/protocol-kit' -import type { JsonRpcSigner } from 'ethers' +import type { Eip1193Provider, JsonRpcSigner } from 'ethers' import { ethers } from 'ethers' import { isWalletRejection, isHardwareWallet, isWalletConnect } from '@/utils/wallets' import { OperationType, type SafeTransaction } from '@safe-global/safe-core-sdk-types' @@ -117,13 +117,9 @@ export const assertWalletChain = async (onboard: OnboardAPI, chainId: string): P return newWallet } -export const getAssertedChainSigner = async ( - onboard: OnboardAPI, - chainId: SafeInfo['chainId'], -): Promise => { - const wallet = await assertWalletChain(onboard, chainId) - const provider = createWeb3(wallet.provider) - return provider.getSigner() +export const getAssertedChainSigner = async (provider: Eip1193Provider): Promise => { + const browserProvider = createWeb3(provider) + return browserProvider.getSigner() } /** @@ -132,8 +128,9 @@ export const getAssertedChainSigner = async ( * most of the values of transactionResponse which is needed when * dealing with smart-contract wallet owners */ -export const getUncheckedSafeSDK = async (onboard: OnboardAPI, chainId: SafeInfo['chainId']): Promise => { - const signer = await getAssertedChainSigner(onboard, chainId) +export const getUncheckedSafeSDK = async (provider: Eip1193Provider): Promise => { + const browserProvider = createWeb3(provider) + const signer = await browserProvider.getSigner() const uncheckedJsonRpcSigner = new UncheckedJsonRpcSigner(signer.provider, await signer.getAddress()) const sdk = getAndValidateSafeSDK() @@ -145,8 +142,9 @@ export const getUncheckedSafeSDK = async (onboard: OnboardAPI, chainId: SafeInfo return sdk.connect({ ethAdapter }) } -export const getSafeSDKWithSigner = async (onboard: OnboardAPI, chainId: SafeInfo['chainId']): Promise => { - const signer = await getAssertedChainSigner(onboard, chainId) +export const getSafeSDKWithSigner = async (provider: Eip1193Provider): Promise => { + const browserProvider = createWeb3(provider) + const signer = await browserProvider.getSigner() const sdk = getAndValidateSafeSDK() const ethAdapter = new EthersAdapter({ diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 0c90822e51..a66781819d 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -2,6 +2,7 @@ import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' import type { OnboardAPI } from '@web3-onboard/core' +import type { Eip1193Provider } from 'ethers' export function invariant(condition: T, error: string): asserts condition { if (condition) { @@ -22,3 +23,7 @@ export function assertWallet(wallet: ConnectedWallet | null): asserts wallet { export function assertOnboard(onboard: OnboardAPI | undefined): asserts onboard { return invariant(onboard, 'Onboard not connected') } + +export function assertProvider(provider: Eip1193Provider | undefined): asserts provider { + return invariant(provider, 'Provider not found') +} From e787a81a54939514a27cd649604bbcf6ece98ec2 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Tue, 4 Jun 2024 09:27:26 +0200 Subject: [PATCH 044/154] feat: slight UI improvements to the new tx flow (#3767) Added a swap tokens button there and slightly updated the visual presentation according to the figma: https://www.figma.com/design/VyA38zUPbJ2zflzCIYR6Nu/Swap?node-id=6588-48609&t=LhvUJl76y7PuTnIS-4 --- src/components/tx-flow/common/TxButton.tsx | 40 +++++++++++++++++++- src/components/tx-flow/flows/NewTx/index.tsx | 14 +++---- src/services/analytics/events/modals.ts | 4 ++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/components/tx-flow/common/TxButton.tsx b/src/components/tx-flow/common/TxButton.tsx index 265b1a0e7d..5071070a72 100644 --- a/src/components/tx-flow/common/TxButton.tsx +++ b/src/components/tx-flow/common/TxButton.tsx @@ -10,6 +10,8 @@ import { useContext } from 'react' import { TxModalContext } from '..' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' +import SwapIcon from '@/public/images/common/swap.svg' +import AssetsIcon from '@/public/images/sidebar/assets.svg' const buttonSx = { height: '58px', @@ -19,7 +21,14 @@ const buttonSx = { export const SendTokensButton = ({ onClick, sx }: { onClick: () => void; sx?: ButtonProps['sx'] }) => { return ( - @@ -60,10 +69,37 @@ export const TxBuilderButton = () => { return ( - ) } + +export const MakeASwapButton = () => { + const router = useRouter() + const { setTxFlow } = useContext(TxModalContext) + const isEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) + + if (!isEnabled) return null + + const isSwapPage = router.pathname === AppRoutes.swap + const onClick = isSwapPage ? () => setTxFlow(undefined) : undefined + + return ( + + + + + + ) +} diff --git a/src/components/tx-flow/flows/NewTx/index.tsx b/src/components/tx-flow/flows/NewTx/index.tsx index c50d0f77b2..b480243925 100644 --- a/src/components/tx-flow/flows/NewTx/index.tsx +++ b/src/components/tx-flow/flows/NewTx/index.tsx @@ -1,9 +1,8 @@ import { useCallback, useContext } from 'react' -import { SendNFTsButton, SendTokensButton, TxBuilderButton } from '@/components/tx-flow/common/TxButton' -import { Container, Grid, Paper, SvgIcon, Typography } from '@mui/material' +import { MakeASwapButton, SendTokensButton, TxBuilderButton } from '@/components/tx-flow/common/TxButton' +import { Container, Grid, Paper, Typography } from '@mui/material' import { TxModalContext } from '../../' import TokenTransferFlow from '../TokenTransfer' -import AssetsIcon from '@/public/images/sidebar/assets.svg' import { useTxBuilderApp } from '@/hooks/safe-apps/useTxBuilderApp' import { ProgressBar } from '@/components/common/ProgressBar' import ChainIndicator from '@/components/common/ChainIndicator' @@ -44,19 +43,16 @@ const NewTxFlow = () => { - - Assets + Manage assets - - + {txBuilder?.app && ( <> - {txBuilder.app.name} Contract - interaction + Interact with contracts diff --git a/src/services/analytics/events/modals.ts b/src/services/analytics/events/modals.ts index b8fd8df76b..e90fb9d334 100644 --- a/src/services/analytics/events/modals.ts +++ b/src/services/analytics/events/modals.ts @@ -63,6 +63,10 @@ export const MODALS_EVENTS = { category: MODALS_CATEGORY, event: EventType.CLICK, }, + SWAP: { + action: 'Swap', + category: MODALS_CATEGORY, + }, } export enum MODAL_NAVIGATION { From ac90a1f4a045c38631d4659ba27b5b84091b4c3e Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Tue, 4 Jun 2024 11:24:38 +0200 Subject: [PATCH 045/154] fix: use zero swap amount from assets table (#3761) --- src/components/balances/AssetsTable/index.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/balances/AssetsTable/index.tsx b/src/components/balances/AssetsTable/index.tsx index 2948b1fc9d..529b063103 100644 --- a/src/components/balances/AssetsTable/index.tsx +++ b/src/components/balances/AssetsTable/index.tsx @@ -2,7 +2,6 @@ import CheckBalance from '@/features/counterfactual/CheckBalance' import { useHasFeature } from '@/hooks/useChains' import ArrowIconNW from '@/public/images/common/arrow-top-right.svg' import { FEATURES } from '@/utils/chains' -import { formatUnits } from 'ethers' import { type ReactElement, useMemo, useContext } from 'react' import { Button, Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material' import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' @@ -228,12 +227,7 @@ const AssetsTable = ({ <> onSendClick(item.tokenInfo.address)} /> - {isSwapFeatureEnabled && ( - - )} + {isSwapFeatureEnabled && } {showHiddenAssets ? ( toggleAsset(item.tokenInfo.address)} /> From f3843eb8c6b03ce6f301e4a3b8846a79106258a3 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Tue, 4 Jun 2024 11:29:19 +0200 Subject: [PATCH 046/154] fix: switch between swap & limit loses token (#3791) We were re-rendering the widget when teh user switches between swap and a limit swap and this was causing the sellToken to reset. --- next-env.d.ts | 1 - src/features/swap/index.tsx | 26 +++++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index fd36f9494e..4f11a03dc6 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 4bf4acc661..e420263099 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -55,6 +55,14 @@ const SwapWidget = ({ sell }: Params) => { const [blockedAddress, setBlockedAddress] = useState('') const wallet = useWallet() const { isConsentAccepted, onAccept } = useSwapConsent() + // useRefs as they don't trigger re-renders + const tradeTypeRef = useRef(tradeType) + const sellTokenRef = useRef( + sell || { + asset: '', + amount: '0', + }, + ) useEffect(() => { if (isBlockedAddress(safeAddress)) { @@ -136,9 +144,14 @@ const SwapWidget = ({ sell }: Params) => { { event: CowEvents.ON_CHANGE_TRADE_PARAMS, handler: (newTradeParams) => { - const { orderType: tradeType, recipient } = newTradeParams + const { orderType: tradeType, recipient, sellToken, sellTokenAmount } = newTradeParams dispatch(setSwapParams({ tradeType })) + tradeTypeRef.current = tradeType + sellTokenRef.current = { + asset: sellToken?.symbol || '', + amount: sellTokenAmount?.units || '0', + } if (recipient && isBlockedAddress(recipient)) { setBlockedAddress(recipient) } @@ -164,13 +177,8 @@ const SwapWidget = ({ sell }: Params) => { orderExecuted: null, postOrder: null, }, - tradeType, // TradeType.SWAP or TradeType.LIMIT - sell: sell - ? sell - : { - asset: '', - amount: '0', - }, + tradeType: tradeTypeRef.current, + sell: sellTokenRef.current, images: { emptyOrders: darkMode ? BASE_URL + '/images/common/swap-empty-dark.svg' @@ -195,7 +203,7 @@ const SwapWidget = ({ sell }: Params) => { 'Any future transaction fee incurred by Cow Protocol here will contribute to a license fee that supports the Safe Community. Neither Safe Ecosystem Foundation nor Core Contributors GmbH operate the CoW Swap Widget and/or Cow Swap.', }, }) - }, [sell, palette, darkMode, tradeType, chainId]) + }, [sell, palette, darkMode, chainId]) const chain = useCurrentChain() From 59b891d6067dbf7ff6844bb21f7e53eaff31b5b9 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:37:33 +0200 Subject: [PATCH 047/154] Fix: same addresses showing same balance (#3795) --- src/components/welcome/MyAccounts/PaginatedSafeList.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/welcome/MyAccounts/PaginatedSafeList.tsx b/src/components/welcome/MyAccounts/PaginatedSafeList.tsx index a022bae193..9fdce8e302 100644 --- a/src/components/welcome/MyAccounts/PaginatedSafeList.tsx +++ b/src/components/welcome/MyAccounts/PaginatedSafeList.tsx @@ -26,7 +26,9 @@ const SafeListPage = ({ safes, onLinkClick }: SafeListPageProps) => { const [overviews] = useSafeOverviews(safes) const findOverview = (item: SafeItem) => { - return overviews?.find((overview) => sameAddress(overview.address.value, item.address)) + return overviews?.find( + (overview) => item.chainId === overview.chainId && sameAddress(overview.address.value, item.address), + ) } return ( From 37ea27394fbe766b31b4968fd7a9708b98a4407f Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Tue, 4 Jun 2024 15:43:22 +0200 Subject: [PATCH 048/154] Feat: address book indicator (#3790) --- .../common/EthHashInfo/SrcEthHashInfo/index.tsx | 15 +++++++++++++-- src/components/common/EthHashInfo/index.tsx | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx index 9decd746ad..902ad5c752 100644 --- a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx @@ -2,7 +2,8 @@ import classnames from 'classnames' import type { ReactNode, ReactElement, SyntheticEvent } from 'react' import { isAddress } from 'ethers' import { useTheme } from '@mui/material/styles' -import Box from '@mui/material/Box' +import { Box, SvgIcon, Tooltip } from '@mui/material' +import AddressBookIcon from '@/public/images/sidebar/address-book.svg' import useMediaQuery from '@mui/material/useMediaQuery' import Identicon from '../../Identicon' import CopyAddressButton from '../../CopyAddressButton' @@ -29,6 +30,7 @@ export type EthHashInfoProps = { children?: ReactNode trusted?: boolean ExplorerButtonProps?: ExplorerButtonProps + isAddressBookName?: boolean } const stopPropagation = (e: SyntheticEvent) => e.stopPropagation() @@ -50,6 +52,7 @@ const SrcEthHashInfo = ({ ExplorerButtonProps, children, trusted = true, + isAddressBookName = false, }: EthHashInfoProps): ReactElement => { const shouldPrefix = isAddress(address) const theme = useTheme() @@ -81,8 +84,16 @@ const SrcEthHashInfo = ({ {name && ( - + {name} + + {isAddressBookName && ( + + + + + + )} )} diff --git a/src/components/common/EthHashInfo/index.tsx b/src/components/common/EthHashInfo/index.tsx index 69d9fe0dad..a141d2d71b 100644 --- a/src/components/common/EthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/index.tsx @@ -26,6 +26,7 @@ const EthHashInfo = ({ copyPrefix={settings.shortName.copy} {...props} name={name} + isAddressBookName={!!addressBookName} customAvatar={props.customAvatar} ExplorerButtonProps={{ title: link?.title || '', href: link?.href || '' }} avatarSize={avatarSize} From c8f78c12c874bb720e7a2e7e199fec4321800d26 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Tue, 4 Jun 2024 15:43:38 +0200 Subject: [PATCH 049/154] Fix: improve chain switch redirect (#3769) --- src/components/common/NetworkSelector/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index 960ddc4e79..296dabd33b 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -15,6 +15,7 @@ import { type ReactElement, useMemo } from 'react' import { useCallback } from 'react' import { AppRoutes } from '@/config/routes' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' +import useWallet from '@/hooks/wallets/useWallet' const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => { const isDarkMode = useDarkMode() @@ -22,6 +23,7 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => const { configs } = useChains() const chainId = useChainId() const router = useRouter() + const isWalletConnected = !!useWallet() const [testNets, prodNets] = useMemo(() => partition(configs, (config) => config.isTestnet), [configs]) @@ -30,7 +32,11 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => const shouldKeepPath = !router.query.safe const route = { - pathname: shouldKeepPath ? router.pathname : AppRoutes.index, + pathname: shouldKeepPath + ? router.pathname + : isWalletConnected + ? AppRoutes.welcome.accounts + : AppRoutes.welcome.index, query: { chain: shortName, } as { @@ -45,7 +51,7 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => return route }, - [router], + [router, isWalletConnected], ) const onChange = (event: SelectChangeEvent) => { From 4b83813eeec46e569ee4a96306dea64ca72efc73 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Tue, 4 Jun 2024 18:59:45 +0200 Subject: [PATCH 050/154] Add regression cf tests (#3797) --- .../sendfunds_connected_wallet.cy.js | 4 +- cypress/e2e/pages/create_wallet.pages.js | 53 +++++- cypress/e2e/pages/main.page.js | 2 +- cypress/e2e/regression/create_safe_cf.cy.js | 152 ++++++++++++++++++ cypress/e2e/regression/spending_limits.cy.js | 2 +- .../regression/spending_limits_nonowner.cy.js | 2 +- cypress/e2e/smoke/spending_limits.cy.js | 2 +- cypress/fixtures/safes/static.json | 3 +- cypress/fixtures/txhistory_data_data.json | 4 +- cypress/support/constants.js | 4 +- cypress/support/localstorage_data.js | 18 +++ src/components/common/AddFunds/index.tsx | 2 +- src/components/common/ChoiceButton/index.tsx | 2 +- src/components/dashboard/FirstSteps/index.tsx | 14 +- .../create/steps/ReviewStep/index.test.tsx | 17 +- .../settings/PushNotifications/index.tsx | 1 + .../__tests__/SignOrExecute.test.tsx | 10 ++ .../counterfactual/ActivateAccountButton.tsx | 8 +- .../counterfactual/ActivateAccountFlow.tsx | 8 +- src/features/counterfactual/CheckBalance.tsx | 7 +- 20 files changed, 296 insertions(+), 19 deletions(-) create mode 100644 cypress/e2e/regression/create_safe_cf.cy.js diff --git a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js index c465ead4b1..108d09c1dc 100644 --- a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js +++ b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js @@ -13,8 +13,8 @@ import { createSafes } from '../../support/api/utils_protocolkit' import { contracts, abi_qtrust, abi_nft_pc2 } from '../../support/api/contracts' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' -const safeBalanceEth = 305220000000000000n -const qtrustBanance = 95000000000000000025n +const safeBalanceEth = 305230000000000000n +const qtrustBanance = 99000000000000000025n const transferAmount = '1' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index 342330fa96..b8a0ed5b57 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -29,6 +29,16 @@ const cancelBtn = '[data-testid="cancel-btn"]' const dialogConfirmBtn = '[data-testid="dialog-confirm-btn"]' const safeActivationSection = '[data-testid="activation-section"]' const addressAutocompleteOptions = '[data-testid="address-item"]' +export const qrCode = '[data-testid="qr-code"]' +export const addressInfo = '[data-testid="address-info"]' +const choiceBtn = '[data-testid="choice-btn"]' +const addFundsBtn = '[data-testid="add-funds-btn"]' +const createTxBtn = '[data-testid="create-tx-btn"]' +const qrCodeSwitch = '[data-testid="qr-code-switch"]' +export const activateAccountBtn = '[data-testid="activate-account-btn"]' +const notificationsSwitch = '[data-testid="notifications-switch"]' +export const addFundsSection = '[data-testid="add-funds-section"]' +export const noTokensAlert = '[data-testid="no-tokens-alert"]' const sponsorStr = 'Your account is sponsored by Goerli' const safeCreationProcessing = 'Transaction is being executed' @@ -38,11 +48,52 @@ const policy1_2 = '1/1 policy' export const walletName = 'test1-sepolia-safe' export const defaultSepoliaPlaceholder = 'Sepolia Safe' const welcomeToSafeStr = 'Welcome to Safe' +const initialSteps = '0 of 2 steps completed' +export const addSignerStr = 'Add signer' +export const accountRecoveryStr = 'Account recovery' +export const sendTokensStr = 'Send tokens' + +export function checkNotificationsSwitchIs(status) { + cy.get(notificationsSwitch).find('input').should(`be.${status}`) +} + +export function clickOnActivateAccountBtn() { + cy.get(activateAccountBtn).click() +} + +export function clickOnQRCodeSwitch() { + cy.get(qrCodeSwitch).click() +} + +export function checkQRCodeSwitchStatus(state) { + cy.get(qrCodeSwitch).find('input').should(state) +} + +export function checkInitialStepsDisplayed() { + cy.contains(initialSteps).should('be.visible') +} + +export function clickOnAddFundsBtn() { + cy.get(addFundsBtn).click() +} + +export function clickOnCreateTxBtn() { + cy.get(createTxBtn).click() + main.verifyElementsCount(choiceBtn, 6) +} + +export function checkAllTxTypesOrder(expectedOrder) { + main.checkTextOrder(choiceBtn, expectedOrder) +} + +export function clickOnTxType(tx) { + cy.get(choiceBtn).contains(tx).click() +} export function verifyNewSafeDialogModal() { main.verifyElementsIsVisible([dialogConfirmBtn]) } -// + export function verifyCFSafeCreated() { main.verifyElementsIsVisible([sidebar.pendingActivationIcon, safeActivationSection]) } diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index b8547b1318..5e05873b53 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -273,7 +273,7 @@ export function addToLocalStorage(key, jsonValue) { export function checkTextOrder(selector, expectedTextArray) { cy.get(selector).each((element, index) => { const text = Cypress.$(element).text().trim() - expect(text).to.eq(expectedTextArray[index]) + expect(text).to.include(expectedTextArray[index]) }) } diff --git a/cypress/e2e/regression/create_safe_cf.cy.js b/cypress/e2e/regression/create_safe_cf.cy.js new file mode 100644 index 0000000000..94583c88dc --- /dev/null +++ b/cypress/e2e/regression/create_safe_cf.cy.js @@ -0,0 +1,152 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as createwallet from '../pages/create_wallet.pages' +import * as owner from '../pages/owners.pages' +import * as navigation from '../pages/navigation.page.js' +import * as ls from '../../support/localstorage_data.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as safeapps from '../pages/safeapps.pages' + +let staticSafes = [] +const txOrder = [ + 'Activate Safe now', + 'Add another signer', + 'Set up recovery', + 'Swap tokens', + 'Custom transaction', + 'Send token', +] + +describe('CF Safe regression tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.homeUrl + staticSafes.SEP_STATIC_SAFE_14) + main.acceptCookies() + }) + + it('Verify Add native assets and Create tx modals can be opened', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + owner.waitForConnectionStatus() + createwallet.clickOnAddFundsBtn() + main.verifyElementsIsVisible([createwallet.qrCode]) + navigation.clickOnModalCloseBtn() + + createwallet.clickOnCreateTxBtn() + navigation.clickOnModalCloseBtn() + }) + + it('Verify "0 out of 2 step completed" is shown in the dashboard', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + owner.waitForConnectionStatus() + createwallet.checkInitialStepsDisplayed() + }) + + it('Verify "Add native assets" button opens a modal with a QR code and the safe address', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + owner.waitForConnectionStatus() + createwallet.clickOnAddFundsBtn() + main.verifyElementsIsVisible([createwallet.qrCode, createwallet.addressInfo]) + }) + + it('Verify QR code switch status change works in "Add native assets" modal', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + owner.waitForConnectionStatus() + createwallet.clickOnAddFundsBtn() + createwallet.checkQRCodeSwitchStatus(constants.checkboxStates.checked) + createwallet.clickOnQRCodeSwitch() + createwallet.checkQRCodeSwitchStatus(constants.checkboxStates.unchecked) + }) + + it('Verify "Create new transaction" modal contains tx types in sequence', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.checkAllTxTypesOrder(txOrder) + }) + + it('Verify "Add safe now" button takes to a tx "Activate account"', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.clickOnTxType(txOrder[0]) + main.verifyElementsIsVisible([createwallet.activateAccountBtn]) + }) + + it('Verify "Add another Owner" takes to a tx Add owner', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.clickOnTxType(txOrder[1]) + main.verifyTextVisibility([createwallet.addSignerStr]) + }) + + it('Verify "Setup recovery" button takes to the "Account recovery" flow', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.clickOnTxType(txOrder[2]) + main.verifyTextVisibility([createwallet.accountRecoveryStr]) + }) + + it('Verify "Send token" takes to the tx form to send tokens', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.clickOnTxType(txOrder[5]) + main.verifyTextVisibility([createwallet.sendTokensStr]) + }) + + it('Verify "Custom transaction" takes to the tx builder app ', () => { + const iframeSelector = `iframe[id="iframe-${constants.TX_Builder_url}"]` + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__SafeApps__infoModal, + ls.appPermissions(constants.safeTestAppurl).infoModalAccepted, + ) + cy.reload() + owner.waitForConnectionStatus() + createwallet.clickOnCreateTxBtn() + createwallet.clickOnTxType(txOrder[4]) + main.getIframeBody(iframeSelector).within(() => { + cy.contains(safeapps.transactionBuilderStr) + }) + }) + + it('Verify "Notifications" in the settings are disabled', () => { + owner.waitForConnectionStatus() + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + cy.visit(constants.notificationsUrl + staticSafes.SEP_STATIC_SAFE_14) + createwallet.checkNotificationsSwitchIs(constants.enabledStates.disabled) + }) + + it('Verify in assets, that a "Add funds" block is present', () => { + owner.waitForConnectionStatus() + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_14) + main.verifyElementsIsVisible([createwallet.addFundsSection, createwallet.noTokensAlert]) + }) + + it('Verify clicking on "Activate now" button opens safe activation flow', () => { + owner.waitForConnectionStatus() + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) + cy.reload() + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_14) + createwallet.clickOnActivateAccountBtn() + main.verifyElementsIsVisible([createwallet.activateAccountBtn]) + }) +}) diff --git a/cypress/e2e/regression/spending_limits.cy.js b/cypress/e2e/regression/spending_limits.cy.js index c99b764779..ad8ea980ab 100644 --- a/cypress/e2e/regression/spending_limits.cy.js +++ b/cypress/e2e/regression/spending_limits.cy.js @@ -19,7 +19,7 @@ describe('Spending limits tests', () => { }) beforeEach(() => { - cy.visit(constants.securityUrl + staticSafes.SEP_STATIC_SAFE_8) + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_8) cy.clearLocalStorage() main.acceptCookies() owner.waitForConnectionStatus() diff --git a/cypress/e2e/regression/spending_limits_nonowner.cy.js b/cypress/e2e/regression/spending_limits_nonowner.cy.js index bd2d2dc1cc..fb2e0eaeed 100644 --- a/cypress/e2e/regression/spending_limits_nonowner.cy.js +++ b/cypress/e2e/regression/spending_limits_nonowner.cy.js @@ -12,7 +12,7 @@ describe('Spending limits non-owner tests', () => { }) beforeEach(() => { - cy.visit(constants.securityUrl + staticSafes.SEP_STATIC_SAFE_3) + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) cy.clearLocalStorage() main.acceptCookies() owner.waitForConnectionStatus() diff --git a/cypress/e2e/smoke/spending_limits.cy.js b/cypress/e2e/smoke/spending_limits.cy.js index ad815a8f39..e21de79926 100644 --- a/cypress/e2e/smoke/spending_limits.cy.js +++ b/cypress/e2e/smoke/spending_limits.cy.js @@ -12,7 +12,7 @@ describe('[SMOKE] Spending limits tests', () => { }) beforeEach(() => { - cy.visit(constants.securityUrl + staticSafes.SEP_STATIC_SAFE_8) + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_8) cy.clearLocalStorage() main.acceptCookies() owner.waitForConnectionStatus() diff --git a/cypress/fixtures/safes/static.json b/cypress/fixtures/safes/static.json index 14e88c853e..3a470ab92b 100644 --- a/cypress/fixtures/safes/static.json +++ b/cypress/fixtures/safes/static.json @@ -12,5 +12,6 @@ "SEP_STATIC_SAFE_10": "sep:0xc2F3645bfd395516d1a18CA6ad9298299d328C01", "SEP_STATIC_SAFE_11": "sep:0x10B45a24640E2170B6AA63ea3A289D723a0C9cba", "SEP_STATIC_SAFE_12": "sep:0xFFfaC243A24EecE6553f0Da278322aCF1Fb6CeF1", - "SEP_STATIC_SAFE_13": "sep:0x027bBe128174F0e5e5d22ECe9623698E01cd3970" + "SEP_STATIC_SAFE_13": "sep:0x027bBe128174F0e5e5d22ECe9623698E01cd3970", + "SEP_STATIC_SAFE_14": "sep:0xe41D568F5040FD9adeE8B64200c6B7C363C68c41" } diff --git a/cypress/fixtures/txhistory_data_data.json b/cypress/fixtures/txhistory_data_data.json index c0b15960a5..b5b2d020d9 100644 --- a/cypress/fixtures/txhistory_data_data.json +++ b/cypress/fixtures/txhistory_data_data.json @@ -130,11 +130,11 @@ } }, "deleteSpendingLimit": { - "title": "Contract interaction", + "title": "AllowanceModule", "summaryTxInfo": "deleteAllowance", "summaryTime": "11:08 AM", "description": "Delete spending limit", - "altImage": "Contract interaction", + "altImage": "AllowanceModule", "beneficiary": "Beneficiary", "beneficiaryAddress": "sep:0xC16Db0251654C0a72E91B190d81eAD367d2C6fED", "transactionHash": "0xd6e8...de8b", diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 8b883c0224..ddd4bdf3ab 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -62,7 +62,8 @@ export const getPermissionsUrl = '/get-permissions' export const appSettingsUrl = '/settings/safe-apps' export const setupUrl = '/settings/setup?safe=' export const dataSettingsUrl = '/settings/data?safe=' -export const securityUrl = '/settings/setup?safe=' +export const securityUrl = '/settings/security?safe=' +export const notificationsUrl = '/settings/notifications?safe=' export const invalidAppUrl = 'https://my-invalid-custom-app.com/manifest.json' export const validAppUrlJson = 'https://my-valid-custom-app.com/manifest.json' export const validAppUrl = 'https://my-valid-custom-app.com' @@ -235,6 +236,7 @@ export const localStorageKeys = { SAFE_v2__customSafeApps_11155111: 'SAFE_v2__customSafeApps-11155111', SAFE_v2__SafeApps__browserPermissions: 'SAFE_v2__SafeApps__browserPermissions', SAFE_v2__SafeApps__infoModal: 'SAFE_v2__SafeApps__infoModal', + SAFE_v2__undeployedSafes: 'SAFE_v2__undeployedSafes', } export const connectWalletNames = { diff --git a/cypress/support/localstorage_data.js b/cypress/support/localstorage_data.js index 7972fa2db0..a2f56569a3 100644 --- a/cypress/support/localstorage_data.js +++ b/cypress/support/localstorage_data.js @@ -656,3 +656,21 @@ export const cookies = { acceptedCookies: { necessary: true, updates: true, analytics: true }, acceptedTokenListOnboarding: true, } + +export const undeployedSafe = { + safe1: { + 11155111: { + '0xe41D568F5040FD9adeE8B64200c6B7C363C68c41': { + props: { + safeAccountConfig: { + threshold: 1, + owners: ['0xC16Db0251654C0a72E91B190d81eAD367d2C6fED'], + fallbackHandler: '0x017062a1dE2FE6b99BE3d9d37841FeD19F573804', + }, + safeDeploymentConfig: { saltNonce: '20', safeVersion: '1.3.0' }, + }, + status: { status: 'AWAITING_EXECUTION' }, + }, + }, + }, +} diff --git a/src/components/common/AddFunds/index.tsx b/src/components/common/AddFunds/index.tsx index f7c6d6dd22..b9e549f9df 100644 --- a/src/components/common/AddFunds/index.tsx +++ b/src/components/common/AddFunds/index.tsx @@ -16,7 +16,7 @@ const AddFundsCTA = () => { const qrCode = `${qrPrefix}${safeAddress}` return ( - +
    diff --git a/src/components/common/ChoiceButton/index.tsx b/src/components/common/ChoiceButton/index.tsx index 8e5dbec291..9a99c8b178 100644 --- a/src/components/common/ChoiceButton/index.tsx +++ b/src/components/common/ChoiceButton/index.tsx @@ -21,7 +21,7 @@ const ChoiceButton = ({ chip?: string }) => { return ( - + { {(isOk) => ( diff --git a/src/features/counterfactual/CheckBalance.tsx b/src/features/counterfactual/CheckBalance.tsx index 1cc5cc9018..6424ea57ee 100644 --- a/src/features/counterfactual/CheckBalance.tsx +++ b/src/features/counterfactual/CheckBalance.tsx @@ -16,7 +16,12 @@ const CheckBalance = () => { const blockExplorerLink = chain ? getBlockExplorerLink(chain, safeAddress) : undefined return ( - + Don't see your tokens? From c1f4064db76d7127b9a74daa2aa08ab5317ae264 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Wed, 5 Jun 2024 08:26:25 +0200 Subject: [PATCH 051/154] Feat: display contract names for WalletConnect txs (#3760) --- cypress/fixtures/txhistory_data_data.json | 2 +- .../safe-messages/MsgDetails/index.tsx | 19 ++++++--- .../safe-messages/MsgSummary/index.tsx | 9 +++- .../safe-messages/MsgType/index.tsx | 42 ++++++++++++++----- .../SingleTxDecoded/index.test.tsx | 4 +- .../DecodedData/SingleTxDecoded/index.tsx | 2 +- .../transactions/TxSummary/styles.module.css | 7 +++- src/config/constants.ts | 9 ---- .../__tests__/WalletConnectContext.test.tsx | 4 +- .../WalletConnectProvider/index.tsx | 10 ++--- src/pages/apps/open.tsx | 9 ---- .../analytics/__tests__/tx-tracking.test.ts | 16 +------ src/services/analytics/tx-tracking.ts | 5 ++- src/utils/gateway.ts | 5 --- 14 files changed, 73 insertions(+), 70 deletions(-) diff --git a/cypress/fixtures/txhistory_data_data.json b/cypress/fixtures/txhistory_data_data.json index b5b2d020d9..7f103076c0 100644 --- a/cypress/fixtures/txhistory_data_data.json +++ b/cypress/fixtures/txhistory_data_data.json @@ -72,7 +72,7 @@ "transactionHash": "0xa5dd...b064", "safeTxHash": "0x4dd0...b2b8", "nativeTransfer": { - "title": "native transfer", + "title": "Native transfer", "description": "Interact with (and send < 0.00001 ETH to)" } }, diff --git a/src/components/safe-messages/MsgDetails/index.tsx b/src/components/safe-messages/MsgDetails/index.tsx index 19897cb504..7a1dc3bbb7 100644 --- a/src/components/safe-messages/MsgDetails/index.tsx +++ b/src/components/safe-messages/MsgDetails/index.tsx @@ -1,11 +1,9 @@ +import { useMemo, type ReactElement } from 'react' import { Accordion, AccordionSummary, Typography, AccordionDetails, Box } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import CodeIcon from '@mui/icons-material/Code' import classNames from 'classnames' -import { SafeMessageStatus } from '@safe-global/safe-gateway-typescript-sdk' -import { useMemo } from 'react' -import type { SafeMessage } from '@safe-global/safe-gateway-typescript-sdk' -import type { ReactElement } from 'react' +import { SafeMessageStatus, type SafeMessage } from '@safe-global/safe-gateway-typescript-sdk' import { formatDateTime } from '@/utils/date' import EthHashInfo from '@/components/common/EthHashInfo' @@ -14,13 +12,14 @@ import { generateDataRowValue, TxDataRow } from '@/components/transactions/TxDet import MsgSigners from '@/components/safe-messages/MsgSigners' import useWallet from '@/hooks/wallets/useWallet' import SignMsgButton from '@/components/safe-messages/SignMsgButton' -import { generateSafeMessageMessage } from '@/utils/safe-messages' +import { generateSafeMessageMessage, isEIP712TypedData } from '@/utils/safe-messages' import txDetailsCss from '@/components/transactions/TxDetails/styles.module.css' import singleTxDecodedCss from '@/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/styles.module.css' import infoDetailsCss from '@/components/transactions/InfoDetails/styles.module.css' import { DecodedMsg } from '../DecodedMsg' import CopyButton from '@/components/common/CopyButton' +import NamedAddressInfo from '@/components/common/NamedAddressInfo' const MsgDetails = ({ msg }: { msg: SafeMessage }): ReactElement => { const wallet = useWallet() @@ -28,6 +27,7 @@ const MsgDetails = ({ msg }: { msg: SafeMessage }): ReactElement => { const safeMessage = useMemo(() => { return generateSafeMessageMessage(msg.message) }, [msg.message]) + const verifyingContract = isEIP712TypedData(msg.message) ? msg.message.domain.verifyingContract : undefined return (
    @@ -44,6 +44,15 @@ const MsgDetails = ({ msg }: { msg: SafeMessage }): ReactElement => { />
    + + {verifyingContract && ( +
    + + + +
    + )} +
    { switch (value) { @@ -28,14 +29,18 @@ const MsgSummary = ({ msg }: { msg: SafeMessage }): ReactElement => { const txStatusLabel = useSafeMessageStatus(msg) const isConfirmed = msg.status === SafeMessageStatus.CONFIRMED const isPending = useIsSafeMessagePending(msg.messageHash) + let type = '' + if (isEIP712TypedData(msg.message)) { + type = (msg.message as unknown as { primaryType: string }).primaryType + } return ( - + - Off-chain signature + {type || 'Signature'} diff --git a/src/components/safe-messages/MsgType/index.tsx b/src/components/safe-messages/MsgType/index.tsx index ce83eac81e..7b0fb197a7 100644 --- a/src/components/safe-messages/MsgType/index.tsx +++ b/src/components/safe-messages/MsgType/index.tsx @@ -1,23 +1,43 @@ -import { Box } from '@mui/material' +import { Box, SvgIcon } from '@mui/material' import type { SafeMessage } from '@safe-global/safe-gateway-typescript-sdk' - +import RequiredIcon from '@/public/images/messages/required.svg' import ImageFallback from '@/components/common/ImageFallback' - import txTypeCss from '@/components/transactions/TxType/styles.module.css' +import { isEIP712TypedData } from '@/utils/safe-messages' const FALLBACK_LOGO_URI = '/images/transactions/custom.svg' +const MAX_TRIMMED_LENGTH = 20 + +const getMessageName = (msg: SafeMessage) => { + if (msg.name != null) return msg.name + + if (isEIP712TypedData(msg.message)) { + return msg.message.domain?.name || '' + } + + const firstLine = msg.message.split('\n')[0] + let trimmed = firstLine.slice(0, MAX_TRIMMED_LENGTH) + if (trimmed.length < firstLine.length) { + trimmed += '…' + } + return trimmed +} const MsgType = ({ msg }: { msg: SafeMessage }) => { return ( - - {msg.name} + {msg.logoUri ? ( + + ) : ( + + )} + {getMessageName(msg)} ) } diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx index 1141df3261..7727ec34db 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx @@ -28,7 +28,7 @@ describe('SingleTxDecoded', () => { />, ) - expect(result.queryByText('native transfer')).not.toBeNull() + expect(result.queryByText('Native transfer')).not.toBeNull() }) it('should show unknown contract interactions', () => { @@ -52,7 +52,7 @@ describe('SingleTxDecoded', () => { />, ) - expect(result.queryByText('Unknown contract interaction')).not.toBeNull() + expect(result.queryByText('Contract interaction')).not.toBeNull() }) it('should show decoded data ', () => { diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx index 3aa6ea378c..44392beac0 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx @@ -36,7 +36,7 @@ export const SingleTxDecoded = ({ }: SingleTxDecodedProps) => { const chain = useCurrentChain() const isNativeTransfer = tx.value !== '0' && (!tx.data || isEmptyHexData(tx.data)) - const method = tx.dataDecoded?.method || (isNativeTransfer ? 'native transfer' : 'Unknown contract interaction') + const method = tx.dataDecoded?.method || (isNativeTransfer ? 'Native transfer' : 'Contract interaction') const { decimals, symbol } = chain?.nativeCurrency || {} const amount = tx.value ? formatVisualAmount(tx.value, decimals) : 0 diff --git a/src/components/transactions/TxSummary/styles.module.css b/src/components/transactions/TxSummary/styles.module.css index ef1bfa6ce8..bde2df708e 100644 --- a/src/components/transactions/TxSummary/styles.module.css +++ b/src/components/transactions/TxSummary/styles.module.css @@ -16,7 +16,7 @@ grid-template-columns: var(--grid-nonce) var(--grid-type) var(--grid-info) var(--grid-date) var(--grid-confirmations) var(--grid-status) var(--grid-actions); - grid-template-areas: 'nonce type info date confirmations status actions'; + grid-template-areas: 'nonce type info date confirmations status actions'; } .gridContainer > * { @@ -35,6 +35,11 @@ grid-template-areas: 'type info date confirmations status actions'; } +.gridContainer.message { + grid-template-columns: var(--grid-type) var(--grid-info) var(--grid-date) var(--grid-status) var(--grid-confirmations); + grid-template-areas: 'type info date status confirmations'; +} + .gridContainer.untrusted { opacity: 0.4; } diff --git a/src/config/constants.ts b/src/config/constants.ts index 3942c84de4..881d7f3643 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -65,15 +65,6 @@ export enum SafeAppsTag { ONRAMP = 'onramp', } -export const WC_APP_PROD = { - id: 111, - url: 'https://apps-portal.safe.global/wallet-connect', -} -export const WC_APP_DEV = { - id: 25, - url: 'https://safe-apps.dev.5afe.dev/wallet-connect', -} - // Help Center export const HELP_CENTER_URL = 'https://help.safe.global' export const HelpCenterArticle = { diff --git a/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx b/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx index b014cb8436..755d0dc258 100644 --- a/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx +++ b/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx @@ -410,10 +410,10 @@ describe('WalletConnectProvider', () => { 1, { method: 'fake', params: [] }, { - id: 25, + id: -1, name: 'name', description: 'description', - url: 'https://safe-apps.dev.5afe.dev/wallet-connect', + url: 'https://apps-portal.safe.global/wallet-connect', iconUrl: 'iconUrl', }, ) diff --git a/src/features/walletconnect/components/WalletConnectProvider/index.tsx b/src/features/walletconnect/components/WalletConnectProvider/index.tsx index e638c9a233..af02ada6c8 100644 --- a/src/features/walletconnect/components/WalletConnectProvider/index.tsx +++ b/src/features/walletconnect/components/WalletConnectProvider/index.tsx @@ -5,7 +5,7 @@ import { formatJsonRpcError } from '@walletconnect/jsonrpc-utils' import useSafeInfo from '@/hooks/useSafeInfo' import useSafeWalletProvider from '@/services/safe-wallet-provider/useSafeWalletProvider' import { asError } from '@/services/exceptions/utils' -import { IS_PRODUCTION, WC_APP_DEV, WC_APP_PROD } from '@/config/constants' +import { IS_PRODUCTION } from '@/config/constants' import { getPeerName, stripEip155Prefix } from '@/features/walletconnect/services/utils' import { trackRequest } from '@/features/walletconnect//services/tracking' import { wcPopupStore } from '@/features/walletconnect/components' @@ -23,8 +23,6 @@ export enum WCLoadingState { DISCONNECT = 'Disconnect', } -const WalletConnectSafeApp = IS_PRODUCTION ? WC_APP_PROD : WC_APP_DEV - const walletConnectSingleton = new WalletConnectWallet() const getWrongChainError = (dappName: string): Error => { @@ -90,9 +88,9 @@ export const WalletConnectProvider = ({ children }: { children: ReactNode }) => // Get response from Safe Wallet Provider return safeWalletProvider.request(event.id, event.params.request, { - id: WalletConnectSafeApp.id, - url: WalletConnectSafeApp.url, - name: getPeerName(session.peer) || 'Unknown dApp', + id: -1, + url: session.peer.metadata.url, + name: getPeerName(session.peer) || 'WalletConnect', description: session.peer.metadata.description, iconUrl: session.peer.metadata.icons[0], }) diff --git a/src/pages/apps/open.tsx b/src/pages/apps/open.tsx index 938804f974..4e4696363d 100644 --- a/src/pages/apps/open.tsx +++ b/src/pages/apps/open.tsx @@ -17,8 +17,6 @@ import { AppRoutes } from '@/config/routes' import { getOrigin } from '@/components/safe-apps/utils' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' -import { openWalletConnect } from '@/features/walletconnect/components' -import { isWalletConnectSafeApp } from '@/utils/gateway' const SafeApps: NextPage = () => { const chainId = useChainId() @@ -28,7 +26,6 @@ const SafeApps: NextPage = () => { const safeAppData = allSafeApps.find((app) => app.url === appUrl) const { safeApp, isLoading } = useSafeAppFromManifest(appUrl || '', chainId, safeAppData) const isSafeAppsEnabled = useHasFeature(FEATURES.SAFE_APPS) - const isWalletConnectEnabled = useHasFeature(FEATURES.NATIVE_WALLETCONNECT) const { addPermissions, getPermissions, getAllowedFeaturesList } = useBrowserPermissions() const origin = getOrigin(appUrl) @@ -58,12 +55,6 @@ const SafeApps: NextPage = () => { // appUrl is required to be present if (!isSafeAppsEnabled || !appUrl || !router.isReady) return null - if (isWalletConnectEnabled && isWalletConnectSafeApp(appUrl)) { - openWalletConnect() - goToList() - return null - } - if (isModalVisible) { return ( { expect(txType).toEqual(TX_TYPES.rejection) }) - it('should return walletconnect for walletconnect transactions', async () => { + it('should return walletconnect for transactions w/o safeAppInfo', async () => { const txType = await getMockTxType({ txInfo: { type: TransactionInfoType.CUSTOM, }, - safeAppInfo: { - url: 'https://safe-apps.dev.5afe.dev/wallet-connect', - }, + safeAppInfo: null, } as unknown) expect(txType).toEqual(TX_TYPES.walletconnect) @@ -175,14 +173,4 @@ describe('getTransactionTrackingType', () => { expect(txType).toEqual(TX_TYPES.batch) }) - - it('should return custom for unknown transactions', async () => { - const txType = await getMockTxType({ - txInfo: { - type: TransactionInfoType.CUSTOM, - }, - } as unknown) - - expect(txType).toEqual(TX_TYPES.custom) - }) }) diff --git a/src/services/analytics/tx-tracking.ts b/src/services/analytics/tx-tracking.ts index 6073c1a489..b34fdb0d62 100644 --- a/src/services/analytics/tx-tracking.ts +++ b/src/services/analytics/tx-tracking.ts @@ -1,6 +1,5 @@ import { TX_TYPES } from '@/services/analytics/events/transactions' import { getTxDetails } from '@/services/transactions' -import { isWalletConnectSafeApp } from '@/utils/gateway' import { SettingsInfoType, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import { isERC721Transfer, @@ -63,12 +62,14 @@ export const getTransactionTrackingType = async (chainId: string, txId: string): } if (details.safeAppInfo) { - return isWalletConnectSafeApp(details.safeAppInfo.url) ? TX_TYPES.walletconnect : details.safeAppInfo.url + return details.safeAppInfo.url } if (isMultiSendTxInfo(txInfo)) { return TX_TYPES.batch } + + return TX_TYPES.walletconnect } return TX_TYPES.custom diff --git a/src/utils/gateway.ts b/src/utils/gateway.ts index a6bb8e31c5..12c657f016 100644 --- a/src/utils/gateway.ts +++ b/src/utils/gateway.ts @@ -1,6 +1,5 @@ import type { JsonRpcSigner } from 'ethers' import { type ChainInfo, deleteTransaction } from '@safe-global/safe-gateway-typescript-sdk' -import { WC_APP_PROD, WC_APP_DEV } from '@/config/constants' import { signTypedData } from './web3' export const _replaceTemplate = (uri: string, data: Record): string => { @@ -30,10 +29,6 @@ export const getExplorerLink = ( return { href, title } } -export const isWalletConnectSafeApp = (url: string): boolean => { - return url === WC_APP_PROD.url || url === WC_APP_DEV.url -} - const signTxServiceMessage = async ( chainId: string, safeAddress: string, From d965ee83f128e727dfc462fe2f4d7cfcca5ea943 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Wed, 5 Jun 2024 10:28:14 +0200 Subject: [PATCH 052/154] fix: typo in feeTooltipMarkdown (#3799) --- src/features/swap/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index e420263099..3a97cdce70 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -200,7 +200,7 @@ const SwapWidget = ({ sell }: Params) => { content: { feeLabel: 'No fee for one month', feeTooltipMarkdown: - 'Any future transaction fee incurred by Cow Protocol here will contribute to a license fee that supports the Safe Community. Neither Safe Ecosystem Foundation nor Core Contributors GmbH operate the CoW Swap Widget and/or Cow Swap.', + 'Any future transaction fee incurred by CoW Protocol here will contribute to a license fee that supports the Safe Community. Neither Safe Ecosystem Foundation nor Core Contributors GmbH operate the CoW Swap Widget and/or CoW Swap.', }, }) }, [sell, palette, darkMode, chainId]) From 2ecdfefeea8ca7a8860e6898b9d2416d6662803c Mon Sep 17 00:00:00 2001 From: Jan-Felix <524089+jfschwarz@users.noreply.github.com> Date: Wed, 5 Jun 2024 14:10:14 +0200 Subject: [PATCH 053/154] feat: Execute transaction through role (#3768) --- package.json | 5 +- .../flows/SuccessScreen/StatusStepper.tsx | 2 +- .../tx-flow/flows/SuccessScreen/index.tsx | 30 +- .../__test__/PermissionsCheck.test.tsx | 269 ++++++++++++++++++ .../PermissionsCheck/hooks.ts | 264 +++++++++++++++++ .../PermissionsCheck/index.tsx | 220 ++++++++++++++ src/components/tx/SignOrExecuteForm/index.tsx | 7 + src/services/analytics/events/transactions.ts | 5 + src/services/analytics/types.ts | 1 + src/services/exceptions/ErrorCodes.ts | 1 + src/services/transactions/index.ts | 29 ++ src/services/tx/tx-sender/dispatch.ts | 55 +++- yarn.lock | 5 + 13 files changed, 879 insertions(+), 14 deletions(-) create mode 100644 src/components/tx/SignOrExecuteForm/PermissionsCheck/__test__/PermissionsCheck.test.tsx create mode 100644 src/components/tx/SignOrExecuteForm/PermissionsCheck/hooks.ts create mode 100644 src/components/tx/SignOrExecuteForm/PermissionsCheck/index.tsx diff --git a/package.json b/package.json index ea0a77454e..e38f308cc7 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,8 @@ "react-hook-form": "7.41.1", "react-papaparse": "^4.0.2", "react-redux": "^8.0.5", - "semver": "^7.5.2" + "semver": "^7.5.2", + "zodiac-roles-deployments": "^2.2.2" }, "devDependencies": { "@chromatic-com/storybook": "^1.3.1", @@ -160,4 +161,4 @@ "minimumChangeThreshold": 0, "showDetails": true } -} +} \ No newline at end of file diff --git a/src/components/tx-flow/flows/SuccessScreen/StatusStepper.tsx b/src/components/tx-flow/flows/SuccessScreen/StatusStepper.tsx index 5852deda79..6952955195 100644 --- a/src/components/tx-flow/flows/SuccessScreen/StatusStepper.tsx +++ b/src/components/tx-flow/flows/SuccessScreen/StatusStepper.tsx @@ -5,7 +5,7 @@ import StatusStep from '@/components/new-safe/create/steps/StatusStep/StatusStep import useSafeInfo from '@/hooks/useSafeInfo' import { PendingStatus } from '@/store/pendingTxsSlice' -const StatusStepper = ({ status, txHash }: { status: PendingStatus; txHash?: string }) => { +const StatusStepper = ({ status, txHash }: { status?: PendingStatus; txHash?: string }) => { const { safeAddress } = useSafeInfo() const isProcessing = status === PendingStatus.PROCESSING || status === PendingStatus.INDEXING || status === undefined diff --git a/src/components/tx-flow/flows/SuccessScreen/index.tsx b/src/components/tx-flow/flows/SuccessScreen/index.tsx index f11a348087..c853ded2f6 100644 --- a/src/components/tx-flow/flows/SuccessScreen/index.tsx +++ b/src/components/tx-flow/flows/SuccessScreen/index.tsx @@ -19,24 +19,33 @@ import useDecodeTx from '@/hooks/useDecodeTx' import { isSwapConfirmationViewOrder } from '@/utils/transaction-guards' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' -const SuccessScreen = ({ txId, safeTx }: { txId: string; safeTx?: SafeTransaction }) => { - const [localTxHash, setLocalTxHash] = useState() +interface Props { + /** The ID assigned to the transaction in the client-gateway */ + txId?: string + /** For module transaction, pass the transaction hash while the `txId` is not yet available */ + txHash?: string + /** The multisig transaction object */ + safeTx?: SafeTransaction +} + +const SuccessScreen = ({ txId, txHash, safeTx }: Props) => { + const [localTxHash, setLocalTxHash] = useState(txHash) const [error, setError] = useState() const { setTxFlow } = useContext(TxModalContext) const chain = useCurrentChain() - const pendingTx = useAppSelector((state) => selectPendingTxById(state, txId)) + const pendingTx = useAppSelector((state) => (txId ? selectPendingTxById(state, txId) : undefined)) const { safeAddress } = useSafeInfo() - const { status } = pendingTx || {} - const txHash = pendingTx && 'txHash' in pendingTx ? pendingTx.txHash : undefined - const txLink = chain && getTxLink(txId, chain, safeAddress) + const status = !txId && txHash ? PendingStatus.INDEXING : pendingTx?.status + const pendingTxHash = pendingTx && 'txHash' in pendingTx ? pendingTx.txHash : undefined + const txLink = chain && txId && getTxLink(txId, chain, safeAddress) const [decodedData] = useDecodeTx(safeTx) const isSwapOrder = isSwapConfirmationViewOrder(decodedData) useEffect(() => { - if (!txHash) return + if (!pendingTxHash) return - setLocalTxHash(txHash) - }, [txHash]) + setLocalTxHash(pendingTxHash) + }, [pendingTxHash]) useEffect(() => { const unsubFns: Array<() => void> = ([TxEvent.FAILED, TxEvent.REVERTED] as const).map((event) => @@ -59,7 +68,8 @@ const SuccessScreen = ({ txId, safeTx }: { txId: string; safeTx?: SafeTransactio switch (status) { case PendingStatus.PROCESSING: case PendingStatus.RELAYING: - StatusComponent = + // status can only have these values if txId & pendingTx are defined + StatusComponent = break case PendingStatus.INDEXING: StatusComponent = diff --git a/src/components/tx/SignOrExecuteForm/PermissionsCheck/__test__/PermissionsCheck.test.tsx b/src/components/tx/SignOrExecuteForm/PermissionsCheck/__test__/PermissionsCheck.test.tsx new file mode 100644 index 0000000000..48063d0b93 --- /dev/null +++ b/src/components/tx/SignOrExecuteForm/PermissionsCheck/__test__/PermissionsCheck.test.tsx @@ -0,0 +1,269 @@ +import { createMockSafeTransaction } from '@/tests/transactions' +import { OperationType } from '@safe-global/safe-core-sdk-types' +import { type ReactElement } from 'react' +import * as zodiacRoles from 'zodiac-roles-deployments' +import { fireEvent, render, waitFor, mockWeb3Provider } from '@/tests/test-utils' + +import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' +import * as useSafeInfoHook from '@/hooks/useSafeInfo' +import * as wallet from '@/hooks/wallets/useWallet' +import * as onboardHooks from '@/hooks/wallets/useOnboard' +import * as txSender from '@/services/tx/tx-sender/dispatch' +import { extendedSafeInfoBuilder } from '@/tests/builders/safe' +import { type OnboardAPI } from '@web3-onboard/core' +import { AbiCoder, ZeroAddress, encodeBytes32String } from 'ethers' +import PermissionsCheck from '..' +import * as hooksModule from '../hooks' + +// We assume that CheckWallet always returns true +jest.mock('@/components/common/CheckWallet', () => ({ + __esModule: true, + default({ children }: { children: (ok: boolean) => ReactElement }) { + return children(true) + }, +})) + +// mock useCurrentChain & useHasFeature +jest.mock('@/hooks/useChains', () => ({ + useCurrentChain: jest.fn(() => ({ + shortName: 'eth', + chainId: '1', + chainName: 'Ethereum', + features: [], + transactionService: 'https://tx.service.mock', + })), + useHasFeature: jest.fn(() => true), // used to check for EIP1559 support +})) + +// mock getModuleTransactionId +jest.mock('@/services/transactions', () => ({ + getModuleTransactionId: jest.fn(() => 'i1234567890'), +})) + +describe('PermissionsCheck', () => { + let executeSpy: jest.SpyInstance + let fetchRolesModMock: jest.SpyInstance + + const mockConnectedWalletAddress = (address: string) => { + // Onboard + jest.spyOn(onboardHooks, 'default').mockReturnValue({ + setChain: jest.fn(), + state: { + get: () => ({ + wallets: [ + { + label: 'MetaMask', + accounts: [{ address }], + connected: true, + chains: [{ id: '1' }], + }, + ], + }), + }, + } as unknown as OnboardAPI) + + // Wallet + jest.spyOn(wallet, 'default').mockReturnValue({ + chainId: '1', + label: 'MetaMask', + address, + } as unknown as ConnectedWallet) + } + + beforeEach(() => { + jest.clearAllMocks() + + // Safe info + jest.spyOn(useSafeInfoHook, 'default').mockImplementation(() => ({ + safe: SAFE_INFO, + safeAddress: SAFE_INFO.address.value, + safeError: undefined, + safeLoading: false, + safeLoaded: true, + })) + + // Roles mod fetching + + // Mock the Roles mod fetching function to return the test roles mod + + fetchRolesModMock = jest.spyOn(zodiacRoles, 'fetchRolesMod').mockReturnValue(Promise.resolve(TEST_ROLES_MOD as any)) + + // Mock signing and dispatching the module transaction + executeSpy = jest + .spyOn(txSender, 'dispatchModuleTxExecution') + .mockReturnValue(Promise.resolve('0xabababababababababababababababababababababababababababababababab')) // tx hash + + // Mock return value of useWeb3ReadOnly + // It's only used for eth_estimateGas requests + mockWeb3Provider([]) + + jest.spyOn(hooksModule, 'pollModuleTransactionId').mockReturnValue(Promise.resolve('i1234567890')) + }) + + it('only shows the card when the user is a member of any role', async () => { + mockConnectedWalletAddress(SAFE_INFO.owners[0].value) // connect as safe owner (not a role member) + + const safeTx = createMockSafeTransaction({ + to: ZeroAddress, + data: '0xd0e30db0', // deposit() + value: AbiCoder.defaultAbiCoder().encode(['uint256'], [123]), + operation: OperationType.Call, + }) + + const { queryByText } = render() + + // wait for the Roles mod to be fetched + await waitFor(() => { + expect(fetchRolesModMock).toBeCalled() + }) + + // the card is not shown + expect(queryByText('Execute without confirmations')).not.toBeInTheDocument() + }) + + it('disables the submit button when the call is not allowed and shows the permission check status', async () => { + mockConnectedWalletAddress(MEMBER_ADDRESS) + + const safeTx = createMockSafeTransaction({ + to: ZeroAddress, + data: '0xd0e30db0', // deposit() + value: AbiCoder.defaultAbiCoder().encode(['uint256'], [123]), + operation: OperationType.Call, + }) + + const { findByText, getByText } = render() + expect(await findByText('Execute')).toBeDisabled() + + expect( + getByText( + textContentMatcher('You are a member of the eth_wrapping role but it does not allow this transaction.'), + ), + ).toBeInTheDocument() + + expect(getByText('TargetAddressNotAllowed')).toBeInTheDocument() + }) + + it('execute the tx when the submit button is clicked', async () => { + mockConnectedWalletAddress(MEMBER_ADDRESS) + + const safeTx = createMockSafeTransaction({ + to: WETH_ADDRESS, + data: '0xd0e30db0', // deposit() + value: AbiCoder.defaultAbiCoder().encode(['uint256'], [123]), + operation: OperationType.Call, + }) + + const onSubmit = jest.fn() + + const { findByText } = render() + + fireEvent.click(await findByText('Execute')) + + await waitFor(() => { + expect(executeSpy).toHaveBeenCalledWith( + // call to the Roles mod's execTransactionWithRole function + expect.objectContaining({ + to: TEST_ROLES_MOD.address, + data: '0xc6fe8747000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b14000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000006574685f7772617070696e67000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000004d0e30db000000000000000000000000000000000000000000000000000000000', + value: '0', + }), + undefined, + expect.anything(), + ) + }) + + // calls provided onSubmit callback + await waitFor(() => { + expect(onSubmit).toHaveBeenCalled() + }) + }) +}) + +const ROLES_MOD_ADDRESS = '0x1234567890000000000000000000000000000000' +const MEMBER_ADDRESS = '0x1111111110000000000000000000000000000000' +const ROLE_KEY = encodeBytes32String('eth_wrapping') + +const SAFE_INFO = extendedSafeInfoBuilder().build() +SAFE_INFO.modules = [{ value: ROLES_MOD_ADDRESS }] +SAFE_INFO.chainId = '1' + +const lowercaseSafeAddress = SAFE_INFO.address.value.toLowerCase() + +const WETH_ADDRESS = '0xfff9976782d46cc05630d1f6ebab18b2324d6b14' + +const { Clearance, ExecutionOptions } = zodiacRoles + +const TEST_ROLES_MOD = { + address: ROLES_MOD_ADDRESS, + owner: lowercaseSafeAddress, + avatar: lowercaseSafeAddress, + target: lowercaseSafeAddress, + roles: [ + { + key: ROLE_KEY, + members: [MEMBER_ADDRESS], + targets: [ + { + address: '0xc36442b4a4522e871399cd717abdd847ab11fe88', + clearance: Clearance.Function, + executionOptions: ExecutionOptions.None, + functions: [ + { + selector: '0x49404b7c', + wildcarded: false, + executionOptions: ExecutionOptions.None, + }, + ], + }, + { + address: WETH_ADDRESS, // WETH + clearance: Clearance.Function, + executionOptions: ExecutionOptions.None, + functions: [ + { + selector: '0x2e1a7d4d', // withdraw(uint256) + wildcarded: true, + executionOptions: ExecutionOptions.None, + }, + { + selector: '0xd0e30db0', // deposit() + wildcarded: true, + executionOptions: ExecutionOptions.Send, + }, + ], + }, + ], + }, + ], +} + +/** + * Getting the deepest element that contain string / match regex even when it split between multiple elements + * + * @example + * For: + *
    + * Hello World + *
    + * + * screen.getByText('Hello World') // ❌ Fail + * screen.getByText(textContentMatcher('Hello World')) // ✅ pass + */ +function textContentMatcher(textMatch: string | RegExp) { + const hasText = + typeof textMatch === 'string' + ? (node: Element) => node.textContent === textMatch + : (node: Element) => textMatch.test(node.textContent || '') + + const matcher = (_content: string, node: Element | null) => { + if (!node || !hasText(node)) { + return false + } + + return Array.from(node?.children || []).every((child) => !hasText(child)) + } + + matcher.toString = () => `textContentMatcher(${textMatch})` + + return matcher +} diff --git a/src/components/tx/SignOrExecuteForm/PermissionsCheck/hooks.ts b/src/components/tx/SignOrExecuteForm/PermissionsCheck/hooks.ts new file mode 100644 index 0000000000..18e07f5af7 --- /dev/null +++ b/src/components/tx/SignOrExecuteForm/PermissionsCheck/hooks.ts @@ -0,0 +1,264 @@ +import useAsync from '@/hooks/useAsync' +import useSafeInfo from '@/hooks/useSafeInfo' +import { useWeb3ReadOnly } from '@/hooks/wallets/web3' +import { Errors, logError } from '@/services/exceptions' +import { getModuleTransactionId } from '@/services/transactions' +import { backOff } from 'exponential-backoff' +import { useEffect, useMemo } from 'react' +import { + type ChainId, + chains, + fetchRolesMod, + Clearance, + type RoleSummary, + ExecutionOptions, + Status, +} from 'zodiac-roles-deployments' +import { OperationType, type Transaction, type MetaTransactionData } from '@safe-global/safe-core-sdk-types' +import { type JsonRpcProvider } from 'ethers' +import { KnownContracts, getModuleInstance } from '@gnosis.pm/zodiac' +import useWallet from '@/hooks/wallets/useWallet' + +const ROLES_V2_SUPPORTED_CHAINS = Object.keys(chains) + +/** + * Returns all Zodiac Roles Modifiers v2 instances that are enabled and correctly configured on this Safe + */ +export const useRolesMods = () => { + const { safe } = useSafeInfo() + + const [data] = useAsync(async () => { + if (!ROLES_V2_SUPPORTED_CHAINS.includes(safe.chainId)) return [] + + const safeModules = safe.modules || [] + const rolesMods = await Promise.all( + safeModules.map((address) => + fetchRolesMod({ address: address.value as `0x${string}`, chainId: parseInt(safe.chainId) as ChainId }), + ), + ) + + return rolesMods.filter( + (mod): mod is Exclude => + mod !== null && + mod.target === safe.address.value.toLowerCase() && + mod.avatar === safe.address.value.toLowerCase() && + mod.roles.length > 0, + ) + }, [safe]) + + return data +} + +/** + * Returns a list of roles mod address + role key assigned to the connected wallet. + * For each role, checks if the role allows the given meta transaction and returns the status. + */ +export const useRoles = (metaTx?: MetaTransactionData) => { + const rolesMods = useRolesMods() + const wallet = useWallet() + const walletAddress = wallet?.address.toLowerCase() as undefined | `0x${string}` + + // find all roles assigned to the connected wallet, statically check if they allow the given meta transaction + const potentialRoles = useMemo(() => { + const result: { + modAddress: `0x${string}` + roleKey: `0x${string}` + status: Status | null + }[] = [] + + if (walletAddress && rolesMods) { + for (const rolesMod of rolesMods) { + for (const role of rolesMod.roles) { + if (role.members.includes(walletAddress)) { + result.push({ + modAddress: rolesMod.address, + roleKey: role.key, + status: metaTx ? checkTransaction(role, metaTx) : null, + }) + } + } + } + } + + return result + }, [rolesMods, walletAddress, metaTx]) + const web3ReadOnly = useWeb3ReadOnly() + + // if the static check is inconclusive (status: null), evaluate the condition through a test call + const [dynamicallyCheckedPotentialRoles] = useAsync( + () => + Promise.all( + potentialRoles.map(async (entry) => { + if (entry.status === null && metaTx && walletAddress && web3ReadOnly) { + entry.status = await checkCondition(entry.modAddress, entry.roleKey, metaTx, walletAddress, web3ReadOnly) + } + return entry + }), + ), + [potentialRoles, metaTx, walletAddress, web3ReadOnly], + ) + + // Return the statically checked roles while the dynamic checks are still pending + return dynamicallyCheckedPotentialRoles || potentialRoles +} + +/** + * Returns the status of the permission check, `null` if it depends on the condition evaluation. + */ +const checkTransaction = (role: RoleSummary, metaTx: MetaTransactionData): Status | null => { + const target = role.targets.find((t) => t.address === metaTx.to.toLowerCase()) + if (!target) return Status.TargetAddressNotAllowed + + if (target.clearance === Clearance.Target) { + // all calls to the target are allowed + return checkExecutionOptions(target.executionOptions, metaTx) + } + + if (target.clearance === Clearance.Function) { + // check if the function is allowed + const selector = metaTx.data.slice(0, 10) as `0x${string}` + const func = target.functions.find((f) => f.selector === selector) + if (func) { + const execOptionsStatus = checkExecutionOptions(func.executionOptions, metaTx) + if (execOptionsStatus !== Status.Ok) return execOptionsStatus + return func.wildcarded ? Status.Ok : null // wildcarded means there's no condition set + } + } + + return Status.FunctionNotAllowed +} + +const checkExecutionOptions = (execOptions: ExecutionOptions, metaTx: MetaTransactionData): Status => { + const isSend = BigInt(metaTx.value || '0') > 0n + const isDelegateCall = metaTx.operation === OperationType.DelegateCall + + if (isSend && execOptions !== ExecutionOptions.Send && execOptions !== ExecutionOptions.Both) { + return Status.SendNotAllowed + } + if (isDelegateCall && execOptions !== ExecutionOptions.DelegateCall && execOptions !== ExecutionOptions.Both) { + return Status.DelegateCallNotAllowed + } + + return Status.Ok +} + +export const useExecuteThroughRole = ({ + modAddress, + roleKey, + metaTx, +}: { + modAddress?: `0x${string}` + roleKey?: `0x${string}` + metaTx?: MetaTransactionData +}) => { + const web3ReadOnly = useWeb3ReadOnly() + const wallet = useWallet() + const walletAddress = wallet?.address.toLowerCase() as undefined | `0x${string}` + + return useMemo( + () => + modAddress && roleKey && metaTx && walletAddress && web3ReadOnly + ? encodeExecuteThroughRole(modAddress, roleKey, metaTx, walletAddress, web3ReadOnly) + : undefined, + [modAddress, roleKey, metaTx, walletAddress, web3ReadOnly], + ) +} + +const encodeExecuteThroughRole = ( + modAddress: `0x${string}`, + roleKey: `0x${string}`, + metaTx: MetaTransactionData, + from: `0x${string}`, + provider: JsonRpcProvider, +): Transaction => { + const rolesModifier = getModuleInstance(KnownContracts.ROLES_V2, modAddress, provider) + const data = rolesModifier.interface.encodeFunctionData('execTransactionWithRole', [ + metaTx.to, + BigInt(metaTx.value), + metaTx.data, + metaTx.operation || 0, + roleKey, + true, + ]) + + return { + to: modAddress, + data, + value: '0', + from, + } +} + +const checkCondition = async ( + modAddress: `0x${string}`, + roleKey: `0x${string}`, + metaTx: MetaTransactionData, + from: `0x${string}`, + provider: JsonRpcProvider, +) => { + const rolesModifier = getModuleInstance(KnownContracts.ROLES_V2, modAddress, provider) + try { + await rolesModifier.execTransactionWithRole.estimateGas( + metaTx.to, + BigInt(metaTx.value), + metaTx.data, + metaTx.operation || 0, + roleKey, + false, + { from }, + ) + + return Status.Ok + } catch (e: any) { + const error = rolesModifier.interface.getError(e.data.slice(0, 10)) + if (error === null || error.name !== 'ConditionViolation') { + console.error('Unexpected error in condition check', error, e.data, e) + return null + } + + // status is a BigInt, convert it to enum + const { status } = rolesModifier.interface.decodeErrorResult(error, e.data) + return Number(status) as Status + } +} + +export const useGasLimit = ( + tx?: Transaction, +): { + gasLimit?: bigint + gasLimitError?: Error + gasLimitLoading: boolean +} => { + const web3ReadOnly = useWeb3ReadOnly() + + const [gasLimit, gasLimitError, gasLimitLoading] = useAsync(async () => { + if (!web3ReadOnly || !tx) return + + return web3ReadOnly.estimateGas(tx) + }, [web3ReadOnly, tx]) + + useEffect(() => { + if (gasLimitError) { + logError(Errors._612, gasLimitError.message) + } + }, [gasLimitError]) + + return { gasLimit, gasLimitError, gasLimitLoading } +} + +export const pollModuleTransactionId = async (props: { + transactionService: string + safeAddress: string + txHash: string +}): Promise => { + // exponential delay between attempts for around 4 min + return backOff(() => getModuleTransactionId(props), { + startingDelay: 750, + maxDelay: 20000, + numOfAttempts: 19, + retry: (e: any) => { + console.info('waiting for transaction-service to index the module transaction', e) + return true + }, + }) +} diff --git a/src/components/tx/SignOrExecuteForm/PermissionsCheck/index.tsx b/src/components/tx/SignOrExecuteForm/PermissionsCheck/index.tsx new file mode 100644 index 0000000000..edbac6d42e --- /dev/null +++ b/src/components/tx/SignOrExecuteForm/PermissionsCheck/index.tsx @@ -0,0 +1,220 @@ +import { useContext, useState } from 'react' +import { Status } from 'zodiac-roles-deployments' +import { type SafeTransaction } from '@safe-global/safe-core-sdk-types' +import { decodeBytes32String } from 'ethers' + +import { Box, Button, CardActions, Chip, CircularProgress, Divider, Typography } from '@mui/material' + +import commonCss from '@/components/tx-flow/common/styles.module.css' +import CheckWallet from '@/components/common/CheckWallet' +import TxCard from '@/components/tx-flow/common/TxCard' +import { getTransactionTrackingType } from '@/services/analytics/tx-tracking' +import { TX_EVENTS } from '@/services/analytics/events/transactions' +import { trackEvent } from '@/services/analytics' +import useSafeInfo from '@/hooks/useSafeInfo' +import WalletRejectionError from '../WalletRejectionError' +import ErrorMessage from '../../ErrorMessage' +import useWallet from '@/hooks/wallets/useWallet' +import { type SubmitCallback } from '..' +import { getTxOptions } from '@/utils/transactions' +import { isWalletRejection } from '@/utils/wallets' +import { Errors, trackError } from '@/services/exceptions' +import { asError } from '@/services/exceptions/utils' +import { SuccessScreenFlow } from '@/components/tx-flow/flows' +import AdvancedParams, { useAdvancedParams } from '../../AdvancedParams' +import { useCurrentChain } from '@/hooks/useChains' +import { dispatchModuleTxExecution } from '@/services/tx/tx-sender' +import useOnboard from '@/hooks/wallets/useOnboard' +import { assertOnboard, assertWallet } from '@/utils/helpers' +import { TxModalContext } from '@/components/tx-flow' +import { pollModuleTransactionId, useExecuteThroughRole, useRoles, useGasLimit } from './hooks' +import { assertWalletChain } from '@/services/tx/tx-sender/sdk' + +const Role = ({ children }: { children: string }) => { + let humanReadableRoleKey = children + try { + humanReadableRoleKey = decodeBytes32String(children) + } catch (e) {} + + return +} + +const PermissionsCheck: React.FC<{ onSubmit?: SubmitCallback; safeTx: SafeTransaction; safeTxError?: Error }> = ({ + onSubmit, + safeTx, + safeTxError, +}) => { + const currentChain = useCurrentChain() + const onboard = useOnboard() + const wallet = useWallet() + const { safe } = useSafeInfo() + + const chainId = currentChain?.chainId || '1' + + const [isPending, setIsPending] = useState(false) + const [isRejectedByUser, setIsRejectedByUser] = useState(false) + const [submitError, setSubmitError] = useState() + + const { setTxFlow } = useContext(TxModalContext) + + const roles = useRoles(safeTx?.data) + const allowingRole = roles.find((role) => role.status === Status.Ok) + + // If a user has multiple roles, we should prioritize the one that allows the transaction's to address (and function selector) + const mostLikelyRole = + allowingRole || + roles.find((role) => role.status !== Status.TargetAddressNotAllowed && role.status !== Status.FunctionNotAllowed) || + roles.find((role) => role.status !== Status.TargetAddressNotAllowed) || + roles[0] + + // Wrap call routing it through the Roles mod with the allowing role + const txThroughRole = useExecuteThroughRole({ + modAddress: allowingRole?.modAddress, + roleKey: allowingRole?.roleKey, + metaTx: safeTx?.data, + }) + // Estimate gas limit + const { gasLimit, gasLimitError } = useGasLimit(txThroughRole) + const [advancedParams, setAdvancedParams] = useAdvancedParams(gasLimit) + + const handleExecute = async () => { + assertWallet(wallet) + assertOnboard(onboard) + + await assertWalletChain(onboard, chainId) + + setIsRejectedByUser(false) + setIsPending(true) + setSubmitError(undefined) + setIsRejectedByUser(false) + + if (!txThroughRole) { + throw new Error('Execution through role is not possible') + } + + const txOptions = getTxOptions(advancedParams, currentChain) + + let txHash: string + try { + txHash = await dispatchModuleTxExecution({ ...txThroughRole, ...txOptions }, wallet.provider, safe.address.value) + } catch (_err) { + const err = asError(_err) + if (isWalletRejection(err)) { + setIsRejectedByUser(true) + } else { + trackError(Errors._815, err) + setSubmitError(err) + } + setIsPending(false) + return + } + + // On success, forward to the success screen, initially without a txId + setTxFlow(, undefined, false) + + // Wait for module tx to be indexed + const transactionService = currentChain?.transactionService + if (!transactionService) { + throw new Error('Transaction service not found') + } + const moduleTxId = await pollModuleTransactionId({ + transactionService, + safeAddress: safe.address.value, + txHash, + }) + + const txId = `module_${safe.address.value}_${moduleTxId}` + + onSubmit?.(txId, true) + + // Track tx event + const txType = await getTransactionTrackingType(chainId, txId) + trackEvent({ ...TX_EVENTS.EXECUTE_THROUGH_ROLE, label: txType }) + + // Update the success screen so it shows a link to the transaction + setTxFlow(, undefined, false) + } + + // Only render the card if the connected wallet is a member of any role + if (roles.length === 0) { + return null + } + + return ( + + Execute without confirmations + + {allowingRole && ( + <> + + As a member of the {allowingRole.roleKey} you can execute this transaction immediately without + confirmations from other owners. + + + + )} + + {!allowingRole && ( + <> + + You are a member of the {mostLikelyRole.roleKey} role but it does not allow this transaction. + + + {mostLikelyRole.status && ( + + The permission check fails with the following status: +
    + {Status[mostLikelyRole.status]} +
    + )} + + )} + + {safeTxError && ( + + This transaction will most likely fail. To save gas costs, avoid confirming the transaction. + + )} + + {submitError && ( + + Error submitting the transaction. Please try again. + + )} + + {isRejectedByUser && ( + + + + )} + +
    + + + + + {(isOk) => ( + + )} + + +
    +
    + ) +} + +export default PermissionsCheck diff --git a/src/components/tx/SignOrExecuteForm/index.tsx b/src/components/tx/SignOrExecuteForm/index.tsx index e85ad42121..c5b3517f88 100644 --- a/src/components/tx/SignOrExecuteForm/index.tsx +++ b/src/components/tx/SignOrExecuteForm/index.tsx @@ -26,6 +26,7 @@ import { getTransactionTrackingType } from '@/services/analytics/tx-tracking' import { TX_EVENTS } from '@/services/analytics/events/transactions' import { trackEvent } from '@/services/analytics' import useChainId from '@/hooks/useChainId' +import PermissionsCheck from './PermissionsCheck' export type SubmitCallback = (txId: string, isExecuted?: boolean) => void @@ -119,6 +120,12 @@ export const SignOrExecuteForm = ({ )} + {!isCounterfactualSafe && safeTx && isCreation && ( + + + + )} + new Date().getTimezoneOffset() * 60 * -1000 @@ -28,3 +29,31 @@ export const getTxHistory = (chainId: string, safeAddress: string, trusted = fal pageUrl, ) } + +/** + * Fetch the module transaction id from the transaction service providing the transaction hash + */ +export const getModuleTransactionId = async ({ + transactionService, + safeAddress, + txHash, +}: { + transactionService: string + safeAddress: string + txHash: string +}) => { + const url = `${trimTrailingSlash( + transactionService, + )}/api/v1/safes/${safeAddress}/module-transactions/?transaction_hash=${txHash}` + const { results } = await fetch(url).then((res) => { + if (res.ok && res.status === 200) { + return res.json() as Promise + } else { + throw new Error('Error fetching Safe module transactions') + } + }) + + if (results.length === 0) throw new Error('module transaction not found') + + return results[0].moduleTransactionId as string +} diff --git a/src/services/tx/tx-sender/dispatch.ts b/src/services/tx/tx-sender/dispatch.ts index 76d6510a1d..49261e41ad 100644 --- a/src/services/tx/tx-sender/dispatch.ts +++ b/src/services/tx/tx-sender/dispatch.ts @@ -1,5 +1,10 @@ import { relayTransaction, type SafeInfo, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' -import type { SafeTransaction, TransactionOptions, TransactionResult } from '@safe-global/safe-core-sdk-types' +import type { + SafeTransaction, + Transaction, + TransactionOptions, + TransactionResult, +} from '@safe-global/safe-core-sdk-types' import { didRevert } from '@/utils/ethers-utils' import type { MultiSendCallOnlyEthersContract } from '@safe-global/protocol-kit' import { type SpendingLimitTxParams } from '@/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx' @@ -315,6 +320,54 @@ export const dispatchBatchExecution = async ( return result!.hash } +/** + * Execute a module transaction + */ +export const dispatchModuleTxExecution = async ( + tx: Transaction, + provider: Eip1193Provider, + safeAddress: string, +): Promise => { + const id = JSON.stringify(tx) + + let result: TransactionResponse | undefined + try { + const browserProvider = createWeb3(provider) + const signer = await browserProvider.getSigner() + + txDispatch(TxEvent.EXECUTING, { groupKey: id }) + result = await signer.sendTransaction(tx) + } catch (error) { + txDispatch(TxEvent.FAILED, { groupKey: id, error: asError(error) }) + throw error + } + + txDispatch(TxEvent.PROCESSING_MODULE, { + groupKey: id, + txHash: result.hash, + }) + + result + ?.wait() + .then((receipt) => { + if (receipt === null) { + txDispatch(TxEvent.FAILED, { groupKey: id, error: new Error('No transaction receipt found') }) + } else if (didRevert(receipt)) { + txDispatch(TxEvent.REVERTED, { + groupKey: id, + error: new Error('Transaction reverted by EVM'), + }) + } else { + txDispatch(TxEvent.PROCESSED, { groupKey: id, safeAddress, txHash: result?.hash }) + } + }) + .catch((error) => { + txDispatch(TxEvent.FAILED, { groupKey: id, error: asError(error) }) + }) + + return result?.hash +} + export const dispatchSpendingLimitTxExecution = async ( txParams: SpendingLimitTxParams, txOptions: TransactionOptions, diff --git a/yarn.lock b/yarn.lock index 864e29157a..2e191e53b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20155,3 +20155,8 @@ yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + +zodiac-roles-deployments@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/zodiac-roles-deployments/-/zodiac-roles-deployments-2.2.2.tgz#feb7e7544398e1572d2f7fa6ff2be033f2c736ce" + integrity sha512-6nG6/AuJh9SIrXR1NieRzSfn1+J6k9p2mb3qns3cRYhx3+i4wWIRh1JcE+jueHSYo+H7yKG/jfwRXMVN1L938A== From 3892de734460cbdc052edf0b1be64eba9c528109 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:42:20 +0200 Subject: [PATCH 054/154] Fix: filter out undefined address in useSafeOverviews (#3801) --- .../__tests__/useSafeOverviews.test.ts | 56 +++++++++++++++++++ .../welcome/MyAccounts/useSafeOverviews.ts | 4 +- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/components/welcome/MyAccounts/__tests__/useSafeOverviews.test.ts diff --git a/src/components/welcome/MyAccounts/__tests__/useSafeOverviews.test.ts b/src/components/welcome/MyAccounts/__tests__/useSafeOverviews.test.ts new file mode 100644 index 0000000000..dc20ee92cd --- /dev/null +++ b/src/components/welcome/MyAccounts/__tests__/useSafeOverviews.test.ts @@ -0,0 +1,56 @@ +import useSafeOverviews from '../useSafeOverviews' +import * as balances from '@/hooks/loadables/useLoadBalances' +import * as sdk from '@safe-global/safe-gateway-typescript-sdk' +import * as useWallet from '@/hooks/wallets/useWallet' +import * as store from '@/store' +import type { Eip1193Provider } from 'ethers' +import { renderHook } from '@testing-library/react' +import { act } from 'react-dom/test-utils' + +jest.spyOn(balances, 'useTokenListSetting').mockReturnValue(false) +jest.spyOn(store, 'useAppSelector').mockReturnValue('USD') +jest + .spyOn(useWallet, 'default') + .mockReturnValue({ label: 'MetaMask', chainId: '1', address: '0x1234', provider: null as unknown as Eip1193Provider }) + +describe('useSafeOverviews', () => { + it('should filter out undefined addresses', async () => { + const spy = jest.spyOn(sdk, 'getSafeOverviews').mockResolvedValue([]) + const safes = [ + { address: '0x1234', chainId: '1' }, + { address: undefined as unknown as string, chainId: '2' }, + { address: '0x5678', chainId: '3' }, + ] + + renderHook(() => useSafeOverviews(safes)) + + await act(() => Promise.resolve()) + + expect(spy).toHaveBeenCalledWith(['1:0x1234', '3:0x5678'], { + currency: 'USD', + exclude_spam: false, + trusted: true, + wallet_address: '0x1234', + }) + }) + + it('should filter out undefined chain ids', async () => { + const spy = jest.spyOn(sdk, 'getSafeOverviews').mockResolvedValue([]) + const safes = [ + { address: '0x1234', chainId: '1' }, + { address: '0x5678', chainId: undefined as unknown as string }, + { address: '0x5678', chainId: '3' }, + ] + + renderHook(() => useSafeOverviews(safes)) + + await act(() => Promise.resolve()) + + expect(spy).toHaveBeenCalledWith(['1:0x1234', '3:0x5678'], { + currency: 'USD', + exclude_spam: false, + trusted: true, + wallet_address: '0x1234', + }) + }) +}) diff --git a/src/components/welcome/MyAccounts/useSafeOverviews.ts b/src/components/welcome/MyAccounts/useSafeOverviews.ts index 3101b54c48..7c3b2ff2e1 100644 --- a/src/components/welcome/MyAccounts/useSafeOverviews.ts +++ b/src/components/welcome/MyAccounts/useSafeOverviews.ts @@ -16,12 +16,14 @@ type SafeParams = { // EIP155 address format const makeSafeId = ({ chainId, address }: SafeParams) => `${chainId}:${address}` as `${number}:0x${string}` +const validateSafeParams = ({ chainId, address }: SafeParams) => chainId != null && address != null + function useSafeOverviews(safes: Array): AsyncResult { const excludeSpam = useTokenListSetting() || false const currency = useAppSelector(selectCurrency) const wallet = useWallet() const walletAddress = wallet?.address - const safesIds = useMemo(() => safes.map(makeSafeId), [safes]) + const safesIds = useMemo(() => safes.filter(validateSafeParams).map(makeSafeId), [safes]) const [data, error, isLoading] = useAsync(async () => { return await getSafeOverviews(safesIds, { From 61c8b85f3271ef392fe00e574cec9b971f6c9059 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:19:06 +0200 Subject: [PATCH 055/154] Tests: Update cypress headers (#3800) --- cypress/support/commands.js | 7 +++++++ cypress/support/e2e.js | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index b8c8e64fee..7b34538717 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -211,3 +211,10 @@ Cypress.Commands.add('enter', (selector, opts) => { return () => cy.wrap($body, { log: false }) }) }) + +Cypress.Commands.add('setupInterceptors', () => { + cy.intercept({ url: '**/*' }, (req) => { + req.headers['Origin'] = 'http://localhost:8080' + req.continue() + }) +}) diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index bdb6fbbc22..fb98bb6297 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -38,3 +38,7 @@ before(() => { } }) }) + +beforeEach(() => { + cy.setupInterceptors() +}) From 3a936851734511b8ad718c6548b369421a164eff Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:58:56 +0200 Subject: [PATCH 056/154] Feat: top 5 assets on the dashboard (#3796) * Feat: top 5 assets on the dashboard * Adjust pending txs height * Rm recovery widget; separate tracking label * Mobile view * Don't show hidden assets; add no assets state * Show Send button when no Swaps * Fix e2e test --- cypress/e2e/pages/dashboard.pages.js | 2 +- .../balances/AssetsTable/SendButton.tsx | 41 ++++++ src/components/balances/AssetsTable/index.tsx | 65 ++-------- .../balances/AssetsTable/useHideAssets.ts | 11 +- src/components/dashboard/Assets/index.tsx | 121 ++++++++++++++++++ .../dashboard/PendingTxs/styles.module.css | 1 + src/components/dashboard/index.tsx | 12 +- .../components/RecoveryWidget/index.tsx | 49 ------- .../RecoveryWidget/styles.module.css | 27 ---- .../swap/components/SwapButton/index.tsx | 15 ++- src/services/analytics/events/swaps.ts | 9 +- 11 files changed, 207 insertions(+), 146 deletions(-) create mode 100644 src/components/balances/AssetsTable/SendButton.tsx create mode 100644 src/components/dashboard/Assets/index.tsx delete mode 100644 src/features/recovery/components/RecoveryWidget/index.tsx delete mode 100644 src/features/recovery/components/RecoveryWidget/styles.module.css diff --git a/cypress/e2e/pages/dashboard.pages.js b/cypress/e2e/pages/dashboard.pages.js index 6dc024f54e..746106e154 100644 --- a/cypress/e2e/pages/dashboard.pages.js +++ b/cypress/e2e/pages/dashboard.pages.js @@ -20,7 +20,7 @@ const txBuilder = 'a[href*="tx-builder"]' const safeSpecificLink = 'a[href*="&appUrl=http"]' const copyShareBtn = '[data-testid="copy-btn-icon"]' const exploreAppsBtn = '[data-testid="explore-apps-btn"]' -const viewAllLink = '[data-testid="view-all-link"]' +const viewAllLink = '[data-testid="view-all-link"][href^="/transactions/queue"]' const noTxIcon = '[data-testid="no-tx-icon"]' const noTxText = '[data-testid="no-tx-text"]' const pendingTxWidget = '[data-testid="pending-tx-widget"]' diff --git a/src/components/balances/AssetsTable/SendButton.tsx b/src/components/balances/AssetsTable/SendButton.tsx new file mode 100644 index 0000000000..f6a1a41b71 --- /dev/null +++ b/src/components/balances/AssetsTable/SendButton.tsx @@ -0,0 +1,41 @@ +import { useContext } from 'react' +import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { Button } from '@mui/material' +import ArrowIconNW from '@/public/images/common/arrow-top-right.svg' +import CheckWallet from '@/components/common/CheckWallet' +import useSpendingLimit from '@/hooks/useSpendingLimit' +import Track from '@/components/common/Track' +import { ASSETS_EVENTS } from '@/services/analytics/events/assets' +import { TokenTransferFlow } from '@/components/tx-flow/flows' +import { TxModalContext } from '@/components/tx-flow' + +const SendButton = ({ tokenInfo, isOutlined }: { tokenInfo: TokenInfo; isOutlined?: boolean }) => { + const spendingLimit = useSpendingLimit(tokenInfo) + const { setTxFlow } = useContext(TxModalContext) + + const onSendClick = () => { + setTxFlow() + } + + return ( + + {(isOk) => ( + + + + )} + + ) +} + +export default SendButton diff --git a/src/components/balances/AssetsTable/index.tsx b/src/components/balances/AssetsTable/index.tsx index 529b063103..a9b327e677 100644 --- a/src/components/balances/AssetsTable/index.tsx +++ b/src/components/balances/AssetsTable/index.tsx @@ -1,9 +1,8 @@ import CheckBalance from '@/features/counterfactual/CheckBalance' import { useHasFeature } from '@/hooks/useChains' -import ArrowIconNW from '@/public/images/common/arrow-top-right.svg' import { FEATURES } from '@/utils/chains' -import { type ReactElement, useMemo, useContext } from 'react' -import { Button, Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material' +import { type ReactElement } from 'react' +import { Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material' import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' import { TokenType } from '@safe-global/safe-gateway-typescript-sdk' import css from './styles.module.css' @@ -18,15 +17,12 @@ import InfoIcon from '@/public/images/notifications/info.svg' import { VisibilityOutlined } from '@mui/icons-material' import TokenMenu from '../TokenMenu' import useBalances from '@/hooks/useBalances' -import useHiddenTokens from '@/hooks/useHiddenTokens' -import { useHideAssets } from './useHideAssets' -import CheckWallet from '@/components/common/CheckWallet' -import useSpendingLimit from '@/hooks/useSpendingLimit' -import { TxModalContext } from '@/components/tx-flow' -import { TokenTransferFlow } from '@/components/tx-flow/flows' +import { useHideAssets, useVisibleAssets } from './useHideAssets' import AddFundsCTA from '@/components/common/AddFunds' import SwapButton from '@/features/swap/components/SwapButton' import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' +import { SWAP_LABELS } from '@/services/analytics/events/swaps' +import SendButton from './SendButton' const skeletonCells: EnhancedTableProps['rows'][0]['cells'] = { asset: { @@ -93,36 +89,6 @@ const headCells = [ }, ] -const SendButton = ({ - tokenInfo, - onClick, -}: { - tokenInfo: TokenInfo - onClick: (tokenAddress: string) => void -}): ReactElement => { - const spendingLimit = useSpendingLimit(tokenInfo) - - return ( - - {(isOk) => ( - - - - )} - - ) -} - const AssetsTable = ({ showHiddenAssets, setShowHiddenAssets, @@ -130,9 +96,7 @@ const AssetsTable = ({ showHiddenAssets: boolean setShowHiddenAssets: (hidden: boolean) => void }): ReactElement => { - const hiddenAssets = useHiddenTokens() const { balances, loading } = useBalances() - const { setTxFlow } = useContext(TxModalContext) const isCounterfactualSafe = useIsCounterfactualSafe() const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) && !isCounterfactualSafe @@ -140,22 +104,13 @@ const AssetsTable = ({ setShowHiddenAssets(false), ) - const visibleAssets = useMemo( - () => - showHiddenAssets - ? balances.items - : balances.items?.filter((item) => !hiddenAssets.includes(item.tokenInfo.address)), - [hiddenAssets, balances.items, showHiddenAssets], - ) + const visible = useVisibleAssets() + const visibleAssets = showHiddenAssets ? balances.items : visible const hasNoAssets = !loading && balances.items.length === 1 && balances.items[0].balance === '0' const selectedAssetCount = visibleAssets?.filter((item) => isAssetSelected(item.tokenInfo.address)).length || 0 - const onSendClick = (tokenAddress: string) => { - setTxFlow() - } - const rows = loading ? skeletonRows : (visibleAssets || []).map((item) => { @@ -225,9 +180,11 @@ const AssetsTable = ({ content: ( <> - onSendClick(item.tokenInfo.address)} /> + - {isSwapFeatureEnabled && } + {isSwapFeatureEnabled && ( + + )} {showHiddenAssets ? ( toggleAsset(item.tokenInfo.address)} /> diff --git a/src/components/balances/AssetsTable/useHideAssets.ts b/src/components/balances/AssetsTable/useHideAssets.ts index f5b36a2050..f4d3a2fce9 100644 --- a/src/components/balances/AssetsTable/useHideAssets.ts +++ b/src/components/balances/AssetsTable/useHideAssets.ts @@ -1,9 +1,9 @@ +import { useCallback, useMemo, useState } from 'react' import useBalances from '@/hooks/useBalances' import useChainId from '@/hooks/useChainId' import useHiddenTokens from '@/hooks/useHiddenTokens' import { useAppDispatch } from '@/store' import { setHiddenTokensForChain } from '@/store/settingsSlice' -import { useCallback, useState } from 'react' // This is the default for MUI Collapse export const COLLAPSE_TIMEOUT_MS = 300 @@ -91,3 +91,12 @@ export const useHideAssets = (closeDialog: () => void) => { hidingAsset, } } + +export const useVisibleAssets = () => { + const hiddenAssets = useHiddenTokens() + const { balances } = useBalances() + return useMemo( + () => balances.items?.filter((item) => !hiddenAssets.includes(item.tokenInfo.address)), + [hiddenAssets, balances.items], + ) +} diff --git a/src/components/dashboard/Assets/index.tsx b/src/components/dashboard/Assets/index.tsx new file mode 100644 index 0000000000..f8590806a1 --- /dev/null +++ b/src/components/dashboard/Assets/index.tsx @@ -0,0 +1,121 @@ +import { useMemo } from 'react' +import { Box, Skeleton, Typography, Paper } from '@mui/material' +import type { SafeBalanceResponse } from '@safe-global/safe-gateway-typescript-sdk' +import useBalances from '@/hooks/useBalances' +import FiatValue from '@/components/common/FiatValue' +import TokenAmount from '@/components/common/TokenAmount' +import SwapButton from '@/features/swap/components/SwapButton' +import { AppRoutes } from '@/config/routes' +import { WidgetContainer, WidgetBody, ViewAllLink } from '../styled' +import css from '../PendingTxs/styles.module.css' +import { useRouter } from 'next/router' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' +import { SWAP_LABELS } from '@/services/analytics/events/swaps' +import { useVisibleAssets } from '@/components/balances/AssetsTable/useHideAssets' +import BuyCryptoButton from '@/components/common/BuyCryptoButton' +import SendButton from '@/components/balances/AssetsTable/SendButton' + +const MAX_ASSETS = 5 + +const AssetsDummy = () => ( + + + {Array.from({ length: 2 }).map((_, index) => ( + + ))} + + +) + +const NoAssets = () => ( + + + Add funds to get started + + + + Add funds directly from your bank account or copy your address to send tokens from a different account. + + + + + + +) + +const AssetRow = ({ item, showSwap }: { item: SafeBalanceResponse['items'][number]; showSwap: boolean }) => ( + + + + + + + + + + + {showSwap ? ( + + ) : ( + + )} + + +) + +const AssetList = ({ items }: { items: SafeBalanceResponse['items'] }) => { + const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) + + return ( + + {items.map((item) => ( + + ))} + + ) +} + +const isNonZeroBalance = (item: SafeBalanceResponse['items'][number]) => item.balance !== '0' + +const AssetsWidget = () => { + const router = useRouter() + const { safe } = router.query + const { loading } = useBalances() + const visibleAssets = useVisibleAssets() + + const items = useMemo(() => { + return visibleAssets.filter(isNonZeroBalance).slice(0, MAX_ASSETS) + }, [visibleAssets]) + + const viewAllUrl = useMemo( + () => ({ + pathname: AppRoutes.balances.index, + query: { safe }, + }), + [safe], + ) + + return ( + +
    + + Top assets + + + {items.length > 0 && } +
    + + + {loading ? : items.length > 0 ? : } + +
    + ) +} + +export default AssetsWidget diff --git a/src/components/dashboard/PendingTxs/styles.module.css b/src/components/dashboard/PendingTxs/styles.module.css index 0162078926..15faf4809f 100644 --- a/src/components/dashboard/PendingTxs/styles.module.css +++ b/src/components/dashboard/PendingTxs/styles.module.css @@ -8,6 +8,7 @@ display: flex; align-items: center; gap: var(--space-2); + min-height: 50px; } .container:hover { diff --git a/src/components/dashboard/index.tsx b/src/components/dashboard/index.tsx index 3c72bf6be8..0331eb3bb5 100644 --- a/src/components/dashboard/index.tsx +++ b/src/components/dashboard/index.tsx @@ -4,6 +4,7 @@ import type { ReactElement } from 'react' import dynamic from 'next/dynamic' import { Grid } from '@mui/material' import PendingTxsList from '@/components/dashboard/PendingTxs/PendingTxsList' +import AssetsWidget from '@/components/dashboard/Assets' import Overview from '@/components/dashboard/Overview/Overview' import { FeaturedApps } from '@/components/dashboard/FeaturedApps/FeaturedApps' import SafeAppsDashboardSection from '@/components/dashboard/SafeAppsDashboardSection/SafeAppsDashboardSection' @@ -17,7 +18,6 @@ import ActivityRewardsSection from '@/components/dashboard/ActivityRewardsSectio import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' const RecoveryHeader = dynamic(() => import('@/features/recovery/components/RecoveryHeader')) -const RecoveryWidget = dynamic(() => import('@/features/recovery/components/RecoveryWidget')) const Dashboard = (): ReactElement => { const router = useRouter() @@ -46,14 +46,12 @@ const Dashboard = (): ReactElement => { - + - {showRecoveryWidget ? ( - - - - ) : null} + + + {showSafeApps && ( diff --git a/src/features/recovery/components/RecoveryWidget/index.tsx b/src/features/recovery/components/RecoveryWidget/index.tsx deleted file mode 100644 index 7a18792285..0000000000 --- a/src/features/recovery/components/RecoveryWidget/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { SetupRecoveryButton } from '@/features/recovery/components/RecoverySettings' -import { Box, Card, Grid, Typography } from '@mui/material' -import type { ReactElement } from 'react' - -import RecoveryLogo from '@/public/images/common/recovery.svg' -import { WidgetBody, WidgetContainer } from '@/components/dashboard/styled' -import { Chip } from '@/components/common/Chip' -import useRecovery from '@/features/recovery/hooks/useRecovery' - -import css from './styles.module.css' - -function RecoveryWidget(): ReactElement { - const [recovery] = useRecovery() - - return ( - - - New in {'Safe{Wallet}'} - - - - - - - - - - - - - Introducing {`Safe{RecoveryHub}`}{' '} - - - - - - Ensure you never lose access to your funds by choosing a recovery option to recover your Safe Account. - - - {(!recovery || recovery.length === 0) && } - - - - - - ) -} - -export default RecoveryWidget diff --git a/src/features/recovery/components/RecoveryWidget/styles.module.css b/src/features/recovery/components/RecoveryWidget/styles.module.css deleted file mode 100644 index f8f7fb0562..0000000000 --- a/src/features/recovery/components/RecoveryWidget/styles.module.css +++ /dev/null @@ -1,27 +0,0 @@ -.label { - font-weight: 700; - margin-bottom: var(--space-2); -} - -.card { - padding: var(--space-3) var(--space-4); - height: inherit; -} - -.grid { - display: flex; - align-items: center; - height: inherit; - gap: var(--space-3); -} - -.wrapper { - display: flex; - align-items: center; - gap: var(--space-1); -} - -.title { - font-weight: 700; - display: inline; -} diff --git a/src/features/swap/components/SwapButton/index.tsx b/src/features/swap/components/SwapButton/index.tsx index e6ac6979f9..2f608a83bf 100644 --- a/src/features/swap/components/SwapButton/index.tsx +++ b/src/features/swap/components/SwapButton/index.tsx @@ -2,21 +2,30 @@ import CheckWallet from '@/components/common/CheckWallet' import Track from '@/components/common/Track' import { AppRoutes } from '@/config/routes' import useSpendingLimit from '@/hooks/useSpendingLimit' -import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' +import type { SWAP_LABELS } from '@/services/analytics/events/swaps' +import { SWAP_EVENTS } from '@/services/analytics/events/swaps' import { Button } from '@mui/material' import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' import { useRouter } from 'next/router' import type { ReactElement } from 'react' import SwapIcon from '@/public/images/common/swap.svg' -const SwapButton = ({ tokenInfo, amount }: { tokenInfo: TokenInfo; amount: string }): ReactElement => { +const SwapButton = ({ + tokenInfo, + amount, + trackingLabel, +}: { + tokenInfo: TokenInfo + amount: string + trackingLabel: SWAP_LABELS +}): ReactElement => { const spendingLimit = useSpendingLimit(tokenInfo) const router = useRouter() return ( {(isOk) => ( - + + + + +
    +
    +
    + + + Swap + + + + + + ) +} + +export default SwapWidget diff --git a/src/features/swap/components/SwapWidget/styles.module.css b/src/features/swap/components/SwapWidget/styles.module.css new file mode 100644 index 0000000000..c6fb8c2608 --- /dev/null +++ b/src/features/swap/components/SwapWidget/styles.module.css @@ -0,0 +1,57 @@ +.label { + font-weight: 700; + margin-bottom: var(--space-2); +} + +.card { + height: inherit; + border: none; +} + +.grid { + display: flex; + height: inherit; + gap: var(--space-3); +} + +.wrapper { + display: flex; + flex-direction: column; + gap: var(--space-1); + padding: var(--space-4); +} + +.title { + font-weight: 700; + display: inline; + margin-right: var(--space-1); +} + +.imageContainer { + display: flex; + align-items: flex-end; +} + +.buttonContainer { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + gap: var(--space-2); +} + +@media (max-width: 599.95px) { + .imageContainer { + width: 100%; + justify-content: flex-end; + } + + .buttonContainer { + gap: 0; + justify-content: space-between; + } + + .wrapper { + padding: var(--space-3); + } +} diff --git a/src/services/analytics/events/swaps.ts b/src/services/analytics/events/swaps.ts index ee79f61a54..c932f83313 100644 --- a/src/services/analytics/events/swaps.ts +++ b/src/services/analytics/events/swaps.ts @@ -12,4 +12,5 @@ export enum SWAP_LABELS { sidebar = 'sidebar', asset = 'asset', dashboard_assets = 'dashboard_assets', + promoWidget = 'promoWidget', } From ff33839defba63a68a31b339fb1321fffe3389d8 Mon Sep 17 00:00:00 2001 From: James Mealy Date: Mon, 10 Jun 2024 05:21:06 +0100 Subject: [PATCH 062/154] Fix: move the execute button on recovery transactions to the correct column [SWAP-75] (#3808) --- src/components/transactions/TxSummary/index.tsx | 2 +- .../components/ExecuteRecoveryButton/index.tsx | 1 + .../recovery/components/RecoverySummary/index.tsx | 14 ++++++++------ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/transactions/TxSummary/index.tsx b/src/components/transactions/TxSummary/index.tsx index 72bd3f6fa6..38a316f5dd 100644 --- a/src/components/transactions/TxSummary/index.tsx +++ b/src/components/transactions/TxSummary/index.tsx @@ -91,7 +91,7 @@ const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => { )} {isQueue && !expiredSwap && ( - + )} diff --git a/src/features/recovery/components/ExecuteRecoveryButton/index.tsx b/src/features/recovery/components/ExecuteRecoveryButton/index.tsx index 9776b89413..5b3662d2f4 100644 --- a/src/features/recovery/components/ExecuteRecoveryButton/index.tsx +++ b/src/features/recovery/components/ExecuteRecoveryButton/index.tsx @@ -74,6 +74,7 @@ export function ExecuteRecoveryButton({ onClick={onClick} variant="contained" disabled={isDisabled} + sx={{ minWidth: '106.5px', py: compact ? 0.8 : undefined }} size={compact ? 'small' : 'stretched'} > Execute diff --git a/src/features/recovery/components/RecoverySummary/index.tsx b/src/features/recovery/components/RecoverySummary/index.tsx index bf5373ea8e..e3fdd022c4 100644 --- a/src/features/recovery/components/RecoverySummary/index.tsx +++ b/src/features/recovery/components/RecoverySummary/index.tsx @@ -30,13 +30,15 @@ export function RecoverySummary({ item }: { item: RecoveryQueueItem }): ReactEle - - {!isExecutable || isPending ? ( + {!isExecutable || isPending ? ( + - ) : ( - !isMalicious && wallet && - )} - + + ) : ( + + {!isMalicious && wallet && } + + )}
    ) } From 437d07aef9a87d66b3f8961e913c4a41530e06cb Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Mon, 10 Jun 2024 10:36:34 +0200 Subject: [PATCH 063/154] feat: add link to tx for order notifications [SWAP-61] (#3803) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add link to tx for order notifications We don’t always have the txId in the order information, but when we do we add a link to the notification. It’s highly likely that order created events won’t ever have a txId as this event is created before submitting the tx to the blockchain. I had to extract getTxLink to a separate file as having it in useTxNotifications was causing a circular dependency error in our tests. --- .../tx-flow/flows/SuccessScreen/index.tsx | 2 +- src/hooks/useTxNotifications.ts | 19 ++----------------- src/store/swapOrderSlice.ts | 19 +++++++++++++++++-- src/utils/tx-link.ts | 17 +++++++++++++++++ 4 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 src/utils/tx-link.ts diff --git a/src/components/tx-flow/flows/SuccessScreen/index.tsx b/src/components/tx-flow/flows/SuccessScreen/index.tsx index c853ded2f6..88e31e4461 100644 --- a/src/components/tx-flow/flows/SuccessScreen/index.tsx +++ b/src/components/tx-flow/flows/SuccessScreen/index.tsx @@ -6,7 +6,6 @@ import css from './styles.module.css' import { useAppSelector } from '@/store' import { PendingStatus, selectPendingTxById } from '@/store/pendingTxsSlice' import { useCallback, useContext, useEffect, useState } from 'react' -import { getTxLink } from '@/hooks/useTxNotifications' import { useCurrentChain } from '@/hooks/useChains' import { TxEvent, txSubscribe } from '@/services/tx/txEvents' import useSafeInfo from '@/hooks/useSafeInfo' @@ -18,6 +17,7 @@ import { DefaultStatus } from '@/components/tx-flow/flows/SuccessScreen/statuses import useDecodeTx from '@/hooks/useDecodeTx' import { isSwapConfirmationViewOrder } from '@/utils/transaction-guards' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' +import { getTxLink } from '@/utils/tx-link' interface Props { /** The ID assigned to the transaction in the client-gateway */ diff --git a/src/hooks/useTxNotifications.ts b/src/hooks/useTxNotifications.ts index afd3dc5c4a..98967e59e8 100644 --- a/src/hooks/useTxNotifications.ts +++ b/src/hooks/useTxNotifications.ts @@ -1,14 +1,12 @@ import { useEffect, useMemo, useRef } from 'react' import { formatError } from '@/utils/formatters' -import type { LinkProps } from 'next/link' import { selectNotifications, showNotification } from '@/store/notificationsSlice' import { useAppDispatch, useAppSelector } from '@/store' import { TxEvent, txSubscribe } from '@/services/tx/txEvents' -import { AppRoutes } from '@/config/routes' import { useCurrentChain } from './useChains' import useTxQueue from './useTxQueue' import { isSignableBy, isTransactionListItem } from '@/utils/transaction-guards' -import { type ChainInfo, TransactionStatus } from '@safe-global/safe-gateway-typescript-sdk' +import { TransactionStatus } from '@safe-global/safe-gateway-typescript-sdk' import { selectPendingTxs } from '@/store/pendingTxsSlice' import useIsSafeOwner from '@/hooks/useIsSafeOwner' import useWallet from './wallets/useWallet' @@ -16,6 +14,7 @@ import useSafeAddress from './useSafeAddress' import { getExplorerLink } from '@/utils/gateway' import { getTxDetails } from '@/services/transactions' import { isWalletRejection } from '@/utils/wallets' +import { getTxLink } from '@/utils/tx-link' const TxNotifications = { [TxEvent.SIGN_FAILED]: 'Failed to sign. Please try again.', @@ -43,20 +42,6 @@ enum Variant { const successEvents = [TxEvent.PROPOSED, TxEvent.SIGNATURE_PROPOSED, TxEvent.ONCHAIN_SIGNATURE_SUCCESS, TxEvent.SUCCESS] -export const getTxLink = ( - txId: string, - chain: ChainInfo, - safeAddress: string, -): { href: LinkProps['href']; title: string } => { - return { - href: { - pathname: AppRoutes.transactions.tx, - query: { id: txId, safe: `${chain?.shortName}:${safeAddress}` }, - }, - title: 'View transaction', - } -} - const useTxNotifications = (): void => { const dispatch = useAppDispatch() const chain = useCurrentChain() diff --git a/src/store/swapOrderSlice.ts b/src/store/swapOrderSlice.ts index 2fd4afcf2b..31fc0349d9 100644 --- a/src/store/swapOrderSlice.ts +++ b/src/store/swapOrderSlice.ts @@ -6,6 +6,8 @@ import { isSwapTxInfo, isTransactionListItem } from '@/utils/transaction-guards' import { txHistorySlice } from '@/store/txHistorySlice' import { showNotification } from '@/store/notificationsSlice' import { selectSafeInfo } from '@/store/safeInfoSlice' +import { selectChainById } from '@/store/chainsSlice' +import { getTxLink } from '@/utils/tx-link' type AllStatuses = OrderStatuses | 'created' type Order = { @@ -80,11 +82,18 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl if (oldStatus === newStatus || newStatus === undefined) { return } + const safeInfo = selectSafeInfo(listenerApi.getState()) + + let link = undefined + if (swapOrder.txId && safeInfo.data?.chainId && safeInfo.data?.address) { + const chainInfo = selectChainById(listenerApi.getState(), safeInfo.data?.chainId) + if (chainInfo !== undefined) { + link = getTxLink(swapOrder.txId, chainInfo, safeInfo.data?.address.value) + } + } switch (newStatus) { case 'created': - const safeInfo = selectSafeInfo(listenerApi.getState()) - dispatch( showNotification({ title: 'Order created', @@ -94,6 +103,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl : 'Waiting for confirmation from signers of your Safe', groupKey, variant: 'info', + link, }), ) @@ -105,6 +115,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl message: 'Waiting for confirmation from signers of your Safe', groupKey, variant: 'info', + link, }), ) break @@ -115,6 +126,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl message: 'Waiting for order execution by the CoW Protocol', groupKey, variant: 'info', + link, }), ) break @@ -129,6 +141,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl message: 'Your order has been successful', groupKey, variant: 'success', + link, }), ) break @@ -143,6 +156,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl message: 'Your order has reached the expiry time and has become invalid', groupKey, variant: 'warning', + link, }), ) break @@ -157,6 +171,7 @@ export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddl message: 'Your order has been cancelled', groupKey, variant: 'warning', + link, }), ) break diff --git a/src/utils/tx-link.ts b/src/utils/tx-link.ts new file mode 100644 index 0000000000..4b7635efc1 --- /dev/null +++ b/src/utils/tx-link.ts @@ -0,0 +1,17 @@ +import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import type { LinkProps } from 'next/link' +import { AppRoutes } from '@/config/routes' + +export const getTxLink = ( + txId: string, + chain: ChainInfo, + safeAddress: string, +): { href: LinkProps['href']; title: string } => { + return { + href: { + pathname: AppRoutes.transactions.tx, + query: { id: txId, safe: `${chain?.shortName}:${safeAddress}` }, + }, + title: 'View transaction', + } +} From 71a8bda36fb937f783fc9d28ec809c101dfd5b7f Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 10 Jun 2024 11:48:35 +0200 Subject: [PATCH 064/154] Fix: restore lowercase "native transfer" (#3814) --- cypress/fixtures/txhistory_data_data.json | 2 +- .../TxData/DecodedData/SingleTxDecoded/index.test.tsx | 4 ++-- .../TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cypress/fixtures/txhistory_data_data.json b/cypress/fixtures/txhistory_data_data.json index 7f103076c0..b5b2d020d9 100644 --- a/cypress/fixtures/txhistory_data_data.json +++ b/cypress/fixtures/txhistory_data_data.json @@ -72,7 +72,7 @@ "transactionHash": "0xa5dd...b064", "safeTxHash": "0x4dd0...b2b8", "nativeTransfer": { - "title": "Native transfer", + "title": "native transfer", "description": "Interact with (and send < 0.00001 ETH to)" } }, diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx index 7727ec34db..357ca4fc1b 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.test.tsx @@ -28,7 +28,7 @@ describe('SingleTxDecoded', () => { />, ) - expect(result.queryByText('Native transfer')).not.toBeNull() + expect(result.queryByText('native transfer')).not.toBeNull() }) it('should show unknown contract interactions', () => { @@ -52,7 +52,7 @@ describe('SingleTxDecoded', () => { />, ) - expect(result.queryByText('Contract interaction')).not.toBeNull() + expect(result.queryByText('contract interaction')).not.toBeNull() }) it('should show decoded data ', () => { diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx index 44392beac0..e6a719529e 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx @@ -36,7 +36,7 @@ export const SingleTxDecoded = ({ }: SingleTxDecodedProps) => { const chain = useCurrentChain() const isNativeTransfer = tx.value !== '0' && (!tx.data || isEmptyHexData(tx.data)) - const method = tx.dataDecoded?.method || (isNativeTransfer ? 'Native transfer' : 'Contract interaction') + const method = tx.dataDecoded?.method || (isNativeTransfer ? 'native transfer' : 'contract interaction') const { decimals, symbol } = chain?.nativeCurrency || {} const amount = tx.value ? formatVisualAmount(tx.value, decimals) : 0 From f5c3df487df2b20bb476c2394e5e361fb1d7d9ab Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Mon, 10 Jun 2024 11:56:40 +0200 Subject: [PATCH 065/154] feat: decode txs in swap modal [SWAP-81] (#3764) --- .../tx-flow/flows/SafeAppsTx/index.tsx | 2 +- .../tx-flow/flows/SignMessage/index.tsx | 13 +++++++++-- src/features/swap/index.tsx | 23 +++++++++++++++++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/components/tx-flow/flows/SafeAppsTx/index.tsx b/src/components/tx-flow/flows/SafeAppsTx/index.tsx index 297d13f569..6bb2476305 100644 --- a/src/components/tx-flow/flows/SafeAppsTx/index.tsx +++ b/src/components/tx-flow/flows/SafeAppsTx/index.tsx @@ -22,7 +22,7 @@ const SafeAppsTxFlow = ({ return ( } + subtitle={} step={0} > diff --git a/src/components/tx-flow/flows/SignMessage/index.tsx b/src/components/tx-flow/flows/SignMessage/index.tsx index 3b9383bfee..4d0b3f64ef 100644 --- a/src/components/tx-flow/flows/SignMessage/index.tsx +++ b/src/components/tx-flow/flows/SignMessage/index.tsx @@ -6,17 +6,26 @@ import { useAppSelector } from '@/store' import { Box, Typography } from '@mui/material' import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard' import { ErrorBoundary } from '@sentry/react' +import { type BaseTransaction } from '@safe-global/safe-apps-sdk' const APP_LOGO_FALLBACK_IMAGE = '/images/apps/apps-icon.svg' const APP_NAME_FALLBACK = 'Sign message' -export const AppTitle = ({ name, logoUri }: { name?: string | null; logoUri?: string | null }) => { +export const AppTitle = ({ + name, + logoUri, + txs, +}: { + name?: string | null + logoUri?: string | null + txs?: BaseTransaction[] +}) => { const swapParams = useAppSelector(selectSwapParams) const appName = name || APP_NAME_FALLBACK const appLogo = logoUri || APP_LOGO_FALLBACK_IMAGE - const title = name === SWAP_TITLE ? getSwapTitle(swapParams.tradeType) : appName + const title = name === SWAP_TITLE ? getSwapTitle(swapParams.tradeType, txs) : appName return ( diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 3a97cdce70..7734b0aafe 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -27,9 +27,16 @@ import { isBlockedAddress } from '@/services/ofac' import { selectSwapParams, setSwapParams, type SwapState } from './store/swapParamsSlice' import { setSwapOrder } from '@/store/swapOrderSlice' import useChainId from '@/hooks/useChainId' +import { type BaseTransaction } from '@safe-global/safe-apps-sdk' +import { APPROVAL_SIGNATURE_HASH } from '@/components/tx/ApprovalEditor/utils/approvals' +import { id } from 'ethers' const BASE_URL = typeof window !== 'undefined' && window.location.origin ? window.location.origin : '' +const PRE_SIGN_SIGHASH = id('setPreSignature(bytes,bool)').slice(0, 10) +const WRAP_SIGHASH = id('deposit()').slice(0, 10) +const UNWRAP_SIGHASH = id('withdraw(uint256)').slice(0, 10) + type Params = { sell?: { asset: string @@ -39,8 +46,20 @@ type Params = { export const SWAP_TITLE = 'Safe Swap' -export const getSwapTitle = (tradeType: SwapState['tradeType']) => { - return tradeType === 'limit' ? 'Limit order' : 'Swap order' +export const getSwapTitle = (tradeType: SwapState['tradeType'], txs: BaseTransaction[] | undefined) => { + const hashToLabel = { + [PRE_SIGN_SIGHASH]: tradeType === 'limit' ? 'Limit order' : 'Swap order', + [APPROVAL_SIGNATURE_HASH]: 'Approve', + [WRAP_SIGHASH]: 'Wrap', + [UNWRAP_SIGHASH]: 'Unwrap', + } + + const swapTitle = txs + ?.map((tx) => hashToLabel[tx.data.slice(0, 10)]) + .filter(Boolean) + .join(' and ') + + return swapTitle } const SwapWidget = ({ sell }: Params) => { From 6bce0ac56d8d6f96c1b47161694f0edf53639791 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:34:03 +0200 Subject: [PATCH 066/154] fix: Optimize RPC requests when predicting safe address (#3780) * fix: Optimize RPC requests when predicting safe address * fix: Explicitely add safeVersion when predicting address --- src/components/new-safe/create/logic/index.ts | 16 +++++++++++++--- .../new-safe/create/logic/utils.test.ts | 14 ++++++++++++-- src/components/new-safe/create/logic/utils.ts | 10 +++++++--- .../new-safe/create/steps/ReviewStep/index.tsx | 4 ++-- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/components/new-safe/create/logic/index.ts b/src/components/new-safe/create/logic/index.ts index 4bf7f1f9eb..a0c504da03 100644 --- a/src/components/new-safe/create/logic/index.ts +++ b/src/components/new-safe/create/logic/index.ts @@ -19,7 +19,7 @@ import { AppRoutes } from '@/config/routes' import { SAFE_APPS_EVENTS, trackEvent } from '@/services/analytics' import type { AppDispatch, AppThunk } from '@/store' import { showNotification } from '@/store/notificationsSlice' -import { SafeFactory } from '@safe-global/protocol-kit' +import { predictSafeAddress, SafeFactory } from '@safe-global/protocol-kit' import type Safe from '@safe-global/protocol-kit' import type { DeploySafeProps } from '@safe-global/protocol-kit' import { createEthersAdapter, isValidSafeVersion } from '@/hooks/coreSDK/safeCoreSDK' @@ -86,9 +86,19 @@ export const createNewSafe = async ( export const computeNewSafeAddress = async ( ethersProvider: BrowserProvider, props: DeploySafeProps, + chainId: string, ): Promise => { - const safeFactory = await getSafeFactory(ethersProvider) - return safeFactory.predictSafeAddress(props.safeAccountConfig, props.saltNonce) + const ethAdapter = await createEthersAdapter(ethersProvider) + + return predictSafeAddress({ + ethAdapter, + chainId: BigInt(chainId), + safeAccountConfig: props.safeAccountConfig, + safeDeploymentConfig: { + saltNonce: props.saltNonce, + safeVersion: LATEST_SAFE_VERSION as SafeVersion, + }, + }) } /** diff --git a/src/components/new-safe/create/logic/utils.test.ts b/src/components/new-safe/create/logic/utils.test.ts index 8990d1b594..91b5c4c54d 100644 --- a/src/components/new-safe/create/logic/utils.test.ts +++ b/src/components/new-safe/create/logic/utils.test.ts @@ -30,8 +30,13 @@ describe('getAvailableSaltNonce', () => { it('should return initial nonce if no contract is deployed to the computed address', async () => { jest.spyOn(web3Utils, 'isSmartContract').mockReturnValue(Promise.resolve(false)) const initialNonce = faker.string.numeric() + const mockChainId = faker.string.numeric() - const result = await getAvailableSaltNonce(mockProvider, { ...mockDeployProps, saltNonce: initialNonce }) + const result = await getAvailableSaltNonce( + mockProvider, + { ...mockDeployProps, saltNonce: initialNonce }, + mockChainId, + ) expect(result).toEqual(initialNonce) }) @@ -39,8 +44,13 @@ describe('getAvailableSaltNonce', () => { it('should return an increased nonce if a contract is deployed to the computed address', async () => { jest.spyOn(web3Utils, 'isSmartContract').mockReturnValueOnce(Promise.resolve(true)) const initialNonce = faker.string.numeric() + const mockChainId = faker.string.numeric() - const result = await getAvailableSaltNonce(mockProvider, { ...mockDeployProps, saltNonce: initialNonce }) + const result = await getAvailableSaltNonce( + mockProvider, + { ...mockDeployProps, saltNonce: initialNonce }, + mockChainId, + ) jest.spyOn(web3Utils, 'isSmartContract').mockReturnValueOnce(Promise.resolve(false)) diff --git a/src/components/new-safe/create/logic/utils.ts b/src/components/new-safe/create/logic/utils.ts index e31e01bfd1..5d616c2d16 100644 --- a/src/components/new-safe/create/logic/utils.ts +++ b/src/components/new-safe/create/logic/utils.ts @@ -3,13 +3,17 @@ import { isSmartContract } from '@/hooks/wallets/web3' import type { DeploySafeProps } from '@safe-global/protocol-kit' import type { BrowserProvider } from 'ethers' -export const getAvailableSaltNonce = async (provider: BrowserProvider, props: DeploySafeProps): Promise => { - const safeAddress = await computeNewSafeAddress(provider, props) +export const getAvailableSaltNonce = async ( + provider: BrowserProvider, + props: DeploySafeProps, + chainId: string, +): Promise => { + const safeAddress = await computeNewSafeAddress(provider, props, chainId) const isContractDeployed = await isSmartContract(provider, safeAddress) // Safe is already deployed so we try the next saltNonce if (isContractDeployed) { - return getAvailableSaltNonce(provider, { ...props, saltNonce: (Number(props.saltNonce) + 1).toString() }) + return getAvailableSaltNonce(provider, { ...props, saltNonce: (Number(props.saltNonce) + 1).toString() }, chainId) } // We know that there will be a saltNonce but the type has it as optional diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index 85141ffaec..3cc24712e7 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -175,8 +175,8 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps Date: Mon, 10 Jun 2024 17:13:39 +0200 Subject: [PATCH 067/154] Feat: better fiat and token amount formatting (#3807) --- src/components/balances/AssetsTable/index.tsx | 6 +- src/components/common/EnhancedTable/index.tsx | 6 +- src/components/common/FiatValue/index.tsx | 6 +- .../common/TokenAmount/index.test.tsx | 2 +- src/components/dashboard/Assets/index.tsx | 2 +- .../dashboard/Overview/Overview.tsx | 2 +- .../welcome/MyAccounts/AccountItem.tsx | 2 +- src/utils/__tests__/formatNumber.test.ts | 307 ++---------------- src/utils/formatNumber.ts | 225 +++---------- 9 files changed, 86 insertions(+), 472 deletions(-) diff --git a/src/components/balances/AssetsTable/index.tsx b/src/components/balances/AssetsTable/index.tsx index a9b327e677..3b52de7ac1 100644 --- a/src/components/balances/AssetsTable/index.tsx +++ b/src/components/balances/AssetsTable/index.tsx @@ -80,6 +80,7 @@ const headCells = [ id: 'value', label: 'Value', width: '20%', + align: 'right', }, { id: 'actions', @@ -151,8 +152,9 @@ const AssetsTable = ({ rawValue: rawFiatValue, collapsed: item.tokenInfo.address === hidingAsset, content: ( - <> + + {rawFiatValue === 0 && ( )} - + ), }, actions: { diff --git a/src/components/common/EnhancedTable/index.tsx b/src/components/common/EnhancedTable/index.tsx index 30a5130830..0874f4beb8 100644 --- a/src/components/common/EnhancedTable/index.tsx +++ b/src/components/common/EnhancedTable/index.tsx @@ -34,6 +34,7 @@ type EnhancedHeadCell = { id: string label: ReactNode width?: string + align?: string sticky?: boolean } @@ -75,7 +76,10 @@ function EnhancedTableHead(props: EnhancedTableHeadProps) { align="left" padding="normal" sortDirection={orderBy === headCell.id ? order : false} - sx={headCell.width ? { width: headCell.width } : undefined} + sx={{ + width: headCell.width ? headCell.width : '', + textAlign: headCell.align ? headCell.align : '', + }} className={classNames({ sticky: headCell.sticky })} > {headCell.label && ( diff --git a/src/components/common/FiatValue/index.tsx b/src/components/common/FiatValue/index.tsx index fb51c4dfab..94d0164591 100644 --- a/src/components/common/FiatValue/index.tsx +++ b/src/components/common/FiatValue/index.tsx @@ -4,12 +4,12 @@ import { useAppSelector } from '@/store' import { selectCurrency } from '@/store/settingsSlice' import { formatCurrency } from '@/utils/formatNumber' -const FiatValue = ({ value }: { value: string | number }): ReactElement => { +const FiatValue = ({ value, maxLength }: { value: string | number; maxLength?: number }): ReactElement => { const currency = useAppSelector(selectCurrency) const fiat = useMemo(() => { - return formatCurrency(value, currency) - }, [value, currency]) + return formatCurrency(value, currency, maxLength) + }, [value, currency, maxLength]) return {fiat} } diff --git a/src/components/common/TokenAmount/index.test.tsx b/src/components/common/TokenAmount/index.test.tsx index e99f5ed266..e04b5ba261 100644 --- a/src/components/common/TokenAmount/index.test.tsx +++ b/src/components/common/TokenAmount/index.test.tsx @@ -14,6 +14,6 @@ describe('TokenAmount', () => { it('should format big amount for zero decimals', async () => { const result = render() - await expect(result.findByText('10,000,000')).resolves.not.toBeNull() + await expect(result.findByText('10M')).resolves.not.toBeNull() }) }) diff --git a/src/components/dashboard/Assets/index.tsx b/src/components/dashboard/Assets/index.tsx index f8590806a1..45f867893a 100644 --- a/src/components/dashboard/Assets/index.tsx +++ b/src/components/dashboard/Assets/index.tsx @@ -55,7 +55,7 @@ const AssetRow = ({ item, showSwap }: { item: SafeBalanceResponse['items'][numbe /> - + diff --git a/src/components/dashboard/Overview/Overview.tsx b/src/components/dashboard/Overview/Overview.tsx index 8307f27b2f..0fbdd41379 100644 --- a/src/components/dashboard/Overview/Overview.tsx +++ b/src/components/dashboard/Overview/Overview.tsx @@ -73,7 +73,7 @@ const Overview = (): ReactElement => { {safe.deployed ? ( - + ) : ( - + {safeOverview?.fiatTotal && } diff --git a/src/utils/__tests__/formatNumber.test.ts b/src/utils/__tests__/formatNumber.test.ts index 84f4c000f5..b3890ffc35 100644 --- a/src/utils/__tests__/formatNumber.test.ts +++ b/src/utils/__tests__/formatNumber.test.ts @@ -1,308 +1,61 @@ -import { formatAmount, formatAmountPrecise, formatCurrency } from '@/utils/formatNumber' +import { formatAmountPrecise, formatAmount, formatCurrency } from '@/utils/formatNumber' describe('formatNumber', () => { - describe('formatAmount', () => { - it('should remove trailing zeroes', () => { - expect(formatAmount('0.10000')).toEqual('0.1') - expect(formatAmount('0.100000000000')).toEqual('0.1') - }) - - it('should use maximum of 5 decimals', () => { - expect(formatAmount('0.123456789')).toEqual('0.12346') - }) - - it('should use five decimals for numbers up until 999.99999', () => { - expect(formatAmount('345.123456789')).toEqual('345.12346') // 9 decimals - expect(formatAmount('999.99999')).toEqual('999.99999') // 5 decimals - - // rounds above the specified limit - expect(formatAmount('999.999992')).toEqual('999.99999') // 6 decimals - expect(formatAmount('999.999996')).toEqual('1,000') - }) - - it('should use four decimals for numbers between 1,000.0001 until 9,999.9999', () => { - // rounds down past the specified precision - expect(formatAmount(1_000.00001)).toEqual('1,000') - - expect(formatAmount(1_000.0001234)).toEqual('1,000.0001') - expect(formatAmount(1_234.123456789)).toEqual('1,234.1235') - expect(formatAmount(9_999.9999)).toEqual('9,999.9999') - - // rounds above the specified limit - expect(formatAmount(9_999.99992)).toEqual('9,999.9999') - expect(formatAmount(9_999.99996)).toEqual('10,000') - }) - - it('should use three decimals for numbers between 10,000.001 until 99,999.999', () => { - // rounds down past the specified precision - expect(formatAmount(10_000.00001)).toEqual('10,000') - - expect(formatAmount(10_000.001)).toEqual('10,000.001') - expect(formatAmount(12_345.123456789)).toEqual('12,345.123') - expect(formatAmount(99_999.999)).toEqual('99,999.999') - - // rounds above the specified limit - expect(formatAmount(99_999.9992)).toEqual('99,999.999') - expect(formatAmount(99_999.9996)).toEqual('100,000') - }) - - it('should use two decimals for numbers between 100,000.01 until 999,999.99', () => { - // rounds down past the specified precision - expect(formatAmount(100_000.00001)).toEqual('100,000') - - expect(formatAmount(100_000.01)).toEqual('100,000.01') - expect(formatAmount(123_456.123456789)).toEqual('123,456.12') - expect(formatAmount(999_999.99)).toEqual('999,999.99') - - // rounds above the specified limit - expect(formatAmount(999_999.992)).toEqual('999,999.99') - expect(formatAmount(999_999.996)).toEqual('1,000,000') - }) - - it('should use one decimal for numbers between 1,000,000.1 until 9,999,999.9', () => { - // rounds down past the specified precision - expect(formatAmount(1_000_000.00001)).toEqual('1,000,000') - - expect(formatAmount(1_000_000.1)).toEqual('1,000,000.1') - expect(formatAmount(1_234_567.123456789)).toEqual('1,234,567.1') - expect(formatAmount(9_999_999.9)).toEqual('9,999,999.9') - - // rounds above the specified limit - expect(formatAmount(9_999_999.92)).toEqual('9,999,999.9') - expect(formatAmount(9_999_999.96)).toEqual('10,000,000') - }) - - it('should use no decimals for numbers between 10,000,000 and 99,999,999.5', () => { - // rounds down past the specified precision - expect(formatAmount(10_000_000.00001)).toEqual('10,000,000') - - expect(formatAmount(10_000_000.1)).toEqual('10,000,000') - expect(formatAmount(12_345_678.123456789)).toEqual('12,345,678') - expect(formatAmount(99_999_999)).toEqual('99,999,999') - - // rounds above the specified limit - expect(formatAmount(99_999_999.2)).toEqual('99,999,999') - expect(formatAmount(99_999_999.6)).toEqual('100M') - }) - - it('should use M symbol for numbers between 100,000,000 and 999,999,500', () => { - // rounds down past the specified precision - expect(formatAmount(100_000_000.00001)).toEqual('100M') - expect(formatAmount(100_000_100)).toEqual('100M') - - expect(formatAmount(100_001_000)).toEqual('100.001M') - expect(formatAmount(123_456_789.123456789)).toEqual('123.457M') - expect(formatAmount(999_999_000)).toEqual('999.999M') - - // rounds above the specified limit - expect(formatAmount(999_999_499)).toEqual('999.999M') - expect(formatAmount(999_999_500)).toEqual('1B') + describe('formatAmountPrecise', () => { + it('should format a number with a defined precision', () => { + expect(formatAmountPrecise(1234.5678, 2)).toBe('1,234.57') }) + }) - it('should use B symbol for numbers between 999,999,500 and 999,999,500,000', () => { - // rounds down past the specified precision - expect(formatAmount(1_000_000_000.00001)).toEqual('1B') - expect(formatAmount(1_000_100_000)).toEqual('1B') - - expect(formatAmount(1_100_000_000)).toEqual('1.1B') - expect(formatAmount(1_234_567_898.123456789)).toEqual('1.235B') - expect(formatAmount(100_001_000_500)).toEqual('100.001B') - expect(formatAmount(999_999_000_000)).toEqual('999.999B') - - // rounds above the specified limit - expect(formatAmount(999_999_499_999)).toEqual('999.999B') - expect(formatAmount(999_999_500_000)).toEqual('1T') + describe('formatAmount', () => { + it('should format a number below 0.0001', () => { + expect(formatAmount(0.000000009)).toBe('< 0.00001') }) - it('should use T notation for numbers between 999,999,500,000 and 999,000,000,000', () => { - // rounds down past the specified precision - expect(formatAmount(1_000_000_000_000.00001)).toEqual('1T') - expect(formatAmount(1_000_100_000_000)).toEqual('1T') - - expect(formatAmount(1_100_000_000_000)).toEqual('1.1T') - expect(formatAmount(1_234_567_898_765.123456789)).toEqual('1.235T') - expect(formatAmount(100_001_000_000_000)).toEqual('100.001T') - expect(formatAmount(999_999_000_000_000)).toEqual('> 999T') + it('should format a number below 1', () => { + expect(formatAmount(0.567811)).toBe('0.56781') }) - it('should use > 999T for numbers above 999,000,000,000,000', () => { - expect(formatAmount(999_000_000_000_001)).toEqual('> 999T') - expect(formatAmount(999_000_000_000_000.001)).toEqual('> 999T') + it('should format a number above 1', () => { + expect(formatAmount(285.1257657)).toBe('285.12577') }) - it('should use < 0.00001 for amounts smaller then 0.00001', () => { - expect(formatAmount(0.00001)).toEqual('0.00001') - expect(formatAmount(0.000014)).toEqual('0.00001') - expect(formatAmount(0.000015)).toEqual('0.00002') - expect(formatAmount(0.000001)).toEqual('< 0.00001') - expect(formatAmount(0.000009)).toEqual('< 0.00001') + it('should abbreviate a number with more than 10 digits', () => { + expect(formatAmount(12345678901)).toBe('12.35B') }) - it('should use < -0.00001 or < +0.00001 when the Eucledian distance of the amount is smaller than 0.00001', () => { - // to keep the '+' sign the amount shall be passed as a string - expect(formatAmount('+0.000001')).toEqual('< +0.00001') - expect(formatAmount('+0.000009')).toEqual('< +0.00001') - - // negative numbers will keep the sign either way - expect(formatAmount(-0.000001)).toEqual('< -0.00001') - expect(formatAmount(-0.000009)).toEqual('< -0.00001') - expect(formatAmount('-0.000001')).toEqual('< -0.00001') - expect(formatAmount('-0.000009')).toEqual('< -0.00001') + it('should abbreviate a number with more than a given amount of digits', () => { + expect(formatAmount(1234.12, 2, 4)).toBe('1.23K') }) }) describe('formatCurrency', () => { - it('returns the correct number of decimals', () => { - const amount1 = 0 - - expect(formatCurrency(amount1, 'JPY')).toBe('0 JPY') - expect(formatCurrency(amount1, 'IQD')).toBe('0 IQD') - expect(formatCurrency(amount1, 'USD')).toBe('0.00 USD') - expect(formatCurrency(amount1, 'EUR')).toBe('0.00 EUR') - expect(formatCurrency(amount1, 'GBP')).toBe('0.00 GBP') - expect(formatCurrency(amount1, 'BHD')).toBe('0.000 BHD') - - const amount2 = 1 - - expect(formatCurrency(amount2, 'JPY')).toBe('1 JPY') - expect(formatCurrency(amount2, 'IQD')).toBe('1 IQD') - expect(formatCurrency(amount2, 'USD')).toBe('1.00 USD') - expect(formatCurrency(amount2, 'EUR')).toBe('1.00 EUR') - expect(formatCurrency(amount2, 'GBP')).toBe('1.00 GBP') - expect(formatCurrency(amount2, 'BHD')).toBe('1.000 BHD') - - const amount3 = '1.7777' - - expect(formatCurrency(amount3, 'JPY')).toBe('2 JPY') - expect(formatCurrency(amount3, 'IQD')).toBe('2 IQD') - expect(formatCurrency(amount3, 'USD')).toBe('1.78 USD') - expect(formatCurrency(amount3, 'EUR')).toBe('1.78 EUR') - expect(formatCurrency(amount3, 'GBP')).toBe('1.78 GBP') - expect(formatCurrency(amount3, 'BHD')).toBe('1.778 BHD') - }) - - it('should drop decimals for values above 1k', () => { - // It should stop - expect(formatCurrency(999.99, 'USD')).toBe('999.99 USD') - expect(formatCurrency(1000.1, 'USD')).toBe('1,000 USD') - expect(formatCurrency(1000.99, 'USD')).toBe('1,001 USD') - expect(formatCurrency(32500.5, 'EUR')).toBe('32,501 EUR') - expect(formatCurrency(314285500.1, 'JPY')).toBe('314.286M JPY') + it('should format a 0', () => { + expect(formatCurrency(0, 'USD')).toBe('$ 0') }) - it('should use M symbol for numbers between 100,000,000 and 999,999,500', () => { - const amount1 = 100_000_100 - - expect(formatCurrency(amount1, 'JPY')).toBe('100M JPY') - - const amount2 = 123_456_789.123456789 - - expect(formatCurrency(amount2, 'JPY')).toBe('123.457M JPY') - - const amount3 = 999_999_500 - - expect(formatCurrency(amount3, 'JPY')).toBe('1B JPY') + it('should format a number below 1', () => { + expect(formatCurrency(0.5678, 'USD')).toBe('$ 0.57') }) - it('should use B symbol for numbers between 999,999,500 and 999,999,500,000', () => { - const amount1 = 1_000_000_000 - - expect(formatCurrency(amount1, 'JPY')).toBe('1B JPY') - - const amount2 = 1_234_567_898.123456789 - - expect(formatCurrency(amount2, 'JPY')).toBe('1.235B JPY') - - const amount3 = 999_999_500_000 - - expect(formatCurrency(amount3, 'JPY')).toBe('1T JPY') - }) - - it('should use T notation for numbers between 999,999,500,000 and 999,000,000,000', () => { - const amount1 = 1_000_100_000_000 - - expect(formatCurrency(amount1, 'JPY')).toBe('1T JPY') - - const amount2 = 1_234_567_898_765.123456789 - - expect(formatCurrency(amount2, 'JPY')).toBe('1.235T JPY') - - const amount3 = 999_999_000_000_000 - - expect(formatCurrency(amount3, 'JPY')).toBe('> 999T JPY') + it('should format a number above 1', () => { + expect(formatCurrency(285.1257657, 'EUR')).toBe('€ 285') }) - it('should use > 999T for numbers above 999,000,000,000,000', () => { - const amount1 = 999_000_000_000_001 - - expect(formatCurrency(amount1, 'JPY')).toBe('> 999T JPY') + it('should abbreviate billions', () => { + expect(formatCurrency(12_345_678_901, 'USD')).toBe('$ 12.35B') }) - it('should use < - smallest denomination or < + smallest denomination when amounts are smaller than the smallest denomination', () => { - const amount = 0.000001 - - expect(formatCurrency(amount, 'JPY')).toBe('< 1 JPY') - expect(formatCurrency(amount, 'IQD')).toBe('< 1 IQD') - expect(formatCurrency(amount, 'USD')).toBe('< 0.01 USD') - expect(formatCurrency(amount, 'EUR')).toBe('< 0.01 EUR') - expect(formatCurrency(amount, 'GBP')).toBe('< 0.01 GBP') - expect(formatCurrency(amount, 'BHD')).toBe('< 0.001 BHD') - - // Preserves sign if specified - const amount2 = '+0.000001' - - expect(formatCurrency(amount2, 'JPY')).toBe('< +1 JPY') - expect(formatCurrency(amount2, 'IQD')).toBe('< +1 IQD') - expect(formatCurrency(amount2, 'USD')).toBe('< +0.01 USD') - expect(formatCurrency(amount2, 'EUR')).toBe('< +0.01 EUR') - expect(formatCurrency(amount2, 'GBP')).toBe('< +0.01 GBP') - expect(formatCurrency(amount2, 'BHD')).toBe('< +0.001 BHD') - - const amount3 = -0.000009 - - expect(formatCurrency(amount3, 'JPY')).toBe('< -1 JPY') - expect(formatCurrency(amount3, 'IQD')).toBe('< -1 IQD') - expect(formatCurrency(amount3, 'USD')).toBe('< -0.01 USD') - expect(formatCurrency(amount3, 'EUR')).toBe('< -0.01 EUR') - expect(formatCurrency(amount3, 'GBP')).toBe('< -0.01 GBP') - expect(formatCurrency(amount3, 'BHD')).toBe('< -0.001 BHD') - - const amount4 = '-0.000009' - - expect(formatCurrency(amount4, 'JPY')).toBe('< -1 JPY') - expect(formatCurrency(amount4, 'IQD')).toBe('< -1 IQD') - expect(formatCurrency(amount4, 'USD')).toBe('< -0.01 USD') - expect(formatCurrency(amount4, 'EUR')).toBe('< -0.01 EUR') - expect(formatCurrency(amount4, 'GBP')).toBe('< -0.01 GBP') - expect(formatCurrency(amount4, 'BHD')).toBe('< -0.001 BHD') + it('should abbreviate millions', () => { + expect(formatCurrency(9_589_009.543645, 'EUR')).toBe('€ 9.59M') }) - }) - - describe('formatAmountPrecise', () => { - it('should format amounts without the compact notation', () => { - const tokenDecimals = 18 - - const amount1 = 100_000_000.00001 // 100M - expect(formatAmountPrecise(amount1, tokenDecimals)).toEqual('100,000,000.00001') - const amount2 = 1_000_000_000.00001 // 1B - expect(formatAmountPrecise(amount2, tokenDecimals)).toEqual('1,000,000,000.00001') - - const amount3 = 1_234_567_898.123456789 // 1.235B - expect(formatAmountPrecise(amount3, tokenDecimals)).toEqual('1,234,567,898.1234567') + it('should abbreviate thousands', () => { + expect(formatCurrency(119_589.543645, 'EUR')).toBe('€ 119.59K') }) - it('should preserve the max fraction digits', () => { - const tokenDecimals = 18 - - const amount1 = 0.000001 // < 0.00001 - expect(formatAmountPrecise(amount1, tokenDecimals)).toEqual('0.000001') - - const amount2 = 0.00000123456789 // 14 decimals - expect(formatAmountPrecise(amount2, tokenDecimals)).toEqual('0.00000123456789') // 14 decimals - - const amount3 = 0.00000123456789012345 // 20 decimals - expect(formatAmountPrecise(amount3, tokenDecimals)).toEqual('0.000001234567890123') // 18 decimals + it('should abbreviate a number with more than a given amount of digits', () => { + expect(formatCurrency(1234.12, 'USD', 4)).toBe('$ 1.23K') }) }) }) diff --git a/src/utils/formatNumber.ts b/src/utils/formatNumber.ts index b0cb100983..b5ecd2d1cb 100644 --- a/src/utils/formatNumber.ts +++ b/src/utils/formatNumber.ts @@ -1,107 +1,26 @@ -import memoize from 'lodash/memoize' - -// These follow the guideline of "How to format amounts" -// https://github.com/5afe/safe/wiki/How-to-format-amounts - -const LOWER_LIMIT = 0.00001 -const COMPACT_LIMIT = 99_999_999.5 -const UPPER_LIMIT = 999 * 10 ** 12 -const NO_DECIMALS_LIMIT = 1000 - /** - * Formatter that restricts the upper and lower limit of numbers that can be formatted + * Intl.NumberFormat number formatter that adheres to our style guide * @param number Number to format - * @param formatter Function to format number - * @param minimum Minimum number to format */ -const format = (number: string | number, formatter: (float: number) => string, minimum = LOWER_LIMIT) => { - const float = Number(number) - - if (float === 0) { - return formatter(float) - } - - if (Math.abs(float) < minimum) { - return `< ${formatter(minimum * Math.sign(float))}` - } - - if (float < UPPER_LIMIT) { - return formatter(float) - } - - return `> ${formatter(UPPER_LIMIT)}` -} - -// Universal amount formatting options - -const getNumberFormatNotation = (number: string | number): Intl.NumberFormatOptions['notation'] => { - return Number(number) >= COMPACT_LIMIT ? 'compact' : undefined -} - -const getNumberFormatSignDisplay = (number: string | number): Intl.NumberFormatOptions['signDisplay'] => { - const shouldDisplaySign = typeof number === 'string' ? number.trim().startsWith('+') : Number(number) < 0 - return shouldDisplaySign ? 'exceptZero' : undefined -} - -// Amount formatting options - -const getAmountFormatterMaxFractionDigits = ( - number: string | number, -): Intl.NumberFormatOptions['maximumFractionDigits'] => { +export const formatAmount = (number: string | number, precision = 5, maxLength = 6): string => { const float = Number(number) + if (float === 0) return '0' + if (float === Math.round(float)) precision = 0 + if (Math.abs(float) < 0.00001) return '< 0.00001' - if (float < 1_000) { - return 5 - } - - if (float < 10_000) { - return 4 - } - - if (float < 100_000) { - return 3 - } - - if (float < 1_000_000) { - return 2 - } - - if (float < 10_000_000) { - return 1 - } - - if (float < COMPACT_LIMIT) { - return 0 - } - - // Represents numbers like 767.343M - if (float < UPPER_LIMIT) { - return 3 - } + const fullNum = new Intl.NumberFormat(undefined, { + style: 'decimal', + maximumFractionDigits: precision, + }).format(Number(number)) - return 0 -} + // +3 for the decimal point and the two decimal places + if (fullNum.length <= maxLength + 3) return fullNum -const getAmountFormatterOptions = (number: string | number): Intl.NumberFormatOptions => { - return { - maximumFractionDigits: getAmountFormatterMaxFractionDigits(number), - notation: getNumberFormatNotation(number), - signDisplay: getNumberFormatSignDisplay(number), - } -} - -/** - * Intl.NumberFormat number formatter that adheres to our style guide - * @param number Number to format - */ -export const formatAmount = (number: string | number, precision?: number): string => { - const options = getAmountFormatterOptions(number) - if (precision !== undefined) { - options.maximumFractionDigits = precision - } - const formatter = new Intl.NumberFormat(undefined, options) - - return format(number, formatter.format) + return new Intl.NumberFormat(undefined, { + style: 'decimal', + notation: 'compact', + maximumFractionDigits: 2, + }).format(float) } /** @@ -110,58 +29,10 @@ export const formatAmount = (number: string | number, precision?: number): strin * @param precision Fraction digits to show */ export const formatAmountPrecise = (number: string | number, precision: number): string => { - const float = Number(number) - - const formatter = new Intl.NumberFormat(undefined, { + return new Intl.NumberFormat(undefined, { + style: 'decimal', maximumFractionDigits: precision, - }) - - return formatter.format(float) -} - -// Fiat formatting - -const getMinimumCurrencyDenominator = memoize((currency: string): number => { - const BASE_VALUE = 1 - - const formatter = new Intl.NumberFormat(undefined, { - style: 'currency', - currency, - }) - - const fraction = formatter.formatToParts(BASE_VALUE).find(({ type }) => type === 'fraction') - - // Currencies may not have decimals, i.e. JPY - return fraction ? Number(`0.${'1'.padStart(fraction.value.length, '0')}`) : 1 -}) - -const getCurrencyFormatterMaxFractionDigits = ( - number: string | number, - currency: string, -): Intl.NumberFormatOptions['maximumFractionDigits'] => { - const float = Number(number) - - if (float < NO_DECIMALS_LIMIT) { - const [, decimals] = getMinimumCurrencyDenominator(currency).toString().split('.') - return decimals?.length ?? 0 - } - - if (float >= COMPACT_LIMIT) { - return 3 - } - - return 0 -} - -const getCurrencyFormatterOptions = (number: string | number, currency: string): Intl.NumberFormatOptions => { - return { - maximumFractionDigits: getCurrencyFormatterMaxFractionDigits(number, currency), - notation: getNumberFormatNotation(number), - signDisplay: getNumberFormatSignDisplay(number), - style: 'currency', - currency, - currencyDisplay: 'code', - } + }).format(Number(number)) } /** @@ -169,42 +40,26 @@ const getCurrencyFormatterOptions = (number: string | number, currency: string): * @param number Number to format * @param currency ISO 4217 currency code */ -export const formatCurrency = (number: string | number, currency: string): string => { - // Note: we will be able to achieve the following once the `roundingMode` option is supported - // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#parameters - - const minimum = getMinimumCurrencyDenominator(currency) - - const currencyFormatter = (float: number): string => { - const options = getCurrencyFormatterOptions(number, currency) - const formatter = new Intl.NumberFormat(undefined, options) - - const parts = formatter.formatToParts(float) // Returns an array of objects with `type` and `value` properties +export const formatCurrency = (number: string | number, currency: string, maxLength = 6): string => { + let float = Number(number) - const fraction = parts.find(({ type }) => type === 'fraction') - - const amount = parts - .filter(({ type }) => type !== 'currency' && type !== 'literal') // Remove currency code and whitespace - .map((part) => { - if (float >= 0) { - return part - } - - if (fraction && part.type === 'fraction') { - return { ...part, value: '1'.padStart(fraction.value.length, '0') } - } - - if (!fraction && part.type === 'integer') { - return { ...part, value: minimum.toString() } - } - - return part - }) - .reduce((acc, { value }) => acc + value, '') - .trim() - - return `${amount} ${currency.toUpperCase()}` - } - - return format(number, currencyFormatter, minimum) + let result = new Intl.NumberFormat(undefined, { + style: 'currency', + currency, + currencyDisplay: 'narrowSymbol', + maximumFractionDigits: Math.abs(float) >= 1 || float === 0 ? 0 : 2, + }).format(Number(number)) + + // +1 for the currency symbol + if (result.length > maxLength + 1) { + result = new Intl.NumberFormat(undefined, { + style: 'currency', + currency, + currencyDisplay: 'narrowSymbol', + notation: 'compact', + maximumFractionDigits: 2, + }).format(Number(number)) + } + + return result.replace(/^(\D+)/, '$1 ') } From 224d91395fd584d68a95dfba2048b85cd4c61e33 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Mon, 10 Jun 2024 17:52:56 +0200 Subject: [PATCH 068/154] feat: update EIP 5792 implementation to match updated standard (#3656) --- jest.config.cjs | 4 +- package.json | 8 +- .../components/WcSessionManager/index.tsx | 12 +- src/features/walletconnect/constants.ts | 7 +- .../services/WalletConnectWallet.ts | 10 +- .../__tests__/WalletConnectWallet.test.ts | 2 + src/services/contracts/deployments.ts | 5 + .../safe-wallet-provider/index.test.ts | 173 +++++++++++-- src/services/safe-wallet-provider/index.ts | 69 +++++- .../useSafeWalletProvider.test.tsx | 35 +++ .../useSafeWalletProvider.tsx | 20 +- yarn.lock | 229 +++++++++++++----- 12 files changed, 462 insertions(+), 112 deletions(-) diff --git a/jest.config.cjs b/jest.config.cjs index aaadccb700..8eb1dc0edf 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -26,5 +26,7 @@ const customJestConfig = { // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async module.exports = async () => ({ ...(await createJestConfig(customJestConfig)()), - transformIgnorePatterns: ['node_modules/(?!(uint8arrays|multiformats|@web3-onboard/common)/)'], + transformIgnorePatterns: [ + 'node_modules/(?!(uint8arrays|multiformats|@web3-onboard/common|@walletconnect/(.*)/uint8arrays)/)', + ], }) diff --git a/package.json b/package.json index e38f308cc7..b231eae244 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,6 @@ "node": ">=16" }, "resolutions": { - "@walletconnect/core": "^2.11.2", - "@walletconnect/ethereum-provider": "^2.11.2", "@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", @@ -65,8 +63,8 @@ "@sentry/react": "^7.91.0", "@spindl-xyz/attribution-lite": "^1.4.0", "@truffle/hdwallet-provider": "^2.1.4", - "@walletconnect/utils": "^2.11.3", - "@walletconnect/web3wallet": "^1.10.3", + "@walletconnect/utils": "^2.13.1", + "@walletconnect/web3wallet": "^1.12.1", "@web3-onboard/coinbase": "^2.2.6", "@web3-onboard/core": "^2.21.4", "@web3-onboard/injected-wallets": "^2.10.14", @@ -131,7 +129,7 @@ "@types/react-gtm-module": "^2.0.3", "@types/semver": "^7.3.10", "@typescript-eslint/eslint-plugin": "^7.6.0", - "@walletconnect/types": "^2.11.3", + "@walletconnect/types": "^2.13.1", "cross-env": "^7.0.3", "cypress": "^12.15.0", "cypress-file-upload": "^5.0.8", diff --git a/src/features/walletconnect/components/WcSessionManager/index.tsx b/src/features/walletconnect/components/WcSessionManager/index.tsx index dbc611d422..689c95b11d 100644 --- a/src/features/walletconnect/components/WcSessionManager/index.tsx +++ b/src/features/walletconnect/components/WcSessionManager/index.tsx @@ -43,7 +43,17 @@ const WcSessionManager = ({ sessions, uri }: WcSessionManagerProps) => { setIsLoading(WCLoadingState.APPROVE) try { - await walletConnect.approveSession(sessionProposal, chainId, safeAddress) + await walletConnect.approveSession(sessionProposal, chainId, safeAddress, { + capabilities: JSON.stringify({ + [safeAddress]: { + [`0x${Number(chainId).toString(16)}`]: { + atomicBatch: { + supported: true, + }, + }, + }, + }), + }) // Auto approve future sessions for non-malicious dApps if ( diff --git a/src/features/walletconnect/constants.ts b/src/features/walletconnect/constants.ts index 1f43d957a2..26f0e7ee7a 100644 --- a/src/features/walletconnect/constants.ts +++ b/src/features/walletconnect/constants.ts @@ -21,9 +21,10 @@ export const SAFE_COMPATIBLE_METHODS = [ 'eth_getLogs', 'eth_gasPrice', 'wallet_switchEthereumChain', - 'wallet_sendFunctionCallBundle', - 'wallet_getBundleStatus', - 'wallet_showBundleStatus', + 'wallet_sendCalls', + 'wallet_getCallsStatus', + 'wallet_showCallsStatus', + 'wallet_getCapabilities', 'safe_setSettings', ] diff --git a/src/features/walletconnect/services/WalletConnectWallet.ts b/src/features/walletconnect/services/WalletConnectWallet.ts index bb2036e55d..51b5747c6c 100644 --- a/src/features/walletconnect/services/WalletConnectWallet.ts +++ b/src/features/walletconnect/services/WalletConnectWallet.ts @@ -3,7 +3,7 @@ import { Web3Wallet } from '@walletconnect/web3wallet' import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils' import type Web3WalletType from '@walletconnect/web3wallet' import type { Web3WalletTypes } from '@walletconnect/web3wallet' -import type { SessionTypes } from '@walletconnect/types' +import type { ProposalTypes, SessionTypes } from '@walletconnect/types' import { type JsonRpcResponse } from '@walletconnect/jsonrpc-utils' import uniq from 'lodash/uniq' @@ -107,7 +107,12 @@ class WalletConnectWallet { }) } - public async approveSession(proposal: Web3WalletTypes.SessionProposal, currentChainId: string, safeAddress: string) { + public async approveSession( + proposal: Web3WalletTypes.SessionProposal, + currentChainId: string, + safeAddress: string, + sessionProperties?: ProposalTypes.SessionProperties, + ) { assertWeb3Wallet(this.web3Wallet) const namespaces = this.getNamespaces(proposal, currentChainId, safeAddress) @@ -116,6 +121,7 @@ class WalletConnectWallet { const session = await this.web3Wallet.approveSession({ id: proposal.id, namespaces, + sessionProperties, }) await this.chainChanged(session.topic, currentChainId) diff --git a/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts b/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts index 643a2f873c..d9a33cdc14 100644 --- a/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts +++ b/src/features/walletconnect/services/__tests__/WalletConnectWallet.test.ts @@ -249,6 +249,7 @@ describe('WalletConnectWallet', () => { publicKey: '123', metadata: {} as SignClientTypes.Metadata, }, + pairingTopic: '0x3456', requiredNamespaces: {} as ProposalTypes.RequiredNamespaces, optionalNamespaces: {} as ProposalTypes.OptionalNamespaces, expiryTimestamp: 2, @@ -283,6 +284,7 @@ describe('WalletConnectWallet', () => { id: 1, expiry: 1, relays: [], + pairingTopic: '0x3456', proposer: { publicKey: '123', metadata: {} as SignClientTypes.Metadata, diff --git a/src/services/contracts/deployments.ts b/src/services/contracts/deployments.ts index d1bf681416..181b8ff5b6 100644 --- a/src/services/contracts/deployments.ts +++ b/src/services/contracts/deployments.ts @@ -6,6 +6,7 @@ import { getFallbackHandlerDeployment, getProxyFactoryDeployment, getSignMessageLibDeployment, + getCreateCallDeployment, } from '@safe-global/safe-deployments' import type { SingletonDeployment, DeploymentFilter } from '@safe-global/safe-deployments' import type { ChainInfo, SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' @@ -79,3 +80,7 @@ export const getProxyFactoryContractDeployment = (chainId: string, safeVersion: export const getSignMessageLibContractDeployment = (chainId: string, safeVersion: SafeInfo['version']) => { return _tryDeploymentVersions(getSignMessageLibDeployment, chainId, safeVersion) } + +export const getCreateCallContractDeployment = (chainId: string, safeVersion: SafeInfo['version']) => { + return _tryDeploymentVersions(getCreateCallDeployment, chainId, safeVersion) +} diff --git a/src/services/safe-wallet-provider/index.test.ts b/src/services/safe-wallet-provider/index.test.ts index ef6f52ed0e..3252f21e23 100644 --- a/src/services/safe-wallet-provider/index.test.ts +++ b/src/services/safe-wallet-provider/index.test.ts @@ -1,8 +1,10 @@ // Unit tests for the SafeWalletProvider class +import { faker } from '@faker-js/faker' import { SafeWalletProvider } from '.' +import { ERC20__factory } from '@/types/contracts' const safe = { - safeAddress: '0x123', + safeAddress: faker.finance.ethereumAddress(), chainId: 1, } @@ -69,7 +71,7 @@ describe('SafeWalletProvider', () => { expect(result).toEqual({ id: 1, jsonrpc: '2.0', - result: ['0x123'], + result: [safe.safeAddress], }) }) }) @@ -136,7 +138,7 @@ describe('SafeWalletProvider', () => { const result = await safeWalletProvider.request( 1, - { method: 'personal_sign', params: ['message', '0x123'] } as any, + { method: 'personal_sign', params: ['message', safe.safeAddress] } as any, {} as any, ) @@ -157,7 +159,7 @@ describe('SafeWalletProvider', () => { const result = await safeWalletProvider.request( 1, - { method: 'eth_sign', params: ['0x123', '0x123'] } as any, + { method: 'eth_sign', params: [safe.safeAddress, '0x345'] } as any, {} as any, ) @@ -214,7 +216,7 @@ describe('SafeWalletProvider', () => { const result = await safeWalletProvider.request( 1, - { method: 'personal_sign', params: ['0x123', '0x123'] } as any, + { method: 'eth_sign', params: [safe.safeAddress, '0x123'] } as any, {} as any, ) @@ -238,7 +240,7 @@ describe('SafeWalletProvider', () => { { method, params: [ - '0x123', + safe.safeAddress, { domain: { chainId: 1, @@ -290,7 +292,7 @@ describe('SafeWalletProvider', () => { { method, params: [ - '0x123', + safe.safeAddress, { domain: { chainId: 1, @@ -320,16 +322,23 @@ describe('SafeWalletProvider', () => { const sdk = { send: jest.fn().mockResolvedValue({ safeTxHash: '0x456' }), } + const toAddress = faker.finance.ethereumAddress() const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) const result = await safeWalletProvider.request( 1, - { method: 'eth_sendTransaction', params: [{ from: '0x123', to: '0x123', value: '0x123', gas: 1000 }] } as any, + { + method: 'eth_sendTransaction', + params: [{ from: safe.safeAddress, to: toAddress, value: '0x01', gas: 1000 }], + } as any, appInfo, ) expect(sdk.send).toHaveBeenCalledWith( - { txs: [{ from: '0x123', to: '0x123', value: '0x123', gas: 1000, data: '0x' }], params: { safeTxGas: 1000 } }, + { + txs: [{ from: safe.safeAddress, to: toAddress, value: '0x01', gas: 1000, data: '0x' }], + params: { safeTxGas: 1000 }, + }, appInfo, ) @@ -426,10 +435,15 @@ describe('SafeWalletProvider', () => { } const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + const toAddress = faker.finance.ethereumAddress() + // Send the transaction await safeWalletProvider.request( 1, - { method: 'eth_sendTransaction', params: [{ from: '0x123', to: '0x123', value: '0x123', gas: 1000 }] } as any, + { + method: 'eth_sendTransaction', + params: [{ from: safe.safeAddress, to: toAddress, value: '0x01', gas: 1000 }], + } as any, appInfo, ) @@ -445,15 +459,15 @@ describe('SafeWalletProvider', () => { result: { blockHash: null, blockNumber: null, - from: '0x123', + from: safe.safeAddress, gas: 0, gasPrice: '0x00', hash: '0x777', input: '0x', nonce: 0, - to: '0x123', + to: toAddress, transactionIndex: null, - value: '0x123', + value: '0x01', }, }) }) @@ -495,7 +509,7 @@ describe('SafeWalletProvider', () => { }) describe('EIP-5792', () => { - describe('wallet_sendFunctionCallBundle', () => { + describe('wallet_sendCalls', () => { it('should send a bundle', async () => { const sdk = { send: jest.fn(), @@ -505,15 +519,16 @@ describe('SafeWalletProvider', () => { const params = [ { chainId: 1, - from: '0x1234', + version: '1.0', + from: faker.finance.ethereumAddress(), calls: [ - { gas: 1000, data: '0x123', to: '0x123', value: '0x123' }, - { gas: 1000, data: '0x456', to: '0x789', value: '0x1' }, + { data: '0x123', to: faker.finance.ethereumAddress(), value: '0x123' }, + { data: '0x456', to: faker.finance.ethereumAddress(), value: '0x1' }, ], }, ] - await safeWalletProvider.request(1, { method: 'wallet_sendFunctionCallBundle', params } as any, appInfo) + await safeWalletProvider.request(1, { method: 'wallet_sendCalls', params } as any, appInfo) expect(sdk.send).toHaveBeenCalledWith( { @@ -529,9 +544,79 @@ describe('SafeWalletProvider', () => { }, ) }) + + it('test contract deployment calls and calls without data / value', async () => { + const fakeCreateCallLib = faker.finance.ethereumAddress() + const sdk = { + send: jest.fn(), + getCreateCallTransaction: jest.fn().mockImplementation((data: string) => { + return { + to: fakeCreateCallLib, + data, + value: '0', + } + }), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + const transferReceiver = faker.finance.ethereumAddress() + const erc20Address = faker.finance.ethereumAddress() + const erc20TransferData = ERC20__factory.createInterface().encodeFunctionData('transfer', [ + transferReceiver, + '100', + ]) + const nativeTransferTo = faker.finance.ethereumAddress() + + const params = [ + { + chainId: 1, + version: '1.0', + from: safe.safeAddress, + calls: [ + { data: '0x1234' }, + { data: '0x', to: nativeTransferTo, value: '0x1' }, + { + to: erc20Address, + data: erc20TransferData, + }, + ], + }, + ] + + await safeWalletProvider.request(1, { method: 'wallet_sendCalls', params } as any, appInfo) + + expect(sdk.send).toHaveBeenCalledWith( + { + txs: [ + { + to: fakeCreateCallLib, + data: '0x1234', + value: '0', + }, + { + to: nativeTransferTo, + data: '0x', + value: '0x1', + }, + { + to: erc20Address, + data: erc20TransferData, + value: '0', + }, + ], + params: { safeTxGas: 0 }, + }, + { + description: 'test', + iconUrl: 'test', + id: 1, + name: 'test', + url: 'test', + }, + ) + }) }) - describe('wallet_getBundleStatus', () => { + describe('wallet_getCallsStatus', () => { it('should look up a tx by txHash', async () => { const sdk = { getBySafeTxHash: jest.fn().mockResolvedValue({ @@ -549,7 +634,7 @@ describe('SafeWalletProvider', () => { const params = ['0x123'] - await safeWalletProvider.request(1, { method: 'wallet_getBundleStatus', params } as any, appInfo) + await safeWalletProvider.request(1, { method: 'wallet_getCallsStatus', params } as any, appInfo) expect(sdk.getBySafeTxHash).toHaveBeenCalledWith(params[0]) expect(sdk.proxy).toHaveBeenCalledWith('eth_getTransactionReceipt', params) @@ -571,14 +656,14 @@ describe('SafeWalletProvider', () => { const params = ['0x123'] - await safeWalletProvider.request(1, { method: 'wallet_getBundleStatus', params } as any, appInfo) + await safeWalletProvider.request(1, { method: 'wallet_getCallsStatus', params } as any, appInfo) expect(sdk.getBySafeTxHash).toHaveBeenCalledWith(params[0]) expect(sdk.proxy).not.toHaveBeenCalled() }) }) - describe('wallet_showBundleStatus', () => { + describe('wallet_showCallsStatus', () => { it('should return the bundle status', async () => { const sdk = { showTxStatus: jest.fn(), @@ -587,10 +672,52 @@ describe('SafeWalletProvider', () => { const params = ['0x123'] - await safeWalletProvider.request(1, { method: 'wallet_showBundleStatus', params } as any, appInfo) + await safeWalletProvider.request(1, { method: 'wallet_showCallsStatus', params } as any, appInfo) expect(sdk.showTxStatus).toHaveBeenCalledWith(params[0]) }) }) + + describe('wallet_getCapabilities', () => { + it('should return atomic batch for the current chain', async () => { + const sdk = { + showTxStatus: jest.fn(), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const params = [safe.safeAddress] + + const result = await safeWalletProvider.request(1, { method: 'wallet_getCapabilities', params } as any, appInfo) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: { + ['0x1']: { + atomicBatch: { + supported: true, + }, + }, + }, + }) + }) + + it('should return an empty object if the safe address does not match', async () => { + const sdk = { + showTxStatus: jest.fn(), + } + const safeWalletProvider = new SafeWalletProvider(safe, sdk as any) + + const params = [faker.finance.ethereumAddress()] + + const result = await safeWalletProvider.request(1, { method: 'wallet_getCapabilities', params } as any, appInfo) + + expect(result).toEqual({ + id: 1, + jsonrpc: '2.0', + result: {}, + }) + }) + }) }) }) diff --git a/src/services/safe-wallet-provider/index.ts b/src/services/safe-wallet-provider/index.ts index ac61be0915..58623772bb 100644 --- a/src/services/safe-wallet-provider/index.ts +++ b/src/services/safe-wallet-provider/index.ts @@ -12,6 +12,8 @@ type SafeSettings = { offChainSigning?: boolean } +type GetCapabilitiesResult = Record<`0x${string}`, Record> + export type AppInfo = { id: number name: string @@ -32,6 +34,11 @@ export type WalletSDK = { switchChain: (chainId: string, appInfo: AppInfo) => Promise setSafeSettings: (safeSettings: SafeSettings) => SafeSettings proxy: (method: string, params?: Array | Record) => Promise + getCreateCallTransaction: (data: string) => { + to: string + data: string + value: '0' + } } interface RpcRequest { @@ -128,28 +135,34 @@ export class SafeWalletProvider { // EIP-5792 // @see https://eips.ethereum.org/EIPS/eip-5792 - case 'wallet_sendFunctionCallBundle': { - return this.wallet_sendFunctionCallBundle( + case 'wallet_sendCalls': { + return this.wallet_sendCalls( ...(params as [ { + version: string chainId: string from: string - calls: Array<{ gas: string; data: string; to?: string; value?: string }> + calls: Array<{ data: string; to?: string; value?: string }> + capabilities?: Record | undefined }, ]), appInfo, ) } - case 'wallet_getBundleStatus': { - return this.wallet_getBundleStatus(...(params as [string])) + case 'wallet_getCallsStatus': { + return this.wallet_getCallsStatus(...(params as [string])) } - case 'wallet_showBundleStatus': { - this.wallet_showBundleStatus(...(params as [string])) + case 'wallet_showCallsStatus': { + this.wallet_showCallsStatus(...(params as [string])) return null } + case 'wallet_getCapabilities': { + return this.wallet_getCapabilities(...(params as [string])) + } + // Safe proprietary methods case 'safe_setSettings': { return this.safe_setSettings(...(params as [SafeSettings])) @@ -314,17 +327,36 @@ export class SafeWalletProvider { // EIP-5792 // @see https://eips.ethereum.org/EIPS/eip-5792 - async wallet_sendFunctionCallBundle( + async wallet_sendCalls( bundle: { chainId: string from: string - calls: Array<{ gas: string; data: string; to?: string; value?: string }> + calls: Array<{ data?: string; to?: string; value?: string }> }, appInfo: AppInfo, ): Promise { + const txs = bundle.calls.map((call) => { + if (!call.to && !call.value && !call.data) { + throw new RpcError(RpcErrorCode.INVALID_PARAMS, 'Invalid call parameters.') + } + if (!call.to && !call.value && call.data) { + // If only data is provided the call is a contract deployment + // We have to use the CreateCall lib + return this.sdk.getCreateCallTransaction(call.data) + } + if (!call.to) { + // For all non-contract deployments we need a to address + throw new RpcError(RpcErrorCode.INVALID_PARAMS, 'Invalid call parameters.') + } + return { + to: call.to, + data: call.data ?? '0x', + value: call.value ?? '0', + } + }) const { safeTxHash } = await this.sdk.send( { - txs: bundle.calls, + txs, params: { safeTxGas: 0 }, }, appInfo, @@ -332,7 +364,7 @@ export class SafeWalletProvider { return safeTxHash } - async wallet_getBundleStatus(safeTxHash: string): Promise<{ + async wallet_getCallsStatus(safeTxHash: string): Promise<{ calls: Array<{ status: BundleStatus receipt: { @@ -381,11 +413,24 @@ export class SafeWalletProvider { calls: calls.map(() => callStatus), } } - async wallet_showBundleStatus(txHash: string): Promise { + async wallet_showCallsStatus(txHash: string): Promise { this.sdk.showTxStatus(txHash) return null } + async wallet_getCapabilities(walletAddress: string): Promise { + if (walletAddress === this.safe.safeAddress) { + return { + [`0x${this.safe.chainId.toString(16)}`]: { + atomicBatch: { + supported: true, + }, + }, + } + } + return {} + } + // Safe proprietary methods async safe_setSettings(settings: SafeSettings): Promise { return this.sdk.setSafeSettings(settings) diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx index 399b616ecf..13043011f7 100644 --- a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx +++ b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx @@ -11,6 +11,9 @@ import useSafeWalletProvider, { _useTxFlowApi } from './useSafeWalletProvider' import { SafeWalletProvider } from '.' import { makeStore } from '@/store' import * as messages from '@/utils/safe-messages' +import { faker } from '@faker-js/faker' +import { Interface } from 'ethers' +import { getCreateCallDeployment } from '@safe-global/safe-deployments' const appInfo = { id: 1, @@ -45,6 +48,7 @@ describe('useSafeWalletProvider', () => { value: '0x1234567890000000000000000000000000000000', }, deployed: true, + version: '1.3.0', } as unknown as ExtendedSafeInfo, }, }, @@ -64,6 +68,7 @@ describe('useSafeWalletProvider', () => { expect(result.current?.getBySafeTxHash).toBeDefined() expect(result.current?.switchChain).toBeDefined() expect(result.current?.proxy).toBeDefined() + expect(result.current?.getCreateCallTransaction).toBeDefined() }) it('should open signing window for off-chain messages', () => { @@ -354,4 +359,34 @@ describe('useSafeWalletProvider', () => { }, }) }) + + it('should create CreateCall lib transactions', () => { + const createCallDeployment = getCreateCallDeployment({ version: '1.3.0', network: '1' }) + const createCallInterface = new Interface(['function performCreate(uint256,bytes)']) + const safeAddress = faker.finance.ethereumAddress() + const { result } = renderHook(() => _useTxFlowApi('1', safeAddress), { + initialReduxState: { + safeInfo: { + loading: false, + error: undefined, + data: { + chainId: '1', + address: { + value: safeAddress, + }, + deployed: true, + version: '1.3.0', + } as unknown as ExtendedSafeInfo, + }, + }, + }) + + const tx = result.current?.getCreateCallTransaction('0x1234') + + expect(tx).toEqual({ + to: createCallDeployment?.networkAddresses['1'], + value: '0', + data: createCallInterface.encodeFunctionData('performCreate', [0, '0x1234']), + }) + }) }) diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.tsx index 6833de0a0f..cd70840652 100644 --- a/src/services/safe-wallet-provider/useSafeWalletProvider.tsx +++ b/src/services/safe-wallet-provider/useSafeWalletProvider.tsx @@ -14,7 +14,7 @@ import { Methods } from '@safe-global/safe-apps-sdk' import type { EIP712TypedData, SafeSettings } from '@safe-global/safe-apps-sdk' import { useWeb3ReadOnly } from '@/hooks/wallets/web3' import { getTransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' -import { getAddress } from 'ethers' +import { Interface, getAddress } from 'ethers' import { AppRoutes } from '@/config/routes' import useChains, { useCurrentChain } from '@/hooks/useChains' import { NotificationMessages, showNotification } from './notifications' @@ -22,6 +22,7 @@ import { SignMessageOnChainFlow } from '@/components/tx-flow/flows' import { useAppSelector } from '@/store' import { selectOnChainSigning } from '@/store/settingsSlice' import { isOffchainEIP1271Supported } from '@/utils/safe-messages' +import { getCreateCallContractDeployment } from '../contracts/deployments' export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK | undefined => { const { safe } = useSafeInfo() @@ -205,6 +206,23 @@ export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK | async proxy(method, params) { return web3ReadOnly?.send(method, params ?? []) }, + + getCreateCallTransaction(data) { + const createCallDeployment = getCreateCallContractDeployment(safe.chainId, safe.version) + if (!createCallDeployment) { + throw new Error('No CreateCall deployment found for chain and safe version') + } + const createCallAddress = createCallDeployment.networkAddresses[safe.chainId] + + const createCallInterface = new Interface(createCallDeployment.abi) + const callData = createCallInterface.encodeFunctionData('performCreate', ['0', data]) + + return { + to: createCallAddress, + data: callData, + value: '0', + } + }, } }, [chainId, safeAddress, safe, currentChain, onChainSigning, settings, setTxFlow, configs, router, web3ReadOnly]) } diff --git a/yarn.lock b/yarn.lock index 2e191e53b8..8e48dd9837 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4488,7 +4488,7 @@ "@stablelib/constant-time" "^1.0.1" "@stablelib/wipe" "^1.0.1" -"@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2": +"@stablelib/random@1.0.2", "@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@stablelib/random/-/random-1.0.2.tgz#2dece393636489bf7e19c51229dd7900eddf742c" integrity sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w== @@ -4519,7 +4519,7 @@ resolved "https://registry.yarnpkg.com/@stablelib/wipe/-/wipe-1.0.1.tgz#d21401f1d59ade56a62e139462a97f104ed19a36" integrity sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg== -"@stablelib/x25519@^1.0.3": +"@stablelib/x25519@1.0.3", "@stablelib/x25519@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@stablelib/x25519/-/x25519-1.0.3.tgz#13c8174f774ea9f3e5e42213cbf9fc68a3c7b7fd" integrity sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw== @@ -6537,7 +6537,53 @@ events "^3.3.0" isomorphic-unfetch "^3.1.0" -"@walletconnect/core@2.11.2", "@walletconnect/core@2.11.3", "@walletconnect/core@^2.10.1", "@walletconnect/core@^2.11.2": +"@walletconnect/core@2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.11.2.tgz#35286be92c645fa461fecc0dfe25de9f076fca8f" + integrity sha512-bB4SiXX8hX3/hyBfVPC5gwZCXCl+OPj+/EDVM71iAO3TDsh78KPbrVAbDnnsbHzZVHlsMohtXX3j5XVsheN3+g== + dependencies: + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-provider" "1.0.13" + "@walletconnect/jsonrpc-types" "1.0.3" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.14" + "@walletconnect/keyvaluestorage" "^1.1.1" + "@walletconnect/logger" "^2.0.1" + "@walletconnect/relay-api" "^1.0.9" + "@walletconnect/relay-auth" "^1.0.4" + "@walletconnect/safe-json" "^1.0.2" + "@walletconnect/time" "^1.0.2" + "@walletconnect/types" "2.11.2" + "@walletconnect/utils" "2.11.2" + events "^3.3.0" + isomorphic-unfetch "3.1.0" + lodash.isequal "4.5.0" + uint8arrays "^3.1.0" + +"@walletconnect/core@2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.13.1.tgz#a59646e39a5beaa3f3551d129af43cd404cf4faf" + integrity sha512-h0MSYKJu9i1VEs5koCTT7c5YeQ1Kj0ncTFiMqANbDnB1r3mBulXn+FKtZ2fCmf1j7KDpgluuUzpSs+sQfPcv4Q== + dependencies: + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.14" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + "@walletconnect/relay-api" "1.0.10" + "@walletconnect/relay-auth" "1.0.4" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.13.1" + "@walletconnect/utils" "2.13.1" + events "3.3.0" + isomorphic-unfetch "3.1.0" + lodash.isequal "4.5.0" + uint8arrays "3.1.0" + +"@walletconnect/core@^2.10.1": version "2.11.3" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.11.3.tgz#c81855722cb9afd411f91f5345c7874f48bade0b" integrity sha512-/9m4EqiggFUwkQDv5PDWbcTI+yCVnBd/iYW5iIHEkivg2/mnBr2bQz2r/vtPjp19r/ZK62Dx0+UN3U+BWP8ulQ== @@ -6583,7 +6629,7 @@ "@walletconnect/utils" "2.11.2" events "^3.3.0" -"@walletconnect/events@^1.0.1": +"@walletconnect/events@1.0.1", "@walletconnect/events@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/events/-/events-1.0.1.tgz#2b5f9c7202019e229d7ccae1369a9e86bda7816c" integrity sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ== @@ -6600,6 +6646,15 @@ "@walletconnect/time" "^1.0.2" tslib "1.14.1" +"@walletconnect/heartbeat@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz#e8dc5179db7769950c6f9cf59b23516d9b95227d" + integrity sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw== + dependencies: + "@walletconnect/events" "^1.0.1" + "@walletconnect/time" "^1.0.2" + events "^3.3.0" + "@walletconnect/jsonrpc-http-connection@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.7.tgz#a6973569b8854c22da707a759d241e4f5c2d5a98" @@ -6619,6 +6674,15 @@ "@walletconnect/safe-json" "^1.0.2" tslib "1.14.1" +"@walletconnect/jsonrpc-provider@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz#696f3e3b6d728b361f2e8b853cfc6afbdf2e4e3e" + integrity sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow== + dependencies: + "@walletconnect/jsonrpc-utils" "^1.0.8" + "@walletconnect/safe-json" "^1.0.2" + events "^3.3.0" + "@walletconnect/jsonrpc-types@1.0.3", "@walletconnect/jsonrpc-types@^1.0.2", "@walletconnect/jsonrpc-types@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.3.tgz#65e3b77046f1a7fa8347ae02bc1b841abe6f290c" @@ -6627,6 +6691,14 @@ keyvaluestorage-interface "^1.0.0" tslib "1.14.1" +"@walletconnect/jsonrpc-types@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz#ce1a667d79eadf2a2d9d002c152ceb68739c230c" + integrity sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ== + dependencies: + events "^3.3.0" + keyvaluestorage-interface "^1.0.0" + "@walletconnect/jsonrpc-utils@1.0.8", "@walletconnect/jsonrpc-utils@^1.0.6", "@walletconnect/jsonrpc-utils@^1.0.7", "@walletconnect/jsonrpc-utils@^1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz#82d0cc6a5d6ff0ecc277cb35f71402c91ad48d72" @@ -6646,7 +6718,7 @@ events "^3.3.0" ws "^7.5.1" -"@walletconnect/keyvaluestorage@^1.1.1": +"@walletconnect/keyvaluestorage@1.1.1", "@walletconnect/keyvaluestorage@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz#dd2caddabfbaf80f6b8993a0704d8b83115a1842" integrity sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA== @@ -6655,7 +6727,15 @@ idb-keyval "^6.2.1" unstorage "^1.9.0" -"@walletconnect/logger@2.0.1", "@walletconnect/logger@^2.0.1": +"@walletconnect/logger@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@walletconnect/logger/-/logger-2.1.2.tgz#813c9af61b96323a99f16c10089bfeb525e2a272" + integrity sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw== + dependencies: + "@walletconnect/safe-json" "^1.0.2" + pino "7.11.0" + +"@walletconnect/logger@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/logger/-/logger-2.0.1.tgz#7f489b96e9a1ff6bf3e58f0fbd6d69718bf844a8" integrity sha512-SsTKdsgWm+oDTBeNE/zHxxr5eJfZmE9/5yp/Ku+zJtcTAjELb3DXueWkDXmE9h8uHIbJzIb5wj5lPdzyrjT6hQ== @@ -6688,6 +6768,13 @@ "@walletconnect/modal-core" "2.6.2" "@walletconnect/modal-ui" "2.6.2" +"@walletconnect/relay-api@1.0.10": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.10.tgz#5aef3cd07c21582b968136179aa75849dcc65499" + integrity sha512-tqrdd4zU9VBNqUaXXQASaexklv6A54yEyQQEXYOCr+Jz8Ket0dmPBDyg19LVSNUN2cipAghQc45/KVmfFJ0cYw== + dependencies: + "@walletconnect/jsonrpc-types" "^1.0.2" + "@walletconnect/relay-api@^1.0.9": version "1.0.9" resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.9.tgz#f8c2c3993dddaa9f33ed42197fc9bfebd790ecaf" @@ -6696,7 +6783,7 @@ "@walletconnect/jsonrpc-types" "^1.0.2" tslib "1.14.1" -"@walletconnect/relay-auth@^1.0.4": +"@walletconnect/relay-auth@1.0.4", "@walletconnect/relay-auth@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@walletconnect/relay-auth/-/relay-auth-1.0.4.tgz#0b5c55c9aa3b0ef61f526ce679f3ff8a5c4c2c7c" integrity sha512-kKJcS6+WxYq5kshpPaxGHdwf5y98ZwbfuS4EE/NkQzqrDFm5Cj+dP8LofzWvjrrLkZq7Afy7WrQMXdLy8Sx7HQ== @@ -6708,7 +6795,7 @@ tslib "1.14.1" uint8arrays "^3.0.0" -"@walletconnect/safe-json@^1.0.1", "@walletconnect/safe-json@^1.0.2": +"@walletconnect/safe-json@1.0.2", "@walletconnect/safe-json@^1.0.1", "@walletconnect/safe-json@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/safe-json/-/safe-json-1.0.2.tgz#7237e5ca48046e4476154e503c6d3c914126fa77" integrity sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA== @@ -6730,22 +6817,22 @@ "@walletconnect/utils" "2.11.2" events "^3.3.0" -"@walletconnect/sign-client@2.11.3": - version "2.11.3" - resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.11.3.tgz#3ea7b3acf92ee31cc42b45d42e66c44b4720b28b" - integrity sha512-JVjLTxN/3NjMXv5zalSGKuSYLRyU2yX6AWEdq17cInlrwODpbWZr6PS1uxMWdH4r90DXBLhdtwDbEq/pfd0BPg== +"@walletconnect/sign-client@2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.13.1.tgz#7bdc9226218fd33caf3aef69dff0b4140abc7fa8" + integrity sha512-e+dcqcLsedB4ZjnePFM5Cy8oxu0dyz5iZfhfKH/MOrQV/hyhZ+hJwh4MmkO2QyEu2PERKs9o2Uc6x8RZdi0UAQ== dependencies: - "@walletconnect/core" "2.11.3" - "@walletconnect/events" "^1.0.1" - "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/core" "2.13.1" + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" "@walletconnect/jsonrpc-utils" "1.0.8" - "@walletconnect/logger" "^2.0.1" - "@walletconnect/time" "^1.0.2" - "@walletconnect/types" "2.11.3" - "@walletconnect/utils" "2.11.3" - events "^3.3.0" + "@walletconnect/logger" "2.1.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.13.1" + "@walletconnect/utils" "2.13.1" + events "3.3.0" -"@walletconnect/time@^1.0.2": +"@walletconnect/time@1.0.2", "@walletconnect/time@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/time/-/time-1.0.2.tgz#6c5888b835750ecb4299d28eecc5e72c6d336523" integrity sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g== @@ -6764,7 +6851,7 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" -"@walletconnect/types@2.11.3", "@walletconnect/types@^2.11.3": +"@walletconnect/types@2.11.3": version "2.11.3" resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.11.3.tgz#8ce43cb77e8fd9d5269847cdd73bcfa7cce7dd1a" integrity sha512-JY4wA9MVosDW9dcJMTpnwliste0aJGJ1X6Q4ulLsQsgWRSEBRkLila0oUT01TDBW9Yq8uUp7uFOUTaKx6KWVAg== @@ -6776,6 +6863,18 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" +"@walletconnect/types@2.13.1", "@walletconnect/types@^2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.13.1.tgz#393e3bd4d60a755f3a70cbe769b58cf153450310" + integrity sha512-CIrdt66d38xdunGCy5peOOP17EQkCEGKweXc3+Gn/RWeSiRU35I7wjC/Bp4iWcgAQ6iBTZv4jGGST5XyrOp+Pg== + dependencies: + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + events "3.3.0" + "@walletconnect/universal-provider@2.11.2": version "2.11.2" resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.11.2.tgz#bec3038f51445d707bbec75f0cb8af0a1f1e04db" @@ -6811,7 +6910,7 @@ query-string "7.1.3" uint8arrays "^3.1.0" -"@walletconnect/utils@2.11.3", "@walletconnect/utils@^2.10.1", "@walletconnect/utils@^2.11.3": +"@walletconnect/utils@2.11.3", "@walletconnect/utils@^2.10.1": version "2.11.3" resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.11.3.tgz#3731809b54902655cf202e0bf0e8f268780e8b54" integrity sha512-jsdNkrl/IcTkzWFn0S2d0urzBXg6RxVJtUYRsUx3qI3wzOGiABP9ui3yiZ3SgZOv9aRe62PaNp1qpbYZ+zPb8Q== @@ -6831,28 +6930,48 @@ query-string "7.1.3" uint8arrays "^3.1.0" -"@walletconnect/web3wallet@^1.10.3": - version "1.10.3" - resolved "https://registry.yarnpkg.com/@walletconnect/web3wallet/-/web3wallet-1.10.3.tgz#8195308757bd298ccc9caa6e3fe9f4ff82b94607" - integrity sha512-1Dr2P8KIDCqEWZ+s4coKGJz/+pj87ogFs+icPDXPu9QpzTgY5Y1WSzuAHaqoY5gTlL7WS58YP49s0E7iacUz4g== +"@walletconnect/utils@2.13.1", "@walletconnect/utils@^2.13.1": + version "2.13.1" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.13.1.tgz#f44e81028754c6e056dba588ad9b9fa5ad047645" + integrity sha512-EcooXXlqy5hk9hy/nK2wBF/qxe7HjH0K8ZHzjKkXRkwAE5pCvy0IGXIXWmUR9sw8LFJEqZyd8rZdWLKNUe8hqA== + dependencies: + "@stablelib/chacha20poly1305" "1.0.1" + "@stablelib/hkdf" "1.0.1" + "@stablelib/random" "1.0.2" + "@stablelib/sha256" "1.0.1" + "@stablelib/x25519" "1.0.3" + "@walletconnect/relay-api" "1.0.10" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.13.1" + "@walletconnect/window-getters" "1.0.1" + "@walletconnect/window-metadata" "1.0.1" + detect-browser "5.3.0" + query-string "7.1.3" + uint8arrays "3.1.0" + +"@walletconnect/web3wallet@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@walletconnect/web3wallet/-/web3wallet-1.12.1.tgz#efe7863c6518b2262bca1ea01650222986963cc4" + integrity sha512-34h7UkWjZvZdtCc/t6tZCSBPjDzJbfG1+OPkJ6FiD1KJP+a0wSwuI7l4LliGgvAdsXfrM+sn3ZEWVWy62zeRDA== dependencies: "@walletconnect/auth-client" "2.1.2" - "@walletconnect/core" "2.11.3" - "@walletconnect/jsonrpc-provider" "1.0.13" + "@walletconnect/core" "2.13.1" + "@walletconnect/jsonrpc-provider" "1.0.14" "@walletconnect/jsonrpc-utils" "1.0.8" - "@walletconnect/logger" "2.0.1" - "@walletconnect/sign-client" "2.11.3" - "@walletconnect/types" "2.11.3" - "@walletconnect/utils" "2.11.3" + "@walletconnect/logger" "2.1.2" + "@walletconnect/sign-client" "2.13.1" + "@walletconnect/types" "2.13.1" + "@walletconnect/utils" "2.13.1" -"@walletconnect/window-getters@^1.0.1": +"@walletconnect/window-getters@1.0.1", "@walletconnect/window-getters@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-getters/-/window-getters-1.0.1.tgz#f36d1c72558a7f6b87ecc4451fc8bd44f63cbbdc" integrity sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q== dependencies: tslib "1.14.1" -"@walletconnect/window-metadata@^1.0.1": +"@walletconnect/window-metadata@1.0.1", "@walletconnect/window-metadata@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz#2124f75447b7e989e4e4e1581d55d25bc75f7be5" integrity sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA== @@ -10949,7 +11068,7 @@ eventemitter3@^4.0.7: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.0.0, events@^3.2.0, events@^3.3.0: +events@3.3.0, events@^3.0.0, events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -17584,16 +17703,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -17681,14 +17791,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -18574,6 +18677,13 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== +uint8arrays@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.0.tgz#8186b8eafce68f28bd29bd29d683a311778901e2" + integrity sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog== + dependencies: + multiformats "^9.4.2" + uint8arrays@^2.0.5, uint8arrays@^2.1.2: version "2.1.10" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-2.1.10.tgz#34d023c843a327c676e48576295ca373c56e286a" @@ -19909,7 +20019,7 @@ workbox-window@7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -19927,15 +20037,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From e99b8ec7c894507320c718d4d3987839a04b53bd Mon Sep 17 00:00:00 2001 From: James Mealy Date: Tue, 11 Jun 2024 06:40:12 +0100 Subject: [PATCH 069/154] Feat: allow custom recovery delay (#3728) * feat: allow custom recovery delay * add validation and fix bugs * align expiry input and delay input * fix: custom delay not being applied * add utils * use utils in recovery review * fix: text cut off in mobile * remove unused code * Improve review window explanation * fix typo --- src/components/tx-flow/common/constants.ts | 2 +- .../UpsertRecoveryFlowReview.tsx | 27 ++-- .../UpsertRecoveryFlowSettings.tsx | 126 ++++++++++-------- .../tx-flow/flows/UpsertRecovery/index.tsx | 8 +- .../UpsertRecovery/useRecoveryPeriods.ts | 41 ++++-- .../tx-flow/flows/UpsertRecovery/utils.ts | 11 ++ 6 files changed, 136 insertions(+), 79 deletions(-) create mode 100644 src/components/tx-flow/flows/UpsertRecovery/utils.ts diff --git a/src/components/tx-flow/common/constants.ts b/src/components/tx-flow/common/constants.ts index ecd6b7a52f..30634cb8ca 100644 --- a/src/components/tx-flow/common/constants.ts +++ b/src/components/tx-flow/common/constants.ts @@ -2,6 +2,6 @@ export const TOOLTIP_TITLES = { THRESHOLD: 'The threshold of a Safe Account specifies how many signers need to confirm a Safe Account transaction before it can be executed.', REVIEW_WINDOW: - 'A period that begins after a recovery submitted on-chain, during which the Safe Account signers can review the proposal and cancel it before it is executable.', + 'A period that begins after a recovery is submitted on-chain, during which the Safe Account signers can review the proposal and cancel it before it is executable.', PROPOSAL_EXPIRY: 'A period after which the recovery proposal will expire and can no longer be executed.', } as const diff --git a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx index 5f498ea19d..169dcfa17e 100644 --- a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx +++ b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx @@ -20,6 +20,7 @@ import { UpsertRecoveryFlowFields } from '.' import { TOOLTIP_TITLES } from '../../common/constants' import { useRecoveryPeriods } from './useRecoveryPeriods' import type { UpsertRecoveryFlowProps } from '.' +import { isCustomDelaySelected } from './utils' enum AddressType { EOA = 'EOA', @@ -37,7 +38,11 @@ const getAddressType = async (address: string, chainId: string) => { return AddressType.Other } -const onSubmit = async (isEdit: boolean, params: UpsertRecoveryFlowProps, chainId: string) => { +const onSubmit = async ( + isEdit: boolean, + params: Omit, + chainId: string, +) => { const addressType = await getAddressType(params.recoverer, chainId) const creationEvent = isEdit ? RECOVERY_EVENTS.SUBMIT_RECOVERY_EDIT : RECOVERY_EVENTS.SUBMIT_RECOVERY_CREATE const settings = `delay_${params.delay},expiry_${params.expiry},type_${addressType}` @@ -56,11 +61,15 @@ export function UpsertRecoveryFlowReview({ const web3ReadOnly = useWeb3ReadOnly() const { safe, safeAddress } = useSafeInfo() const { setSafeTx, safeTxError, setSafeTxError } = useContext(SafeTxContext) - const periods = useRecoveryPeriods() - const recoverer = params[UpsertRecoveryFlowFields.recoverer] - const delay = periods.delay.find(({ value }) => value === params[UpsertRecoveryFlowFields.delay])!.label - const expiry = periods.expiration.find(({ value }) => value === params[UpsertRecoveryFlowFields.expiry])!.label + + const { recoverer, expiry, delay, customDelay, selectedDelay } = params + const isCustomDelay = isCustomDelaySelected(selectedDelay) + + const expiryLabel = periods.expiration.find(({ value }) => value === params[UpsertRecoveryFlowFields.expiry])!.label + const delayLabel = isCustomDelay + ? `${customDelay} days` + : periods.delay.find(({ value }) => value === selectedDelay)?.label useEffect(() => { if (!web3ReadOnly) { @@ -90,7 +99,7 @@ export function UpsertRecoveryFlowReview({ const isEdit = !!moduleAddress return ( - onSubmit(isEdit, params, safe.chainId)}> + onSubmit(isEdit, { recoverer, expiry, delay }, safe.chainId)}> This transaction will {moduleAddress ? 'update' : 'enable'} the Account recovery feature once executed. @@ -117,10 +126,10 @@ export function UpsertRecoveryFlowReview({ } > - {delay} + {delayLabel} - {expiry !== '0' && ( + {expiryLabel !== '0' && ( @@ -139,7 +148,7 @@ export function UpsertRecoveryFlowReview({ } > - {expiry} + {expiryLabel} )} diff --git a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx index 0c470cd158..0682285e35 100644 --- a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx +++ b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowSettings.tsx @@ -13,17 +13,17 @@ import { FormControlLabel, Tooltip, Alert, + Box, } from '@mui/material' import ExpandLessIcon from '@mui/icons-material/ExpandLess' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { useForm, FormProvider, Controller } from 'react-hook-form' import { useState } from 'react' -import type { TextFieldProps } from '@mui/material' import type { ReactElement } from 'react' import TxCard from '../../common/TxCard' -import { UpsertRecoveryFlowFields } from '.' import { useRecoveryPeriods } from './useRecoveryPeriods' +import { UpsertRecoveryFlowFields, type UpsertRecoveryFlowProps } from '.' import AddressBookInput from '@/components/common/AddressBookInput' import { sameAddress } from '@/utils/addresses' import useSafeInfo from '@/hooks/useSafeInfo' @@ -33,11 +33,12 @@ import ExternalLink from '@/components/common/ExternalLink' import { HelpCenterArticle, HelperCenterArticleTitles } from '@/config/constants' import { TOOLTIP_TITLES } from '../../common/constants' import Track from '@/components/common/Track' -import type { UpsertRecoveryFlowProps } from '.' import type { RecoveryStateItem } from '@/features/recovery/services/recovery-state' import commonCss from '@/components/tx-flow/common/styles.module.css' import css from './styles.module.css' +import NumberField from '@/components/common/NumberField' +import { getDelay, isCustomDelaySelected } from './utils' export function UpsertRecoveryFlowSettings({ params, @@ -59,8 +60,12 @@ export function UpsertRecoveryFlowSettings({ }) const recoverer = formMethods.watch(UpsertRecoveryFlowFields.recoverer) - const delay = formMethods.watch(UpsertRecoveryFlowFields.delay) const expiry = formMethods.watch(UpsertRecoveryFlowFields.expiry) + const selectedDelay = formMethods.watch(UpsertRecoveryFlowFields.selectedDelay) + const customDelay = formMethods.watch(UpsertRecoveryFlowFields.customDelay) + const customDelayState = formMethods.getFieldState(UpsertRecoveryFlowFields.customDelay) + + const delay = getDelay(customDelay, selectedDelay) // RHF's dirty check is tempermental with our address input dropdown const isDirty = delayModifier @@ -77,17 +82,28 @@ export function UpsertRecoveryFlowSettings({ } } + const validateCustomDelay = (delay: string) => { + if (!delay) return '' + if (delay === '0' || !Number.isInteger(Number(delay))) { + return 'Invalid number' + } + } + const onShowAdvanced = () => { setShowAdvanced((prev) => !prev) trackEvent(RECOVERY_EVENTS.SHOW_ADVANCED) } - const isDisabled = !understandsRisk || !isDirty + const isDisabled = !understandsRisk || !isDirty || !!customDelayState.error + + const handleSubmit = () => { + onSubmit({ expiry, delay, customDelay, selectedDelay, recoverer }) + } return ( <> -
    + Your Recoverer will be able to reset your Account setup. Only select an address that you trust.{' '} @@ -97,7 +113,6 @@ export function UpsertRecoveryFlowSettings({ -
    Trusted Recoverer @@ -108,7 +123,6 @@ export function UpsertRecoveryFlowSettings({ can initiate the recovery process in the future.
    -
    - You can cancel any recovery proposal when it is not needed or wanted during this period. + The recovery proposal will be available for execution after this period of time. You can cancel any + recovery proposal when it is not needed or wanted during this period.
    - - ( - - {periods.delay.map(({ label, value }, index) => ( - - {label} - - ))} - - )} - /> - + + ( + + {periods.delay.map(({ label, value }, index) => ( + + {label} + + ))} + + )} + /> + + {isCustomDelaySelected(selectedDelay) && ( + <> + ( + + )} + /> + days. + + )} + + Advanced {showAdvanced ? : } -
    @@ -192,13 +229,13 @@ export function UpsertRecoveryFlowSettings({ // Don't reset value if advanced section is collapsed shouldUnregister={false} render={({ field: { ref, ...field } }) => ( - + {periods.expiration.map(({ label, value }, index) => ( {label} ))} - + )} /> @@ -225,26 +262,3 @@ export function UpsertRecoveryFlowSettings({ ) } - -function SelectField(props: TextFieldProps) { - return ( - - ) -} diff --git a/src/components/tx-flow/flows/UpsertRecovery/index.tsx b/src/components/tx-flow/flows/UpsertRecovery/index.tsx index 7810b65e7c..48339019a7 100644 --- a/src/components/tx-flow/flows/UpsertRecovery/index.tsx +++ b/src/components/tx-flow/flows/UpsertRecovery/index.tsx @@ -15,12 +15,16 @@ const Subtitles = ['How does recovery work?', 'Set up recovery settings', 'Set u export enum UpsertRecoveryFlowFields { recoverer = 'recoverer', delay = 'delay', + customDelay = 'customDelay', + selectedDelay = 'selectedDelay', expiry = 'expiry', } export type UpsertRecoveryFlowProps = { [UpsertRecoveryFlowFields.recoverer]: string [UpsertRecoveryFlowFields.delay]: string + [UpsertRecoveryFlowFields.customDelay]: string + [UpsertRecoveryFlowFields.selectedDelay]: string [UpsertRecoveryFlowFields.expiry]: string } @@ -28,7 +32,9 @@ function UpsertRecoveryFlow({ delayModifier }: { delayModifier?: RecoveryState[n const { data, step, nextStep, prevStep } = useTxStepper( { [UpsertRecoveryFlowFields.recoverer]: delayModifier?.recoverers?.[0] ?? '', - [UpsertRecoveryFlowFields.delay]: delayModifier?.delay?.toString() ?? `${DAY_IN_SECONDS * 28}`, // 28 days in seconds + [UpsertRecoveryFlowFields.delay]: '', + [UpsertRecoveryFlowFields.selectedDelay]: delayModifier?.delay?.toString() ?? `${DAY_IN_SECONDS * 28}`, // 28 days in seconds + [UpsertRecoveryFlowFields.customDelay]: '', [UpsertRecoveryFlowFields.expiry]: delayModifier?.expiry?.toString() ?? '0', }, SETUP_RECOVERY_CATEGORY, diff --git a/src/components/tx-flow/flows/UpsertRecovery/useRecoveryPeriods.ts b/src/components/tx-flow/flows/UpsertRecovery/useRecoveryPeriods.ts index b84850c6a1..dd7fcc026f 100644 --- a/src/components/tx-flow/flows/UpsertRecovery/useRecoveryPeriods.ts +++ b/src/components/tx-flow/flows/UpsertRecovery/useRecoveryPeriods.ts @@ -5,7 +5,7 @@ export const DAY_IN_SECONDS = 60 * 60 * 24 type Periods = Array<{ label: string; value: string | number }> -const DefaultRecoveryDelayPeriods: Periods = [ +const ExpirationPeriods: Periods = [ { label: '2 days', value: `${DAY_IN_SECONDS * 2}`, @@ -28,15 +28,7 @@ const DefaultRecoveryDelayPeriods: Periods = [ }, ] -const DefaultRecoveryExpirationPeriods: Periods = [ - { - label: 'Never', - value: '0', - }, - ...DefaultRecoveryDelayPeriods, -] - -const TestRecoveryDelayPeriods: Periods = [ +const TestPeriods: Periods = [ { label: '1 minute', value: '60', @@ -49,7 +41,31 @@ const TestRecoveryDelayPeriods: Periods = [ label: '1 hour', value: `${60 * 60}`, }, - ...DefaultRecoveryDelayPeriods, +] + +const DefaultRecoveryDelayPeriods: Periods = [ + { + label: 'Custom period', + value: '0', + }, + ...ExpirationPeriods, +] + +const DefaultRecoveryExpirationPeriods: Periods = [ + { + label: 'Never', + value: '0', + }, + ...ExpirationPeriods, +] + +const TestRecoveryDelayPeriods: Periods = [ + { + label: 'Custom period', + value: '0', + }, + ...TestPeriods, + ...ExpirationPeriods, ] const TestRecoveryExpirationPeriods: Periods = [ @@ -57,7 +73,8 @@ const TestRecoveryExpirationPeriods: Periods = [ label: 'Never', value: '0', }, - ...TestRecoveryDelayPeriods, + ...TestPeriods, + ...ExpirationPeriods, ] export function useRecoveryPeriods(): { delay: Periods; expiration: Periods } { diff --git a/src/components/tx-flow/flows/UpsertRecovery/utils.ts b/src/components/tx-flow/flows/UpsertRecovery/utils.ts new file mode 100644 index 0000000000..6630da51fe --- /dev/null +++ b/src/components/tx-flow/flows/UpsertRecovery/utils.ts @@ -0,0 +1,11 @@ +import { DAY_IN_SECONDS } from './useRecoveryPeriods' + +export const isCustomDelaySelected = (selectedDelay: string) => { + return !Number(selectedDelay) +} + +export const getDelay = (customDelay: string, selectedDelay: string) => { + const isCustom = isCustomDelaySelected(selectedDelay) + if (!isCustom) return selectedDelay + return customDelay ? `${Number(customDelay) * DAY_IN_SECONDS}` : '' +} From 2d7b400a7fbb49c29b53c17c90b426a34219b106 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jun 2024 07:42:48 +0200 Subject: [PATCH 070/154] chore(deps): bump @grpc/grpc-js from 1.9.5 to 1.9.15 (#3822) Bumps [@grpc/grpc-js](https://github.com/grpc/grpc-node) from 1.9.5 to 1.9.15. - [Release notes](https://github.com/grpc/grpc-node/releases) - [Commits](https://github.com/grpc/grpc-node/compare/@grpc/grpc-js@1.9.5...@grpc/grpc-js@1.9.15) --- updated-dependencies: - dependency-name: "@grpc/grpc-js" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8e48dd9837..1d0f4f9248 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2944,9 +2944,9 @@ ethers "^6.9.2" "@grpc/grpc-js@~1.9.0": - version "1.9.5" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.5.tgz#22e283754b7b10d1ad26c3fb21849028dcaabc53" - integrity sha512-iouYNlPxRAwZ2XboDT+OfRKHuaKHiqjB5VFYZ0NFrHkbEF+AV3muIUY9olQsp8uxU4VvRCMiRk9ftzFDGb61aw== + version "1.9.15" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.15.tgz#433d7ac19b1754af690ea650ab72190bd700739b" + integrity sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ== dependencies: "@grpc/proto-loader" "^0.7.8" "@types/node" ">=12.12.47" @@ -17703,7 +17703,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -17791,7 +17800,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -20019,7 +20035,7 @@ workbox-window@7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -20037,6 +20053,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 9720343fa967c6100a25fab3a1de15a3a5bae15a Mon Sep 17 00:00:00 2001 From: James Mealy Date: Tue, 11 Jun 2024 09:13:08 +0100 Subject: [PATCH 071/154] Feat: add swaps card to safe apps list (#3786) --- .../ActivityRewardsSection/index.tsx | 4 +- .../ActivityRewardsSection/styles.module.css | 2 +- src/components/dashboard/index.tsx | 16 +++-- .../NativeSwapsCard/index.stories.tsx | 31 ++++++++++ .../safe-apps/NativeSwapsCard/index.tsx | 61 +++++++++++++++++++ .../NativeSwapsCard/styles.module.css | 50 +++++++++++++++ .../safe-apps/SafeAppList/index.tsx | 5 ++ .../swap/components/SwapWidget/index.tsx | 22 ++++--- .../components/SwapWidget/styles.module.css | 37 ++++++----- src/pages/apps/index.tsx | 1 + src/services/analytics/events/swaps.ts | 1 + 11 files changed, 197 insertions(+), 33 deletions(-) create mode 100644 src/components/safe-apps/NativeSwapsCard/index.stories.tsx create mode 100644 src/components/safe-apps/NativeSwapsCard/index.tsx create mode 100644 src/components/safe-apps/NativeSwapsCard/styles.module.css diff --git a/src/components/dashboard/ActivityRewardsSection/index.tsx b/src/components/dashboard/ActivityRewardsSection/index.tsx index e64132e8db..ece466568c 100644 --- a/src/components/dashboard/ActivityRewardsSection/index.tsx +++ b/src/components/dashboard/ActivityRewardsSection/index.tsx @@ -51,7 +51,7 @@ const ActivityRewardsSection = () => { } return ( - + <> { - + ) } diff --git a/src/components/dashboard/ActivityRewardsSection/styles.module.css b/src/components/dashboard/ActivityRewardsSection/styles.module.css index 7b62eaec1e..92c6ac1639 100644 --- a/src/components/dashboard/ActivityRewardsSection/styles.module.css +++ b/src/components/dashboard/ActivityRewardsSection/styles.module.css @@ -67,7 +67,7 @@ .links { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; margin-top: var(--space-3); text-wrap: nowrap; diff --git a/src/components/dashboard/index.tsx b/src/components/dashboard/index.tsx index a03adb52d9..50e27e0f27 100644 --- a/src/components/dashboard/index.tsx +++ b/src/components/dashboard/index.tsx @@ -18,9 +18,9 @@ import ActivityRewardsSection from '@/components/dashboard/ActivityRewardsSectio import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' import css from './styles.module.css' +import SwapWidget from '@/features/swap/components/SwapWidget' const RecoveryHeader = dynamic(() => import('@/features/recovery/components/RecoveryHeader')) -const SwapWidget = dynamic(() => import('@/features/swap/components/SwapWidget')) const Dashboard = (): ReactElement => { const router = useRouter() @@ -44,13 +44,17 @@ const Dashboard = (): ReactElement => { - - - - {safe.deployed && ( <> - + + + + + + + + + diff --git a/src/components/safe-apps/NativeSwapsCard/index.stories.tsx b/src/components/safe-apps/NativeSwapsCard/index.stories.tsx new file mode 100644 index 0000000000..9781c14934 --- /dev/null +++ b/src/components/safe-apps/NativeSwapsCard/index.stories.tsx @@ -0,0 +1,31 @@ +import type { Meta, StoryObj } from '@storybook/react' +import NativeSwapsCard from './index' +import { Box } from '@mui/material' +import { StoreDecorator } from '@/stories/storeDecorator' + +const meta = { + component: NativeSwapsCard, + parameters: { + componentSubtitle: 'Renders a promo card for native swaps', + }, + + decorators: [ + (Story) => { + return ( + + + + + + ) + }, + ], + tags: ['autodocs'], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {}, +} diff --git a/src/components/safe-apps/NativeSwapsCard/index.tsx b/src/components/safe-apps/NativeSwapsCard/index.tsx new file mode 100644 index 0000000000..25f029ae0d --- /dev/null +++ b/src/components/safe-apps/NativeSwapsCard/index.tsx @@ -0,0 +1,61 @@ +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' +import { Button, Paper, Stack } from '@mui/material' +import SafeAppIconCard from '../SafeAppIconCard' +import css from './styles.module.css' +import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' +import Track from '@/components/common/Track' +import Link from 'next/link' +import { AppRoutes } from '@/config/routes' +import { useRouter } from 'next/router' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' + +const SWAPS_APP_CARD_STORAGE_KEY = 'showSwapsAppCard' + +const NativeSwapsCard = () => { + const router = useRouter() + const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) + const [isSwapsCardVisible = true, setIsSwapsCardVisible] = useLocalStorage(SWAPS_APP_CARD_STORAGE_KEY) + if (!isSwapFeatureEnabled || !isSwapsCardVisible) return null + + return ( + + + +
    + } + /> + + + + Native swaps are here! + + + + Experience seamless trading with better decoding and security in native swaps. + + + + + + + + + + + + + ) +} + +export default NativeSwapsCard diff --git a/src/components/safe-apps/NativeSwapsCard/styles.module.css b/src/components/safe-apps/NativeSwapsCard/styles.module.css new file mode 100644 index 0000000000..68b767d1a5 --- /dev/null +++ b/src/components/safe-apps/NativeSwapsCard/styles.module.css @@ -0,0 +1,50 @@ +.container { + transition: background-color 0.3s ease-in-out, border 0.3s ease-in-out; + border: 1px solid transparent; + height: 100%; +} + +.container:hover { + background-color: var(--color-background-light); + border: 1px solid var(--color-secondary-light); +} + +.header { + padding: var(--space-3) var(--space-2) var(--space-1) var(--space-2); +} + +.content { + padding: var(--space-2); +} + +.iconContainer { + position: relative; + background: var(--color-secondary-light); + border-radius: 50%; + display: flex; + padding: var(--space-1); +} + +.title { + line-height: 175%; + margin: 0; + + flex-grow: 1; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.description { + /* Truncate Safe App Description (3 lines) */ + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.buttons { + padding-top: var(--space-2); + white-space: nowrap; +} diff --git a/src/components/safe-apps/SafeAppList/index.tsx b/src/components/safe-apps/SafeAppList/index.tsx index c9a7daf84e..1f57227405 100644 --- a/src/components/safe-apps/SafeAppList/index.tsx +++ b/src/components/safe-apps/SafeAppList/index.tsx @@ -10,6 +10,7 @@ import useSafeAppPreviewDrawer from '@/hooks/safe-apps/useSafeAppPreviewDrawer' 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' type SafeAppListProps = { safeAppsList: SafeAppData[] @@ -20,6 +21,7 @@ type SafeAppListProps = { removeCustomApp?: (safeApp: SafeAppData) => void title: string query?: string + isFiltered?: boolean } const SafeAppList = ({ @@ -31,6 +33,7 @@ const SafeAppList = ({ removeCustomApp, title, query, + isFiltered = false, }: SafeAppListProps) => { const { isPreviewDrawerOpen, previewDrawerApp, openPreviewDrawer, closePreviewDrawer } = useSafeAppPreviewDrawer() const { openedSafeAppIds } = useOpenedSafeApps() @@ -69,6 +72,8 @@ const SafeAppList = ({ ))} + {!isFiltered && } + {/* Flat list filtered by search query */} {safeAppsList.map((safeApp) => (
  • diff --git a/src/features/swap/components/SwapWidget/index.tsx b/src/features/swap/components/SwapWidget/index.tsx index e306a38854..7e7d8eb206 100644 --- a/src/features/swap/components/SwapWidget/index.tsx +++ b/src/features/swap/components/SwapWidget/index.tsx @@ -14,10 +14,10 @@ import Track from '@/components/common/Track' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' -const SWAP_PROMO_WIDGET_IS_HIDDEN = 'SWAP_PROMO_WIDGET_IS_HIDDEN' +const SWAPS_PROMO_WIDGET_IS_HIDDEN = 'swapsPromoWidgetIsHidden' function SwapWidget(): ReactElement | null { - const [isHidden = false, setIsHidden] = useLocalStorage(SWAP_PROMO_WIDGET_IS_HIDDEN) + const [isHidden = false, setIsHidden] = useLocalStorage(SWAPS_PROMO_WIDGET_IS_HIDDEN) const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) const onClick = useCallback(() => { @@ -36,7 +36,7 @@ function SwapWidget(): ReactElement | null { - + Introducing native swaps @@ -44,11 +44,14 @@ function SwapWidget(): ReactElement | null { - + Experience our native swaps, powered by CoW Protocol! Trade seamlessly and efficiently with decoded transactions that are easy to understand. + + + - + - - - Swap + + + Swap diff --git a/src/features/swap/components/SwapWidget/styles.module.css b/src/features/swap/components/SwapWidget/styles.module.css index c6fb8c2608..a276f52fba 100644 --- a/src/features/swap/components/SwapWidget/styles.module.css +++ b/src/features/swap/components/SwapWidget/styles.module.css @@ -10,6 +10,7 @@ .grid { display: flex; + flex-wrap: nowrap; height: inherit; gap: var(--space-3); } @@ -27,31 +28,39 @@ margin-right: var(--space-1); } -.imageContainer { - display: flex; - align-items: flex-end; -} - .buttonContainer { display: flex; flex-direction: row; justify-content: flex-start; align-items: flex-start; gap: var(--space-2); + white-space: nowrap; + position: relative; + z-index: 2; } -@media (max-width: 599.95px) { - .imageContainer { - width: 100%; - justify-content: flex-end; - } +.imageContainer { + position: relative; + z-index: 1; +} - .buttonContainer { - gap: 0; - justify-content: space-between; - } +.imageContainer img { + display: block; + position: absolute; + z-index: 0; + right: 0; + bottom: 0; + max-width: 500px; +} +@media (max-width: 599.95px) { .wrapper { padding: var(--space-3); } } + +@media (max-width: 970px) { + .imageContainer { + display: none; + } +} diff --git a/src/pages/apps/index.tsx b/src/pages/apps/index.tsx index 4839e57974..ea2f82b7a1 100644 --- a/src/pages/apps/index.tsx +++ b/src/pages/apps/index.tsx @@ -73,6 +73,7 @@ const SafeApps: NextPage = () => { {/* All apps */} Date: Tue, 11 Jun 2024 13:34:38 +0200 Subject: [PATCH 072/154] Refactor: Consistently use latest MultiSendCallOnly address (#3811) --- .../flows/ExecuteBatch/ReviewBatch.tsx | 6 +-- .../security/tenderly/__tests__/utils.test.ts | 4 +- src/components/tx/security/tenderly/utils.ts | 2 +- .../services/__tests__/recovery-state.test.ts | 12 ++---- .../__tests__/transaction-list.test.ts | 1 - .../recovery/services/recovery-state.ts | 6 +-- .../contracts/__tests__/deployments.test.ts | 39 ------------------- .../contracts/__tests__/safeContracts.test.ts | 16 +------- src/services/contracts/deployments.ts | 5 --- src/services/contracts/safeContracts.ts | 37 +++--------------- .../modules/DelegateCallModule/index.test.ts | 1 - .../modules/DelegateCallModule/index.ts | 4 +- 12 files changed, 20 insertions(+), 113 deletions(-) diff --git a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx index 5c3d9bb5fb..d4d718421e 100644 --- a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx +++ b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx @@ -70,9 +70,9 @@ export const ReviewBatch = ({ params }: { params: ExecuteBatchFlowProps }) => { }, [params.txs, chain?.chainId]) const [multiSendContract] = useAsync(async () => { - if (!chain?.chainId || !safe.version) return - return await getReadOnlyMultiSendCallOnlyContract(chain.chainId, safe.version) - }, [chain?.chainId, safe.version]) + if (!chain?.chainId) return + return await getReadOnlyMultiSendCallOnlyContract(chain.chainId) + }, [chain?.chainId]) const [multisendContractAddress = ''] = useAsync(async () => { if (!multiSendContract) return '' diff --git a/src/components/tx/security/tenderly/__tests__/utils.test.ts b/src/components/tx/security/tenderly/__tests__/utils.test.ts index 3005c06a73..fc18ad31d9 100644 --- a/src/components/tx/security/tenderly/__tests__/utils.test.ts +++ b/src/components/tx/security/tenderly/__tests__/utils.test.ts @@ -19,8 +19,8 @@ const SIGNATURE_LENGTH = 65 * 2 const getPreValidatedSignature = (addr: string): string => generatePreValidatedSignature(addr).data describe('simulation utils', () => { - const safeContractInterface = new Interface(getSafeSingletonDeployment({ version: '1.3.0' })?.abi || []) - const multiSendContractInterface = new Interface(getMultiSendCallOnlyDeployment({ version: '1.3.0' })?.abi || []) + const safeContractInterface = new Interface(getSafeSingletonDeployment()?.abi || []) + const multiSendContractInterface = new Interface(getMultiSendCallOnlyDeployment()?.abi || []) const mockSafeAddress = zeroPadValue('0x0123', 20) const mockMultisendAddress = zeroPadValue('0x1234', 20) diff --git a/src/components/tx/security/tenderly/utils.ts b/src/components/tx/security/tenderly/utils.ts index 775ccf9c2d..ba6c411902 100644 --- a/src/components/tx/security/tenderly/utils.ts +++ b/src/components/tx/security/tenderly/utils.ts @@ -121,7 +121,7 @@ export const _getMultiSendCallOnlyPayload = async ( params: MultiSendTransactionSimulationParams, ): Promise> => { const data = encodeMultiSendData(params.transactions) - const readOnlyMultiSendContract = await getReadOnlyMultiSendCallOnlyContract(params.safe.chainId, params.safe.version) + const readOnlyMultiSendContract = await getReadOnlyMultiSendCallOnlyContract(params.safe.chainId) return { to: await readOnlyMultiSendContract.getAddress(), diff --git a/src/features/recovery/services/__tests__/recovery-state.test.ts b/src/features/recovery/services/__tests__/recovery-state.test.ts index b70a2d7a0b..ec657d56b7 100644 --- a/src/features/recovery/services/__tests__/recovery-state.test.ts +++ b/src/features/recovery/services/__tests__/recovery-state.test.ts @@ -72,9 +72,7 @@ describe('recovery-state', () => { const safeAbi = getSafeSingletonDeployment({ network: chainId, version })!.abi const safeInterface = new Interface(safeAbi) - const multiSendAbi = - getMultiSendCallOnlyDeployment({ network: chainId, version }) ?? - getMultiSendCallOnlyDeployment({ network: chainId, version: '1.3.0' }) + const multiSendAbi = getMultiSendCallOnlyDeployment({ network: chainId }) const multiSendInterface = new Interface(multiSendAbi!.abi) const multiSendData = encodeMultiSendData([ @@ -109,9 +107,7 @@ describe('recovery-state', () => { const safeAbi = getSafeSingletonDeployment({ network: chainId, version })!.abi const safeInterface = new Interface(safeAbi) - const multiSendDeployment = - getMultiSendCallOnlyDeployment({ network: chainId, version }) ?? - getMultiSendCallOnlyDeployment({ network: chainId, version: '1.3.0' }) + const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId }) const multiSendInterface = new Interface(multiSendDeployment!.abi) const multiSendData = encodeMultiSendData([ @@ -146,7 +142,7 @@ describe('recovery-state', () => { const safeAbi = getSafeSingletonDeployment({ network: chainId, version })!.abi const safeInterface = new Interface(safeAbi) - const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId, version })! + const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId })! const multiSendInterface = new Interface(multiSendDeployment.abi) const multiSendData = encodeMultiSendData([ @@ -181,7 +177,7 @@ describe('recovery-state', () => { const safeAbi = getSafeSingletonDeployment({ network: chainId, version })!.abi const safeInterface = new Interface(safeAbi) - const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId, version: '1.3.0' })! + const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId })! const multiSendInterface = new Interface(multiSendDeployment.abi) const multiSendData = encodeMultiSendData([ diff --git a/src/features/recovery/services/__tests__/transaction-list.test.ts b/src/features/recovery/services/__tests__/transaction-list.test.ts index 431a86fe47..296e80b8d7 100644 --- a/src/features/recovery/services/__tests__/transaction-list.test.ts +++ b/src/features/recovery/services/__tests__/transaction-list.test.ts @@ -124,7 +124,6 @@ describe('getRecoveredSafeInfo', () => { const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: safe.chainId, - version: safe.version ?? undefined, }) const multiSendAddress = multiSendDeployment!.networkAddresses[safe.chainId] const multiSendInterface = new Interface(multiSendDeployment!.abi) diff --git a/src/features/recovery/services/recovery-state.ts b/src/features/recovery/services/recovery-state.ts index 6303750e05..b25075dd2c 100644 --- a/src/features/recovery/services/recovery-state.ts +++ b/src/features/recovery/services/recovery-state.ts @@ -44,8 +44,6 @@ export function _isMaliciousRecovery({ safeAddress: string transaction: Pick }) { - const BASE_MULTI_SEND_CALL_ONLY_VERSION = '1.3.0' - const isMultiSend = isMultiSendCalldata(transaction.data) const transactions = isMultiSend ? decodeMultiSendTxs(transaction.data) : [transaction] @@ -54,9 +52,7 @@ export function _isMaliciousRecovery({ return !sameAddress(transaction.to, safeAddress) } - const multiSendDeployment = - getMultiSendCallOnlyDeployment({ network: chainId, version: version ?? undefined }) ?? - getMultiSendCallOnlyDeployment({ network: chainId, version: BASE_MULTI_SEND_CALL_ONLY_VERSION }) + const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId }) if (!multiSendDeployment) { return true diff --git a/src/services/contracts/__tests__/deployments.test.ts b/src/services/contracts/__tests__/deployments.test.ts index dc194418f0..8cd9f44475 100644 --- a/src/services/contracts/__tests__/deployments.test.ts +++ b/src/services/contracts/__tests__/deployments.test.ts @@ -232,45 +232,6 @@ describe('deployments', () => { }) }) - describe('getMultiSendCallOnlyContractDeployment', () => { - it('should return the versioned deployment for supported version/chain', () => { - const expected = safeDeployments.getMultiSendCallOnlyDeployment({ - version: '1.3.0', // First available version - network: '1', - }) - - expect(expected).toBeDefined() - const deployment = deployments.getMultiSendCallOnlyContractDeployment('1', '1.3.0') - expect(deployment).toStrictEqual(expected) - }) - - it('should return undefined for supported version/unsupported chain', () => { - const deployment = deployments.getMultiSendCallOnlyContractDeployment('69420', '1.3.0') - expect(deployment).toBe(undefined) - }) - - it('should return undefined for unsupported version/chain', () => { - const deployment = deployments.getMultiSendCallOnlyContractDeployment('69420', '1.2.3') - expect(deployment).toBe(undefined) - }) - - it('should return the latest deployment for no version/supported chain', () => { - const expected = safeDeployments.getMultiSendCallOnlyDeployment({ - version: LATEST_SAFE_VERSION, - network: '1', - }) - - expect(expected).toBeDefined() - const deployment = deployments.getMultiSendCallOnlyContractDeployment('1', null) - expect(deployment).toStrictEqual(expected) - }) - - it('should return undefined for no version/unsupported chain', () => { - const deployment = deployments.getMultiSendCallOnlyContractDeployment('69420', null) - expect(deployment).toBe(undefined) - }) - }) - describe('getFallbackHandlerContractDeployment', () => { it('should return the versioned deployment for supported version/chain', () => { const expected = safeDeployments.getFallbackHandlerDeployment({ diff --git a/src/services/contracts/__tests__/safeContracts.test.ts b/src/services/contracts/__tests__/safeContracts.test.ts index 6e269bdc85..754b914f59 100644 --- a/src/services/contracts/__tests__/safeContracts.test.ts +++ b/src/services/contracts/__tests__/safeContracts.test.ts @@ -1,5 +1,5 @@ import { ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' -import { _getValidatedGetContractProps, isValidMasterCopy, _getMinimumMultiSendCallOnlyVersion } from '../safeContracts' +import { _getValidatedGetContractProps, isValidMasterCopy } from '../safeContracts' describe('safeContracts', () => { describe('isValidMasterCopy', () => { @@ -49,18 +49,4 @@ describe('safeContracts', () => { expect(() => _getValidatedGetContractProps('')).toThrow(' is not a valid Safe Account version') }) }) - - describe('_getMinimumMultiSendCallOnlyVersion', () => { - it('should return the initial version if the Safe version is null', () => { - expect(_getMinimumMultiSendCallOnlyVersion(null)).toBe('1.3.0') - }) - - it('should return the initial version if the Safe version is lower than the initial version', () => { - expect(_getMinimumMultiSendCallOnlyVersion('1.0.0')).toBe('1.3.0') - }) - - it('should return the Safe version if the Safe version is higher than the initial version', () => { - expect(_getMinimumMultiSendCallOnlyVersion('1.4.1')).toBe('1.4.1') - }) - }) }) diff --git a/src/services/contracts/deployments.ts b/src/services/contracts/deployments.ts index 181b8ff5b6..37792c4b1c 100644 --- a/src/services/contracts/deployments.ts +++ b/src/services/contracts/deployments.ts @@ -2,7 +2,6 @@ import semverSatisfies from 'semver/functions/satisfies' import { getSafeSingletonDeployment, getSafeL2SingletonDeployment, - getMultiSendCallOnlyDeployment, getFallbackHandlerDeployment, getProxyFactoryDeployment, getSignMessageLibDeployment, @@ -65,10 +64,6 @@ export const getSafeContractDeployment = ( return _tryDeploymentVersions(getDeployment, chain.chainId, safeVersion) } -export const getMultiSendCallOnlyContractDeployment = (chainId: string, safeVersion: SafeInfo['version']) => { - return _tryDeploymentVersions(getMultiSendCallOnlyDeployment, chainId, safeVersion) -} - export const getFallbackHandlerContractDeployment = (chainId: string, safeVersion: SafeInfo['version']) => { return _tryDeploymentVersions(getFallbackHandlerDeployment, chainId, safeVersion) } diff --git a/src/services/contracts/safeContracts.ts b/src/services/contracts/safeContracts.ts index 2a99c4ba23..bd95768fd7 100644 --- a/src/services/contracts/safeContracts.ts +++ b/src/services/contracts/safeContracts.ts @@ -1,6 +1,5 @@ import { getFallbackHandlerContractDeployment, - getMultiSendCallOnlyContractDeployment, getProxyFactoryContractDeployment, getSafeContractDeployment, getSignMessageLibContractDeployment, @@ -12,9 +11,9 @@ import type { GetContractProps, SafeVersion } from '@safe-global/safe-core-sdk-t import { assertValidSafeVersion, createEthersAdapter, createReadOnlyEthersAdapter } from '@/hooks/coreSDK/safeCoreSDK' import type { BrowserProvider } from 'ethers' import type { EthersAdapter, SafeContractEthers, SignMessageLibEthersContract } from '@safe-global/protocol-kit' -import semver from 'semver' import type CompatibilityFallbackHandlerEthersContract from '@safe-global/protocol-kit/dist/src/adapters/ethers/contracts/CompatibilityFallbackHandler/CompatibilityFallbackHandlerEthersContract' +import { getMultiSendCallOnlyDeployment } from '@safe-global/safe-deployments' // `UNKNOWN` is returned if the mastercopy does not match supported ones // @see https://github.com/safe-global/safe-client-gateway/blob/main/src/routes/safes/handlers/safes.rs#L28-L31 @@ -70,37 +69,13 @@ export const getReadOnlyGnosisSafeContract = async (chain: ChainInfo, safeVersio // MultiSend -export const _getMinimumMultiSendCallOnlyVersion = (safeVersion: SafeInfo['version']) => { - const INITIAL_CALL_ONLY_VERSION = '1.3.0' - - if (!safeVersion) { - return INITIAL_CALL_ONLY_VERSION - } - - return semver.gte(safeVersion, INITIAL_CALL_ONLY_VERSION) ? safeVersion : INITIAL_CALL_ONLY_VERSION -} - -export const getMultiSendCallOnlyContract = async ( - chainId: string, - safeVersion: SafeInfo['version'], - provider: BrowserProvider, -) => { - const ethAdapter = await createEthersAdapter(provider) - const multiSendVersion = _getMinimumMultiSendCallOnlyVersion(safeVersion) - - return ethAdapter.getMultiSendCallOnlyContract({ - singletonDeployment: getMultiSendCallOnlyContractDeployment(chainId, multiSendVersion), - ..._getValidatedGetContractProps(safeVersion), - }) -} - -export const getReadOnlyMultiSendCallOnlyContract = async (chainId: string, safeVersion: SafeInfo['version']) => { +export const getReadOnlyMultiSendCallOnlyContract = async (chainId: string) => { const ethAdapter = createReadOnlyEthersAdapter() - const multiSendVersion = _getMinimumMultiSendCallOnlyVersion(safeVersion) - + const singletonDeployment = getMultiSendCallOnlyDeployment({ network: chainId }) + if (!singletonDeployment) throw new Error('No deployment found for MultiSendCallOnly') return ethAdapter.getMultiSendCallOnlyContract({ - singletonDeployment: getMultiSendCallOnlyContractDeployment(chainId, multiSendVersion), - ..._getValidatedGetContractProps(safeVersion), + singletonDeployment: getMultiSendCallOnlyDeployment({ network: chainId }), + safeVersion: singletonDeployment.version as SafeVersion, }) } diff --git a/src/services/security/modules/DelegateCallModule/index.test.ts b/src/services/security/modules/DelegateCallModule/index.test.ts index a7278a739f..a28965e622 100644 --- a/src/services/security/modules/DelegateCallModule/index.test.ts +++ b/src/services/security/modules/DelegateCallModule/index.test.ts @@ -34,7 +34,6 @@ describe('DelegateCallModule', () => { const multiSend = getMultiSendCallOnlyDeployment({ network: CHAIN_ID, - version: SAFE_VERSION, })!.defaultAddress const recipient1 = toBeHex('0x2', 20) diff --git a/src/services/security/modules/DelegateCallModule/index.ts b/src/services/security/modules/DelegateCallModule/index.ts index 85555bafc8..5755eeacea 100644 --- a/src/services/security/modules/DelegateCallModule/index.ts +++ b/src/services/security/modules/DelegateCallModule/index.ts @@ -1,10 +1,10 @@ import { OperationType } from '@safe-global/safe-core-sdk-types' -import { getMultiSendCallOnlyContractDeployment } from '@/services/contracts/deployments' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' import { SecuritySeverity } from '../types' import type { SecurityModule, SecurityResponse } from '../types' +import { getMultiSendCallOnlyDeployment } from '@safe-global/safe-deployments' type DelegateCallModuleRequest = { chainId: string @@ -28,7 +28,7 @@ export class DelegateCallModule implements SecurityModule Date: Tue, 11 Jun 2024 14:20:21 +0200 Subject: [PATCH 073/154] Fix: explicitely pass a locale to NumberFormat (#3828) --- src/components/common/FiatValue/index.tsx | 10 ++++++++-- src/utils/formatNumber.ts | 12 +++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/components/common/FiatValue/index.tsx b/src/components/common/FiatValue/index.tsx index 94d0164591..9cc09f1ea4 100644 --- a/src/components/common/FiatValue/index.tsx +++ b/src/components/common/FiatValue/index.tsx @@ -1,9 +1,11 @@ -import type { ReactElement } from 'react' +import type { CSSProperties, ReactElement } from 'react' import { useMemo } from 'react' import { useAppSelector } from '@/store' import { selectCurrency } from '@/store/settingsSlice' import { formatCurrency } from '@/utils/formatNumber' +const style = { whiteSpace: 'nowrap' } as CSSProperties + const FiatValue = ({ value, maxLength }: { value: string | number; maxLength?: number }): ReactElement => { const currency = useAppSelector(selectCurrency) @@ -11,7 +13,11 @@ const FiatValue = ({ value, maxLength }: { value: string | number; maxLength?: n return formatCurrency(value, currency, maxLength) }, [value, currency, maxLength]) - return {fiat} + return ( + + {fiat} + + ) } export default FiatValue diff --git a/src/utils/formatNumber.ts b/src/utils/formatNumber.ts index b5ecd2d1cb..77b5c05b13 100644 --- a/src/utils/formatNumber.ts +++ b/src/utils/formatNumber.ts @@ -1,3 +1,5 @@ +const locale = typeof navigator !== 'undefined' ? navigator.language : undefined + /** * Intl.NumberFormat number formatter that adheres to our style guide * @param number Number to format @@ -8,7 +10,7 @@ export const formatAmount = (number: string | number, precision = 5, maxLength = if (float === Math.round(float)) precision = 0 if (Math.abs(float) < 0.00001) return '< 0.00001' - const fullNum = new Intl.NumberFormat(undefined, { + const fullNum = new Intl.NumberFormat(locale, { style: 'decimal', maximumFractionDigits: precision, }).format(Number(number)) @@ -16,7 +18,7 @@ export const formatAmount = (number: string | number, precision = 5, maxLength = // +3 for the decimal point and the two decimal places if (fullNum.length <= maxLength + 3) return fullNum - return new Intl.NumberFormat(undefined, { + return new Intl.NumberFormat(locale, { style: 'decimal', notation: 'compact', maximumFractionDigits: 2, @@ -29,7 +31,7 @@ export const formatAmount = (number: string | number, precision = 5, maxLength = * @param precision Fraction digits to show */ export const formatAmountPrecise = (number: string | number, precision: number): string => { - return new Intl.NumberFormat(undefined, { + return new Intl.NumberFormat(locale, { style: 'decimal', maximumFractionDigits: precision, }).format(Number(number)) @@ -43,7 +45,7 @@ export const formatAmountPrecise = (number: string | number, precision: number): export const formatCurrency = (number: string | number, currency: string, maxLength = 6): string => { let float = Number(number) - let result = new Intl.NumberFormat(undefined, { + let result = new Intl.NumberFormat(locale, { style: 'currency', currency, currencyDisplay: 'narrowSymbol', @@ -52,7 +54,7 @@ export const formatCurrency = (number: string | number, currency: string, maxLen // +1 for the currency symbol if (result.length > maxLength + 1) { - result = new Intl.NumberFormat(undefined, { + result = new Intl.NumberFormat(locale, { style: 'currency', currency, currencyDisplay: 'narrowSymbol', From 8d404461d8e85e34e85802544a40d48a9f8e0d63 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:00:16 +0200 Subject: [PATCH 074/154] Tests: Fix-refactor tests (#3829) --- cypress.config.js | 4 +- .../sendfunds_connected_wallet.cy.js | 2 +- cypress/e2e/pages/create_tx.pages.js | 2 +- cypress/e2e/pages/spending_limits.pages.js | 7 +-- cypress/e2e/regression/spending_limits.cy.js | 8 ++- .../e2e/safe-apps/permissions_settings.cy.js | 9 ++-- .../messages_offchain.cy.js | 52 +++++++++---------- cypress/fixtures/txmessages_data.json | 3 ++ cypress/support/commands.js | 5 +- 9 files changed, 52 insertions(+), 40 deletions(-) rename cypress/e2e/{regression => smoke}/messages_offchain.cy.js (63%) diff --git a/cypress.config.js b/cypress.config.js index a518d1421d..11e0f5208d 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -10,8 +10,8 @@ export default defineConfig({ mochaFile: 'reports/junit-[hash].xml', }, retries: { - runMode: 2, - openMode: 0, + runMode: 3, + openMode: 3, }, e2e: { setupNodeEvents(on, config) { diff --git a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js index 108d09c1dc..b397feb460 100644 --- a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js +++ b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js @@ -13,7 +13,7 @@ import { createSafes } from '../../support/api/utils_protocolkit' import { contracts, abi_qtrust, abi_nft_pc2 } from '../../support/api/contracts' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' -const safeBalanceEth = 305230000000000000n +const safeBalanceEth = 305240000000000000n const qtrustBanance = 99000000000000000025n const transferAmount = '1' diff --git a/cypress/e2e/pages/create_tx.pages.js b/cypress/e2e/pages/create_tx.pages.js index 53ec026d18..2a4eab0803 100644 --- a/cypress/e2e/pages/create_tx.pages.js +++ b/cypress/e2e/pages/create_tx.pages.js @@ -106,7 +106,7 @@ export function fillFilterForm({ address, startDate, endDate, amount, token, non if (value !== undefined) { const { selector, findInput } = inputMap[key] const element = findInput ? cy.get(selector).find('input') : cy.get(selector) - element.clear().type(value) + element.should('be.enabled').clear().type(value, { force: true }) } }) } diff --git a/cypress/e2e/pages/spending_limits.pages.js b/cypress/e2e/pages/spending_limits.pages.js index 8cf1c1aca8..d6178686b8 100644 --- a/cypress/e2e/pages/spending_limits.pages.js +++ b/cypress/e2e/pages/spending_limits.pages.js @@ -20,7 +20,8 @@ const reviewSpendingLimit = '[data-testid="spending-limit-label"]' const deleteBtn = '[data-testid="delete-btn"]' const resetTimeInfo = '[data-testid="reset-time"]' const spentAmountInfo = '[data-testid="spent-amount"]' -const spendingLimitTxOption = '[data-testid="spending-limit-tx"]' +export const spendingLimitTxOption = '[data-testid="spending-limit-tx"]' +export const standardTx = '[data-testid="standard-tx"]' const tokenBalance = '[data-testid="token-balance"]' const tokenItem = '[data-testid="token-item"]' const maxBtn = '[data-testid="max-btn"]' @@ -121,8 +122,8 @@ export function selectSpendingLimitOption() { main.checkRadioButtonState(input, constants.checkboxStates.checked) } -export function verifySpendingOptionExists() { - cy.get(spendingLimitTxOption).should('exist').and('be.visible') +export function verifyTxOptionExist(options) { + main.verifyElementsIsVisible(options) } export function verifySpendingOptionShowsBalance(balance) { diff --git a/cypress/e2e/regression/spending_limits.cy.js b/cypress/e2e/regression/spending_limits.cy.js index ad8ea980ab..9efac54169 100644 --- a/cypress/e2e/regression/spending_limits.cy.js +++ b/cypress/e2e/regression/spending_limits.cy.js @@ -46,7 +46,7 @@ describe('Spending limits tests', () => { it('Verify Spending limit option is available when selecting the corresponding token', () => { navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() - spendinglimit.verifySpendingOptionExists() + spendinglimit.verifyTxOptionExist([spendinglimit.spendingLimitTxOption]) }) it('Verify spending limit option shows available amount', () => { @@ -55,6 +55,12 @@ describe('Spending limits tests', () => { spendinglimit.verifySpendingOptionShowsBalance([spendingLimitBalance]) }) + it('Verify when owner is a delegate, standard tx and spending limit tx are present', () => { + navigation.clickOnNewTxBtn() + tx.clickOnSendTokensBtn() + spendinglimit.verifyTxOptionExist([spendinglimit.spendingLimitTxOption, spendinglimit.standardTx]) + }) + it('Verify when spending limit is selected the nonce field is removed', () => { navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() diff --git a/cypress/e2e/safe-apps/permissions_settings.cy.js b/cypress/e2e/safe-apps/permissions_settings.cy.js index d332f11550..53df8b6ce4 100644 --- a/cypress/e2e/safe-apps/permissions_settings.cy.js +++ b/cypress/e2e/safe-apps/permissions_settings.cy.js @@ -1,6 +1,6 @@ -import * as constants from '../../support/constants' -import * as main from '../pages/main.page' -import * as safeapps from '../pages/safeapps.pages' +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as safeapps from '../pages/safeapps.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' let $dapps, @@ -8,7 +8,8 @@ let $dapps, const app1 = 'https://app1.com' const app3 = 'https://app3.com' -describe('Permissions settings tests', () => { +// TODO: Skip until connection error is resolved +describe.skip('Permissions settings tests', () => { before(() => { getSafes(CATEGORIES.static).then((statics) => { staticSafes = statics diff --git a/cypress/e2e/regression/messages_offchain.cy.js b/cypress/e2e/smoke/messages_offchain.cy.js similarity index 63% rename from cypress/e2e/regression/messages_offchain.cy.js rename to cypress/e2e/smoke/messages_offchain.cy.js index 28bfacda76..930af7f4b2 100644 --- a/cypress/e2e/regression/messages_offchain.cy.js +++ b/cypress/e2e/smoke/messages_offchain.cy.js @@ -13,7 +13,7 @@ const offchainMessage = 'Test message 2 off-chain' const typeMessagesGeneral = msg_data.type.general const typeMessagesOffchain = msg_data.type.offChain -describe('Offchain Messages tests', () => { +describe('[SMOKE] Offchain Messages tests', () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) }) @@ -24,38 +24,38 @@ describe('Offchain Messages tests', () => { main.acceptCookies() }) - it('Verify summary for off-chain unsigned messages', () => { - createTx.verifySummaryByIndex( - 0, - [typeMessagesGeneral.sign, typeMessagesGeneral.oneOftwo, typeMessagesOffchain.walletConnect], - typeMessagesOffchain.altTmage, - ) - createTx.verifySummaryByIndex( - 2, - [typeMessagesGeneral.sign, typeMessagesGeneral.oneOftwo, typeMessagesOffchain.walletConnect], - typeMessagesOffchain.altTmage, - ) + it('[SMOKE] Verify summary for off-chain unsigned messages', () => { + createTx.verifySummaryByIndex(0, [ + typeMessagesGeneral.sign, + typeMessagesGeneral.oneOftwo, + typeMessagesOffchain.testMessage1, + ]) + createTx.verifySummaryByIndex(2, [ + typeMessagesGeneral.sign, + typeMessagesGeneral.oneOftwo, + typeMessagesOffchain.testMessage2, + ]) }) - it('Verify summary for off-chain signed messages', () => { - createTx.verifySummaryByIndex( - 1, - [typeMessagesGeneral.confirmed, typeMessagesGeneral.twoOftwo, typeMessagesOffchain.walletConnect], - typeMessagesOffchain.altTmage, - ) - createTx.verifySummaryByIndex( - 3, - [typeMessagesGeneral.confirmed, typeMessagesGeneral.twoOftwo, typeMessagesOffchain.walletConnect], - typeMessagesOffchain.altTmage, - ) + it('[SMOKE] Verify summary for off-chain signed messages', () => { + createTx.verifySummaryByIndex(1, [ + typeMessagesGeneral.confirmed, + typeMessagesGeneral.twoOftwo, + typeMessagesOffchain.name, + ]) + createTx.verifySummaryByIndex(3, [ + typeMessagesGeneral.confirmed, + typeMessagesGeneral.twoOftwo, + typeMessagesOffchain.testMessage3, + ]) }) - it('Verify exapanded details for EIP 191 off-chain message', () => { + it('[SMOKE] Verify exapanded details for EIP 191 off-chain message', () => { createTx.clickOnTransactionItemByIndex(2) cy.contains(typeMessagesOffchain.message2).should('be.visible') }) - it('Verify exapanded details for EIP 712 off-chain message', () => { + it('[SMOKE] Verify exapanded details for EIP 712 off-chain message', () => { const jsonString = createTx.messageNestedStr const values = [ typeMessagesOffchain.name, @@ -76,7 +76,7 @@ describe('Offchain Messages tests', () => { main.verifyTextVisibility(values) }) - it('Verify confirmation window is displayed for unsigned message', () => { + it('[SMOKE] Verify confirmation window is displayed for unsigned message', () => { messages.clickOnMessageSignBtn(2) msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmMsg) msg_confirmation_modal.verifyMessagePresent(offchainMessage) diff --git a/cypress/fixtures/txmessages_data.json b/cypress/fixtures/txmessages_data.json index 807e0065db..fdd609947c 100644 --- a/cypress/fixtures/txmessages_data.json +++ b/cypress/fixtures/txmessages_data.json @@ -8,6 +8,9 @@ }, "offChain": { "walletConnect": "WalletConnect", + "testMessage1": "Test message 1 on-ch…", + "testMessage2": "Test message 2 off-c…", + "testMessage3": "Test message 1 off-c…", "altTmage": "Message type", "sign": "Sign", "oneOftwo": "1 out of 2", diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 7b34538717..d30653b333 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -213,8 +213,9 @@ Cypress.Commands.add('enter', (selector, opts) => { }) Cypress.Commands.add('setupInterceptors', () => { - cy.intercept({ url: '**/*' }, (req) => { + cy.intercept('*', (req) => { req.headers['Origin'] = 'http://localhost:8080' + console.log('Intercepted request with headers:', req.headers) req.continue() - }) + }).as('headers') }) From 64e6e48d13c5a1dc4c18dd437c3f84714eb8e266 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Tue, 11 Jun 2024 15:13:29 +0200 Subject: [PATCH 075/154] fix: always show tx value (#3793) * fix: always show tx value * fix: add to all comoonents decoding txs --- .../DecodedData/SingleTxDecoded/index.tsx | 41 ++++++++++------- .../TxDetails/TxData/DecodedData/index.tsx | 45 ++++++++++++------- src/components/tx/DecodedTx/index.tsx | 36 ++++++++++++--- src/components/tx/FieldsGrid/index.tsx | 2 +- src/components/tx/SendToBlock/index.tsx | 21 ++++++++- 5 files changed, 104 insertions(+), 41 deletions(-) diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx index e6a719529e..4de7ade91c 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx @@ -1,19 +1,25 @@ import { isEmptyHexData } from '@/utils/hex' -import { type InternalTransaction, Operation, type TransactionData } from '@safe-global/safe-gateway-typescript-sdk' +import { + type InternalTransaction, + Operation, + type TransactionData, + TokenType, +} from '@safe-global/safe-gateway-typescript-sdk' import type { AccordionProps } from '@mui/material/Accordion/Accordion' import { useCurrentChain } from '@/hooks/useChains' import { formatVisualAmount } from '@/utils/formatters' import { MethodDetails } from '@/components/transactions/TxDetails/TxData/DecodedData/MethodDetails' import { HexEncodedData } from '@/components/transactions/HexEncodedData' import { isDeleteAllowance, isSetAllowance } from '@/utils/transaction-guards' -import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material' +import { Accordion, AccordionDetails, AccordionSummary, Stack, Typography } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import css from './styles.module.css' import accordionCss from '@/styles/accordion.module.css' import CodeIcon from '@mui/icons-material/Code' import { DelegateCallWarning } from '@/components/transactions/Warning' -import { InfoDetails } from '@/components/transactions/InfoDetails' -import NamedAddressInfo from '@/components/common/NamedAddressInfo' +import SendAmountBlock from '@/components/tx-flow/flows/TokenTransfer/SendAmountBlock' +import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' +import SendToBlock from '@/components/tx/SendToBlock' type SingleTxDecodedProps = { tx: InternalTransaction @@ -51,8 +57,6 @@ export const SingleTxDecoded = ({ const addressInfo = txData.addressInfoIndex?.[tx.to] const name = addressInfo?.name const avatarUrl = addressInfo?.logoUri - - const title = `Interact with${Number(amount) !== 0 ? ` (and send ${amount} ${symbol} to)` : ''}:` const isDelegateCall = tx.operation === Operation.DELEGATE && showDelegateCallWarning const isSpendingLimitMethod = isSetAllowance(tx.dataDecoded?.method) || isDeleteAllowance(tx.dataDecoded?.method) @@ -73,16 +77,21 @@ export const SingleTxDecoded = ({ {/* We always warn of nested delegate calls */} {isDelegateCall && } {!isSpendingLimitMethod && ( - - - + + {amount !== '0' && ( + + )} + + )} {details} diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx index cce1e5af95..156d8b1949 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx @@ -1,10 +1,15 @@ import type { ReactElement } from 'react' -import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' -import { isCustomTxInfo } from '@/utils/transaction-guards' -import { InfoDetails } from '@/components/transactions/InfoDetails' +import { TokenType, type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' + import { HexEncodedData } from '@/components/transactions/HexEncodedData' import { MethodDetails } from '@/components/transactions/TxDetails/TxData/DecodedData/MethodDetails' -import NamedAddressInfo from '@/components/common/NamedAddressInfo' +import { useCurrentChain } from '@/hooks/useChains' +import { formatVisualAmount } from '@/utils/formatters' +import SendAmountBlock from '@/components/tx-flow/flows/TokenTransfer/SendAmountBlock' +import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' +import SendToBlock from '@/components/tx/SendToBlock' +import { Stack } from '@mui/material' +import { isCustomTxInfo } from '@/utils/transaction-guards' interface Props { txData: TransactionDetails['txData'] @@ -12,6 +17,7 @@ interface Props { } export const DecodedData = ({ txData, txInfo }: Props): ReactElement | null => { + const chainInfo = useCurrentChain() // nothing to render if (!txData) { return null @@ -25,22 +31,31 @@ export const DecodedData = ({ txData, txInfo }: Props): ReactElement | null => { decodedData = } + const amount = txData.value ? formatVisualAmount(txData.value, chainInfo?.nativeCurrency.decimals) : '0' // we render the decoded data return ( - <> - - + {amount !== '0' && ( + - + )} + {decodedData} - + ) } diff --git a/src/components/tx/DecodedTx/index.tsx b/src/components/tx/DecodedTx/index.tsx index f8b03f895e..1788bc7654 100644 --- a/src/components/tx/DecodedTx/index.tsx +++ b/src/components/tx/DecodedTx/index.tsx @@ -1,4 +1,3 @@ -import { getInteractionTitle } from '@/components/safe-apps/utils' import SendToBlock from '@/components/tx/SendToBlock' import SwapOrderConfirmationView from '@/features/swap/components/SwapOrderConfirmationView' import { useCurrentChain } from '@/hooks/useChains' @@ -10,13 +9,19 @@ import { AccordionSummary, Box, Skeleton, + Stack, SvgIcon, Tooltip, Typography, } from '@mui/material' import { OperationType, type SafeTransaction } from '@safe-global/safe-core-sdk-types' import type { DecodedDataResponse } from '@safe-global/safe-gateway-typescript-sdk' -import { getTransactionDetails, type TransactionDetails, Operation } from '@safe-global/safe-gateway-typescript-sdk' +import { + getTransactionDetails, + type TransactionDetails, + Operation, + TokenType, +} from '@safe-global/safe-gateway-typescript-sdk' import useChainId from '@/hooks/useChainId' import useAsync from '@/hooks/useAsync' import { MethodDetails } from '@/components/transactions/TxDetails/TxData/DecodedData/MethodDetails' @@ -29,6 +34,9 @@ import ExternalLink from '@/components/common/ExternalLink' import { HelpCenterArticle } from '@/config/constants' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import accordionCss from '@/styles/accordion.module.css' +import { formatVisualAmount } from '@/utils/formatters' +import SendAmountBlock from '@/components/tx-flow/flows/TokenTransfer/SendAmountBlock' +import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' type DecodedTxProps = { tx?: SafeTransaction @@ -68,16 +76,30 @@ const DecodedTx = ({ if (!decodedData) return null + const amount = tx?.data.value ? formatVisualAmount(tx.data.value, chain?.nativeCurrency.decimals) : '0' + return ( -
    + + {!isSwapOrder && tx && showToBlock && amount !== '0' && ( + + )} {!isSwapOrder && tx && showToBlock && ( - + )} {isSwapOrder && tx && } {isMultisend && showMultisend && ( - + )} - + -
    + ) } diff --git a/src/components/tx/FieldsGrid/index.tsx b/src/components/tx/FieldsGrid/index.tsx index d679b815df..375ed85320 100644 --- a/src/components/tx/FieldsGrid/index.tsx +++ b/src/components/tx/FieldsGrid/index.tsx @@ -4,7 +4,7 @@ import { Grid, Typography } from '@mui/material' const FieldsGrid = ({ title, children }: { title: string; children: ReactNode }) => { return ( - + {title} diff --git a/src/components/tx/SendToBlock/index.tsx b/src/components/tx/SendToBlock/index.tsx index 8ae3ef4060..af5423f65c 100644 --- a/src/components/tx/SendToBlock/index.tsx +++ b/src/components/tx/SendToBlock/index.tsx @@ -2,11 +2,28 @@ import { Typography } from '@mui/material' import EthHashInfo from '@/components/common/EthHashInfo' import FieldsGrid from '../FieldsGrid' -const SendToBlock = ({ address, title = 'To' }: { address: string; title?: string }) => { +const SendToBlock = ({ + address, + title = 'To', + avatarSize, + name, +}: { + address: string + name?: string + title?: string + avatarSize?: number +}) => { return ( - + ) From 02c99b678083c939438b23e23eb786d9da068670 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Wed, 12 Jun 2024 09:08:56 +0200 Subject: [PATCH 076/154] feat: add feature toggle for zodiac roles integration (#3830) * feat: add feature toggle for zodiac roles integration * refactor: rewrite mock --- .../__test__/PermissionsCheck.test.tsx | 67 ++++++++++++++----- .../PermissionsCheck/hooks.ts | 7 +- .../__tests__/useRecoveryState.test.tsx | 1 - src/utils/chains.ts | 1 + 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/src/components/tx/SignOrExecuteForm/PermissionsCheck/__test__/PermissionsCheck.test.tsx b/src/components/tx/SignOrExecuteForm/PermissionsCheck/__test__/PermissionsCheck.test.tsx index 48063d0b93..814c6faca9 100644 --- a/src/components/tx/SignOrExecuteForm/PermissionsCheck/__test__/PermissionsCheck.test.tsx +++ b/src/components/tx/SignOrExecuteForm/PermissionsCheck/__test__/PermissionsCheck.test.tsx @@ -14,6 +14,9 @@ import { type OnboardAPI } from '@web3-onboard/core' import { AbiCoder, ZeroAddress, encodeBytes32String } from 'ethers' import PermissionsCheck from '..' import * as hooksModule from '../hooks' +import { FEATURES } from '@/utils/chains' +import { chainBuilder } from '@/tests/builders/chains' +import { useHasFeature } from '@/hooks/useChains' // We assume that CheckWallet always returns true jest.mock('@/components/common/CheckWallet', () => ({ @@ -23,16 +26,25 @@ jest.mock('@/components/common/CheckWallet', () => ({ }, })) -// mock useCurrentChain & useHasFeature +const mockChain = chainBuilder() + // @ts-expect-error - we are using a local FEATURES enum + .with({ features: [FEATURES.ZODIAC_ROLES, FEATURES.EIP1559] }) + .with({ chainId: '1' }) + .with({ shortName: 'eth' }) + .with({ chainName: 'Ethereum' }) + .with({ transactionService: 'https://tx.service.mock' }) + .build() + +// mock useCurrentChain jest.mock('@/hooks/useChains', () => ({ - useCurrentChain: jest.fn(() => ({ - shortName: 'eth', - chainId: '1', - chainName: 'Ethereum', - features: [], - transactionService: 'https://tx.service.mock', - })), - useHasFeature: jest.fn(() => true), // used to check for EIP1559 support + __esModule: true, + ...jest.requireActual('@/hooks/useChains'), + useCurrentChain: jest.fn(() => mockChain), + useHasFeature: jest.fn(), +})) + +jest.mock('@/hooks/useChainId', () => ({ + useChainId: jest.fn().mockReturnValue(() => '1'), })) // mock getModuleTransactionId @@ -72,15 +84,15 @@ describe('PermissionsCheck', () => { beforeEach(() => { jest.clearAllMocks() - - // Safe info - jest.spyOn(useSafeInfoHook, 'default').mockImplementation(() => ({ - safe: SAFE_INFO, - safeAddress: SAFE_INFO.address.value, - safeError: undefined, - safeLoading: false, - safeLoaded: true, - })) + ;(useHasFeature as jest.Mock).mockImplementation((feature) => mockChain.features.includes(feature)), + // Safe info + jest.spyOn(useSafeInfoHook, 'default').mockImplementation(() => ({ + safe: SAFE_INFO, + safeAddress: SAFE_INFO.address.value, + safeError: undefined, + safeLoading: false, + safeLoaded: true, + })) // Roles mod fetching @@ -100,6 +112,25 @@ describe('PermissionsCheck', () => { jest.spyOn(hooksModule, 'pollModuleTransactionId').mockReturnValue(Promise.resolve('i1234567890')) }) + it('only fetch roles and show the card if the feature is enabled', async () => { + ;(useHasFeature as jest.Mock).mockImplementation((feature) => feature !== FEATURES.ZODIAC_ROLES) + mockConnectedWalletAddress(SAFE_INFO.owners[0].value) // connect as safe owner (not a role member) + + const safeTx = createMockSafeTransaction({ + to: ZeroAddress, + data: '0xd0e30db0', // deposit() + value: AbiCoder.defaultAbiCoder().encode(['uint256'], [123]), + operation: OperationType.Call, + }) + + const { queryByText } = render() + + // the card is not shown + expect(queryByText('Execute without confirmations')).not.toBeInTheDocument() + + expect(fetchRolesModMock).not.toHaveBeenCalled() + }) + it('only shows the card when the user is a member of any role', async () => { mockConnectedWalletAddress(SAFE_INFO.owners[0].value) // connect as safe owner (not a role member) diff --git a/src/components/tx/SignOrExecuteForm/PermissionsCheck/hooks.ts b/src/components/tx/SignOrExecuteForm/PermissionsCheck/hooks.ts index 18e07f5af7..6922a96787 100644 --- a/src/components/tx/SignOrExecuteForm/PermissionsCheck/hooks.ts +++ b/src/components/tx/SignOrExecuteForm/PermissionsCheck/hooks.ts @@ -18,6 +18,8 @@ import { OperationType, type Transaction, type MetaTransactionData } from '@safe import { type JsonRpcProvider } from 'ethers' import { KnownContracts, getModuleInstance } from '@gnosis.pm/zodiac' import useWallet from '@/hooks/wallets/useWallet' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' const ROLES_V2_SUPPORTED_CHAINS = Object.keys(chains) @@ -26,9 +28,10 @@ const ROLES_V2_SUPPORTED_CHAINS = Object.keys(chains) */ export const useRolesMods = () => { const { safe } = useSafeInfo() + const isFeatureEnabled = useHasFeature(FEATURES.ZODIAC_ROLES) const [data] = useAsync(async () => { - if (!ROLES_V2_SUPPORTED_CHAINS.includes(safe.chainId)) return [] + if (!ROLES_V2_SUPPORTED_CHAINS.includes(safe.chainId) || !isFeatureEnabled) return [] const safeModules = safe.modules || [] const rolesMods = await Promise.all( @@ -44,7 +47,7 @@ export const useRolesMods = () => { mod.avatar === safe.address.value.toLowerCase() && mod.roles.length > 0, ) - }, [safe]) + }, [safe, isFeatureEnabled]) return data } diff --git a/src/features/recovery/components/RecoveryContext/__tests__/useRecoveryState.test.tsx b/src/features/recovery/components/RecoveryContext/__tests__/useRecoveryState.test.tsx index 4296ddbd9a..55099f081d 100644 --- a/src/features/recovery/components/RecoveryContext/__tests__/useRecoveryState.test.tsx +++ b/src/features/recovery/components/RecoveryContext/__tests__/useRecoveryState.test.tsx @@ -25,7 +25,6 @@ jest.mock('@/hooks/useSafeInfo') jest.mock('@/hooks/wallets/web3') jest.mock('@/hooks/useChains') jest.mock('@/hooks/useTxHistory') -jest.mock('@/hooks/useChains') const mockUseSafeInfo = useSafeInfo as jest.MockedFunction const mockUseWeb3ReadOnly = useWeb3ReadOnly as jest.MockedFunction diff --git a/src/utils/chains.ts b/src/utils/chains.ts index 0485ac42b0..2cebd45e8b 100644 --- a/src/utils/chains.ts +++ b/src/utils/chains.ts @@ -23,6 +23,7 @@ export enum FEATURES { SAP_BANNER = 'SAP_BANNER', NATIVE_SWAPS = 'NATIVE_SWAPS', RELAY_NATIVE_SWAPS = 'RELAY_NATIVE_SWAPS', + ZODIAC_ROLES = 'ZODIAC_ROLES', } export const FeatureRoutes = { From 35a2224747683efa3399f49c55f73858af017f27 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:57:06 +0200 Subject: [PATCH 077/154] Fix: add loading skeletons in the Account List (#3831) --- src/components/dashboard/index.tsx | 3 ++- src/components/welcome/MyAccounts/AccountItem.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/dashboard/index.tsx b/src/components/dashboard/index.tsx index 50e27e0f27..c2fdee5817 100644 --- a/src/components/dashboard/index.tsx +++ b/src/components/dashboard/index.tsx @@ -27,6 +27,7 @@ const Dashboard = (): ReactElement => { const { safe } = useSafeInfo() const { [CREATION_MODAL_QUERY_PARM]: showCreationModal = '' } = router.query const showSafeApps = useHasFeature(FEATURES.SAFE_APPS) + const isSAPBannerEnabled = useHasFeature(FEATURES.SAP_BANNER) const supportsRecovery = useIsRecoverySupported() const [recovery] = useRecovery() const showRecoveryWidget = supportsRecovery && !recovery @@ -46,7 +47,7 @@ const Dashboard = (): ReactElement => { {safe.deployed && ( <> - + diff --git a/src/components/welcome/MyAccounts/AccountItem.tsx b/src/components/welcome/MyAccounts/AccountItem.tsx index 84d05954a9..683959e872 100644 --- a/src/components/welcome/MyAccounts/AccountItem.tsx +++ b/src/components/welcome/MyAccounts/AccountItem.tsx @@ -2,7 +2,7 @@ import { LoopIcon } from '@/features/counterfactual/CounterfactualStatusButton' import { selectUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' import type { ChainInfo, SafeOverview } from '@safe-global/safe-gateway-typescript-sdk' import { useCallback, useMemo } from 'react' -import { ListItemButton, Box, Typography, Chip } from '@mui/material' +import { ListItemButton, Box, Typography, Chip, Skeleton } from '@mui/material' import Link from 'next/link' import SafeIcon from '@/components/common/SafeIcon' import Track from '@/components/common/Track' @@ -110,7 +110,7 @@ const AccountItem = ({ onLinkClick, safeItem, safeOverview }: AccountItemProps) - {safeOverview?.fiatTotal && } + {safeOverview ? : } From 5a9ab122af06359154e9da2e3c11cd4746c42250 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:17:00 +0200 Subject: [PATCH 078/154] Fix: text overflow for Safe names (#3833) --- src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx index 902ad5c752..1600ac6ba6 100644 --- a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx @@ -84,8 +84,10 @@ const SrcEthHashInfo = ({ {name && ( - - {name} + + + {name} + {isAddressBookName && ( From e92c01dbf1d43de802079b49968aa55eb4fecb94 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:55:52 +0200 Subject: [PATCH 079/154] Fix regression tests (#3835) --- cypress.config.js | 2 +- cypress/e2e/pages/assets.pages.js | 13 +++++++++---- cypress/e2e/regression/balances_pagination.cy.js | 3 ++- cypress/e2e/regression/sidebar_2.cy.js | 4 ++-- cypress/e2e/regression/tokens.cy.js | 6 +++--- cypress/support/constants.js | 5 ----- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/cypress.config.js b/cypress.config.js index 11e0f5208d..b03786f202 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -11,7 +11,7 @@ export default defineConfig({ }, retries: { runMode: 3, - openMode: 3, + openMode: 0, }, e2e: { setupNodeEvents(on, config) { diff --git a/cypress/e2e/pages/assets.pages.js b/cypress/e2e/pages/assets.pages.js index 421ac8854d..d2bd528715 100644 --- a/cypress/e2e/pages/assets.pages.js +++ b/cypress/e2e/pages/assets.pages.js @@ -1,6 +1,7 @@ import * as main from './main.page' import * as addressbook from '../pages/address_book.page' import * as createTx from '../pages/create_tx.pages' +import { tableRow } from '../pages/address_book.page' let etherscanLinkSepolia = 'a[aria-label="View on sepolia.etherscan.io"]' export const balanceSingleRow = '[aria-labelledby="tableTitle"] > tbody tr' @@ -43,14 +44,16 @@ const pageCountString1to25 = '1–25 of' const pageCountString1to10 = '1–10 of' const pageCountString10to20 = '11–20 of' -export const fiatRegex = new RegExp(`([0-9]{1,3},)*[0-9]{1,3}.[0-9]{2}`) +export const fiatRegex = new RegExp(`\\$?(([0-9]{1,3},)*[0-9]{1,3}(\\.[0-9]{2})?|0)`) export const tokenListOptions = { allTokens: 'span[data-track="assets: Show all tokens"]', default: 'span[data-track="assets: Show default tokens"]', } -export const currencyEUR = 'EUR' -export const currencyUSD = 'USD' +export const currencyEUR = '€' +export const currencyOptionEUR = 'EUR' +export const currency$ = '$' +export const currencyCAD = 'CAD' export const currentcySepoliaFormat = '0.09996 ETH' @@ -192,10 +195,12 @@ export function clickOnTokenBalanceSortBtn() { export function verifyTokenNamesOrder(option = 'ascending') { const tokens = [] - main.getTextToArray('tr p', tokens) + main.getTextToArray(tableRow, tokens) cy.wrap(tokens).then((arr) => { + cy.log('*** Original array ' + tokens) let sortedNames = [...arr].sort() + cy.log('*** Sorted array ' + sortedNames) if (option == 'descending') sortedNames = [...arr].sort().reverse() expect(arr).to.deep.equal(sortedNames) }) diff --git a/cypress/e2e/regression/balances_pagination.cy.js b/cypress/e2e/regression/balances_pagination.cy.js index a0569de6cc..7b2bff0a67 100644 --- a/cypress/e2e/regression/balances_pagination.cy.js +++ b/cypress/e2e/regression/balances_pagination.cy.js @@ -4,7 +4,8 @@ import * as main from '../../e2e/pages/main.page' const ASSETS_LENGTH = 8 -describe('Balance pagination tests', () => { +// Skip until connection error is resolved +describe.skip('Balance pagination tests', () => { before(() => { cy.clearLocalStorage() // Open the Safe used for testing diff --git a/cypress/e2e/regression/sidebar_2.cy.js b/cypress/e2e/regression/sidebar_2.cy.js index 7f82bfcfa2..1daae9f126 100644 --- a/cypress/e2e/regression/sidebar_2.cy.js +++ b/cypress/e2e/regression/sidebar_2.cy.js @@ -45,8 +45,8 @@ describe('Sidebar added sidebar tests', () => { }) it('Verify Fiat currency changes when edited in the assets tab', () => { - assets.changeCurrency(constants.currencies.cad) - sideBar.checkCurrencyInHeader(constants.currencies.cad) + assets.changeCurrency(assets.currencyCAD) + sideBar.checkCurrencyInHeader(assets.currency$) }) // Waiting for endpoint from CGW diff --git a/cypress/e2e/regression/tokens.cy.js b/cypress/e2e/regression/tokens.cy.js index 0ffdd593c8..0654402338 100644 --- a/cypress/e2e/regression/tokens.cy.js +++ b/cypress/e2e/regression/tokens.cy.js @@ -93,10 +93,10 @@ describe('Tokens tests', () => { it('Verify the default Fiat currency and the effects after changing it', () => { assets.selectTokenList(assets.tokenListOptions.allTokens) assets.verifyFirstRowDoesNotContainCurrency(assets.currencyEUR, FIAT_AMOUNT_COLUMN) - assets.verifyFirstRowContainsCurrency(assets.currencyUSD, FIAT_AMOUNT_COLUMN) + assets.verifyFirstRowContainsCurrency(assets.currency$, FIAT_AMOUNT_COLUMN) assets.clickOnCurrencyDropdown() - assets.selectCurrency(assets.currencyEUR) - assets.verifyFirstRowDoesNotContainCurrency(assets.currencyUSD, FIAT_AMOUNT_COLUMN) + assets.selectCurrency(assets.currencyOptionEUR) + assets.verifyFirstRowDoesNotContainCurrency(assets.currency$, FIAT_AMOUNT_COLUMN) assets.verifyFirstRowContainsCurrency(assets.currencyEUR, FIAT_AMOUNT_COLUMN) }) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index ddd4bdf3ab..f3cd08b65e 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -137,11 +137,6 @@ export const tokenAbbreviation = { tpcc: 'tpcc', } -export const currencies = { - cad: 'CAD', - aud: 'AUD', -} - export const appNames = { walletConnect: 'walletconnect', txbuilder: 'transaction builder', From 3c432bbc041dfda80ffff299c5299d4b7cbabbb8 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 12 Jun 2024 14:00:02 +0200 Subject: [PATCH 080/154] Revert "Refactor: Consistently use latest MultiSendCallOnly address (#3811)" This reverts commit 544d0072ef55dffbdc95e33f586fe99e0ed4229a. --- .../flows/ExecuteBatch/ReviewBatch.tsx | 6 +-- .../security/tenderly/__tests__/utils.test.ts | 4 +- src/components/tx/security/tenderly/utils.ts | 2 +- .../services/__tests__/recovery-state.test.ts | 12 ++++-- .../__tests__/transaction-list.test.ts | 1 + .../recovery/services/recovery-state.ts | 6 ++- .../contracts/__tests__/deployments.test.ts | 39 +++++++++++++++++++ .../contracts/__tests__/safeContracts.test.ts | 16 +++++++- src/services/contracts/deployments.ts | 5 +++ src/services/contracts/safeContracts.ts | 37 +++++++++++++++--- .../modules/DelegateCallModule/index.test.ts | 1 + .../modules/DelegateCallModule/index.ts | 4 +- 12 files changed, 113 insertions(+), 20 deletions(-) diff --git a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx index d4d718421e..5c3d9bb5fb 100644 --- a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx +++ b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx @@ -70,9 +70,9 @@ export const ReviewBatch = ({ params }: { params: ExecuteBatchFlowProps }) => { }, [params.txs, chain?.chainId]) const [multiSendContract] = useAsync(async () => { - if (!chain?.chainId) return - return await getReadOnlyMultiSendCallOnlyContract(chain.chainId) - }, [chain?.chainId]) + if (!chain?.chainId || !safe.version) return + return await getReadOnlyMultiSendCallOnlyContract(chain.chainId, safe.version) + }, [chain?.chainId, safe.version]) const [multisendContractAddress = ''] = useAsync(async () => { if (!multiSendContract) return '' diff --git a/src/components/tx/security/tenderly/__tests__/utils.test.ts b/src/components/tx/security/tenderly/__tests__/utils.test.ts index fc18ad31d9..3005c06a73 100644 --- a/src/components/tx/security/tenderly/__tests__/utils.test.ts +++ b/src/components/tx/security/tenderly/__tests__/utils.test.ts @@ -19,8 +19,8 @@ const SIGNATURE_LENGTH = 65 * 2 const getPreValidatedSignature = (addr: string): string => generatePreValidatedSignature(addr).data describe('simulation utils', () => { - const safeContractInterface = new Interface(getSafeSingletonDeployment()?.abi || []) - const multiSendContractInterface = new Interface(getMultiSendCallOnlyDeployment()?.abi || []) + const safeContractInterface = new Interface(getSafeSingletonDeployment({ version: '1.3.0' })?.abi || []) + const multiSendContractInterface = new Interface(getMultiSendCallOnlyDeployment({ version: '1.3.0' })?.abi || []) const mockSafeAddress = zeroPadValue('0x0123', 20) const mockMultisendAddress = zeroPadValue('0x1234', 20) diff --git a/src/components/tx/security/tenderly/utils.ts b/src/components/tx/security/tenderly/utils.ts index ba6c411902..775ccf9c2d 100644 --- a/src/components/tx/security/tenderly/utils.ts +++ b/src/components/tx/security/tenderly/utils.ts @@ -121,7 +121,7 @@ export const _getMultiSendCallOnlyPayload = async ( params: MultiSendTransactionSimulationParams, ): Promise> => { const data = encodeMultiSendData(params.transactions) - const readOnlyMultiSendContract = await getReadOnlyMultiSendCallOnlyContract(params.safe.chainId) + const readOnlyMultiSendContract = await getReadOnlyMultiSendCallOnlyContract(params.safe.chainId, params.safe.version) return { to: await readOnlyMultiSendContract.getAddress(), diff --git a/src/features/recovery/services/__tests__/recovery-state.test.ts b/src/features/recovery/services/__tests__/recovery-state.test.ts index ec657d56b7..b70a2d7a0b 100644 --- a/src/features/recovery/services/__tests__/recovery-state.test.ts +++ b/src/features/recovery/services/__tests__/recovery-state.test.ts @@ -72,7 +72,9 @@ describe('recovery-state', () => { const safeAbi = getSafeSingletonDeployment({ network: chainId, version })!.abi const safeInterface = new Interface(safeAbi) - const multiSendAbi = getMultiSendCallOnlyDeployment({ network: chainId }) + const multiSendAbi = + getMultiSendCallOnlyDeployment({ network: chainId, version }) ?? + getMultiSendCallOnlyDeployment({ network: chainId, version: '1.3.0' }) const multiSendInterface = new Interface(multiSendAbi!.abi) const multiSendData = encodeMultiSendData([ @@ -107,7 +109,9 @@ describe('recovery-state', () => { const safeAbi = getSafeSingletonDeployment({ network: chainId, version })!.abi const safeInterface = new Interface(safeAbi) - const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId }) + const multiSendDeployment = + getMultiSendCallOnlyDeployment({ network: chainId, version }) ?? + getMultiSendCallOnlyDeployment({ network: chainId, version: '1.3.0' }) const multiSendInterface = new Interface(multiSendDeployment!.abi) const multiSendData = encodeMultiSendData([ @@ -142,7 +146,7 @@ describe('recovery-state', () => { const safeAbi = getSafeSingletonDeployment({ network: chainId, version })!.abi const safeInterface = new Interface(safeAbi) - const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId })! + const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId, version })! const multiSendInterface = new Interface(multiSendDeployment.abi) const multiSendData = encodeMultiSendData([ @@ -177,7 +181,7 @@ describe('recovery-state', () => { const safeAbi = getSafeSingletonDeployment({ network: chainId, version })!.abi const safeInterface = new Interface(safeAbi) - const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId })! + const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId, version: '1.3.0' })! const multiSendInterface = new Interface(multiSendDeployment.abi) const multiSendData = encodeMultiSendData([ diff --git a/src/features/recovery/services/__tests__/transaction-list.test.ts b/src/features/recovery/services/__tests__/transaction-list.test.ts index 296e80b8d7..431a86fe47 100644 --- a/src/features/recovery/services/__tests__/transaction-list.test.ts +++ b/src/features/recovery/services/__tests__/transaction-list.test.ts @@ -124,6 +124,7 @@ describe('getRecoveredSafeInfo', () => { const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: safe.chainId, + version: safe.version ?? undefined, }) const multiSendAddress = multiSendDeployment!.networkAddresses[safe.chainId] const multiSendInterface = new Interface(multiSendDeployment!.abi) diff --git a/src/features/recovery/services/recovery-state.ts b/src/features/recovery/services/recovery-state.ts index b25075dd2c..6303750e05 100644 --- a/src/features/recovery/services/recovery-state.ts +++ b/src/features/recovery/services/recovery-state.ts @@ -44,6 +44,8 @@ export function _isMaliciousRecovery({ safeAddress: string transaction: Pick }) { + const BASE_MULTI_SEND_CALL_ONLY_VERSION = '1.3.0' + const isMultiSend = isMultiSendCalldata(transaction.data) const transactions = isMultiSend ? decodeMultiSendTxs(transaction.data) : [transaction] @@ -52,7 +54,9 @@ export function _isMaliciousRecovery({ return !sameAddress(transaction.to, safeAddress) } - const multiSendDeployment = getMultiSendCallOnlyDeployment({ network: chainId }) + const multiSendDeployment = + getMultiSendCallOnlyDeployment({ network: chainId, version: version ?? undefined }) ?? + getMultiSendCallOnlyDeployment({ network: chainId, version: BASE_MULTI_SEND_CALL_ONLY_VERSION }) if (!multiSendDeployment) { return true diff --git a/src/services/contracts/__tests__/deployments.test.ts b/src/services/contracts/__tests__/deployments.test.ts index 8cd9f44475..dc194418f0 100644 --- a/src/services/contracts/__tests__/deployments.test.ts +++ b/src/services/contracts/__tests__/deployments.test.ts @@ -232,6 +232,45 @@ describe('deployments', () => { }) }) + describe('getMultiSendCallOnlyContractDeployment', () => { + it('should return the versioned deployment for supported version/chain', () => { + const expected = safeDeployments.getMultiSendCallOnlyDeployment({ + version: '1.3.0', // First available version + network: '1', + }) + + expect(expected).toBeDefined() + const deployment = deployments.getMultiSendCallOnlyContractDeployment('1', '1.3.0') + expect(deployment).toStrictEqual(expected) + }) + + it('should return undefined for supported version/unsupported chain', () => { + const deployment = deployments.getMultiSendCallOnlyContractDeployment('69420', '1.3.0') + expect(deployment).toBe(undefined) + }) + + it('should return undefined for unsupported version/chain', () => { + const deployment = deployments.getMultiSendCallOnlyContractDeployment('69420', '1.2.3') + expect(deployment).toBe(undefined) + }) + + it('should return the latest deployment for no version/supported chain', () => { + const expected = safeDeployments.getMultiSendCallOnlyDeployment({ + version: LATEST_SAFE_VERSION, + network: '1', + }) + + expect(expected).toBeDefined() + const deployment = deployments.getMultiSendCallOnlyContractDeployment('1', null) + expect(deployment).toStrictEqual(expected) + }) + + it('should return undefined for no version/unsupported chain', () => { + const deployment = deployments.getMultiSendCallOnlyContractDeployment('69420', null) + expect(deployment).toBe(undefined) + }) + }) + describe('getFallbackHandlerContractDeployment', () => { it('should return the versioned deployment for supported version/chain', () => { const expected = safeDeployments.getFallbackHandlerDeployment({ diff --git a/src/services/contracts/__tests__/safeContracts.test.ts b/src/services/contracts/__tests__/safeContracts.test.ts index 754b914f59..6e269bdc85 100644 --- a/src/services/contracts/__tests__/safeContracts.test.ts +++ b/src/services/contracts/__tests__/safeContracts.test.ts @@ -1,5 +1,5 @@ import { ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' -import { _getValidatedGetContractProps, isValidMasterCopy } from '../safeContracts' +import { _getValidatedGetContractProps, isValidMasterCopy, _getMinimumMultiSendCallOnlyVersion } from '../safeContracts' describe('safeContracts', () => { describe('isValidMasterCopy', () => { @@ -49,4 +49,18 @@ describe('safeContracts', () => { expect(() => _getValidatedGetContractProps('')).toThrow(' is not a valid Safe Account version') }) }) + + describe('_getMinimumMultiSendCallOnlyVersion', () => { + it('should return the initial version if the Safe version is null', () => { + expect(_getMinimumMultiSendCallOnlyVersion(null)).toBe('1.3.0') + }) + + it('should return the initial version if the Safe version is lower than the initial version', () => { + expect(_getMinimumMultiSendCallOnlyVersion('1.0.0')).toBe('1.3.0') + }) + + it('should return the Safe version if the Safe version is higher than the initial version', () => { + expect(_getMinimumMultiSendCallOnlyVersion('1.4.1')).toBe('1.4.1') + }) + }) }) diff --git a/src/services/contracts/deployments.ts b/src/services/contracts/deployments.ts index 37792c4b1c..181b8ff5b6 100644 --- a/src/services/contracts/deployments.ts +++ b/src/services/contracts/deployments.ts @@ -2,6 +2,7 @@ import semverSatisfies from 'semver/functions/satisfies' import { getSafeSingletonDeployment, getSafeL2SingletonDeployment, + getMultiSendCallOnlyDeployment, getFallbackHandlerDeployment, getProxyFactoryDeployment, getSignMessageLibDeployment, @@ -64,6 +65,10 @@ export const getSafeContractDeployment = ( return _tryDeploymentVersions(getDeployment, chain.chainId, safeVersion) } +export const getMultiSendCallOnlyContractDeployment = (chainId: string, safeVersion: SafeInfo['version']) => { + return _tryDeploymentVersions(getMultiSendCallOnlyDeployment, chainId, safeVersion) +} + export const getFallbackHandlerContractDeployment = (chainId: string, safeVersion: SafeInfo['version']) => { return _tryDeploymentVersions(getFallbackHandlerDeployment, chainId, safeVersion) } diff --git a/src/services/contracts/safeContracts.ts b/src/services/contracts/safeContracts.ts index bd95768fd7..2a99c4ba23 100644 --- a/src/services/contracts/safeContracts.ts +++ b/src/services/contracts/safeContracts.ts @@ -1,5 +1,6 @@ import { getFallbackHandlerContractDeployment, + getMultiSendCallOnlyContractDeployment, getProxyFactoryContractDeployment, getSafeContractDeployment, getSignMessageLibContractDeployment, @@ -11,9 +12,9 @@ import type { GetContractProps, SafeVersion } from '@safe-global/safe-core-sdk-t import { assertValidSafeVersion, createEthersAdapter, createReadOnlyEthersAdapter } from '@/hooks/coreSDK/safeCoreSDK' import type { BrowserProvider } from 'ethers' import type { EthersAdapter, SafeContractEthers, SignMessageLibEthersContract } from '@safe-global/protocol-kit' +import semver from 'semver' import type CompatibilityFallbackHandlerEthersContract from '@safe-global/protocol-kit/dist/src/adapters/ethers/contracts/CompatibilityFallbackHandler/CompatibilityFallbackHandlerEthersContract' -import { getMultiSendCallOnlyDeployment } from '@safe-global/safe-deployments' // `UNKNOWN` is returned if the mastercopy does not match supported ones // @see https://github.com/safe-global/safe-client-gateway/blob/main/src/routes/safes/handlers/safes.rs#L28-L31 @@ -69,13 +70,37 @@ export const getReadOnlyGnosisSafeContract = async (chain: ChainInfo, safeVersio // MultiSend -export const getReadOnlyMultiSendCallOnlyContract = async (chainId: string) => { +export const _getMinimumMultiSendCallOnlyVersion = (safeVersion: SafeInfo['version']) => { + const INITIAL_CALL_ONLY_VERSION = '1.3.0' + + if (!safeVersion) { + return INITIAL_CALL_ONLY_VERSION + } + + return semver.gte(safeVersion, INITIAL_CALL_ONLY_VERSION) ? safeVersion : INITIAL_CALL_ONLY_VERSION +} + +export const getMultiSendCallOnlyContract = async ( + chainId: string, + safeVersion: SafeInfo['version'], + provider: BrowserProvider, +) => { + const ethAdapter = await createEthersAdapter(provider) + const multiSendVersion = _getMinimumMultiSendCallOnlyVersion(safeVersion) + + return ethAdapter.getMultiSendCallOnlyContract({ + singletonDeployment: getMultiSendCallOnlyContractDeployment(chainId, multiSendVersion), + ..._getValidatedGetContractProps(safeVersion), + }) +} + +export const getReadOnlyMultiSendCallOnlyContract = async (chainId: string, safeVersion: SafeInfo['version']) => { const ethAdapter = createReadOnlyEthersAdapter() - const singletonDeployment = getMultiSendCallOnlyDeployment({ network: chainId }) - if (!singletonDeployment) throw new Error('No deployment found for MultiSendCallOnly') + const multiSendVersion = _getMinimumMultiSendCallOnlyVersion(safeVersion) + return ethAdapter.getMultiSendCallOnlyContract({ - singletonDeployment: getMultiSendCallOnlyDeployment({ network: chainId }), - safeVersion: singletonDeployment.version as SafeVersion, + singletonDeployment: getMultiSendCallOnlyContractDeployment(chainId, multiSendVersion), + ..._getValidatedGetContractProps(safeVersion), }) } diff --git a/src/services/security/modules/DelegateCallModule/index.test.ts b/src/services/security/modules/DelegateCallModule/index.test.ts index a28965e622..a7278a739f 100644 --- a/src/services/security/modules/DelegateCallModule/index.test.ts +++ b/src/services/security/modules/DelegateCallModule/index.test.ts @@ -34,6 +34,7 @@ describe('DelegateCallModule', () => { const multiSend = getMultiSendCallOnlyDeployment({ network: CHAIN_ID, + version: SAFE_VERSION, })!.defaultAddress const recipient1 = toBeHex('0x2', 20) diff --git a/src/services/security/modules/DelegateCallModule/index.ts b/src/services/security/modules/DelegateCallModule/index.ts index 5755eeacea..85555bafc8 100644 --- a/src/services/security/modules/DelegateCallModule/index.ts +++ b/src/services/security/modules/DelegateCallModule/index.ts @@ -1,10 +1,10 @@ import { OperationType } from '@safe-global/safe-core-sdk-types' +import { getMultiSendCallOnlyContractDeployment } from '@/services/contracts/deployments' import type { SafeTransaction } from '@safe-global/safe-core-sdk-types' import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' import { SecuritySeverity } from '../types' import type { SecurityModule, SecurityResponse } from '../types' -import { getMultiSendCallOnlyDeployment } from '@safe-global/safe-deployments' type DelegateCallModuleRequest = { chainId: string @@ -28,7 +28,7 @@ export class DelegateCallModule implements SecurityModule Date: Wed, 12 Jun 2024 14:02:34 +0200 Subject: [PATCH 081/154] fix: do not pass negative app id (#3836) --- .../walletconnect/__tests__/WalletConnectContext.test.tsx | 1 - .../walletconnect/components/WalletConnectProvider/index.tsx | 1 - src/services/safe-wallet-provider/index.ts | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx b/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx index 755d0dc258..b9a504ab8a 100644 --- a/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx +++ b/src/features/walletconnect/__tests__/WalletConnectContext.test.tsx @@ -410,7 +410,6 @@ describe('WalletConnectProvider', () => { 1, { method: 'fake', params: [] }, { - id: -1, name: 'name', description: 'description', url: 'https://apps-portal.safe.global/wallet-connect', diff --git a/src/features/walletconnect/components/WalletConnectProvider/index.tsx b/src/features/walletconnect/components/WalletConnectProvider/index.tsx index af02ada6c8..fd9b0442fb 100644 --- a/src/features/walletconnect/components/WalletConnectProvider/index.tsx +++ b/src/features/walletconnect/components/WalletConnectProvider/index.tsx @@ -88,7 +88,6 @@ export const WalletConnectProvider = ({ children }: { children: ReactNode }) => // Get response from Safe Wallet Provider return safeWalletProvider.request(event.id, event.params.request, { - id: -1, url: session.peer.metadata.url, name: getPeerName(session.peer) || 'WalletConnect', description: session.peer.metadata.description, diff --git a/src/services/safe-wallet-provider/index.ts b/src/services/safe-wallet-provider/index.ts index 58623772bb..d44109b528 100644 --- a/src/services/safe-wallet-provider/index.ts +++ b/src/services/safe-wallet-provider/index.ts @@ -15,7 +15,7 @@ type SafeSettings = { type GetCapabilitiesResult = Record<`0x${string}`, Record> export type AppInfo = { - id: number + id?: number name: string description: string url: string From d826efa1fa3d62f08d7f05950cca49996a427236 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 12 Jun 2024 16:28:18 +0200 Subject: [PATCH 082/154] 1.38.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b231eae244..9b19e9ad80 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.37.2", + "version": "1.38.0", "type": "module", "scripts": { "dev": "next dev", @@ -159,4 +159,4 @@ "minimumChangeThreshold": 0, "showDetails": true } -} \ No newline at end of file +} From 067d73802ad0a509ec415c26b922d8240c061261 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:52:34 +0200 Subject: [PATCH 083/154] Fix: Chip warnings about nested tags (#3840) --- src/components/sidebar/SidebarNavigation/config.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index 3829c7d4f3..ea7778a259 100644 --- a/src/components/sidebar/SidebarNavigation/config.tsx +++ b/src/components/sidebar/SidebarNavigation/config.tsx @@ -33,7 +33,7 @@ export const navItems: NavItem[] = [ label: 'Swap', icon: , href: AppRoutes.swap, - tag: , + tag: , }, { label: 'Transactions', From 6d2f6beb42a5d21e0b97a889bbde0367b10b3765 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Thu, 13 Jun 2024 16:53:37 +0200 Subject: [PATCH 084/154] feat: return more info in appCommunicator getInfo (#3839) The return type of the getInfo endpoint now returns more data about the safe. --- package.json | 2 +- .../safe-apps/AppFrame/useAppCommunicator.ts | 4 +- .../safe-apps/AppFrame/useGetSafeInfo.ts | 21 ++++- yarn.lock | 82 +++++++------------ 4 files changed, 54 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 9b19e9ad80..cfcedea768 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@reduxjs/toolkit": "^1.9.5", "@safe-global/api-kit": "^2.3.2", "@safe-global/protocol-kit": "^3.1.1", - "@safe-global/safe-apps-sdk": "^9.0.0-next.1", + "@safe-global/safe-apps-sdk": "^9.1.0", "@safe-global/safe-deployments": "^1.36.0", "@safe-global/safe-gateway-typescript-sdk": "3.21.1", "@safe-global/safe-modules-deployments": "^1.2.0", diff --git a/src/components/safe-apps/AppFrame/useAppCommunicator.ts b/src/components/safe-apps/AppFrame/useAppCommunicator.ts index 90822aa4f6..1342d32857 100644 --- a/src/components/safe-apps/AppFrame/useAppCommunicator.ts +++ b/src/components/safe-apps/AppFrame/useAppCommunicator.ts @@ -15,13 +15,13 @@ import type { GetTxBySafeTxHashParams, RequestId, RPCPayload, - SafeInfo, SendTransactionRequestParams, SendTransactionsParams, SignMessageParams, SignTypedMessageParams, ChainInfo, SafeBalances, + SafeInfoExtended, } from '@safe-global/safe-apps-sdk' import { Methods, RPC_CALLS } from '@safe-global/safe-apps-sdk' import type { Permission, PermissionRequest } from '@safe-global/safe-apps-sdk/dist/types/types/permissions' @@ -56,7 +56,7 @@ export type UseAppCommunicatorHandlers = { onGetTxBySafeTxHash: (transactionId: string) => Promise onGetEnvironmentInfo: () => EnvironmentInfo onGetSafeBalances: (currency: string) => Promise - onGetSafeInfo: () => SafeInfo + onGetSafeInfo: () => SafeInfoExtended onGetChainInfo: () => ChainInfo | undefined onGetPermissions: (origin: string) => Permission[] onSetPermissions: (permissionsRequest?: SafePermissionsRequest) => void diff --git a/src/components/safe-apps/AppFrame/useGetSafeInfo.ts b/src/components/safe-apps/AppFrame/useGetSafeInfo.ts index ad3a4048b3..1dcb0c6c09 100644 --- a/src/components/safe-apps/AppFrame/useGetSafeInfo.ts +++ b/src/components/safe-apps/AppFrame/useGetSafeInfo.ts @@ -19,9 +19,28 @@ const useGetSafeInfo = () => { owners: safe.owners.map((owner) => owner.value), threshold: safe.threshold, isReadOnly: !isOwner, + nonce: safe.nonce, + implementation: safe.implementation.value, + modules: safe.modules ? safe.modules.map((module) => module.value) : null, + fallbackHandler: safe.fallbackHandler ? safe.fallbackHandler?.value : null, + guard: safe.guard?.value || null, + version: safe.version, network: getLegacyChainName(chainName || '', chainId).toUpperCase(), }), - [chainId, chainName, isOwner, safeAddress, safe.owners, safe.threshold], + [ + chainId, + chainName, + isOwner, + safeAddress, + safe.owners, + safe.threshold, + safe.nonce, + safe.implementation, + safe.modules, + safe.fallbackHandler, + safe.guard, + safe.version, + ], ) } diff --git a/yarn.lock b/yarn.lock index 1d0f4f9248..16403368c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4162,13 +4162,13 @@ web3-core "^1.10.3" web3-utils "^1.10.3" -"@safe-global/safe-apps-sdk@^9.0.0-next.1": - version "9.0.0-next.1" - resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.0.0-next.1.tgz#762186e36a3580c93e7efe7f49259c75043c5b7b" - integrity sha512-0XsgPXCOXx0wKhumKX/b65alRgd3umfNhsCq/S0K/hhMD/Uh6BTOzHRNFCosbnGvWQT75Hp3Y11LVS7ZD49t/w== +"@safe-global/safe-apps-sdk@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.1.0.tgz#0e65913e0f202e529ed3c846e0f5a98c2d35aa98" + integrity sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q== dependencies: "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" - viem "^1.6.0" + viem "^2.1.1" "@safe-global/safe-core-sdk-types@^4.1.1": version "4.1.1" @@ -4193,20 +4193,25 @@ integrity sha512-7nakIjcRSs6781LkizYpIfXh1DYlkUDqyALciqz/BjFU/S97sVjZdL4cuKsG9NEarytE+f6p0Qbq2Bo1aocVUA== "@safe-global/safe-gateway-typescript-sdk@^3.5.3": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.19.0.tgz#18637c205c83bfc0a6be5fddbf202d6bb4927302" - integrity sha512-TRlP05KY6t3wjLJ74FiirWlEt3xTclnUQM2YdYto1jx5G1o0meMnugIUZXhzm7Bs3rDEDNhz/aDf2KMSZtoCFg== + version "3.21.2" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.2.tgz#2123a7429c2d9713365f51c359bfc055d4c8e913" + integrity sha512-N9Y2CKPBVbc8FbOKzqepy8TJUY2VILX7bmxV4ruByLJvR9PBnGvGfnOhw975cDn6PmSziXL0RaUWHpSW23rsng== "@safe-global/safe-modules-deployments@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-modules-deployments/-/safe-modules-deployments-1.2.0.tgz#ca871c3f553cd16cbea1aac8f8be16498329a7d3" integrity sha512-/pjHIPaYwGRM5oOB7lc+yf28fWEq7twNP5dJxpLFgG/9UR4E3F+XfFdYkpP22eIvmOkBwCJXJZfPfESh9WSF2w== -"@scure/base@~1.1.0", "@scure/base@~1.1.2": +"@scure/base@~1.1.0": version "1.1.3" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f" integrity sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q== +"@scure/base@~1.1.2": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" + integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== + "@scure/bip32@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.0.tgz#dea45875e7fbc720c2b4560325f1cf5d2246d95b" @@ -7254,10 +7259,10 @@ abab@^2.0.6: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== -abitype@0.9.8: - version "0.9.8" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" - integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== +abitype@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" + integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== abort-controller@^3.0.0: version "3.0.0" @@ -13070,10 +13075,10 @@ isomorphic-ws@^4.0.1: resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== -isows@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.3.tgz#93c1cf0575daf56e7120bab5c8c448b0809d0d74" - integrity sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg== +isows@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.4.tgz#810cd0d90cc4995c26395d2aa4cfa4037ebdf061" + integrity sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ== isstream@~0.1.2: version "0.1.2" @@ -17703,16 +17708,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -17800,14 +17796,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -19049,18 +19038,18 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -viem@^1.6.0: - version "1.20.3" - resolved "https://registry.yarnpkg.com/viem/-/viem-1.20.3.tgz#8b8360daee622295f5385949c02c86d943d14e0f" - integrity sha512-7CrmeCb2KYkeCgUmUyb1hsf+IX/PLwi+Np+Vm4YUTPeG82y3HRSgGHSaCOp3d0YtR2kXD3nv9y5kE7LBFE+wWw== +viem@^2.1.1: + version "2.13.8" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.13.8.tgz#d6aaeecc84e5ee5cac1566f103ed4cf0335ef811" + integrity sha512-JX8dOrCJKazNVs7YAahXnX+NANp0nlK16GyYjtQXILnar1daCPsLy4uzKgZDBVBD6DdRP2lsbPfo4X7QX3q5EQ== dependencies: "@adraffy/ens-normalize" "1.10.0" "@noble/curves" "1.2.0" "@noble/hashes" "1.3.2" "@scure/bip32" "1.3.2" "@scure/bip39" "1.2.1" - abitype "0.9.8" - isows "1.0.3" + abitype "1.0.0" + isows "1.0.4" ws "8.13.0" vm-browserify@^1.1.2: @@ -20035,7 +20024,7 @@ workbox-window@7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -20053,15 +20042,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 010d629312b23ba560aa6b6c4a83d143260d1393 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:23:40 +0200 Subject: [PATCH 085/154] Feat: private key signer (#3784) --- package.json | 1 - src/hooks/wallets/consts.ts | 2 + src/hooks/wallets/wallets.ts | 14 +- src/hooks/wallets/web3.ts | 2 +- src/pages/_app.tsx | 3 + src/pages/licenses.tsx | 8 - src/services/ExternalStore.ts | 2 +- src/services/local-storage/session.ts | 6 + .../private-key-module/PkModulePopup.tsx | 43 + src/services/private-key-module/icon.ts | 5 + src/services/private-key-module/index.ts | 126 +++ .../private-key-module/pk-popup-store.ts | 23 + src/tests/e2e-wallet.ts | 90 +- src/utils/wallets.ts | 3 + yarn.lock | 866 +----------------- 15 files changed, 333 insertions(+), 861 deletions(-) create mode 100644 src/services/private-key-module/PkModulePopup.tsx create mode 100644 src/services/private-key-module/icon.ts create mode 100644 src/services/private-key-module/index.ts create mode 100644 src/services/private-key-module/pk-popup-store.ts diff --git a/package.json b/package.json index cfcedea768..4a1b6d7360 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "@safe-global/safe-modules-deployments": "^1.2.0", "@sentry/react": "^7.91.0", "@spindl-xyz/attribution-lite": "^1.4.0", - "@truffle/hdwallet-provider": "^2.1.4", "@walletconnect/utils": "^2.13.1", "@walletconnect/web3wallet": "^1.12.1", "@web3-onboard/coinbase": "^2.2.6", diff --git a/src/hooks/wallets/consts.ts b/src/hooks/wallets/consts.ts index 829376aaac..e12c1ccd21 100644 --- a/src/hooks/wallets/consts.ts +++ b/src/hooks/wallets/consts.ts @@ -5,6 +5,7 @@ export const enum WALLET_KEYS { LEDGER = 'LEDGER', TREZOR = 'TREZOR', KEYSTONE = 'KEYSTONE', + PK = 'PK', } // TODO: Check if undefined is needed as a return type, possibly couple this with WALLET_MODULES @@ -15,4 +16,5 @@ export const CGW_NAMES: { [key in WALLET_KEYS]: string | undefined } = { [WALLET_KEYS.LEDGER]: 'ledger', [WALLET_KEYS.TREZOR]: 'trezor', [WALLET_KEYS.KEYSTONE]: 'keystone', + [WALLET_KEYS.PK]: 'pk', } diff --git a/src/hooks/wallets/wallets.ts b/src/hooks/wallets/wallets.ts index eaac33f065..6cab345d53 100644 --- a/src/hooks/wallets/wallets.ts +++ b/src/hooks/wallets/wallets.ts @@ -1,4 +1,4 @@ -import { CYPRESS_MNEMONIC, TREZOR_APP_URL, TREZOR_EMAIL, WC_PROJECT_ID } from '@/config/constants' +import { CYPRESS_MNEMONIC, IS_PRODUCTION, TREZOR_APP_URL, TREZOR_EMAIL, WC_PROJECT_ID } from '@/config/constants' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import type { InitOptions } from '@web3-onboard/core' import coinbaseModule from '@web3-onboard/coinbase' @@ -7,6 +7,7 @@ import keystoneModule from '@web3-onboard/keystone/dist/index' import ledgerModule from '@web3-onboard/ledger/dist/index' import trezorModule from '@web3-onboard/trezor' import walletConnect from '@web3-onboard/walletconnect' +import pkModule from '@/services/private-key-module' import e2eWalletModule from '@/tests/e2e-wallet' import { CGW_NAMES, WALLET_KEYS } from './consts' @@ -38,7 +39,7 @@ const walletConnectV2 = (chain: ChainInfo) => { }) } -const WALLET_MODULES: { [key in WALLET_KEYS]: (chain: ChainInfo) => WalletInit } = { +const WALLET_MODULES: Partial<{ [key in WALLET_KEYS]: (chain: ChainInfo) => WalletInit }> = { [WALLET_KEYS.INJECTED]: () => injectedWalletModule() as WalletInit, [WALLET_KEYS.WALLETCONNECT_V2]: (chain) => walletConnectV2(chain) as WalletInit, [WALLET_KEYS.COINBASE]: () => coinbaseModule({ darkMode: prefersDarkMode() }) as WalletInit, @@ -47,6 +48,11 @@ const WALLET_MODULES: { [key in WALLET_KEYS]: (chain: ChainInfo) => WalletInit } [WALLET_KEYS.KEYSTONE]: () => keystoneModule() as WalletInit, } +// Testing wallet module +if (!IS_PRODUCTION) { + WALLET_MODULES[WALLET_KEYS.PK] = (chain) => pkModule(chain.chainId, chain.rpcUri) as WalletInit +} + export const getAllWallets = (chain: ChainInfo): WalletInits => { return Object.values(WALLET_MODULES).map((module) => module(chain)) } @@ -58,12 +64,12 @@ export const isWalletSupported = (disabledWallets: string[], walletLabel: string export const getSupportedWallets = (chain: ChainInfo): WalletInits => { if (window.Cypress && CYPRESS_MNEMONIC) { - return [e2eWalletModule(chain.rpcUri) as WalletInit] + return [e2eWalletModule(chain.chainId, chain.rpcUri) as WalletInit] } const enabledWallets = Object.entries(WALLET_MODULES).filter(([key]) => isWalletSupported(chain.disabledWallets, key)) if (enabledWallets.length === 0) { - return [WALLET_MODULES.INJECTED(chain)] + return [injectedWalletModule()] } return enabledWallets.map(([, module]) => module(chain)) diff --git a/src/hooks/wallets/web3.ts b/src/hooks/wallets/web3.ts index 2bb3b649d0..ae31d511c3 100644 --- a/src/hooks/wallets/web3.ts +++ b/src/hooks/wallets/web3.ts @@ -29,7 +29,7 @@ export const getRpcServiceUrl = (rpcUri: RpcUri): string => { export const createWeb3ReadOnly = (chain: ChainInfo, customRpc?: string): JsonRpcProvider | undefined => { const url = customRpc || getRpcServiceUrl(chain.rpcUri) if (!url) return - return new JsonRpcProvider(url, undefined, { + return new JsonRpcProvider(url, Number(chain.chainId), { staticNetwork: true, batchMaxCount: BATCH_MAX_COUNT, }) diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 48d935f13d..605929f139 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -42,6 +42,7 @@ import { useNotificationTracking } from '@/components/settings/PushNotifications import Recovery from '@/features/recovery/components/Recovery' import WalletProvider from '@/components/common/WalletProvider' import CounterfactualHooks from '@/features/counterfactual/CounterfactualHooks' +import PkModulePopup from '@/services/private-key-module/PkModulePopup' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -128,6 +129,8 @@ const WebCoreApp = ({ + + diff --git a/src/pages/licenses.tsx b/src/pages/licenses.tsx index defd4a398f..7d271c8981 100644 --- a/src/pages/licenses.tsx +++ b/src/pages/licenses.tsx @@ -482,14 +482,6 @@ const SafeLicenses = () => ( - - @truffle/hdwallet-provider - - - https://github.com/trufflesuite/truffle/blob/develop/LICENSE - - - @web3-onboard/coinbase diff --git a/src/services/ExternalStore.ts b/src/services/ExternalStore.ts index 34b65821e9..fbd5d71960 100644 --- a/src/services/ExternalStore.ts +++ b/src/services/ExternalStore.ts @@ -23,7 +23,7 @@ class ExternalStore { } } - private readonly subscribe = (listener: Listener): (() => void) => { + public readonly subscribe = (listener: Listener): (() => void) => { this.listeners.add(listener) return () => { this.listeners.delete(listener) diff --git a/src/services/local-storage/session.ts b/src/services/local-storage/session.ts index 1d297b5fcd..1a1a45178d 100644 --- a/src/services/local-storage/session.ts +++ b/src/services/local-storage/session.ts @@ -2,4 +2,10 @@ import Storage from './Storage' const session = new Storage(typeof window !== 'undefined' ? window.sessionStorage : undefined) +export const sessionItem = (key: string) => ({ + get: () => session.getItem(key), + set: (value: T) => session.setItem(key, value), + remove: () => session.removeItem(key), +}) + export default session diff --git a/src/services/private-key-module/PkModulePopup.tsx b/src/services/private-key-module/PkModulePopup.tsx new file mode 100644 index 0000000000..c226cd81c3 --- /dev/null +++ b/src/services/private-key-module/PkModulePopup.tsx @@ -0,0 +1,43 @@ +import type { FormEvent } from 'react' +import { Button, TextField, Typography, Box } from '@mui/material' +import ModalDialog from '@/components/common/ModalDialog' +import pkStore from './pk-popup-store' +const { useStore, setStore } = pkStore + +const PkModulePopup = () => { + const { isOpen, privateKey } = useStore() ?? { isOpen: false, privateKey: '' } + + const onClose = () => { + setStore({ isOpen: false, privateKey }) + } + + const onSubmit = (e: FormEvent) => { + e.preventDefault() + const privateKey = (e.target as unknown as { 'private-key': HTMLInputElement })['private-key'].value + + setStore({ + isOpen: false, + privateKey, + }) + } + + return ( + + + + Enter your signer private key. The key will be saved for the duration of this browser session. + + + + + + + + + + ) +} + +export default PkModulePopup diff --git a/src/services/private-key-module/icon.ts b/src/services/private-key-module/icon.ts new file mode 100644 index 0000000000..69c435dfb8 --- /dev/null +++ b/src/services/private-key-module/icon.ts @@ -0,0 +1,5 @@ +const icon = ` + +` + +export default icon diff --git a/src/services/private-key-module/index.ts b/src/services/private-key-module/index.ts new file mode 100644 index 0000000000..88d0cb4e5c --- /dev/null +++ b/src/services/private-key-module/index.ts @@ -0,0 +1,126 @@ +import { JsonRpcProvider, Wallet } from 'ethers' +import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { type WalletInit, createEIP1193Provider } from '@web3-onboard/common' +import { getRpcServiceUrl } from '@/hooks/wallets/web3' +import pkPopupStore from './pk-popup-store' +import { numberToHex } from '@/utils/hex' + +export const PRIVATE_KEY_MODULE_LABEL = 'Private key' + +async function getPrivateKey() { + const savedKey = pkPopupStore.getStore()?.privateKey + if (savedKey) return savedKey + + pkPopupStore.setStore({ + isOpen: true, + privateKey: '', + }) + + return new Promise((resolve) => { + const unsubscribe = pkPopupStore.subscribe(() => { + unsubscribe() + resolve(pkPopupStore.getStore()?.privateKey ?? '') + }) + }) +} + +let currentChainId = '' +let currentRpcUri = '' + +const PrivateKeyModule = (chainId: ChainInfo['chainId'], rpcUri: ChainInfo['rpcUri']): WalletInit => { + currentChainId = chainId + currentRpcUri = getRpcServiceUrl(rpcUri) + + return () => { + return { + label: PRIVATE_KEY_MODULE_LABEL, + getIcon: async () => (await import('./icon')).default, + getInterface: async () => { + const privateKey = await getPrivateKey() + if (!privateKey) { + throw new Error('You rejected the connection') + } + + let provider: JsonRpcProvider + let wallet: Wallet + let lastChainId = '' + const chainChangedListeners = new Set<(chainId: string) => void>() + + const updateProvider = () => { + console.log('[Private key signer] Updating provider to chainId', currentChainId, currentRpcUri) + provider?.destroy() + provider = new JsonRpcProvider(currentRpcUri, Number(currentChainId), { staticNetwork: true }) + wallet = new Wallet(privateKey, provider) + lastChainId = currentChainId + chainChangedListeners.forEach((listener) => listener(numberToHex(Number(currentChainId)))) + } + + updateProvider() + + return { + provider: createEIP1193Provider( + { + on: (event: string, listener: (...args: any[]) => void) => { + if (event === 'accountsChanged') { + return + } else if (event === 'chainChanged') { + chainChangedListeners.add(listener) + } else { + provider.on(event, listener) + } + }, + + request: async (request: { method: string; params: any[] }) => { + if (currentChainId !== lastChainId) { + updateProvider() + } + return provider.send(request.method, request.params) + }, + + disconnect: () => { + pkPopupStore.setStore({ + isOpen: false, + privateKey: '', + }) + }, + }, + { + eth_chainId: async () => currentChainId, + + // @ts-ignore + eth_getCode: async ({ params }) => provider.getCode(params[0], params[1]), + + eth_accounts: async () => [wallet.address], + eth_requestAccounts: async () => [wallet.address], + + eth_call: async ({ params }: { params: any }) => wallet.call(params[0]), + + eth_sendTransaction: async ({ params }) => { + const tx = await wallet.sendTransaction(params[0] as any) + return tx.hash // return transaction hash + }, + + personal_sign: async ({ params }) => { + const signedMessage = wallet.signingKey.sign(params[0]) + return signedMessage.serialized + }, + + eth_signTypedData: async ({ params }) => { + return await wallet.signTypedData(params[1].domain, params[1].data, params[1].value) + }, + + // @ts-ignore + wallet_switchEthereumChain: async ({ params }) => { + console.log('[Private key signer] Switching chain', params) + updateProvider() + }, + }, + ), + } + }, + platforms: ['desktop'], + } + } +} + +export default PrivateKeyModule diff --git a/src/services/private-key-module/pk-popup-store.ts b/src/services/private-key-module/pk-popup-store.ts new file mode 100644 index 0000000000..5779540b50 --- /dev/null +++ b/src/services/private-key-module/pk-popup-store.ts @@ -0,0 +1,23 @@ +import ExternalStore from '@/services/ExternalStore' +import { sessionItem } from '@/services/local-storage/session' + +type PkModulePopupStore = { + isOpen: boolean + privateKey: string +} + +const defaultValue = { + isOpen: false, + privateKey: '', +} + +const STORAGE_KEY = 'privateKeyModulePK' +const pkStorage = sessionItem(STORAGE_KEY) + +const popupStore = new ExternalStore(pkStorage.get() || defaultValue) + +popupStore.subscribe(() => { + pkStorage.set(popupStore.getStore() || defaultValue) +}) + +export default popupStore diff --git a/src/tests/e2e-wallet.ts b/src/tests/e2e-wallet.ts index 58d79323b3..702cedc973 100644 --- a/src/tests/e2e-wallet.ts +++ b/src/tests/e2e-wallet.ts @@ -1,30 +1,92 @@ +import { type HDNodeWallet, JsonRpcProvider, Wallet } from 'ethers' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import type { WalletInit } from '@web3-onboard/common' - -import { CYPRESS_MNEMONIC } from '@/config/constants' +import { type WalletInit, createEIP1193Provider } from '@web3-onboard/common' import { getRpcServiceUrl } from '@/hooks/wallets/web3' +import { numberToHex } from '@/utils/hex' +import { CYPRESS_MNEMONIC } from '@/config/constants' export const E2E_WALLET_NAME = 'E2E Wallet' -const e2eWalletModule = (rpcUri: ChainInfo['rpcUri']): WalletInit => { +let currentChainId = '' +let currentRpcUri = '' + +const E2EWalletMoule = (chainId: ChainInfo['chainId'], rpcUri: ChainInfo['rpcUri']): WalletInit => { + currentChainId = chainId + currentRpcUri = getRpcServiceUrl(rpcUri) + return () => { return { label: E2E_WALLET_NAME, getIcon: async () => '', getInterface: async () => { - const { createEIP1193Provider } = await import('@web3-onboard/common') + let provider: JsonRpcProvider + let wallet: HDNodeWallet + let lastChainId = '' + const chainChangedListeners = new Set<(chainId: string) => void>() - const { default: HDWalletProvider } = await import('@truffle/hdwallet-provider') + const updateProvider = () => { + provider?.destroy() + provider = new JsonRpcProvider(currentRpcUri, Number(currentChainId), { staticNetwork: true }) + wallet = Wallet.fromPhrase(CYPRESS_MNEMONIC, provider) + lastChainId = currentChainId + chainChangedListeners.forEach((listener) => listener(numberToHex(Number(currentChainId)))) + } - const provider = new HDWalletProvider({ - mnemonic: CYPRESS_MNEMONIC, - providerOrUrl: getRpcServiceUrl(rpcUri), - }) + updateProvider() return { - provider: createEIP1193Provider(provider.engine, { - eth_requestAccounts: async () => provider.getAddresses(), - }), + provider: createEIP1193Provider( + { + on: (event: string, listener: (...args: any[]) => void) => { + if (event === 'accountsChanged') { + return + } else if (event === 'chainChanged') { + chainChangedListeners.add(listener) + } else { + provider.on(event, listener) + } + }, + + request: async (request: { method: string; params: any[] }) => { + if (currentChainId !== lastChainId) { + updateProvider() + } + return provider.send(request.method, request.params) + }, + + disconnect: () => {}, + }, + { + eth_chainId: async () => currentChainId, + + // @ts-ignore + eth_getCode: async ({ params }) => provider.getCode(params[0], params[1]), + + eth_accounts: async () => [wallet.address], + eth_requestAccounts: async () => [wallet.address], + + eth_call: async ({ params }: { params: any }) => wallet.call(params[0]), + + eth_sendTransaction: async ({ params }) => { + const tx = await wallet.sendTransaction(params[0] as any) + return tx.hash // return transaction hash + }, + + personal_sign: async ({ params }) => { + const signedMessage = wallet.signingKey.sign(params[0]) + return signedMessage.serialized + }, + + eth_signTypedData: async ({ params }) => { + return await wallet.signTypedData(params[1].domain, params[1].data, params[1].value) + }, + + // @ts-ignore + wallet_switchEthereumChain: async ({ params }) => { + updateProvider() + }, + }, + ), } }, platforms: ['desktop'], @@ -32,4 +94,4 @@ const e2eWalletModule = (rpcUri: ChainInfo['rpcUri']): WalletInit => { } } -export default e2eWalletModule +export default E2EWalletMoule diff --git a/src/utils/wallets.ts b/src/utils/wallets.ts index 411ac1c408..9a2469e4d9 100644 --- a/src/utils/wallets.ts +++ b/src/utils/wallets.ts @@ -3,6 +3,7 @@ import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import { getWeb3ReadOnly, isSmartContract } from '@/hooks/wallets/web3' import { WALLET_KEYS } from '@/hooks/wallets/consts' import memoize from 'lodash/memoize' +import { PRIVATE_KEY_MODULE_LABEL } from '@/services/private-key-module' const WALLETCONNECT = 'WalletConnect' @@ -47,6 +48,8 @@ export const isSmartContractWallet = memoize( /* Check if the wallet is unlocked. */ export const isWalletUnlocked = async (walletName: string): Promise => { + if (walletName === PRIVATE_KEY_MODULE_LABEL) return true + const METAMASK_LIKE = ['MetaMask', 'Rabby Wallet', 'Zerion'] // Only MetaMask exposes a method to check if the wallet is unlocked diff --git a/yarn.lock b/yarn.lock index 16403368c4..71f0d89847 100644 --- a/yarn.lock +++ b/yarn.lock @@ -864,7 +864,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-runtime@^7.23.2", "@babel/plugin-transform-runtime@^7.5.5": +"@babel/plugin-transform-runtime@^7.23.2": version "7.24.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.3.tgz#dc58ad4a31810a890550365cc922e1ff5acb5d7f" integrity sha512-J0BuRPNlNqlMTRJ72eVptpt9VcInbxO6iP3jaxr+1NPhC0UkKL+6oeX6VXMEYdADnuqmMmsBspt4d5w8Y/TCbQ== @@ -1682,14 +1682,6 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== -"@ethereumjs/common@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.5.0.tgz#ec61551b31bef7a69d1dc634d8932468866a4268" - integrity sha512-DEHjW6e38o+JmB/NO3GZBpW4lpaiBpkFgXF6jLcJ6gETBYpEyaA5nTimsWBUJR3Vmtm/didUEbNjajskugZORg== - dependencies: - crc-32 "^1.2.0" - ethereumjs-util "^7.1.1" - "@ethereumjs/common@2.6.2": version "2.6.2" resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.2.tgz#eb006c9329c75c80f634f340dc1719a5258244df" @@ -1698,7 +1690,7 @@ crc-32 "^1.2.0" ethereumjs-util "^7.1.4" -"@ethereumjs/common@2.6.5", "@ethereumjs/common@^2.0.0", "@ethereumjs/common@^2.4.0", "@ethereumjs/common@^2.5.0", "@ethereumjs/common@^2.6.3", "@ethereumjs/common@^2.6.4": +"@ethereumjs/common@2.6.5", "@ethereumjs/common@^2.0.0", "@ethereumjs/common@^2.6.3", "@ethereumjs/common@^2.6.4": version "2.6.5" resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.6.5.tgz#0a75a22a046272579d91919cb12d84f2756e8d30" integrity sha512-lRyVQOeCDaIVtgfbowla32pzeDv2Obr8oR8Put5RdUBNRGr1VGPGQNGP6elWIpgK3YdpzqTOh4GyUGOureVeeA== @@ -1719,14 +1711,6 @@ "@ethereumjs/common" "^2.0.0" ethereumjs-util "^7.0.7" -"@ethereumjs/tx@3.3.2": - version "3.3.2" - resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.3.2.tgz#348d4624bf248aaab6c44fec2ae67265efe3db00" - integrity sha512-6AaJhwg4ucmwTvw/1qLaZUX5miWrwZ4nLOUsKyb/HtzS3BMw/CasKhdi1ims9mBKeK9sOJCH4qGKOBGyJCeeog== - dependencies: - "@ethereumjs/common" "^2.5.0" - ethereumjs-util "^7.1.2" - "@ethereumjs/tx@3.5.1": version "3.5.1" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.5.1.tgz#8d941b83a602b4a89949c879615f7ea9a90e6671" @@ -1735,7 +1719,7 @@ "@ethereumjs/common" "^2.6.3" ethereumjs-util "^7.1.4" -"@ethereumjs/tx@3.5.2", "@ethereumjs/tx@^3.3.0", "@ethereumjs/tx@^3.4.0": +"@ethereumjs/tx@3.5.2", "@ethereumjs/tx@^3.4.0": version "3.5.2" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.5.2.tgz#197b9b6299582ad84f9527ca961466fce2296c1c" integrity sha512-gQDNJWKrSDGu2w7w0PzVXVBNMzb7wwdDOmOqczmhNjqFxFuIbhVJDwiGEnxFNC2/b8ifcZzY7MLcluizohRzNw== @@ -3513,7 +3497,7 @@ dependencies: "@types/mdx" "^2.0.0" -"@metamask/eth-sig-util@4.0.1", "@metamask/eth-sig-util@^4.0.0": +"@metamask/eth-sig-util@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz#3ad61f6ea9ad73ba5b19db780d40d9aae5157088" integrity sha512-tghyZKLHZjcdlDqCA3gNZmLeR0XvOE9U1qoQO9ohyAZT6Pya+H9vkBPcsyXytmYLNgVoin7CKCmweo/R43V+tQ== @@ -3830,11 +3814,6 @@ dependencies: "@noble/hashes" "1.3.2" -"@noble/hashes@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" - integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA== - "@noble/hashes@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" @@ -3850,16 +3829,6 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== -"@noble/hashes@~1.1.1": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.5.tgz#1a0377f3b9020efe2fae03290bd2a12140c95c11" - integrity sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ== - -"@noble/secp256k1@1.6.3", "@noble/secp256k1@~1.6.0": - version "1.6.3" - resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.6.3.tgz#7eed12d9f4404b416999d0c87686836c4c5c9b94" - integrity sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ== - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -4212,15 +4181,6 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== -"@scure/bip32@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.0.tgz#dea45875e7fbc720c2b4560325f1cf5d2246d95b" - integrity sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q== - dependencies: - "@noble/hashes" "~1.1.1" - "@noble/secp256k1" "~1.6.0" - "@scure/base" "~1.1.0" - "@scure/bip32@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" @@ -4239,14 +4199,6 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.2" -"@scure/bip39@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.0.tgz#92f11d095bae025f166bef3defcc5bf4945d419a" - integrity sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w== - dependencies: - "@noble/hashes" "~1.1.1" - "@scure/base" "~1.1.0" - "@scure/bip39@1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" @@ -5650,33 +5602,6 @@ varuint-bitcoin "^1.1.2" wif "^2.0.6" -"@truffle/hdwallet-provider@^2.1.4": - version "2.1.15" - resolved "https://registry.yarnpkg.com/@truffle/hdwallet-provider/-/hdwallet-provider-2.1.15.tgz#fbf8e19d112db81b109ebc06ac6d9d42124b512c" - integrity sha512-I5cSS+5LygA3WFzru9aC5+yDXVowEEbLCx0ckl/RqJ2/SCiYXkzYlR5/DjjDJuCtYhivhrn2RP9AheeFlRF+qw== - dependencies: - "@ethereumjs/common" "^2.4.0" - "@ethereumjs/tx" "^3.3.0" - "@metamask/eth-sig-util" "4.0.1" - "@truffle/hdwallet" "^0.1.4" - "@types/ethereum-protocol" "^1.0.0" - "@types/web3" "1.0.20" - "@types/web3-provider-engine" "^14.0.0" - ethereum-cryptography "1.1.2" - ethereum-protocol "^1.0.1" - ethereumjs-util "^7.1.5" - web3 "1.10.0" - web3-provider-engine "16.0.3" - -"@truffle/hdwallet@^0.1.4": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@truffle/hdwallet/-/hdwallet-0.1.4.tgz#eeb21163d9e295692a0ba2fa848cc7b5a29b0ded" - integrity sha512-D3SN0iw3sMWUXjWAedP6RJtopo9qQXYi80inzbtcsoso4VhxFxCwFvCErCl4b27AEJ9pkAtgnxEFRaSKdMmi1Q== - dependencies: - ethereum-cryptography "1.1.2" - keccak "3.0.2" - secp256k1 "4.0.3" - "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" @@ -5738,13 +5663,6 @@ dependencies: "@babel/types" "^7.20.7" -"@types/bn.js@*", "@types/bn.js@^5.1.0", "@types/bn.js@^5.1.1": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.2.tgz#162f5238c46f4bcbac07a98561724eca1fcf0c5e" - integrity sha512-dkpZu0szUtn9UXTmw+e0AJFd4D2XAxDnsCLdc05SfqpqzPEBft8eQr8uaFitfo/dUUOZERaLec2hHMG87A4Dxg== - dependencies: - "@types/node" "*" - "@types/bn.js@5.1.1": version "5.1.1" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682" @@ -5759,6 +5677,13 @@ dependencies: "@types/node" "*" +"@types/bn.js@^5.1.0", "@types/bn.js@^5.1.1": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.2.tgz#162f5238c46f4bcbac07a98561724eca1fcf0c5e" + integrity sha512-dkpZu0szUtn9UXTmw+e0AJFd4D2XAxDnsCLdc05SfqpqzPEBft8eQr8uaFitfo/dUUOZERaLec2hHMG87A4Dxg== + dependencies: + "@types/node" "*" + "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" @@ -5859,13 +5784,6 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== -"@types/ethereum-protocol@*", "@types/ethereum-protocol@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/ethereum-protocol/-/ethereum-protocol-1.0.3.tgz#64a4001b8ef7d3f09e89123feb8c35d04efd00a7" - integrity sha512-peaCYb+wAT3Gnttt8Ep6+b3ciVK+mWX5wyVnJiDtmWXU1c9RXi5qDxEjGyZrjU/9EYdXPd3hMiXXBjDDPu96yQ== - dependencies: - bignumber.js "7.2.1" - "@types/express-serve-static-core@^4.17.33": version "4.19.0" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz#3ae8ab3767d98d0b682cda063c3339e1e86ccfaa" @@ -6235,11 +6153,6 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65" integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ== -"@types/underscore@*": - version "1.11.11" - resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.11.11.tgz#d687e649dd7f3c4045b71f756cd80892d55d3bb1" - integrity sha512-J/ZgSP9Yv0S+wfUfeRh9ynktcCvycfW4S9NbzkFdiHLBth+Ctdy5nYg3ZAqUKq7v3gcJce6rXo41zJV6IqsXsQ== - "@types/unist@*", "@types/unist@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" @@ -6260,21 +6173,6 @@ resolved "https://registry.yarnpkg.com/@types/w3c-web-usb/-/w3c-web-usb-1.0.8.tgz#c593fef468b6e6051209c8aa89d1ead08005e23d" integrity sha512-ouEoUTyB27wFXUUyl0uKIE6VkeCczDtazWTiZGD1M4onceJnp8KnHDf7CzLbpwzek2ZFWXTC5KrNDRc9q/Jf6Q== -"@types/web3-provider-engine@^14.0.0": - version "14.0.2" - resolved "https://registry.yarnpkg.com/@types/web3-provider-engine/-/web3-provider-engine-14.0.2.tgz#ff571f2077abab015616edec3b6437e8dc1f8e40" - integrity sha512-i+vgIh873kDu6MnYZkIqrho4JCan1c8TcPnYY6te2lq1ODD4SPA8JxFCyQjK+vwbLMr5F3N/I37AfK/wxiyuEA== - dependencies: - "@types/ethereum-protocol" "*" - -"@types/web3@1.0.20": - version "1.0.20" - resolved "https://registry.yarnpkg.com/@types/web3/-/web3-1.0.20.tgz#234dd1f976702c0daaff147c80f24a5582e09d0e" - integrity sha512-KTDlFuYjzCUlBDGt35Ir5QRtyV9klF84MMKUsEJK10sTWga/71V+8VYLT7yysjuBjaOx2uFYtIWNGoz3yrNDlg== - dependencies: - "@types/bn.js" "*" - "@types/underscore" "*" - "@types/web@^0.0.100": version "0.0.100" resolved "https://registry.yarnpkg.com/@types/web/-/web-0.0.100.tgz#174f5952c40ab0940b0aa04e76d2f2776005b8c6" @@ -7271,25 +7169,11 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -abortcontroller-polyfill@^1.7.3, abortcontroller-polyfill@^1.7.5: +abortcontroller-polyfill@^1.7.5: version "1.7.5" resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed" integrity sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ== -abstract-leveldown@~2.6.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz#1c5e8c6a5ef965ae8c35dfb3a8770c476b82c4b8" - integrity sha512-2++wDf/DYqkPR3o5tbfdhF96EfMApo1GpPfzOsR/ZYXdkSmELlvOOEAl9iKkRsktMPHdGjO4rtkBpf2I7TiTeA== - dependencies: - xtend "~4.0.0" - -abstract-leveldown@~2.7.1: - version "2.7.2" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.7.2.tgz#87a44d7ebebc341d59665204834c8b7e0932cc93" - integrity sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w== - dependencies: - xtend "~4.0.0" - accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -7697,13 +7581,6 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async-eventemitter@^0.2.2: - version "0.2.4" - resolved "https://registry.yarnpkg.com/async-eventemitter/-/async-eventemitter-0.2.4.tgz#f5e7c8ca7d3e46aab9ec40a292baf686a0bafaca" - integrity sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw== - dependencies: - async "^2.4.0" - async-limiter@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" @@ -7716,18 +7593,6 @@ async-mutex@^0.2.6: dependencies: tslib "^2.0.0" -async@^1.4.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - integrity sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w== - -async@^2.0.1, async@^2.1.2, async@^2.4.0, async@^2.5.0: - version "2.6.4" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" - integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== - dependencies: - lodash "^4.17.14" - async@^3.2.0, async@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" @@ -7916,13 +7781,6 @@ babel-preset-jest@^29.6.3: babel-plugin-jest-hoist "^29.6.3" babel-preset-current-node-syntax "^1.0.0" -backoff@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/backoff/-/backoff-2.5.0.tgz#f616eda9d3e4b66b8ca7fca79f695722c5f8e26f" - integrity sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA== - dependencies: - precond "0.2" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -8032,11 +7890,6 @@ bigint-buffer@^1.1.5: dependencies: bindings "^1.3.0" -bignumber.js@7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" - integrity sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ== - bignumber.js@^9.0.0, bignumber.js@^9.0.1, bignumber.js@^9.1.0, bignumber.js@^9.1.1, bignumber.js@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" @@ -8336,11 +8189,6 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -btoa@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" - integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== - buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -8576,13 +8424,6 @@ check-more-types@^2.24.0: resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== -checkpoint-store@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/checkpoint-store/-/checkpoint-store-1.1.0.tgz#04e4cb516b91433893581e6d4601a78e9552ea06" - integrity sha512-J/NdY2WvIx654cc6LWSq/IYFFCUf75fFTgwzFnmbqyORH4MwgiQCgswLLKBGzmsyTI5V7i5bp/So6sMbDWhedg== - dependencies: - functional-red-black-tree "^1.0.1" - "chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3, chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -8786,11 +8627,6 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -clone@^2.0.0, clone@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== - clsx@^1.1.0, clsx@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" @@ -9135,14 +8971,6 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" -cross-fetch@^2.1.0: - version "2.2.6" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.6.tgz#2ef0bb39a24ac034787965c457368a28730e220a" - integrity sha512-9JZz+vXCmfKUZ68zAptS7k4Nu8e2qcibe7WVZYps7sAgk5R8GYTc+T1WR0v1rlP9HxgARmOX1UTIJZFytajpNA== - dependencies: - node-fetch "^2.6.7" - whatwg-fetch "^2.0.4" - cross-fetch@^3.1.4, cross-fetch@^3.1.5, cross-fetch@^3.1.6: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" @@ -9504,13 +9332,6 @@ defer-to-connect@^2.0.0, defer-to-connect@^2.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== -deferred-leveldown@~1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-1.2.2.tgz#3acd2e0b75d1669924bc0a4b642851131173e1eb" - integrity sha512-uukrWD2bguRtXilKt6cAWKyoXrTSMo5m7crUdLfWQmu8kIm88w3QZoUL+6nhpfKVmhHANER6Re3sKoNoZ3IKMA== - dependencies: - abstract-leveldown "~2.6.0" - define-data-property@^1.0.1, define-data-property@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -9984,7 +9805,7 @@ err-code@^3.0.0, err-code@^3.0.1: resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA== -errno@^0.1.1, errno@~0.1.1: +errno@^0.1.1: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== @@ -10583,18 +10404,6 @@ eth-block-tracker@6.1.0: json-rpc-random-id "^1.0.1" pify "^3.0.0" -eth-block-tracker@^4.4.2: - version "4.4.3" - resolved "https://registry.yarnpkg.com/eth-block-tracker/-/eth-block-tracker-4.4.3.tgz#766a0a0eb4a52c867a28328e9ae21353812cf626" - integrity sha512-A8tG4Z4iNg4mw5tP1Vung9N9IjgMNqpiMoJ/FouSFwNCGHv2X0mmOYwtQOJzki6XN7r7Tyo01S29p7b224I4jw== - dependencies: - "@babel/plugin-transform-runtime" "^7.5.5" - "@babel/runtime" "^7.5.5" - eth-query "^2.1.0" - json-rpc-random-id "^1.0.1" - pify "^3.0.0" - safe-event-emitter "^1.0.1" - eth-crypto@^2.1.0: version "2.6.0" resolved "https://registry.yarnpkg.com/eth-crypto/-/eth-crypto-2.6.0.tgz#b777f367ae8c70e5917b3b7d52adab6b34841e29" @@ -10627,45 +10436,6 @@ eth-json-rpc-filters@5.1.0: json-rpc-engine "^6.1.0" pify "^5.0.0" -eth-json-rpc-filters@^4.2.1: - version "4.2.2" - resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-4.2.2.tgz#eb35e1dfe9357ace8a8908e7daee80b2cd60a10d" - integrity sha512-DGtqpLU7bBg63wPMWg1sCpkKCf57dJ+hj/k3zF26anXMzkmtSBDExL8IhUu7LUd34f0Zsce3PYNO2vV2GaTzaw== - dependencies: - "@metamask/safe-event-emitter" "^2.0.0" - async-mutex "^0.2.6" - eth-json-rpc-middleware "^6.0.0" - eth-query "^2.1.2" - json-rpc-engine "^6.1.0" - pify "^5.0.0" - -eth-json-rpc-infura@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eth-json-rpc-infura/-/eth-json-rpc-infura-5.1.0.tgz#e6da7dc47402ce64c54e7018170d89433c4e8fb6" - integrity sha512-THzLye3PHUSGn1EXMhg6WTLW9uim7LQZKeKaeYsS9+wOBcamRiCQVGHa6D2/4P0oS0vSaxsBnU/J6qvn0MPdow== - dependencies: - eth-json-rpc-middleware "^6.0.0" - eth-rpc-errors "^3.0.0" - json-rpc-engine "^5.3.0" - node-fetch "^2.6.0" - -eth-json-rpc-middleware@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/eth-json-rpc-middleware/-/eth-json-rpc-middleware-6.0.0.tgz#4fe16928b34231a2537856f08a5ebbc3d0c31175" - integrity sha512-qqBfLU2Uq1Ou15Wox1s+NX05S9OcAEL4JZ04VZox2NS0U+RtCMjSxzXhLFWekdShUPZ+P8ax3zCO2xcPrp6XJQ== - dependencies: - btoa "^1.2.1" - clone "^2.1.1" - eth-query "^2.1.2" - eth-rpc-errors "^3.0.0" - eth-sig-util "^1.4.2" - ethereumjs-util "^5.1.2" - json-rpc-engine "^5.3.0" - json-stable-stringify "^1.0.1" - node-fetch "^2.6.1" - pify "^3.0.0" - safe-event-emitter "^1.0.1" - eth-lib@0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/eth-lib/-/eth-lib-0.2.8.tgz#b194058bef4b220ad12ea497431d6cb6aa0623c8" @@ -10687,7 +10457,7 @@ eth-lib@^0.1.26: ws "^3.0.0" xhr-request-promise "^0.1.2" -eth-query@^2.1.0, eth-query@^2.1.2: +eth-query@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/eth-query/-/eth-query-2.1.2.tgz#d6741d9000106b51510c72db92d6365456a6da5e" integrity sha512-srES0ZcvwkR/wd5OQBRA1bIJMww1skfGS0s8wlwK3/oNP4+wnds60krvu5R1QbpRQjMmpG5OMIWro5s7gvDPsA== @@ -10702,13 +10472,6 @@ eth-rpc-errors@4.0.2: dependencies: fast-safe-stringify "^2.0.6" -eth-rpc-errors@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-3.0.0.tgz#d7b22653c70dbf9defd4ef490fd08fe70608ca10" - integrity sha512-iPPNHPrLwUlR9xCSYm7HHQjWBasor3+KZfRvwEWxMz3ca0yqnlBeJrnyphkGIXZ4J7AMAaOLmwy4AWhnxOiLxg== - dependencies: - fast-safe-stringify "^2.0.6" - eth-rpc-errors@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz#6ddb6190a4bf360afda82790bb7d9d5e724f423a" @@ -10716,14 +10479,6 @@ eth-rpc-errors@^4.0.2: dependencies: fast-safe-stringify "^2.0.6" -eth-sig-util@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-1.4.2.tgz#8d958202c7edbaae839707fba6f09ff327606210" - integrity sha512-iNZ576iTOGcfllftB73cPB5AN+XUQAT/T8xzsILsghXC1o8gJUqe3RHlcDqagu+biFpYQ61KQrZZJza8eRSYqw== - dependencies: - ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git" - ethereumjs-util "^5.1.1" - ethereum-bloom-filters@^1.0.6: version "1.0.10" resolved "https://registry.yarnpkg.com/ethereum-bloom-filters/-/ethereum-bloom-filters-1.0.10.tgz#3ca07f4aed698e75bd134584850260246a5fed8a" @@ -10731,26 +10486,6 @@ ethereum-bloom-filters@^1.0.6: dependencies: js-sha3 "^0.8.0" -ethereum-common@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.2.0.tgz#13bf966131cce1eeade62a1b434249bb4cb120ca" - integrity sha512-XOnAR/3rntJgbCdGhqdaLIxDLWKLmsZOGhHdBKadEr6gEnJLH52k93Ou+TUdFaPN3hJc3isBZBal3U/XZ15abA== - -ethereum-common@^0.0.18: - version "0.0.18" - resolved "https://registry.yarnpkg.com/ethereum-common/-/ethereum-common-0.0.18.tgz#2fdc3576f232903358976eb39da783213ff9523f" - integrity sha512-EoltVQTRNg2Uy4o84qpa2aXymXDJhxm7eos/ACOg0DG4baAbMjhbdAEsx9GeE8sC3XCxnYvrrzZDH8D8MtA2iQ== - -ethereum-cryptography@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz#74f2ac0f0f5fe79f012c889b3b8446a9a6264e6d" - integrity sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ== - dependencies: - "@noble/hashes" "1.1.2" - "@noble/secp256k1" "1.6.3" - "@scure/bip32" "1.1.0" - "@scure/bip39" "1.1.0" - ethereum-cryptography@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz#8d6143cfc3d74bf79bbd8edecdf29e4ae20dd191" @@ -10782,11 +10517,6 @@ ethereum-cryptography@^2.0.0, ethereum-cryptography@^2.1.2: "@scure/bip32" "1.3.1" "@scure/bip39" "1.2.1" -ethereum-protocol@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ethereum-protocol/-/ethereum-protocol-1.0.1.tgz#b7d68142f4105e0ae7b5e178cf42f8d4dc4b93cf" - integrity sha512-3KLX1mHuEsBW0dKG+c6EOJS1NBNqdCICvZW9sInmZTt5aY0oxmHVggYRE0lJu1tcnMD1K+AKHdLi6U43Awm1Vg== - ethereumjs-abi@^0.6.8: version "0.6.8" resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz#71bc152db099f70e62f108b7cdfca1b362c6fcae" @@ -10795,66 +10525,7 @@ ethereumjs-abi@^0.6.8: bn.js "^4.11.8" ethereumjs-util "^6.0.0" -"ethereumjs-abi@git+https://github.com/ethereumjs/ethereumjs-abi.git": - version "0.6.8" - resolved "git+https://github.com/ethereumjs/ethereumjs-abi.git#ee3994657fa7a427238e6ba92a84d0b529bbcde0" - dependencies: - bn.js "^4.11.8" - ethereumjs-util "^6.0.0" - -ethereumjs-account@^2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/ethereumjs-account/-/ethereumjs-account-2.0.5.tgz#eeafc62de544cb07b0ee44b10f572c9c49e00a84" - integrity sha512-bgDojnXGjhMwo6eXQC0bY6UK2liSFUSMwwylOmQvZbSl/D7NXQ3+vrGO46ZeOgjGfxXmgIeVNDIiHw7fNZM4VA== - dependencies: - ethereumjs-util "^5.0.0" - rlp "^2.0.0" - safe-buffer "^5.1.1" - -ethereumjs-block@^1.2.2: - version "1.7.1" - resolved "https://registry.yarnpkg.com/ethereumjs-block/-/ethereumjs-block-1.7.1.tgz#78b88e6cc56de29a6b4884ee75379b6860333c3f" - integrity sha512-B+sSdtqm78fmKkBq78/QLKJbu/4Ts4P2KFISdgcuZUPDm9x+N7qgBPIIFUGbaakQh8bzuquiRVbdmvPKqbILRg== - dependencies: - async "^2.0.1" - ethereum-common "0.2.0" - ethereumjs-tx "^1.2.2" - ethereumjs-util "^5.0.0" - merkle-patricia-tree "^2.1.2" - -ethereumjs-block@~2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/ethereumjs-block/-/ethereumjs-block-2.2.2.tgz#c7654be7e22df489fda206139ecd63e2e9c04965" - integrity sha512-2p49ifhek3h2zeg/+da6XpdFR3GlqY3BIEiqxGF8j9aSRIgkb7M1Ky+yULBKJOu8PAZxfhsYA+HxUk2aCQp3vg== - dependencies: - async "^2.0.1" - ethereumjs-common "^1.5.0" - ethereumjs-tx "^2.1.1" - ethereumjs-util "^5.0.0" - merkle-patricia-tree "^2.1.2" - -ethereumjs-common@^1.1.0, ethereumjs-common@^1.5.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/ethereumjs-common/-/ethereumjs-common-1.5.2.tgz#2065dbe9214e850f2e955a80e650cb6999066979" - integrity sha512-hTfZjwGX52GS2jcVO6E2sx4YuFnf0Fhp5ylo4pEPhEffNln7vS59Hr5sLnp3/QCazFLluuBZ+FZ6J5HTp0EqCA== - -ethereumjs-tx@^1.2.2: - version "1.3.7" - resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz#88323a2d875b10549b8347e09f4862b546f3d89a" - integrity sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA== - dependencies: - ethereum-common "^0.0.18" - ethereumjs-util "^5.0.0" - -ethereumjs-tx@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ethereumjs-tx/-/ethereumjs-tx-2.1.2.tgz#5dfe7688bf177b45c9a23f86cf9104d47ea35fed" - integrity sha512-zZEK1onCeiORb0wyCXUvg94Ve5It/K6GD1K+26KfFKodiBiS6d9lfCXlUKGBBdQ+bv7Day+JK0tj1K+BeNFRAw== - dependencies: - ethereumjs-common "^1.5.0" - ethereumjs-util "^6.0.0" - -ethereumjs-util@7.1.5, ethereumjs-util@^7.0.7, ethereumjs-util@^7.0.8, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.1, ethereumjs-util@^7.1.2, ethereumjs-util@^7.1.3, ethereumjs-util@^7.1.4, ethereumjs-util@^7.1.5: +ethereumjs-util@7.1.5, ethereumjs-util@^7.0.7, ethereumjs-util@^7.0.8, ethereumjs-util@^7.1.3, ethereumjs-util@^7.1.4, ethereumjs-util@^7.1.5: version "7.1.5" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181" integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg== @@ -10865,19 +10536,6 @@ ethereumjs-util@7.1.5, ethereumjs-util@^7.0.7, ethereumjs-util@^7.0.8, ethereumj ethereum-cryptography "^0.1.3" rlp "^2.2.4" -ethereumjs-util@^5.0.0, ethereumjs-util@^5.1.1, ethereumjs-util@^5.1.2, ethereumjs-util@^5.1.5: - version "5.2.1" - resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz#a833f0e5fca7e5b361384dc76301a721f537bf65" - integrity sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ== - dependencies: - bn.js "^4.11.0" - create-hash "^1.1.2" - elliptic "^6.5.2" - ethereum-cryptography "^0.1.3" - ethjs-util "^0.1.3" - rlp "^2.0.0" - safe-buffer "^5.1.1" - ethereumjs-util@^6.0.0, ethereumjs-util@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz#fcb4e4dd5ceacb9d2305426ab1a5cd93e3163b69" @@ -10891,23 +10549,6 @@ ethereumjs-util@^6.0.0, ethereumjs-util@^6.2.1: ethjs-util "0.1.6" rlp "^2.2.3" -ethereumjs-vm@^2.3.4: - version "2.6.0" - resolved "https://registry.yarnpkg.com/ethereumjs-vm/-/ethereumjs-vm-2.6.0.tgz#76243ed8de031b408793ac33907fb3407fe400c6" - integrity sha512-r/XIUik/ynGbxS3y+mvGnbOKnuLo40V5Mj1J25+HEO63aWYREIqvWeRO/hnROlMBE5WoniQmPmhiaN0ctiHaXw== - dependencies: - async "^2.1.2" - async-eventemitter "^0.2.2" - ethereumjs-account "^2.0.3" - ethereumjs-block "~2.2.0" - ethereumjs-common "^1.1.0" - ethereumjs-util "^6.0.0" - fake-merkle-patricia-tree "^1.0.1" - functional-red-black-tree "^1.0.1" - merkle-patricia-tree "^2.3.2" - rustbn.js "~0.2.0" - safe-buffer "^5.1.1" - ethers@5.5.3: version "5.5.3" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.5.3.tgz#1e361516711c0c3244b6210e7e3ecabf0c75fca0" @@ -11037,7 +10678,7 @@ ethjs-unit@0.1.6: bn.js "4.11.6" number-to-bn "1.7.0" -ethjs-util@0.1.6, ethjs-util@^0.1.3, ethjs-util@^0.1.6: +ethjs-util@0.1.6, ethjs-util@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w== @@ -11073,7 +10714,7 @@ eventemitter3@^4.0.7: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@3.3.0, events@^3.0.0, events@^3.2.0, events@^3.3.0: +events@3.3.0, events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -11251,13 +10892,6 @@ fake-indexeddb@^4.0.2: dependencies: realistic-structured-clone "^3.0.0" -fake-merkle-patricia-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz#4b8c3acfb520afadf9860b1f14cd8ce3402cddd3" - integrity sha512-Tgq37lkc9pUIgIKw5uitNUKcgcYL3R6JvXtKQbOf/ZSavXbidsksgp/pAY6p//uhw0I4yoMsvTSovvVIsk/qxA== - dependencies: - checkpoint-store "^1.1.0" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -11723,11 +11357,6 @@ function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: es-abstract "^1.22.1" functions-have-names "^1.2.3" -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" @@ -12495,11 +12124,6 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== -immediate@^3.2.3: - version "3.3.0" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" - integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== - immer@^9.0.21: version "9.0.21" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" @@ -12786,11 +12410,6 @@ is-finalizationregistry@^1.0.2: dependencies: call-bind "^1.0.2" -is-fn@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fn/-/is-fn-1.0.0.tgz#9543d5de7bcf5b08a22ec8a20bae6e286d510d8c" - integrity sha512-XoFPJQmsAShb3jEQRfzf2rqXavq7fIqF/jOekp308JlThqrODnMpweVSGilKTCXELfLhltGP2AGgbQGVP8F1dg== - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -13745,14 +13364,6 @@ json-rpc-engine@6.1.0, json-rpc-engine@^6.1.0: "@metamask/safe-event-emitter" "^2.0.0" eth-rpc-errors "^4.0.2" -json-rpc-engine@^5.3.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.4.0.tgz#75758609d849e1dba1e09021ae473f3ab63161e5" - integrity sha512-rAffKbPoNDjuRnXkecTjnsE3xLLrb00rEkdgalINhaYVYIxDwWtvYBr9UFbhTvPB1B2qUOLoFd/cV6f4Q7mh7g== - dependencies: - eth-rpc-errors "^3.0.0" - safe-event-emitter "^1.0.1" - json-rpc-random-id@^1.0.0, json-rpc-random-id@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz#ba49d96aded1444dbb8da3d203748acbbcdec8c8" @@ -13778,7 +13389,7 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json-stable-stringify@^1.0.1, json-stable-stringify@^1.0.2: +json-stable-stringify@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz#e06f23128e0bbe342dc996ed5a19e28b57b580e0" integrity sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g== @@ -13883,15 +13494,6 @@ jsqr@^1.2.0: object.assign "^4.1.4" object.values "^1.1.6" -keccak@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0" - integrity sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ== - dependencies: - node-addon-api "^2.0.0" - node-gyp-build "^4.2.0" - readable-stream "^3.6.0" - keccak@^3.0.0, keccak@^3.0.1, keccak@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" @@ -13971,56 +13573,6 @@ less@^4.1.3: needle "^3.1.0" source-map "~0.6.0" -level-codec@~7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-7.0.1.tgz#341f22f907ce0f16763f24bddd681e395a0fb8a7" - integrity sha512-Ua/R9B9r3RasXdRmOtd+t9TCOEIIlts+TN/7XTT2unhDaL6sJn83S3rUyljbr6lVtw49N3/yA0HHjpV6Kzb2aQ== - -level-errors@^1.0.3: - version "1.1.2" - resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-1.1.2.tgz#4399c2f3d3ab87d0625f7e3676e2d807deff404d" - integrity sha512-Sw/IJwWbPKF5Ai4Wz60B52yj0zYeqzObLh8k1Tk88jVmD51cJSKWSYpRyhVIvFzZdvsPqlH5wfhp/yxdsaQH4w== - dependencies: - errno "~0.1.1" - -level-errors@~1.0.3: - version "1.0.5" - resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-1.0.5.tgz#83dbfb12f0b8a2516bdc9a31c4876038e227b859" - integrity sha512-/cLUpQduF6bNrWuAC4pwtUKA5t669pCsCi2XbmojG2tFeOr9j6ShtdDCtFFQO1DRt+EVZhx9gPzP9G2bUaG4ig== - dependencies: - errno "~0.1.1" - -level-iterator-stream@~1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/level-iterator-stream/-/level-iterator-stream-1.3.1.tgz#e43b78b1a8143e6fa97a4f485eb8ea530352f2ed" - integrity sha512-1qua0RHNtr4nrZBgYlpV0qHHeHpcRRWTxEZJ8xsemoHAXNL5tbooh4tPEEqIqsbWCAJBmUmkwYK/sW5OrFjWWw== - dependencies: - inherits "^2.0.1" - level-errors "^1.0.3" - readable-stream "^1.0.33" - xtend "^4.0.0" - -level-ws@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/level-ws/-/level-ws-0.0.0.tgz#372e512177924a00424b0b43aef2bb42496d228b" - integrity sha512-XUTaO/+Db51Uiyp/t7fCMGVFOTdtLS/NIACxE/GHsij15mKzxksZifKVjlXDF41JMUP/oM1Oc4YNGdKnc3dVLw== - dependencies: - readable-stream "~1.0.15" - xtend "~2.1.1" - -levelup@^1.2.1: - version "1.3.9" - resolved "https://registry.yarnpkg.com/levelup/-/levelup-1.3.9.tgz#2dbcae845b2bb2b6bea84df334c475533bbd82ab" - integrity sha512-VVGHfKIlmw8w1XqpGOAGwq6sZm2WwWLmlDcULkKWQXEA5EopA8OBNJ2Ck2v6bdk8HeEZSbCSEgzXadyQFm76sQ== - dependencies: - deferred-leveldown "~1.2.1" - level-codec "~7.0.0" - level-errors "~1.0.3" - level-iterator-stream "~1.3.0" - prr "~1.0.1" - semver "~5.4.1" - xtend "~4.0.0" - leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -14204,7 +13756,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww== -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: +lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -14304,11 +13856,6 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" -ltgt@~2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" - integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA== - lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" @@ -14396,18 +13943,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -memdown@^1.0.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/memdown/-/memdown-1.4.1.tgz#b4e4e192174664ffbae41361aa500f3119efe215" - integrity sha512-iVrGHZB8i4OQfM155xx8akvG9FIj+ht14DX5CQkCTG4EHzZ3d3sgckIf/Lm9ivZalEsFuEVnWv2B2WZvbrro2w== - dependencies: - abstract-leveldown "~2.7.1" - functional-red-black-tree "^1.0.1" - immediate "^3.2.3" - inherits "~2.0.1" - ltgt "~2.2.0" - safe-buffer "~5.1.1" - memfs@^3.4.1, memfs@^3.4.12: version "3.6.0" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" @@ -14476,20 +14011,6 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -merkle-patricia-tree@^2.1.2, merkle-patricia-tree@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/merkle-patricia-tree/-/merkle-patricia-tree-2.3.2.tgz#982ca1b5a0fde00eed2f6aeed1f9152860b8208a" - integrity sha512-81PW5m8oz/pz3GvsAwbauj7Y00rqm81Tzad77tHBwU7pIAtN+TJnMSOJhxBKflSVYhptMMb9RskhqHqrSm1V+g== - dependencies: - async "^1.4.2" - ethereumjs-util "^5.0.0" - level-ws "0.0.0" - levelup "^1.2.1" - memdown "^1.0.0" - readable-stream "^2.0.0" - rlp "^2.0.0" - semaphore ">=1.0.1" - methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -14992,7 +14513,7 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -node-fetch@^2.0.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.7.0: +node-fetch@^2.0.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -15827,11 +15348,6 @@ prebuild-install@^7.1.1: tar-fs "^2.0.0" tunnel-agent "^0.6.0" -precond@0.2: - version "0.2.3" - resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" - integrity sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ== - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -15905,14 +15421,6 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -promise-to-callback@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/promise-to-callback/-/promise-to-callback-1.0.0.tgz#5d2a749010bfb67d963598fcd3960746a68feef7" - integrity sha512-uhMIZmKM5ZteDMfLgJnoSq9GCwsNKrYau73Awf1jIy6/eUcuuZ3P+CD9zUv0kJsIUbU+x6uLNIhXhLHDs1pNPA== - dependencies: - is-fn "^1.0.0" - set-immediate-shim "^1.0.1" - prompts@^2.0.1, prompts@^2.4.0: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -16470,17 +15978,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^1.0.33: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.2.9, readable-stream@^2.3.8, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.3.8, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -16513,7 +16011,7 @@ readable-stream@^4.0.0: process "^0.11.10" string_decoder "^1.3.0" -readable-stream@~1.0.15, readable-stream@~1.0.17, readable-stream@~1.0.27-1: +readable-stream@~1.0.17, readable-stream@~1.0.27-1: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" integrity sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg== @@ -16713,7 +16211,7 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" -request@^2.79.0, request@^2.85.0: +request@^2.79.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -16950,7 +16448,7 @@ ripple-lib@^1.10.1: ripple-lib-transactionparser "0.8.2" ws "^7.2.0" -rlp@^2.0.0, rlp@^2.2.3, rlp@^2.2.4: +rlp@^2.2.3, rlp@^2.2.4: version "2.2.7" resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf" integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ== @@ -17001,11 +16499,6 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rustbn.js@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/rustbn.js/-/rustbn.js-0.2.0.tgz#8082cb886e707155fd1cb6f23bd591ab8d55d0ca" - integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA== - rxjs@6, rxjs@^6.6.3, rxjs@^6.6.7: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" @@ -17047,13 +16540,6 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-event-emitter@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/safe-event-emitter/-/safe-event-emitter-1.0.1.tgz#5b692ef22329ed8f69fdce607e50ca734f6f20af" - integrity sha512-e1wFe99A91XYYxoQbcq2ZJUWurxEyP8vfz7A7vuUe1s95q8r5ebraVaA1BukYJcpM6V16ugWoD9vngi8Ccu5fg== - dependencies: - events "^3.0.0" - safe-regex-test@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" @@ -17173,15 +16659,6 @@ secp256k1@3.7.1: nan "^2.14.0" safe-buffer "^5.1.2" -secp256k1@4.0.3, secp256k1@^4.0.0, secp256k1@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" - integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== - dependencies: - elliptic "^6.5.4" - node-addon-api "^2.0.0" - node-gyp-build "^4.2.0" - secp256k1@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-5.0.0.tgz#be6f0c8c7722e2481e9773336d351de8cddd12f7" @@ -17191,10 +16668,14 @@ secp256k1@5.0.0: node-addon-api "^5.0.0" node-gyp-build "^4.2.0" -semaphore@>=1.0.1, semaphore@^1.0.3: - version "1.1.0" - resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa" - integrity sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA== +secp256k1@^4.0.0, secp256k1@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" + integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== + dependencies: + elliptic "^6.5.4" + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" @@ -17220,11 +16701,6 @@ semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semve dependencies: lru-cache "^6.0.0" -semver@~5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" - integrity sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg== - send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -17305,11 +16781,6 @@ set-function-name@^2.0.0, set-function-name@^2.0.1: functions-have-names "^1.2.3" has-property-descriptors "^1.0.0" -set-immediate-shim@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - integrity sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ== - setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -19093,15 +18564,6 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -web3-bzz@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.10.0.tgz#ac74bc71cdf294c7080a79091079192f05c5baed" - integrity sha512-o9IR59io3pDUsXTsps5pO5hW1D5zBmg46iNc2t4j2DkaYHNdDLwk2IP9ukoM2wg47QILfPEJYzhTfkS/CcX0KA== - dependencies: - "@types/node" "^12.12.6" - got "12.1.0" - swarm-js "^0.1.40" - web3-bzz@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.10.3.tgz#13942b37757eb850f3500a8e08bf605448b67566" @@ -19111,14 +18573,6 @@ web3-bzz@1.10.3: got "12.1.0" swarm-js "^0.1.40" -web3-core-helpers@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.10.0.tgz#1016534c51a5df77ed4f94d1fcce31de4af37fad" - integrity sha512-pIxAzFDS5vnbXvfvLSpaA1tfRykAe9adw43YCKsEYQwH0gCLL0kMLkaCX3q+Q8EVmAh+e1jWL/nl9U0de1+++g== - dependencies: - web3-eth-iban "1.10.0" - web3-utils "1.10.0" - web3-core-helpers@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.10.3.tgz#f2db40ea57e888795e46f229b06113b60bcd671c" @@ -19127,17 +18581,6 @@ web3-core-helpers@1.10.3: web3-eth-iban "1.10.3" web3-utils "1.10.3" -web3-core-method@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.10.0.tgz#82668197fa086e8cc8066742e35a9d72535e3412" - integrity sha512-4R700jTLAMKDMhQ+nsVfIXvH6IGJlJzGisIfMKWAIswH31h5AZz7uDUW2YctI+HrYd+5uOAlS4OJeeT9bIpvkA== - dependencies: - "@ethersproject/transactions" "^5.6.2" - web3-core-helpers "1.10.0" - web3-core-promievent "1.10.0" - web3-core-subscriptions "1.10.0" - web3-utils "1.10.0" - web3-core-method@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.10.3.tgz#63f16310ccab4eec8eca0a337d534565c2ba8d33" @@ -19149,13 +18592,6 @@ web3-core-method@1.10.3: web3-core-subscriptions "1.10.3" web3-utils "1.10.3" -web3-core-promievent@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.10.0.tgz#cbb5b3a76b888df45ed3a8d4d8d4f54ccb66a37b" - integrity sha512-68N7k5LWL5R38xRaKFrTFT2pm2jBNFaM4GioS00YjAKXRQ3KjmhijOMG3TICz6Aa5+6GDWYelDNx21YAeZ4YTg== - dependencies: - eventemitter3 "4.0.4" - web3-core-promievent@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.10.3.tgz#9765dd42ce6cf2dc0a08eaffee607b855644f290" @@ -19163,17 +18599,6 @@ web3-core-promievent@1.10.3: dependencies: eventemitter3 "4.0.4" -web3-core-requestmanager@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.10.0.tgz#4b34f6e05837e67c70ff6f6993652afc0d54c340" - integrity sha512-3z/JKE++Os62APml4dvBM+GAuId4h3L9ckUrj7ebEtS2AR0ixyQPbrBodgL91Sv7j7cQ3Y+hllaluqjguxvSaQ== - dependencies: - util "^0.12.5" - web3-core-helpers "1.10.0" - web3-providers-http "1.10.0" - web3-providers-ipc "1.10.0" - web3-providers-ws "1.10.0" - web3-core-requestmanager@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.10.3.tgz#c34ca8e998a18d6ca3fa7f7a11d4391da401c987" @@ -19185,14 +18610,6 @@ web3-core-requestmanager@1.10.3: web3-providers-ipc "1.10.3" web3-providers-ws "1.10.3" -web3-core-subscriptions@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.10.0.tgz#b534592ee1611788fc0cb0b95963b9b9b6eacb7c" - integrity sha512-HGm1PbDqsxejI075gxBc5OSkwymilRWZufIy9zEpnWKNmfbuv5FfHgW1/chtJP6aP3Uq2vHkvTDl3smQBb8l+g== - dependencies: - eventemitter3 "4.0.4" - web3-core-helpers "1.10.0" - web3-core-subscriptions@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.10.3.tgz#58768cd72a9313252ef05dc52c09536f009a9479" @@ -19201,19 +18618,6 @@ web3-core-subscriptions@1.10.3: eventemitter3 "4.0.4" web3-core-helpers "1.10.3" -web3-core@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.10.0.tgz#9aa07c5deb478cf356c5d3b5b35afafa5fa8e633" - integrity sha512-fWySwqy2hn3TL89w5TM8wXF1Z2Q6frQTKHWmP0ppRQorEK8NcHJRfeMiv/mQlSKoTS1F6n/nv2uyZsixFycjYQ== - dependencies: - "@types/bn.js" "^5.1.1" - "@types/node" "^12.12.6" - bignumber.js "^9.0.0" - web3-core-helpers "1.10.0" - web3-core-method "1.10.0" - web3-core-requestmanager "1.10.0" - web3-utils "1.10.0" - web3-core@1.10.3, web3-core@^1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.10.3.tgz#4aeb8f4b0cb5775d9fa4edf1127864743f1c3ae3" @@ -19227,14 +18631,6 @@ web3-core@1.10.3, web3-core@^1.10.3: web3-core-requestmanager "1.10.3" web3-utils "1.10.3" -web3-eth-abi@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.10.0.tgz#53a7a2c95a571e205e27fd9e664df4919483cce1" - integrity sha512-cwS+qRBWpJ43aI9L3JS88QYPfFcSJJ3XapxOQ4j40v6mk7ATpA8CVK1vGTzpihNlOfMVRBkR95oAj7oL6aiDOg== - dependencies: - "@ethersproject/abi" "^5.6.3" - web3-utils "1.10.0" - web3-eth-abi@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.10.3.tgz#7decfffa8fed26410f32cfefdc32d3e76f717ca2" @@ -19243,22 +18639,6 @@ web3-eth-abi@1.10.3: "@ethersproject/abi" "^5.6.3" web3-utils "1.10.3" -web3-eth-accounts@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.10.0.tgz#2942beca0a4291455f32cf09de10457a19a48117" - integrity sha512-wiq39Uc3mOI8rw24wE2n15hboLE0E9BsQLdlmsL4Zua9diDS6B5abXG0XhFcoNsXIGMWXVZz4TOq3u4EdpXF/Q== - dependencies: - "@ethereumjs/common" "2.5.0" - "@ethereumjs/tx" "3.3.2" - eth-lib "0.2.8" - ethereumjs-util "^7.1.5" - scrypt-js "^3.0.1" - uuid "^9.0.0" - web3-core "1.10.0" - web3-core-helpers "1.10.0" - web3-core-method "1.10.0" - web3-utils "1.10.0" - web3-eth-accounts@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.10.3.tgz#9ecb816b81cd97333988bfcd0afaee5d13bbb198" @@ -19275,20 +18655,6 @@ web3-eth-accounts@1.10.3: web3-core-method "1.10.3" web3-utils "1.10.3" -web3-eth-contract@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.10.0.tgz#8e68c7654576773ec3c91903f08e49d0242c503a" - integrity sha512-MIC5FOzP/+2evDksQQ/dpcXhSqa/2hFNytdl/x61IeWxhh6vlFeSjq0YVTAyIzdjwnL7nEmZpjfI6y6/Ufhy7w== - dependencies: - "@types/bn.js" "^5.1.1" - web3-core "1.10.0" - web3-core-helpers "1.10.0" - web3-core-method "1.10.0" - web3-core-promievent "1.10.0" - web3-core-subscriptions "1.10.0" - web3-eth-abi "1.10.0" - web3-utils "1.10.0" - web3-eth-contract@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.10.3.tgz#8880468e2ba7d8a4791cf714f67d5e1ec1591275" @@ -19303,20 +18669,6 @@ web3-eth-contract@1.10.3: web3-eth-abi "1.10.3" web3-utils "1.10.3" -web3-eth-ens@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.10.0.tgz#96a676524e0b580c87913f557a13ed810cf91cd9" - integrity sha512-3hpGgzX3qjgxNAmqdrC2YUQMTfnZbs4GeLEmy8aCWziVwogbuqQZ+Gzdfrym45eOZodk+lmXyLuAdqkNlvkc1g== - dependencies: - content-hash "^2.5.2" - eth-ens-namehash "2.0.8" - web3-core "1.10.0" - web3-core-helpers "1.10.0" - web3-core-promievent "1.10.0" - web3-eth-abi "1.10.0" - web3-eth-contract "1.10.0" - web3-utils "1.10.0" - web3-eth-ens@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-1.10.3.tgz#ae5b49bcb9823027e0b28aa6b1de58d726cbaafa" @@ -19331,14 +18683,6 @@ web3-eth-ens@1.10.3: web3-eth-contract "1.10.3" web3-utils "1.10.3" -web3-eth-iban@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.10.0.tgz#5a46646401965b0f09a4f58e7248c8a8cd22538a" - integrity sha512-0l+SP3IGhInw7Q20LY3IVafYEuufo4Dn75jAHT7c2aDJsIolvf2Lc6ugHkBajlwUneGfbRQs/ccYPQ9JeMUbrg== - dependencies: - bn.js "^5.2.1" - web3-utils "1.10.0" - web3-eth-iban@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.10.3.tgz#91d458e5400195edc883a0d4383bf1cecd17240d" @@ -19347,18 +18691,6 @@ web3-eth-iban@1.10.3: bn.js "^5.2.1" web3-utils "1.10.3" -web3-eth-personal@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.10.0.tgz#94d525f7a29050a0c2a12032df150ac5ea633071" - integrity sha512-anseKn98w/d703eWq52uNuZi7GhQeVjTC5/svrBWEKob0WZ5kPdo+EZoFN0sp5a5ubbrk/E0xSl1/M5yORMtpg== - dependencies: - "@types/node" "^12.12.6" - web3-core "1.10.0" - web3-core-helpers "1.10.0" - web3-core-method "1.10.0" - web3-net "1.10.0" - web3-utils "1.10.0" - web3-eth-personal@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-1.10.3.tgz#4e72008aa211327ccc3bfa7671c510e623368457" @@ -19371,24 +18703,6 @@ web3-eth-personal@1.10.3: web3-net "1.10.3" web3-utils "1.10.3" -web3-eth@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.10.0.tgz#38b905e2759697c9624ab080cfcf4e6c60b3a6cf" - integrity sha512-Z5vT6slNMLPKuwRyKGbqeGYC87OAy8bOblaqRTgg94CXcn/mmqU7iPIlG4506YdcdK3x6cfEDG7B6w+jRxypKA== - dependencies: - web3-core "1.10.0" - web3-core-helpers "1.10.0" - web3-core-method "1.10.0" - web3-core-subscriptions "1.10.0" - web3-eth-abi "1.10.0" - web3-eth-accounts "1.10.0" - web3-eth-contract "1.10.0" - web3-eth-ens "1.10.0" - web3-eth-iban "1.10.0" - web3-eth-personal "1.10.0" - web3-net "1.10.0" - web3-utils "1.10.0" - web3-eth@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-1.10.3.tgz#b8c6f37f1aac52422583a5a9c29130983a3fb3b1" @@ -19407,15 +18721,6 @@ web3-eth@1.10.3: web3-net "1.10.3" web3-utils "1.10.3" -web3-net@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.10.0.tgz#be53e7f5dafd55e7c9013d49c505448b92c9c97b" - integrity sha512-NLH/N3IshYWASpxk4/18Ge6n60GEvWBVeM8inx2dmZJVmRI6SJIlUxbL8jySgiTn3MMZlhbdvrGo8fpUW7a1GA== - dependencies: - web3-core "1.10.0" - web3-core-method "1.10.0" - web3-utils "1.10.0" - web3-net@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-1.10.3.tgz#9486c2fe51452cb958e11915db6f90bd6caa5482" @@ -19425,44 +18730,6 @@ web3-net@1.10.3: web3-core-method "1.10.3" web3-utils "1.10.3" -web3-provider-engine@16.0.3: - version "16.0.3" - resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-16.0.3.tgz#8ff93edf3a8da2f70d7f85c5116028c06a0d9f07" - integrity sha512-Q3bKhGqLfMTdLvkd4TtkGYJHcoVQ82D1l8jTIwwuJp/sAp7VHnRYb9YJ14SW/69VMWoOhSpPLZV2tWb9V0WJoA== - dependencies: - "@ethereumjs/tx" "^3.3.0" - async "^2.5.0" - backoff "^2.5.0" - clone "^2.0.0" - cross-fetch "^2.1.0" - eth-block-tracker "^4.4.2" - eth-json-rpc-filters "^4.2.1" - eth-json-rpc-infura "^5.1.0" - eth-json-rpc-middleware "^6.0.0" - eth-rpc-errors "^3.0.0" - eth-sig-util "^1.4.2" - ethereumjs-block "^1.2.2" - ethereumjs-util "^5.1.5" - ethereumjs-vm "^2.3.4" - json-stable-stringify "^1.0.1" - promise-to-callback "^1.0.0" - readable-stream "^2.2.9" - request "^2.85.0" - semaphore "^1.0.3" - ws "^5.1.1" - xhr "^2.2.0" - xtend "^4.0.1" - -web3-providers-http@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.10.0.tgz#864fa48675e7918c9a4374e5f664b32c09d0151b" - integrity sha512-eNr965YB8a9mLiNrkjAWNAPXgmQWfpBfkkn7tpEFlghfww0u3I0tktMZiaToJVcL2+Xq+81cxbkpeWJ5XQDwOA== - dependencies: - abortcontroller-polyfill "^1.7.3" - cross-fetch "^3.1.4" - es6-promise "^4.2.8" - web3-core-helpers "1.10.0" - web3-providers-http@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.10.3.tgz#d8166ee89db82d37281ea9e15c5882a2d7928755" @@ -19473,14 +18740,6 @@ web3-providers-http@1.10.3: es6-promise "^4.2.8" web3-core-helpers "1.10.3" -web3-providers-ipc@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.10.0.tgz#9747c7a6aee96a51488e32fa7c636c3460b39889" - integrity sha512-OfXG1aWN8L1OUqppshzq8YISkWrYHaATW9H8eh0p89TlWMc1KZOL9vttBuaBEi96D/n0eYDn2trzt22bqHWfXA== - dependencies: - oboe "2.1.5" - web3-core-helpers "1.10.0" - web3-providers-ipc@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.10.3.tgz#a7e015957fc037d8a87bd4b6ae3561c1b1ad1f46" @@ -19489,15 +18748,6 @@ web3-providers-ipc@1.10.3: oboe "2.1.5" web3-core-helpers "1.10.3" -web3-providers-ws@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.10.0.tgz#cb0b87b94c4df965cdf486af3a8cd26daf3975e5" - integrity sha512-sK0fNcglW36yD5xjnjtSGBnEtf59cbw4vZzJ+CmOWIKGIR96mP5l684g0WD0Eo+f4NQc2anWWXG74lRc9OVMCQ== - dependencies: - eventemitter3 "4.0.4" - web3-core-helpers "1.10.0" - websocket "^1.0.32" - web3-providers-ws@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.10.3.tgz#03c84958f9da251349cd26fd7a4ae567e3af6caa" @@ -19507,16 +18757,6 @@ web3-providers-ws@1.10.3: web3-core-helpers "1.10.3" websocket "^1.0.32" -web3-shh@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-shh/-/web3-shh-1.10.0.tgz#c2979b87e0f67a7fef2ce9ee853bd7bfbe9b79a8" - integrity sha512-uNUUuNsO2AjX41GJARV9zJibs11eq6HtOe6Wr0FtRUcj8SN6nHeYIzwstAvJ4fXA53gRqFMTxdntHEt9aXVjpg== - dependencies: - web3-core "1.10.0" - web3-core-method "1.10.0" - web3-core-subscriptions "1.10.0" - web3-net "1.10.0" - web3-shh@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-shh/-/web3-shh-1.10.3.tgz#ee44f760598a65a290d611c443838aac854ee858" @@ -19527,19 +18767,6 @@ web3-shh@1.10.3: web3-core-subscriptions "1.10.3" web3-net "1.10.3" -web3-utils@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.0.tgz#ca4c1b431a765c14ac7f773e92e0fd9377ccf578" - integrity sha512-kSaCM0uMcZTNUSmn5vMEhlo02RObGNRRCkdX0V9UTAU0+lrvn0HSaudyCo6CQzuXUsnuY2ERJGCGPfeWmv19Rg== - dependencies: - bn.js "^5.2.1" - ethereum-bloom-filters "^1.0.6" - ethereumjs-util "^7.1.0" - ethjs-unit "0.1.6" - number-to-bn "1.7.0" - randombytes "^2.1.0" - utf8 "3.0.0" - web3-utils@1.10.3, web3-utils@^1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.3.tgz#f1db99c82549c7d9f8348f04ffe4e0188b449714" @@ -19554,19 +18781,6 @@ web3-utils@1.10.3, web3-utils@^1.10.3: randombytes "^2.1.0" utf8 "3.0.0" -web3@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/web3/-/web3-1.10.0.tgz#2fde0009f59aa756c93e07ea2a7f3ab971091274" - integrity sha512-YfKY9wSkGcM8seO+daR89oVTcbu18NsVfvOngzqMYGUU0pPSQmE57qQDvQzUeoIOHAnXEBNzrhjQJmm8ER0rng== - dependencies: - web3-bzz "1.10.0" - web3-core "1.10.0" - web3-eth "1.10.0" - web3-eth-personal "1.10.0" - web3-net "1.10.0" - web3-shh "1.10.0" - web3-utils "1.10.0" - web3@^1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/web3/-/web3-1.10.3.tgz#5e80ac532dc432b09fde668d570b0ad4e6710897" @@ -19729,11 +18943,6 @@ whatwg-encoding@^2.0.0: dependencies: iconv-lite "0.6.3" -whatwg-fetch@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" - integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== - whatwg-mimetype@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" @@ -20102,13 +19311,6 @@ ws@^3.0.0: safe-buffer "~5.1.0" ultron "~1.1.0" -ws@^5.1.1: - version "5.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d" - integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA== - dependencies: - async-limiter "~1.0.0" - ws@^8.11.0, ws@^8.2.3, ws@^8.5.0: version "8.16.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" @@ -20134,7 +19336,7 @@ xhr-request@^1.0.1, xhr-request@^1.1.0: url-set-query "^1.0.0" xhr "^2.0.4" -xhr@^2.0.4, xhr@^2.2.0, xhr@^2.3.3: +xhr@^2.0.4, xhr@^2.3.3: version "2.6.0" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.6.0.tgz#b69d4395e792b4173d6b7df077f0fc5e4e2b249d" integrity sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA== @@ -20154,7 +19356,7 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1: +xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== From ed75771a8d29ca66ff1e1f0538bbf640c121ef72 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Fri, 14 Jun 2024 15:56:59 +0200 Subject: [PATCH 086/154] Tests: add testid for pk input --- src/services/private-key-module/PkModulePopup.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/services/private-key-module/PkModulePopup.tsx b/src/services/private-key-module/PkModulePopup.tsx index c226cd81c3..a32558f14c 100644 --- a/src/services/private-key-module/PkModulePopup.tsx +++ b/src/services/private-key-module/PkModulePopup.tsx @@ -29,7 +29,15 @@ const PkModulePopup = () => {
    - + diff --git a/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx b/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx index f08b4395e2..0bfa985221 100644 --- a/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx +++ b/src/components/tx-flow/flows/ReplaceTx/DeleteTxModal.tsx @@ -130,6 +130,7 @@ const _DeleteTxModal = ({ safeTxHash, onSuccess, onClose, wallet, safeAddress, c - - - {'Safe{Wallet}'} mobile signer key (optional){' '} - - - - - - - Use your mobile phone as an additional signer key - From 2ecbdb81ee661113735b47255a3cc718676a6dd7 Mon Sep 17 00:00:00 2001 From: James Mealy Date: Thu, 20 Jun 2024 14:07:02 +0100 Subject: [PATCH 092/154] Feat: visually group transactions executed in bulk in the transaction history (#3772) * Group transactions in the history by transaction hash * remove commented code * create component for bulk transaction groups * layout for bulk group * fix: misalignment of txs on smaller screens * align grouped columns with non grouped columns * fix: column widths on mobile * Add unit test, wrap grouping functions * fix: lint errors * fix: untrusted txs warning placement * Use tx hash returned from backend * lint errors * remove unused function * Change layout and change explorer link * add unit test for grouping function * lodash import * fix typo * remove orange border from tx group * fix: transaction count color * change bulk group title --- package.json | 2 +- .../batch/BatchSidebar/BatchTxItem.tsx | 1 + .../common/ExplorerButton/index.tsx | 42 ++++++++--- .../AppFrame/__tests__/AppFrame.test.tsx | 1 + .../transactions/BulkTxListGroup/index.tsx | 53 ++++++++++++++ .../BulkTxListGroup/styles.module.css | 60 ++++++++++++++++ .../transactions/GroupedTxListItems/index.tsx | 2 +- src/components/transactions/TxList/index.tsx | 26 +++++-- .../TxListItem/ExpandableTransactionItem.tsx | 8 ++- .../transactions/TxSummary/index.test.tsx | 28 +++++--- .../transactions/TxSummary/index.tsx | 10 +-- .../transactions/TxSummary/styles.module.css | 12 +++- src/tests/mocks/transactions.ts | 1 + src/utils/__tests__/tx-list.test.ts | 71 +++++++++++++++++-- src/utils/transactions.ts | 1 + src/utils/tx-list.ts | 30 ++++++++ yarn.lock | 8 +-- 17 files changed, 310 insertions(+), 46 deletions(-) create mode 100644 src/components/transactions/BulkTxListGroup/index.tsx create mode 100644 src/components/transactions/BulkTxListGroup/styles.module.css diff --git a/package.json b/package.json index 4a1b6d7360..360daff529 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@safe-global/protocol-kit": "^3.1.1", "@safe-global/safe-apps-sdk": "^9.1.0", "@safe-global/safe-deployments": "^1.36.0", - "@safe-global/safe-gateway-typescript-sdk": "3.21.1", + "@safe-global/safe-gateway-typescript-sdk": "3.21.2", "@safe-global/safe-modules-deployments": "^1.2.0", "@sentry/react": "^7.91.0", "@spindl-xyz/attribution-lite": "^1.4.0", diff --git a/src/components/batch/BatchSidebar/BatchTxItem.tsx b/src/components/batch/BatchSidebar/BatchTxItem.tsx index a9b27f44ca..b000dd5d2a 100644 --- a/src/components/batch/BatchSidebar/BatchTxItem.tsx +++ b/src/components/batch/BatchSidebar/BatchTxItem.tsx @@ -26,6 +26,7 @@ const BatchTxItem = ({ id, count, timestamp, txDetails, onDelete }: BatchTxItemP txInfo: txDetails.txInfo, txStatus: txDetails.txStatus, safeAppInfo: txDetails.safeAppInfo, + txHash: txDetails.txHash || null, }), [timestamp, txDetails], ) diff --git a/src/components/common/ExplorerButton/index.tsx b/src/components/common/ExplorerButton/index.tsx index c29133bf79..4bc6db7373 100644 --- a/src/components/common/ExplorerButton/index.tsx +++ b/src/components/common/ExplorerButton/index.tsx @@ -1,6 +1,7 @@ import type { ReactElement, ComponentType, SyntheticEvent } from 'react' -import { IconButton, SvgIcon, Tooltip } from '@mui/material' +import { Box, IconButton, SvgIcon, Tooltip, Typography } from '@mui/material' import LinkIcon from '@/public/images/common/link.svg' +import Link from 'next/link' export type ExplorerButtonProps = { title?: string @@ -8,6 +9,7 @@ export type ExplorerButtonProps = { className?: string icon?: ComponentType onClick?: (e: SyntheticEvent) => void + isCompact?: boolean } const ExplorerButton = ({ @@ -16,21 +18,41 @@ const ExplorerButton = ({ icon = LinkIcon, className, onClick, -}: ExplorerButtonProps): ReactElement => ( - - { + return isCompact ? ( + + + + + + ) : ( + - - - -) + + + View on explorer + + + + + + ) +} export default ExplorerButton diff --git a/src/components/safe-apps/AppFrame/__tests__/AppFrame.test.tsx b/src/components/safe-apps/AppFrame/__tests__/AppFrame.test.tsx index 9ef6c07eb4..584485a9ad 100644 --- a/src/components/safe-apps/AppFrame/__tests__/AppFrame.test.tsx +++ b/src/components/safe-apps/AppFrame/__tests__/AppFrame.test.tsx @@ -61,6 +61,7 @@ describe('AppFrame', () => { }, ], }, + txHash: null, }, conflictType: ConflictType.NONE, }, diff --git a/src/components/transactions/BulkTxListGroup/index.tsx b/src/components/transactions/BulkTxListGroup/index.tsx new file mode 100644 index 0000000000..32c04fc70a --- /dev/null +++ b/src/components/transactions/BulkTxListGroup/index.tsx @@ -0,0 +1,53 @@ +import type { ReactElement } from 'react' +import { Box, Paper, SvgIcon, Typography } from '@mui/material' +import type { Transaction } from '@safe-global/safe-gateway-typescript-sdk' +import { isMultisigExecutionInfo } from '@/utils/transaction-guards' +import ExpandableTransactionItem from '@/components/transactions/TxListItem/ExpandableTransactionItem' +import BatchIcon from '@/public/images/common/batch.svg' +import css from './styles.module.css' +import ExplorerButton from '@/components/common/ExplorerButton' +import { getBlockExplorerLink } from '@/utils/chains' +import { useCurrentChain } from '@/hooks/useChains' + +const GroupedTxListItems = ({ + groupedListItems, + transactionHash, +}: { + groupedListItems: Transaction[] + transactionHash: string +}): ReactElement | null => { + const chain = useCurrentChain() + const explorerLink = chain && getBlockExplorerLink(chain, transactionHash)?.href + if (groupedListItems.length === 0) return null + + return ( + + + + + + Bulk transactions + + {groupedListItems.length} transactions + + + + + + {groupedListItems.map((tx) => { + const nonce = isMultisigExecutionInfo(tx.transaction.executionInfo) ? tx.transaction.executionInfo.nonce : '' + return ( + + + {nonce} + + + + ) + })} + + + ) +} + +export default GroupedTxListItems diff --git a/src/components/transactions/BulkTxListGroup/styles.module.css b/src/components/transactions/BulkTxListGroup/styles.module.css new file mode 100644 index 0000000000..da621dcdbc --- /dev/null +++ b/src/components/transactions/BulkTxListGroup/styles.module.css @@ -0,0 +1,60 @@ +.container { + position: relative; + padding: var(--space-2); + display: grid; + align-items: center; + grid-template-columns: minmax(50px, 0.25fr) minmax(240px, 2fr) minmax(150px, 4fr) minmax(170px, 1fr); + grid-template-areas: + 'icon info action hash' + 'nonce items items items'; +} + +.action { + margin-left: var(--space-2); + grid-area: action; + color: var(--color-text-secondary); +} + +.hash { + grid-area: hash; + display: grid; + justify-content: flex-end; +} + +.nonce { + position: absolute; + left: -24px; + top: var(--space-1); +} + +.txItems { + display: flex; + flex-direction: column; + gap: var(--space-1); + margin-top: var(--space-2); +} + +.txItems :global(.MuiAccordion-root) { + border-color: var(--color-border-light); +} + +@media (max-width: 699px) { + .container { + grid-template-columns: minmax(30px, 0.25fr) minmax(230px, 3fr); + grid-template-areas: + 'icon info ' + 'nonce action' + 'nonce hash ' + 'nonce items'; + } + + .action { + margin: 0; + } + .hash { + justify-content: flex-start; + } + .nonce { + left: -16px; + } +} diff --git a/src/components/transactions/GroupedTxListItems/index.tsx b/src/components/transactions/GroupedTxListItems/index.tsx index 6bd632611d..424bcdcc89 100644 --- a/src/components/transactions/GroupedTxListItems/index.tsx +++ b/src/components/transactions/GroupedTxListItems/index.tsx @@ -45,7 +45,7 @@ const TxGroup = ({ groupedListItems }: { groupedListItems: Transaction[] }): Rea key={tx.transaction.id} className={replacedTxIds.includes(tx.transaction.id) ? css.willBeReplaced : undefined} > - +
  • ))} diff --git a/src/components/transactions/TxList/index.tsx b/src/components/transactions/TxList/index.tsx index ac816b689b..d7f8b45866 100644 --- a/src/components/transactions/TxList/index.tsx +++ b/src/components/transactions/TxList/index.tsx @@ -1,29 +1,41 @@ import GroupedTxListItems from '@/components/transactions/GroupedTxListItems' -import { groupConflictingTxs } from '@/utils/tx-list' +import { groupTxs } from '@/utils/tx-list' import { Box } from '@mui/material' -import type { TransactionListPage } from '@safe-global/safe-gateway-typescript-sdk' +import type { Transaction, TransactionListPage } from '@safe-global/safe-gateway-typescript-sdk' import type { ReactElement, ReactNode } from 'react' import { useMemo } from 'react' import TxListItem from '../TxListItem' import css from './styles.module.css' +import uniq from 'lodash/uniq' +import BulkTxListGroup from '@/components/transactions/BulkTxListGroup' type TxListProps = { items: TransactionListPage['results'] } +const getBulkGroupTxHash = (group: Transaction[]) => { + const hashList = group.map((item) => item.transaction.txHash) + return uniq(hashList).length === 1 ? hashList[0] : undefined +} + export const TxListGrid = ({ children }: { children: ReactNode }): ReactElement => { return {children} } const TxList = ({ items }: TxListProps): ReactElement => { - const groupedItems = useMemo(() => groupConflictingTxs(items), [items]) + const groupedTransactions = useMemo(() => groupTxs(items), [items]) + + const transactions = groupedTransactions.map((item, index) => { + if (!Array.isArray(item)) { + return + } - const transactions = groupedItems.map((item, index) => { - if (Array.isArray(item)) { - return + const bulkTransactionHash = getBulkGroupTxHash(item) + if (bulkTransactionHash) { + return } - return + return }) return {transactions} diff --git a/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx b/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx index 9db293bb7c..f94fb2b358 100644 --- a/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx +++ b/src/components/transactions/TxListItem/ExpandableTransactionItem.tsx @@ -12,13 +12,15 @@ import classNames from 'classnames' import { trackEvent, TX_LIST_EVENTS } from '@/services/analytics' type ExpandableTransactionItemProps = { - isGrouped?: boolean + isConflictGroup?: boolean + isBulkGroup?: boolean item: Transaction txDetails?: TransactionDetails } export const ExpandableTransactionItem = ({ - isGrouped = false, + isConflictGroup = false, + isBulkGroup = false, item, txDetails, testId, @@ -56,7 +58,7 @@ export const ExpandableTransactionItem = ({ }, }} > - + diff --git a/src/components/transactions/TxSummary/index.test.tsx b/src/components/transactions/TxSummary/index.test.tsx index f14dfa919a..84a1a719ef 100644 --- a/src/components/transactions/TxSummary/index.test.tsx +++ b/src/components/transactions/TxSummary/index.test.tsx @@ -54,64 +54,72 @@ const mockTransactionInHistory = { describe('TxSummary', () => { it('should display a nonce if transaction is not grouped', () => { - const { getByText } = render() + const { getByText } = render() expect(getByText('7')).toBeInTheDocument() }) it('should not display a nonce if transaction is grouped', () => { - const { queryByText } = render() + const { queryByText } = render() expect(queryByText('7')).not.toBeInTheDocument() }) it('should not display a nonce if there is no executionInfo', () => { - const { queryByText } = render() + const { queryByText } = render() + + expect(queryByText('7')).not.toBeInTheDocument() + }) + + it('should not display a nonce for items in bulk execution group', () => { + const { queryByText } = render( + , + ) expect(queryByText('7')).not.toBeInTheDocument() }) it('should display confirmations if transactions is in queue', () => { - const { getByText } = render() + const { getByText } = render() expect(getByText('1 out of 3')).toBeInTheDocument() }) it('should not display confirmations if transactions is already executed', () => { - const { queryByText } = render() + const { queryByText } = render() expect(queryByText('1 out of 3')).not.toBeInTheDocument() }) it('should not display confirmations if there is no executionInfo', () => { - const { queryByText } = render() + const { queryByText } = render() expect(queryByText('1 out of 3')).not.toBeInTheDocument() }) it('should display a Sign button if confirmations are missing', () => { - const { getByText } = render() + const { getByText } = render() expect(getByText('Confirm')).toBeInTheDocument() }) it('should display a status label if transaction is in queue and pending', () => { jest.spyOn(pending, 'default').mockReturnValue(true) - const { getByTestId } = render() + const { getByTestId } = render() expect(getByTestId('tx-status-label')).toBeInTheDocument() }) it('should display a status label if transaction is not in queue', () => { jest.spyOn(pending, 'default').mockReturnValue(true) - const { getByTestId } = render() + const { getByTestId } = render() expect(getByTestId('tx-status-label')).toBeInTheDocument() }) it('should not display a status label if transaction is in queue and not pending', () => { jest.spyOn(pending, 'default').mockReturnValue(false) - const { queryByTestId } = render() + const { queryByTestId } = render() expect(queryByTestId('tx-status-label')).not.toBeInTheDocument() }) diff --git a/src/components/transactions/TxSummary/index.tsx b/src/components/transactions/TxSummary/index.tsx index 38a316f5dd..14081165da 100644 --- a/src/components/transactions/TxSummary/index.tsx +++ b/src/components/transactions/TxSummary/index.tsx @@ -20,11 +20,12 @@ import { FEATURES } from '@/utils/chains' import TxStatusLabel from '@/components/transactions/TxStatusLabel' type TxSummaryProps = { - isGrouped?: boolean + isConflictGroup?: boolean + isBulkGroup?: boolean item: Transaction } -const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => { +const TxSummary = ({ item, isConflictGroup, isBulkGroup }: TxSummaryProps): ReactElement => { const hasDefaultTokenlist = useHasFeature(FEATURES.DEFAULT_TOKENLIST) const tx = item.transaction @@ -40,12 +41,13 @@ const TxSummary = ({ item, isGrouped }: TxSummaryProps): ReactElement => { data-testid="transaction-item" className={classNames(css.gridContainer, { [css.history]: !isQueue, - [css.grouped]: isGrouped, + [css.conflictGroup]: isConflictGroup, + [css.bulkGroup]: isBulkGroup, [css.untrusted]: !isTrusted, })} id={tx.id} > - {nonce !== undefined && !isGrouped && ( + {nonce !== undefined && !isConflictGroup && !isBulkGroup && ( {nonce} diff --git a/src/components/transactions/TxSummary/styles.module.css b/src/components/transactions/TxSummary/styles.module.css index bde2df708e..80ce87ffbd 100644 --- a/src/components/transactions/TxSummary/styles.module.css +++ b/src/components/transactions/TxSummary/styles.module.css @@ -28,13 +28,23 @@ grid-template-areas: 'nonce type info date status'; } -.gridContainer.grouped { +.gridContainer.conflictGroup { grid-template-columns: var(--grid-type) var(--grid-info) var(--grid-date) var(--grid-confirmations) var(--grid-status) var( --grid-actions ); grid-template-areas: 'type info date confirmations status actions'; } +.gridContainer.bulkGroup { + grid-template-columns: var(--grid-type) var(--grid-info) var(--grid-date) var(--grid-status); + grid-template-areas: 'type info date status'; +} + +.gridContainer.bulkGroup.untrusted { + grid-template-columns: var(--grid-nonce) minmax(200px, 2.4fr) var(--grid-info) var(--grid-date) var(--grid-status); + grid-template-areas: 'nonce type info date status'; +} + .gridContainer.message { grid-template-columns: var(--grid-type) var(--grid-info) var(--grid-date) var(--grid-status) var(--grid-confirmations); grid-template-areas: 'type info date status confirmations'; diff --git a/src/tests/mocks/transactions.ts b/src/tests/mocks/transactions.ts index 427c3ad4b5..fa4c548a95 100644 --- a/src/tests/mocks/transactions.ts +++ b/src/tests/mocks/transactions.ts @@ -44,6 +44,7 @@ export const defaultTx: TransactionSummary = { confirmationsRequired: 2, confirmationsSubmitted: 2, }, + txHash: null, } export const getMockTx = ({ nonce }: { nonce?: number }): Transaction => { diff --git a/src/utils/__tests__/tx-list.test.ts b/src/utils/__tests__/tx-list.test.ts index 340cea2f7b..db7ca71c0f 100644 --- a/src/utils/__tests__/tx-list.test.ts +++ b/src/utils/__tests__/tx-list.test.ts @@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker' import { TransactionInfoType } from '@safe-global/safe-gateway-typescript-sdk' import type { TransactionListItem } from '@safe-global/safe-gateway-typescript-sdk' -import { groupConflictingTxs, groupRecoveryTransactions, _getRecoveryCancellations } from '@/utils/tx-list' +import { groupTxs, groupRecoveryTransactions, _getRecoveryCancellations } from '@/utils/tx-list' describe('tx-list', () => { describe('groupConflictingTxs', () => { @@ -25,7 +25,7 @@ describe('tx-list', () => { }, ] - const result = groupConflictingTxs(list as TransactionListItem[]) + const result = groupTxs(list as TransactionListItem[]) expect(result).toEqual([ [ { @@ -67,7 +67,7 @@ describe('tx-list', () => { }, ] - const result = groupConflictingTxs(list as TransactionListItem[]) + const result = groupTxs(list as TransactionListItem[]) expect(result).toEqual([ [ { @@ -90,12 +90,13 @@ describe('tx-list', () => { ]) }) - it('should return non-conflicting transaction lists as is', () => { + it('should group transactions with the same txHash (bulk txs)', () => { const list = [ { type: 'TRANSACTION', transaction: { id: 1, + txHash: '0x123', }, conflictType: 'None', }, @@ -103,12 +104,72 @@ describe('tx-list', () => { type: 'TRANSACTION', transaction: { id: 2, + txHash: '0x123', + }, + conflictType: 'None', + }, + { + type: 'TRANSACTION', + transaction: { + id: 3, + txHash: '0x456', + }, + conflictType: 'None', + }, + ] + + const result = groupTxs(list as unknown as TransactionListItem[]) + expect(result).toEqual([ + [ + { + type: 'TRANSACTION', + transaction: { + id: 1, + txHash: '0x123', + }, + conflictType: 'None', + }, + { + type: 'TRANSACTION', + transaction: { + id: 2, + txHash: '0x123', + }, + conflictType: 'None', + }, + ], + { + type: 'TRANSACTION', + transaction: { + id: 3, + txHash: '0x456', + }, + conflictType: 'None', + }, + ]) + }) + + it('should return non-conflicting, and non bulk transaction lists as is', () => { + const list = [ + { + type: 'TRANSACTION', + transaction: { + id: 1, + txHash: '0x123', + }, + conflictType: 'None', + }, + { + type: 'TRANSACTION', + transaction: { + id: 2, + txHash: '0x345', }, conflictType: 'None', }, ] - const result = groupConflictingTxs(list as unknown as TransactionListItem[]) + const result = groupTxs(list as unknown as TransactionListItem[]) expect(result).toEqual(list) }) }) diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index 4ef03485a4..3e9d5d0f99 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -86,6 +86,7 @@ export const makeTxFromDetails = (txDetails: TransactionDetails): Transaction => txInfo: txDetails.txInfo, executionInfo, safeAppInfo: txDetails?.safeAppInfo, + txHash: txDetails?.txHash || null, }, conflictType: ConflictType.NONE, } diff --git a/src/utils/tx-list.ts b/src/utils/tx-list.ts index f9737be4be..f54b89bf0b 100644 --- a/src/utils/tx-list.ts +++ b/src/utils/tx-list.ts @@ -7,6 +7,11 @@ import type { RecoveryQueueItem } from '@/features/recovery/services/recovery-st type GroupedTxs = Array +export const groupTxs = (list: TransactionListItem[]) => { + const groupedByConflicts = groupConflictingTxs(list) + return groupBulkTxs(groupedByConflicts) +} + /** * Group txs by conflict header */ @@ -33,6 +38,31 @@ export const groupConflictingTxs = (list: TransactionListItem[]): GroupedTxs => }) } +/** + * Group txs by tx hash + */ +const groupBulkTxs = (list: GroupedTxs): GroupedTxs => { + return list + .reduce((resultItems, item) => { + if (Array.isArray(item) || !isTransactionListItem(item)) { + return resultItems.concat([item]) + } + const currentTxHash = item.transaction.txHash + + const prevItem = resultItems[resultItems.length - 1] + if (!Array.isArray(prevItem)) return resultItems.concat([[item]]) + const prevTxHash = prevItem[0].transaction.txHash + + if (currentTxHash && currentTxHash === prevTxHash) { + prevItem.push(item) + return resultItems + } + + return resultItems.concat([[item]]) + }, []) + .map((item) => (Array.isArray(item) && item.length === 1 ? item[0] : item)) +} + export function _getRecoveryCancellations(moduleAddress: string, transactions: Array) { const CANCELLATION_TX_METHOD_NAME = 'setTxNonce' diff --git a/yarn.lock b/yarn.lock index 71f0d89847..5a8ca3b324 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4156,10 +4156,10 @@ dependencies: semver "^7.6.0" -"@safe-global/safe-gateway-typescript-sdk@3.21.1": - version "3.21.1" - resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.1.tgz#984ec2d3d4211caf6a96786ab922b39909093538" - integrity sha512-7nakIjcRSs6781LkizYpIfXh1DYlkUDqyALciqz/BjFU/S97sVjZdL4cuKsG9NEarytE+f6p0Qbq2Bo1aocVUA== +"@safe-global/safe-gateway-typescript-sdk@3.21.2": + version "3.21.2" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.2.tgz#2123a7429c2d9713365f51c359bfc055d4c8e913" + integrity sha512-N9Y2CKPBVbc8FbOKzqepy8TJUY2VILX7bmxV4ruByLJvR9PBnGvGfnOhw975cDn6PmSziXL0RaUWHpSW23rsng== "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.21.2" From edda9148a3800fc641c857edfa933107fa38e9bb Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:25:35 +0200 Subject: [PATCH 093/154] Fix: ignore nonce for risk scanning requests (#3854) Move nonce reset to useRedefine --- .../tx/security/redefine/useRedefine.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/tx/security/redefine/useRedefine.ts b/src/components/tx/security/redefine/useRedefine.ts index dde017cba6..d1f6d3b620 100644 --- a/src/components/tx/security/redefine/useRedefine.ts +++ b/src/components/tx/security/redefine/useRedefine.ts @@ -80,22 +80,33 @@ export const useRedefine = ( const [retryCounter, setRetryCounter] = useState(0) const isFeatureEnabled = useHasFeature(FEATURES.RISK_MITIGATION) + // Memoized JSON data to avoid unnecessary requests + const jsonData = useMemo(() => { + if (!data) return '' + let adjustedData = data + if ('data' in data) { + // We need to set nonce to 0 to avoid repeated requests with an updated nonce + adjustedData = { ...data, data: { ...data.data, nonce: 0 } } + } + return JSON.stringify(adjustedData) + }, [data]) + const [redefinePayload, redefineErrors, redefineLoading] = useAsync>( () => { - if (!isFeatureEnabled || !data || !wallet?.address) { + if (!isFeatureEnabled || !jsonData || !wallet?.address) { return } return RedefineModuleInstance.scanTransaction({ chainId: Number(safe.chainId), - data, + data: JSON.parse(jsonData), safeAddress, walletAddress: wallet.address, threshold: safe.threshold, }) }, // eslint-disable-next-line react-hooks/exhaustive-deps - [safe.chainId, safe.threshold, safeAddress, data, wallet?.address, retryCounter, isFeatureEnabled], + [safe.chainId, safe.threshold, safeAddress, jsonData, wallet?.address, retryCounter, isFeatureEnabled], false, ) From f7134ca69eafe398a0118a76a3c42e292a1269b1 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Fri, 21 Jun 2024 09:25:59 +0200 Subject: [PATCH 094/154] 1.38.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b19e9ad80..bbfbd32363 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.38.0", + "version": "1.38.1", "type": "module", "scripts": { "dev": "next dev", From e47d1087facdee12a6d31439be005d2365a96617 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:52:06 +0200 Subject: [PATCH 095/154] Fix: delay chainChanged event in test wallet (#3859) --- src/services/private-key-module/index.ts | 10 ++++------ src/tests/e2e-wallet.ts | 12 ++++-------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/services/private-key-module/index.ts b/src/services/private-key-module/index.ts index 8e0f6f04ef..355bd2ded9 100644 --- a/src/services/private-key-module/index.ts +++ b/src/services/private-key-module/index.ts @@ -43,7 +43,6 @@ const PrivateKeyModule = (chainId: ChainInfo['chainId'], rpcUri: ChainInfo['rpcU let provider: JsonRpcProvider let wallet: Wallet - let lastChainId = '' const chainChangedListeners = new Set<(chainId: string) => void>() const updateProvider = () => { @@ -51,8 +50,10 @@ const PrivateKeyModule = (chainId: ChainInfo['chainId'], rpcUri: ChainInfo['rpcU provider?.destroy() provider = new JsonRpcProvider(currentRpcUri, Number(currentChainId), { staticNetwork: true }) wallet = new Wallet(privateKey, provider) - lastChainId = currentChainId - chainChangedListeners.forEach((listener) => listener(numberToHex(Number(currentChainId)))) + + setTimeout(() => { + chainChangedListeners.forEach((listener) => listener(numberToHex(Number(currentChainId)))) + }, 100) } updateProvider() @@ -71,9 +72,6 @@ const PrivateKeyModule = (chainId: ChainInfo['chainId'], rpcUri: ChainInfo['rpcU }, request: async (request: { method: string; params: any[] }) => { - if (currentChainId !== lastChainId) { - updateProvider() - } return provider.send(request.method, request.params) }, diff --git a/src/tests/e2e-wallet.ts b/src/tests/e2e-wallet.ts index f29a056412..17ce32c32e 100644 --- a/src/tests/e2e-wallet.ts +++ b/src/tests/e2e-wallet.ts @@ -21,15 +21,16 @@ const E2EWalletMoule = (chainId: ChainInfo['chainId'], rpcUri: ChainInfo['rpcUri getInterface: async () => { let provider: JsonRpcProvider let wallet: HDNodeWallet - let lastChainId = '' const chainChangedListeners = new Set<(chainId: string) => void>() const updateProvider = () => { provider?.destroy() provider = new JsonRpcProvider(currentRpcUri, Number(currentChainId), { staticNetwork: true }) wallet = Wallet.fromPhrase(CYPRESS_MNEMONIC, provider) - lastChainId = currentChainId - chainChangedListeners.forEach((listener) => listener(numberToHex(Number(currentChainId)))) + + setTimeout(() => { + chainChangedListeners.forEach((listener) => listener(numberToHex(Number(currentChainId)))) + }, 100) } updateProvider() @@ -48,13 +49,8 @@ const E2EWalletMoule = (chainId: ChainInfo['chainId'], rpcUri: ChainInfo['rpcUri }, request: async (request: { method: string; params: any[] }) => { - if (currentChainId !== lastChainId) { - updateProvider() - } return provider.send(request.method, request.params) }, - - disconnect: () => {}, }, { eth_chainId: async () => currentChainId, From 7da267f7423f653f6e47a36c223290116233cf4d Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:48:39 +0200 Subject: [PATCH 096/154] Fix: add loading indicator for safe list (#3860) --- src/components/dashboard/ActivityRewardsSection/index.tsx | 4 ++-- src/components/welcome/MyAccounts/PaginatedSafeList.tsx | 8 ++++---- src/components/welcome/MyAccounts/index.tsx | 7 ++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/dashboard/ActivityRewardsSection/index.tsx b/src/components/dashboard/ActivityRewardsSection/index.tsx index ece466568c..63dd020631 100644 --- a/src/components/dashboard/ActivityRewardsSection/index.tsx +++ b/src/components/dashboard/ActivityRewardsSection/index.tsx @@ -87,11 +87,11 @@ const ActivityRewardsSection = () => { - + - + How it works diff --git a/src/components/welcome/MyAccounts/PaginatedSafeList.tsx b/src/components/welcome/MyAccounts/PaginatedSafeList.tsx index 9fdce8e302..da910de584 100644 --- a/src/components/welcome/MyAccounts/PaginatedSafeList.tsx +++ b/src/components/welcome/MyAccounts/PaginatedSafeList.tsx @@ -8,7 +8,7 @@ import { sameAddress } from '@/utils/addresses' import InfiniteScroll from '@/components/common/InfiniteScroll' type PaginatedSafeListProps = { - safes: SafeItem[] + safes?: SafeItem[] title: ReactNode noSafesMessage?: ReactNode action?: ReactElement @@ -79,7 +79,7 @@ const PaginatedSafeList = ({ safes, title, action, noSafesMessage, onLinkClick } {title} - {safes.length > 0 && ( + {safes && safes.length > 0 && ( {' '} ({safes.length}) @@ -90,11 +90,11 @@ const PaginatedSafeList = ({ safes, title, action, noSafesMessage, onLinkClick } {action}
    - {safes.length > 0 ? ( + {safes && safes.length > 0 ? ( ) : ( - {noSafesMessage} + {safes ? noSafesMessage : 'Loading...'} )}
    diff --git a/src/components/welcome/MyAccounts/index.tsx b/src/components/welcome/MyAccounts/index.tsx index 6bf3e19f3e..193d65fac9 100644 --- a/src/components/welcome/MyAccounts/index.tsx +++ b/src/components/welcome/MyAccounts/index.tsx @@ -17,9 +17,10 @@ import { useRouter } from 'next/router' import useTrackSafesCount from './useTrackedSafesCount' const NO_SAFES_MESSAGE = "You don't have any Safe Accounts yet" +const NO_WATCHED_MESSAGE = 'Watch any Safe Account to keep an eye on its activity' type AccountsListProps = { - safes: SafeItems | undefined + safes?: SafeItems | undefined onLinkClick?: () => void } const AccountsList = ({ safes, onLinkClick }: AccountsListProps) => { @@ -47,7 +48,7 @@ const AccountsList = ({ safes, onLinkClick }: AccountsListProps) => { { } - noSafesMessage={NO_SAFES_MESSAGE} + noSafesMessage={NO_WATCHED_MESSAGE} onLinkClick={onLinkClick} /> From 2fd1a6fcef6bb364076d100b15a2a913c9d30f03 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:49:29 +0200 Subject: [PATCH 097/154] Chore: update privacy policy (#3857) * Chore: update privacy policy * Safe Wallet spelling --- src/pages/privacy.tsx | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/pages/privacy.tsx b/src/pages/privacy.tsx index 6e68c86e18..3e26d6b2db 100644 --- a/src/pages/privacy.tsx +++ b/src/pages/privacy.tsx @@ -49,7 +49,7 @@ const SafePrivacyPolicy = () => ( `}

    Privacy Policy

    -

    Last updated: January 2024.

    +

    Last updated: June 2024.

    Your privacy is important to us. It is our policy to respect your privacy and comply with any applicable law and regulation regarding any personal information we may collect about you, including across our website,{' '} @@ -338,7 +338,7 @@ const SafePrivacyPolicy = () => ( SECTION 2, THIS DATA WILL BECOME PUBLIC AND IT WILL NOT LIKELY BE POSSIBLE TO DELETE OR CHANGE THE DATA AT ANY GIVEN TIME.

    -

    4.2. Tracking

    +

    4.2. Tracking & Analysis

    4.2.1 We will process the following personal data to analyze your behavior:

    1. IP address (will not be stored for EU users),
    2. @@ -370,7 +370,24 @@ const SafePrivacyPolicy = () => (

    - 4.2.2 We conduct technical monitoring of your activity on the platform in order to ensure availability, integrity + 4.2.2 For general operational analysis of the {'Safe{Wallet}'} interface, monitoring transaction origins and + measuring transaction failure rates to ensure improved service performance and reliability, we process information + which constitutes the transaction service database, such as: +

    +
      +
    1. signatures
    2. +
    3. signature_type
    4. +
    5. ethereum_tx_id
    6. +
    7. message_hash
    8. +
    9. safe_app_id
    10. +
    11. safe_message_id
    12. +
    +

    + We conduct this analysis in our legitimate interest to continuously improve our product and service and ensure + increased service performance and reliability. +

    +

    + 4.2.3 We conduct technical monitoring of your activity on the platform in order to ensure availability, integrity and robustness of the service. For this purpose we process your:

      @@ -383,7 +400,7 @@ const SafePrivacyPolicy = () => ( The lawful basis for this processing is our legitimate interest (GDPR Art.6.1f) in ensuring the correctness of the service.

      -

      4.2.3. Anonymized tracking

      +

      4.2.4 Anonymized tracking

      We will anonymize the following personal data to gather anonymous user statistics on your browsing behavior on our website: @@ -689,15 +706,7 @@ const SafePrivacyPolicy = () => ( -

      5.13. Web3Auth

      -

      - We use{' '} - - Web3Auth - {' '} - to create a signer wallet/an owner account by using the user's Gmail account or Apple ID information. -

      -

      5.14. MoonPay

      +

      5.13. MoonPay

      We use{' '} @@ -706,7 +715,7 @@ const SafePrivacyPolicy = () => ( to offer on-ramp and off-ramp services. For that purpose personal data is required for KYC/AML or other financial regulatory requirements. This data is encrypted by MoonPay.

      -

      5.15. Spindl

      +

      5.14. Spindl

      We use{' '} From d37d92bb6d125b9709331dbfafc5f721a4f29fea Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:28:24 +0200 Subject: [PATCH 098/154] Fix: redirect routes w/o ?safe (#3846) * Fix: redirect routes w/o ?safe * Add settings routes --- .../settings/SecurityLogin/index.tsx | 4 ++- src/hooks/useAdjustUrl.ts | 27 +++++++++++++++++-- src/pages/apps/open.tsx | 9 +++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/components/settings/SecurityLogin/index.tsx b/src/components/settings/SecurityLogin/index.tsx index 0fb33dc324..d2ba01e986 100644 --- a/src/components/settings/SecurityLogin/index.tsx +++ b/src/components/settings/SecurityLogin/index.tsx @@ -2,15 +2,17 @@ import { Box } from '@mui/material' import dynamic from 'next/dynamic' import { useIsRecoverySupported } from '@/features/recovery/hooks/useIsRecoverySupported' import SecuritySettings from '../SecuritySettings' +import { useRouter } from 'next/router' const RecoverySettings = dynamic(() => import('@/features/recovery/components/RecoverySettings')) const SecurityLogin = () => { const isRecoverySupported = useIsRecoverySupported() + const router = useRouter() return ( - {isRecoverySupported && } + {isRecoverySupported && router.query.safe ? : null} diff --git a/src/hooks/useAdjustUrl.ts b/src/hooks/useAdjustUrl.ts index 033eef9015..dacb11a3fb 100644 --- a/src/hooks/useAdjustUrl.ts +++ b/src/hooks/useAdjustUrl.ts @@ -1,16 +1,39 @@ import { useEffect } from 'react' import { useRouter } from 'next/router' +import { AppRoutes } from '@/config/routes' + +const SAFE_ROUTES = [ + AppRoutes.balances.index, + AppRoutes.balances.nfts, + AppRoutes.home, + AppRoutes.settings.modules, + AppRoutes.settings.setup, + AppRoutes.swap, + AppRoutes.transactions.index, + AppRoutes.transactions.history, + AppRoutes.transactions.messages, + AppRoutes.transactions.queue, + AppRoutes.transactions.tx, +] // Replace %3A with : in the ?safe= parameter +// Redirect to index if a required safe parameter is missing const useAdjustUrl = () => { - const { asPath } = useRouter() + const router = useRouter() useEffect(() => { + const { asPath, isReady, query, pathname } = router + const newPath = asPath.replace(/([?&]safe=.+?)%3A(?=0x)/g, '$1:') if (newPath !== asPath) { history.replaceState(history.state, '', newPath) + return + } + + if (isReady && !query.safe && SAFE_ROUTES.includes(pathname)) { + router.replace({ pathname: AppRoutes.index }) } - }, [asPath]) + }, [router]) } export default useAdjustUrl diff --git a/src/pages/apps/open.tsx b/src/pages/apps/open.tsx index 4e4696363d..938e690dcb 100644 --- a/src/pages/apps/open.tsx +++ b/src/pages/apps/open.tsx @@ -55,6 +55,15 @@ const SafeApps: NextPage = () => { // appUrl is required to be present if (!isSafeAppsEnabled || !appUrl || !router.isReady) return null + // No `safe` query param, redirect to the share route + if (router.isReady && !router.query.safe) { + router.push({ + pathname: AppRoutes.share.safeApp, + query: { appUrl }, + }) + return null + } + if (isModalVisible) { return ( Date: Mon, 24 Jun 2024 11:28:46 +0200 Subject: [PATCH 099/154] Fix: full token amount in tx details (#3848) * Fix: full token amount in tx details * Use formatAmountPrecise * Update e2e tests --- cypress/fixtures/txhistory_data_data.json | 4 ++-- src/components/common/TokenAmount/index.tsx | 7 ++++++- .../transactions/TxDetails/TxData/Transfer/index.tsx | 6 +++--- src/components/transactions/TxInfo/index.tsx | 12 +++++++++++- src/utils/formatNumber.ts | 4 ++-- src/utils/formatters.ts | 5 +++-- 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/cypress/fixtures/txhistory_data_data.json b/cypress/fixtures/txhistory_data_data.json index b5b2d020d9..1397385c76 100644 --- a/cypress/fixtures/txhistory_data_data.json +++ b/cypress/fixtures/txhistory_data_data.json @@ -36,7 +36,7 @@ "summaryTitle": "Received", "summaryTxInfo": "< 0.00001 ETH", "summaryTime": "11:00 AM", - "receivedFrom": "Received < 0.00001 ETH from:", + "receivedFrom": "Received 0.00000000001 ETH from:", "senderAddress": "sep:0x96D4c6fFC338912322813a77655fCC926b9A5aC5", "transactionHash": "0x4159...3e7d", "transactionHashCopied": "0x415977f4e4912e22a5cabc4116f7e8f8984996e00a641dcccf8cbe1eb3db3e7d", @@ -47,7 +47,7 @@ "title": "Sent", "summaryTxInfo": "-< 0.00001 ETH", "summaryTime": "11:02 AM", - "sentTo": "Sent < 0.00001 ETH to:", + "sentTo": "Sent 0.000000000001 ETH to:", "recipientAddress": "sep:0x06373d5e45AD31BD354CeBfA8dB4eD2c75B8708e", "transactionHash": "0x6a59...6a98", "altImage": "Sent", diff --git a/src/components/common/TokenAmount/index.tsx b/src/components/common/TokenAmount/index.tsx index aed38b31ad..0e8f874404 100644 --- a/src/components/common/TokenAmount/index.tsx +++ b/src/components/common/TokenAmount/index.tsx @@ -5,6 +5,8 @@ import { formatVisualAmount } from '@/utils/formatters' import TokenIcon from '../TokenIcon' import classNames from 'classnames' +const PRECISION = 20 + const TokenAmount = ({ value, decimals, @@ -12,6 +14,7 @@ const TokenAmount = ({ tokenSymbol, direction, fallbackSrc, + preciseAmount, }: { value: string decimals?: number @@ -19,9 +22,11 @@ const TokenAmount = ({ tokenSymbol?: string direction?: TransferDirection fallbackSrc?: string + preciseAmount?: boolean }): ReactElement => { const sign = direction === TransferDirection.OUTGOING ? '-' : '' - const amount = decimals !== undefined ? formatVisualAmount(value, decimals) : value + const amount = + decimals !== undefined ? formatVisualAmount(value, decimals, preciseAmount ? PRECISION : undefined) : value return ( diff --git a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx index 4fde362c28..97338cbbcf 100644 --- a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx +++ b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx @@ -14,7 +14,7 @@ type TransferTxInfoProps = { txStatus: TransactionStatus } -const TransferTxInfoSummary = ({ txInfo, txStatus, trusted }: TransferTxInfoProps & { trusted: boolean }) => { +const TransferTxInfoMain = ({ txInfo, txStatus, trusted }: TransferTxInfoProps & { trusted: boolean }) => { const { direction } = txInfo return ( @@ -22,7 +22,7 @@ const TransferTxInfoSummary = ({ txInfo, txStatus, trusted }: TransferTxInfoProp {direction === TransferDirection.INCOMING ? 'Received' : isTxQueued(txStatus) ? 'Send' : 'Sent'}{' '} - + {direction === TransferDirection.INCOMING ? ' from:' : ' to:'} @@ -36,7 +36,7 @@ const TransferTxInfo = ({ txInfo, txStatus, trusted }: TransferTxInfoProps & { t return ( - + { const chainConfig = useCurrentChain() const { nativeCurrency } = chainConfig || {} @@ -48,12 +50,20 @@ export const TransferTx = ({ decimals={nativeCurrency?.decimals} tokenSymbol={nativeCurrency?.symbol} logoUri={withLogo ? nativeCurrency?.logoUri : undefined} + preciseAmount={preciseAmount} /> ) } if (isERC20Transfer(transfer)) { - return + return ( + + ) } if (isERC721Transfer(transfer)) { diff --git a/src/utils/formatNumber.ts b/src/utils/formatNumber.ts index 77b5c05b13..89ec083074 100644 --- a/src/utils/formatNumber.ts +++ b/src/utils/formatNumber.ts @@ -13,7 +13,7 @@ export const formatAmount = (number: string | number, precision = 5, maxLength = const fullNum = new Intl.NumberFormat(locale, { style: 'decimal', maximumFractionDigits: precision, - }).format(Number(number)) + }).format(float) // +3 for the decimal point and the two decimal places if (fullNum.length <= maxLength + 3) return fullNum @@ -30,7 +30,7 @@ export const formatAmount = (number: string | number, precision = 5, maxLength = * @param number Number to format * @param precision Fraction digits to show */ -export const formatAmountPrecise = (number: string | number, precision: number): string => { +export const formatAmountPrecise = (number: string | number, precision?: number): string => { return new Intl.NumberFormat(locale, { style: 'decimal', maximumFractionDigits: precision, diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index a74fab0143..bbff73c66b 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -1,6 +1,6 @@ import type { BigNumberish } from 'ethers' import { formatUnits, parseUnits } from 'ethers' -import { formatAmount } from './formatNumber' +import { formatAmount, formatAmountPrecise } from './formatNumber' const GWEI = 'gwei' @@ -38,7 +38,8 @@ export const formatVisualAmount = ( decimals: number | string = GWEI, precision?: number, ): string => { - return formatAmount(safeFormatUnits(value, decimals), precision) + const amount = safeFormatUnits(value, decimals) + return precision ? formatAmountPrecise(amount, precision) : formatAmount(amount) } export const safeParseUnits = (value: string, decimals: number | string = GWEI): bigint | undefined => { From 046684fde8f826159119ede558a72dad87b2d068 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:42:34 +0200 Subject: [PATCH 100/154] Tests: Fix regression tests (#3862) * Fix tests * Update tests --- cypress/e2e/pages/owners.pages.js | 2 +- cypress/e2e/regression/remove_owner.cy.js | 7 +++---- cypress/e2e/regression/swaps_history.cy.js | 10 +++++----- cypress/e2e/regression/swaps_tokens.cy.js | 1 - 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index 9dd97666b9..afde4caa6b 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -5,7 +5,7 @@ import * as navigation from '../pages/navigation.page' import * as addressBook from '../pages/address_book.page' const tooltipLabel = (label) => `span[aria-label="${label}"]` -const removeOwnerBtn = 'span[data-track="settings: Remove owner"] > span > button' +export const removeOwnerBtn = 'span[data-track="settings: Remove owner"] > span > button' const replaceOwnerBtn = 'span[data-track="settings: Replace owner"] > span > button' const tooltip = 'div[role="tooltip"]' const expandMoreIcon = 'svg[data-testid="ExpandMoreIcon"]' diff --git a/cypress/e2e/regression/remove_owner.cy.js b/cypress/e2e/regression/remove_owner.cy.js index 2ea8f5a129..7c92dc766b 100644 --- a/cypress/e2e/regression/remove_owner.cy.js +++ b/cypress/e2e/regression/remove_owner.cy.js @@ -25,11 +25,10 @@ describe('Remove Owners tests', () => { owner.verifyRemoveBtnIsEnabled().should('have.length', 2) }) - it('Verify Tooltip displays correct message for Non-Owner', () => { - cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_1) + it('Verify remove button does not exist for Non-Owner when there is only 1 owner in the safe', () => { + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) main.waitForHistoryCallToComplete() - owner.waitForConnectionStatus() - owner.verifyRemoveBtnIsDisabled() + main.verifyElementsCount(owner.removeOwnerBtn, 0) }) it('Verify Tooltip displays correct message for disconnected user', () => { diff --git a/cypress/e2e/regression/swaps_history.cy.js b/cypress/e2e/regression/swaps_history.cy.js index 52dfbbbc2f..46da373ee2 100644 --- a/cypress/e2e/regression/swaps_history.cy.js +++ b/cypress/e2e/regression/swaps_history.cy.js @@ -22,7 +22,8 @@ describe('[SMOKE] Swaps history tests', () => { main.acceptCookies() }) - it('Verify operation names are correct for buying and selling of tokens', { defaultCommandTimeout: 30000 }, () => { + it('Verify sawp buying operation with approve and swap', { defaultCommandTimeout: 30000 }, () => { + // approve, preSignature actions create_tx.clickOnTransactionItemByName('8:05 AM') create_tx.verifyExpandedDetails([ swapsHistory.buyOrder, @@ -34,9 +35,10 @@ describe('[SMOKE] Swaps history tests', () => { swapsHistory.actionApprove, swapsHistory.actionPreSignature, ]) - cy.reload() + }) - create_tx.clickOnTransactionItemByName('11:14 AM') + it('Verify swap selling operation with one action', { defaultCommandTimeout: 30000 }, () => { + create_tx.clickOnTransactionItemByName('14') create_tx.verifyExpandedDetails([ swapsHistory.sellOrder, swapsHistory.sell, @@ -44,8 +46,6 @@ describe('[SMOKE] Swaps history tests', () => { swapsHistory.forAtLeast, swapsHistory.dai, swapsHistory.filled, - swapsHistory.actionApprove, - swapsHistory.actionPreSignature, ]) }) diff --git a/cypress/e2e/regression/swaps_tokens.cy.js b/cypress/e2e/regression/swaps_tokens.cy.js index 2a581c26ae..a303fbb20b 100644 --- a/cypress/e2e/regression/swaps_tokens.cy.js +++ b/cypress/e2e/regression/swaps_tokens.cy.js @@ -27,7 +27,6 @@ describe('[SMOKE] Swaps token tests', () => { swaps.clickOnAssetSwapBtn(0) swaps.acceptLegalDisclaimer() - swaps.waitForOrdersCallToComplete() cy.wait(2000) main.getIframeBody(iframeSelector).within(() => { swaps.verifySelectedInputCurrancy(swaps.swapTokens.eth) From b51729389568c6b3e914bd8a6c88e021766f6dbf Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:44:23 +0200 Subject: [PATCH 101/154] Fix: adjust approval editor with swaps (#3855) * Fix: adjust approval editor with swaps * Update src/components/tx/ApprovalEditor/EditableApprovalItem.tsx Co-authored-by: Manuel Gellfart * Fix: do not reload when changing approval amount --------- Co-authored-by: Manuel Gellfart --- .../tx/ApprovalEditor/ApprovalEditorForm.tsx | 3 +- .../ApprovalEditor/EditableApprovalItem.tsx | 12 +++++-- src/components/tx/ApprovalEditor/index.tsx | 2 +- src/components/tx/DecodedTx/index.tsx | 31 +++++++++---------- src/components/tx/SignOrExecuteForm/index.tsx | 9 ++++++ .../SwapOrderConfirmationView/index.tsx | 10 +++--- src/hooks/useDecodeTx.ts | 18 ++++++----- 7 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx b/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx index 45a8e1b98f..3db81b91d2 100644 --- a/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx +++ b/src/components/tx/ApprovalEditor/ApprovalEditorForm.tsx @@ -46,7 +46,6 @@ export const ApprovalEditorForm = ({ {Object.entries(groupedApprovals).map(([spender, approvals], spenderIdx) => ( - {approvals.map((tx) => ( ))} + + {spenderIdx !== Object.keys(groupedApprovals).length - 1 && } diff --git a/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx b/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx index 8a9c27995e..8208fcc1a3 100644 --- a/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx +++ b/src/components/tx/ApprovalEditor/EditableApprovalItem.tsx @@ -41,12 +41,20 @@ const EditableApprovalItem = ({ } return ( - + + - + + {readOnly ? ( diff --git a/src/components/tx/ApprovalEditor/index.tsx b/src/components/tx/ApprovalEditor/index.tsx index 719341d5da..3f1a3bcf07 100644 --- a/src/components/tx/ApprovalEditor/index.tsx +++ b/src/components/tx/ApprovalEditor/index.tsx @@ -54,7 +54,7 @@ export const ApprovalEditor = ({ const isReadOnly = (safeTransaction && safeTransaction.signatures.size > 0) || safeMessage !== undefined return ( - + {error ? ( <Alert severity="error">Error while decoding approval transactions.</Alert> diff --git a/src/components/tx/DecodedTx/index.tsx b/src/components/tx/DecodedTx/index.tsx index 1788bc7654..506a83aef8 100644 --- a/src/components/tx/DecodedTx/index.tsx +++ b/src/components/tx/DecodedTx/index.tsx @@ -1,5 +1,4 @@ import SendToBlock from '@/components/tx/SendToBlock' -import SwapOrderConfirmationView from '@/features/swap/components/SwapOrderConfirmationView' import { useCurrentChain } from '@/hooks/useChains' import { isSwapConfirmationViewOrder } from '@/utils/transaction-guards' import { type SyntheticEvent, type ReactElement, memo } from 'react' @@ -80,24 +79,24 @@ const DecodedTx = ({ return ( <Stack spacing={2}> - {!isSwapOrder && tx && showToBlock && amount !== '0' && ( - <SendAmountBlock - amount={amount} - tokenInfo={{ - type: TokenType.NATIVE_TOKEN, - address: ZERO_ADDRESS, - decimals: chain?.nativeCurrency.decimals ?? 18, - symbol: chain?.nativeCurrency.symbol ?? 'ETH', - logoUri: chain?.nativeCurrency.logoUri, - }} - /> - )} {!isSwapOrder && tx && showToBlock && ( - <SendToBlock address={tx.data.to} title="Interact with" name={addressInfoIndex?.[tx.data.to]?.name} /> + <> + {amount !== '0' && ( + <SendAmountBlock + amount={amount} + tokenInfo={{ + type: TokenType.NATIVE_TOKEN, + address: ZERO_ADDRESS, + decimals: chain?.nativeCurrency.decimals ?? 18, + symbol: chain?.nativeCurrency.symbol ?? 'ETH', + logoUri: chain?.nativeCurrency.logoUri, + }} + /> + )} + <SendToBlock address={tx.data.to} title="Interact with" name={addressInfoIndex?.[tx.data.to]?.name} /> + </> )} - {isSwapOrder && tx && <SwapOrderConfirmationView order={decodedData} settlementContract={tx.data.to} />} - {isMultisend && showMultisend && ( <Box> <Multisend diff --git a/src/components/tx/SignOrExecuteForm/index.tsx b/src/components/tx/SignOrExecuteForm/index.tsx index c5b3517f88..32a62c86d4 100644 --- a/src/components/tx/SignOrExecuteForm/index.tsx +++ b/src/components/tx/SignOrExecuteForm/index.tsx @@ -27,6 +27,8 @@ import { TX_EVENTS } from '@/services/analytics/events/transactions' import { trackEvent } from '@/services/analytics' import useChainId from '@/hooks/useChainId' import PermissionsCheck from './PermissionsCheck' +import { isSwapConfirmationViewOrder } from '@/utils/transaction-guards' +import SwapOrderConfirmationView from '@/features/swap/components/SwapOrderConfirmationView' export type SubmitCallback = (txId: string, isExecuted?: boolean) => void @@ -74,6 +76,7 @@ export const SignOrExecuteForm = ({ const isCorrectNonce = useValidateNonce(safeTx) const [decodedData, decodedDataError, decodedDataLoading] = useDecodeTx(safeTx) const isBatchable = props.isBatchable !== false && safeTx && !isDelegateCall(safeTx) + const isSwapOrder = isSwapConfirmationViewOrder(decodedData) const { safe } = useSafeInfo() const isCounterfactualSafe = !safe.deployed @@ -97,6 +100,12 @@ export const SignOrExecuteForm = ({ <TxCard> {props.children} + {isSwapOrder && ( + <ErrorBoundary fallback={<></>}> + <SwapOrderConfirmationView order={decodedData} settlementContract={safeTx?.data.to ?? ''} /> + </ErrorBoundary> + )} + <ErrorBoundary fallback={<div>Error parsing data</div>}> <ApprovalEditor safeTransaction={safeTx} /> </ErrorBoundary> diff --git a/src/features/swap/components/SwapOrderConfirmationView/index.tsx b/src/features/swap/components/SwapOrderConfirmationView/index.tsx index ed6dda4717..828790eeac 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/index.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/index.tsx @@ -1,6 +1,6 @@ import OrderId from '@/features/swap/components/OrderId' import { formatDateTime, formatTimeInWords } from '@/utils/date' -import type { ReactElement } from 'react' +import { Fragment, type ReactElement } from 'react' import { DataRow } from '@/components/common/Table/DataRow' import { DataTable } from '@/components/common/Table/DataTable' import { compareAsc } from 'date-fns' @@ -20,9 +20,7 @@ type SwapOrderProps = { settlementContract: string } -export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrderProps): ReactElement | null => { - if (!order) return null - +export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrderProps): ReactElement => { const { uid, owner, kind, validUntil, sellToken, buyToken, sellAmount, buyAmount, explorerUrl, receiver } = order const limitPrice = getLimitPrice(order) @@ -36,7 +34,7 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd return ( <div className={css.tableWrapper}> <DataTable - header="Order Details" + header="Order details" rows={[ <div key="amount"> <SwapTokens @@ -78,7 +76,7 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd {slippage}% </DataRow> ) : ( - <></> + <Fragment key="none" /> ), <DataRow key="Order ID" title="Order ID"> <OrderId orderId={uid} href={explorerUrl} /> diff --git a/src/hooks/useDecodeTx.ts b/src/hooks/useDecodeTx.ts index 7054a8ae24..d59afb8131 100644 --- a/src/hooks/useDecodeTx.ts +++ b/src/hooks/useDecodeTx.ts @@ -23,13 +23,17 @@ const useDecodeTx = ( const [data, error, loading] = useAsync< DecodedDataResponse | BaselineConfirmationView | CowSwapConfirmationView | undefined - >(() => { - if (!encodedData || isEmptyData) { - const nativeTransfer = isEmptyData && !isRejection ? getNativeTransferData(tx?.data) : undefined - return Promise.resolve(nativeTransfer) - } - return getConfirmationView(chainId, safeAddress, encodedData, tx.data.to) - }, [chainId, encodedData, isEmptyData, tx?.data, isRejection, safeAddress]) + >( + () => { + if (!encodedData || isEmptyData) { + const nativeTransfer = isEmptyData && !isRejection ? getNativeTransferData(tx?.data) : undefined + return Promise.resolve(nativeTransfer) + } + return getConfirmationView(chainId, safeAddress, encodedData, tx.data.to) + }, + [chainId, encodedData, isEmptyData, tx?.data, isRejection, safeAddress], + false, + ) return [data, error, loading] } From f1bd9a24a2a9349f8a76b5ad35bca1386bd970f7 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Tue, 25 Jun 2024 08:56:07 +0200 Subject: [PATCH 102/154] Refactor: lazy-load governance widget (#3864) --- .../ActivityRewardsSection/index.tsx | 2 +- .../GovernanceSection/GovernanceSection.tsx | 124 +++++++++--------- .../GovernanceSection/styles.module.css | 28 +--- src/components/dashboard/index.tsx | 2 +- src/hooks/useOnceVisible.ts | 10 +- 5 files changed, 74 insertions(+), 92 deletions(-) diff --git a/src/components/dashboard/ActivityRewardsSection/index.tsx b/src/components/dashboard/ActivityRewardsSection/index.tsx index 63dd020631..6d4d31ca5f 100644 --- a/src/components/dashboard/ActivityRewardsSection/index.tsx +++ b/src/components/dashboard/ActivityRewardsSection/index.tsx @@ -66,7 +66,7 @@ const ActivityRewardsSection = () => { display: { xs: 'none', sm: 'block' }, }} /> - <Grid container xs={12} sx={{ gap: { xs: 4, lg: 0 } }}> + <Grid container item xs={12} sx={{ gap: { xs: 4, lg: 0 } }}> <Grid item xs={12} lg={6} p={0}> <SvgIcon component={SafePass} diff --git a/src/components/dashboard/GovernanceSection/GovernanceSection.tsx b/src/components/dashboard/GovernanceSection/GovernanceSection.tsx index dca21de833..909e75c522 100644 --- a/src/components/dashboard/GovernanceSection/GovernanceSection.tsx +++ b/src/components/dashboard/GovernanceSection/GovernanceSection.tsx @@ -1,10 +1,6 @@ -import { useEffect, useRef, useState } from 'react' -import { Typography, Card, Box, Alert, IconButton, Link, SvgIcon } from '@mui/material' +import { useCallback, useEffect, useRef, useState } from 'react' +import { Typography, Card, Box, Link, SvgIcon } from '@mui/material' import { WidgetBody } from '@/components/dashboard/styled' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import Accordion from '@mui/material/Accordion' -import AccordionSummary from '@mui/material/AccordionSummary' -import AccordionDetails from '@mui/material/AccordionDetails' import css from './styles.module.css' import { useBrowserPermissions } from '@/hooks/safe-apps/permissions' import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' @@ -24,6 +20,7 @@ import useSafeInfo from '@/hooks/useSafeInfo' import { fetchSafeAppFromManifest } from '@/services/safe-apps/manifest' import useAsync from '@/hooks/useAsync' import { getOrigin } from '@/components/safe-apps/utils' +import InfiniteScroll from '@/components/common/InfiniteScroll' // A fallback component when the Safe App fails to load const WidgetLoadErrorFallback = () => ( @@ -85,73 +82,76 @@ const GovernanceSection = () => { const { safeLoading } = useSafeInfo() return ( - <Accordion className={css.accordion} defaultExpanded> - <AccordionSummary - expandIcon={ - <IconButton size="small"> - <ExpandMoreIcon color="border" /> - </IconButton> - } - > - <div> - <Typography component="h2" variant="subtitle1" fontWeight={700}> - Governance - </Typography> - <Typography variant="body2" mb={2} color="text.secondary"> - Use your SAFE tokens to vote on important proposals or participate in forum discussions. - </Typography> - </div> - </AccordionSummary> - - <AccordionDetails sx={({ spacing }) => ({ padding: `0 ${spacing(3)}` })}> - {governanceApp || fetchingSafeGovernanceApp ? ( - <WidgetBody> - <Card className={css.widgetWrapper}> - {governanceApp && !safeLoading ? ( - <MiniAppFrame app={governanceApp} title="Safe Governance" /> - ) : ( - <Box - className={css.widgetWrapper} - display="flex" - alignItems="center" - justifyContent="center" - textAlign="center" - > - <Typography variant="h1" color="text.secondary"> - Loading section... - </Typography> - </Box> - )} - </Card> - </WidgetBody> - ) : ( - <Alert severity="warning" elevation={3}> - There was an error fetching the Governance section. Please reload the page. - </Alert> - )} - </AccordionDetails> - </Accordion> + <> + {governanceApp || fetchingSafeGovernanceApp ? ( + <WidgetBody> + <Card className={css.widgetWrapper}> + {governanceApp && !safeLoading ? ( + <MiniAppFrame app={governanceApp} title="Safe Governance" /> + ) : ( + <Box + className={css.widgetWrapper} + display="flex" + alignItems="center" + justifyContent="center" + textAlign="center" + > + <Typography variant="h1" color="text.secondary"> + Loading section... + </Typography> + </Box> + )} + </Card> + </WidgetBody> + ) : ( + <WidgetLoadErrorFallback /> + )} + </> ) } -// Prevent `GovernanceSection` hooks from needlessly being called -const GovernanceSectionWrapper = () => { - const chainId = useChainId() - const [isLayoutReady, setIsLayoutReady] = useState(false) +const LazyGovernanceSection = () => { + const [isVisible, setIsVisible] = useState(false) + const [hasScrolled, setHasScrolled] = useState(false) + + const onVisible = useCallback(() => { + setIsVisible(true) + }, []) useEffect(() => { - setIsLayoutReady(true) + const handleScroll = () => { + if (window.scrollY > 0) { + setHasScrolled(true) + window.removeEventListener('scroll', handleScroll) + } + } + window.addEventListener('scroll', handleScroll) + return () => window.removeEventListener('scroll', handleScroll) }, []) - if (!isLayoutReady) { - return null - } + return ( + <> + {hasScrolled && <InfiniteScroll onLoadMore={onVisible} />} + <Typography component="h2" variant="subtitle1" fontWeight={700}> + Governance + </Typography> + <Typography variant="body2" mb={2} color="text.secondary"> + Use your SAFE tokens to vote on important proposals or participate in forum discussions. + </Typography> + + <div className={css.lazyWrapper}>{isVisible && <GovernanceSection />}</div> + </> + ) +} + +// Prevent `GovernanceSection` hooks from needlessly being called +const GovernanceSectionWrapper = () => { + const chainId = useChainId() if (!getSafeTokenAddress(chainId)) { return null } - - return <GovernanceSection /> + return <LazyGovernanceSection /> } export default GovernanceSectionWrapper diff --git a/src/components/dashboard/GovernanceSection/styles.module.css b/src/components/dashboard/GovernanceSection/styles.module.css index 5ea0cd9502..71592fad43 100644 --- a/src/components/dashboard/GovernanceSection/styles.module.css +++ b/src/components/dashboard/GovernanceSection/styles.module.css @@ -1,31 +1,11 @@ -.accordion { - box-shadow: none; - border: none; - background: transparent; -} - -.accordion :global .MuiAccordionSummary-root, -.accordion :global .MuiAccordionDetails-root { - padding: 0; -} - -.accordion:hover :global .MuiAccordionSummary-root, -.accordion :global .MuiAccordionSummary-root:hover, -.accordion :global .Mui-expanded.MuiAccordionSummary-root { - background: inherit; -} - -.accordion :global .MuiAccordionSummary-root { - pointer-events: none; -} - -.accordion :global .MuiAccordionSummary-expandIconWrapper { - pointer-events: auto; +.lazyWrapper { + height: 300px; + overflow: hidden; } .widgetWrapper { - border: none; height: 300px; + border: none; } /* iframe sm breakpoint + paddings */ diff --git a/src/components/dashboard/index.tsx b/src/components/dashboard/index.tsx index c2fdee5817..6bf7442871 100644 --- a/src/components/dashboard/index.tsx +++ b/src/components/dashboard/index.tsx @@ -77,7 +77,7 @@ const Dashboard = (): ReactElement => { </Grid> )} - <Grid item xs={12}> + <Grid item xs={12} className={css.hideIfEmpty}> <GovernanceSection /> </Grid> </> diff --git a/src/hooks/useOnceVisible.ts b/src/hooks/useOnceVisible.ts index 3ccd72a247..e7dc3e527f 100644 --- a/src/hooks/useOnceVisible.ts +++ b/src/hooks/useOnceVisible.ts @@ -7,11 +7,13 @@ const useOnceVisible = (element: MutableRefObject<HTMLElement | null>): boolean // Create and memoize an instance of IntersectionObserver const observer = useMemo(() => { + if (typeof IntersectionObserver === 'undefined') return + return new IntersectionObserver((entries) => { const intersectingEntry = entries.find((entry) => entry.isIntersecting) if (intersectingEntry) { setOnceVisible(true) - observer.unobserve(intersectingEntry.target) + observer?.unobserve(intersectingEntry.target) } }) }, []) @@ -19,7 +21,7 @@ const useOnceVisible = (element: MutableRefObject<HTMLElement | null>): boolean // Disconnect the observer on unmount useEffect(() => { return () => { - observer.disconnect() + observer?.disconnect() } }, [observer]) @@ -28,12 +30,12 @@ const useOnceVisible = (element: MutableRefObject<HTMLElement | null>): boolean const target = element.current if (target) { - observer.observe(target) + observer?.observe(target) } return () => { if (target) { - observer.unobserve(target) + observer?.unobserve(target) } } }, [observer, element]) From a39c4e2cd739f5f2e4528c4b856ad933a51e5721 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Tue, 25 Jun 2024 11:42:57 +0200 Subject: [PATCH 103/154] Fix: update meta description (#3866) * Fix: update meta description * Update index.tsx --- src/components/common/MetaTags/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/common/MetaTags/index.tsx b/src/components/common/MetaTags/index.tsx index b5ca7edbc5..fe590265a5 100644 --- a/src/components/common/MetaTags/index.tsx +++ b/src/components/common/MetaTags/index.tsx @@ -3,8 +3,7 @@ import { ContentSecurityPolicy, StrictTransportSecurity } from '@/config/securit import lightPalette from '@/components/theme/lightPalette' import darkPalette from '@/components/theme/darkPalette' -const descriptionText = - 'Safe (prev. Gnosis Safe) is the most trusted platform to manage digital assets on Ethereum and multiple EVMs. Over $40B secured.' +const descriptionText = 'Safe{Wallet} is the most trusted smart account wallet on Ethereum with over $100B secured.' const titleText = 'Safe{Wallet}' const MetaTags = ({ prefetchUrl }: { prefetchUrl: string }) => ( From 93bb38f44fe323774b27304c61cfbd8e871ef201 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Tue, 25 Jun 2024 16:29:39 +0200 Subject: [PATCH 104/154] feat: Improve safe creation status screen (#3778) * feat: Improve safe creation status screen * fix: Failing tests * fix: Extract useUndeployedSafe * fix: Add safeViewRedirectURL and remove old creation modal * fix: Show address in success modal * fix: Adjust undeployedSafeSlice to contain pay method * fix: Add safe to added safes and address book * chore: Remove usePendingSafe hook * fix: Show reverted error and adjust rejected error * test: Adjust create_safe_cf smoke test * fix: Adjust success screen wording * fix: Reset status fields on fail * fix: Add missing events, remove GET_STARTED event --------- Co-authored-by: James Mealy <james@safe.global> --- cypress/e2e/pages/create_wallet.pages.js | 10 - cypress/e2e/smoke/create_safe_cf.cy.js | 2 - public/images/common/tx-failed.svg | 8 + .../dashboard/CreationDialog/index.tsx | 88 ----- src/components/dashboard/index.tsx | 7 - src/components/new-safe/CardStepper/index.tsx | 4 +- .../new-safe/CardStepper/useCardStepper.ts | 3 + .../__tests__/useSyncSafeCreationStep.test.ts | 28 +- src/components/new-safe/create/index.tsx | 3 +- .../new-safe/create/logic/index.test.ts | 185 +--------- src/components/new-safe/create/logic/index.ts | 140 +------ .../create/steps/ReviewStep/index.tsx | 89 ++++- .../create/steps/StatusStep/StatusMessage.tsx | 106 +++--- .../create/steps/StatusStep/StatusStepper.tsx | 68 ---- .../steps/StatusStep/__tests__/index.test.tsx | 22 -- .../__tests__/usePendingSafe.test.ts | 71 ---- .../__tests__/useSafeCreation.test.ts | 348 ------------------ .../__tests__/useSafeCreationEffects.test.ts | 85 ----- .../create/steps/StatusStep/index.tsx | 204 +++++----- .../create/steps/StatusStep/usePendingSafe.ts | 29 -- .../steps/StatusStep/useSafeCreation.ts | 188 ---------- .../StatusStep/useSafeCreationEffects.ts | 90 ----- .../steps/StatusStep/useUndeployedSafe.ts | 19 + .../create/useSyncSafeCreationStep.ts | 7 +- .../counterfactual/ActivateAccountFlow.tsx | 4 +- .../counterfactual/CounterfactualHooks.tsx | 5 +- .../CounterfactualStatusButton.tsx | 14 +- .../CounterfactualSuccessScreen.tsx | 29 +- .../hooks/usePendingSafeStatuses.ts | 93 +++-- .../services/safeCreationEvents.ts | 4 + .../store/undeployedSafesSlice.ts | 17 +- src/features/counterfactual/utils.ts | 29 +- src/hooks/coreSDK/safeCoreSDK.ts | 3 +- .../analytics/events/createLoadSafe.ts | 4 - src/services/tx/__tests__/txMonitor.test.ts | 131 +------ src/services/tx/txMonitor.ts | 39 -- 36 files changed, 397 insertions(+), 1779 deletions(-) create mode 100644 public/images/common/tx-failed.svg delete mode 100644 src/components/dashboard/CreationDialog/index.tsx delete mode 100644 src/components/new-safe/create/steps/StatusStep/StatusStepper.tsx delete mode 100644 src/components/new-safe/create/steps/StatusStep/__tests__/index.test.tsx delete mode 100644 src/components/new-safe/create/steps/StatusStep/__tests__/usePendingSafe.test.ts delete mode 100644 src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts delete mode 100644 src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreationEffects.test.ts delete mode 100644 src/components/new-safe/create/steps/StatusStep/usePendingSafe.ts delete mode 100644 src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts delete mode 100644 src/components/new-safe/create/steps/StatusStep/useSafeCreationEffects.ts create mode 100644 src/components/new-safe/create/steps/StatusStep/useUndeployedSafe.ts diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index a6f8a50c2b..6dfe94971a 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -26,7 +26,6 @@ const networkFeeSection = '[data-tetid="network-fee-section"]' const nextBtn = '[data-testid="next-btn"]' const backBtn = '[data-testid="back-btn"]' const cancelBtn = '[data-testid="cancel-btn"]' -const dialogConfirmBtn = '[data-testid="dialog-confirm-btn"]' const safeActivationSection = '[data-testid="activation-section"]' const addressAutocompleteOptions = '[data-testid="address-item"]' export const qrCode = '[data-testid="qr-code"]' @@ -90,19 +89,10 @@ export function clickOnTxType(tx) { cy.get(choiceBtn).contains(tx).click() } -export function verifyNewSafeDialogModal() { - main.verifyElementsIsVisible([dialogConfirmBtn]) -} - export function verifyCFSafeCreated() { main.verifyElementsIsVisible([sidebar.pendingActivationIcon, safeActivationSection]) } -export function clickOnGotitBtn() { - cy.get(dialogConfirmBtn).click() - main.verifyElementsCount(connectedWalletExecMethod, 0) -} - export function selectPayLaterOption() { cy.get(connectedWalletExecMethod).click() } diff --git a/cypress/e2e/smoke/create_safe_cf.cy.js b/cypress/e2e/smoke/create_safe_cf.cy.js index 4d4d775159..a882074c23 100644 --- a/cypress/e2e/smoke/create_safe_cf.cy.js +++ b/cypress/e2e/smoke/create_safe_cf.cy.js @@ -17,8 +17,6 @@ describe('[SMOKE] CF Safe creation tests', () => { createwallet.clickOnNextBtn() createwallet.selectPayLaterOption() createwallet.clickOnReviewStepNextBtn() - createwallet.verifyNewSafeDialogModal() - createwallet.clickOnGotitBtn() createwallet.verifyCFSafeCreated() }) }) diff --git a/public/images/common/tx-failed.svg b/public/images/common/tx-failed.svg new file mode 100644 index 0000000000..c78cf169da --- /dev/null +++ b/public/images/common/tx-failed.svg @@ -0,0 +1,8 @@ +<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M67.5 123C90.9726 123 110 103.972 110 80.5C110 57.028 90.9726 38 67.5 38C44.0279 38 25 57.028 25 80.5C25 103.972 44.0279 123 67.5 123Z" fill="#FFF4F6"/> +<path d="M55 90C71.5686 90 85 76.5686 85 60C85 43.4314 71.5686 30 55 30C38.4314 30 25 43.4314 25 60C25 76.5686 38.4314 90 55 90Z" fill="#FFB4BD"/> +<path d="M52.1882 86C51.8577 86 51.5916 85.7335 51.5916 85.4026C51.5916 85.0717 51.8577 84.8052 52.1882 84.796C53.6934 84.7868 55.1987 84.6581 56.6765 84.4191C56.9977 84.364 57.3098 84.5846 57.3648 84.9155C57.4199 85.2371 57.1996 85.5496 56.8692 85.6048C55.3364 85.8529 53.7669 85.9908 52.1973 86H52.1882ZM48.5168 85.7978C48.5168 85.7978 48.4709 85.7978 48.4433 85.7978C46.8922 85.614 45.341 85.3107 43.8449 84.8879C43.5236 84.796 43.3401 84.4651 43.4318 84.1526C43.5236 83.8309 43.8541 83.6471 44.1661 83.739C45.6072 84.1434 47.0941 84.4375 48.581 84.6122C48.9114 84.6489 49.1409 84.9522 49.1042 85.2739C49.0675 85.5772 48.8105 85.807 48.5076 85.807L48.5168 85.7978ZM60.3479 84.796C60.0909 84.796 59.8522 84.6305 59.7788 84.364C59.687 84.0423 59.8614 83.7114 60.1826 83.6195C61.6145 83.1967 63.028 82.6453 64.3864 82.0019C64.6801 81.8549 65.0381 81.9835 65.1849 82.2868C65.3318 82.5809 65.2033 82.9394 64.9004 83.0864C63.4961 83.7574 62.0184 84.3272 60.5223 84.7684C60.4672 84.7868 60.4121 84.796 60.3479 84.796ZM40.5406 83.6839C40.4672 83.6839 40.3846 83.6655 40.3111 83.6379C38.8793 83.0313 37.475 82.3052 36.1533 81.478C35.8687 81.3034 35.7861 80.9357 35.9605 80.6508C36.1349 80.3659 36.5021 80.2832 36.7866 80.4578C38.0624 81.2574 39.4025 81.956 40.7793 82.535C41.0821 82.6637 41.229 83.0129 41.1005 83.3162C40.9995 83.546 40.7793 83.6839 40.5498 83.6839H40.5406ZM67.8467 81.3585C67.654 81.3585 67.4612 81.2574 67.3419 81.0828C67.1675 80.8071 67.2409 80.4302 67.5255 80.2556C68.7829 79.4468 69.9945 78.5277 71.1143 77.5351C71.3621 77.3145 71.7384 77.3329 71.9587 77.581C72.179 77.8292 72.1514 78.206 71.9128 78.4266C70.7471 79.4652 69.4897 80.4211 68.1771 81.2574C68.0762 81.3218 67.966 81.3493 67.8559 81.3493L67.8467 81.3585ZM33.464 79.4284C33.3355 79.4284 33.1978 79.3825 33.0877 79.2905C31.8761 78.3071 30.738 77.2134 29.6916 76.0553C29.4713 75.8071 29.4897 75.4303 29.7375 75.2097C29.9853 74.9891 30.3616 75.0075 30.5819 75.2557C31.5824 76.3678 32.6838 77.4156 33.8403 78.3623C34.0973 78.5737 34.134 78.9505 33.9229 79.2078C33.8036 79.3549 33.6292 79.4284 33.4548 79.4284H33.464ZM74.0973 75.9726C73.9596 75.9726 73.8128 75.9266 73.7026 75.8255C73.4548 75.6049 73.4273 75.2281 73.6475 74.98C74.6388 73.8495 75.5475 72.6362 76.346 71.3679C76.5204 71.0921 76.8967 71.0094 77.1721 71.1841C77.4474 71.3587 77.5301 71.7355 77.3557 72.0112C76.5204 73.3256 75.575 74.5939 74.547 75.7704C74.4277 75.9082 74.2625 75.9726 74.0973 75.9726ZM27.8559 73.3807C27.6631 73.3807 27.4796 73.2888 27.3694 73.1234C26.4791 71.8366 25.6806 70.4763 25.0014 69.0793C24.8545 68.7852 24.983 68.4267 25.2767 68.2797C25.5704 68.1326 25.9284 68.2613 26.0753 68.5554C26.7269 69.9065 27.4979 71.2116 28.3515 72.4432C28.5443 72.719 28.4709 73.0866 28.2047 73.2796C28.1037 73.3531 27.9844 73.3899 27.8651 73.3899L27.8559 73.3807ZM78.6223 69.0609C78.5397 69.0609 78.4479 69.0425 78.3653 69.0058C78.0624 68.8679 77.9339 68.5095 78.0808 68.2062C78.7141 66.8459 79.2464 65.4305 79.6687 63.9875C79.7604 63.6658 80.1 63.4912 80.4121 63.5739C80.7334 63.6658 80.9169 63.9967 80.8251 64.3184C80.3938 65.8165 79.8339 67.2962 79.173 68.7117C79.072 68.9323 78.8518 69.0609 78.6315 69.0609H78.6223ZM24.1386 66.0095C23.8908 66.0095 23.6521 65.8533 23.5695 65.6051C23.0739 64.1253 22.6884 62.5996 22.4222 61.0555C22.3671 60.7338 22.5874 60.4214 22.9087 60.3662C23.2299 60.3111 23.542 60.5316 23.5971 60.8533C23.8449 62.3331 24.2212 63.8037 24.6985 65.2191C24.8086 65.5316 24.6342 65.8716 24.3222 65.9819C24.2579 66.0003 24.1937 66.0095 24.1294 66.0095H24.1386ZM81.0363 61.1658C81.0363 61.1658 80.972 61.1658 80.9445 61.1658C80.614 61.1107 80.3938 60.8074 80.4488 60.4857C80.6783 59.0059 80.7976 57.4986 80.7976 55.9913C80.7976 55.1365 80.7609 54.2726 80.6875 53.427C80.6599 53.0961 80.8986 52.8112 81.229 52.7744C81.5594 52.7468 81.844 52.9858 81.8807 53.3167C81.9541 54.199 82 55.0997 82 55.9913C82 57.5537 81.8807 59.1346 81.642 60.6695C81.5961 60.9636 81.3391 61.175 81.0546 61.175L81.0363 61.1658ZM22.6241 57.8938C22.3029 57.8938 22.0367 57.6457 22.0275 57.324C22.0092 56.8828 22 56.4324 22 55.9913C22 54.87 22.0643 53.7487 22.1836 52.6365C22.2203 52.3057 22.514 52.0667 22.8444 52.1035C23.1749 52.1402 23.4135 52.4343 23.3768 52.7652C23.2575 53.8314 23.2024 54.9067 23.2024 55.9821C23.2024 56.4141 23.2116 56.8368 23.2299 57.2688C23.2483 57.5997 22.9913 57.8754 22.6609 57.8938C22.6609 57.8938 22.6425 57.8938 22.6333 57.8938H22.6241ZM80.7242 50.3204C80.4488 50.3204 80.201 50.1274 80.1368 49.8517C79.8155 48.3903 79.3841 46.9381 78.8426 45.5319C78.7233 45.2194 78.8793 44.8793 79.1822 44.7598C79.4851 44.6403 79.8339 44.7966 79.9532 45.0999C80.5223 46.5521 80.972 48.0686 81.3024 49.5943C81.3759 49.916 81.1647 50.2377 80.8435 50.302C80.7976 50.3112 80.7609 50.3112 80.715 50.3112L80.7242 50.3204ZM23.4135 49.6678C23.3676 49.6678 23.3217 49.6678 23.2758 49.6495C22.9546 49.5759 22.7526 49.2451 22.8352 48.9234C23.2024 47.3977 23.6888 45.8995 24.2855 44.4565C24.414 44.1532 24.7627 44.0062 25.0656 44.1348C25.3685 44.2635 25.5154 44.6128 25.3869 44.9161C24.8086 46.3039 24.3405 47.7469 23.9917 49.1991C23.9275 49.4748 23.6797 49.6587 23.4135 49.6587V49.6678ZM77.8605 42.5816C77.6494 42.5816 77.4383 42.4713 77.3373 42.2691C76.6214 40.9547 75.8045 39.6772 74.8958 38.4915C74.6939 38.225 74.749 37.8574 75.006 37.6552C75.2721 37.453 75.6393 37.5081 75.8412 37.7655C76.7866 39.0062 77.6494 40.3298 78.3928 41.7084C78.5489 42.0025 78.4387 42.361 78.1542 42.5172C78.0624 42.5632 77.9614 42.5907 77.8697 42.5907L77.8605 42.5816ZM26.4608 41.9933C26.3598 41.9933 26.2588 41.9658 26.167 41.9106C25.8825 41.7452 25.7816 41.3775 25.9468 41.0926C26.7178 39.7415 27.6081 38.4364 28.581 37.214C28.7921 36.9566 29.1684 36.9107 29.4254 37.1221C29.6824 37.3335 29.7283 37.7103 29.5172 37.9677C28.581 39.1441 27.7274 40.3941 26.9839 41.69C26.8738 41.883 26.6719 41.9933 26.4608 41.9933ZM72.95 35.9456C72.7939 35.9456 72.6379 35.8813 72.5278 35.771C71.4722 34.7048 70.3341 33.703 69.1317 32.8115C68.8655 32.6185 68.8105 32.2416 69.0032 31.9751C69.196 31.7086 69.5723 31.6534 69.8385 31.8464C71.0867 32.7747 72.2799 33.8133 73.3722 34.9254C73.6017 35.1644 73.6017 35.5412 73.3722 35.771C73.2529 35.8813 73.106 35.9456 72.95 35.9456ZM31.5181 35.4677C31.3621 35.4677 31.2061 35.4034 31.0867 35.2839C30.8573 35.0449 30.8665 34.6681 31.0959 34.4383C32.2157 33.3538 33.4273 32.3427 34.7031 31.442C34.9693 31.249 35.3456 31.3133 35.5383 31.5891C35.7311 31.8556 35.6668 32.2324 35.3915 32.4255C34.1707 33.2894 33.005 34.2637 31.9312 35.3023C31.8118 35.4125 31.665 35.4677 31.5181 35.4677ZM66.3965 30.9273C66.2955 30.9273 66.1946 30.8997 66.1028 30.8538C64.7994 30.1185 63.4227 29.4751 62.0184 28.9604C61.7063 28.841 61.5503 28.5009 61.6696 28.1884C61.7797 27.8759 62.1285 27.7197 62.4406 27.8391C63.8999 28.3814 65.3318 29.0524 66.6902 29.8152C66.9748 29.9806 67.0757 30.3391 66.9197 30.6332C66.8095 30.8262 66.6076 30.9365 66.3965 30.9365V30.9273ZM38.1817 30.6056C37.9706 30.6056 37.7595 30.4953 37.6494 30.284C37.4933 29.9898 37.6035 29.6314 37.8972 29.4751C39.274 28.7399 40.7242 28.1057 42.1927 27.6002C42.5048 27.4899 42.8444 27.6553 42.9546 27.9678C43.0647 28.2803 42.8995 28.6204 42.5874 28.7307C41.1739 29.2178 39.788 29.8244 38.4663 30.5321C38.3745 30.5781 38.2827 30.6056 38.1817 30.6056ZM58.7049 27.931C58.7049 27.931 58.6131 27.931 58.5672 27.9127C57.1078 27.5726 55.6117 27.3428 54.1156 27.2325C53.7852 27.205 53.5374 26.92 53.5649 26.5892C53.5925 26.2583 53.8678 26.0101 54.2074 26.0377C55.7678 26.148 57.3281 26.387 58.8426 26.7454C59.1638 26.8189 59.3658 27.1406 59.2923 27.4623C59.2281 27.738 58.9803 27.9219 58.7141 27.9219L58.7049 27.931ZM45.9284 27.784C45.6531 27.784 45.4052 27.591 45.341 27.3061C45.2767 26.9844 45.4787 26.6627 45.7999 26.5983C47.3235 26.2767 48.8839 26.0745 50.4442 26.0009C50.7838 25.9825 51.0592 26.2399 51.0684 26.5708C51.0867 26.9016 50.8297 27.1866 50.4993 27.1958C49.0032 27.2693 47.4979 27.4623 46.0385 27.7748C45.9927 27.7748 45.9559 27.784 45.9192 27.784H45.9284Z" fill="#121312"/> +<path d="M56.0085 55.5757L55.5842 56L56.0085 56.4243L62.6577 63.0734C63.6474 64.0632 63.6474 65.6679 62.6577 66.6577C61.6679 67.6474 60.0632 67.6474 59.0734 66.6577L52.4243 60.0085L52 59.5842L51.5757 60.0085L44.9266 66.6577C43.9368 67.6474 42.3321 67.6474 41.3423 66.6577C40.3526 65.6679 40.3526 64.0632 41.3423 63.0734L47.9915 56.4243L48.4158 56L47.9915 55.5757L41.3423 48.9266C40.3526 47.9368 40.3526 46.3321 41.3423 45.3423C42.3321 44.3526 43.9368 44.3526 44.9266 45.3423L51.5757 51.9915L52 52.4158L52.4243 51.9915L59.0734 45.3423C60.0632 44.3526 61.6679 44.3526 62.6577 45.3423C63.6474 46.3321 63.6474 47.9368 62.6577 48.9266L56.0085 55.5757Z" stroke="#121312" stroke-width="1.2"/> +<path d="M144.472 102.172L144.356 102.299L125.471 121.184C124.69 121.965 123.423 121.965 122.642 121.184C121.902 120.444 121.863 119.269 122.525 118.483L122.642 118.356L138.114 102.884L76.0007 102.885C74.8961 102.885 74.0007 101.989 74.0007 100.885C74.0007 99.8304 74.8166 98.9666 75.8514 98.8903L76.0007 98.8848L138.114 98.884L122.642 83.4142C121.902 82.6743 121.863 81.4988 122.525 80.713L122.642 80.5858C123.382 79.8458 124.558 79.8069 125.343 80.469L125.471 80.5858L144.356 99.4708C145.096 100.211 145.135 101.386 144.472 102.172Z" stroke="#121312" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="3 3"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M94.9407 113.885C94.9407 114.407 94.5408 114.836 94.0316 114.881L93.9224 114.885L29.4126 114.886L46.5922 132.063C46.9575 132.428 46.9811 133.007 46.6626 133.399L46.5795 133.489C46.2136 133.843 45.6435 133.863 45.256 133.548L45.1629 133.462L26.2929 114.592C25.9276 114.227 25.9039 113.648 26.2224 113.256L26.3079 113.163L45.1779 94.2929C45.5684 93.9024 46.2016 93.9024 46.5921 94.2929C46.9575 94.6582 46.9812 95.237 46.6626 95.629L46.5771 95.7222L29.4127 112.886L93.9407 112.885C94.493 112.885 94.9407 113.333 94.9407 113.885Z" fill="#121312"/> +</svg> diff --git a/src/components/dashboard/CreationDialog/index.tsx b/src/components/dashboard/CreationDialog/index.tsx deleted file mode 100644 index 9a2ef19e11..0000000000 --- a/src/components/dashboard/CreationDialog/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { type ElementType } from 'react' -import { Box, Button, Dialog, DialogContent, Grid, SvgIcon, Typography } from '@mui/material' -import { useRouter } from 'next/router' - -import HomeIcon from '@/public/images/sidebar/home.svg' -import TransactionIcon from '@/public/images/sidebar/transactions.svg' -import AppsIcon from '@/public/images/sidebar/apps.svg' -import SettingsIcon from '@/public/images/sidebar/settings.svg' -import BeamerIcon from '@/public/images/sidebar/whats-new.svg' -import HelpCenterIcon from '@/public/images/sidebar/help-center.svg' -import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' -import { useCurrentChain } from '@/hooks/useChains' -import { CREATION_MODAL_QUERY_PARM } from '@/components/new-safe/create/logic' - -const HintItem = ({ Icon, title, description }: { Icon: ElementType; title: string; description: string }) => { - return ( - <Grid item md={6}> - <Box display="flex" alignItems="center" gap={1} mb={1}> - <SvgIcon component={Icon} inheritViewBox fontSize="small" /> - <Typography variant="subtitle2" fontWeight="700"> - {title} - </Typography> - </Box> - - <Typography variant="body2">{description}</Typography> - </Grid> - ) -} - -const CreationDialog = () => { - const router = useRouter() - const [open, setOpen] = React.useState(true) - const [remoteSafeApps = []] = useRemoteSafeApps() - const chain = useCurrentChain() - - const onClose = () => { - const { [CREATION_MODAL_QUERY_PARM]: _, ...query } = router.query - router.replace({ pathname: router.pathname, query }) - - setOpen(false) - } - - return ( - <Dialog open={open}> - <DialogContent sx={{ paddingX: 8, paddingTop: 9, paddingBottom: 6 }}> - <Typography variant="h3" fontWeight="700" mb={1}> - Welcome to {'Safe{Wallet}'}! - </Typography> - <Typography variant="body2"> - Congratulations on your first step to truly unlock ownership. Enjoy the experience and discover our app. - </Typography> - - <Grid container mt={2} mb={4} spacing={3}> - <HintItem Icon={HomeIcon} title="Home" description="Get a status overview of your Safe Account here." /> - <HintItem - Icon={TransactionIcon} - title="Transactions" - description="Review, approve, execute and keep track of asset movement." - /> - <HintItem - Icon={AppsIcon} - title="Apps" - description={`Over ${remoteSafeApps.length} dApps available for you on ${chain?.chainName}.`} - /> - <HintItem - Icon={SettingsIcon} - title="Settings" - description="Want to change your Safe Account setup? Settings is the right place to go." - /> - <HintItem Icon={BeamerIcon} title="What's new" description="Don't miss any future Safe updates." /> - <HintItem - Icon={HelpCenterIcon} - title="Help center" - description="Have any questions? Check out our collection of articles." - /> - </Grid> - - <Box display="flex" justifyContent="center"> - <Button data-testid="dialog-confirm-btn" onClick={onClose} variant="contained" size="stretched"> - Got it - </Button> - </Box> - </DialogContent> - </Dialog> - ) -} - -export default CreationDialog diff --git a/src/components/dashboard/index.tsx b/src/components/dashboard/index.tsx index 6bf7442871..fa480c931f 100644 --- a/src/components/dashboard/index.tsx +++ b/src/components/dashboard/index.tsx @@ -9,9 +9,6 @@ import Overview from '@/components/dashboard/Overview/Overview' import { FeaturedApps } from '@/components/dashboard/FeaturedApps/FeaturedApps' import SafeAppsDashboardSection from '@/components/dashboard/SafeAppsDashboardSection/SafeAppsDashboardSection' import GovernanceSection from '@/components/dashboard/GovernanceSection/GovernanceSection' -import CreationDialog from '@/components/dashboard/CreationDialog' -import { useRouter } from 'next/router' -import { CREATION_MODAL_QUERY_PARM } from '../new-safe/create/logic' import useRecovery from '@/features/recovery/hooks/useRecovery' import { useIsRecoverySupported } from '@/features/recovery/hooks/useIsRecoverySupported' import ActivityRewardsSection from '@/components/dashboard/ActivityRewardsSection' @@ -23,9 +20,7 @@ import SwapWidget from '@/features/swap/components/SwapWidget' const RecoveryHeader = dynamic(() => import('@/features/recovery/components/RecoveryHeader')) const Dashboard = (): ReactElement => { - const router = useRouter() const { safe } = useSafeInfo() - const { [CREATION_MODAL_QUERY_PARM]: showCreationModal = '' } = router.query const showSafeApps = useHasFeature(FEATURES.SAFE_APPS) const isSAPBannerEnabled = useHasFeature(FEATURES.SAP_BANNER) const supportsRecovery = useIsRecoverySupported() @@ -83,8 +78,6 @@ const Dashboard = (): ReactElement => { </> )} </Grid> - - {showCreationModal ? <CreationDialog /> : null} </> ) } diff --git a/src/components/new-safe/CardStepper/index.tsx b/src/components/new-safe/CardStepper/index.tsx index 8a7f0768ab..c58a781c00 100644 --- a/src/components/new-safe/CardStepper/index.tsx +++ b/src/components/new-safe/CardStepper/index.tsx @@ -8,7 +8,7 @@ import { useCardStepper } from './useCardStepper' export function CardStepper<StepperData>(props: TxStepperProps<StepperData>) { const [progressColor, setProgressColor] = useState(lightPalette.secondary.main) - const { activeStep, onSubmit, onBack, stepData, setStep } = useCardStepper<StepperData>(props) + const { activeStep, onSubmit, onBack, stepData, setStep, setStepData } = useCardStepper<StepperData>(props) const { steps } = props const currentStep = steps[activeStep] const progress = ((activeStep + 1) / steps.length) * 100 @@ -33,7 +33,7 @@ export function CardStepper<StepperData>(props: TxStepperProps<StepperData>) { /> )} <CardContent className={css.content}> - {currentStep.render(stepData, onSubmit, onBack, setStep, setProgressColor)} + {currentStep.render(stepData, onSubmit, onBack, setStep, setProgressColor, setStepData)} </CardContent> </Card> ) diff --git a/src/components/new-safe/CardStepper/useCardStepper.ts b/src/components/new-safe/CardStepper/useCardStepper.ts index c8abd82092..4598325ada 100644 --- a/src/components/new-safe/CardStepper/useCardStepper.ts +++ b/src/components/new-safe/CardStepper/useCardStepper.ts @@ -8,6 +8,7 @@ export type StepRenderProps<TData> = { onBack: (data?: Partial<TData>) => void setStep: (step: number) => void setProgressColor?: Dispatch<SetStateAction<string>> + setStepData?: Dispatch<SetStateAction<TData>> } type Step<TData> = { @@ -19,6 +20,7 @@ type Step<TData> = { onBack: StepRenderProps<TData>['onBack'], setStep: StepRenderProps<TData>['setStep'], setProgressColor: StepRenderProps<TData>['setProgressColor'], + setStepData: StepRenderProps<TData>['setStepData'], ) => ReactElement } @@ -84,5 +86,6 @@ export const useCardStepper = <TData>({ activeStep, stepData, firstStep, + setStepData, } } diff --git a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts index 7a5cd855dd..1c0d1cb8c7 100644 --- a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts +++ b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts @@ -1,24 +1,17 @@ +import { PayMethod } from '@/features/counterfactual/PayNowPayLater' +import { PendingSafeStatus } from '@/features/counterfactual/store/undeployedSafesSlice' import { renderHook } from '@/tests/test-utils' import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCreationStep' import * as wallet from '@/hooks/wallets/useWallet' import * as localStorage from '@/services/local-storage/useLocalStorage' import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' -import * as usePendingSafe from '../steps/StatusStep/usePendingSafe' +import * as useChainId from '@/hooks/useChainId' import * as useIsWrongChain from '@/hooks/useIsWrongChain' import * as useRouter from 'next/router' import { type NextRouter } from 'next/router' import { AppRoutes } from '@/config/routes' describe('useSyncSafeCreationStep', () => { - const mockPendingSafe = { - name: 'joyful-rinkeby-safe', - threshold: 1, - owners: [], - saltNonce: 123, - address: '0x10', - } - const setPendingSafeSpy = jest.fn() - beforeEach(() => { jest.clearAllMocks() }) @@ -26,7 +19,6 @@ describe('useSyncSafeCreationStep', () => { it('should go to the first step if no wallet is connected and there is no pending safe', async () => { const mockPushRoute = jest.fn() jest.spyOn(wallet, 'default').mockReturnValue(null) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([undefined, setPendingSafeSpy]) jest.spyOn(useRouter, 'useRouter').mockReturnValue({ push: mockPushRoute, } as unknown as NextRouter) @@ -42,14 +34,22 @@ describe('useSyncSafeCreationStep', () => { const mockPushRoute = jest.fn() jest.spyOn(localStorage, 'default').mockReturnValue([{}, jest.fn()]) jest.spyOn(wallet, 'default').mockReturnValue({ address: '0x1' } as ConnectedWallet) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, setPendingSafeSpy]) + jest.spyOn(useChainId, 'default').mockReturnValue('11155111') jest.spyOn(useRouter, 'useRouter').mockReturnValue({ push: mockPushRoute, } as unknown as NextRouter) const mockSetStep = jest.fn() - renderHook(() => useSyncSafeCreationStep(mockSetStep)) + renderHook(() => useSyncSafeCreationStep(mockSetStep), { + initialReduxState: { + undeployedSafes: { + '11155111': { + '0x123': { status: { status: PendingSafeStatus.PROCESSING, type: PayMethod.PayNow }, props: {} as any }, + }, + }, + }, + }) expect(mockSetStep).toHaveBeenCalledWith(3) @@ -59,7 +59,6 @@ describe('useSyncSafeCreationStep', () => { it('should go to the second step if the wrong chain is connected', async () => { jest.spyOn(localStorage, 'default').mockReturnValue([{}, jest.fn()]) jest.spyOn(wallet, 'default').mockReturnValue({ address: '0x1' } as ConnectedWallet) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([undefined, setPendingSafeSpy]) jest.spyOn(useIsWrongChain, 'default').mockReturnValue(true) const mockSetStep = jest.fn() @@ -72,7 +71,6 @@ describe('useSyncSafeCreationStep', () => { it('should not do anything if wallet is connected and there is no pending safe', async () => { jest.spyOn(localStorage, 'default').mockReturnValue([undefined, jest.fn()]) jest.spyOn(wallet, 'default').mockReturnValue({ address: '0x1' } as ConnectedWallet) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([undefined, setPendingSafeSpy]) jest.spyOn(useIsWrongChain, 'default').mockReturnValue(false) const mockSetStep = jest.fn() diff --git a/src/components/new-safe/create/index.tsx b/src/components/new-safe/create/index.tsx index 9a14bad22d..8f738c082a 100644 --- a/src/components/new-safe/create/index.tsx +++ b/src/components/new-safe/create/index.tsx @@ -135,13 +135,14 @@ const CreateSafe = () => { { title: '', subtitle: '', - render: (data, onSubmit, onBack, setStep, setProgressColor) => ( + render: (data, onSubmit, onBack, setStep, setProgressColor, setStepData) => ( <CreateSafeStatus data={data} onSubmit={onSubmit} onBack={onBack} setStep={setStep} setProgressColor={setProgressColor} + setStepData={setStepData} /> ), }, diff --git a/src/components/new-safe/create/logic/index.test.ts b/src/components/new-safe/create/logic/index.test.ts index 155e952ffb..6457eca09c 100644 --- a/src/components/new-safe/create/logic/index.test.ts +++ b/src/components/new-safe/create/logic/index.test.ts @@ -1,15 +1,7 @@ -import { JsonRpcProvider, type TransactionResponse } from 'ethers' +import { JsonRpcProvider } from 'ethers' import { EMPTY_DATA, ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' import * as web3 from '@/hooks/wallets/web3' -import type { TransactionReceipt } from 'ethers' -import { - checkSafeCreationTx, - relaySafeCreation, - handleSafeCreationError, -} from '@/components/new-safe/create/logic/index' -import { type ErrorCode } from 'ethers' -import { EthersTxReplacedReason } from '@/utils/ethers-utils' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' +import { relaySafeCreation } from '@/components/new-safe/create/logic/index' import { relayTransaction, type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import { toBeHex } from 'ethers' import { @@ -57,179 +49,6 @@ jest.mock('@safe-global/protocol-kit', () => { } }) -describe('checkSafeCreationTx', () => { - let waitForTxSpy = jest.spyOn(provider, 'waitForTransaction') - - beforeEach(() => { - jest.resetAllMocks() - - jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => provider) - - waitForTxSpy = jest.spyOn(provider, 'waitForTransaction') - jest.spyOn(provider, 'getBlockNumber').mockReturnValue(Promise.resolve(4)) - jest.spyOn(provider, 'getTransaction').mockReturnValue(Promise.resolve(mockTransaction as TransactionResponse)) - }) - - it('returns SUCCESS if promise was resolved', async () => { - const receipt = { - status: 1, - } as TransactionReceipt - - waitForTxSpy.mockImplementationOnce(() => Promise.resolve(receipt)) - - const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) - - expect(result).toBe(SafeCreationStatus.SUCCESS) - }) - - it('returns REVERTED if transaction was reverted', async () => { - const receipt = { - status: 0, - } as TransactionReceipt - - waitForTxSpy.mockImplementationOnce(() => Promise.resolve(receipt)) - - const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) - - expect(result).toBe(SafeCreationStatus.REVERTED) - }) - - it('returns TIMEOUT if transaction could not be found within the timeout limit', async () => { - const mockEthersError = { - ...new Error(), - code: 'TIMEOUT' as ErrorCode, - } - - waitForTxSpy.mockImplementationOnce(() => Promise.reject(mockEthersError)) - - const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) - - expect(result).toBe(SafeCreationStatus.TIMEOUT) - }) - - it('returns SUCCESS if transaction was replaced', async () => { - const mockEthersError = { - ...new Error(), - code: 'TRANSACTION_REPLACED', - reason: 'repriced', - } - waitForTxSpy.mockImplementationOnce(() => Promise.reject(mockEthersError)) - - const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) - - expect(result).toBe(SafeCreationStatus.SUCCESS) - }) - - it('returns ERROR if transaction was cancelled', async () => { - const mockEthersError = { - ...new Error(), - code: 'TRANSACTION_REPLACED', - reason: 'cancelled', - } - waitForTxSpy.mockImplementationOnce(() => Promise.reject(mockEthersError)) - - const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) - - expect(result).toBe(SafeCreationStatus.ERROR) - }) -}) - -describe('handleSafeCreationError', () => { - it('returns WALLET_REJECTED if the tx was rejected in the wallet', () => { - const mockEthersError = { - ...new Error(), - code: 'ACTION_REJECTED' as ErrorCode, - reason: '' as EthersTxReplacedReason, - receipt: {} as TransactionReceipt, - } - - const result = handleSafeCreationError(mockEthersError) - - expect(result).toEqual(SafeCreationStatus.WALLET_REJECTED) - }) - - it('returns WALLET_REJECTED if the tx was rejected via WC', () => { - const mockEthersError = { - ...new Error(), - code: 'UNKNOWN_ERROR' as ErrorCode, - reason: '' as EthersTxReplacedReason, - receipt: {} as TransactionReceipt, - message: 'rejected', - } - - const result = handleSafeCreationError(mockEthersError) - - expect(result).toEqual(SafeCreationStatus.WALLET_REJECTED) - }) - - it('returns ERROR if the tx was cancelled', () => { - const mockEthersError = { - ...new Error(), - code: 'TRANSACTION_REPLACED' as ErrorCode, - reason: EthersTxReplacedReason.cancelled, - receipt: {} as TransactionReceipt, - } - - const result = handleSafeCreationError(mockEthersError) - - expect(result).toEqual(SafeCreationStatus.ERROR) - }) - - it('returns SUCCESS if the tx was replaced', () => { - const mockEthersError = { - ...new Error(), - code: 'TRANSACTION_REPLACED' as ErrorCode, - reason: EthersTxReplacedReason.replaced, - receipt: {} as TransactionReceipt, - } - - const result = handleSafeCreationError(mockEthersError) - - expect(result).toEqual(SafeCreationStatus.SUCCESS) - }) - - it('returns SUCCESS if the tx was repriced', () => { - const mockEthersError = { - ...new Error(), - code: 'TRANSACTION_REPLACED' as ErrorCode, - reason: EthersTxReplacedReason.repriced, - receipt: {} as TransactionReceipt, - } - - const result = handleSafeCreationError(mockEthersError) - - expect(result).toEqual(SafeCreationStatus.SUCCESS) - }) - - it('returns ERROR if the tx was not rejected, cancelled or replaced', () => { - const mockEthersError = { - ...new Error(), - code: 'UNKNOWN_ERROR' as ErrorCode, - reason: '' as EthersTxReplacedReason, - receipt: {} as TransactionReceipt, - } - - const result = handleSafeCreationError(mockEthersError) - - expect(result).toEqual(SafeCreationStatus.ERROR) - }) - - it('returns REVERTED if the tx failed', () => { - const mockEthersError = { - ...new Error(), - code: 'UNKNOWN_ERROR' as ErrorCode, - reason: '' as EthersTxReplacedReason, - receipt: { - status: 0, - } as TransactionReceipt, - } - - const result = handleSafeCreationError(mockEthersError) - - expect(result).toEqual(SafeCreationStatus.REVERTED) - }) -}) - describe('createNewSafeViaRelayer', () => { const owner1 = toBeHex('0x1', 20) const owner2 = toBeHex('0x2', 20) diff --git a/src/components/new-safe/create/logic/index.ts b/src/components/new-safe/create/logic/index.ts index a0c504da03..ff50492ebd 100644 --- a/src/components/new-safe/create/logic/index.ts +++ b/src/components/new-safe/create/logic/index.ts @@ -7,18 +7,9 @@ import { getReadOnlyGnosisSafeContract, getReadOnlyProxyFactoryContract, } from '@/services/contracts/safeContracts' -import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import { didRevert, type EthersError } from '@/utils/ethers-utils' -import { Errors, trackError } from '@/services/exceptions' -import { isWalletRejection } from '@/utils/wallets' -import type { PendingSafeTx } from '@/components/new-safe/create/types' -import type { NewSafeFormData } from '@/components/new-safe/create' import type { UrlObject } from 'url' import { AppRoutes } from '@/config/routes' import { SAFE_APPS_EVENTS, trackEvent } from '@/services/analytics' -import type { AppDispatch, AppThunk } from '@/store' -import { showNotification } from '@/store/notificationsSlice' import { predictSafeAddress, SafeFactory } from '@safe-global/protocol-kit' import type Safe from '@safe-global/protocol-kit' import type { DeploySafeProps } from '@safe-global/protocol-kit' @@ -27,7 +18,6 @@ import { createEthersAdapter, isValidSafeVersion } from '@/hooks/coreSDK/safeCor import { backOff } from 'exponential-backoff' import { LATEST_SAFE_VERSION } from '@/config/constants' import { EMPTY_DATA, ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' -import { formatError } from '@/utils/formatters' export type SafeCreationProps = { owners: string[] @@ -35,27 +25,6 @@ export type SafeCreationProps = { saltNonce: number } -/** - * Prepare data for creating a Safe for the Core SDK - */ -export const getSafeDeployProps = async ( - safeParams: SafeCreationProps, - callback: (txHash: string) => void, - chainId: string, -): Promise<DeploySafeProps & { callback: DeploySafeProps['callback'] }> => { - const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(chainId, LATEST_SAFE_VERSION) - - return { - safeAccountConfig: { - threshold: safeParams.threshold, - owners: safeParams.owners, - fallbackHandler: await readOnlyFallbackHandlerContract.getAddress(), - }, - saltNonce: safeParams.saltNonce.toString(), - callback, - } -} - const getSafeFactory = async ( ethersProvider: BrowserProvider, safeVersion = LATEST_SAFE_VERSION, @@ -133,36 +102,6 @@ export const encodeSafeCreationTx = async ({ ]) } -/** - * Encode a Safe creation tx in a way that we can store locally and monitor using _waitForTransaction - */ -export const getSafeCreationTxInfo = async ( - provider: Provider, - owners: NewSafeFormData['owners'], - threshold: NewSafeFormData['threshold'], - saltNonce: NewSafeFormData['saltNonce'], - chain: ChainInfo, - wallet: ConnectedWallet, -): Promise<PendingSafeTx> => { - const readOnlyProxyContract = await getReadOnlyProxyFactoryContract(chain.chainId, LATEST_SAFE_VERSION) - - const data = await encodeSafeCreationTx({ - owners: owners.map((owner) => owner.address), - threshold, - saltNonce, - chain, - }) - - return { - data, - from: wallet.address, - nonce: await provider.getTransactionCount(wallet.address), - to: await readOnlyProxyContract.getAddress(), - value: BigInt(0), - startBlock: await provider.getBlockNumber(), - } -} - export const estimateSafeCreationGas = async ( chain: ChainInfo, provider: Provider, @@ -194,83 +133,6 @@ export const pollSafeInfo = async (chainId: string, safeAddress: string): Promis }) } -export const handleSafeCreationError = (error: EthersError) => { - trackError(Errors._800, error.message) - - if (isWalletRejection(error)) { - return SafeCreationStatus.WALLET_REJECTED - } - - if (error.code === 'TRANSACTION_REPLACED') { - if (error.reason === 'cancelled') { - return SafeCreationStatus.ERROR - } else { - return SafeCreationStatus.SUCCESS - } - } - - if (error.receipt && didRevert(error.receipt)) { - return SafeCreationStatus.REVERTED - } - - if (error.code === 'TIMEOUT') { - return SafeCreationStatus.TIMEOUT - } - - return SafeCreationStatus.ERROR -} - -export const SAFE_CREATION_ERROR_KEY = 'create-safe-error' -export const showSafeCreationError = (error: EthersError | Error): AppThunk => { - return (dispatch) => { - dispatch( - showNotification({ - message: `Your transaction was unsuccessful. Reason: ${formatError(error)}`, - detailedMessage: error.message, - groupKey: SAFE_CREATION_ERROR_KEY, - variant: 'error', - }), - ) - } -} - -export const checkSafeCreationTx = async ( - provider: Provider, - pendingTx: PendingSafeTx, - txHash: string, - dispatch: AppDispatch, -): Promise<SafeCreationStatus> => { - const TIMEOUT_TIME = 60 * 1000 // 1 minute - - try { - // TODO: Use the fix from checkSafeActivation to detect cancellation and speed-up txs again - const receipt = await provider.waitForTransaction(txHash, 1, TIMEOUT_TIME) - - /** The receipt should always be non-null as we require 1 confirmation */ - if (receipt === null) { - throw new Error('Transaction should have a receipt, but got null instead.') - } - - if (didRevert(receipt)) { - return SafeCreationStatus.REVERTED - } - - return SafeCreationStatus.SUCCESS - } catch (err) { - const _err = err as EthersError - - const status = handleSafeCreationError(_err) - - if (status !== SafeCreationStatus.SUCCESS) { - dispatch(showSafeCreationError(_err)) - } - - return status - } -} - -export const CREATION_MODAL_QUERY_PARM = 'showCreationModal' - export const getRedirect = ( chainPrefix: string, safeAddress: string, @@ -284,7 +146,7 @@ export const getRedirect = ( // Go to the dashboard if no specific redirect is provided if (!redirectUrl) { - return { pathname: AppRoutes.home, query: { safe: address, [CREATION_MODAL_QUERY_PARM]: true } } + return { pathname: AppRoutes.home, query: { safe: address } } } // Otherwise, redirect to the provided URL (e.g. from a Safe App) diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index 3cc24712e7..5ed1f11c39 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -1,10 +1,12 @@ import ChainIndicator from '@/components/common/ChainIndicator' import type { NamedAddress } from '@/components/new-safe/create/types' import EthHashInfo from '@/components/common/EthHashInfo' +import { safeCreationDispatch, SafeCreationEvent } from '@/features/counterfactual/services/safeCreationEvents' +import { addUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' import { getTotalFeeFormatted } from '@/hooks/useGasPrice' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' import type { NewSafeFormData } from '@/components/new-safe/create' -import { computeNewSafeAddress } from '@/components/new-safe/create/logic' +import { computeNewSafeAddress, createNewSafe, relaySafeCreation } from '@/components/new-safe/create/logic' import { getAvailableSaltNonce } from '@/components/new-safe/create/logic/utils' import NetworkWarning from '@/components/new-safe/create/NetworkWarning' import css from '@/components/new-safe/create/steps/ReviewStep/styles.module.css' @@ -16,7 +18,7 @@ import ErrorMessage from '@/components/tx/ErrorMessage' import { ExecutionMethod, ExecutionMethodSelector } from '@/components/tx/ExecutionMethodSelector' import { LATEST_SAFE_VERSION } from '@/config/constants' import PayNowPayLater, { PayMethod } from '@/features/counterfactual/PayNowPayLater' -import { createCounterfactualSafe } from '@/features/counterfactual/utils' +import { CF_TX_GROUP_KEY, createCounterfactualSafe } from '@/features/counterfactual/utils' import { useCurrentChain, useHasFeature } from '@/hooks/useChains' import useGasPrice from '@/hooks/useGasPrice' import useIsWrongChain from '@/hooks/useIsWrongChain' @@ -27,17 +29,19 @@ import { useWeb3 } from '@/hooks/wallets/web3' import { CREATE_SAFE_CATEGORY, CREATE_SAFE_EVENTS, OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' import { gtmSetSafeAddress } from '@/services/analytics/gtm' import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeContracts' +import { asError } from '@/services/exceptions/utils' import { useAppDispatch } from '@/store' -import { FEATURES } from '@/utils/chains' +import { FEATURES, hasFeature } from '@/utils/chains' import { hasRemainingRelays } from '@/utils/relaying' +import { isWalletRejection } from '@/utils/wallets' import ArrowBackIcon from '@mui/icons-material/ArrowBack' import { Box, Button, CircularProgress, Divider, Grid, Typography } from '@mui/material' import { type DeploySafeProps } from '@safe-global/protocol-kit' +import type { SafeVersion } from '@safe-global/safe-core-sdk-types' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import classnames from 'classnames' import { useRouter } from 'next/router' import { useMemo, useState } from 'react' -import { usePendingSafe } from '../StatusStep/usePendingSafe' export const NetworkFee = ({ totalFee, @@ -118,12 +122,12 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe const dispatch = useAppDispatch() const router = useRouter() const [gasPrice] = useGasPrice() - const [_, setPendingSafe] = usePendingSafe() const [payMethod, setPayMethod] = useState(PayMethod.PayLater) const [executionMethod, setExecutionMethod] = useState(ExecutionMethod.RELAY) const [isCreating, setIsCreating] = useState<boolean>(false) const [submitError, setSubmitError] = useState<string>() const isCounterfactualEnabled = useHasFeature(FEATURES.COUNTERFACTUAL) + const isEIP1559 = chain && hasFeature(chain, FEATURES.EIP1559) const ownerAddresses = useMemo(() => data.owners.map((owner) => owner.address), [data.owners]) const [minRelays] = useLeastRemainingRelays(ownerAddresses) @@ -187,19 +191,76 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe return } - const pendingSafe = { - ...data, - saltNonce: Number(saltNonce), - safeAddress, - willRelay, + const options: DeploySafeProps['options'] = isEIP1559 + ? { + maxFeePerGas: maxFeePerGas?.toString(), + maxPriorityFeePerGas: maxPriorityFeePerGas?.toString(), + gasLimit: gasLimit?.toString(), + } + : { gasPrice: maxFeePerGas?.toString(), gasLimit: gasLimit?.toString() } + + const undeployedSafe = { + chainId: chain.chainId, + address: safeAddress, + type: PayMethod.PayNow, + safeProps: { + safeAccountConfig: props.safeAccountConfig, + safeDeploymentConfig: { + saltNonce, + safeVersion: LATEST_SAFE_VERSION as SafeVersion, + }, + }, } - trackEvent({ ...OVERVIEW_EVENTS.PROCEED_WITH_TX, label: 'deployment', category: CREATE_SAFE_CATEGORY }) + const onSubmitCallback = async (taskId?: string, txHash?: string) => { + dispatch(addUndeployedSafe(undeployedSafe)) + + if (taskId) { + safeCreationDispatch(SafeCreationEvent.RELAYING, { groupKey: CF_TX_GROUP_KEY, taskId, safeAddress }) + } + + if (txHash) { + safeCreationDispatch(SafeCreationEvent.PROCESSING, { + groupKey: CF_TX_GROUP_KEY, + txHash, + safeAddress, + }) + } + + trackEvent(CREATE_SAFE_EVENTS.SUBMIT_CREATE_SAFE) + trackEvent({ ...OVERVIEW_EVENTS.PROCEED_WITH_TX, label: 'deployment', category: CREATE_SAFE_CATEGORY }) + + onSubmit(data) + } - setPendingSafe(pendingSafe) - onSubmit(pendingSafe) + if (willRelay) { + const taskId = await relaySafeCreation( + chain, + props.safeAccountConfig.owners, + props.safeAccountConfig.threshold, + Number(saltNonce), + ) + onSubmitCallback(taskId) + } else { + await createNewSafe(provider, { + safeAccountConfig: props.safeAccountConfig, + saltNonce, + options, + callback: (txHash) => { + onSubmitCallback(undefined, txHash) + }, + }) + } } catch (_err) { - setSubmitError('Error creating the Safe Account. Please try again later.') + const error = asError(_err) + const submitError = isWalletRejection(error) + ? 'User rejected signing.' + : 'Error creating the Safe Account. Please try again later.' + setSubmitError(submitError) + + if (isWalletRejection(error)) { + trackEvent(CREATE_SAFE_EVENTS.REJECT_CREATE_SAFE) + } } setIsCreating(false) diff --git a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx index 90300a14bc..3776d953f7 100644 --- a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx +++ b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx @@ -1,91 +1,79 @@ -import { Box, Typography } from '@mui/material' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' +import ExternalLink from '@/components/common/ExternalLink' import LoadingSpinner, { SpinnerStatus } from '@/components/new-safe/create/steps/StatusStep/LoadingSpinner' +import { SafeCreationEvent } from '@/features/counterfactual/services/safeCreationEvents' +import type { UndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' +import { useCurrentChain } from '@/hooks/useChains' +import { getBlockExplorerLink } from '@/utils/chains' +import { Box, Typography } from '@mui/material' +import FailedIcon from '@/public/images/common/tx-failed.svg' -const getStep = (status: SafeCreationStatus) => { - const ERROR_TEXT = 'Please cancel the process or retry the transaction.' - +const getStep = (status: SafeCreationEvent) => { switch (status) { - case SafeCreationStatus.AWAITING: - return { - description: 'Waiting for transaction confirmation.', - instruction: 'Please confirm the transaction with your connected wallet.', - } - case SafeCreationStatus.WALLET_REJECTED: - return { - description: 'Transaction was rejected.', - instruction: ERROR_TEXT, - } - case SafeCreationStatus.PROCESSING: - return { - description: 'Transaction is being executed.', - instruction: 'Please do not leave this page.', - } - case SafeCreationStatus.ERROR: + case SafeCreationEvent.PROCESSING: + case SafeCreationEvent.RELAYING: return { - description: 'There was an error.', - instruction: ERROR_TEXT, + description: 'We are activating your account', + instruction: 'It can take some minutes to create your account, but you can check the progress below.', } - case SafeCreationStatus.REVERTED: + case SafeCreationEvent.FAILED: return { - description: 'Transaction was reverted.', - instruction: ERROR_TEXT, + description: "Your account couldn't be created", + instruction: + 'The creation transaction was rejected by the connected wallet. You can retry or create an account from scratch.', } - case SafeCreationStatus.TIMEOUT: + case SafeCreationEvent.REVERTED: return { - description: 'Transaction was not found. Be aware that it might still be processed.', - instruction: ERROR_TEXT, + description: "Your account couldn't be created", + instruction: 'The creation transaction reverted. You can retry or create an account from scratch.', } - case SafeCreationStatus.SUCCESS: + case SafeCreationEvent.SUCCESS: return { description: 'Your Safe Account is being indexed..', instruction: 'The account will be ready for use shortly. Please do not leave this page.', } - case SafeCreationStatus.INDEXED: + case SafeCreationEvent.INDEXED: return { description: 'Your Safe Account was successfully created!', instruction: '', } - case SafeCreationStatus.INDEX_FAILED: - return { - description: 'Your Safe Account is successfully created!', - instruction: - 'You can already open Safe{Wallet}. It might take a moment until it becomes fully usable in the interface.', - } } } -const StatusMessage = ({ status, isError }: { status: SafeCreationStatus; isError: boolean }) => { +const StatusMessage = ({ + status, + isError, + pendingSafe, +}: { + status: SafeCreationEvent + isError: boolean + pendingSafe: UndeployedSafe | undefined +}) => { const stepInfo = getStep(status) + const chain = useCurrentChain() - const color = isError ? 'error' : 'info' - const isSuccess = status >= SafeCreationStatus.SUCCESS - const spinnerStatus = isError ? SpinnerStatus.ERROR : isSuccess ? SpinnerStatus.SUCCESS : SpinnerStatus.PROCESSING + const isSuccess = status === SafeCreationEvent.SUCCESS + const spinnerStatus = isSuccess ? SpinnerStatus.SUCCESS : SpinnerStatus.PROCESSING + const explorerLink = + chain && pendingSafe?.status.txHash ? getBlockExplorerLink(chain, pendingSafe.status.txHash) : undefined return ( <> <Box data-testid="safe-status-info" paddingX={3} mt={3}> - <LoadingSpinner status={spinnerStatus} /> - <Typography variant="h6" marginTop={2} fontWeight={700}> + <Box width="160px" height="160px" display="flex" margin="auto"> + {isError ? <FailedIcon /> : <LoadingSpinner status={spinnerStatus} />} + </Box> + <Typography variant="h3" marginTop={2} fontWeight={700}> {stepInfo.description} </Typography> </Box> - {stepInfo.instruction && ( - <Box - sx={({ palette }) => ({ - backgroundColor: palette[color].background, - borderColor: palette[color].light, - borderWidth: 1, - borderStyle: 'solid', - borderRadius: '6px', - })} - padding={3} - mt={4} - mb={0} - > - <Typography variant="body2">{stepInfo.instruction}</Typography> - </Box> - )} + <Box maxWidth={390} margin="auto"> + {stepInfo.instruction && ( + <Typography variant="body2" my={2}> + {stepInfo.instruction} + </Typography> + )} + {!isError && explorerLink && <ExternalLink href={explorerLink.href}>Check Status</ExternalLink>} + </Box> </> ) } diff --git a/src/components/new-safe/create/steps/StatusStep/StatusStepper.tsx b/src/components/new-safe/create/steps/StatusStep/StatusStepper.tsx deleted file mode 100644 index de73ce73b6..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/StatusStepper.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Box, Step, StepConnector, Stepper, Typography } from '@mui/material' -import css from '@/components/new-safe/create/steps/StatusStep/styles.module.css' -import EthHashInfo from '@/components/common/EthHashInfo' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import StatusStep from '@/components/new-safe/create/steps/StatusStep/StatusStep' -import { usePendingSafe } from './usePendingSafe' - -const StatusStepper = ({ status }: { status: SafeCreationStatus }) => { - const [pendingSafe] = usePendingSafe() - if (!pendingSafe?.safeAddress) return null - - return ( - <Stepper orientation="vertical" nonLinear connector={<StepConnector className={css.connector} />}> - <Step> - <StatusStep isLoading={!pendingSafe.safeAddress} safeAddress={pendingSafe.safeAddress}> - <Box> - <Typography variant="body2" fontWeight="700"> - Your Safe Account address - </Typography> - <EthHashInfo - address={pendingSafe.safeAddress} - hasExplorer - showCopyButton - showName={false} - shortAddress={false} - showAvatar={false} - /> - </Box> - </StatusStep> - </Step> - <Step> - <StatusStep isLoading={!(pendingSafe.txHash || pendingSafe.taskId)} safeAddress={pendingSafe.safeAddress}> - <Box> - <Typography variant="body2" fontWeight="700"> - Validating transaction - </Typography> - {pendingSafe.txHash && ( - <EthHashInfo - address={pendingSafe.txHash} - hasExplorer - showCopyButton - showName={false} - shortAddress={true} - showAvatar={false} - /> - )} - </Box> - </StatusStep> - </Step> - <Step> - <StatusStep isLoading={status < SafeCreationStatus.SUCCESS} safeAddress={pendingSafe.safeAddress}> - <Typography variant="body2" fontWeight="700"> - Indexing - </Typography> - </StatusStep> - </Step> - <Step> - <StatusStep isLoading={status !== SafeCreationStatus.INDEXED} safeAddress={pendingSafe.safeAddress}> - <Typography variant="body2" fontWeight="700"> - Safe Account is ready - </Typography> - </StatusStep> - </Step> - </Stepper> - ) -} - -export default StatusStepper diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/index.test.tsx b/src/components/new-safe/create/steps/StatusStep/__tests__/index.test.tsx deleted file mode 100644 index 31b5aae5ec..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/index.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import { render } from '@/tests/test-utils' -import { CreateSafeStatus } from '@/components/new-safe/create/steps/StatusStep' -import { type NewSafeFormData } from '@/components/new-safe/create' -import * as useSafeCreation from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' - -describe('StatusStep', () => { - it('should call useSafeCreation with PROCESSING status if relaying', () => { - const useSafeCreationSpy = jest.spyOn(useSafeCreation, 'useSafeCreation') - - render( - <CreateSafeStatus - data={{ willRelay: true } as NewSafeFormData} - onSubmit={() => {}} - onBack={() => {}} - setStep={() => {}} - />, - ) - - expect(useSafeCreationSpy).toHaveBeenCalledWith(SafeCreationStatus.PROCESSING, expect.anything(), true) - }) -}) diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/usePendingSafe.test.ts b/src/components/new-safe/create/steps/StatusStep/__tests__/usePendingSafe.test.ts deleted file mode 100644 index 03cad0f74c..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/usePendingSafe.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { renderHook } from '@/tests/test-utils' -import { usePendingSafe } from '../usePendingSafe' - -import { toBeHex } from 'ethers' -import { useCurrentChain } from '@/hooks/useChains' - -// mock useCurrentChain -jest.mock('@/hooks/useChains', () => ({ - useCurrentChain: jest.fn(() => ({ - shortName: 'gor', - chainId: '5', - chainName: 'Goerli', - features: [], - })), -})) - -describe('usePendingSafe()', () => { - const mockPendingSafe1 = { - name: 'joyful-rinkeby-safe', - threshold: 1, - owners: [], - saltNonce: 123, - address: toBeHex('0x10', 20), - } - const mockPendingSafe2 = { - name: 'joyful-rinkeby-safe', - threshold: 1, - owners: [], - saltNonce: 123, - address: toBeHex('0x10', 20), - } - - beforeEach(() => { - window.localStorage.clear() - }) - it('Should initially be undefined', () => { - const { result } = renderHook(() => usePendingSafe()) - expect(result.current[0]).toBeUndefined() - }) - - it('Should set the pendingSafe per ChainId', async () => { - const { result, rerender } = renderHook(() => usePendingSafe()) - - result.current[1](mockPendingSafe1) - - rerender() - - expect(result.current[0]).toEqual(mockPendingSafe1) - ;(useCurrentChain as jest.Mock).mockImplementation(() => ({ - shortName: 'eth', - chainId: '1', - chainName: 'Ethereum', - features: [], - })) - - rerender() - expect(result.current[0]).toEqual(undefined) - - result.current[1](mockPendingSafe2) - rerender() - expect(result.current[0]).toEqual(mockPendingSafe2) - ;(useCurrentChain as jest.Mock).mockImplementation(() => ({ - shortName: 'gor', - chainId: '5', - chainName: 'Goerli', - features: [], - })) - rerender() - expect(result.current[0]).toEqual(mockPendingSafe1) - }) -}) diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts b/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts deleted file mode 100644 index 2fc29f51b4..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts +++ /dev/null @@ -1,348 +0,0 @@ -import { renderHook } from '@/tests/test-utils' -import { SafeCreationStatus, useSafeCreation } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import * as web3 from '@/hooks/wallets/web3' -import * as chain from '@/hooks/useChains' -import * as wallet from '@/hooks/wallets/useWallet' -import * as logic from '@/components/new-safe/create/logic' -import * as contracts from '@/services/contracts/safeContracts' -import * as txMonitor from '@/services/tx/txMonitor' -import * as usePendingSafe from '@/components/new-safe/create/steps/StatusStep/usePendingSafe' -import { BrowserProvider, zeroPadValue, type JsonRpcProvider } from 'ethers' -import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' -import { chainBuilder } from '@/tests/builders/chains' -import { waitFor } from '@testing-library/react' -import type Safe from '@safe-global/protocol-kit' -import type CompatibilityFallbackHandlerEthersContract from '@safe-global/protocol-kit/dist/src/adapters/ethers/contracts/CompatibilityFallbackHandler/CompatibilityFallbackHandlerEthersContract' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' -import * as gasPrice from '@/hooks/useGasPrice' -import { MockEip1193Provider } from '@/tests/mocks/providers' - -const mockSafeInfo = { - data: '0x', - from: '0x1', - to: '0x2', - nonce: 1, - value: BigInt(0), - startBlock: 1, -} - -jest.mock('@safe-global/protocol-kit', () => { - const originalModule = jest.requireActual('@safe-global/protocol-kit') - - // Mock class - class MockEthersAdapter extends originalModule.EthersAdapter { - getChainId = jest.fn().mockImplementation(() => Promise.resolve(BigInt(4))) - } - - return { - ...originalModule, - EthersAdapter: MockEthersAdapter, - } -}) - -describe('useSafeCreation', () => { - const mockPendingSafe = { - name: 'joyful-rinkeby-safe', - threshold: 1, - owners: [], - saltNonce: 123, - address: '0x10', - } - const mockSetPendingSafe = jest.fn() - const mockStatus = SafeCreationStatus.AWAITING - const mockSetStatus = jest.fn() - const mockProvider: BrowserProvider = new BrowserProvider(MockEip1193Provider) - const mockReadOnlyProvider = { - getCode: jest.fn(), - } as unknown as JsonRpcProvider - - beforeEach(() => { - jest.resetAllMocks() - jest.restoreAllMocks() - - const mockChain = chainBuilder().with({ features: [] }).build() - jest.spyOn(web3, 'useWeb3').mockImplementation(() => mockProvider) - jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => mockReadOnlyProvider) - jest.spyOn(web3, 'useWeb3ReadOnly').mockImplementation(() => mockReadOnlyProvider) - jest.spyOn(chain, 'useCurrentChain').mockImplementation(() => mockChain) - jest.spyOn(wallet, 'default').mockReturnValue({} as ConnectedWallet) - jest.spyOn(logic, 'getSafeCreationTxInfo').mockReturnValue(Promise.resolve(mockSafeInfo)) - jest.spyOn(logic, 'estimateSafeCreationGas').mockReturnValue(Promise.resolve(BigInt(200000))) - jest.spyOn(contracts, 'getReadOnlyFallbackHandlerContract').mockResolvedValue({ - getAddress: () => zeroPadValue('0x0123', 20), - } as unknown as CompatibilityFallbackHandlerEthersContract) - jest - .spyOn(gasPrice, 'default') - .mockReturnValue([{ maxFeePerGas: BigInt(123), maxPriorityFeePerGas: undefined }, undefined, false]) - }) - - it('should create a safe with gas params if there is no txHash and status is AWAITING', async () => { - const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).toHaveBeenCalled() - - const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = createSafeSpy.mock.calls[0][1].options || {} - - expect(gasPrice).toBe('123') - - expect(maxFeePerGas).toBeUndefined() - expect(maxPriorityFeePerGas).toBeUndefined() - }) - }) - - it('should create a safe with EIP-1559 gas params if there is no txHash and status is AWAITING', async () => { - jest - .spyOn(gasPrice, 'default') - .mockReturnValue([{ maxFeePerGas: BigInt(123), maxPriorityFeePerGas: BigInt(456) }, undefined, false]) - - jest.spyOn(chain, 'useCurrentChain').mockImplementation(() => - chainBuilder() - .with({ features: [FEATURES.EIP1559] }) - .build(), - ) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - - const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).toHaveBeenCalled() - - const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = createSafeSpy.mock.calls[0][1].options || {} - - expect(maxFeePerGas).toBe('123') - expect(maxPriorityFeePerGas).toBe('456') - - expect(gasPrice).toBeUndefined() - }) - }) - - it('should create a safe with no gas params if the gas estimation threw, there is no txHash and status is AWAITING', async () => { - jest.spyOn(gasPrice, 'default').mockReturnValue([undefined, Error('Error for testing'), false]) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - - const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).toHaveBeenCalled() - - const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = createSafeSpy.mock.calls[0][1].options || {} - - expect(gasPrice).toBeUndefined() - expect(maxFeePerGas).toBeUndefined() - expect(maxPriorityFeePerGas).toBeUndefined() - }) - }) - - it('should not create a safe if there is no txHash, status is AWAITING but gas is loading', async () => { - jest.spyOn(gasPrice, 'default').mockReturnValue([undefined, undefined, true]) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - - const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - }) - - it('should not create a safe if the status is not AWAITING', async () => { - const createSafeSpy = jest.spyOn(logic, 'createNewSafe') - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - - renderHook(() => useSafeCreation(SafeCreationStatus.WALLET_REJECTED, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.PROCESSING, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.ERROR, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.REVERTED, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.TIMEOUT, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.SUCCESS, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.INDEXED, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - - renderHook(() => useSafeCreation(SafeCreationStatus.INDEX_FAILED, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - }) - - it('should not create a safe if there is a txHash', async () => { - const createSafeSpy = jest.spyOn(logic, 'createNewSafe') - jest - .spyOn(usePendingSafe, 'usePendingSafe') - .mockReturnValue([{ ...mockPendingSafe, txHash: '0x123' }, mockSetPendingSafe]) - - renderHook(() => useSafeCreation(SafeCreationStatus.AWAITING, mockSetStatus, false)) - - await waitFor(() => { - expect(createSafeSpy).not.toHaveBeenCalled() - }) - }) - - it('should watch a tx if there is a txHash and a tx object', async () => { - const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ - { - ...mockPendingSafe, - txHash: '0x123', - tx: { - data: '0x', - from: '0x1234', - nonce: 0, - startBlock: 0, - to: '0x456', - value: BigInt(0), - }, - }, - mockSetPendingSafe, - ]) - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(watchSafeTxSpy).toHaveBeenCalledTimes(1) - }) - }) - - it('should watch a tx even if no wallet is connected', async () => { - jest.spyOn(wallet, 'default').mockReturnValue(null) - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ - { - ...mockPendingSafe, - txHash: '0x123', - tx: { - data: '0x', - from: '0x1234', - nonce: 0, - startBlock: 0, - to: '0x456', - value: BigInt(0), - }, - }, - mockSetPendingSafe, - ]) - const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(watchSafeTxSpy).toHaveBeenCalledTimes(1) - }) - }) - - it('should not watch a tx if there is no txHash', async () => { - const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(watchSafeTxSpy).not.toHaveBeenCalled() - }) - }) - - it('should not watch a tx if there is no tx object', async () => { - const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ - { - ...mockPendingSafe, - tx: { - data: '0x', - from: '0x1234', - nonce: 0, - startBlock: 0, - to: '0x456', - value: BigInt(0), - }, - }, - mockSetPendingSafe, - ]) - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(watchSafeTxSpy).not.toHaveBeenCalled() - }) - }) - - it('should set a PROCESSING state when watching a tx', async () => { - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ - { - ...mockPendingSafe, - txHash: '0x123', - tx: { - data: '0x', - from: '0x1234', - nonce: 0, - startBlock: 0, - to: '0x456', - value: BigInt(0), - }, - }, - mockSetPendingSafe, - ]) - - renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) - - await waitFor(() => { - expect(mockSetStatus).toHaveBeenCalledWith(SafeCreationStatus.PROCESSING) - }) - }) - - it('should set a PROCESSING state and monitor relay taskId after successfully tx relay', async () => { - jest.spyOn(logic, 'relaySafeCreation').mockResolvedValue('0x456') - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ - { - ...mockPendingSafe, - }, - mockSetPendingSafe, - ]) - const txMonitorSpy = jest.spyOn(txMonitor, 'waitForCreateSafeTx').mockImplementation(jest.fn()) - - const initialStatus = SafeCreationStatus.PROCESSING - - renderHook(() => useSafeCreation(initialStatus, mockSetStatus, true)) - - await waitFor(() => { - expect(mockSetStatus).toHaveBeenCalledWith(SafeCreationStatus.PROCESSING) - expect(txMonitorSpy).toHaveBeenCalledWith('0x456', expect.anything()) - }) - }) -}) diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreationEffects.test.ts b/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreationEffects.test.ts deleted file mode 100644 index 2c878565a0..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreationEffects.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { renderHook } from '@/tests/test-utils' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import { type SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' -import * as web3 from '@/hooks/wallets/web3' -import * as pendingSafe from '@/components/new-safe/create/logic' -import * as usePendingSafe from '@/components/new-safe/create/steps/StatusStep/usePendingSafe' -import * as addressbook from '@/components/new-safe/create/logic/address-book' -import useSafeCreationEffects from '@/components/new-safe/create/steps/StatusStep/useSafeCreationEffects' -import type { PendingSafeData } from '@/components/new-safe/create/types' -import { toBeHex, BrowserProvider } from 'ethers' -import { MockEip1193Provider } from '@/tests/mocks/providers' - -describe('useSafeCreationEffects', () => { - beforeEach(() => { - jest.resetAllMocks() - jest.spyOn(pendingSafe, 'pollSafeInfo').mockImplementation(jest.fn(() => Promise.resolve({} as SafeInfo))) - jest.spyOn(addressbook, 'updateAddressBook').mockReturnValue(() => {}) - - const mockProvider: BrowserProvider = new BrowserProvider(MockEip1193Provider) - jest.spyOn(web3, 'useWeb3').mockImplementation(() => mockProvider) - }) - - it('should clear the tx hash if it exists on ERROR or REVERTED', () => { - const setStatusSpy = jest.fn() - const setPendingSafeSpy = jest.fn() - jest - .spyOn(usePendingSafe, 'usePendingSafe') - .mockReturnValue([{ txHash: '0x123' } as PendingSafeData, setPendingSafeSpy]) - - renderHook(() => - useSafeCreationEffects({ - status: SafeCreationStatus.ERROR, - setStatus: setStatusSpy, - }), - ) - - expect(setPendingSafeSpy).toHaveBeenCalled() - }) - - it('should not clear the tx hash if it doesnt exist on ERROR or REVERTED', () => { - const setStatusSpy = jest.fn() - const setPendingSafeSpy = jest.fn() - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([{} as PendingSafeData, setPendingSafeSpy]) - renderHook(() => - useSafeCreationEffects({ - status: SafeCreationStatus.ERROR, - setStatus: setStatusSpy, - }), - ) - - expect(setPendingSafeSpy).not.toHaveBeenCalled() - }) - - it('should poll safe info on SUCCESS', () => { - const pollSafeInfoSpy = jest.spyOn(pendingSafe, 'pollSafeInfo') - const setStatusSpy = jest.fn() - const setPendingSafeSpy = jest.fn() - jest - .spyOn(usePendingSafe, 'usePendingSafe') - .mockReturnValue([{ safeAddress: toBeHex('0x123', 20) } as PendingSafeData, setPendingSafeSpy]) - renderHook(() => - useSafeCreationEffects({ - status: SafeCreationStatus.SUCCESS, - setStatus: setStatusSpy, - }), - ) - - expect(pollSafeInfoSpy).toHaveBeenCalled() - }) - - it('should not poll safe info on SUCCESS if there is no safe address', () => { - const pollSafeInfoSpy = jest.spyOn(pendingSafe, 'pollSafeInfo') - const setStatusSpy = jest.fn() - const setPendingSafeSpy = jest.fn() - jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([{} as PendingSafeData, setPendingSafeSpy]) - renderHook(() => - useSafeCreationEffects({ - status: SafeCreationStatus.SUCCESS, - setStatus: setStatusSpy, - }), - ) - - expect(pollSafeInfoSpy).not.toHaveBeenCalled() - }) -}) diff --git a/src/components/new-safe/create/steps/StatusStep/index.tsx b/src/components/new-safe/create/steps/StatusStep/index.tsx index 59920ed04d..2a6b01ac0d 100644 --- a/src/components/new-safe/create/steps/StatusStep/index.tsx +++ b/src/components/new-safe/create/steps/StatusStep/index.tsx @@ -1,75 +1,64 @@ -import { useCallback, useEffect, useState } from 'react' -import { Box, Button, Divider, Paper, Tooltip, Typography } from '@mui/material' -import { useRouter } from 'next/router' - -import Track from '@/components/common/Track' -import { CREATE_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' -import StatusMessage from '@/components/new-safe/create/steps/StatusStep/StatusMessage' -import useWallet from '@/hooks/wallets/useWallet' -import useIsWrongChain from '@/hooks/useIsWrongChain' -import type { NewSafeFormData } from '@/components/new-safe/create' +import { useCounter } from '@/components/common/Notifications/useCounter' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' -import useSafeCreationEffects from '@/components/new-safe/create/steps/StatusStep/useSafeCreationEffects' -import { SafeCreationStatus, useSafeCreation } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import StatusStepper from '@/components/new-safe/create/steps/StatusStep/StatusStepper' -import { OPEN_SAFE_LABELS, OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' +import type { NewSafeFormData } from '@/components/new-safe/create' import { getRedirect } from '@/components/new-safe/create/logic' -import layoutCss from '@/components/new-safe/create/styles.module.css' -import { AppRoutes } from '@/config/routes' +import { updateAddressBook } from '@/components/new-safe/create/logic/address-book' +import StatusMessage from '@/components/new-safe/create/steps/StatusStep/StatusMessage' +import useUndeployedSafe from '@/components/new-safe/create/steps/StatusStep/useUndeployedSafe' import lightPalette from '@/components/theme/lightPalette' +import { AppRoutes } from '@/config/routes' +import { safeCreationPendingStatuses } from '@/features/counterfactual/hooks/usePendingSafeStatuses' +import { SafeCreationEvent, safeCreationSubscribe } from '@/features/counterfactual/services/safeCreationEvents' import { useCurrentChain } from '@/hooks/useChains' -import { usePendingSafe } from './usePendingSafe' +import Rocket from '@/public/images/common/rocket.svg' +import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' +import { useAppDispatch } from '@/store' +import { Alert, AlertTitle, Box, Button, Paper, Stack, SvgIcon, Typography } from '@mui/material' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' import useSyncSafeCreationStep from '../../useSyncSafeCreationStep' -export const getInitialCreationStatus = (willRelay: boolean): SafeCreationStatus => - willRelay ? SafeCreationStatus.PROCESSING : SafeCreationStatus.AWAITING +const SPEED_UP_THRESHOLD_IN_SECONDS = 15 -export const CreateSafeStatus = ({ data, setProgressColor, setStep }: StepRenderProps<NewSafeFormData>) => { +export const CreateSafeStatus = ({ + data, + setProgressColor, + setStep, + setStepData, +}: StepRenderProps<NewSafeFormData>) => { + const [status, setStatus] = useState<SafeCreationEvent>(SafeCreationEvent.PROCESSING) + const [safeAddress, pendingSafe] = useUndeployedSafe() const router = useRouter() - const chainInfo = useCurrentChain() - const chainPrefix = chainInfo?.shortName || '' - const wallet = useWallet() - const isWrongChain = useIsWrongChain() - const isConnected = wallet && !isWrongChain - const [pendingSafe, setPendingSafe] = usePendingSafe() - useSyncSafeCreationStep(setStep) - - // The willRelay flag can come from the previous step or from local storage - const willRelay = !!(data.willRelay || pendingSafe?.willRelay) - const initialStatus = getInitialCreationStatus(willRelay) - const [status, setStatus] = useState<SafeCreationStatus>(initialStatus) - - const { handleCreateSafe } = useSafeCreation(status, setStatus, willRelay) - - useSafeCreationEffects({ - status, - setStatus, - }) + const chain = useCurrentChain() + const dispatch = useAppDispatch() - const onClose = useCallback(() => { - setPendingSafe(undefined) + const counter = useCounter(pendingSafe?.status.submittedAt) - router.push(AppRoutes.welcome.index) - }, [router, setPendingSafe]) + const isError = status === SafeCreationEvent.FAILED || status === SafeCreationEvent.REVERTED - const handleRetry = useCallback(() => { - setStatus(initialStatus) - void handleCreateSafe() - }, [handleCreateSafe, initialStatus]) + useSyncSafeCreationStep(setStep) - const onFinish = useCallback(() => { - trackEvent(CREATE_SAFE_EVENTS.GET_STARTED) + useEffect(() => { + const unsubFns = Object.entries(safeCreationPendingStatuses).map(([event]) => + safeCreationSubscribe(event as SafeCreationEvent, async () => { + setStatus(event as SafeCreationEvent) + }), + ) + + return () => { + unsubFns.forEach((unsub) => unsub()) + } + }, []) - const { safeAddress } = pendingSafe || {} + useEffect(() => { + if (!chain || !safeAddress) return - if (safeAddress) { - setPendingSafe(undefined) - router.push(getRedirect(chainPrefix, safeAddress, router.query?.safeViewRedirectURL)) + if (status === SafeCreationEvent.SUCCESS) { + dispatch(updateAddressBook(chain.chainId, safeAddress, data.name, data.owners, data.threshold)) + router.push(getRedirect(chain.shortName, safeAddress, router.query?.safeViewRedirectURL)) } - }, [chainPrefix, pendingSafe, router, setPendingSafe]) - - const displaySafeLink = status >= SafeCreationStatus.INDEXED - const isError = status >= SafeCreationStatus.WALLET_REJECTED && status <= SafeCreationStatus.TIMEOUT + }, [dispatch, chain, data.name, data.owners, data.threshold, router, safeAddress, status]) useEffect(() => { if (!setProgressColor) return @@ -81,63 +70,64 @@ export const CreateSafeStatus = ({ data, setProgressColor, setStep }: StepRender } }, [isError, setProgressColor]) + const tryAgain = () => { + trackEvent(CREATE_SAFE_EVENTS.RETRY_CREATE_SAFE) + + if (!pendingSafe) { + setStep(0) + return + } + + setProgressColor?.(lightPalette.secondary.main) + setStep(2) + setStepData?.({ + owners: pendingSafe.props.safeAccountConfig.owners.map((owner) => ({ name: '', address: owner })), + name: '', + threshold: pendingSafe.props.safeAccountConfig.threshold, + saltNonce: Number(pendingSafe.props.safeDeploymentConfig?.saltNonce), + safeAddress, + }) + } + + const onCancel = () => { + trackEvent(CREATE_SAFE_EVENTS.CANCEL_CREATE_SAFE) + } + return ( <Paper sx={{ textAlign: 'center', }} > - <Box className={layoutCss.row}> - <StatusMessage status={status} isError={isError} /> - </Box> - - {!isError && pendingSafe && ( - <> - <Divider /> - <Box className={layoutCss.row}> - <StatusStepper status={status} /> - </Box> - </> - )} - - {displaySafeLink && ( - <> - <Divider /> - <Box className={layoutCss.row}> - <Track {...OVERVIEW_EVENTS.OPEN_SAFE} label={OPEN_SAFE_LABELS.after_create}> - <Button data-testid="start-using-safe-btn" variant="contained" onClick={onFinish}> - Start using {'Safe{Wallet}'} + <Box p={{ xs: 2, sm: 8 }}> + <StatusMessage status={status} isError={isError} pendingSafe={pendingSafe} /> + + {counter && counter > SPEED_UP_THRESHOLD_IN_SECONDS && !isError && ( + <Alert severity="warning" icon={<SvgIcon component={Rocket} />} sx={{ mt: 5 }}> + <AlertTitle> + <Typography variant="body2" textAlign="left" fontWeight="bold"> + Transaction is taking too long + </Typography> + </AlertTitle> + <Typography variant="body2" textAlign="left"> + Try to speed it up with better gas parameters in your wallet. + </Typography> + </Alert> + )} + + {isError && ( + <Stack direction="row" justifyContent="center" gap={2}> + <Link href={AppRoutes.welcome.index} passHref> + <Button variant="outlined" onClick={onCancel}> + Go to homepage </Button> - </Track> - </Box> - </> - )} - - {isError && ( - <> - <Divider /> - <Box className={layoutCss.row}> - <Box display="flex" flexDirection="row" justifyContent="space-between" gap={3}> - <Track {...CREATE_SAFE_EVENTS.CANCEL_CREATE_SAFE}> - <Button onClick={onClose} variant="outlined"> - Cancel - </Button> - </Track> - <Track {...CREATE_SAFE_EVENTS.RETRY_CREATE_SAFE}> - <Tooltip - title={!isConnected ? 'Please make sure your wallet is connected on the correct network.' : ''} - > - <Typography display="flex" height={1}> - <Button onClick={handleRetry} variant="contained" disabled={!isConnected}> - Retry - </Button> - </Typography> - </Tooltip> - </Track> - </Box> - </Box> - </> - )} + </Link> + <Button variant="contained" onClick={tryAgain}> + Try again + </Button> + </Stack> + )} + </Box> </Paper> ) } diff --git a/src/components/new-safe/create/steps/StatusStep/usePendingSafe.ts b/src/components/new-safe/create/steps/StatusStep/usePendingSafe.ts deleted file mode 100644 index 08c3ac543e..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/usePendingSafe.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useCurrentChain } from '@/hooks/useChains' -import useLocalStorage from '@/services/local-storage/useLocalStorage' -import { useCallback } from 'react' -import type { PendingSafeByChain, PendingSafeData } from '../../types' - -const SAFE_PENDING_CREATION_STORAGE_KEY = 'pendingSafe_v2' - -export const usePendingSafe = (): [PendingSafeData | undefined, (safe: PendingSafeData | undefined) => void] => { - const [pendingSafes, setPendingSafes] = useLocalStorage<PendingSafeByChain>(SAFE_PENDING_CREATION_STORAGE_KEY) - - const chainInfo = useCurrentChain() - - const pendingSafe = chainInfo && pendingSafes?.[chainInfo.chainId] - const setPendingSafe = useCallback( - (safe: PendingSafeData | undefined) => { - if (!chainInfo?.chainId) { - return - } - - // Always copy the object because useLocalStorage does not check for deep equality when writing back to ls - const newPendingSafes = pendingSafes ? { ...pendingSafes } : {} - newPendingSafes[chainInfo.chainId] = safe - setPendingSafes(newPendingSafes) - }, - [chainInfo?.chainId, pendingSafes, setPendingSafes], - ) - - return [pendingSafe, setPendingSafe] -} diff --git a/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts b/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts deleted file mode 100644 index b0fae21862..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts +++ /dev/null @@ -1,188 +0,0 @@ -import type { Dispatch, SetStateAction } from 'react' -import { useCallback, useEffect, useState } from 'react' -import { useWeb3, useWeb3ReadOnly } from '@/hooks/wallets/web3' -import { useCurrentChain } from '@/hooks/useChains' -import useWallet from '@/hooks/wallets/useWallet' -import type { EthersError } from '@/utils/ethers-utils' -import { getInitialCreationStatus } from '@/components/new-safe/create/steps/StatusStep/index' -import type { PendingSafeTx } from '@/components/new-safe/create/types' -import { - createNewSafe, - getSafeDeployProps, - checkSafeCreationTx, - getSafeCreationTxInfo, - handleSafeCreationError, - SAFE_CREATION_ERROR_KEY, - showSafeCreationError, - relaySafeCreation, - estimateSafeCreationGas, -} from '@/components/new-safe/create/logic' -import { useAppDispatch } from '@/store' -import { closeByGroupKey } from '@/store/notificationsSlice' -import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' -import { waitForCreateSafeTx } from '@/services/tx/txMonitor' -import useGasPrice from '@/hooks/useGasPrice' -import { hasFeature } from '@/utils/chains' -import { FEATURES } from '@/utils/chains' -import type { DeploySafeProps } from '@safe-global/protocol-kit' -import { usePendingSafe } from './usePendingSafe' - -export enum SafeCreationStatus { - AWAITING, - PROCESSING, - WALLET_REJECTED, - ERROR, - REVERTED, - TIMEOUT, - SUCCESS, - INDEXED, - INDEX_FAILED, -} - -export const useSafeCreation = ( - status: SafeCreationStatus, - setStatus: Dispatch<SetStateAction<SafeCreationStatus>>, - willRelay: boolean, -) => { - const [isCreating, setIsCreating] = useState(false) - const [isWatching, setIsWatching] = useState(false) - const dispatch = useAppDispatch() - const [pendingSafe, setPendingSafe] = usePendingSafe() - - const wallet = useWallet() - const provider = useWeb3() - const web3ReadOnly = useWeb3ReadOnly() - const chain = useCurrentChain() - const [gasPrice, , gasPriceLoading] = useGasPrice() - - const maxFeePerGas = gasPrice?.maxFeePerGas - const maxPriorityFeePerGas = gasPrice?.maxPriorityFeePerGas - - const isEIP1559 = chain && hasFeature(chain, FEATURES.EIP1559) - - const createSafeCallback = useCallback( - async (txHash: string, tx: PendingSafeTx) => { - setStatus(SafeCreationStatus.PROCESSING) - trackEvent(CREATE_SAFE_EVENTS.SUBMIT_CREATE_SAFE) - setPendingSafe(pendingSafe ? { ...pendingSafe, txHash, tx } : undefined) - }, - [setStatus, setPendingSafe, pendingSafe], - ) - - const handleCreateSafe = useCallback(async () => { - if (!pendingSafe || !provider || !chain || !wallet || isCreating || gasPriceLoading) return - - setIsCreating(true) - dispatch(closeByGroupKey({ groupKey: SAFE_CREATION_ERROR_KEY })) - - const { owners, threshold, saltNonce } = pendingSafe - const ownersAddresses = owners.map((owner) => owner.address) - - try { - if (willRelay) { - const taskId = await relaySafeCreation(chain, ownersAddresses, threshold, saltNonce) - - setPendingSafe(pendingSafe ? { ...pendingSafe, taskId } : undefined) - setStatus(SafeCreationStatus.PROCESSING) - waitForCreateSafeTx(taskId, setStatus) - } else { - const tx = await getSafeCreationTxInfo(provider, owners, threshold, saltNonce, chain, wallet) - - const safeParams = { - threshold, - owners: owners.map((owner) => owner.address), - saltNonce, - } - - const safeDeployProps = await getSafeDeployProps( - safeParams, - (txHash) => createSafeCallback(txHash, tx), - chain.chainId, - ) - - const gasLimit = await estimateSafeCreationGas(chain, provider, tx.from, safeParams) - - const options: DeploySafeProps['options'] = isEIP1559 - ? { - maxFeePerGas: maxFeePerGas?.toString(), - maxPriorityFeePerGas: maxPriorityFeePerGas?.toString(), - gasLimit: gasLimit.toString(), - } - : { gasPrice: maxFeePerGas?.toString(), gasLimit: gasLimit.toString() } - - await createNewSafe(provider, { - ...safeDeployProps, - options, - }) - setStatus(SafeCreationStatus.SUCCESS) - } - } catch (err) { - const _err = err as EthersError - const status = handleSafeCreationError(_err) - - setStatus(status) - - if (status !== SafeCreationStatus.SUCCESS) { - dispatch(showSafeCreationError(_err)) - } - } - - setIsCreating(false) - }, [ - chain, - createSafeCallback, - dispatch, - gasPriceLoading, - isCreating, - isEIP1559, - maxFeePerGas, - maxPriorityFeePerGas, - pendingSafe, - provider, - setPendingSafe, - setStatus, - wallet, - willRelay, - ]) - - const watchSafeTx = useCallback(async () => { - if (!pendingSafe?.tx || !pendingSafe?.txHash || !web3ReadOnly || isWatching) return - - setStatus(SafeCreationStatus.PROCESSING) - setIsWatching(true) - - const txStatus = await checkSafeCreationTx(web3ReadOnly, pendingSafe.tx, pendingSafe.txHash, dispatch) - setStatus(txStatus) - setIsWatching(false) - }, [isWatching, pendingSafe, web3ReadOnly, setStatus, dispatch]) - - // Create or monitor Safe creation - useEffect(() => { - if (status !== getInitialCreationStatus(willRelay)) return - - if (pendingSafe?.txHash && !isCreating) { - void watchSafeTx() - return - } - - if (pendingSafe?.taskId && !isCreating) { - waitForCreateSafeTx(pendingSafe.taskId, setStatus) - return - } - - void handleCreateSafe() - }, [ - handleCreateSafe, - isCreating, - pendingSafe?.taskId, - pendingSafe?.txHash, - setStatus, - status, - watchSafeTx, - willRelay, - ]) - - return { - handleCreateSafe, - } -} diff --git a/src/components/new-safe/create/steps/StatusStep/useSafeCreationEffects.ts b/src/components/new-safe/create/steps/StatusStep/useSafeCreationEffects.ts deleted file mode 100644 index e656fecd49..0000000000 --- a/src/components/new-safe/create/steps/StatusStep/useSafeCreationEffects.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Dispatch, SetStateAction } from 'react' -import { useEffect } from 'react' -import { pollSafeInfo } from '@/components/new-safe/create/logic' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' -import { updateAddressBook } from '@/components/new-safe/create/logic/address-book' -import { useAppDispatch } from '@/store' -import useChainId from '@/hooks/useChainId' -import { usePendingSafe } from './usePendingSafe' -import { gtmSetSafeAddress } from '@/services/analytics/gtm' - -const useSafeCreationEffects = ({ - status, - setStatus, -}: { - status: SafeCreationStatus - setStatus: Dispatch<SetStateAction<SafeCreationStatus>> -}) => { - const dispatch = useAppDispatch() - const chainId = useChainId() - const [pendingSafe, setPendingSafe] = usePendingSafe() - - // Asynchronously wait for Safe creation - useEffect(() => { - if (status === SafeCreationStatus.SUCCESS && pendingSafe?.safeAddress) { - pollSafeInfo(chainId, pendingSafe.safeAddress) - .then(() => setStatus(SafeCreationStatus.INDEXED)) - .catch(() => setStatus(SafeCreationStatus.INDEX_FAILED)) - } - }, [chainId, pendingSafe?.safeAddress, status, setStatus]) - - // Warn about leaving the page before Safe creation - useEffect(() => { - if (status !== SafeCreationStatus.PROCESSING && status !== SafeCreationStatus.AWAITING) return - - const onBeforeUnload = (event: BeforeUnloadEvent) => { - event.preventDefault() - event.returnValue = 'Are you sure you want to leave before your Safe Account is fully created?' - return event.returnValue - } - - window.addEventListener('beforeunload', onBeforeUnload) - - return () => window.removeEventListener('beforeunload', onBeforeUnload) - }, [status]) - - // Add Safe to Added Safes and add owner and safe names to Address Book - useEffect(() => { - if (status === SafeCreationStatus.SUCCESS && pendingSafe?.safeAddress) { - dispatch( - updateAddressBook( - chainId, - pendingSafe.safeAddress, - pendingSafe.name, - pendingSafe.owners, - pendingSafe.threshold, - ), - ) - } - }, [status, chainId, dispatch, pendingSafe]) - - // Reset pending Safe on error - useEffect(() => { - if ( - status === SafeCreationStatus.WALLET_REJECTED || - status === SafeCreationStatus.ERROR || - status === SafeCreationStatus.REVERTED - ) { - if (pendingSafe?.txHash) { - setPendingSafe(pendingSafe ? { ...pendingSafe, txHash: undefined, tx: undefined } : undefined) - } - } - }, [pendingSafe, setPendingSafe, status]) - - // Tracking - useEffect(() => { - if (status === SafeCreationStatus.SUCCESS) { - pendingSafe?.safeAddress && gtmSetSafeAddress(pendingSafe.safeAddress) - trackEvent({ ...CREATE_SAFE_EVENTS.CREATED_SAFE, label: 'deployment' }) - return - } - - if (status === SafeCreationStatus.WALLET_REJECTED) { - trackEvent(CREATE_SAFE_EVENTS.REJECT_CREATE_SAFE) - return - } - }, [pendingSafe?.safeAddress, status]) -} - -export default useSafeCreationEffects diff --git a/src/components/new-safe/create/steps/StatusStep/useUndeployedSafe.ts b/src/components/new-safe/create/steps/StatusStep/useUndeployedSafe.ts new file mode 100644 index 0000000000..029019d86e --- /dev/null +++ b/src/components/new-safe/create/steps/StatusStep/useUndeployedSafe.ts @@ -0,0 +1,19 @@ +import { PayMethod } from '@/features/counterfactual/PayNowPayLater' +import { selectUndeployedSafes } from '@/features/counterfactual/store/undeployedSafesSlice' +import useChainId from '@/hooks/useChainId' +import { useAppSelector } from '@/store' + +// Returns the undeployed safe for the current network +const useUndeployedSafe = () => { + const chainId = useChainId() + const undeployedSafes = useAppSelector(selectUndeployedSafes) + const undeployedSafe = + undeployedSafes[chainId] && + Object.entries(undeployedSafes[chainId]).find((undeployedSafe) => { + return undeployedSafe[1].status.type === PayMethod.PayNow + }) + + return undeployedSafe || [] +} + +export default useUndeployedSafe diff --git a/src/components/new-safe/create/useSyncSafeCreationStep.ts b/src/components/new-safe/create/useSyncSafeCreationStep.ts index 90a32c0e7f..1e645d6c93 100644 --- a/src/components/new-safe/create/useSyncSafeCreationStep.ts +++ b/src/components/new-safe/create/useSyncSafeCreationStep.ts @@ -1,21 +1,22 @@ +import useUndeployedSafe from '@/components/new-safe/create/steps/StatusStep/useUndeployedSafe' import { useEffect } from 'react' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' import type { NewSafeFormData } from '@/components/new-safe/create/index' import useWallet from '@/hooks/wallets/useWallet' -import { usePendingSafe } from './steps/StatusStep/usePendingSafe' import useIsWrongChain from '@/hooks/useIsWrongChain' import { useRouter } from 'next/router' import { AppRoutes } from '@/config/routes' const useSyncSafeCreationStep = (setStep: StepRenderProps<NewSafeFormData>['setStep']) => { - const [pendingSafe] = usePendingSafe() + const [safeAddress, pendingSafe] = useUndeployedSafe() + const wallet = useWallet() const isWrongChain = useIsWrongChain() const router = useRouter() useEffect(() => { // Jump to the status screen if there is already a tx submitted - if (pendingSafe) { + if (pendingSafe && pendingSafe.status.status !== 'AWAITING_EXECUTION') { setStep(3) return } diff --git a/src/features/counterfactual/ActivateAccountFlow.tsx b/src/features/counterfactual/ActivateAccountFlow.tsx index b9c20b2221..4de9d8883a 100644 --- a/src/features/counterfactual/ActivateAccountFlow.tsx +++ b/src/features/counterfactual/ActivateAccountFlow.tsx @@ -88,7 +88,7 @@ const ActivateAccountFlow = () => { trackEvent(WALLET_EVENTS.ONCHAIN_INTERACTION) if (txHash) { - safeCreationDispatch(SafeCreationEvent.PROCESSING, { groupKey: CF_TX_GROUP_KEY, txHash }) + safeCreationDispatch(SafeCreationEvent.PROCESSING, { groupKey: CF_TX_GROUP_KEY, txHash, safeAddress }) } setTxFlow(undefined) } @@ -104,7 +104,7 @@ const ActivateAccountFlow = () => { try { if (willRelay) { const taskId = await relaySafeCreation(chain, owners, threshold, Number(saltNonce!), safeVersion) - safeCreationDispatch(SafeCreationEvent.RELAYING, { groupKey: CF_TX_GROUP_KEY, taskId }) + safeCreationDispatch(SafeCreationEvent.RELAYING, { groupKey: CF_TX_GROUP_KEY, taskId, safeAddress }) onSubmit() } else { diff --git a/src/features/counterfactual/CounterfactualHooks.tsx b/src/features/counterfactual/CounterfactualHooks.tsx index 0eae26fa8e..d6be915487 100644 --- a/src/features/counterfactual/CounterfactualHooks.tsx +++ b/src/features/counterfactual/CounterfactualHooks.tsx @@ -1,16 +1,13 @@ import CounterfactualSuccessScreen from '@/features/counterfactual/CounterfactualSuccessScreen' import dynamic from 'next/dynamic' -import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' const LazyCounterfactual = dynamic(() => import('./LazyCounterfactual')) function CounterfactualHooks() { - const isCounterfactualSafe = useIsCounterfactualSafe() - return ( <> <CounterfactualSuccessScreen /> - {isCounterfactualSafe && <LazyCounterfactual />} + <LazyCounterfactual /> </> ) } diff --git a/src/features/counterfactual/CounterfactualStatusButton.tsx b/src/features/counterfactual/CounterfactualStatusButton.tsx index e4440e38e7..1d09f7aa11 100644 --- a/src/features/counterfactual/CounterfactualStatusButton.tsx +++ b/src/features/counterfactual/CounterfactualStatusButton.tsx @@ -1,4 +1,4 @@ -import { PendingSafeStatus, selectUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' +import { selectUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' import useSafeInfo from '@/hooks/useSafeInfo' import InfoIcon from '@/public/images/notifications/info.svg' import { useAppSelector } from '@/store' @@ -27,30 +27,28 @@ export const LoopIcon = (props: SvgIconProps) => { ) } -const processingStates = [PendingSafeStatus.PROCESSING, PendingSafeStatus.RELAYING] - const CounterfactualStatusButton = () => { const { safe, safeAddress } = useSafeInfo() const undeployedSafe = useAppSelector((state) => selectUndeployedSafe(state, safe.chainId, safeAddress)) if (safe.deployed) return null - const processing = undeployedSafe && processingStates.includes(undeployedSafe.status.status) + const isActivating = undeployedSafe?.status.status !== 'AWAITING_EXECUTION' return ( <Tooltip placement="right" - title={processing ? 'Safe Account is being activated' : 'Safe Account is not activated'} + title={isActivating ? 'Safe Account is being activated' : 'Safe Account is not activated'} arrow > <IconButton data-testid="pending-activation-icon" - className={classnames(css.statusButton, { [css.processing]: processing })} + className={classnames(css.statusButton, { [css.processing]: isActivating })} size="small" - color={processing ? 'info' : 'warning'} + color={isActivating ? 'info' : 'warning'} disableRipple > - {processing ? <LoopIcon /> : <InfoIcon />} + {isActivating ? <LoopIcon /> : <InfoIcon />} </IconButton> </Tooltip> ) diff --git a/src/features/counterfactual/CounterfactualSuccessScreen.tsx b/src/features/counterfactual/CounterfactualSuccessScreen.tsx index bd5312f0de..f798e5b99e 100644 --- a/src/features/counterfactual/CounterfactualSuccessScreen.tsx +++ b/src/features/counterfactual/CounterfactualSuccessScreen.tsx @@ -1,16 +1,23 @@ +import EthHashInfo from '@/components/common/EthHashInfo' import { safeCreationPendingStatuses } from '@/features/counterfactual/hooks/usePendingSafeStatuses' import { SafeCreationEvent, safeCreationSubscribe } from '@/features/counterfactual/services/safeCreationEvents' +import { useCurrentChain } from '@/hooks/useChains' import { useEffect, useState } from 'react' import { Box, Button, Dialog, DialogContent, Typography } from '@mui/material' import CheckRoundedIcon from '@mui/icons-material/CheckRounded' const CounterfactualSuccessScreen = () => { const [open, setOpen] = useState<boolean>(false) + const [safeAddress, setSafeAddress] = useState<string>() + const chain = useCurrentChain() useEffect(() => { const unsubFns = Object.entries(safeCreationPendingStatuses).map(([event]) => - safeCreationSubscribe(event as SafeCreationEvent, async () => { - if (event === SafeCreationEvent.INDEXED) setOpen(true) + safeCreationSubscribe(event as SafeCreationEvent, async (detail) => { + if (event === SafeCreationEvent.INDEXED) { + setSafeAddress(detail.safeAddress) + setOpen(true) + } }), ) @@ -42,17 +49,23 @@ const CounterfactualSuccessScreen = () => { > <CheckRoundedIcon sx={{ width: 50, height: 50 }} color="success" /> </Box> + <Box textAlign="center"> <Typography variant="h3" fontWeight="bold" mb={1}> - Account is activated! - </Typography> - <Typography> - Your Safe Account was successfully deployed on chain. You can continue making improvements to your account - setup and security. + Your account is all set! </Typography> + <Typography>Start your journey to the smart account security now.</Typography> + <Typography>Use your address to receive funds {chain?.chainName && `on ${chain.chainName}`}.</Typography> </Box> + + {safeAddress && ( + <Box p={2} bgcolor="background.main" borderRadius={1} fontSize={14}> + <EthHashInfo address={safeAddress} shortAddress={false} showCopyButton avatarSize={32} /> + </Box> + )} + <Button variant="contained" onClick={() => setOpen(false)}> - Continue + Let's go </Button> </DialogContent> </Dialog> diff --git a/src/features/counterfactual/hooks/usePendingSafeStatuses.ts b/src/features/counterfactual/hooks/usePendingSafeStatuses.ts index 783dc89244..208a8f0cea 100644 --- a/src/features/counterfactual/hooks/usePendingSafeStatuses.ts +++ b/src/features/counterfactual/hooks/usePendingSafeStatuses.ts @@ -7,7 +7,7 @@ import { import { PendingSafeStatus, removeUndeployedSafe, - selectUndeployedSafe, + selectUndeployedSafes, updateUndeployedSafeStatus, } from '@/features/counterfactual/store/undeployedSafesSlice' import { checkSafeActionViaRelay, checkSafeActivation } from '@/features/counterfactual/utils' @@ -16,7 +16,7 @@ import useSafeInfo from '@/hooks/useSafeInfo' import { isSmartContract, useWeb3ReadOnly } from '@/hooks/wallets/web3' import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' import { useAppDispatch, useAppSelector } from '@/store' -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' export const safeCreationPendingStatuses: Partial<Record<SafeCreationEvent, PendingSafeStatus | null>> = { [SafeCreationEvent.PROCESSING]: PendingSafeStatus.PROCESSING, @@ -28,9 +28,7 @@ export const safeCreationPendingStatuses: Partial<Record<SafeCreationEvent, Pend } const usePendingSafeMonitor = (): void => { - const chainId = useChainId() - const { safeAddress } = useSafeInfo() - const undeployedSafe = useAppSelector((state) => selectUndeployedSafe(state, chainId, safeAddress)) + const undeployedSafesByChain = useAppSelector(selectUndeployedSafes) const provider = useWeb3ReadOnly() const dispatch = useAppDispatch() @@ -39,43 +37,49 @@ const usePendingSafeMonitor = (): void => { // Monitor pending safe creation mining/validating progress useEffect(() => { - if (undeployedSafe?.status.status === PendingSafeStatus.AWAITING_EXECUTION) { - monitoredSafes.current[safeAddress] = false - } + Object.entries(undeployedSafesByChain).forEach(([chainId, undeployedSafes]) => { + Object.entries(undeployedSafes).forEach(([safeAddress, undeployedSafe]) => { + if (undeployedSafe?.status.status === PendingSafeStatus.AWAITING_EXECUTION) { + monitoredSafes.current[safeAddress] = false + } - if (!provider || !undeployedSafe || undeployedSafe.status.status === PendingSafeStatus.AWAITING_EXECUTION) { - return - } + if (!provider || !undeployedSafe || undeployedSafe.status.status === PendingSafeStatus.AWAITING_EXECUTION) { + return + } - const monitorPendingSafe = async () => { - const { - status: { status, txHash, taskId, startBlock }, - } = undeployedSafe + const monitorPendingSafe = async () => { + const { + status: { status, txHash, taskId, startBlock }, + } = undeployedSafe - const isProcessing = status === PendingSafeStatus.PROCESSING && txHash !== undefined - const isRelaying = status === PendingSafeStatus.RELAYING && taskId !== undefined - const isMonitored = monitoredSafes.current[safeAddress] + const isProcessing = status === PendingSafeStatus.PROCESSING && txHash !== undefined + const isRelaying = status === PendingSafeStatus.RELAYING && taskId !== undefined + const isMonitored = monitoredSafes.current[safeAddress] - if ((!isProcessing && !isRelaying) || isMonitored) return + if ((!isProcessing && !isRelaying) || isMonitored) return - monitoredSafes.current[safeAddress] = true + monitoredSafes.current[safeAddress] = true - if (isProcessing) { - checkSafeActivation(provider, txHash, safeAddress, startBlock) - } + if (isProcessing) { + checkSafeActivation(provider, txHash, safeAddress, startBlock) + } - if (isRelaying) { - checkSafeActionViaRelay(taskId, safeAddress) - } - } + if (isRelaying) { + checkSafeActionViaRelay(taskId, safeAddress) + } + } - monitorPendingSafe() - }, [dispatch, provider, safeAddress, undeployedSafe]) + monitorPendingSafe() + }) + }) + }, [dispatch, provider, undeployedSafesByChain]) } const usePendingSafeStatus = (): void => { + const [safeAddress, setSafeAddress] = useState<string>('') const dispatch = useAppDispatch() - const { safe, safeAddress } = useSafeInfo() + const { safe } = useSafeInfo() + const chainId = useChainId() const provider = useWeb3ReadOnly() usePendingSafeMonitor() @@ -103,25 +107,35 @@ const usePendingSafeStatus = (): void => { useEffect(() => { const unsubFns = Object.entries(safeCreationPendingStatuses).map(([event, status]) => safeCreationSubscribe(event as SafeCreationEvent, async (detail) => { + setSafeAddress(detail.safeAddress) + if (event === SafeCreationEvent.SUCCESS) { // TODO: Possible to add a label with_tx, without_tx? trackEvent(CREATE_SAFE_EVENTS.ACTIVATED_SAFE) - pollSafeInfo(safe.chainId, safeAddress).finally(() => { - safeCreationDispatch(SafeCreationEvent.INDEXED, { groupKey: detail.groupKey, safeAddress }) + pollSafeInfo(chainId, detail.safeAddress).finally(() => { + safeCreationDispatch(SafeCreationEvent.INDEXED, { + groupKey: detail.groupKey, + safeAddress: detail.safeAddress, + }) }) return } if (event === SafeCreationEvent.INDEXED) { - dispatch(removeUndeployedSafe({ chainId: safe.chainId, address: safeAddress })) + dispatch(removeUndeployedSafe({ chainId, address: detail.safeAddress })) } if (status === null) { dispatch( updateUndeployedSafeStatus({ - chainId: safe.chainId, - address: safeAddress, - status: { status: PendingSafeStatus.AWAITING_EXECUTION }, + chainId, + address: detail.safeAddress, + status: { + status: PendingSafeStatus.AWAITING_EXECUTION, + startBlock: undefined, + txHash: undefined, + submittedAt: undefined, + }, }), ) return @@ -129,13 +143,14 @@ const usePendingSafeStatus = (): void => { dispatch( updateUndeployedSafeStatus({ - chainId: safe.chainId, - address: safeAddress, + chainId, + address: detail.safeAddress, status: { status, txHash: 'txHash' in detail ? detail.txHash : undefined, taskId: 'taskId' in detail ? detail.taskId : undefined, startBlock: await provider?.getBlockNumber(), + submittedAt: Date.now(), }, }), ) @@ -145,7 +160,7 @@ const usePendingSafeStatus = (): void => { return () => { unsubFns.forEach((unsub) => unsub()) } - }, [safe.chainId, dispatch, safeAddress, provider]) + }, [chainId, dispatch, provider]) } export default usePendingSafeStatus diff --git a/src/features/counterfactual/services/safeCreationEvents.ts b/src/features/counterfactual/services/safeCreationEvents.ts index 29e92c40e1..883f601562 100644 --- a/src/features/counterfactual/services/safeCreationEvents.ts +++ b/src/features/counterfactual/services/safeCreationEvents.ts @@ -13,10 +13,12 @@ export interface SafeCreationEvents { [SafeCreationEvent.PROCESSING]: { groupKey: string txHash: string + safeAddress: string } [SafeCreationEvent.RELAYING]: { groupKey: string taskId: string + safeAddress: string } [SafeCreationEvent.SUCCESS]: { groupKey: string @@ -29,10 +31,12 @@ export interface SafeCreationEvents { [SafeCreationEvent.FAILED]: { groupKey: string error: Error + safeAddress: string } [SafeCreationEvent.REVERTED]: { groupKey: string error: Error + safeAddress: string } } diff --git a/src/features/counterfactual/store/undeployedSafesSlice.ts b/src/features/counterfactual/store/undeployedSafesSlice.ts index 40b1b6bdb1..0196052e95 100644 --- a/src/features/counterfactual/store/undeployedSafesSlice.ts +++ b/src/features/counterfactual/store/undeployedSafesSlice.ts @@ -1,3 +1,4 @@ +import type { PayMethod } from '@/features/counterfactual/PayNowPayLater' import { type RootState } from '@/store' import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit' import type { PredictedSafeProps } from '@safe-global/protocol-kit' @@ -10,9 +11,13 @@ export enum PendingSafeStatus { type UndeployedSafeStatus = { status: PendingSafeStatus + type: PayMethod txHash?: string taskId?: string startBlock?: number + submittedAt?: number + signerAddress?: string + signerNonce?: number | null } export type UndeployedSafe = { @@ -32,9 +37,9 @@ export const undeployedSafesSlice = createSlice({ reducers: { addUndeployedSafe: ( state, - action: PayloadAction<{ chainId: string; address: string; safeProps: PredictedSafeProps }>, + action: PayloadAction<{ chainId: string; address: string; type: PayMethod; safeProps: PredictedSafeProps }>, ) => { - const { chainId, address, safeProps } = action.payload + const { chainId, address, type, safeProps } = action.payload if (!state[chainId]) { state[chainId] = {} @@ -44,13 +49,14 @@ export const undeployedSafesSlice = createSlice({ props: safeProps, status: { status: PendingSafeStatus.AWAITING_EXECUTION, + type, }, } }, updateUndeployedSafeStatus: ( state, - action: PayloadAction<{ chainId: string; address: string; status: UndeployedSafeStatus }>, + action: PayloadAction<{ chainId: string; address: string; status: Omit<UndeployedSafeStatus, 'type'> }>, ) => { const { chainId, address, status } = action.payload @@ -58,7 +64,10 @@ export const undeployedSafesSlice = createSlice({ state[chainId][address] = { props: state[chainId][address].props, - status, + status: { + ...state[chainId][address].status, + ...status, + }, } }, diff --git a/src/features/counterfactual/utils.ts b/src/features/counterfactual/utils.ts index 5812ba6eed..8b33717818 100644 --- a/src/features/counterfactual/utils.ts +++ b/src/features/counterfactual/utils.ts @@ -1,7 +1,7 @@ import type { NewSafeFormData } from '@/components/new-safe/create' -import { CREATION_MODAL_QUERY_PARM } from '@/components/new-safe/create/logic' import { LATEST_SAFE_VERSION, POLLING_INTERVAL } from '@/config/constants' import { AppRoutes } from '@/config/routes' +import { PayMethod } from '@/features/counterfactual/PayNowPayLater' import { safeCreationDispatch, SafeCreationEvent } from '@/features/counterfactual/services/safeCreationEvents' import { addUndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' @@ -68,11 +68,12 @@ export const dispatchTxExecutionAndDeploySafe = async ( // @ts-ignore TODO: Check why TransactionResponse type doesn't work result = await signer.sendTransaction({ ...deploymentTx, gasLimit: gas }) } catch (error) { - safeCreationDispatch(SafeCreationEvent.FAILED, { ...eventParams, error: asError(error) }) + safeCreationDispatch(SafeCreationEvent.FAILED, { ...eventParams, error: asError(error), safeAddress: '' }) throw error } - safeCreationDispatch(SafeCreationEvent.PROCESSING, { ...eventParams, txHash: result!.hash }) + // TODO: Probably need to pass the actual safe address + safeCreationDispatch(SafeCreationEvent.PROCESSING, { ...eventParams, txHash: result!.hash, safeAddress: '' }) return result!.hash } @@ -142,6 +143,7 @@ export const createCounterfactualSafe = ( const undeployedSafe = { chainId: chain.chainId, address: safeAddress, + type: PayMethod.PayLater, safeProps: { safeAccountConfig: props.safeAccountConfig, safeDeploymentConfig: { @@ -169,7 +171,7 @@ export const createCounterfactualSafe = ( ) return router.push({ pathname: AppRoutes.home, - query: { safe: `${chain.shortName}:${safeAddress}`, [CREATION_MODAL_QUERY_PARM]: true }, + query: { safe: `${chain.shortName}:${safeAddress}` }, }) } @@ -197,20 +199,17 @@ async function retryGetTransaction(provider: Provider, txHash: string, maxAttemp throw new Error('Transaction not found') } -// TODO: Reuse this for safe creation flow instead of checkSafeCreationTx export const checkSafeActivation = async ( provider: Provider, txHash: string, safeAddress: string, startBlock?: number, ) => { - const TIMEOUT_TIME = 2 * 60 * 1000 // 2 minutes - try { const txResponse = await retryGetTransaction(provider, txHash) const replaceableTx = startBlock ? txResponse.replaceableTransaction(startBlock) : txResponse - const receipt = await replaceableTx?.wait(1, TIMEOUT_TIME) + const receipt = await replaceableTx?.wait(1) /** The receipt should always be non-null as we require 1 confirmation */ if (receipt === null) { @@ -221,6 +220,7 @@ export const checkSafeActivation = async ( safeCreationDispatch(SafeCreationEvent.REVERTED, { groupKey: CF_TX_GROUP_KEY, error: new Error('Transaction reverted'), + safeAddress, }) } @@ -239,14 +239,23 @@ export const checkSafeActivation = async ( return } + if (didRevert(_err.receipt)) { + safeCreationDispatch(SafeCreationEvent.REVERTED, { + groupKey: CF_TX_GROUP_KEY, + error: new Error('Transaction reverted'), + safeAddress, + }) + return + } + safeCreationDispatch(SafeCreationEvent.FAILED, { groupKey: CF_TX_GROUP_KEY, error: _err, + safeAddress, }) } } -// TODO: Reuse this for safe creation flow instead of waitForCreateSafeTx export const checkSafeActionViaRelay = (taskId: string, safeAddress: string) => { const TIMEOUT_TIME = 2 * 60 * 1000 // 2 minutes @@ -273,6 +282,7 @@ export const checkSafeActionViaRelay = (taskId: string, safeAddress: string) => safeCreationDispatch(SafeCreationEvent.FAILED, { groupKey: CF_TX_GROUP_KEY, error: new Error('Transaction failed'), + safeAddress, }) break default: @@ -288,6 +298,7 @@ export const checkSafeActionViaRelay = (taskId: string, safeAddress: string) => safeCreationDispatch(SafeCreationEvent.FAILED, { groupKey: CF_TX_GROUP_KEY, error: new Error('Transaction failed'), + safeAddress, }) clearInterval(intervalId) diff --git a/src/hooks/coreSDK/safeCoreSDK.ts b/src/hooks/coreSDK/safeCoreSDK.ts index f5fb90c761..eeaaaf5638 100644 --- a/src/hooks/coreSDK/safeCoreSDK.ts +++ b/src/hooks/coreSDK/safeCoreSDK.ts @@ -1,6 +1,7 @@ import chains from '@/config/chains' import type { UndeployedSafe } from '@/features/counterfactual/store/undeployedSafesSlice' import { getWeb3ReadOnly } from '@/hooks/wallets/web3' +import { UncheckedJsonRpcSigner } from '@/utils/providers/UncheckedJsonRpcSigner' import { getSafeSingletonDeployment, getSafeL2SingletonDeployment } from '@safe-global/safe-deployments' import ExternalStore from '@/services/ExternalStore' import { Gnosis_safe__factory } from '@/types/contracts' @@ -30,7 +31,7 @@ export function assertValidSafeVersion<T extends SafeInfo['version']>(safeVersio } export const createEthersAdapter = async (provider: BrowserProvider) => { - const signer = await provider.getSigner(0) + const signer = new UncheckedJsonRpcSigner(provider, (await provider.getSigner()).address) return new EthersAdapter({ ethers, signerOrProvider: signer, diff --git a/src/services/analytics/events/createLoadSafe.ts b/src/services/analytics/events/createLoadSafe.ts index 32e860e2a8..991c84d294 100644 --- a/src/services/analytics/events/createLoadSafe.ts +++ b/src/services/analytics/events/createLoadSafe.ts @@ -61,10 +61,6 @@ export const CREATE_SAFE_EVENTS = { action: 'Activated Safe', category: CREATE_SAFE_CATEGORY, }, - GET_STARTED: { - action: 'Load Safe', - category: CREATE_SAFE_CATEGORY, - }, OPEN_HINT: { action: 'Open Hint', category: CREATE_SAFE_CATEGORY, diff --git a/src/services/tx/__tests__/txMonitor.test.ts b/src/services/tx/__tests__/txMonitor.test.ts index c87a161c58..cc46409c63 100644 --- a/src/services/tx/__tests__/txMonitor.test.ts +++ b/src/services/tx/__tests__/txMonitor.test.ts @@ -3,14 +3,13 @@ import * as txEvents from '@/services/tx/txEvents' import * as txMonitor from '@/services/tx/txMonitor' import { act } from '@testing-library/react' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' import { toBeHex } from 'ethers' import { MockEip1193Provider } from '@/tests/mocks/providers' import { BrowserProvider, type JsonRpcProvider, type TransactionReceipt } from 'ethers' import { faker } from '@faker-js/faker' import { SimpleTxWatcher } from '@/utils/SimpleTxWatcher' -const { waitForTx, waitForRelayedTx, waitForCreateSafeTx } = txMonitor +const { waitForTx, waitForRelayedTx } = txMonitor const provider = new BrowserProvider(MockEip1193Provider) as unknown as JsonRpcProvider @@ -243,134 +242,6 @@ describe('txMonitor', () => { }) }) }) - - describe('waitForCreateSafeTx', () => { - it("sets the status to SUCCESS if taskStatus 'ExecSuccess'", async () => { - const mockData = { - task: { - taskState: 'ExecSuccess', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(15_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalledTimes(1) - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.SUCCESS) - }) - - it("sets the status to ERROR if taskStatus 'ExecReverted'", async () => { - const mockData = { - task: { - taskState: 'ExecReverted', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(15_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalledTimes(1) - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.ERROR) - }) - - it("sets the status to ERROR if taskStatus 'Blacklisted'", async () => { - const mockData = { - task: { - taskState: 'Blacklisted', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(15_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalledTimes(1) - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.ERROR) - }) - - it("sets the status to ERROR if taskStatus 'Cancelled'", async () => { - const mockData = { - task: { - taskState: 'Cancelled', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(15_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalledTimes(1) - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.ERROR) - }) - - it("sets the status to ERROR if taskStatus 'NotFound'", async () => { - const mockData = { - task: { - taskState: 'NotFound', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(15_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalledTimes(1) - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.ERROR) - }) - - it('sets the status to ERROR if the tx relaying timed out', async () => { - const mockData = { - task: { - taskState: 'WaitingForConfirmation', - }, - } - global.fetch = jest.fn().mockImplementation(setupFetchStub(mockData)) - - const mockFetch = jest.spyOn(global, 'fetch') - const setStatusSpy = jest.fn() - - waitForCreateSafeTx('0x1', setStatusSpy) - - await act(() => { - jest.advanceTimersByTime(3 * 60_000 + 1) - }) - - expect(mockFetch).toHaveBeenCalled() - expect(setStatusSpy).toHaveBeenCalledWith(SafeCreationStatus.ERROR) - }) - }) }) describe('getRemainingTimeout', () => { diff --git a/src/services/tx/txMonitor.ts b/src/services/tx/txMonitor.ts index f0dcc0d5d2..2931808128 100644 --- a/src/services/tx/txMonitor.ts +++ b/src/services/tx/txMonitor.ts @@ -4,7 +4,6 @@ import { txDispatch, TxEvent } from '@/services/tx/txEvents' import { POLLING_INTERVAL } from '@/config/constants' import { Errors, logError } from '@/services/exceptions' -import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' import { asError } from '../exceptions/utils' import { type JsonRpcProvider, type TransactionReceipt } from 'ethers' import { SimpleTxWatcher } from '@/utils/SimpleTxWatcher' @@ -205,41 +204,3 @@ export const waitForRelayedTx = (taskId: string, txIds: string[], safeAddress: s clearInterval(intervalId) }, WAIT_FOR_RELAY_TIMEOUT) } - -export const waitForCreateSafeTx = (taskId: string, setStatus: (value: SafeCreationStatus) => void): void => { - let intervalId: NodeJS.Timeout - let failAfterTimeoutId: NodeJS.Timeout - - intervalId = setInterval(async () => { - const status = await getRelayTxStatus(taskId) - - // 404 - if (!status) { - return - } - - switch (status.task.taskState) { - case TaskState.ExecSuccess: - setStatus(SafeCreationStatus.SUCCESS) - break - case TaskState.ExecReverted: - case TaskState.Blacklisted: - case TaskState.Cancelled: - case TaskState.NotFound: - setStatus(SafeCreationStatus.ERROR) - break - default: - // Don't clear interval as we're still waiting for the tx to be relayed - return - } - - clearTimeout(failAfterTimeoutId) - clearInterval(intervalId) - }, POLLING_INTERVAL) - - failAfterTimeoutId = setTimeout(() => { - setStatus(SafeCreationStatus.ERROR) - - clearInterval(intervalId) - }, WAIT_FOR_RELAY_TIMEOUT) -} From 866da04ec214342741d7c04349aa63f29c167379 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov <daniel.d@safe.global> Date: Wed, 26 Jun 2024 09:17:20 +0200 Subject: [PATCH 105/154] Feat: TWAP order decoding in tx history/queue [SW-1] (#3861) * fix: <p> cannot appear as a descendant of <p> * feat: add decoding for twaps txs in history tab * chore: update @safe-global/safe-gateway-typescript-sdk * fix: lint errors * chore: prettier * fix: typo * refactor: move swap components to swap feature folder * refactor: use null instead of <></> * refactor: clean up * refactor: use more abstract naming for function names --- package.json | 2 +- src/components/common/Table/EmptyRow.tsx | 6 + src/components/common/Table/styles.module.css | 10 + .../transactions/TxDetails/TxData/index.tsx | 8 +- .../transactions/TxDetails/index.tsx | 10 +- src/components/transactions/TxInfo/index.tsx | 35 +- .../transactions/TxStatusLabel/index.tsx | 4 +- .../tx-flow/flows/ConfirmTx/index.tsx | 4 +- .../swap/components/SwapOrder/index.tsx | 304 +++++++++++++----- .../{index.stories.tsx => swap.stories.tsx} | 2 - .../components/SwapOrder/twap.stories.tsx | 73 +++++ .../swap/components/SwapProgress/index.tsx | 4 +- .../swap/components/SwapTxInfo/SwapTx.tsx | 50 +++ .../{index.tsx => interactWith.tsx} | 7 +- src/features/swap/helpers/swapOrderBuilder.ts | 41 ++- src/features/swap/helpers/utils.ts | 2 +- src/features/swap/hooks/useIsExpiredSwap.ts | 4 +- src/features/swap/index.tsx | 15 +- src/features/swap/store/swapParamsSlice.ts | 1 + src/hooks/useTransactionStatus.ts | 4 +- src/hooks/useTransactionType.ts | 6 + src/services/analytics/tx-tracking.ts | 4 +- src/store/swapOrderSlice.ts | 4 +- src/tests/mocks/chains.ts | 48 +++ src/tests/mocks/transactions.ts | 1 + src/utils/transaction-guards.ts | 20 +- yarn.lock | 8 +- 27 files changed, 517 insertions(+), 160 deletions(-) create mode 100644 src/components/common/Table/EmptyRow.tsx rename src/features/swap/components/SwapOrder/{index.stories.tsx => swap.stories.tsx} (99%) create mode 100644 src/features/swap/components/SwapOrder/twap.stories.tsx create mode 100644 src/features/swap/components/SwapTxInfo/SwapTx.tsx rename src/features/swap/components/SwapTxInfo/{index.tsx => interactWith.tsx} (85%) diff --git a/package.json b/package.json index f324c18639..27c9d3e8cb 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@safe-global/protocol-kit": "^3.1.1", "@safe-global/safe-apps-sdk": "^9.1.0", "@safe-global/safe-deployments": "^1.36.0", - "@safe-global/safe-gateway-typescript-sdk": "3.21.2", + "@safe-global/safe-gateway-typescript-sdk": "^3.21.5", "@safe-global/safe-modules-deployments": "^1.2.0", "@sentry/react": "^7.91.0", "@spindl-xyz/attribution-lite": "^1.4.0", diff --git a/src/components/common/Table/EmptyRow.tsx b/src/components/common/Table/EmptyRow.tsx new file mode 100644 index 0000000000..ecd1986097 --- /dev/null +++ b/src/components/common/Table/EmptyRow.tsx @@ -0,0 +1,6 @@ +import type { ReactElement } from 'react' +import css from './styles.module.css' + +export const EmptyRow = (): ReactElement | null => { + return <div className={css.gridEmptyRow}></div> +} diff --git a/src/components/common/Table/styles.module.css b/src/components/common/Table/styles.module.css index 91bad958c2..e2407529f6 100644 --- a/src/components/common/Table/styles.module.css +++ b/src/components/common/Table/styles.module.css @@ -6,6 +6,16 @@ max-width: 900px; } +.gridEmptyRow { + display: grid; + grid-template-columns: 35% auto; + gap: var(--space-1); + justify-content: flex-start; + max-width: 900px; + margin-top: var(--space-1); + margin-bottom: var(--space-1); + border-top: 1px solid var(--color-border-light); +} .title { color: var(--color-primary-light); font-weight: 400; diff --git a/src/components/transactions/TxDetails/TxData/index.tsx b/src/components/transactions/TxDetails/TxData/index.tsx index 80e10048f5..53d1064350 100644 --- a/src/components/transactions/TxDetails/TxData/index.tsx +++ b/src/components/transactions/TxDetails/TxData/index.tsx @@ -1,5 +1,4 @@ import SettingsChangeTxInfo from '@/components/transactions/TxDetails/TxData/SettingsChange' -import SwapTxInfo from '@/features/swap/components/SwapTxInfo' import type { SpendingLimitMethods } from '@/utils/transaction-guards' import { isCancellationTxInfo, @@ -9,7 +8,7 @@ import { isSettingsChangeTxInfo, isSpendingLimitMethod, isSupportedSpendingLimitAddress, - isSwapTxInfo, + isSwapOrderTxInfo, isTransferTxInfo, } from '@/utils/transaction-guards' import { SpendingLimits } from '@/components/transactions/TxDetails/TxData/SpendingLimits' @@ -20,6 +19,7 @@ import DecodedData from '@/components/transactions/TxDetails/TxData/DecodedData' import TransferTxInfo from '@/components/transactions/TxDetails/TxData/Transfer' import useChainId from '@/hooks/useChainId' import { MultiSendTxInfo } from '@/components/transactions/TxDetails/TxData/MultiSendTxInfo' +import InteractWith from '@/features/swap/components/SwapTxInfo/interactWith' const TxData = ({ txDetails, trusted }: { txDetails: TransactionDetails; trusted: boolean }): ReactElement => { const chainId = useChainId() @@ -46,8 +46,8 @@ const TxData = ({ txDetails, trusted }: { txDetails: TransactionDetails; trusted return <SpendingLimits txData={txDetails.txData} txInfo={txInfo} type={method} /> } - if (isSwapTxInfo(txInfo)) { - return <SwapTxInfo txData={txDetails.txData} /> + if (isSwapOrderTxInfo(txInfo)) { + return <InteractWith txData={txDetails.txData} /> } return <DecodedData txData={txDetails.txData} txInfo={txInfo} /> diff --git a/src/components/transactions/TxDetails/index.tsx b/src/components/transactions/TxDetails/index.tsx index 3305e3af40..57579acd71 100644 --- a/src/components/transactions/TxDetails/index.tsx +++ b/src/components/transactions/TxDetails/index.tsx @@ -13,12 +13,12 @@ import useChainId from '@/hooks/useChainId' import useAsync from '@/hooks/useAsync' import { isAwaitingExecution, + isOrderTxInfo, isModuleExecutionInfo, isMultiSendTxInfo, isMultisigDetailedExecutionInfo, isMultisigExecutionInfo, - isOpenSwap, - isSwapTxInfo, + isOpenSwapOrder, isTxQueued, } from '@/utils/transaction-guards' import { InfoDetails } from '@/components/transactions/InfoDetails' @@ -73,7 +73,7 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement <> {/* /Details */} <div className={`${css.details} ${isUnsigned ? css.noSigners : ''}`}> - {isSwapTxInfo(txDetails.txInfo) && ( + {isOrderTxInfo(txDetails.txInfo) && ( <div className={css.swapOrder}> <ErrorBoundary fallback={<div>Error parsing data</div>}> <SwapOrder txData={txDetails.txData} txInfo={txDetails.txInfo} /> @@ -116,7 +116,7 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement <Summary txDetails={txDetails} /> </div> - {(isMultiSendTxInfo(txDetails.txInfo) || isSwapTxInfo(txDetails.txInfo)) && ( + {(isMultiSendTxInfo(txDetails.txInfo) || isOrderTxInfo(txDetails.txInfo)) && ( <div className={css.multiSend}> <ErrorBoundary fallback={<div>Error parsing data</div>}> <Multisend txData={txDetails.txData} /> @@ -159,7 +159,7 @@ const TxDetails = ({ const { safe } = useSafeInfo() const [pollCount] = useIntervalCounter(POLLING_INTERVAL) - const swapPollCount = isOpenSwap(txSummary.txInfo) ? pollCount : 0 + const swapPollCount = isOpenSwapOrder(txSummary.txInfo) ? pollCount : 0 const [txDetailsData, error, loading] = useAsync<TransactionDetails>( async () => { diff --git a/src/components/transactions/TxInfo/index.tsx b/src/components/transactions/TxInfo/index.tsx index fde6d39a46..2556365262 100644 --- a/src/components/transactions/TxInfo/index.tsx +++ b/src/components/transactions/TxInfo/index.tsx @@ -1,16 +1,16 @@ import { type ReactElement } from 'react' import type { - Transfer, - Custom, Creation, - TransactionInfo, + Custom, MultiSend, SettingsChange, - SwapOrder, + TransactionInfo, + Transfer, } from '@safe-global/safe-gateway-typescript-sdk' import { SettingsInfoType } from '@safe-global/safe-gateway-typescript-sdk' import TokenAmount from '@/components/common/TokenAmount' import { + isOrderTxInfo, isCreationTxInfo, isCustomTxInfo, isERC20Transfer, @@ -18,13 +18,11 @@ import { isMultiSendTxInfo, isNativeTokenTransfer, isSettingsChangeTxInfo, - isSwapTxInfo, isTransferTxInfo, } from '@/utils/transaction-guards' import { ellipsis, shortenAddress } from '@/utils/formatters' import { useCurrentChain } from '@/hooks/useChains' -import TokenIcon from '@/components/common/TokenIcon' -import { Box, Typography } from '@mui/material' +import { SwapTx } from '@/features/swap/components/SwapTxInfo/SwapTx' export const TransferTx = ({ info, @@ -101,27 +99,6 @@ const MultiSendTx = ({ info }: { info: MultiSend }): ReactElement => { ) } -const SwapTx = ({ info }: { info: SwapOrder }): ReactElement => { - return ( - <Box display="flex"> - <Typography display="flex" alignItems="center" fontWeight="bold"> - <Box style={{ paddingRight: 5, display: 'inline-block' }}> - <TokenIcon logoUri={info.sellToken.logoUri || undefined} tokenSymbol={info.sellToken.symbol} /> - </Box> - <Typography sx={{ maxWidth: '60px' }} noWrap> - {info.sellToken.symbol}  - </Typography> - to - <Box style={{ paddingLeft: 5, paddingRight: 5, display: 'inline-block' }}> - <TokenIcon logoUri={info.buyToken.logoUri || undefined} tokenSymbol={info.buyToken.symbol} /> - </Box>{' '} - <Typography sx={{ maxWidth: '60px' }} noWrap> - {info.buyToken.symbol} - </Typography> - </Typography> - </Box> - ) -} const SettingsChangeTx = ({ info }: { info: SettingsChange }): ReactElement => { if ( info.settingsInfo?.type === SettingsInfoType.ENABLE_MODULE || @@ -154,7 +131,7 @@ const TxInfo = ({ info, ...rest }: { info: TransactionInfo; omitSign?: boolean; return <CreationTx info={info} /> } - if (isSwapTxInfo(info)) { + if (isOrderTxInfo(info)) { return <SwapTx info={info} /> } diff --git a/src/components/transactions/TxStatusLabel/index.tsx b/src/components/transactions/TxStatusLabel/index.tsx index 60122905b8..adb4abbda4 100644 --- a/src/components/transactions/TxStatusLabel/index.tsx +++ b/src/components/transactions/TxStatusLabel/index.tsx @@ -1,11 +1,11 @@ -import { isCancelledSwap } from '@/utils/transaction-guards' +import { isCancelledSwapOrder } from '@/utils/transaction-guards' import { CircularProgress, type Palette, Typography } from '@mui/material' import { TransactionStatus, type TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk' import useIsPending from '@/hooks/useIsPending' import useTransactionStatus from '@/hooks/useTransactionStatus' const getStatusColor = (tx: TransactionSummary, palette: Palette) => { - if (isCancelledSwap(tx.txInfo)) { + if (isCancelledSwapOrder(tx.txInfo)) { return palette.error.main } diff --git a/src/components/tx-flow/flows/ConfirmTx/index.tsx b/src/components/tx-flow/flows/ConfirmTx/index.tsx index 00cbf2cfe2..aad926a8c9 100644 --- a/src/components/tx-flow/flows/ConfirmTx/index.tsx +++ b/src/components/tx-flow/flows/ConfirmTx/index.tsx @@ -1,4 +1,4 @@ -import { isSwapTxInfo } from '@/utils/transaction-guards' +import { isSwapOrderTxInfo } from '@/utils/transaction-guards' import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk' import TxLayout from '@/components/tx-flow/common/TxLayout' import ConfirmProposedTx from './ConfirmProposedTx' @@ -8,7 +8,7 @@ import SwapIcon from '@/public/images/common/swap.svg' const ConfirmTxFlow = ({ txSummary }: { txSummary: TransactionSummary }) => { const { text } = useTransactionType(txSummary) - const isSwapOrder = isSwapTxInfo(txSummary.txInfo) + const isSwapOrder = isSwapOrderTxInfo(txSummary.txInfo) return ( <TxLayout diff --git a/src/features/swap/components/SwapOrder/index.tsx b/src/features/swap/components/SwapOrder/index.tsx index 2dead8a80e..ff5e4aa173 100644 --- a/src/features/swap/components/SwapOrder/index.tsx +++ b/src/features/swap/components/SwapOrder/index.tsx @@ -6,10 +6,15 @@ import { capitalize } from '@/hooks/useMnemonicName' import { formatDateTime, formatTimeInWords } from '@/utils/date' import Stack from '@mui/material/Stack' import type { ReactElement } from 'react' -import { type SwapOrder as SwapOrderType, type TransactionData } from '@safe-global/safe-gateway-typescript-sdk' +import type { TwapOrder as SwapTwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import { + type SwapOrder as SwapOrderType, + type Order, + type TransactionData, + TransactionInfoType, +} from '@safe-global/safe-gateway-typescript-sdk' import { DataRow } from '@/components/common/Table/DataRow' import { DataTable } from '@/components/common/Table/DataTable' -import { HexEncodedData } from '@/components/transactions/HexEncodedData' import { compareAsc } from 'date-fns' import css from './styles.module.css' import { Typography } from '@mui/material' @@ -25,103 +30,243 @@ import { } from '@/features/swap/helpers/utils' import EthHashInfo from '@/components/common/EthHashInfo' import useSafeInfo from '@/hooks/useSafeInfo' +import { isSwapOrderTxInfo, isTwapOrderTxInfo } from '@/utils/transaction-guards' +import { EmptyRow } from '@/components/common/Table/EmptyRow' type SwapOrderProps = { txData?: TransactionData - txInfo?: SwapOrderType + txInfo?: Order } -export const SellOrder = ({ order }: { order: SwapOrderType }) => { - const { safeAddress } = useSafeInfo() - const { uid, kind, validUntil, status, sellToken, buyToken, sellAmount, buyAmount, explorerUrl, receiver } = order +const AmountRow = ({ order }: { order: Order }) => { + const { sellToken, buyToken, sellAmount, buyAmount, kind } = order + const orderKindLabel = capitalize(kind) + const isSellOrder = kind === 'sell' + return ( + <DataRow key="Amount" title="Amount"> + <Stack flexDirection={isSellOrder ? 'column' : 'column-reverse'}> + <div> + <span className={css.value}> + {isSellOrder ? 'Sell' : 'For at most'}{' '} + {sellToken.logoUri && <TokenIcon logoUri={sellToken.logoUri} size={24} />}{' '} + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(sellAmount, sellToken.decimals)} {sellToken.symbol} + </Typography> + </span> + </div> + <div> + <span className={css.value}> + {isSellOrder ? 'for at least' : 'Buy'}{' '} + {buyToken.logoUri && <TokenIcon logoUri={buyToken.logoUri} size={24} />} + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(buyAmount, buyToken.decimals)} {buyToken.symbol} + </Typography> + </span> + </div> + </Stack> + </DataRow> + ) +} - const isPartiallyFilled = isOrderPartiallyFilled(order) +const PriceRow = ({ order }: { order: Order }) => { + const { status, sellToken, buyToken } = order const executionPrice = getExecutionPrice(order) const limitPrice = getLimitPrice(order) + + if (status === 'fulfilled') { + return ( + <DataRow key="Execution price" title="Execution price"> + 1 {buyToken.symbol} = {formatAmount(executionPrice)} {sellToken.symbol} + </DataRow> + ) + } + + return ( + <DataRow key="Limit price" title="Limit price"> + 1 {buyToken.symbol} = {formatAmount(limitPrice)} {sellToken.symbol} + </DataRow> + ) +} + +const ExpiryRow = ({ order }: { order: Order }) => { + const { validUntil, status } = order + const now = new Date() + const expires = new Date(validUntil * 1000) + if (status! == 'fulfilled') { + if (compareAsc(now, expires) !== 1) { + return ( + <DataRow key="Expiry" title="Expiry"> + <Typography> + <Typography fontWeight={700} component="span"> + {formatTimeInWords(validUntil * 1000)} + </Typography>{' '} + ({formatDateTime(validUntil * 1000)}) + </Typography> + </DataRow> + ) + } else { + return ( + <DataRow key="Expiry" title="Expiry"> + {formatDateTime(validUntil * 1000)} + </DataRow> + ) + } + } + + return null +} + +const SurplusRow = ({ order }: { order: Order }) => { + const { status, kind } = order + const isPartiallyFilled = isOrderPartiallyFilled(order) const surplusPrice = isPartiallyFilled ? getPartiallyFilledSurplus(order) : getSurplusPrice(order) + const { sellToken, buyToken } = order + const isSellOrder = kind === 'sell' + if (status === 'fulfilled' || isPartiallyFilled) { + return ( + <DataRow key="Surplus" title="Surplus"> + {formatAmount(surplusPrice)} {isSellOrder ? buyToken.symbol : sellToken.symbol} + </DataRow> + ) + } + + return null +} + +const FilledRow = ({ order }: { order: Order }) => { + const orderClass = getOrderClass(order) + if (['limit', 'twap'].includes(orderClass)) { + return ( + <DataRow title="Filled" key="Filled"> + <SwapProgress order={order} /> + </DataRow> + ) + } + + return null +} + +const OrderUidRow = ({ order }: { order: Order }) => { + if (order.type === TransactionInfoType.SWAP_ORDER) { + const { uid, explorerUrl } = order + return ( + <DataRow key="Order ID" title="Order ID"> + <OrderId orderId={uid} href={explorerUrl} /> + </DataRow> + ) + } + return null +} + +const StatusRow = ({ order }: { order: Order }) => { + const { status } = order + const isPartiallyFilled = isOrderPartiallyFilled(order) + return ( + <DataRow key="Status" title="Status"> + <StatusLabel status={isPartiallyFilled ? 'partiallyFilled' : status} /> + </DataRow> + ) +} + +const RecipientRow = ({ order }: { order: Order }) => { + const { safeAddress } = useSafeInfo() + const { receiver } = order + + if (receiver && receiver !== safeAddress) { + return ( + <DataRow key="Recipient" title="Recipient"> + <EthHashInfo address={receiver} showAvatar={false} /> + </DataRow> + ) + } + + return null +} + +export const SellOrder = ({ order }: { order: SwapOrderType }) => { + const { kind } = order + const orderKindLabel = capitalize(kind) + + return ( + <DataTable + header={`${orderKindLabel} order`} + rows={[ + <AmountRow order={order} key="amount-row" />, + <PriceRow order={order} key="price-row" />, + <SurplusRow order={order} key="surplus-row" />, + <ExpiryRow order={order} key="expiry-row" />, + <FilledRow order={order} key="filled-row" />, + <OrderUidRow order={order} key="order-uid-row" />, + <StatusRow order={order} key="status-row" />, + <RecipientRow order={order} key="recipient-row" />, + ]} + /> + ) +} + +export const TwapOrder = ({ order }: { order: SwapTwapOrder }) => { + const { + kind, + validUntil, + status, + sellToken, + buyToken, + numberOfParts, + partSellAmount, + minPartLimit, + timeBetweenParts, + } = order + + const isPartiallyFilled = isOrderPartiallyFilled(order) const expires = new Date(validUntil * 1000) const now = new Date() - const orderClass = getOrderClass(order) const orderKindLabel = capitalize(kind) - const isSellOrder = kind === 'sell' return ( <DataTable header={`${orderKindLabel} order`} rows={[ - <DataRow key="Amount" title="Amount"> - <Stack flexDirection={isSellOrder ? 'column' : 'column-reverse'}> - <div> - <span className={css.value}> - {isSellOrder ? 'Sell' : 'For at most'}{' '} - {sellToken.logoUri && <TokenIcon logoUri={sellToken.logoUri} size={24} />}{' '} - {formatVisualAmount(sellAmount, sellToken.decimals)} {sellToken.symbol} - </span> - </div> - <div> - <span className={css.value}> - {isSellOrder ? 'for at least' : 'Buy'}{' '} - {buyToken.logoUri && <TokenIcon logoUri={buyToken.logoUri} size={24} />} - {formatVisualAmount(buyAmount, buyToken.decimals)} {buyToken.symbol} - </span> - </div> - </Stack> + <AmountRow order={order} key="amount-row" />, + <PriceRow order={order} key="price-row" />, + <SurplusRow order={order} key="surplus-row" />, + <RecipientRow order={order} key="recipient-row" />, + <EmptyRow key="spacer-0" />, + <DataRow title="No of parts" key="n_of_parts"> + {numberOfParts} </DataRow>, - status === 'fulfilled' ? ( - <DataRow key="Execution price" title="Execution price"> - 1 {buyToken.symbol} = {formatAmount(executionPrice)} {sellToken.symbol} - </DataRow> - ) : ( - <DataRow key="Limit price" title="Limit price"> - 1 {buyToken.symbol} = {formatAmount(limitPrice)} {sellToken.symbol} - </DataRow> - ), - status === 'fulfilled' || isPartiallyFilled ? ( - <DataRow key="Surplus" title="Surplus"> - {formatAmount(surplusPrice)} {isSellOrder ? buyToken.symbol : sellToken.symbol} + <DataRow title="Sell amount" key="sell_amount_part"> + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(partSellAmount, sellToken.decimals)} {sellToken.symbol} + </Typography> + </DataRow>, + <DataRow title="Buy amount" key="buy_amount_part"> + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(minPartLimit, buyToken.decimals)} {buyToken.symbol} + </Typography> + </DataRow>, + <FilledRow order={order} key="filled-row" />, + <DataRow title="Part duration" key="part_duration"> + {+timeBetweenParts / 60} minutes + </DataRow>, + <EmptyRow key="spacer-1" />, + status !== 'fulfilled' && compareAsc(now, expires) !== 1 ? ( + <DataRow key="Expiry" title="Expiry"> + <Typography> + <Typography fontWeight={700} component="span"> + {formatTimeInWords(validUntil * 1000)} + </Typography>{' '} + ({formatDateTime(validUntil * 1000)}) + </Typography> </DataRow> ) : ( - <></> - ), - status !== 'fulfilled' ? ( - compareAsc(now, expires) !== 1 ? ( - <DataRow key="Expiry" title="Expiry"> - <Typography> - <Typography fontWeight={700} component="span"> - {formatTimeInWords(validUntil * 1000)} - </Typography>{' '} - ({formatDateTime(validUntil * 1000)}) - </Typography> - </DataRow> - ) : ( - <DataRow key="Expiry" title="Expiry"> - {formatDateTime(validUntil * 1000)} - </DataRow> - ) - ) : ( - <></> - ), - orderClass === 'limit' ? ( - <DataRow title="Filled" key="Filled"> - <SwapProgress order={order} /> + <DataRow key="Expired" title="Expired"> + {formatDateTime(validUntil * 1000)} </DataRow> - ) : ( - <></> ), - <DataRow key="Order ID" title="Order ID"> - <OrderId orderId={uid} href={explorerUrl} /> - </DataRow>, <DataRow key="Status" title="Status"> <StatusLabel status={isPartiallyFilled ? 'partiallyFilled' : status} /> </DataRow>, - receiver && receiver !== safeAddress ? ( - <DataRow key="Recipient" title="Recipient"> - <EthHashInfo address={receiver} showAvatar={false} /> - </DataRow> - ) : ( - <></> - ), ]} /> ) @@ -130,15 +275,14 @@ export const SellOrder = ({ order }: { order: SwapOrderType }) => { export const SwapOrder = ({ txData, txInfo }: SwapOrderProps): ReactElement | null => { if (!txData || !txInfo) return null - // ? when can a multiSend call take no parameters? - if (!txData.dataDecoded?.parameters) { - if (txData.hexData) { - return <HexEncodedData title="Data (hex encoded)" hexData={txData.hexData} /> - } - return null + if (isTwapOrderTxInfo(txInfo)) { + return <TwapOrder order={txInfo} /> } - return <SellOrder order={txInfo} /> + if (isSwapOrderTxInfo(txInfo)) { + return <SellOrder order={txInfo} /> + } + return null } export default SwapOrder diff --git a/src/features/swap/components/SwapOrder/index.stories.tsx b/src/features/swap/components/SwapOrder/swap.stories.tsx similarity index 99% rename from src/features/swap/components/SwapOrder/index.stories.tsx rename to src/features/swap/components/SwapOrder/swap.stories.tsx index f122770f23..808702690f 100644 --- a/src/features/swap/components/SwapOrder/index.stories.tsx +++ b/src/features/swap/components/SwapOrder/swap.stories.tsx @@ -39,8 +39,6 @@ const FulfilledSwapOrder = swapOrderBuilder() fullAppData: appDataBuilder('market').build(), }) -console.log(FulfilledSwapOrder) - const NonFulfilledSwapOrder = { ...FulfilledSwapOrder.build(), status: 'open' as OrderStatuses, diff --git a/src/features/swap/components/SwapOrder/twap.stories.tsx b/src/features/swap/components/SwapOrder/twap.stories.tsx new file mode 100644 index 0000000000..ae11b8883b --- /dev/null +++ b/src/features/swap/components/SwapOrder/twap.stories.tsx @@ -0,0 +1,73 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { TwapOrder as TwapOrderComponent } from './index' +import { Paper } from '@mui/material' +import { appDataBuilder, orderTokenBuilder, twapOrderBuilder } from '@/features/swap/helpers/swapOrderBuilder' +import { StoreDecorator } from '@/stories/storeDecorator' + +const FullfilledTwapOrder = twapOrderBuilder() + .with({ status: 'fulfilled' }) + .with({ kind: 'sell' }) + .with({ orderClass: 'limit' }) + .with({ sellAmount: '10000000000000000' }) + .with({ executedSellAmount: '10000000000000000' }) + .with({ buyAmount: '3388586928324482608' }) + .with({ executedBuyAmount: '3388586928324482608' }) + .with({ validUntil: 1713520008 }) + .with({ + sellToken: { + ...orderTokenBuilder().build(), + logoUri: + 'https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14.png', + }, + }) + .with({ + buyToken: { + ...orderTokenBuilder().build(), + logoUri: + 'https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0xbe72E441BF55620febc26715db68d3494213D8Cb.png', + }, + }) + .with({ numberOfParts: 2 }) + + .with({ partSellAmount: '5000000000000000' }) + .with({ minPartLimit: '1694293464162241304' }) + .with({ timeBetweenParts: '1800' }) + .with({ + fullAppData: appDataBuilder('twap').build(), + }) + +const meta = { + component: TwapOrderComponent, + parameters: { + componentSubtitle: 'Renders a Status label with icon and text for a swap order', + }, + + decorators: [ + (Story) => { + return ( + <StoreDecorator initialState={{}}> + <Paper sx={{ padding: 2 }}> + <Story /> + </Paper> + </StoreDecorator> + ) + }, + ], + tags: ['autodocs'], + // excludeStories: ['SwapOrderProps'], +} satisfies Meta<typeof TwapOrderComponent> + +export default meta +type Story = StoryObj<typeof meta> + +export const ExecutedTwap: Story = { + args: { + order: FullfilledTwapOrder.build(), + }, + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/design/VyA38zUPbJ2zflzCIYR6Nu/Swap?node-id=6655-24390&t=pg5ZPJArWFJOiEsn-4', + }, + }, +} diff --git a/src/features/swap/components/SwapProgress/index.tsx b/src/features/swap/components/SwapProgress/index.tsx index 813413f9db..ca4a1a2b97 100644 --- a/src/features/swap/components/SwapProgress/index.tsx +++ b/src/features/swap/components/SwapProgress/index.tsx @@ -1,9 +1,9 @@ import { getFilledAmount, getFilledPercentage } from '@/features/swap/helpers/utils' import { formatAmount } from '@/utils/formatNumber' import { LinearProgress, Stack, Typography } from '@mui/material' -import type { SwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import type { Order } from '@safe-global/safe-gateway-typescript-sdk' -const SwapProgress = ({ order }: { order: SwapOrder }) => { +const SwapProgress = ({ order }: { order: Order }) => { const filledPercentage = getFilledPercentage(order) const filledAmount = formatAmount(getFilledAmount(order)) diff --git a/src/features/swap/components/SwapTxInfo/SwapTx.tsx b/src/features/swap/components/SwapTxInfo/SwapTx.tsx new file mode 100644 index 0000000000..0b3805784a --- /dev/null +++ b/src/features/swap/components/SwapTxInfo/SwapTx.tsx @@ -0,0 +1,50 @@ +import type { Order } from '@safe-global/safe-gateway-typescript-sdk' +import type { ReactElement } from 'react' +import { capitalize } from '@/hooks/useMnemonicName' +import { Box, Typography } from '@mui/material' +import TokenIcon from '@/components/common/TokenIcon' +import { formatVisualAmount } from '@/utils/formatters' + +export const SwapTx = ({ info }: { info: Order }): ReactElement => { + const { kind, sellToken, sellAmount, buyToken } = info + const orderKindLabel = capitalize(kind) + const isSellOrder = kind === 'sell' + + let from = ( + <> + <Box style={{ paddingRight: 5, display: 'inline-block' }}> + <TokenIcon logoUri={sellToken.logoUri || undefined} tokenSymbol={sellToken.symbol} /> + </Box> + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(sellAmount, sellToken.decimals)} {sellToken.symbol}{' '} + </Typography> + </> + ) + + let to = ( + <> + <Box style={{ paddingLeft: 5, paddingRight: 5, display: 'inline-block' }}> + <TokenIcon logoUri={buyToken.logoUri || undefined} tokenSymbol={buyToken.symbol} /> + </Box>{' '} + <Typography component="span" fontWeight="bold"> + {buyToken.symbol} + </Typography> + </> + ) + if (!isSellOrder) { + // switch them around for buy order + ;[from, to] = [to, from] + } + + return ( + <Box display="flex"> + <Typography component="div" display="flex" alignItems="center" fontWeight="bold"> + {from} + <Typography component="span" ml={0.5}> + to + </Typography> + {to} + </Typography> + </Box> + ) +} diff --git a/src/features/swap/components/SwapTxInfo/index.tsx b/src/features/swap/components/SwapTxInfo/interactWith.tsx similarity index 85% rename from src/features/swap/components/SwapTxInfo/index.tsx rename to src/features/swap/components/SwapTxInfo/interactWith.tsx index 4960ef865b..92f3a822d6 100644 --- a/src/features/swap/components/SwapTxInfo/index.tsx +++ b/src/features/swap/components/SwapTxInfo/interactWith.tsx @@ -1,8 +1,8 @@ import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' -import EthHashInfo from '@/components/common/EthHashInfo' import { Box, Typography } from '@mui/material' +import EthHashInfo from '@/components/common/EthHashInfo' -const SwapTxInfo = ({ txData }: { txData: TransactionDetails['txData'] }) => { +const InteractWith = ({ txData }: { txData: TransactionDetails['txData'] }) => { if (!txData) return null return ( @@ -20,5 +20,4 @@ const SwapTxInfo = ({ txData }: { txData: TransactionDetails['txData'] }) => { </Typography> ) } - -export default SwapTxInfo +export default InteractWith diff --git a/src/features/swap/helpers/swapOrderBuilder.ts b/src/features/swap/helpers/swapOrderBuilder.ts index 863e4b5c95..d144ae33eb 100644 --- a/src/features/swap/helpers/swapOrderBuilder.ts +++ b/src/features/swap/helpers/swapOrderBuilder.ts @@ -1,14 +1,16 @@ import { Builder, type IBuilder } from '@/tests/Builder' import { faker } from '@faker-js/faker' import type { - SwapOrder, + CowSwapConfirmationView, OrderToken, + SwapOrder, TransactionInfoType, - CowSwapConfirmationView, + TwapOrder, } from '@safe-global/safe-gateway-typescript-sdk' +import { DurationType, StartTimeValue } from '@safe-global/safe-gateway-typescript-sdk' export function appDataBuilder( - orderClass: 'limit' | 'market' | 'liquidity' = 'limit', + orderClass: 'limit' | 'market' | 'twap' | 'liquidity' = 'limit', ): IBuilder<Record<string, unknown>> { return Builder.new<Record<string, unknown>>().with({ appCode: 'Safe Wallet Swaps', @@ -65,6 +67,39 @@ export function swapOrderBuilder(): IBuilder<SwapOrder> { }) } +export function twapOrderBuilder(): IBuilder<TwapOrder> { + return Builder.new<TwapOrder>().with({ + type: 'TwapOrder' as TransactionInfoType.TWAP_ORDER, + status: faker.helpers.arrayElement(['presignaturePending', 'open', 'cancelled', 'fulfilled', 'expired']), + kind: faker.helpers.arrayElement(['buy', 'sell']), + orderClass: faker.helpers.arrayElement(['limit', 'market', 'liquidity']), + validUntil: faker.date.future().getTime(), + sellAmount: faker.string.numeric(), + buyAmount: faker.string.numeric(), + executedSellAmount: faker.string.numeric(), + executedBuyAmount: faker.string.numeric(), + sellToken: orderTokenBuilder().build(), + buyToken: orderTokenBuilder().build(), + executedSurplusFee: faker.string.numeric(), + fullAppData: appDataBuilder().build(), + numberOfParts: faker.number.int({ min: 1, max: 10 }), + /** @description The amount of sellToken to sell in each part */ + partSellAmount: faker.string.numeric(), + /** @description The amount of buyToken that must be bought in each part */ + minPartLimit: faker.string.numeric(), + /** @description The duration of the TWAP interval */ + timeBetweenParts: faker.string.numeric(), + /** @description Whether the TWAP is valid for the entire interval or not */ + durationOfPart: { + durationType: DurationType.Auto, + }, + /** @description The start time of the TWAP */ + startTime: { + startType: StartTimeValue.AtMiningTime, + }, + }) +} + // create a builder for SwapOrderConfirmationView export function swapOrderConfirmationViewBuilder(): IBuilder<CowSwapConfirmationView> { const ownerAndReceiver = faker.finance.ethereumAddress() diff --git a/src/features/swap/helpers/utils.ts b/src/features/swap/helpers/utils.ts index 22bb79cbfb..87286e7c4f 100644 --- a/src/features/swap/helpers/utils.ts +++ b/src/features/swap/helpers/utils.ts @@ -1,4 +1,4 @@ -import type { SwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import type { Order as SwapOrder } from '@safe-global/safe-gateway-typescript-sdk' import { formatUnits } from 'ethers' import type { AnyAppDataDocVersion, latest } from '@cowprotocol/app-data' diff --git a/src/features/swap/hooks/useIsExpiredSwap.ts b/src/features/swap/hooks/useIsExpiredSwap.ts index 045760da27..bb563d99a4 100644 --- a/src/features/swap/hooks/useIsExpiredSwap.ts +++ b/src/features/swap/hooks/useIsExpiredSwap.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import type { TransactionInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { isSwapTxInfo } from '@/utils/transaction-guards' +import { isSwapOrderTxInfo } from '@/utils/transaction-guards' import useIntervalCounter from '@/hooks/useIntervalCounter' const INTERVAL_IN_MS = 10_000 @@ -15,7 +15,7 @@ const useIsExpiredSwap = (txInfo: TransactionInfo) => { const [counter] = useIntervalCounter(INTERVAL_IN_MS) useEffect(() => { - if (!isSwapTxInfo(txInfo)) return + if (!isSwapOrderTxInfo(txInfo)) return setIsExpired(Date.now() > txInfo.validUntil * 1000) }, [counter, txInfo]) diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 7734b0aafe..8781f7875c 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -1,11 +1,10 @@ import { FEATURES } from '@/utils/chains' import { CowSwapWidget } from '@cowprotocol/widget-react' import { type CowSwapWidgetParams, TradeType } from '@cowprotocol/widget-lib' -import { CowEvents, type CowEventListeners } from '@cowprotocol/events' -import { useState, useEffect, type MutableRefObject, useMemo } from 'react' -import { Container, Grid, useTheme } from '@mui/material' -import { useRef } from 'react' -import { Box } from '@mui/material' +import type { OnTradeParamsPayload } from '@cowprotocol/events' +import { type CowEventListeners, CowEvents } from '@cowprotocol/events' +import { type MutableRefObject, useEffect, useMemo, useRef, useState } from 'react' +import { Box, Container, Grid, useTheme } from '@mui/material' import { SafeAppAccessPolicyTypes, type SafeAppData, @@ -75,7 +74,7 @@ const SwapWidget = ({ sell }: Params) => { const wallet = useWallet() const { isConsentAccepted, onAccept } = useSwapConsent() // useRefs as they don't trigger re-renders - const tradeTypeRef = useRef<TradeType>(tradeType) + const tradeTypeRef = useRef<TradeType>(tradeType === 'twap' ? TradeType.ADVANCED : TradeType.SWAP) const sellTokenRef = useRef<Params['sell']>( sell || { asset: '', @@ -162,7 +161,7 @@ const SwapWidget = ({ sell }: Params) => { }, { event: CowEvents.ON_CHANGE_TRADE_PARAMS, - handler: (newTradeParams) => { + handler: (newTradeParams: OnTradeParamsPayload) => { const { orderType: tradeType, recipient, sellToken, sellTokenAmount } = newTradeParams dispatch(setSwapParams({ tradeType })) @@ -203,7 +202,7 @@ const SwapWidget = ({ sell }: Params) => { ? BASE_URL + '/images/common/swap-empty-dark.svg' : BASE_URL + '/images/common/swap-empty-light.svg', }, - enabledTradeTypes: [TradeType.SWAP, TradeType.LIMIT], + enabledTradeTypes: [TradeType.SWAP, TradeType.LIMIT, TradeType.ADVANCED], theme: { baseTheme: darkMode ? 'dark' : 'light', primary: palette.primary.main, diff --git a/src/features/swap/store/swapParamsSlice.ts b/src/features/swap/store/swapParamsSlice.ts index e7a620b5a0..d8a7910170 100644 --- a/src/features/swap/store/swapParamsSlice.ts +++ b/src/features/swap/store/swapParamsSlice.ts @@ -6,6 +6,7 @@ import { createSlice } from '@reduxjs/toolkit' enum TradeType { SWAP = 'swap', LIMIT = 'limit', + TWAP = 'twap', } export type SwapState = { diff --git a/src/hooks/useTransactionStatus.ts b/src/hooks/useTransactionStatus.ts index 946cf19527..ce79a694ae 100644 --- a/src/hooks/useTransactionStatus.ts +++ b/src/hooks/useTransactionStatus.ts @@ -1,7 +1,7 @@ import { ReplaceTxHoverContext } from '@/components/transactions/GroupedTxListItems/ReplaceTxHoverProvider' import { useAppSelector } from '@/store' import { PendingStatus, selectPendingTxById } from '@/store/pendingTxsSlice' -import { isCancelledSwap, isSignableBy } from '@/utils/transaction-guards' +import { isCancelledSwapOrder, isSignableBy } from '@/utils/transaction-guards' import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk' import { TransactionStatus } from '@safe-global/safe-gateway-typescript-sdk' import { useContext } from 'react' @@ -37,7 +37,7 @@ const useTransactionStatus = (txSummary: TransactionSummary): string => { const wallet = useWallet() const pendingTx = useAppSelector((state) => selectPendingTxById(state, id)) - if (isCancelledSwap(txSummary.txInfo)) { + if (isCancelledSwapOrder(txSummary.txInfo)) { return STATUS_LABELS['CANCELLED'] } diff --git a/src/hooks/useTransactionType.ts b/src/hooks/useTransactionType.ts index b5d9c703ca..da9e9fef86 100644 --- a/src/hooks/useTransactionType.ts +++ b/src/hooks/useTransactionType.ts @@ -70,6 +70,12 @@ export const getTransactionType = (tx: TransactionSummary, addressBook: AddressB text: orderClass === 'limit' ? 'Limit order' : 'Swap order', } } + case 'TwapOrder': { + return { + icon: '/images/common/swap.svg', + text: 'TWAP order', + } + } case TransactionInfoType.CUSTOM: { if (isModuleExecutionInfo(tx.executionInfo)) { return { diff --git a/src/services/analytics/tx-tracking.ts b/src/services/analytics/tx-tracking.ts index b34fdb0d62..0f9af3359b 100644 --- a/src/services/analytics/tx-tracking.ts +++ b/src/services/analytics/tx-tracking.ts @@ -8,7 +8,7 @@ import { isTransferTxInfo, isCustomTxInfo, isCancellationTxInfo, - isSwapTxInfo, + isSwapOrderTxInfo, } from '@/utils/transaction-guards' export const getTransactionTrackingType = async (chainId: string, txId: string): Promise<string> => { @@ -29,7 +29,7 @@ export const getTransactionTrackingType = async (chainId: string, txId: string): return TX_TYPES.transfer_token } - if (isSwapTxInfo(txInfo)) { + if (isSwapOrderTxInfo(txInfo)) { return TX_TYPES.native_swap } diff --git a/src/store/swapOrderSlice.ts b/src/store/swapOrderSlice.ts index 31fc0349d9..c8f884aef6 100644 --- a/src/store/swapOrderSlice.ts +++ b/src/store/swapOrderSlice.ts @@ -2,7 +2,7 @@ import type { listenerMiddlewareInstance } from '@/store' import { createSelector, createSlice } from '@reduxjs/toolkit' import type { OrderStatuses } from '@safe-global/safe-gateway-typescript-sdk' import type { RootState } from '@/store' -import { isSwapTxInfo, isTransactionListItem } from '@/utils/transaction-guards' +import { isSwapOrderTxInfo, isTransactionListItem } from '@/utils/transaction-guards' import { txHistorySlice } from '@/store/txHistorySlice' import { showNotification } from '@/store/notificationsSlice' import { selectSafeInfo } from '@/store/safeInfoSlice' @@ -197,7 +197,7 @@ export const swapOrderListener = (listenerMiddleware: typeof listenerMiddlewareI continue } - if (isSwapTxInfo(result.transaction.txInfo)) { + if (isSwapOrderTxInfo(result.transaction.txInfo)) { const swapOrder = result.transaction.txInfo const oldStatus = selectSwapOrderStatus(listenerApi.getOriginalState(), swapOrder.uid) diff --git a/src/tests/mocks/chains.ts b/src/tests/mocks/chains.ts index b7285b8b52..f21904c274 100644 --- a/src/tests/mocks/chains.ts +++ b/src/tests/mocks/chains.ts @@ -51,6 +51,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.xdai.gnosis.io', @@ -100,6 +104,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.polygon.gnosis.io', @@ -155,6 +163,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.bsc.gnosis.io', @@ -206,6 +218,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.ewc.gnosis.io', @@ -255,6 +271,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SAFE_TX_GAS_OPTIONAL, FEATURES.SPENDING_LIMIT, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.arbitrum.gnosis.io', @@ -297,6 +317,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL, FEATURES.TX_SIMULATION], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.aurora.gnosis.io', @@ -340,6 +364,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [FEATURES.CONTRACT_INTERACTION, FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.avalanche.gnosis.io', @@ -394,6 +422,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.optimism.gnosis.io', @@ -436,6 +468,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ 'walletLink', ], features: [FEATURES.ERC721, FEATURES.SAFE_APPS, FEATURES.SAFE_TX_GAS_OPTIONAL, FEATURES.TX_SIMULATION], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.goerli.gnosis.io/', @@ -486,6 +522,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.rinkeby.gnosis.io', @@ -526,6 +566,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SPENDING_LIMIT, FEATURES.TX_SIMULATION, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, { transactionService: 'https://safe-transaction.volta.gnosis.io', @@ -575,6 +619,10 @@ const CONFIG_SERVICE_CHAINS: ChainInfo[] = [ FEATURES.SAFE_TX_GAS_OPTIONAL, FEATURES.SPENDING_LIMIT, ], + balancesProvider: { + chainName: null, + enabled: false, + }, }, ] diff --git a/src/tests/mocks/transactions.ts b/src/tests/mocks/transactions.ts index fa4c548a95..e70ae51ecd 100644 --- a/src/tests/mocks/transactions.ts +++ b/src/tests/mocks/transactions.ts @@ -23,6 +23,7 @@ const mockTransferInfo: TransferInfo = { tokenAddress: 'string', value: 'string', trusted: true, + imitation: false, } const mockTxInfo: TransactionInfo = { diff --git a/src/utils/transaction-guards.ts b/src/utils/transaction-guards.ts index 5b622126ec..cdd44159c2 100644 --- a/src/utils/transaction-guards.ts +++ b/src/utils/transaction-guards.ts @@ -28,6 +28,8 @@ import type { DecodedDataResponse, BaselineConfirmationView, CowSwapConfirmationView, + TwapOrder, + Order, } from '@safe-global/safe-gateway-typescript-sdk' import { TransferDirection } from '@safe-global/safe-gateway-typescript-sdk' import { @@ -92,10 +94,18 @@ export const isMultiSendTxInfo = (value: TransactionInfo): value is MultiSend => ) } -export const isSwapTxInfo = (value: TransactionInfo): value is SwapOrder => { +export const isOrderTxInfo = (value: TransactionInfo): value is Order => { + return isSwapOrderTxInfo(value) || isTwapOrderTxInfo(value) +} + +export const isSwapOrderTxInfo = (value: TransactionInfo): value is SwapOrder => { return value.type === TransactionInfoType.SWAP_ORDER } +export const isTwapOrderTxInfo = (value: TransactionInfo): value is TwapOrder => { + return value.type === TransactionInfoType.TWAP_ORDER +} + export const isSwapConfirmationViewOrder = ( decodedData: DecodedDataResponse | BaselineConfirmationView | CowSwapConfirmationView | undefined, ): decodedData is CowSwapConfirmationView => { @@ -106,12 +116,12 @@ export const isSwapConfirmationViewOrder = ( return false } -export const isCancelledSwap = (value: TransactionInfo) => { - return isSwapTxInfo(value) && value.status === 'cancelled' +export const isCancelledSwapOrder = (value: TransactionInfo) => { + return isSwapOrderTxInfo(value) && value.status === 'cancelled' } -export const isOpenSwap = (value: TransactionInfo) => { - return isSwapTxInfo(value) && value.status === 'open' +export const isOpenSwapOrder = (value: TransactionInfo) => { + return isSwapOrderTxInfo(value) && value.status === 'open' } export const isCancellationTxInfo = (value: TransactionInfo): value is Cancellation => { diff --git a/yarn.lock b/yarn.lock index 5a8ca3b324..d99578f244 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4156,10 +4156,10 @@ dependencies: semver "^7.6.0" -"@safe-global/safe-gateway-typescript-sdk@3.21.2": - version "3.21.2" - resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.2.tgz#2123a7429c2d9713365f51c359bfc055d4c8e913" - integrity sha512-N9Y2CKPBVbc8FbOKzqepy8TJUY2VILX7bmxV4ruByLJvR9PBnGvGfnOhw975cDn6PmSziXL0RaUWHpSW23rsng== +"@safe-global/safe-gateway-typescript-sdk@^3.21.5": + version "3.21.5" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.5.tgz#8fc96719a9ac81d1070ad61987c7b49c5324a83e" + integrity sha512-KZOAfHDzXCmxVB7SpG6OnRUZontIatwe312NrG7XekmdldCxU78HHecb/tflRZTwJUZhD/USGGZezE7pqjESqQ== "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.21.2" From 34da84f6ebcfb88e4b3340f34b0c18eb04a37227 Mon Sep 17 00:00:00 2001 From: James Mealy <james@safe.global> Date: Wed, 26 Jun 2024 15:34:56 +0100 Subject: [PATCH 106/154] Feat: Show warning for address poisoning transactions (#3851) * Flag imitation transactions and add warning component * add imitation to Erc20Transfer type * make txddetails full width when there is no execution info to display * Make warning symbold full opacity, remove tooltip for imitation txs * handle poisoned addresses * hide share * temporary cast for changes to SDK type * restore share link * remove unused css and rename imitation prop * remove learn more link from imitation tx warning * reuse malicious transaction warning component. * fix: missing tooltip for untrusted txs * update gateway sdks * add mock fields for updated gateway types * remove temporary cast for Erc20Transfer type from gateway sdk * remove unused css * change hide untrusted button to hide suspicious toggle switch * fix: unit tests --- .../batch/BatchSidebar/BatchTxItem.tsx | 2 +- .../ImitationTransactionWarning/index.tsx | 18 +++++++++++++++ .../styles.module.css | 3 +++ .../index.tsx | 17 +++++++------- .../TrustedToggle/TrustedToggleButton.tsx | 22 +++++-------------- .../TxDetails/TxData/Transfer/index.tsx | 14 +++++++----- .../transactions/TxDetails/TxData/index.tsx | 12 ++++++++-- .../transactions/TxDetails/index.tsx | 5 +++-- .../transactions/TxDetails/styles.module.css | 4 ---- .../transactions/TxSummary/index.tsx | 9 ++++---- .../transactions/TxSummary/styles.module.css | 2 +- src/utils/__tests__/tx-history-filter.test.ts | 5 +++-- src/utils/transactions.ts | 4 ++++ src/utils/tx-history-filter.ts | 1 + 14 files changed, 71 insertions(+), 47 deletions(-) create mode 100644 src/components/transactions/ImitationTransactionWarning/index.tsx create mode 100644 src/components/transactions/ImitationTransactionWarning/styles.module.css rename src/components/transactions/{UntrustedTxWarning => MaliciousTxWarning}/index.tsx (53%) diff --git a/src/components/batch/BatchSidebar/BatchTxItem.tsx b/src/components/batch/BatchSidebar/BatchTxItem.tsx index b000dd5d2a..8ab81c8dc7 100644 --- a/src/components/batch/BatchSidebar/BatchTxItem.tsx +++ b/src/components/batch/BatchSidebar/BatchTxItem.tsx @@ -75,7 +75,7 @@ const BatchTxItem = ({ id, count, timestamp, txDetails, onDelete }: BatchTxItemP <AccordionDetails> <div className={css.details}> - <TxData txDetails={txDetails} trusted /> + <TxData txDetails={txDetails} trusted imitation={false} /> <TxDataRow title="Created:">{timestamp ? dateString(timestamp) : null}</TxDataRow> diff --git a/src/components/transactions/ImitationTransactionWarning/index.tsx b/src/components/transactions/ImitationTransactionWarning/index.tsx new file mode 100644 index 0000000000..9ca4a675bb --- /dev/null +++ b/src/components/transactions/ImitationTransactionWarning/index.tsx @@ -0,0 +1,18 @@ +import type { ReactElement } from 'react' +import { Alert, SvgIcon } from '@mui/material' + +import InfoOutlinedIcon from '@/public/images/notifications/info.svg' +import css from './styles.module.css' + +export const ImitationTransactionWarning = (): ReactElement => { + return ( + <Alert + className={css.alert} + sx={{ borderLeft: ({ palette }) => `3px solid ${palette['error'].main} !important` }} + severity="error" + icon={<SvgIcon component={InfoOutlinedIcon} inheritViewBox color="error" />} + > + <b>This may be a malicious transaction.</b> Check and confirm the address before interacting with it.{' '} + </Alert> + ) +} diff --git a/src/components/transactions/ImitationTransactionWarning/styles.module.css b/src/components/transactions/ImitationTransactionWarning/styles.module.css new file mode 100644 index 0000000000..4a851f74d5 --- /dev/null +++ b/src/components/transactions/ImitationTransactionWarning/styles.module.css @@ -0,0 +1,3 @@ +.alert { + padding: 0px 10px; +} diff --git a/src/components/transactions/UntrustedTxWarning/index.tsx b/src/components/transactions/MaliciousTxWarning/index.tsx similarity index 53% rename from src/components/transactions/UntrustedTxWarning/index.tsx rename to src/components/transactions/MaliciousTxWarning/index.tsx index e95e15fdf0..adbb973fb7 100644 --- a/src/components/transactions/UntrustedTxWarning/index.tsx +++ b/src/components/transactions/MaliciousTxWarning/index.tsx @@ -1,19 +1,18 @@ import { Tooltip, SvgIcon, Box } from '@mui/material' import WarningIcon from '@/public/images/notifications/warning.svg' -const UntrustedTxWarning = () => { - return ( +const MaliciousTxWarning = ({ withTooltip = true }: { withTooltip?: boolean }) => { + return withTooltip ? ( <Tooltip title="This token is unfamiliar and may pose risks when interacting with it or involved addresses"> - <Box - lineHeight="16px" - sx={{ - opacity: 1, - }} - > + <Box lineHeight="16px"> <SvgIcon component={WarningIcon} fontSize="small" inheritViewBox color="warning" /> </Box> </Tooltip> + ) : ( + <Box lineHeight="16px"> + <SvgIcon component={WarningIcon} fontSize="small" inheritViewBox color="warning" /> + </Box> ) } -export default UntrustedTxWarning +export default MaliciousTxWarning diff --git a/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx b/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx index a044c38a78..c5bc393a8d 100644 --- a/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx +++ b/src/components/transactions/TrustedToggle/TrustedToggleButton.tsx @@ -1,6 +1,5 @@ import { type ReactElement } from 'react' -import { Typography, Button, SvgIcon } from '@mui/material' -import GhostIcon from '@/public/images/transactions/ghost.svg' +import { FormControlLabel, Switch } from '@mui/material' import { TX_LIST_EVENTS } from '@/services/analytics' import Track from '@/components/common/Track' @@ -23,22 +22,11 @@ const _TrustedToggleButton = ({ return ( <Track {...TX_LIST_EVENTS.TOGGLE_UNTRUSTED} label={onlyTrusted ? 'show' : 'hide'}> - <Button - sx={{ - gap: 1, - height: '38px', - minWidth: '186px', - }} - onClick={onClick} + <FormControlLabel data-testid="toggle-untrusted" - variant="outlined" - size="small" - > - <> - <SvgIcon component={GhostIcon} fontSize="small" inheritViewBox /> - <Typography fontSize="medium">{onlyTrusted ? 'Show' : 'Hide'} unknown</Typography> - </> - </Button> + control={<Switch checked={onlyTrusted} onChange={onClick} />} + label={<>Hide suspicious</>} + /> </Track> ) } diff --git a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx index 97338cbbcf..7254c81657 100644 --- a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx +++ b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx @@ -7,14 +7,17 @@ import { Box, Typography } from '@mui/material' import React from 'react' import TransferActions from '@/components/transactions/TxDetails/TxData/Transfer/TransferActions' -import UntrustedTxWarning from '@/components/transactions/UntrustedTxWarning' +import MaliciousTxWarning from '@/components/transactions/MaliciousTxWarning' +import { ImitationTransactionWarning } from '@/components/transactions/ImitationTransactionWarning' type TransferTxInfoProps = { txInfo: Transfer txStatus: TransactionStatus + trusted: boolean + imitation: boolean } -const TransferTxInfoMain = ({ txInfo, txStatus, trusted }: TransferTxInfoProps & { trusted: boolean }) => { +const TransferTxInfoMain = ({ txInfo, txStatus, trusted, imitation }: TransferTxInfoProps) => { const { direction } = txInfo return ( @@ -26,17 +29,17 @@ const TransferTxInfoMain = ({ txInfo, txStatus, trusted }: TransferTxInfoProps & </b> {direction === TransferDirection.INCOMING ? ' from:' : ' to:'} </Typography> - {!trusted && <UntrustedTxWarning />} + {!trusted && !imitation && <MaliciousTxWarning />} </Box> ) } -const TransferTxInfo = ({ txInfo, txStatus, trusted }: TransferTxInfoProps & { trusted: boolean }) => { +const TransferTxInfo = ({ txInfo, txStatus, trusted, imitation }: TransferTxInfoProps) => { const address = txInfo.direction.toUpperCase() === TransferDirection.INCOMING ? txInfo.sender : txInfo.recipient return ( <Box display="flex" flexDirection="column" gap={1}> - <TransferTxInfoMain txInfo={txInfo} txStatus={txStatus} trusted={trusted} /> + <TransferTxInfoMain txInfo={txInfo} txStatus={txStatus} trusted={trusted} imitation={imitation} /> <Box display="flex" alignItems="center" width="100%"> <EthHashInfo @@ -51,6 +54,7 @@ const TransferTxInfo = ({ txInfo, txStatus, trusted }: TransferTxInfoProps & { t <TransferActions address={address.value} txInfo={txInfo} trusted={trusted} /> </EthHashInfo> </Box> + {imitation && <ImitationTransactionWarning />} </Box> ) } diff --git a/src/components/transactions/TxDetails/TxData/index.tsx b/src/components/transactions/TxDetails/TxData/index.tsx index 53d1064350..060e7656a5 100644 --- a/src/components/transactions/TxDetails/TxData/index.tsx +++ b/src/components/transactions/TxDetails/TxData/index.tsx @@ -21,12 +21,20 @@ import useChainId from '@/hooks/useChainId' import { MultiSendTxInfo } from '@/components/transactions/TxDetails/TxData/MultiSendTxInfo' import InteractWith from '@/features/swap/components/SwapTxInfo/interactWith' -const TxData = ({ txDetails, trusted }: { txDetails: TransactionDetails; trusted: boolean }): ReactElement => { +const TxData = ({ + txDetails, + trusted, + imitation, +}: { + txDetails: TransactionDetails + trusted: boolean + imitation: boolean +}): ReactElement => { const chainId = useChainId() const txInfo = txDetails.txInfo if (isTransferTxInfo(txInfo)) { - return <TransferTxInfo txInfo={txInfo} txStatus={txDetails.txStatus} trusted={trusted} /> + return <TransferTxInfo txInfo={txInfo} txStatus={txDetails.txStatus} trusted={trusted} imitation={imitation} /> } if (isSettingsChangeTxInfo(txInfo)) { diff --git a/src/components/transactions/TxDetails/index.tsx b/src/components/transactions/TxDetails/index.tsx index 57579acd71..65a1829b0b 100644 --- a/src/components/transactions/TxDetails/index.tsx +++ b/src/components/transactions/TxDetails/index.tsx @@ -34,7 +34,7 @@ import { DelegateCallWarning, UnsignedWarning } from '@/components/transactions/ import Multisend from '@/components/transactions/TxDetails/TxData/DecodedData/Multisend' import useSafeInfo from '@/hooks/useSafeInfo' import useIsPending from '@/hooks/useIsPending' -import { isTrustedTx } from '@/utils/transactions' +import { isImitation, isTrustedTx } from '@/utils/transactions' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' import { SwapOrder } from '@/features/swap/components/SwapOrder' @@ -60,6 +60,7 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement // If we have no token list we always trust the transfer const isTrustedTransfer = !hasDefaultTokenlist || isTrustedTx(txSummary) + const isImitationTransaction = isImitation(txSummary) let proposer, safeTxHash if (isMultisigDetailedExecutionInfo(txDetails.detailedExecutionInfo)) { @@ -87,7 +88,7 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement <div className={css.txData}> <ErrorBoundary fallback={<div>Error parsing data</div>}> - <TxData txDetails={txDetails} trusted={isTrustedTransfer} /> + <TxData txDetails={txDetails} trusted={isTrustedTransfer} imitation={isImitationTransaction} /> </ErrorBoundary> </div> diff --git a/src/components/transactions/TxDetails/styles.module.css b/src/components/transactions/TxDetails/styles.module.css index 5818f7b943..7be58a00b7 100644 --- a/src/components/transactions/TxDetails/styles.module.css +++ b/src/components/transactions/TxDetails/styles.module.css @@ -17,10 +17,6 @@ top: 16px; } -.details .noSigners { - width: 100%; -} - .loading, .error, .txData, diff --git a/src/components/transactions/TxSummary/index.tsx b/src/components/transactions/TxSummary/index.tsx index 14081165da..611a10f03d 100644 --- a/src/components/transactions/TxSummary/index.tsx +++ b/src/components/transactions/TxSummary/index.tsx @@ -10,8 +10,8 @@ import TxInfo from '@/components/transactions/TxInfo' import { isMultisigExecutionInfo, isTxQueued } from '@/utils/transaction-guards' import TxType from '@/components/transactions/TxType' import classNames from 'classnames' -import { isTrustedTx } from '@/utils/transactions' -import UntrustedTxWarning from '../UntrustedTxWarning' +import { isImitation, isTrustedTx } from '@/utils/transactions' +import MaliciousTxWarning from '../MaliciousTxWarning' import QueueActions from './QueueActions' import useIsPending from '@/hooks/useIsPending' import TxConfirmations from '../TxConfirmations' @@ -32,6 +32,7 @@ const TxSummary = ({ item, isConflictGroup, isBulkGroup }: TxSummaryProps): Reac const isQueue = isTxQueued(tx.txStatus) const nonce = isMultisigExecutionInfo(tx.executionInfo) ? tx.executionInfo.nonce : undefined const isTrusted = !hasDefaultTokenlist || isTrustedTx(tx) + const isImitationTransaction = isImitation(tx) const isPending = useIsPending(tx.id) const executionInfo = isMultisigExecutionInfo(tx.executionInfo) ? tx.executionInfo : undefined const expiredSwap = useIsExpiredSwap(tx.txInfo) @@ -53,9 +54,9 @@ const TxSummary = ({ item, isConflictGroup, isBulkGroup }: TxSummaryProps): Reac </Box> )} - {!isTrusted && ( + {(isImitationTransaction || !isTrusted) && ( <Box data-testid="warning" gridArea="nonce"> - <UntrustedTxWarning /> + <MaliciousTxWarning withTooltip={!isImitationTransaction} /> </Box> )} diff --git a/src/components/transactions/TxSummary/styles.module.css b/src/components/transactions/TxSummary/styles.module.css index 80ce87ffbd..fc8ed69fac 100644 --- a/src/components/transactions/TxSummary/styles.module.css +++ b/src/components/transactions/TxSummary/styles.module.css @@ -50,7 +50,7 @@ grid-template-areas: 'type info date status confirmations'; } -.gridContainer.untrusted { +.gridContainer.untrusted :not(:first-child):is(div) { opacity: 0.4; } diff --git a/src/utils/__tests__/tx-history-filter.test.ts b/src/utils/__tests__/tx-history-filter.test.ts index 05115d8deb..956715118d 100644 --- a/src/utils/__tests__/tx-history-filter.test.ts +++ b/src/utils/__tests__/tx-history-filter.test.ts @@ -394,7 +394,7 @@ describe('tx-history-filter', () => { expect(getIncomingTransfers).toHaveBeenCalledWith( '4', '0x123', - { value: '123', executed: undefined, timezone_offset: 3600000, trusted: false }, + { value: '123', executed: undefined, timezone_offset: 3600000, trusted: false, imitation: false }, 'pageUrl1', ) @@ -422,6 +422,7 @@ describe('tx-history-filter', () => { executed: 'true', timezone_offset: 3600000, trusted: false, + imitation: false, }, 'pageUrl2', ) @@ -442,7 +443,7 @@ describe('tx-history-filter', () => { expect(getModuleTransactions).toHaveBeenCalledWith( '1', '0x789', - { to: '0x123', executed: undefined, timezone_offset: 3600000, trusted: false }, + { to: '0x123', executed: undefined, timezone_offset: 3600000, trusted: false, imitation: false }, 'pageUrl3', ) diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index 3e9d5d0f99..97c2b028b9 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -298,3 +298,7 @@ export const isTrustedTx = (tx: TransactionSummary) => { Boolean(tx.txInfo.transferInfo.trusted) ) } + +export const isImitation = ({ txInfo }: TransactionSummary): boolean => { + return isTransferTxInfo(txInfo) && isERC20Transfer(txInfo.transferInfo) && Boolean(txInfo.transferInfo.imitation) +} diff --git a/src/utils/tx-history-filter.ts b/src/utils/tx-history-filter.ts index bad2691c00..bcc4c5ba47 100644 --- a/src/utils/tx-history-filter.ts +++ b/src/utils/tx-history-filter.ts @@ -127,6 +127,7 @@ export const fetchFilteredTxHistory = async ( ...filterData.filter, timezone_offset: getTimezoneOffset(), trusted: onlyTrusted ?? false, + imitation: onlyTrusted ?? false, executed: filterData.type === TxFilterType.MULTISIG ? 'true' : undefined, } From a49eee3cf725c1b77a04cef8c6481b81313ed065 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart <manu@safe.global> Date: Wed, 26 Jun 2024 20:04:08 +0200 Subject: [PATCH 107/154] fix: update recommendedNonce when history tag changes (#3872) * fix: update recommendedNonce when history tag changes * chore: adjust comment in test --- .../tx/SignOrExecuteForm/hooks.test.ts | 174 ++++++++++++++++-- src/components/tx/SignOrExecuteForm/hooks.ts | 2 +- 2 files changed, 156 insertions(+), 20 deletions(-) diff --git a/src/components/tx/SignOrExecuteForm/hooks.test.ts b/src/components/tx/SignOrExecuteForm/hooks.test.ts index 735030b3f1..98def65661 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.test.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.test.ts @@ -1,5 +1,5 @@ -import { extendedSafeInfoBuilder } from '@/tests/builders/safe' -import { renderHook } from '@/tests/test-utils' +import { extendedSafeInfoBuilder, safeInfoBuilder } from '@/tests/builders/safe' +import { renderHook, waitFor } from '@/tests/test-utils' import { zeroPadValue } from 'ethers' import { createSafeTx } from '@/tests/builders/safeTx' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' @@ -10,7 +10,16 @@ import * as pending from '@/hooks/usePendingTxs' import * as txSender from '@/services/tx/tx-sender/dispatch' import * as onboardHooks from '@/hooks/wallets/useOnboard' import { type OnboardAPI } from '@web3-onboard/core' -import { useAlreadySigned, useImmediatelyExecutable, useIsExecutionLoop, useTxActions, useValidateNonce } from './hooks' +import { + useAlreadySigned, + useImmediatelyExecutable, + useIsExecutionLoop, + useRecommendedNonce, + useTxActions, + useValidateNonce, +} from './hooks' +import * as recommendedNonce from '@/services/tx/tx-sender/recommendedNonce' +import { defaultSafeInfo } from '@/store/safeInfoSlice' describe('SignOrExecute hooks', () => { const extendedSafeInfo = extendedSafeInfoBuilder().build() @@ -566,24 +575,151 @@ describe('SignOrExecute hooks', () => { const { result } = renderHook(() => useAlreadySigned(tx)) expect(result.current).toEqual(true) }) + + it('should return false if wallet has not signed a tx yet', () => { + // Wallet + jest.spyOn(wallet, 'default').mockReturnValue({ + chainId: '1', + label: 'MetaMask', + address: '0x1234567890000000000000000000000000000000', + } as unknown as ConnectedWallet) + + const tx = createSafeTx() + tx.addSignature({ + signer: '0x00000000000000000000000000000000000000000', + data: '0x0001', + staticPart: () => '', + dynamicPart: () => '', + isContractSignature: false, + }) + const { result } = renderHook(() => useAlreadySigned(tx)) + expect(result.current).toEqual(false) + }) }) - it('should return false if wallet has not signed a tx yet', () => { - // Wallet - jest.spyOn(wallet, 'default').mockReturnValue({ - chainId: '1', - label: 'MetaMask', - address: '0x1234567890000000000000000000000000000000', - } as unknown as ConnectedWallet) - const tx = createSafeTx() - tx.addSignature({ - signer: '0x00000000000000000000000000000000000000000', - data: '0x0001', - staticPart: () => '', - dynamicPart: () => '', - isContractSignature: false, + describe('useRecommendedNonce', () => { + it('should return undefined without safe info', async () => { + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...defaultSafeInfo, deployed: false }, + safeAddress: '', + safeLoaded: true, + safeLoading: false, + }) + + const { result } = renderHook(useRecommendedNonce) + await waitFor(() => { + expect(result.current).toBeUndefined() + }) + }) + it('should return 0 for counterfactual Safes', async () => { + const mockSafeInfo = safeInfoBuilder().build() + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...mockSafeInfo, deployed: false }, + safeAddress: mockSafeInfo.address.value, + safeLoaded: true, + safeLoading: false, + }) + + const { result } = renderHook(useRecommendedNonce) + await waitFor(() => { + expect(result.current).toEqual(0) + }) + }) + + it('should update if queueTag changes', async () => { + jest.spyOn(recommendedNonce, 'getNonces').mockResolvedValue({ + currentNonce: 1, + recommendedNonce: 1, + }) + const mockSafeInfo = safeInfoBuilder() + .with({ + txQueuedTag: '1', + }) + .build() + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...mockSafeInfo, deployed: true }, + safeAddress: mockSafeInfo.address.value, + safeLoaded: true, + safeLoading: false, + }) + + const { result, rerender } = renderHook(useRecommendedNonce) + await waitFor(() => { + expect(result.current).toEqual(1) + }) + + jest.spyOn(recommendedNonce, 'getNonces').mockResolvedValue({ + currentNonce: 1, + recommendedNonce: 2, + }) + + rerender() + // The hook does not rerender as the queue tag did not change yet + await waitFor(() => { + expect(result.current).toEqual(1) + }) + + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...mockSafeInfo, deployed: true, txQueuedTag: '2' }, + safeAddress: mockSafeInfo.address.value, + safeLoaded: true, + safeLoading: false, + }) + + rerender() + + // Now the queue tag changed from 1 to 2 and the hook should reflect the new recommended Nonce + await waitFor(() => { + expect(result.current).toEqual(2) + }) + }) + + it('should update if historyTag changes', async () => { + jest.spyOn(recommendedNonce, 'getNonces').mockResolvedValue({ + currentNonce: 1, + recommendedNonce: 1, + }) + const mockSafeInfo = safeInfoBuilder() + .with({ + txHistoryTag: '1', + }) + .build() + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...mockSafeInfo, deployed: true }, + safeAddress: mockSafeInfo.address.value, + safeLoaded: true, + safeLoading: false, + }) + + const { result, rerender } = renderHook(useRecommendedNonce) + await waitFor(() => { + expect(result.current).toEqual(1) + }) + + jest.spyOn(recommendedNonce, 'getNonces').mockResolvedValue({ + currentNonce: 2, + recommendedNonce: 2, + }) + + rerender() + // The hook does not rerender as the history tag did not change yet + await waitFor(() => { + expect(result.current).toEqual(1) + }) + + jest.spyOn(useSafeInfoHook, 'default').mockReturnValue({ + safe: { ...mockSafeInfo, deployed: true, txHistoryTag: '2' }, + safeAddress: mockSafeInfo.address.value, + safeLoaded: true, + safeLoading: false, + }) + + rerender() + + // Now the history tag changed from 1 to 2 and the hook should reflect the new recommended Nonce + await waitFor(() => { + expect(result.current).toEqual(2) + }) }) - const { result } = renderHook(() => useAlreadySigned(tx)) - expect(result.current).toEqual(false) }) }) diff --git a/src/components/tx/SignOrExecuteForm/hooks.ts b/src/components/tx/SignOrExecuteForm/hooks.ts index dd84f9a6f9..aae69d8b24 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.ts @@ -167,7 +167,7 @@ export const useRecommendedNonce = (): number | undefined => { return nonces?.recommendedNonce }, // eslint-disable-next-line react-hooks/exhaustive-deps - [safeAddress, safe.chainId, safe.txQueuedTag], // update when tx queue changes + [safeAddress, safe.chainId, safe.txQueuedTag, safe.txHistoryTag], // update when tx queue or history changes false, // keep old recommended nonce while refreshing to avoid skeleton ) From bc82d70208cd153c67a6c44d0278b48f900c93b4 Mon Sep 17 00:00:00 2001 From: Muhammad Haris <68734123+MHarisAshfaq@users.noreply.github.com> Date: Wed, 26 Jun 2024 23:07:42 +0500 Subject: [PATCH 108/154] Fix: removed border from messages txs (#3874) --- .../safe-messages/MsgListItem/ExpandableMsgItem.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/safe-messages/MsgListItem/ExpandableMsgItem.tsx b/src/components/safe-messages/MsgListItem/ExpandableMsgItem.tsx index a5e6ebfc78..0aee1bf5e7 100644 --- a/src/components/safe-messages/MsgListItem/ExpandableMsgItem.tsx +++ b/src/components/safe-messages/MsgListItem/ExpandableMsgItem.tsx @@ -10,7 +10,12 @@ import txListItemCss from '@/components/transactions/TxListItem/styles.module.cs const ExpandableMsgItem = ({ msg }: { msg: SafeMessage }): ReactElement => { return ( - <Accordion disableGutters elevation={0} className={txListItemCss.accordion}> + <Accordion + disableGutters + elevation={0} + className={txListItemCss.accordion} + sx={{ border: 'none', '&:before': { display: 'none' } }} + > <AccordionSummary data-testid="message-item" expandIcon={<ExpandMoreIcon />} From 6beff838b9f13d9e833c71565d51ae85caa6cb0d Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Thu, 27 Jun 2024 08:11:11 +0200 Subject: [PATCH 109/154] Fix: wrong number formatting in SingleTxDecoded (#3868) --- .../TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx | 7 +++---- src/utils/formatNumber.ts | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx index 4de7ade91c..920f350015 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/SingleTxDecoded/index.tsx @@ -7,7 +7,7 @@ import { } from '@safe-global/safe-gateway-typescript-sdk' import type { AccordionProps } from '@mui/material/Accordion/Accordion' import { useCurrentChain } from '@/hooks/useChains' -import { formatVisualAmount } from '@/utils/formatters' +import { safeFormatUnits } from '@/utils/formatters' import { MethodDetails } from '@/components/transactions/TxDetails/TxData/DecodedData/MethodDetails' import { HexEncodedData } from '@/components/transactions/HexEncodedData' import { isDeleteAllowance, isSetAllowance } from '@/utils/transaction-guards' @@ -43,8 +43,8 @@ export const SingleTxDecoded = ({ const chain = useCurrentChain() const isNativeTransfer = tx.value !== '0' && (!tx.data || isEmptyHexData(tx.data)) const method = tx.dataDecoded?.method || (isNativeTransfer ? 'native transfer' : 'contract interaction') - const { decimals, symbol } = chain?.nativeCurrency || {} - const amount = tx.value ? formatVisualAmount(tx.value, decimals) : 0 + const { decimals } = chain?.nativeCurrency || {} + const amount = tx.value ? safeFormatUnits(tx.value, decimals) : 0 let details if (tx.dataDecoded) { @@ -56,7 +56,6 @@ export const SingleTxDecoded = ({ const addressInfo = txData.addressInfoIndex?.[tx.to] const name = addressInfo?.name - const avatarUrl = addressInfo?.logoUri const isDelegateCall = tx.operation === Operation.DELEGATE && showDelegateCallWarning const isSpendingLimitMethod = isSetAllowance(tx.dataDecoded?.method) || isDeleteAllowance(tx.dataDecoded?.method) diff --git a/src/utils/formatNumber.ts b/src/utils/formatNumber.ts index 89ec083074..f0d0a1b202 100644 --- a/src/utils/formatNumber.ts +++ b/src/utils/formatNumber.ts @@ -50,7 +50,7 @@ export const formatCurrency = (number: string | number, currency: string, maxLen currency, currencyDisplay: 'narrowSymbol', maximumFractionDigits: Math.abs(float) >= 1 || float === 0 ? 0 : 2, - }).format(Number(number)) + }).format(float) // +1 for the currency symbol if (result.length > maxLength + 1) { @@ -60,7 +60,7 @@ export const formatCurrency = (number: string | number, currency: string, maxLen currencyDisplay: 'narrowSymbol', notation: 'compact', maximumFractionDigits: 2, - }).format(Number(number)) + }).format(float) } return result.replace(/^(\D+)/, '$1 ') From dcfef2291f2f1569e2703a1c35484b33f9934fd3 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Fri, 28 Jun 2024 09:54:50 +0200 Subject: [PATCH 110/154] Tests: E2e wallet (#3878) * Tests: remove e2e wallet * Tests: Update tests as per new signer (#3867) * Update tests as per new signer * Update tests * Tests: Fix tests (#3873) * Fix tests --- cypress/e2e/happypath/recovery_hp_1.cy.js | 7 +- .../sendfunds_connected_wallet.cy.js | 172 +++++++++--------- cypress/e2e/happypath/sendfunds_queue_1.cy.js | 5 + cypress/e2e/happypath/sendfunds_relay.cy.js | 74 ++++---- cypress/e2e/pages/assets.pages.js | 4 +- cypress/e2e/pages/create_wallet.pages.js | 18 +- cypress/e2e/pages/main.page.js | 5 - cypress/e2e/regression/add_owner.cy.js | 16 +- cypress/e2e/regression/address_book.cy.js | 4 + .../e2e/regression/balances_pagination.cy.js | 5 +- cypress/e2e/regression/batch_tx.cy.js | 4 + cypress/e2e/regression/create_safe_cf.cy.js | 19 +- .../e2e/regression/create_safe_simple.cy.js | 23 ++- .../e2e/regression/create_safe_simple_2.cy.js | 37 +++- cypress/e2e/regression/create_tx.cy.js | 5 + cypress/e2e/regression/nfts.cy.js | 6 + cypress/e2e/regression/remove_owner.cy.js | 11 +- cypress/e2e/regression/replace_owner.cy.js | 11 +- cypress/e2e/regression/sidebar.cy.js | 7 +- cypress/e2e/regression/sidebar_nonowner.cy.js | 4 + cypress/e2e/regression/spending_limits.cy.js | 16 +- .../regression/spending_limits_nonowner.cy.js | 2 - cypress/e2e/regression/swaps_tokens.cy.js | 4 + cypress/e2e/regression/tokens.cy.js | 5 - cypress/e2e/smoke/add_owner.cy.js | 30 +-- cypress/e2e/smoke/address_book.cy.js | 4 + cypress/e2e/smoke/assets.cy.js | 15 +- cypress/e2e/smoke/batch_tx.cy.js | 10 +- cypress/e2e/smoke/create_safe_cf.cy.js | 5 + cypress/e2e/smoke/create_safe_simple.cy.js | 11 +- cypress/e2e/smoke/create_tx.cy.js | 47 +---- cypress/e2e/smoke/create_tx_2.cy.js | 54 ++++++ cypress/e2e/smoke/import_export_data_2.cy.js | 8 + cypress/e2e/smoke/messages_offchain.cy.js | 5 + cypress/e2e/smoke/replace_owner.cy.js | 9 +- cypress/e2e/smoke/spending_limits.cy.js | 4 + cypress/support/constants.js | 5 - cypress/support/utils/wallet.js | 47 +++++ .../ConnectWallet/ConnectWalletButton.tsx | 1 + src/hooks/wallets/useOnboard.ts | 3 +- src/hooks/wallets/wallets.ts | 6 +- .../private-key-module/PkModulePopup.tsx | 2 +- 42 files changed, 449 insertions(+), 281 deletions(-) create mode 100644 cypress/e2e/smoke/create_tx_2.cy.js create mode 100644 cypress/support/utils/wallet.js diff --git a/cypress/e2e/happypath/recovery_hp_1.cy.js b/cypress/e2e/happypath/recovery_hp_1.cy.js index 30ecce703a..bb036e11fc 100644 --- a/cypress/e2e/happypath/recovery_hp_1.cy.js +++ b/cypress/e2e/happypath/recovery_hp_1.cy.js @@ -4,9 +4,12 @@ import * as owner from '../pages/owners.pages' import * as recovery from '../pages/recovery.pages' import * as tx from '../pages/transactions.page' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let recoverySafes = [] -// +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + describe('Recovery happy path tests 1', () => { before(async () => { recoverySafes = await getSafes(CATEGORIES.recovery) @@ -20,8 +23,8 @@ describe('Recovery happy path tests 1', () => { // Check that recovery can be setup and removed it('Recovery setup happy path 1', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() - cy.reload() recovery.clearRecoverers() recovery.clickOnSetupRecoveryBtn() recovery.clickOnSetupRecoveryModalBtn() diff --git a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js index b38b8472ef..a7cd2d52c0 100644 --- a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js +++ b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js @@ -12,12 +12,15 @@ import { createEthersAdapter, createSigners } from '../../support/api/utils_ethe import { createSafes } from '../../support/api/utils_protocolkit' import { contracts, abi_qtrust, abi_nft_pc2 } from '../../support/api/contracts' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' +const safeBalanceEth = 505360000000000000n const safeBalanceEth = 505320000000000000n const qtrustBanance = 99000000000000000025n const transferAmount = '1' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const tokenAmount2 = '0.00001' const netwrok = 'sepolia' @@ -49,7 +52,7 @@ function visit(url) { cy.visit(url) } -describe('Send funds with connected signer happy path tests', { defaultCommandTimeout: 600000 }, () => { +describe('Send funds with connected signer happy path tests', { defaultCommandTimeout: 60000 }, () => { before(async () => { safesData = await getSafes(CATEGORIES.funds) main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__cookies, ls.cookies.acceptedCookies) @@ -75,12 +78,13 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi protocolKitOwner2_S3 = safes[1] }) - it('Verify tx creation and execution of NFT with connected signer', { defaultCommandTimeout: 300000 }, () => { + it('Verify tx creation and execution of NFT with connected signer', () => { cy.wait(2000) const originatingSafe = safesData.SEP_FUNDS_SAFE_7.substring(4) function executeTransactionFlow(fromSafe, toSafe) { return cy.visit(constants.balanceNftsUrl + fromSafe).then(() => { + wallet.connectSigner(signer) nfts.selectNFTs(1) nfts.sendNFT() nfts.typeRecipientAddress(toSafe) @@ -107,96 +111,90 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi }) }) - it( - 'Verify tx creation and execution of native token with connected signer', - { defaultCommandTimeout: 300000 }, - () => { - cy.wait(2000) - const targetSafe = safesData.SEP_FUNDS_SAFE_12.substring(4) - function executeTransactionFlow(fromSafe, toSafe, tokenAmount) { - visit(constants.BALANCE_URL + fromSafe) - assets.clickOnSendBtn(0) - loadsafe.inputOwnerAddress(0, toSafe) - assets.checkSelectedToken(constants.tokenAbbreviation.sep) - assets.enterAmount(tokenAmount) - navigation.clickOnNewTxBtnS() - tx.executeFlow_1() - cy.wait(5000) - } - cy.wrap(null) - .then(() => { - return main.fetchCurrentNonce(network_pref + targetSafe) + it('Verify tx creation and execution of native token with connected signer', () => { + cy.wait(2000) + const targetSafe = safesData.SEP_FUNDS_SAFE_12.substring(4) + function executeTransactionFlow(fromSafe, toSafe, tokenAmount) { + visit(constants.BALANCE_URL + fromSafe) + wallet.connectSigner(signer) + assets.clickOnSendBtn(0) + loadsafe.inputOwnerAddress(0, toSafe) + assets.checkSelectedToken(constants.tokenAbbreviation.sep) + assets.enterAmount(tokenAmount) + navigation.clickOnNewTxBtnS() + tx.executeFlow_1() + cy.wait(5000) + } + cy.wrap(null) + .then(() => { + return main.fetchCurrentNonce(network_pref + targetSafe) + }) + .then(async (currentNonce) => { + executeTransactionFlow(targetSafe, walletAddress.toString(), tokenAmount2) + const amount = ethers.parseUnits(tokenAmount2, unit_eth).toString() + const safeTransactionData = { + to: targetSafe, + data: '0x', + value: amount.toString(), + } + + const safeTransaction = await protocolKitOwner1_S3.createTransaction({ transactions: [safeTransactionData] }) + const safeTxHash = await protocolKitOwner1_S3.getTransactionHash(safeTransaction) + const senderSignature = await protocolKitOwner1_S3.signHash(safeTxHash) + const safeAddress = outgoingSafeAddress + + await apiKit.proposeTransaction({ + safeAddress, + safeTransactionData: safeTransaction.data, + safeTxHash, + senderAddress: await owner1Signer.getAddress(), + senderSignature: senderSignature.data, }) - .then(async (currentNonce) => { - executeTransactionFlow(targetSafe, walletAddress.toString(), tokenAmount2) - const amount = ethers.parseUnits(tokenAmount2, unit_eth).toString() - const safeTransactionData = { - to: targetSafe, - data: '0x', - value: amount.toString(), - } - - const safeTransaction = await protocolKitOwner1_S3.createTransaction({ transactions: [safeTransactionData] }) - const safeTxHash = await protocolKitOwner1_S3.getTransactionHash(safeTransaction) - const senderSignature = await protocolKitOwner1_S3.signHash(safeTxHash) - const safeAddress = outgoingSafeAddress - - await apiKit.proposeTransaction({ - safeAddress, - safeTransactionData: safeTransaction.data, - safeTxHash, - senderAddress: await owner1Signer.getAddress(), - senderSignature: senderSignature.data, - }) - const pendingTransactions = await apiKit.getPendingTransactions(safeAddress) - const safeTxHashofExistingTx = pendingTransactions.results[0].safeTxHash + const pendingTransactions = await apiKit.getPendingTransactions(safeAddress) + const safeTxHashofExistingTx = pendingTransactions.results[0].safeTxHash - const signature = await protocolKitOwner2_S3.signHash(safeTxHashofExistingTx) - await apiKit.confirmTransaction(safeTxHashofExistingTx, signature.data) + const signature = await protocolKitOwner2_S3.signHash(safeTxHashofExistingTx) + await apiKit.confirmTransaction(safeTxHashofExistingTx, signature.data) - const safeTx = await apiKit.getTransaction(safeTxHashofExistingTx) - await protocolKitOwner2_S3.executeTransaction(safeTx) - main.verifyNonceChange(network_pref + targetSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + targetSafe, constants.tokenAbbreviation.eth, safeBalanceEth) - }) - }, - ) - - it( - 'Verify tx creation and execution of non-native token with connected signer', - { defaultCommandTimeout: 300000 }, - () => { - cy.wait(2000) - const originatingSafe = safesData.SEP_FUNDS_SAFE_11.substring(4) - const amount = ethers.parseUnits(transferAmount, unit_eth).toString() - - function executeTransactionFlow(fromSafe, toSafe) { - visit(constants.BALANCE_URL + fromSafe) - assets.selectTokenList(assets.tokenListOptions.allTokens) - assets.clickOnSendBtn(1) - loadsafe.inputOwnerAddress(0, toSafe) - assets.enterAmount(1) - navigation.clickOnNewTxBtnS() - tx.executeFlow_1() - cy.wait(5000) - } - cy.wrap(null) - .then(() => { - return main.fetchCurrentNonce(network_pref + originatingSafe) - }) - .then(async (currentNonce) => { - executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount) + const safeTx = await apiKit.getTransaction(safeTxHashofExistingTx) + await protocolKitOwner2_S3.executeTransaction(safeTx) + main.verifyNonceChange(network_pref + targetSafe, currentNonce + 1) + main.checkTokenBalance(network_pref + targetSafe, constants.tokenAbbreviation.eth, safeBalanceEth) + }) + }) - const contractWithWallet = tokenContract.connect(signers[0]) - const tx = await contractWithWallet.transfer(originatingSafe, amount, { - gasLimit: 200000, - }) + it('Verify tx creation and execution of non-native token with connected signer', () => { + cy.wait(2000) + const originatingSafe = safesData.SEP_FUNDS_SAFE_11.substring(4) + const amount = ethers.parseUnits(transferAmount, unit_eth).toString() - await tx.wait() - main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + originatingSafe, constants.tokenAbbreviation.qtrust, qtrustBanance) + function executeTransactionFlow(fromSafe, toSafe) { + visit(constants.BALANCE_URL + fromSafe) + wallet.connectSigner(signer) + assets.selectTokenList(assets.tokenListOptions.allTokens) + assets.clickOnSendBtn(1) + loadsafe.inputOwnerAddress(0, toSafe) + assets.enterAmount(1) + navigation.clickOnNewTxBtnS() + tx.executeFlow_1() + cy.wait(5000) + } + cy.wrap(null) + .then(() => { + return main.fetchCurrentNonce(network_pref + originatingSafe) + }) + .then(async (currentNonce) => { + executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount) + + const contractWithWallet = tokenContract.connect(signers[0]) + const tx = await contractWithWallet.transfer(originatingSafe, amount, { + gasLimit: 200000, }) - }, - ) + + await tx.wait() + main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) + main.checkTokenBalance(network_pref + originatingSafe, constants.tokenAbbreviation.qtrust, qtrustBanance) + }) + }) }) diff --git a/cypress/e2e/happypath/sendfunds_queue_1.cy.js b/cypress/e2e/happypath/sendfunds_queue_1.cy.js index 83ae5089c8..f0a6d56242 100644 --- a/cypress/e2e/happypath/sendfunds_queue_1.cy.js +++ b/cypress/e2e/happypath/sendfunds_queue_1.cy.js @@ -7,9 +7,11 @@ import SafeApiKit from '@safe-global/api-kit' import { createEthersAdapter, createSigners } from '../../support/api/utils_ether' import { createSafes } from '../../support/api/utils_protocolkit' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const receiver = walletCredentials.OWNER_2_WALLET_ADDRESS +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const tokenAmount = '0.0001' const netwrok = 'sepolia' @@ -46,6 +48,7 @@ function visit(url) { function executeTransactionFlow(fromSafe) { visit(constants.transactionQueueUrl + fromSafe) + wallet.connectSigner(signer) assets.clickOnConfirmBtn(0) tx.executeFlow_1() cy.wait(5000) @@ -117,6 +120,7 @@ describe('Send funds from queue happy path tests 1', () => { it.skip('Verify confirmation and execution of native token queued tx by second signer with relayer', () => { function executeTransactionFlow(fromSafe) { visit(constants.transactionQueueUrl + fromSafe) + wallet.connectSigner(signer) assets.clickOnConfirmBtn(0) tx.executeFlow_2() cy.wait(5000) @@ -155,6 +159,7 @@ describe('Send funds from queue happy path tests 1', () => { it('Verify 1 signer can execute a tx confirmed by 2 signers', { defaultCommandTimeout: 300000 }, () => { function executeTransaction(fromSafe) { visit(constants.transactionQueueUrl + fromSafe) + wallet.connectSigner(signer) assets.clickOnExecuteBtn(0) tx.executeFlow_3() cy.wait(5000) diff --git a/cypress/e2e/happypath/sendfunds_relay.cy.js b/cypress/e2e/happypath/sendfunds_relay.cy.js index 7ea597497e..3048ce3934 100644 --- a/cypress/e2e/happypath/sendfunds_relay.cy.js +++ b/cypress/e2e/happypath/sendfunds_relay.cy.js @@ -12,12 +12,14 @@ import { createEthersAdapter, createSigners } from '../../support/api/utils_ethe import { createSafes } from '../../support/api/utils_protocolkit' import { contracts, abi_qtrust, abi_nft_pc2 } from '../../support/api/contracts' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' const safeBalanceEth = 405240000000000000n const qtrustBanance = 60000000000000000000n const transferAmount = '1' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const tokenAmount2 = '0.00001' const netwrok = 'sepolia' @@ -75,11 +77,12 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 3000 protocolKitOwner2_S3 = safes[1] }) - it('Verify tx creation and execution of NFT with relay', { defaultCommandTimeout: 300000 }, () => { + it('Verify tx creation and execution of NFT with relay', () => { cy.wait(2000) const originatingSafe = safesData.SEP_FUNDS_SAFE_9.substring(4) function executeTransactionFlow(fromSafe, toSafe) { return cy.visit(constants.balanceNftsUrl + fromSafe).then(() => { + wallet.connectSigner(signer) nfts.selectNFTs(1) nfts.sendNFT() nfts.typeRecipientAddress(toSafe) @@ -107,11 +110,12 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 3000 }) }) - it('Verify tx creation and execution of native token with relay', { defaultCommandTimeout: 300000 }, () => { + it('Verify tx creation and execution of native token with relay', () => { cy.wait(2000) const targetSafe = safesData.SEP_FUNDS_SAFE_1.substring(4) function executeTransactionFlow(fromSafe, toSafe, tokenAmount) { visit(constants.BALANCE_URL + fromSafe) + wallet.connectSigner(signer) assets.clickOnSendBtn(0) loadsafe.inputOwnerAddress(0, toSafe) assets.checkSelectedToken(constants.tokenAbbreviation.sep) @@ -159,43 +163,39 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 3000 }) }) - // TODO: Too many requests error occurs. Skip until resolved. - it.skip( - 'Verify tx creation and execution of non-native token with with relay', - { defaultCommandTimeout: 300000 }, - () => { - cy.wait(2000) - const originatingSafe = safesData.SEP_FUNDS_SAFE_2.substring(4) - const amount = ethers.parseUnits(transferAmount, unit_eth).toString() - - function executeTransactionFlow(fromSafe, toSafe) { - visit(constants.BALANCE_URL + fromSafe) - assets.selectTokenList(assets.tokenListOptions.allTokens) - assets.clickOnSendBtn(1) - - loadsafe.inputOwnerAddress(0, toSafe) - assets.enterAmount(1) - navigation.clickOnNewTxBtnS() - tx.executeFlow_2() - cy.wait(5000) - } + it('Verify tx creation and execution of non-native token with with relay', () => { + cy.wait(2000) + const originatingSafe = safesData.SEP_FUNDS_SAFE_2.substring(4) + const amount = ethers.parseUnits(transferAmount, unit_eth).toString() - cy.wrap(null) - .then(() => { - return main.fetchCurrentNonce(network_pref + originatingSafe) - }) - .then(async (currentNonce) => { - executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount) + function executeTransactionFlow(fromSafe, toSafe) { + visit(constants.BALANCE_URL + fromSafe) + wallet.connectSigner(signer) + assets.selectTokenList(assets.tokenListOptions.allTokens) + assets.clickOnSendBtn(1) - const contractWithWallet = tokenContract.connect(signers[0]) - const tx = await contractWithWallet.transfer(originatingSafe, amount, { - gasLimit: 200000, - }) + loadsafe.inputOwnerAddress(0, toSafe) + assets.enterAmount(1) + navigation.clickOnNewTxBtnS() + tx.executeFlow_2() + cy.wait(5000) + } - await tx.wait() - main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + originatingSafe, constants.tokenAbbreviation.qtrust, qtrustBanance) + cy.wrap(null) + .then(() => { + return main.fetchCurrentNonce(network_pref + originatingSafe) + }) + .then(async (currentNonce) => { + executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount) + + const contractWithWallet = tokenContract.connect(signers[0]) + const tx = await contractWithWallet.transfer(originatingSafe, amount, { + gasLimit: 200000, }) - }, - ) + + await tx.wait() + main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) + main.checkTokenBalance(network_pref + originatingSafe, constants.tokenAbbreviation.qtrust, qtrustBanance) + }) + }) }) diff --git a/cypress/e2e/pages/assets.pages.js b/cypress/e2e/pages/assets.pages.js index c80b78b602..b92af57eb5 100644 --- a/cypress/e2e/pages/assets.pages.js +++ b/cypress/e2e/pages/assets.pages.js @@ -264,9 +264,9 @@ export function verifyTokenIsPresent(token) { export function selectTokenList(option) { cy.get(tokenListDropdown) - .click() + .click({ force: true }) .then(() => { - cy.get(option).click() + cy.get(option).click({ force: true }) }) } diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index 6dfe94971a..813ba6d614 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -1,4 +1,3 @@ -import * as constants from '../../support/constants' import * as main from '../pages/main.page' import { connectedWalletExecMethod } from '../pages/create_tx.pages' import * as sidebar from '../pages/sidebar.pages' @@ -12,8 +11,7 @@ const thresholdInput = 'input[name="threshold"]' export const removeOwnerBtn = 'button[aria-label="Remove signer"]' const connectingContainer = 'div[class*="connecting-container"]' const createNewSafeBtn = '[data-testid="create-safe-btn"]' -const connectWalletBtn = 'Connect wallet' -const continueWithWalletBtn = 'Continue with E2E Wallet' +const continueWithWalletBtn = 'Continue with Private key' const googleConnectBtn = '[data-testid="google-connect-btn"]' const googleSignedinBtn = '[data-testid="signed-in-account-btn"]' export const accountInfoHeader = '[data-testid="open-account-center"]' @@ -52,6 +50,7 @@ export const addSignerStr = 'Add signer' export const accountRecoveryStr = 'Account recovery' export const sendTokensStr = 'Send tokens' +const connectWalletBtn = '[data-testid="connect-wallet-btn"]' export function checkNotificationsSwitchIs(status) { cy.get(notificationsSwitch).find('input').should(`be.${status}`) } @@ -149,15 +148,6 @@ export function checkNetworkChangeWarningMsg() { cy.get('div').contains(changeNetworkWarningStr).should('exist') } -export function connectWallet() { - cy.get('onboard-v2') - .shadow() - .within(($modal) => { - cy.wrap($modal).contains('div', constants.connectWalletNames.e2e).click() - cy.wrap($modal).get(connectingContainer).should('exist') - }) -} - export function clickOnCreateNewSafeBtn() { cy.get(createNewSafeBtn).click().wait(1000) } @@ -168,7 +158,7 @@ export function clickOnContinueWithWalletBtn() { export function clickOnConnectWalletBtn() { cy.get(welcomeLoginScreen).within(() => { - cy.get('button').contains(connectWalletBtn).should('be.visible').should('be.enabled').click().wait(1000) + cy.get(connectWalletBtn).should('be.visible').should('be.enabled').click().wait(1000) }) } @@ -182,7 +172,7 @@ export function clearWalletName() { export function selectNetwork(network) { cy.wait(1000) - cy.get(expandMoreIcon).eq(1).parents('div').eq(1).click() + cy.get(expandMoreIcon).parents('div').eq(1).click() cy.wait(1000) let regex = new RegExp(`^${network}$`) cy.get('li').contains(regex).click() diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index 398d77e6aa..5e05873b53 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -304,8 +304,3 @@ export function verifyTextVisibility(stringsArray) { export function getIframeBody(iframe) { return cy.get(iframe).its('0.contentDocument.body').should('not.be.empty').then(cy.wrap) } - -export function connectSigner(signer) { - cy.get('button').contains('Connect').click() - cy.contains('Private key').click() -} diff --git a/cypress/e2e/regression/add_owner.cy.js b/cypress/e2e/regression/add_owner.cy.js index 23c1e802e4..6129ab561e 100644 --- a/cypress/e2e/regression/add_owner.cy.js +++ b/cypress/e2e/regression/add_owner.cy.js @@ -3,8 +3,11 @@ import * as main from '../../e2e/pages/main.page' 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' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Add Owners tests', () => { before(async () => { @@ -18,20 +21,17 @@ describe('Add Owners tests', () => { cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) - it('Verify Tooltip displays correct message for disconnected user', () => { - owner.waitForConnectionStatus() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() + it('Verify add owner button is disabled for disconnected user', () => { owner.verifyAddOwnerBtnIsDisabled() }) it('Verify the Add New Owner Form can be opened', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() }) it('Verify error message displayed if character limit is exceeded in Name input', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerName(main.generateRandomString(51)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) @@ -45,14 +45,14 @@ describe('Add Owners tests', () => { addressBook.clickOnSaveEntryBtn() addressBook.verifyNewEntryAdded(constants.addresBookContacts.user1.name, constants.addresBookContacts.user1.address) cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.addresBookContacts.user1.address) owner.verifyNewOwnerName(constants.addresBookContacts.user1.name) }) it('Verify that Name field not mandatory', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) owner.clickOnNextBtn() diff --git a/cypress/e2e/regression/address_book.cy.js b/cypress/e2e/regression/address_book.cy.js index aa5f04bcb8..1413b95a09 100644 --- a/cypress/e2e/regression/address_book.cy.js +++ b/cypress/e2e/regression/address_book.cy.js @@ -7,8 +7,11 @@ import * as main from '../../e2e/pages/main.page' import * as ls from '../../support/localstorage_data.js' import * as sidebar from '../pages/sidebar.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const NAME = 'Owner1' const EDITED_NAME = 'Edited Owner1' @@ -111,6 +114,7 @@ describe('Address book tests', () => { addressBook.clickOnImportFileBtn() addressBook.importCSVFile(addressBook.addedSafesCSVFile) addressBook.clickOnImportBtn() + wallet.connectSigner(signer) sidebar.openSidebar() sidebar.verifyAddedSafesExist([importedSafe]) }) diff --git a/cypress/e2e/regression/balances_pagination.cy.js b/cypress/e2e/regression/balances_pagination.cy.js index 7b2bff0a67..b110310b9c 100644 --- a/cypress/e2e/regression/balances_pagination.cy.js +++ b/cypress/e2e/regression/balances_pagination.cy.js @@ -4,14 +4,11 @@ import * as main from '../../e2e/pages/main.page' const ASSETS_LENGTH = 8 -// Skip until connection error is resolved -describe.skip('Balance pagination tests', () => { +describe('Balance pagination tests', () => { before(() => { cy.clearLocalStorage() - // Open the Safe used for testing cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_6) main.acceptCookies() - assets.selectTokenList(assets.tokenListOptions.allTokens) }) diff --git a/cypress/e2e/regression/batch_tx.cy.js b/cypress/e2e/regression/batch_tx.cy.js index 6986e03460..b9ecb13740 100644 --- a/cypress/e2e/regression/batch_tx.cy.js +++ b/cypress/e2e/regression/batch_tx.cy.js @@ -3,12 +3,15 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as owner from '../../e2e/pages/owners.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' const currentNonce = 3 const funds_first_tx = '0.001' const funds_second_tx = '0.002' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Batch transaction tests', () => { before(async () => { @@ -18,6 +21,7 @@ describe('Batch transaction tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_2) + wallet.connectSigner(signer) owner.waitForConnectionStatus() main.acceptCookies() }) diff --git a/cypress/e2e/regression/create_safe_cf.cy.js b/cypress/e2e/regression/create_safe_cf.cy.js index 94583c88dc..4b936701d9 100644 --- a/cypress/e2e/regression/create_safe_cf.cy.js +++ b/cypress/e2e/regression/create_safe_cf.cy.js @@ -6,8 +6,12 @@ import * as navigation from '../pages/navigation.page.js' import * as ls from '../../support/localstorage_data.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as safeapps from '../pages/safeapps.pages' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + const txOrder = [ 'Activate Safe now', 'Add another signer', @@ -31,6 +35,7 @@ describe('CF Safe regression tests', () => { it('Verify Add native assets and Create tx modals can be opened', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnAddFundsBtn() main.verifyElementsIsVisible([createwallet.qrCode]) @@ -43,13 +48,13 @@ describe('CF Safe regression tests', () => { it('Verify "0 out of 2 step completed" is shown in the dashboard', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() - owner.waitForConnectionStatus() createwallet.checkInitialStepsDisplayed() }) it('Verify "Add native assets" button opens a modal with a QR code and the safe address', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnAddFundsBtn() main.verifyElementsIsVisible([createwallet.qrCode, createwallet.addressInfo]) @@ -58,6 +63,7 @@ describe('CF Safe regression tests', () => { it('Verify QR code switch status change works in "Add native assets" modal', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnAddFundsBtn() createwallet.checkQRCodeSwitchStatus(constants.checkboxStates.checked) @@ -68,6 +74,7 @@ describe('CF Safe regression tests', () => { it('Verify "Create new transaction" modal contains tx types in sequence', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnCreateTxBtn() createwallet.checkAllTxTypesOrder(txOrder) @@ -76,15 +83,17 @@ describe('CF Safe regression tests', () => { it('Verify "Add safe now" button takes to a tx "Activate account"', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnCreateTxBtn() createwallet.clickOnTxType(txOrder[0]) - main.verifyElementsIsVisible([createwallet.activateAccountBtn]) + main.verifyElementsExist([createwallet.activateAccountBtn]) }) it('Verify "Add another Owner" takes to a tx Add owner', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnCreateTxBtn() createwallet.clickOnTxType(txOrder[1]) @@ -94,6 +103,7 @@ describe('CF Safe regression tests', () => { it('Verify "Setup recovery" button takes to the "Account recovery" flow', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnCreateTxBtn() createwallet.clickOnTxType(txOrder[2]) @@ -103,6 +113,7 @@ describe('CF Safe regression tests', () => { it('Verify "Send token" takes to the tx form to send tokens', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnCreateTxBtn() createwallet.clickOnTxType(txOrder[5]) @@ -117,6 +128,7 @@ describe('CF Safe regression tests', () => { ls.appPermissions(constants.safeTestAppurl).infoModalAccepted, ) cy.reload() + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnCreateTxBtn() createwallet.clickOnTxType(txOrder[4]) @@ -126,7 +138,6 @@ describe('CF Safe regression tests', () => { }) it('Verify "Notifications" in the settings are disabled', () => { - owner.waitForConnectionStatus() main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() cy.visit(constants.notificationsUrl + staticSafes.SEP_STATIC_SAFE_14) @@ -134,7 +145,6 @@ describe('CF Safe regression tests', () => { }) it('Verify in assets, that a "Add funds" block is present', () => { - owner.waitForConnectionStatus() main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_14) @@ -142,7 +152,6 @@ describe('CF Safe regression tests', () => { }) it('Verify clicking on "Activate now" button opens safe activation flow', () => { - owner.waitForConnectionStatus() main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_14) diff --git a/cypress/e2e/regression/create_safe_simple.cy.js b/cypress/e2e/regression/create_safe_simple.cy.js index 5755c3c42a..2580029ba8 100644 --- a/cypress/e2e/regression/create_safe_simple.cy.js +++ b/cypress/e2e/regression/create_safe_simple.cy.js @@ -3,16 +3,21 @@ import * as main from '../../e2e/pages/main.page' 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' + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Safe creation tests', () => { beforeEach(() => { cy.visit(constants.welcomeUrl + '?chain=sep') cy.clearLocalStorage() main.acceptCookies() + wallet.connectSigner(signer) + owner.waitForConnectionStatus() }) it('Verify Next button is disabled until switching to network is done', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.selectNetwork(constants.networks.ethereum) createwallet.clickOnCreateNewSafeBtn() @@ -24,7 +29,6 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify error message is displayed if wallet name input exceeds 50 characters', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.typeWalletName(main.generateRandomString(51)) @@ -35,7 +39,6 @@ describe('Safe creation tests', () => { // TODO: Replace wallet with Safe // TODO: Check unit tests it('Verify there is no error message is displayed if wallet name input contains less than 50 characters', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.typeWalletName(main.generateRandomString(50)) @@ -43,7 +46,6 @@ describe('Safe creation tests', () => { }) it('Verify current connected account is shown as default owner', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() @@ -52,7 +54,6 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify error message is displayed if owner name input exceeds 50 characters', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() owner.typeExistingOwnerName(main.generateRandomString(51)) @@ -61,7 +62,6 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify there is no error message is displayed if owner name input contains less than 50 characters', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() owner.typeExistingOwnerName(main.generateRandomString(50)) @@ -70,7 +70,6 @@ describe('Safe creation tests', () => { it('Verify data persistence', () => { const ownerName = 'David' - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() @@ -103,7 +102,6 @@ describe('Safe creation tests', () => { }) it('Verify tip is displayed on right side for threshold 1/1', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() @@ -112,7 +110,6 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify address input validation rules', () => { - owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() @@ -132,10 +129,12 @@ describe('Safe creation tests', () => { it('Verify duplicated signer error using the autocomplete feature', () => { cy.visit(constants.createNewSafeSepoliaUrl + '?chain=sep') - main - .addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName) + cy.wrap(null) + .then(() => + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName), + ) .then(() => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() diff --git a/cypress/e2e/regression/create_safe_simple_2.cy.js b/cypress/e2e/regression/create_safe_simple_2.cy.js index ea6f06e50f..f0ad5bf2bd 100644 --- a/cypress/e2e/regression/create_safe_simple_2.cy.js +++ b/cypress/e2e/regression/create_safe_simple_2.cy.js @@ -4,10 +4,13 @@ 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 safe from '../pages/load_safe.pages' +import * as wallet from '../../support/utils/wallet.js' const ownerSepolia = ['Automation owner Sepolia'] const ownerName = 'Owner name' const owner1 = 'Owner1' +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Safe creation tests 2', () => { beforeEach(() => { @@ -17,6 +20,7 @@ describe('Safe creation tests 2', () => { }) it('Cancel button cancels safe creation', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -27,6 +31,7 @@ describe('Safe creation tests 2', () => { // Owners and confirmation step it('Verify Next button is disabled when address is empty', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -36,11 +41,14 @@ describe('Safe creation tests 2', () => { }) it('Verify owner names are autocompleted if they are present in the Address book ', () => { - owner.waitForConnectionStatus() - main - .addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName) + cy.wrap(null) + .then(() => + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName), + ) .then(() => { cy.visit(constants.welcomeUrl + '?chain=sep') + wallet.connectSigner(signer) + owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() safe.clickOnNextBtn() @@ -49,11 +57,14 @@ describe('Safe creation tests 2', () => { }) it("Verify names don't autofill if they are added to another chain's Address book", () => { - owner.waitForConnectionStatus() - main - .addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName[1]) + cy.wrap(null) + .then(() => + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName[1]), + ) .then(() => { cy.visit(constants.welcomeUrl + '?chain=sep') + wallet.connectSigner(signer) + owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() safe.clickOnNextBtn() @@ -62,6 +73,7 @@ describe('Safe creation tests 2', () => { }) it('Verify an valid name for owner can be inputed', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -71,6 +83,7 @@ describe('Safe creation tests 2', () => { }) it('Verify Threshold matching required confirmations max with amount of owners', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -80,6 +93,7 @@ describe('Safe creation tests 2', () => { }) it('Verify deleting owner rows updates the currenlty set policies value', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -91,6 +105,7 @@ describe('Safe creation tests 2', () => { }) it('Verify ENS name in the address and name fields is resolved', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -101,6 +116,7 @@ describe('Safe creation tests 2', () => { }) it('Verify deleting owner rows is possible', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -112,11 +128,14 @@ describe('Safe creation tests 2', () => { }) it('Verify existing owner in address book will have their names filled when their address is pasted', () => { - main - .addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sepoliaAddress1) + cy.wrap(null) + .then(() => + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sepoliaAddress1), + ) .then(() => { - owner.waitForConnectionStatus() cy.visit(constants.welcomeUrl + '?chain=sep') + wallet.connectSigner(signer) + owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() safe.clickOnNextBtn() diff --git a/cypress/e2e/regression/create_tx.cy.js b/cypress/e2e/regression/create_tx.cy.js index fbb218bed9..9148283023 100644 --- a/cypress/e2e/regression/create_tx.cy.js +++ b/cypress/e2e/regression/create_tx.cy.js @@ -2,11 +2,15 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as createtx from '../../e2e/pages/create_tx.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] const sendValue = 0.00002 +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + function happyPathToStepTwo() { createtx.typeRecipientAddress(constants.EOA) createtx.clickOnTokenselectorAndSelectSepoliaEth() @@ -23,6 +27,7 @@ describe('Create transactions tests', () => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_6) main.acceptCookies() + wallet.connectSigner(signer) createtx.clickOnNewtransactionBtn() createtx.clickOnSendTokensBtn() }) diff --git a/cypress/e2e/regression/nfts.cy.js b/cypress/e2e/regression/nfts.cy.js index 700161492e..386af62674 100644 --- a/cypress/e2e/regression/nfts.cy.js +++ b/cypress/e2e/regression/nfts.cy.js @@ -4,6 +4,7 @@ import * as nfts from '../pages/nfts.pages' 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' const singleNFT = ['safeTransferFrom'] const multipleNFT = ['multiSend'] @@ -13,6 +14,9 @@ const NFTSentName = 'GTT #22' let nftsSafes, staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + describe('NFTs tests', () => { before(() => { getSafes(CATEGORIES.nfts) @@ -29,6 +33,7 @@ describe('NFTs tests', () => { cy.clearLocalStorage() cy.visit(constants.balanceNftsUrl + staticSafes.SEP_STATIC_SAFE_2) main.acceptCookies() + wallet.connectSigner(signer) nfts.waitForNftItems(2) }) @@ -82,6 +87,7 @@ describe('NFTs tests', () => { it('Verify Send NFT transaction has been created', () => { cy.visit(constants.balanceNftsUrl + nftsSafes.SEP_NFT_SAFE_1) + wallet.connectSigner(signer) nfts.verifyInitialNFTData() nfts.selectNFTs(1) nfts.sendNFT() diff --git a/cypress/e2e/regression/remove_owner.cy.js b/cypress/e2e/regression/remove_owner.cy.js index 7c92dc766b..15b0e69632 100644 --- a/cypress/e2e/regression/remove_owner.cy.js +++ b/cypress/e2e/regression/remove_owner.cy.js @@ -4,8 +4,11 @@ import * as owner from '../pages/owners.pages' import * as createwallet from '../pages/create_wallet.pages' import * as createTx from '../pages/create_tx.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Remove Owners tests', () => { before(async () => { @@ -17,7 +20,6 @@ describe('Remove Owners tests', () => { main.waitForHistoryCallToComplete() cy.clearLocalStorage() main.acceptCookies() - owner.waitForConnectionStatus() cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) @@ -31,23 +33,24 @@ describe('Remove Owners tests', () => { main.verifyElementsCount(owner.removeOwnerBtn, 0) }) - it('Verify Tooltip displays correct message for disconnected user', () => { - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() + it('Verify remove owner button is disabled for disconnected user', () => { owner.verifyRemoveBtnIsDisabled() }) it('Verify owner removal form can be opened', () => { + wallet.connectSigner(signer) owner.openRemoveOwnerWindow(1) }) it('Verify threshold input displays the upper limit as the current safe number of owners minus one', () => { + wallet.connectSigner(signer) owner.openRemoveOwnerWindow(1) owner.verifyThresholdLimit(1, 1) owner.getThresholdOptions().should('have.length', 1) }) it('Verify owner deletion transaction has been created', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openRemoveOwnerWindow(1) cy.wait(3000) diff --git a/cypress/e2e/regression/replace_owner.cy.js b/cypress/e2e/regression/replace_owner.cy.js index ce408ba072..f332faded2 100644 --- a/cypress/e2e/regression/replace_owner.cy.js +++ b/cypress/e2e/regression/replace_owner.cy.js @@ -4,8 +4,11 @@ import * as owner from '../pages/owners.pages' import * as addressBook from '../pages/address_book.page' import * as createTx from '../pages/create_tx.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const ownerName = 'Replacement Signer Name' @@ -22,14 +25,12 @@ describe('Replace Owners tests', () => { }) it('Verify Tooltip displays correct message for disconnected user', () => { - owner.waitForConnectionStatus() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() owner.verifyReplaceBtnIsDisabled() }) // TODO: Check unit tests it('Verify max characters in name field', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() owner.typeOwnerName(main.generateRandomString(51)) @@ -45,6 +46,7 @@ describe('Replace Owners tests', () => { addressBook.clickOnSaveEntryBtn() addressBook.verifyNewEntryAdded(constants.addresBookContacts.user1.name, constants.addresBookContacts.user1.address) cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() owner.typeOwnerAddress(constants.addresBookContacts.user1.address) @@ -52,6 +54,7 @@ describe('Replace Owners tests', () => { }) it('Verify that Name field not mandatory. Verify confirmation for owner replacement is displayed', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) @@ -60,6 +63,7 @@ describe('Replace Owners tests', () => { }) it('Verify relevant error messages are displayed in Address input', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() owner.typeOwnerAddress(main.generateRandomString(10)) @@ -80,6 +84,7 @@ describe('Replace Owners tests', () => { it("Verify 'Replace' tx is created", () => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() cy.wait(1000) diff --git a/cypress/e2e/regression/sidebar.cy.js b/cypress/e2e/regression/sidebar.cy.js index 024f278bd7..597b8e3816 100644 --- a/cypress/e2e/regression/sidebar.cy.js +++ b/cypress/e2e/regression/sidebar.cy.js @@ -3,8 +3,11 @@ import * as main from '../pages/main.page' import * as sideBar from '../pages/sidebar.pages' import * as navigation from '../pages/navigation.page' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Sidebar tests', () => { before(async () => { @@ -39,17 +42,17 @@ describe('Sidebar tests', () => { }) it('Verify New transaction button enabled for owners', () => { + wallet.connectSigner(signer) sideBar.verifyNewTxBtnStatus(constants.enabledStates.enabled) }) it('Verify New transaction button enabled for beneficiaries who are non-owners', () => { cy.visit(constants.homeUrl + staticSafes.SEP_STATIC_SAFE_11) + wallet.connectSigner(signer) sideBar.verifyNewTxBtnStatus(constants.enabledStates.enabled) }) it('Verify New Transaction button disabled for non-owners', () => { - navigation.clickOnWalletExpandMoreIcon() - navigation.clickOnDisconnectBtn() main.verifyElementsCount(navigation.newTxBtn, 0) }) diff --git a/cypress/e2e/regression/sidebar_nonowner.cy.js b/cypress/e2e/regression/sidebar_nonowner.cy.js index 69051fe5a0..629bbbea48 100644 --- a/cypress/e2e/regression/sidebar_nonowner.cy.js +++ b/cypress/e2e/regression/sidebar_nonowner.cy.js @@ -4,8 +4,11 @@ import * as sideBar from '../pages/sidebar.pages.js' import * as navigation from '../pages/navigation.page.js' import * as ls from '../../support/localstorage_data.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const addedOwner = 'Added owner' const addedNonowner = 'Added non-owner' @@ -25,6 +28,7 @@ describe('Sidebar non-owner tests', () => { }) it('Verify New Transaction button enabled for users with Spending limits allowed', () => { + wallet.connectSigner(signer) navigation.verifyTxBtnStatus(constants.enabledStates.enabled) }) diff --git a/cypress/e2e/regression/spending_limits.cy.js b/cypress/e2e/regression/spending_limits.cy.js index 9efac54169..91513b1350 100644 --- a/cypress/e2e/regression/spending_limits.cy.js +++ b/cypress/e2e/regression/spending_limits.cy.js @@ -1,13 +1,15 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' import * as spendinglimit from '../pages/spending_limits.pages' -import * as owner from '../pages/owners.pages' import * as navigation from '../pages/navigation.page' 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' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY const tokenAmount = 0.1 const newTokenAmount = 0.001 @@ -22,12 +24,12 @@ describe('Spending limits tests', () => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_8) cy.clearLocalStorage() main.acceptCookies() - owner.waitForConnectionStatus() cy.get(spendinglimit.spendingLimitsSection).should('be.visible') }) it('Verify that the Review step shows beneficiary, amount allowed, reset time', () => { //Assume that default reset time is set to One time + wallet.connectSigner(signer) spendinglimit.clickOnNewSpendingLimitBtn() spendinglimit.enterBeneficiaryAddress(staticSafes.SEP_STATIC_SAFE_6) spendinglimit.enterSpendingLimitAmount(0.1) @@ -44,24 +46,28 @@ describe('Spending limits tests', () => { }) it('Verify Spending limit option is available when selecting the corresponding token', () => { + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.verifyTxOptionExist([spendinglimit.spendingLimitTxOption]) }) it('Verify spending limit option shows available amount', () => { + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.verifySpendingOptionShowsBalance([spendingLimitBalance]) }) it('Verify when owner is a delegate, standard tx and spending limit tx are present', () => { + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.verifyTxOptionExist([spendinglimit.spendingLimitTxOption, spendinglimit.standardTx]) }) it('Verify when spending limit is selected the nonce field is removed', () => { + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.selectSpendingLimitOption() @@ -69,6 +75,7 @@ describe('Spending limits tests', () => { }) it('Verify "Max" button value set to be no more than the allowed amount', () => { + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.clickOnMaxBtn() @@ -76,12 +83,14 @@ describe('Spending limits tests', () => { }) it('Verify selecting a native token from the dropdown in new tx', () => { + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.selectToken(constants.tokenNames.sepoliaEther) }) it('Verify that when replacing spending limit for the same owner, previous values are displayed in red', () => { + wallet.connectSigner(signer) spendinglimit.clickOnNewSpendingLimitBtn() spendinglimit.enterBeneficiaryAddress(constants.DEFAULT_OWNER_ADDRESS) spendinglimit.enterSpendingLimitAmount(newTokenAmount) @@ -92,6 +101,7 @@ describe('Spending limits tests', () => { }) it('Verify that when editing spending limit for owner who used some of it, relevant actions are displayed', () => { + wallet.connectSigner(signer) spendinglimit.clickOnNewSpendingLimitBtn() spendinglimit.enterBeneficiaryAddress(constants.SPENDING_LIMIT_ADDRESS_2) spendinglimit.enterSpendingLimitAmount(newTokenAmount) @@ -112,6 +122,7 @@ describe('Spending limits tests', () => { ) .then(() => { cy.reload() + wallet.connectSigner(signer) navigation.clickOnNewTxBtn() tx.clickOnSendTokensBtn() spendinglimit.clickOnTokenDropdown() @@ -129,6 +140,7 @@ describe('Spending limits tests', () => { ) .then(() => { cy.reload() + wallet.connectSigner(signer) spendinglimit.clickOnNewSpendingLimitBtn() spendinglimit.enterBeneficiaryAddress(constants.DEFAULT_OWNER_ADDRESS.substring(30)) spendinglimit.selectRecipient(constants.DEFAULT_OWNER_ADDRESS) diff --git a/cypress/e2e/regression/spending_limits_nonowner.cy.js b/cypress/e2e/regression/spending_limits_nonowner.cy.js index fb2e0eaeed..e64bda33fb 100644 --- a/cypress/e2e/regression/spending_limits_nonowner.cy.js +++ b/cypress/e2e/regression/spending_limits_nonowner.cy.js @@ -1,7 +1,6 @@ import * as constants from '../../support/constants.js' import * as main from '../pages/main.page.js' import * as spendinglimit from '../pages/spending_limits.pages.js' -import * as owner from '../pages/owners.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' let staticSafes = [] @@ -15,7 +14,6 @@ describe('Spending limits non-owner tests', () => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) cy.clearLocalStorage() main.acceptCookies() - owner.waitForConnectionStatus() cy.get(spendinglimit.spendingLimitsSection).should('be.visible') }) diff --git a/cypress/e2e/regression/swaps_tokens.cy.js b/cypress/e2e/regression/swaps_tokens.cy.js index a303fbb20b..b642b8f154 100644 --- a/cypress/e2e/regression/swaps_tokens.cy.js +++ b/cypress/e2e/regression/swaps_tokens.cy.js @@ -3,8 +3,11 @@ import * as main from '../pages/main.page.js' import * as swaps from '../pages/swaps.pages.js' import * as assets from '../pages/assets.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY let iframeSelector = `iframe[src*="${constants.swapWidget}"]` @@ -23,6 +26,7 @@ describe('[SMOKE] Swaps token tests', () => { 'Verify that clicking the swap from assets tab, autofills that token automatically in the form', { defaultCommandTimeout: 30000 }, () => { + wallet.connectSigner(signer) assets.selectTokenList(assets.tokenListOptions.allTokens) swaps.clickOnAssetSwapBtn(0) diff --git a/cypress/e2e/regression/tokens.cy.js b/cypress/e2e/regression/tokens.cy.js index 0654402338..e941adeca3 100644 --- a/cypress/e2e/regression/tokens.cy.js +++ b/cypress/e2e/regression/tokens.cy.js @@ -1,7 +1,6 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' import * as assets from '../pages/assets.pages' -import * as owner from '../pages/owners.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' const ASSET_NAME_COLUMN = 0 @@ -173,10 +172,6 @@ describe('Tokens tests', () => { //Include in smoke. it('Verify that when owner is disconnected, Send button is disabled', () => { - //waits for the user to look connected. Sends a default prefix "sep:" if it is called with no params - main.verifyOwnerConnected() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() assets.selectTokenList(assets.tokenListOptions.allTokens) assets.showSendBtn(0) assets.VerifySendButtonIsDisabled() diff --git a/cypress/e2e/smoke/add_owner.cy.js b/cypress/e2e/smoke/add_owner.cy.js index 8d3fb799e4..4539f4cb49 100644 --- a/cypress/e2e/smoke/add_owner.cy.js +++ b/cypress/e2e/smoke/add_owner.cy.js @@ -2,9 +2,12 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as owner from '../pages/owners.pages' import * as navigation from '../pages/navigation.page' +import * as wallet from '../../support/utils/wallet.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Add Owners tests', () => { before(async () => { @@ -19,19 +22,9 @@ describe('[SMOKE] Add Owners tests', () => { main.verifyElementsExist([navigation.setupSection]) }) - it('[SMOKE] Verify the presence of "Add Owner" button', () => { - owner.verifyAddOwnerBtnIsEnabled() - }) - - it('[SMOKE] Verify “Add new owner” button tooltip displays correct message for Non-Owner', () => { - cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) - main.waitForHistoryCallToComplete() - owner.verifyAddOwnerBtnIsDisabled() - }) - // TODO: Check if this test is covered with unit tests it('[SMOKE] Verify relevant error messages are displayed in Address input', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerAddress(main.generateRandomString(10)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) @@ -49,15 +42,26 @@ describe('[SMOKE] Add Owners tests', () => { owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.alreadyAdded) }) + it('[SMOKE] Verify the presence of "Add Owner" button', () => { + wallet.connectSigner(signer) + owner.verifyAddOwnerBtnIsEnabled() + }) + + it('[SMOKE] Verify “Add new owner” button is disabled for Non-Owner', () => { + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) + main.waitForHistoryCallToComplete() + owner.verifyAddOwnerBtnIsDisabled() + }) + it('[SMOKE] Verify default threshold value. Verify correct threshold calculation', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS) owner.verifyThreshold(1, 2) }) it('[SMOKE] Verify valid Address validation', () => { - owner.waitForConnectionStatus() + wallet.connectSigner(signer) owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) owner.clickOnNextBtn() diff --git a/cypress/e2e/smoke/address_book.cy.js b/cypress/e2e/smoke/address_book.cy.js index 8be8923ffa..842369c382 100644 --- a/cypress/e2e/smoke/address_book.cy.js +++ b/cypress/e2e/smoke/address_book.cy.js @@ -2,6 +2,7 @@ import 'cypress-file-upload' import * as constants from '../../support/constants' import * as addressBook from '../../e2e/pages/address_book.page' import * as main from '../../e2e/pages/main.page' +import * as wallet from '../../support/utils/wallet.js' import * as ls from '../../support/localstorage_data.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' @@ -14,6 +15,8 @@ const duplicateEntry = 'test-sepolia-90' const owner1 = 'Automation owner' const recipientData = [owner1, constants.DEFAULT_OWNER_ADDRESS] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Address book tests', () => { before(async () => { @@ -104,6 +107,7 @@ describe('[SMOKE] Address book tests', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sepoliaAddress2) cy.wait(1000) cy.reload() + wallet.connectSigner(signer) addressBook.clickOnSendBtn() addressBook.verifyRecipientData(recipientData) }) diff --git a/cypress/e2e/smoke/assets.cy.js b/cypress/e2e/smoke/assets.cy.js index fc9562cd4b..368b266f36 100644 --- a/cypress/e2e/smoke/assets.cy.js +++ b/cypress/e2e/smoke/assets.cy.js @@ -2,12 +2,12 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as assets from '../pages/assets.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] -const ASSET_NAME_COLUMN = 0 -const TOKEN_AMOUNT_COLUMN = 1 -const FIAT_AMOUNT_COLUMN = 2 +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Assets tests', () => { const fiatRegex = assets.fiatRegex @@ -22,14 +22,14 @@ describe('[SMOKE] Assets tests', () => { main.acceptCookies() }) - it('[SMOKE] Verify that the token tab is selected by default and the table is visible', () => { - assets.verifyTokensTabIsSelected('true') - }) - it('[SMOKE] Verify that the native token is visible', () => { assets.verifyTokenIsPresent(constants.tokenNames.sepoliaEther) }) + it('[SMOKE] Verify that the token tab is selected by default and the table is visible', () => { + assets.verifyTokensTabIsSelected('true') + }) + it('[SMOKE] Verify that Token list dropdown down options show/hide spam tokens', () => { let spamTokens = [ assets.currencyAave, @@ -53,6 +53,7 @@ describe('[SMOKE] Assets tests', () => { }) it('[SMOKE] Verify that clicking the button with an owner opens the Send funds form', () => { + wallet.connectSigner(signer) assets.selectTokenList(assets.tokenListOptions.allTokens) assets.clickOnSendBtn(0) }) diff --git a/cypress/e2e/smoke/batch_tx.cy.js b/cypress/e2e/smoke/batch_tx.cy.js index 01a5f1a929..93f459c7ec 100644 --- a/cypress/e2e/smoke/batch_tx.cy.js +++ b/cypress/e2e/smoke/batch_tx.cy.js @@ -1,9 +1,9 @@ import * as batch from '../pages/batches.pages' import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' -import * as owner from '../../e2e/pages/owners.pages.js' import * as ls from '../../support/localstorage_data.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] @@ -11,6 +11,9 @@ const currentNonce = 3 const funds_first_tx = '0.001' const funds_second_tx = '0.002' +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + describe('[SMOKE] Batch transaction tests', () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) @@ -19,16 +22,17 @@ describe('[SMOKE] Batch transaction tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_2) - owner.waitForConnectionStatus() main.acceptCookies() }) it('[SMOKE] Verify empty batch list can be opened', () => { + wallet.connectSigner(signer) batch.openBatchtransactionsModal() batch.openNewTransactionModal() }) it('[SMOKE] Verify a transaction can be added to the batch', () => { + wallet.connectSigner(signer) batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) cy.contains(batch.transactionAddedToBatchStr).should('be.visible') batch.verifyBatchIconCount(1) @@ -43,6 +47,7 @@ describe('[SMOKE] Batch transaction tests', () => { .then(() => { cy.reload() batch.clickOnBatchCounter() + wallet.connectSigner(signer) batch.clickOnConfirmBatchBtn() batch.verifyBatchTransactionsCount(2) batch.clickOnBatchCounter() @@ -59,6 +64,7 @@ describe('[SMOKE] Batch transaction tests', () => { .then(() => { cy.reload() batch.clickOnBatchCounter() + wallet.connectSigner(signer) cy.contains(batch.batchedTransactionsStr).should('be.visible').parents('aside').find('ul > li').as('BatchList') cy.get('@BatchList').find(batch.deleteTransactionbtn).eq(0).click() cy.get('@BatchList').should('have.length', 1) diff --git a/cypress/e2e/smoke/create_safe_cf.cy.js b/cypress/e2e/smoke/create_safe_cf.cy.js index a882074c23..6eba4ed3e8 100644 --- a/cypress/e2e/smoke/create_safe_cf.cy.js +++ b/cypress/e2e/smoke/create_safe_cf.cy.js @@ -2,6 +2,10 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' import * as createwallet from '../pages/create_wallet.pages' import * as owner from '../pages/owners.pages' +import * as wallet from '../../support/utils/wallet.js' + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] CF Safe creation tests', () => { beforeEach(() => { @@ -10,6 +14,7 @@ describe('[SMOKE] CF Safe creation tests', () => { main.acceptCookies() }) it('[SMOKE] CF creation happy path', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() diff --git a/cypress/e2e/smoke/create_safe_simple.cy.js b/cypress/e2e/smoke/create_safe_simple.cy.js index 2d86511c8d..3b749b1e88 100644 --- a/cypress/e2e/smoke/create_safe_simple.cy.js +++ b/cypress/e2e/smoke/create_safe_simple.cy.js @@ -2,6 +2,10 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as createwallet from '../pages/create_wallet.pages' import * as owner from '../pages/owners.pages' +import * as wallet from '../../support/utils/wallet.js' + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Safe creation tests', () => { beforeEach(() => { @@ -10,17 +14,18 @@ describe('[SMOKE] Safe creation tests', () => { main.acceptCookies() }) it('[SMOKE] Verify a Wallet can be connected', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() owner.clickOnWalletExpandMoreIcon() owner.clickOnDisconnectBtn() - createwallet.clickOnConnectWalletBtn() - createwallet.connectWallet() + wallet.connectSigner(signer) owner.waitForConnectionStatus() }) it('[SMOKE] Verify that a new Wallet has default name related to the selected network', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -28,6 +33,7 @@ describe('[SMOKE] Safe creation tests', () => { }) it('[SMOKE] Verify Add and Remove Owner Row works as expected', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() @@ -43,6 +49,7 @@ describe('[SMOKE] Safe creation tests', () => { }) it('[SMOKE] Verify Threshold Setup', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() diff --git a/cypress/e2e/smoke/create_tx.cy.js b/cypress/e2e/smoke/create_tx.cy.js index ebc2735dee..0a7dd1c6f7 100644 --- a/cypress/e2e/smoke/create_tx.cy.js +++ b/cypress/e2e/smoke/create_tx.cy.js @@ -2,18 +2,14 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as createtx from '../../e2e/pages/create_tx.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] -const sendValue = 0.00002 const currentNonce = 5 -function happyPathToStepTwo() { - createtx.typeRecipientAddress(constants.EOA) - createtx.clickOnTokenselectorAndSelectSepoliaEth() - createtx.setSendValue(sendValue) - createtx.clickOnNextBtn() -} +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Create transactions tests', () => { before(async () => { @@ -24,10 +20,16 @@ describe('[SMOKE] Create transactions tests', () => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_10) main.acceptCookies() + wallet.connectSigner(signer) createtx.clickOnNewtransactionBtn() createtx.clickOnSendTokensBtn() }) + it('[SMOKE] Verify MaxAmount button', () => { + createtx.setMaxAmount() + createtx.verifyMaxAmount(constants.tokenNames.sepoliaEther, constants.tokenAbbreviation.sep) + }) + it('[SMOKE] Verify error messages for invalid address input', () => { createtx.verifyRandomStringAddress('Lorem Ipsum') createtx.verifyWrongChecksum(constants.WRONGLY_CHECKSUMMED_ADDRESS) @@ -43,11 +45,6 @@ describe('[SMOKE] Create transactions tests', () => { createtx.verifyAmountLargerThanCurrentBalance() }) - it('[SMOKE] Verify MaxAmount button', () => { - createtx.setMaxAmount() - createtx.verifyMaxAmount(constants.tokenNames.sepoliaEther, constants.tokenAbbreviation.sep) - }) - it('[SMOKE] Verify nonce tooltip warning messages', () => { createtx.changeNonce(0) createtx.verifyTooltipMessage(constants.nonceTooltipMsg.lowerThanCurrent + currentNonce.toString()) @@ -56,30 +53,4 @@ describe('[SMOKE] Create transactions tests', () => { createtx.changeNonce(currentNonce + 150) createtx.verifyTooltipMessage(constants.nonceTooltipMsg.muchHigherThanRecommended) }) - - it('[SMOKE] Verify advance parameters gas limit input', () => { - cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_6) - createtx.clickOnNewtransactionBtn() - createtx.clickOnSendTokensBtn() - happyPathToStepTwo() - createtx.changeNonce('1') - createtx.selectCurrentWallet() - createtx.openExecutionParamsModal() - createtx.verifyAndSubmitExecutionParams() - }) - - it('[SMOKE] Verify a transaction shows relayer and addToBatch button', () => { - cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_6) - createtx.clickOnNewtransactionBtn() - createtx.clickOnSendTokensBtn() - happyPathToStepTwo() - createtx.verifySubmitBtnIsEnabled() - createtx.verifyNativeTokenTransfer() - createtx.changeNonce('1') - createtx.verifyConfirmTransactionData() - createtx.verifyRelayerAttemptsAvailable() - createtx.selectCurrentWallet() - createtx.clickOnNoLaterOption() - createtx.verifyAddToBatchBtnIsEnabled() - }) }) diff --git a/cypress/e2e/smoke/create_tx_2.cy.js b/cypress/e2e/smoke/create_tx_2.cy.js new file mode 100644 index 0000000000..7030235f60 --- /dev/null +++ b/cypress/e2e/smoke/create_tx_2.cy.js @@ -0,0 +1,54 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as createtx from '../pages/create_tx.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' + +let staticSafes = [] + +const sendValue = 0.00002 + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + +function happyPathToStepTwo() { + createtx.typeRecipientAddress(constants.EOA) + createtx.clickOnTokenselectorAndSelectSepoliaEth() + createtx.setSendValue(sendValue) + createtx.clickOnNextBtn() +} + +describe('[SMOKE] Create transactions tests 2', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_6) + main.acceptCookies() + wallet.connectSigner(signer) + createtx.clickOnNewtransactionBtn() + createtx.clickOnSendTokensBtn() + }) + + it('[SMOKE] Verify advance parameters gas limit input', () => { + happyPathToStepTwo() + createtx.changeNonce('1') + createtx.selectCurrentWallet() + createtx.openExecutionParamsModal() + createtx.verifyAndSubmitExecutionParams() + }) + + it('[SMOKE] Verify a transaction shows relayer and addToBatch button', () => { + happyPathToStepTwo() + createtx.verifySubmitBtnIsEnabled() + createtx.verifyNativeTokenTransfer() + createtx.changeNonce('1') + createtx.verifyConfirmTransactionData() + createtx.verifyRelayerAttemptsAvailable() + createtx.selectCurrentWallet() + createtx.clickOnNoLaterOption() + createtx.verifyAddToBatchBtnIsEnabled() + }) +}) diff --git a/cypress/e2e/smoke/import_export_data_2.cy.js b/cypress/e2e/smoke/import_export_data_2.cy.js index 675ace9b87..7aa5ee3dca 100644 --- a/cypress/e2e/smoke/import_export_data_2.cy.js +++ b/cypress/e2e/smoke/import_export_data_2.cy.js @@ -4,6 +4,7 @@ import * as main from '../pages/main.page.js' import * as constants from '../../support/constants.js' import * as sidebar from '../pages/sidebar.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] @@ -14,6 +15,9 @@ const invalidJsonPath_3 = 'cypress/fixtures/test-empty-batch.json' const appNames = ['Transaction Builder'] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + describe('[SMOKE] Import Export Data tests 2', () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) @@ -26,11 +30,13 @@ describe('[SMOKE] Import Export Data tests 2', () => { }) it('[SMOKE] Verify that the Sidebar Import button opens an import modal', () => { + wallet.connectSigner(signer) sidebar.openSidebar() sidebar.clickOnSidebarImportBtn() }) it('[SMOKE] Verify that correctly formatted json file can be uploaded and shows data', () => { + wallet.connectSigner(signer) sidebar.openSidebar() sidebar.clickOnSidebarImportBtn() file.dragAndDropFile(validJsonPath) @@ -44,6 +50,7 @@ describe('[SMOKE] Import Export Data tests 2', () => { }) it('[SMOKE] Verify that only json files can be imported', () => { + wallet.connectSigner(signer) sidebar.openSidebar() sidebar.clickOnSidebarImportBtn() file.dragAndDropFile(invalidJsonPath) @@ -58,6 +65,7 @@ describe('[SMOKE] Import Export Data tests 2', () => { }) it('[SMOKE] Verify that json files with wrong information are rejected', () => { + wallet.connectSigner(signer) sidebar.openSidebar() sidebar.clickOnSidebarImportBtn() file.dragAndDropFile(invalidJsonPath_3) diff --git a/cypress/e2e/smoke/messages_offchain.cy.js b/cypress/e2e/smoke/messages_offchain.cy.js index 930af7f4b2..78839b83b6 100644 --- a/cypress/e2e/smoke/messages_offchain.cy.js +++ b/cypress/e2e/smoke/messages_offchain.cy.js @@ -6,6 +6,7 @@ import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as modal from '../pages/modals.page' import * as messages from '../pages/messages.pages.js' import * as msg_confirmation_modal from '../pages/modals/message_confirmation.pages.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] const offchainMessage = 'Test message 2 off-chain' @@ -13,6 +14,9 @@ const offchainMessage = 'Test message 2 off-chain' const typeMessagesGeneral = msg_data.type.general const typeMessagesOffchain = msg_data.type.offChain +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + describe('[SMOKE] Offchain Messages tests', () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) @@ -77,6 +81,7 @@ describe('[SMOKE] Offchain Messages tests', () => { }) it('[SMOKE] Verify confirmation window is displayed for unsigned message', () => { + wallet.connectSigner(signer) messages.clickOnMessageSignBtn(2) msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmMsg) msg_confirmation_modal.verifyMessagePresent(offchainMessage) diff --git a/cypress/e2e/smoke/replace_owner.cy.js b/cypress/e2e/smoke/replace_owner.cy.js index 60dd4e5b84..ca931b8956 100644 --- a/cypress/e2e/smoke/replace_owner.cy.js +++ b/cypress/e2e/smoke/replace_owner.cy.js @@ -2,8 +2,11 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as owner from '../pages/owners.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Replace Owners tests', () => { before(async () => { @@ -18,17 +21,17 @@ describe('[SMOKE] Replace Owners tests', () => { }) it('[SMOKE] Verify that "Replace" icon is visible', () => { + wallet.connectSigner(signer) owner.verifyReplaceBtnIsEnabled() }) - // TODO: Remove "tooltip" from title - it('[SMOKE] Verify Tooltip displays correct message for Non-Owner', () => { + it('[SMOKE] Verify owner replace button is disabled for Non-Owner', () => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) - owner.waitForConnectionStatus() owner.verifyReplaceBtnIsDisabled() }) it('[SMOKE] Verify that the owner replacement form is opened', () => { + wallet.connectSigner(signer) owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() }) diff --git a/cypress/e2e/smoke/spending_limits.cy.js b/cypress/e2e/smoke/spending_limits.cy.js index e21de79926..63451967e1 100644 --- a/cypress/e2e/smoke/spending_limits.cy.js +++ b/cypress/e2e/smoke/spending_limits.cy.js @@ -3,8 +3,11 @@ import * as main from '../pages/main.page' import * as spendinglimit from '../pages/spending_limits.pages' import * as owner from '../pages/owners.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Spending limits tests', () => { before(async () => { @@ -15,6 +18,7 @@ describe('[SMOKE] Spending limits tests', () => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_8) cy.clearLocalStorage() main.acceptCookies() + wallet.connectSigner(signer) owner.waitForConnectionStatus() cy.get(spendinglimit.spendingLimitsSection).should('be.visible') spendinglimit.clickOnNewSpendingLimitBtn() diff --git a/cypress/support/constants.js b/cypress/support/constants.js index dc024bf477..1e83251268 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -33,7 +33,6 @@ export const BROWSER_PERMISSIONS_KEY = `${LS_NAMESPACE}SafeApps__browserPermissi export const SAFE_PERMISSIONS_KEY = `${LS_NAMESPACE}SafeApps__safePermissions` export const INFO_MODAL_KEY = `${LS_NAMESPACE}SafeApps__infoModal` -export const goerlyE2EWallet = /E2E Wallet @ G(ö|oe)rli/ export const goerlySafeName = /g(ö|oe)rli-safe/ export const sepoliaSafeName = 'sepolia-safe' export const goerliToken = /G(ö|oe)rli Ether/ @@ -238,7 +237,3 @@ export const localStorageKeys = { SAFE_v2__SafeApps__infoModal: 'SAFE_v2__SafeApps__infoModal', SAFE_v2__undeployedSafes: 'SAFE_v2__undeployedSafes', } - -export const connectWalletNames = { - e2e: 'E2E Wallet', -} diff --git a/cypress/support/utils/wallet.js b/cypress/support/utils/wallet.js new file mode 100644 index 0000000000..a9ea82fd88 --- /dev/null +++ b/cypress/support/utils/wallet.js @@ -0,0 +1,47 @@ +const onboardv2 = 'onboard-v2' +const pkInput = '[data-testid="private-key-input"]' +const pkConnectBtn = '[data-testid="pk-connect-btn"]' +const connectWalletBtn = '[data-testid="connect-wallet-btn"]' + +const privateKeyStr = 'Private key' + +export function connectSigner(signer) { + const actions = { + privateKey: () => { + cy.get(onboardv2) + .shadow() + .find('button') + .contains(privateKeyStr) + .click() + .then(() => handlePkConnect()) + }, + retry: () => { + cy.wait(1000).then(enterPrivateKey) + }, + } + + function handlePkConnect() { + cy.get('body').then(($body) => { + if ($body.find(pkConnectBtn).length > 0) { + cy.get(pkInput).find('input').clear().type(signer) + cy.get(pkConnectBtn).click() + } + }) + } + + function enterPrivateKey() { + cy.wait(1000) + cy.get(connectWalletBtn) + .should('be.enabled') + .and('be.visible') + .click() + .then(() => { + cy.get('body').then(($body) => { + const actionKey = $body.find(onboardv2).length > 0 ? 'privateKey' : 'retry' + actions[actionKey]() + }) + }) + } + + enterPrivateKey() +} diff --git a/src/components/common/ConnectWallet/ConnectWalletButton.tsx b/src/components/common/ConnectWallet/ConnectWalletButton.tsx index 648fec06ef..e5870315b8 100644 --- a/src/components/common/ConnectWallet/ConnectWalletButton.tsx +++ b/src/components/common/ConnectWallet/ConnectWalletButton.tsx @@ -21,6 +21,7 @@ const ConnectWalletButton = ({ return ( <Button + data-testid="connect-wallet-btn" onClick={handleConnect} variant={contained ? 'contained' : 'text'} size={small ? 'small' : 'medium'} diff --git a/src/hooks/wallets/useOnboard.ts b/src/hooks/wallets/useOnboard.ts index eb60896cd9..4520682198 100644 --- a/src/hooks/wallets/useOnboard.ts +++ b/src/hooks/wallets/useOnboard.ts @@ -9,7 +9,6 @@ import { logError, Errors } from '@/services/exceptions' import { trackEvent, WALLET_EVENTS } from '@/services/analytics' import { useAppSelector } from '@/store' import { type EnvState, selectRpc } from '@/store/settingsSlice' -import { E2E_WALLET_NAME } from '@/tests/e2e-wallet' import { formatAmount } from '@/utils/formatNumber' import { localItem } from '@/services/local-storage/local' import { isWalletConnect, isWalletUnlocked } from '@/utils/wallets' @@ -180,7 +179,7 @@ export const useInitOnboard = () => { // e2e wallet if (typeof window !== 'undefined' && window.Cypress) { connectWallet(onboard, { - autoSelect: { label: E2E_WALLET_NAME, disableModals: true }, + autoSelect: { label: 'e2e wallet', disableModals: true }, }) } diff --git a/src/hooks/wallets/wallets.ts b/src/hooks/wallets/wallets.ts index 6cab345d53..852b6ccf4f 100644 --- a/src/hooks/wallets/wallets.ts +++ b/src/hooks/wallets/wallets.ts @@ -1,4 +1,4 @@ -import { CYPRESS_MNEMONIC, IS_PRODUCTION, TREZOR_APP_URL, TREZOR_EMAIL, WC_PROJECT_ID } from '@/config/constants' +import { IS_PRODUCTION, TREZOR_APP_URL, TREZOR_EMAIL, WC_PROJECT_ID } from '@/config/constants' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import type { InitOptions } from '@web3-onboard/core' import coinbaseModule from '@web3-onboard/coinbase' @@ -9,7 +9,6 @@ import trezorModule from '@web3-onboard/trezor' import walletConnect from '@web3-onboard/walletconnect' import pkModule from '@/services/private-key-module' -import e2eWalletModule from '@/tests/e2e-wallet' import { CGW_NAMES, WALLET_KEYS } from './consts' const prefersDarkMode = (): boolean => { @@ -63,9 +62,6 @@ export const isWalletSupported = (disabledWallets: string[], walletLabel: string } export const getSupportedWallets = (chain: ChainInfo): WalletInits => { - if (window.Cypress && CYPRESS_MNEMONIC) { - return [e2eWalletModule(chain.chainId, chain.rpcUri) as WalletInit] - } const enabledWallets = Object.entries(WALLET_MODULES).filter(([key]) => isWalletSupported(chain.disabledWallets, key)) if (enabledWallets.length === 0) { diff --git a/src/services/private-key-module/PkModulePopup.tsx b/src/services/private-key-module/PkModulePopup.tsx index a32558f14c..5bdbbb2452 100644 --- a/src/services/private-key-module/PkModulePopup.tsx +++ b/src/services/private-key-module/PkModulePopup.tsx @@ -39,7 +39,7 @@ const PkModulePopup = () => { data-testid="private-key-input" /> - <Button variant="contained" color="primary" fullWidth type="submit"> + <Button data-testid="pk-connect-btn" variant="contained" color="primary" fullWidth type="submit"> Connect </Button> </form> From ec7a1e110e7c71a01dc221d8e1a00a758d8feb07 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov <daniel.d@safe.global> Date: Fri, 28 Jun 2024 15:55:06 +0200 Subject: [PATCH 111/154] Feat: TWAP decoding on confirmation screen [SW-2] (#3865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update safe-gateway-typescript-sdk to 3.21.6 * feat: add decoding for twap confirmation view * refactor: use the gateway-sdk types * fix: typo * fix: confirm tx was missing “twap order” title * refactor: improve times * fix: show correct start time * fix: Unknown transaction type: TwapOrder [SW-46] * fix: confirm transaction title was showing swap info * fix: match figma designs a bit better * fix: incorrect twap order title --- package.json | 2 +- src/components/common/Table/DataTable.tsx | 10 ++-- src/components/common/Table/styles.module.css | 2 +- .../tx-flow/flows/ConfirmTx/index.tsx | 8 +-- .../flows/SafeAppsTx/ReviewSafeAppsTx.tsx | 2 +- .../tx-flow/flows/SignMessage/index.tsx | 3 +- src/components/tx/DecodedTx/index.tsx | 4 +- src/components/tx/SignOrExecuteForm/index.tsx | 4 +- .../swap/components/SwapOrder/index.tsx | 31 +++-------- .../SwapOrder/rows/PartBuyAmount.tsx | 27 ++++++++++ .../SwapOrder/rows/PartDuration.tsx | 12 +++++ .../SwapOrder/rows/PartSellAmount.tsx | 27 ++++++++++ .../components/SwapOrder/twap.stories.tsx | 4 +- .../index.stories.tsx | 6 +-- .../SwapOrderConfirmationView/index.tsx | 52 ++++++++++++++++--- .../styles.module.css | 21 +++++++- src/features/swap/constants.ts | 4 ++ src/features/swap/helpers/swapOrderBuilder.ts | 17 +++--- src/features/swap/index.tsx | 7 +-- src/hooks/useDecodeTx.ts | 24 ++++----- src/hooks/useTransactionType.ts | 5 +- src/services/tx/extractTxInfo.ts | 12 +++-- src/utils/transaction-guards.ts | 27 ++++++++-- yarn.lock | 8 +-- 24 files changed, 221 insertions(+), 98 deletions(-) create mode 100644 src/features/swap/components/SwapOrder/rows/PartBuyAmount.tsx create mode 100644 src/features/swap/components/SwapOrder/rows/PartDuration.tsx create mode 100644 src/features/swap/components/SwapOrder/rows/PartSellAmount.tsx create mode 100644 src/features/swap/constants.ts diff --git a/package.json b/package.json index 27c9d3e8cb..9ef19d38ca 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@safe-global/protocol-kit": "^3.1.1", "@safe-global/safe-apps-sdk": "^9.1.0", "@safe-global/safe-deployments": "^1.36.0", - "@safe-global/safe-gateway-typescript-sdk": "^3.21.5", + "@safe-global/safe-gateway-typescript-sdk": "3.21.7", "@safe-global/safe-modules-deployments": "^1.2.0", "@sentry/react": "^7.91.0", "@spindl-xyz/attribution-lite": "^1.4.0", diff --git a/src/components/common/Table/DataTable.tsx b/src/components/common/Table/DataTable.tsx index 31c8fd450c..8d06bfb3f5 100644 --- a/src/components/common/Table/DataTable.tsx +++ b/src/components/common/Table/DataTable.tsx @@ -3,16 +3,18 @@ import { Stack, Typography } from '@mui/material' import type { DataRow } from '@/components/common/Table/DataRow' type DataTableProps = { - header: string + header?: string rows: ReactElement<typeof DataRow>[] } export const DataTable = ({ header, rows }: DataTableProps): ReactElement | null => { return ( <Stack gap="4px"> - <Typography variant="body1"> - <b>{header}</b> - </Typography> + {header && ( + <Typography variant="body1"> + <b>{header}</b> + </Typography> + )} {rows.map((row) => { return row })} diff --git a/src/components/common/Table/styles.module.css b/src/components/common/Table/styles.module.css index e2407529f6..ccc68717d1 100644 --- a/src/components/common/Table/styles.module.css +++ b/src/components/common/Table/styles.module.css @@ -1,6 +1,6 @@ .gridRow { display: grid; - grid-template-columns: 35% auto; + grid-template-columns: 25% auto; gap: var(--space-1); justify-content: flex-start; max-width: 900px; diff --git a/src/components/tx-flow/flows/ConfirmTx/index.tsx b/src/components/tx-flow/flows/ConfirmTx/index.tsx index aad926a8c9..47c91cc764 100644 --- a/src/components/tx-flow/flows/ConfirmTx/index.tsx +++ b/src/components/tx-flow/flows/ConfirmTx/index.tsx @@ -3,7 +3,6 @@ import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sd import TxLayout from '@/components/tx-flow/common/TxLayout' import ConfirmProposedTx from './ConfirmProposedTx' import { useTransactionType } from '@/hooks/useTransactionType' -import TxInfo from '@/components/transactions/TxInfo' import SwapIcon from '@/public/images/common/swap.svg' const ConfirmTxFlow = ({ txSummary }: { txSummary: TransactionSummary }) => { @@ -13,12 +12,7 @@ const ConfirmTxFlow = ({ txSummary }: { txSummary: TransactionSummary }) => { return ( <TxLayout title="Confirm transaction" - subtitle={ - <> - {text}  - {!isSwapOrder && <TxInfo info={txSummary.txInfo} withLogo={false} omitSign />} - </> - } + subtitle={<>{text} </>} icon={isSwapOrder && SwapIcon} step={0} txSummary={txSummary} diff --git a/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx b/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx index 4d1138ce0b..815195e7bc 100644 --- a/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx +++ b/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx @@ -1,4 +1,3 @@ -import { SWAP_TITLE } from '@/features/swap' import useWallet from '@/hooks/wallets/useWallet' import { assertWalletChain } from '@/services/tx/tx-sender/sdk' import { useContext, useEffect, useMemo } from 'react' @@ -16,6 +15,7 @@ import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' import { isTxValid } from '@/components/safe-apps/utils' import ErrorMessage from '@/components/tx/ErrorMessage' import { asError } from '@/services/exceptions/utils' +import { SWAP_TITLE } from '@/features/swap/constants' type ReviewSafeAppsTxProps = { safeAppsTx: SafeAppsTxParams diff --git a/src/components/tx-flow/flows/SignMessage/index.tsx b/src/components/tx-flow/flows/SignMessage/index.tsx index 4d0b3f64ef..ac9503ef59 100644 --- a/src/components/tx-flow/flows/SignMessage/index.tsx +++ b/src/components/tx-flow/flows/SignMessage/index.tsx @@ -1,12 +1,13 @@ import TxLayout from '@/components/tx-flow/common/TxLayout' import SignMessage, { type ConfirmProps, type ProposeProps } from '@/components/tx-flow/flows/SignMessage/SignMessage' -import { getSwapTitle, SWAP_TITLE } from '@/features/swap' +import { getSwapTitle } from '@/features/swap' import { selectSwapParams } from '@/features/swap/store/swapParamsSlice' import { useAppSelector } from '@/store' import { Box, Typography } from '@mui/material' import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard' import { ErrorBoundary } from '@sentry/react' import { type BaseTransaction } from '@safe-global/safe-apps-sdk' +import { SWAP_TITLE } from '@/features/swap/constants' const APP_LOGO_FALLBACK_IMAGE = '/images/apps/apps-icon.svg' const APP_NAME_FALLBACK = 'Sign message' diff --git a/src/components/tx/DecodedTx/index.tsx b/src/components/tx/DecodedTx/index.tsx index 506a83aef8..e31efca392 100644 --- a/src/components/tx/DecodedTx/index.tsx +++ b/src/components/tx/DecodedTx/index.tsx @@ -1,6 +1,6 @@ import SendToBlock from '@/components/tx/SendToBlock' import { useCurrentChain } from '@/hooks/useChains' -import { isSwapConfirmationViewOrder } from '@/utils/transaction-guards' +import { isConfirmationViewOrder } from '@/utils/transaction-guards' import { type SyntheticEvent, type ReactElement, memo } from 'react' import { Accordion, @@ -58,7 +58,7 @@ const DecodedTx = ({ }: DecodedTxProps): ReactElement | null => { const chainId = useChainId() const chain = useCurrentChain() - const isSwapOrder = isSwapConfirmationViewOrder(decodedData) + const isSwapOrder = isConfirmationViewOrder(decodedData) const isMultisend = !!decodedData?.parameters?.[0]?.valueDecoded diff --git a/src/components/tx/SignOrExecuteForm/index.tsx b/src/components/tx/SignOrExecuteForm/index.tsx index 32a62c86d4..344962ceac 100644 --- a/src/components/tx/SignOrExecuteForm/index.tsx +++ b/src/components/tx/SignOrExecuteForm/index.tsx @@ -27,7 +27,7 @@ import { TX_EVENTS } from '@/services/analytics/events/transactions' import { trackEvent } from '@/services/analytics' import useChainId from '@/hooks/useChainId' import PermissionsCheck from './PermissionsCheck' -import { isSwapConfirmationViewOrder } from '@/utils/transaction-guards' +import { isConfirmationViewOrder } from '@/utils/transaction-guards' import SwapOrderConfirmationView from '@/features/swap/components/SwapOrderConfirmationView' export type SubmitCallback = (txId: string, isExecuted?: boolean) => void @@ -76,7 +76,7 @@ export const SignOrExecuteForm = ({ const isCorrectNonce = useValidateNonce(safeTx) const [decodedData, decodedDataError, decodedDataLoading] = useDecodeTx(safeTx) const isBatchable = props.isBatchable !== false && safeTx && !isDelegateCall(safeTx) - const isSwapOrder = isSwapConfirmationViewOrder(decodedData) + const isSwapOrder = isConfirmationViewOrder(decodedData) const { safe } = useSafeInfo() const isCounterfactualSafe = !safe.deployed diff --git a/src/features/swap/components/SwapOrder/index.tsx b/src/features/swap/components/SwapOrder/index.tsx index ff5e4aa173..41351aa93a 100644 --- a/src/features/swap/components/SwapOrder/index.tsx +++ b/src/features/swap/components/SwapOrder/index.tsx @@ -32,6 +32,9 @@ import EthHashInfo from '@/components/common/EthHashInfo' import useSafeInfo from '@/hooks/useSafeInfo' import { isSwapOrderTxInfo, isTwapOrderTxInfo } from '@/utils/transaction-guards' import { EmptyRow } from '@/components/common/Table/EmptyRow' +import { PartDuration } from '@/features/swap/components/SwapOrder/rows/PartDuration' +import { PartSellAmount } from '@/features/swap/components/SwapOrder/rows/PartSellAmount' +import { PartBuyAmount } from '@/features/swap/components/SwapOrder/rows/PartBuyAmount' type SwapOrderProps = { txData?: TransactionData @@ -205,17 +208,7 @@ export const SellOrder = ({ order }: { order: SwapOrderType }) => { } export const TwapOrder = ({ order }: { order: SwapTwapOrder }) => { - const { - kind, - validUntil, - status, - sellToken, - buyToken, - numberOfParts, - partSellAmount, - minPartLimit, - timeBetweenParts, - } = order + const { kind, validUntil, status, numberOfParts } = order const isPartiallyFilled = isOrderPartiallyFilled(order) const expires = new Date(validUntil * 1000) @@ -235,20 +228,10 @@ export const TwapOrder = ({ order }: { order: SwapTwapOrder }) => { <DataRow title="No of parts" key="n_of_parts"> {numberOfParts} </DataRow>, - <DataRow title="Sell amount" key="sell_amount_part"> - <Typography component="span" fontWeight="bold"> - {formatVisualAmount(partSellAmount, sellToken.decimals)} {sellToken.symbol} - </Typography> - </DataRow>, - <DataRow title="Buy amount" key="buy_amount_part"> - <Typography component="span" fontWeight="bold"> - {formatVisualAmount(minPartLimit, buyToken.decimals)} {buyToken.symbol} - </Typography> - </DataRow>, + <PartSellAmount order={order} key="part_sell_amount" />, + <PartBuyAmount order={order} key="part_buy_amount" />, <FilledRow order={order} key="filled-row" />, - <DataRow title="Part duration" key="part_duration"> - {+timeBetweenParts / 60} minutes - </DataRow>, + <PartDuration order={order} key="part_duration" />, <EmptyRow key="spacer-1" />, status !== 'fulfilled' && compareAsc(now, expires) !== 1 ? ( <DataRow key="Expiry" title="Expiry"> diff --git a/src/features/swap/components/SwapOrder/rows/PartBuyAmount.tsx b/src/features/swap/components/SwapOrder/rows/PartBuyAmount.tsx new file mode 100644 index 0000000000..5076242c14 --- /dev/null +++ b/src/features/swap/components/SwapOrder/rows/PartBuyAmount.tsx @@ -0,0 +1,27 @@ +import { Typography } from '@mui/material' +import { formatVisualAmount } from '@/utils/formatters' +import { type TwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import { DataRow } from '@/components/common/Table/DataRow' +import { Box } from '@mui/system' + +export const PartBuyAmount = ({ + order, + addonText = '', +}: { + order: Pick<TwapOrder, 'minPartLimit' | 'buyToken'> + addonText?: string +}) => { + const { minPartLimit, buyToken } = order + return ( + <DataRow title="Buy amount" key="buy_amount_part"> + <Box> + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(minPartLimit, buyToken.decimals)} {buyToken.symbol} + </Typography> + <Typography component="span" color="var(--color-primary-light)"> + {` ${addonText}`} + </Typography> + </Box> + </DataRow> + ) +} diff --git a/src/features/swap/components/SwapOrder/rows/PartDuration.tsx b/src/features/swap/components/SwapOrder/rows/PartDuration.tsx new file mode 100644 index 0000000000..1154b513fc --- /dev/null +++ b/src/features/swap/components/SwapOrder/rows/PartDuration.tsx @@ -0,0 +1,12 @@ +import { DataRow } from '@/components/common/Table/DataRow' +import { type TwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import { getPeriod } from '@/utils/date' + +export const PartDuration = ({ order }: { order: Pick<TwapOrder, 'timeBetweenParts'> }) => { + const { timeBetweenParts } = order + return ( + <DataRow title="Part duration" key="part_duration"> + {getPeriod(+timeBetweenParts)} + </DataRow> + ) +} diff --git a/src/features/swap/components/SwapOrder/rows/PartSellAmount.tsx b/src/features/swap/components/SwapOrder/rows/PartSellAmount.tsx new file mode 100644 index 0000000000..ac8fcf13b6 --- /dev/null +++ b/src/features/swap/components/SwapOrder/rows/PartSellAmount.tsx @@ -0,0 +1,27 @@ +import { Typography } from '@mui/material' +import { formatVisualAmount } from '@/utils/formatters' +import { type TwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import { DataRow } from '@/components/common/Table/DataRow' +import { Box } from '@mui/system' + +export const PartSellAmount = ({ + order, + addonText = '', +}: { + order: Pick<TwapOrder, 'partSellAmount' | 'sellToken'> + addonText?: string +}) => { + const { partSellAmount, sellToken } = order + return ( + <DataRow title="Sell amount" key="sell_amount_part"> + <Box> + <Typography component="span" fontWeight="bold"> + {formatVisualAmount(partSellAmount, sellToken.decimals)} {sellToken.symbol} + </Typography> + <Typography component="span" color="var(--color-primary-light)"> + {` ${addonText}`} + </Typography> + </Box> + </DataRow> + ) +} diff --git a/src/features/swap/components/SwapOrder/twap.stories.tsx b/src/features/swap/components/SwapOrder/twap.stories.tsx index ae11b8883b..efb8f53e35 100644 --- a/src/features/swap/components/SwapOrder/twap.stories.tsx +++ b/src/features/swap/components/SwapOrder/twap.stories.tsx @@ -27,11 +27,11 @@ const FullfilledTwapOrder = twapOrderBuilder() 'https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0xbe72E441BF55620febc26715db68d3494213D8Cb.png', }, }) - .with({ numberOfParts: 2 }) + .with({ numberOfParts: '2' }) .with({ partSellAmount: '5000000000000000' }) .with({ minPartLimit: '1694293464162241304' }) - .with({ timeBetweenParts: '1800' }) + .with({ timeBetweenParts: 1800 }) .with({ fullAppData: appDataBuilder('twap').build(), }) diff --git a/src/features/swap/components/SwapOrderConfirmationView/index.stories.tsx b/src/features/swap/components/SwapOrderConfirmationView/index.stories.tsx index 71f258d021..a154b0d645 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/index.stories.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/index.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react' -import SwapOrderConfirmationView from './index' +import CowOrderConfirmationView from './index' import { Paper } from '@mui/material' import type { OrderStatuses } from '@safe-global/safe-gateway-typescript-sdk' import { orderTokenBuilder, swapOrderConfirmationViewBuilder } from '@/features/swap/helpers/swapOrderBuilder' @@ -15,7 +15,7 @@ const Order = swapOrderConfirmationViewBuilder() .with({ status: 'open' as OrderStatuses }) const meta = { - component: SwapOrderConfirmationView, + component: CowOrderConfirmationView, decorators: [ (Story) => { @@ -29,7 +29,7 @@ const meta = { }, ], tags: ['autodocs'], -} satisfies Meta<typeof SwapOrderConfirmationView> +} satisfies Meta<typeof CowOrderConfirmationView> export default meta type Story = StoryObj<typeof meta> diff --git a/src/features/swap/components/SwapOrderConfirmationView/index.tsx b/src/features/swap/components/SwapOrderConfirmationView/index.tsx index 828790eeac..029bec33f3 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/index.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/index.tsx @@ -1,5 +1,5 @@ import OrderId from '@/features/swap/components/OrderId' -import { formatDateTime, formatTimeInWords } from '@/utils/date' +import { formatDateTime, formatTimeInWords, getPeriod } from '@/utils/date' import { Fragment, type ReactElement } from 'react' import { DataRow } from '@/components/common/Table/DataRow' import { DataTable } from '@/components/common/Table/DataTable' @@ -8,20 +8,27 @@ import { Alert, Typography } from '@mui/material' import { formatAmount } from '@/utils/formatNumber' import { formatVisualAmount } from '@/utils/formatters' import { getLimitPrice, getOrderClass, getSlippageInPercent } from '@/features/swap/helpers/utils' -import type { CowSwapConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' +import type { OrderConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' +import { StartTimeValue } from '@safe-global/safe-gateway-typescript-sdk' +import { ConfirmationViewTypes } from '@safe-global/safe-gateway-typescript-sdk' import SwapTokens from '@/features/swap/components/SwapTokens' import AlertIcon from '@/public/images/common/alert.svg' import EthHashInfo from '@/components/common/EthHashInfo' import css from './styles.module.css' import NamedAddress from '@/components/common/NamedAddressInfo' +import { PartDuration } from '@/features/swap/components/SwapOrder/rows/PartDuration' +import { PartSellAmount } from '@/features/swap/components/SwapOrder/rows/PartSellAmount' +import { PartBuyAmount } from '@/features/swap/components/SwapOrder/rows/PartBuyAmount' type SwapOrderProps = { - order: CowSwapConfirmationView + order: OrderConfirmationView settlementContract: string } export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrderProps): ReactElement => { - const { uid, owner, kind, validUntil, sellToken, buyToken, sellAmount, buyAmount, explorerUrl, receiver } = order + const { owner, kind, validUntil, sellToken, buyToken, sellAmount, buyAmount, explorerUrl, receiver } = order + + const isTwapOrder = order.type === ConfirmationViewTypes.COW_SWAP_TWAP_ORDER const limitPrice = getLimitPrice(order) const orderClass = getOrderClass(order) @@ -36,7 +43,7 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd <DataTable header="Order details" rows={[ - <div key="amount"> + <div key="amount" className={css.amount}> <SwapTokens first={{ value: formatVisualAmount(sellAmount, sellToken.decimals), @@ -78,9 +85,13 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd ) : ( <Fragment key="none" /> ), - <DataRow key="Order ID" title="Order ID"> - <OrderId orderId={uid} href={explorerUrl} /> - </DataRow>, + !isTwapOrder ? ( + <DataRow key="Order ID" title="Order ID"> + <OrderId orderId={order.uid} href={explorerUrl} /> + </DataRow> + ) : ( + <></> + ), <DataRow key="Interact with" title="Interact with"> <NamedAddress address={settlementContract} onlyName hasExplorer shortAddress={false} avatarSize={24} /> </DataRow>, @@ -105,6 +116,31 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd ), ]} /> + + {isTwapOrder && ( + <div className={css.partsBlock}> + <DataTable + rows={[ + <Typography key="title" variant="body1" className={css.partsBlockTitle}> + <strong> + Order will be split in{' '} + <span className={css.numberOfPartsLabel}>{order.numberOfParts} equal parts</span> + </strong> + </Typography>, + <PartSellAmount order={order} addonText="per part" key="sell_part" />, + <PartBuyAmount order={order} addonText="per part" key="buy_part" />, + <DataRow title="Start time" key="Start time"> + {order.startTime.startType === StartTimeValue.AT_MINING_TIME && 'Now'} + {order.startTime.startType === StartTimeValue.AT_EPOCH && `At block number: ${order.startTime.epoch}`} + </DataRow>, + <PartDuration order={order} key="part_duration" />, + <DataRow title="Total duration" key="total_duration"> + {getPeriod(+order.timeBetweenParts * +order.numberOfParts)} + </DataRow>, + ]} + /> + </div> + )} </div> ) } diff --git a/src/features/swap/components/SwapOrderConfirmationView/styles.module.css b/src/features/swap/components/SwapOrderConfirmationView/styles.module.css index 3cffed151f..fdabf93ff5 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/styles.module.css +++ b/src/features/swap/components/SwapOrderConfirmationView/styles.module.css @@ -1,3 +1,20 @@ -.tableWrapper > div > * { - margin: 6px 0; +.amount { + margin-bottom: var(--space-1); +} +.partsBlock { + border: 1px solid var(--color-border-light); + border-radius: 4px; + padding: calc(var(--space-1) - 6px) var(--space-2); + margin-top: var(--space-1); +} + +.partsBlockTitle { + padding: var(--space-1) 0; +} + +.numberOfPartsLabel { + display: inline-block; + border-radius: 4px; + padding: 2px 8px; + background-color: var(--color-border-background); } diff --git a/src/features/swap/constants.ts b/src/features/swap/constants.ts new file mode 100644 index 0000000000..f1ecfba7c1 --- /dev/null +++ b/src/features/swap/constants.ts @@ -0,0 +1,4 @@ +export const SWAP_TITLE = 'Safe Swap' +export const SWAP_ORDER_TITLE = 'Swap order' +export const LIMIT_ORDER_TITLE = 'Limit order' +export const TWAP_ORDER_TITLE = 'TWAP order' diff --git a/src/features/swap/helpers/swapOrderBuilder.ts b/src/features/swap/helpers/swapOrderBuilder.ts index d144ae33eb..cf5b9d156b 100644 --- a/src/features/swap/helpers/swapOrderBuilder.ts +++ b/src/features/swap/helpers/swapOrderBuilder.ts @@ -1,12 +1,13 @@ import { Builder, type IBuilder } from '@/tests/Builder' import { faker } from '@faker-js/faker' import type { - CowSwapConfirmationView, OrderToken, SwapOrder, TransactionInfoType, TwapOrder, + SwapOrderConfirmationView, } from '@safe-global/safe-gateway-typescript-sdk' +import { ConfirmationViewTypes } from '@safe-global/safe-gateway-typescript-sdk' import { DurationType, StartTimeValue } from '@safe-global/safe-gateway-typescript-sdk' export function appDataBuilder( @@ -82,29 +83,29 @@ export function twapOrderBuilder(): IBuilder<TwapOrder> { buyToken: orderTokenBuilder().build(), executedSurplusFee: faker.string.numeric(), fullAppData: appDataBuilder().build(), - numberOfParts: faker.number.int({ min: 1, max: 10 }), + numberOfParts: faker.number.int({ min: 1, max: 10 }).toString(), /** @description The amount of sellToken to sell in each part */ partSellAmount: faker.string.numeric(), /** @description The amount of buyToken that must be bought in each part */ minPartLimit: faker.string.numeric(), /** @description The duration of the TWAP interval */ - timeBetweenParts: faker.string.numeric(), + timeBetweenParts: faker.number.int({ min: 1, max: 10000000 }), /** @description Whether the TWAP is valid for the entire interval or not */ durationOfPart: { - durationType: DurationType.Auto, + durationType: DurationType.AUTO, }, /** @description The start time of the TWAP */ startTime: { - startType: StartTimeValue.AtMiningTime, + startType: StartTimeValue.AT_MINING_TIME, }, }) } // create a builder for SwapOrderConfirmationView -export function swapOrderConfirmationViewBuilder(): IBuilder<CowSwapConfirmationView> { +export function swapOrderConfirmationViewBuilder(): IBuilder<SwapOrderConfirmationView> { const ownerAndReceiver = faker.finance.ethereumAddress() - return Builder.new<CowSwapConfirmationView>().with({ - type: 'COW_SWAP_ORDER', + return Builder.new<SwapOrderConfirmationView>().with({ + type: ConfirmationViewTypes.COW_SWAP_ORDER, uid: faker.string.uuid(), kind: faker.helpers.arrayElement(['buy', 'sell']), orderClass: faker.helpers.arrayElement(['limit', 'market', 'liquidity']), diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 8781f7875c..6b59e418a6 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -29,12 +29,14 @@ import useChainId from '@/hooks/useChainId' import { type BaseTransaction } from '@safe-global/safe-apps-sdk' import { APPROVAL_SIGNATURE_HASH } from '@/components/tx/ApprovalEditor/utils/approvals' import { id } from 'ethers' +import { LIMIT_ORDER_TITLE, SWAP_TITLE, SWAP_ORDER_TITLE, TWAP_ORDER_TITLE } from '@/features/swap/constants' const BASE_URL = typeof window !== 'undefined' && window.location.origin ? window.location.origin : '' const PRE_SIGN_SIGHASH = id('setPreSignature(bytes,bool)').slice(0, 10) const WRAP_SIGHASH = id('deposit()').slice(0, 10) const UNWRAP_SIGHASH = id('withdraw(uint256)').slice(0, 10) +const CREATE_WITH_CONTEXT = id('createWithContext((address,bytes32,bytes),address,bytes,bool)').slice(0, 10) type Params = { sell?: { @@ -43,14 +45,13 @@ type Params = { } } -export const SWAP_TITLE = 'Safe Swap' - export const getSwapTitle = (tradeType: SwapState['tradeType'], txs: BaseTransaction[] | undefined) => { const hashToLabel = { - [PRE_SIGN_SIGHASH]: tradeType === 'limit' ? 'Limit order' : 'Swap order', + [PRE_SIGN_SIGHASH]: tradeType === 'limit' ? LIMIT_ORDER_TITLE : SWAP_ORDER_TITLE, [APPROVAL_SIGNATURE_HASH]: 'Approve', [WRAP_SIGHASH]: 'Wrap', [UNWRAP_SIGHASH]: 'Unwrap', + [CREATE_WITH_CONTEXT]: TWAP_ORDER_TITLE, } const swapTitle = txs diff --git a/src/hooks/useDecodeTx.ts b/src/hooks/useDecodeTx.ts index d59afb8131..f4fbfd0127 100644 --- a/src/hooks/useDecodeTx.ts +++ b/src/hooks/useDecodeTx.ts @@ -2,7 +2,7 @@ import { type SafeTransaction } from '@safe-global/safe-core-sdk-types' import { getConfirmationView, type BaselineConfirmationView, - type CowSwapConfirmationView, + type OrderConfirmationView, type DecodedDataResponse, } from '@safe-global/safe-gateway-typescript-sdk' import { getNativeTransferData } from '@/services/tx/tokenTransferParams' @@ -14,7 +14,7 @@ import useSafeAddress from '@/hooks/useSafeAddress' const useDecodeTx = ( tx?: SafeTransaction, -): AsyncResult<DecodedDataResponse | BaselineConfirmationView | CowSwapConfirmationView> => { +): AsyncResult<DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView> => { const chainId = useChainId() const safeAddress = useSafeAddress() const encodedData = tx?.data.data @@ -22,18 +22,14 @@ const useDecodeTx = ( const isRejection = isEmptyData && tx?.data.value === '0' const [data, error, loading] = useAsync< - DecodedDataResponse | BaselineConfirmationView | CowSwapConfirmationView | undefined - >( - () => { - if (!encodedData || isEmptyData) { - const nativeTransfer = isEmptyData && !isRejection ? getNativeTransferData(tx?.data) : undefined - return Promise.resolve(nativeTransfer) - } - return getConfirmationView(chainId, safeAddress, encodedData, tx.data.to) - }, - [chainId, encodedData, isEmptyData, tx?.data, isRejection, safeAddress], - false, - ) + DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined + >(() => { + if (!encodedData || isEmptyData) { + const nativeTransfer = isEmptyData && !isRejection ? getNativeTransferData(tx?.data) : undefined + return Promise.resolve(nativeTransfer) + } + return getConfirmationView(chainId, safeAddress, encodedData, tx.data.to) + }, [chainId, encodedData, isEmptyData, tx?.data, isRejection, safeAddress]) return [data, error, loading] } diff --git a/src/hooks/useTransactionType.ts b/src/hooks/useTransactionType.ts index da9e9fef86..d625f0fc33 100644 --- a/src/hooks/useTransactionType.ts +++ b/src/hooks/useTransactionType.ts @@ -10,6 +10,7 @@ import { import { isCancellationTxInfo, isModuleExecutionInfo, isOutgoingTransfer, isTxQueued } from '@/utils/transaction-guards' import useAddressBook from './useAddressBook' import type { AddressBook } from '@/store/addressBookSlice' +import { TWAP_ORDER_TITLE } from '@/features/swap/constants' const getTxTo = ({ txInfo }: Pick<TransactionSummary, 'txInfo'>): AddressEx | undefined => { switch (txInfo.type) { @@ -70,10 +71,10 @@ export const getTransactionType = (tx: TransactionSummary, addressBook: AddressB text: orderClass === 'limit' ? 'Limit order' : 'Swap order', } } - case 'TwapOrder': { + case TransactionInfoType.TWAP_ORDER: { return { icon: '/images/common/swap.svg', - text: 'TWAP order', + text: TWAP_ORDER_TITLE, } } case TransactionInfoType.CUSTOM: { diff --git a/src/services/tx/extractTxInfo.ts b/src/services/tx/extractTxInfo.ts index fc8da6248d..6fc690beb4 100644 --- a/src/services/tx/extractTxInfo.ts +++ b/src/services/tx/extractTxInfo.ts @@ -57,6 +57,8 @@ const extractTxInfo = ( } else { return txDetails.txData?.value ?? '0' } + case 'TwapOrder': + return txDetails.txData?.value ?? '0' case 'SwapOrder': return txDetails.txData?.value ?? '0' case 'Custom': @@ -79,12 +81,12 @@ const extractTxInfo = ( return txDetails.txInfo.transferInfo.tokenAddress } case 'SwapOrder': - const swapOrderTo = txDetails.txData?.to.value - // TODO: Remove assertion after type is corrected - if (!swapOrderTo) { - throw new Error('SwapOrder tx data does not have a `to` field') + case 'TwapOrder': + const orderTo = txDetails.txData?.to.value + if (!orderTo) { + throw new Error('Order tx data does not have a `to` field') } - return swapOrderTo + return orderTo case 'Custom': return txDetails.txInfo.to.value case 'Creation': diff --git a/src/utils/transaction-guards.ts b/src/utils/transaction-guards.ts index cdd44159c2..4656718cb0 100644 --- a/src/utils/transaction-guards.ts +++ b/src/utils/transaction-guards.ts @@ -27,10 +27,13 @@ import type { SwapOrder, DecodedDataResponse, BaselineConfirmationView, - CowSwapConfirmationView, + OrderConfirmationView, TwapOrder, Order, + SwapOrderConfirmationView, + TwapOrderConfirmationView, } from '@safe-global/safe-gateway-typescript-sdk' +import { ConfirmationViewTypes } from '@safe-global/safe-gateway-typescript-sdk' import { TransferDirection } from '@safe-global/safe-gateway-typescript-sdk' import { ConflictType, @@ -106,11 +109,27 @@ export const isTwapOrderTxInfo = (value: TransactionInfo): value is TwapOrder => return value.type === TransactionInfoType.TWAP_ORDER } +export const isConfirmationViewOrder = ( + decodedData: DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined, +): decodedData is OrderConfirmationView => { + return isSwapConfirmationViewOrder(decodedData) || isTwapConfirmationViewOrder(decodedData) +} + +export const isTwapConfirmationViewOrder = ( + decodedData: DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined, +): decodedData is TwapOrderConfirmationView => { + if (decodedData && 'type' in decodedData) { + return decodedData.type === ConfirmationViewTypes.COW_SWAP_TWAP_ORDER + } + + return false +} + export const isSwapConfirmationViewOrder = ( - decodedData: DecodedDataResponse | BaselineConfirmationView | CowSwapConfirmationView | undefined, -): decodedData is CowSwapConfirmationView => { + decodedData: DecodedDataResponse | BaselineConfirmationView | OrderConfirmationView | undefined, +): decodedData is SwapOrderConfirmationView => { if (decodedData && 'type' in decodedData) { - return decodedData.type === 'COW_SWAP_ORDER' + return decodedData.type === ConfirmationViewTypes.COW_SWAP_ORDER } return false diff --git a/yarn.lock b/yarn.lock index d99578f244..03400c1f5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4156,10 +4156,10 @@ dependencies: semver "^7.6.0" -"@safe-global/safe-gateway-typescript-sdk@^3.21.5": - version "3.21.5" - resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.5.tgz#8fc96719a9ac81d1070ad61987c7b49c5324a83e" - integrity sha512-KZOAfHDzXCmxVB7SpG6OnRUZontIatwe312NrG7XekmdldCxU78HHecb/tflRZTwJUZhD/USGGZezE7pqjESqQ== +"@safe-global/safe-gateway-typescript-sdk@3.21.7": + version "3.21.7" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.7.tgz#4592677dee4f9e3c86befce659b361f37133578a" + integrity sha512-V9vOqQjb/O0Ylt5sKUtVl6f7fKDpH7HUQUCEON42BXk4PUpcKWdmziQjmf3/PR3OnkahcmXb7ULNwUi+04HmCw== "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.21.2" From 61047eb218970258b2ca2bf0e7a974fd92ad4e5e Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Fri, 28 Jun 2024 17:05:10 +0200 Subject: [PATCH 112/154] feat: Add method to Sign in with Ethereum [SW-28] (#3853) * feat: Add siwe * fix: Extract functions into gateway-sdk --- src/services/siwe/index.ts | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/services/siwe/index.ts diff --git a/src/services/siwe/index.ts b/src/services/siwe/index.ts new file mode 100644 index 0000000000..682e79c1bc --- /dev/null +++ b/src/services/siwe/index.ts @@ -0,0 +1,41 @@ +import { getAuthNonce, verifyAuth } from '@safe-global/safe-gateway-typescript-sdk' +import type { BrowserProvider } from 'ethers' + +/** + * Prompt the user to sign in with their wallet and set an access_token cookie + * @param provider + */ +async function signInWithEthereum(provider: BrowserProvider) { + const { nonce } = await getAuthNonce() + + const [network, signer] = await Promise.all([provider.getNetwork(), provider.getSigner()]) + + const message = { + domain: window.location.host, + address: signer.address as `0x${string}`, + // Results in special signing window in MetaMask + statement: 'Sign in with Ethereum to the app.', + uri: window.location.origin, + version: '1', + chainId: Number(network.chainId), + nonce, + issuedAt: new Date(), + } + + const signableMessage = `${message.domain} wants you to sign in with your Ethereum account: +${message.address} + +${message.statement} + +URI: ${message.uri} +Version: ${message.version} +Chain ID: ${message.chainId} +Nonce: ${message.nonce} +Issued At: ${message.issuedAt.toISOString()}` + + const signature = await signer.signMessage(signableMessage) + + return verifyAuth({ message: signableMessage, signature }) +} + +export default signInWithEthereum From c5e04e260fa1126bda5b706c802506ba8ef12b89 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov <daniel.d@safe.global> Date: Mon, 1 Jul 2024 12:05:43 +0200 Subject: [PATCH 113/154] Display swap info on sent/receive events [SW-3] (#3877) * feat: display swap info on sent/receive events * chore: update safe-gateway-typescript-sdk * feat: improve the title for order settlements * Update src/features/swap/components/SwapOrder/index.tsx Co-authored-by: Usame Algan <5880855+usame-algan@users.noreply.github.com> --------- Co-authored-by: Usame Algan <5880855+usame-algan@users.noreply.github.com> --- package.json | 2 +- .../transactions/BulkTxListGroup/index.tsx | 25 ++++++++++++++--- .../transactions/TxDetails/index.tsx | 8 ++++++ .../transactions/TxDetails/styles.module.css | 8 ++++++ .../swap/components/SwapOrder/index.tsx | 12 ++++---- src/hooks/useTransactionType.ts | 1 + src/utils/transaction-guards.ts | 28 +++++++++++++------ yarn.lock | 8 +++--- 8 files changed, 67 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 9ef19d38ca..8bd9a38026 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@safe-global/protocol-kit": "^3.1.1", "@safe-global/safe-apps-sdk": "^9.1.0", "@safe-global/safe-deployments": "^1.36.0", - "@safe-global/safe-gateway-typescript-sdk": "3.21.7", + "@safe-global/safe-gateway-typescript-sdk": "3.21.8", "@safe-global/safe-modules-deployments": "^1.2.0", "@sentry/react": "^7.91.0", "@spindl-xyz/attribution-lite": "^1.4.0", diff --git a/src/components/transactions/BulkTxListGroup/index.tsx b/src/components/transactions/BulkTxListGroup/index.tsx index 32c04fc70a..b793fef6cc 100644 --- a/src/components/transactions/BulkTxListGroup/index.tsx +++ b/src/components/transactions/BulkTxListGroup/index.tsx @@ -1,13 +1,26 @@ import type { ReactElement } from 'react' import { Box, Paper, SvgIcon, Typography } from '@mui/material' -import type { Transaction } from '@safe-global/safe-gateway-typescript-sdk' -import { isMultisigExecutionInfo } from '@/utils/transaction-guards' +import type { Order, Transaction } from '@safe-global/safe-gateway-typescript-sdk' +import { isMultisigExecutionInfo, isSwapTransferOrderTxInfo } from '@/utils/transaction-guards' import ExpandableTransactionItem from '@/components/transactions/TxListItem/ExpandableTransactionItem' import BatchIcon from '@/public/images/common/batch.svg' import css from './styles.module.css' import ExplorerButton from '@/components/common/ExplorerButton' import { getBlockExplorerLink } from '@/utils/chains' import { useCurrentChain } from '@/hooks/useChains' +import { getOrderClass } from '@/features/swap/helpers/utils' + +const orderClassTitles: Record<string, string> = { + limit: 'Limit order settlement', + twap: 'TWAP order settlement', + liquidity: 'Liquidity order settlement', + market: 'Swap order settlement', +} + +const getSettlementOrderTitle = (order: Order): string => { + const orderClass = getOrderClass(order) + return orderClassTitles[orderClass] || orderClassTitles['market'] +} const GroupedTxListItems = ({ groupedListItems, @@ -19,14 +32,18 @@ const GroupedTxListItems = ({ const chain = useCurrentChain() const explorerLink = chain && getBlockExplorerLink(chain, transactionHash)?.href if (groupedListItems.length === 0) return null - + let title = 'Bulk transactions' + const isSwapTransfer = isSwapTransferOrderTxInfo(groupedListItems[0].transaction.txInfo) + if (isSwapTransfer) { + title = getSettlementOrderTitle(groupedListItems[0].transaction.txInfo as Order) + } return ( <Paper className={css.container}> <Box gridArea="icon"> <SvgIcon className={css.icon} component={BatchIcon} inheritViewBox fontSize="medium" /> </Box> <Box gridArea="info"> - <Typography noWrap>Bulk transactions</Typography> + <Typography noWrap>{title}</Typography> </Box> <Box className={css.action}>{groupedListItems.length} transactions</Box> <Box className={css.hash}> diff --git a/src/components/transactions/TxDetails/index.tsx b/src/components/transactions/TxDetails/index.tsx index 65a1829b0b..c84423d4da 100644 --- a/src/components/transactions/TxDetails/index.tsx +++ b/src/components/transactions/TxDetails/index.tsx @@ -20,6 +20,7 @@ import { isMultisigExecutionInfo, isOpenSwapOrder, isTxQueued, + isSwapTransferOrderTxInfo, } from '@/utils/transaction-guards' import { InfoDetails } from '@/components/transactions/InfoDetails' import EthHashInfo from '@/components/common/EthHashInfo' @@ -89,6 +90,13 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement <div className={css.txData}> <ErrorBoundary fallback={<div>Error parsing data</div>}> <TxData txDetails={txDetails} trusted={isTrustedTransfer} imitation={isImitationTransaction} /> + {isSwapTransferOrderTxInfo(txDetails.txInfo) && ( + <div className={css.swapOrderTransfer}> + <ErrorBoundary fallback={<div>Error parsing data</div>}> + <SwapOrder txData={txDetails.txData} txInfo={txDetails.txInfo} /> + </ErrorBoundary> + </div> + )} </ErrorBoundary> </div> diff --git a/src/components/transactions/TxDetails/styles.module.css b/src/components/transactions/TxDetails/styles.module.css index 7be58a00b7..21d7b6ae27 100644 --- a/src/components/transactions/TxDetails/styles.module.css +++ b/src/components/transactions/TxDetails/styles.module.css @@ -27,6 +27,14 @@ padding: var(--space-2); } +.swapOrderTransfer { + border-top: 1px solid var(--color-border-light); + margin-top: var(--space-2); + margin-left: calc(var(--space-2) * -1); + margin-right: calc(var(--space-2) * -1); + padding: var(--space-2); + padding-top: var(--space-3) +} .txData, .swapOrder { border-bottom: 1px solid var(--color-border-light); diff --git a/src/features/swap/components/SwapOrder/index.tsx b/src/features/swap/components/SwapOrder/index.tsx index 41351aa93a..a2fe88d3ca 100644 --- a/src/features/swap/components/SwapOrder/index.tsx +++ b/src/features/swap/components/SwapOrder/index.tsx @@ -11,7 +11,6 @@ import { type SwapOrder as SwapOrderType, type Order, type TransactionData, - TransactionInfoType, } from '@safe-global/safe-gateway-typescript-sdk' import { DataRow } from '@/components/common/Table/DataRow' import { DataTable } from '@/components/common/Table/DataTable' @@ -30,7 +29,7 @@ import { } from '@/features/swap/helpers/utils' import EthHashInfo from '@/components/common/EthHashInfo' import useSafeInfo from '@/hooks/useSafeInfo' -import { isSwapOrderTxInfo, isTwapOrderTxInfo } from '@/utils/transaction-guards' +import { isSwapOrderTxInfo, isSwapTransferOrderTxInfo, isTwapOrderTxInfo } from '@/utils/transaction-guards' import { EmptyRow } from '@/components/common/Table/EmptyRow' import { PartDuration } from '@/features/swap/components/SwapOrder/rows/PartDuration' import { PartSellAmount } from '@/features/swap/components/SwapOrder/rows/PartSellAmount' @@ -43,7 +42,6 @@ type SwapOrderProps = { const AmountRow = ({ order }: { order: Order }) => { const { sellToken, buyToken, sellAmount, buyAmount, kind } = order - const orderKindLabel = capitalize(kind) const isSellOrder = kind === 'sell' return ( <DataRow key="Amount" title="Amount"> @@ -150,7 +148,7 @@ const FilledRow = ({ order }: { order: Order }) => { } const OrderUidRow = ({ order }: { order: Order }) => { - if (order.type === TransactionInfoType.SWAP_ORDER) { + if (isSwapOrderTxInfo(order) || isSwapTransferOrderTxInfo(order)) { const { uid, explorerUrl } = order return ( <DataRow key="Order ID" title="Order ID"> @@ -255,14 +253,14 @@ export const TwapOrder = ({ order }: { order: SwapTwapOrder }) => { ) } -export const SwapOrder = ({ txData, txInfo }: SwapOrderProps): ReactElement | null => { - if (!txData || !txInfo) return null +export const SwapOrder = ({ txInfo }: SwapOrderProps): ReactElement | null => { + if (!txInfo) return null if (isTwapOrderTxInfo(txInfo)) { return <TwapOrder order={txInfo} /> } - if (isSwapOrderTxInfo(txInfo)) { + if (isSwapOrderTxInfo(txInfo) || isSwapTransferOrderTxInfo(txInfo)) { return <SellOrder order={txInfo} /> } return null diff --git a/src/hooks/useTransactionType.ts b/src/hooks/useTransactionType.ts index d625f0fc33..a688c1975b 100644 --- a/src/hooks/useTransactionType.ts +++ b/src/hooks/useTransactionType.ts @@ -45,6 +45,7 @@ export const getTransactionType = (tx: TransactionSummary, addressBook: AddressB text: 'Safe Account created', } } + case TransactionInfoType.SWAP_TRANSFER: case TransactionInfoType.TRANSFER: { const isSendTx = isOutgoingTransfer(tx.txInfo) diff --git a/src/utils/transaction-guards.ts b/src/utils/transaction-guards.ts index 4656718cb0..a92fe19a12 100644 --- a/src/utils/transaction-guards.ts +++ b/src/utils/transaction-guards.ts @@ -1,10 +1,12 @@ import type { AddressEx, + BaselineConfirmationView, Cancellation, ConflictHeader, Creation, Custom, DateLabel, + DecodedDataResponse, DetailedExecutionInfo, Erc20Transfer, Erc721Transfer, @@ -16,32 +18,30 @@ import type { MultisigExecutionDetails, MultisigExecutionInfo, NativeCoinTransfer, + Order, + OrderConfirmationView, SafeInfo, SettingsChange, + SwapOrder, + SwapOrderConfirmationView, Transaction, TransactionInfo, TransactionListItem, TransactionSummary, Transfer, TransferInfo, - SwapOrder, - DecodedDataResponse, - BaselineConfirmationView, - OrderConfirmationView, TwapOrder, - Order, - SwapOrderConfirmationView, TwapOrderConfirmationView, } from '@safe-global/safe-gateway-typescript-sdk' -import { ConfirmationViewTypes } from '@safe-global/safe-gateway-typescript-sdk' -import { TransferDirection } from '@safe-global/safe-gateway-typescript-sdk' import { + ConfirmationViewTypes, ConflictType, DetailedExecutionInfoType, TransactionInfoType, TransactionListItemType, TransactionStatus, TransactionTokenType, + TransferDirection, } from '@safe-global/safe-gateway-typescript-sdk' import { getSpendingLimitModuleAddress } from '@/services/contracts/spendingLimitContracts' import { sameAddress } from '@/utils/addresses' @@ -78,7 +78,17 @@ export const isModuleDetailedExecutionInfo = (value?: DetailedExecutionInfo): va // TransactionInfo type guards export const isTransferTxInfo = (value: TransactionInfo): value is Transfer => { - return value.type === TransactionInfoType.TRANSFER + return value.type === TransactionInfoType.TRANSFER || isSwapTransferOrderTxInfo(value) +} + +/** + * A fulfillment transaction for swap, limit or twap order is always a SwapOrder + * It cannot be a TWAP order + * + * @param value + */ +export const isSwapTransferOrderTxInfo = (value: TransactionInfo): value is SwapOrder => { + return value.type === TransactionInfoType.SWAP_TRANSFER } export const isSettingsChangeTxInfo = (value: TransactionInfo): value is SettingsChange => { diff --git a/yarn.lock b/yarn.lock index 03400c1f5b..77bd445e20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4156,10 +4156,10 @@ dependencies: semver "^7.6.0" -"@safe-global/safe-gateway-typescript-sdk@3.21.7": - version "3.21.7" - resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.7.tgz#4592677dee4f9e3c86befce659b361f37133578a" - integrity sha512-V9vOqQjb/O0Ylt5sKUtVl6f7fKDpH7HUQUCEON42BXk4PUpcKWdmziQjmf3/PR3OnkahcmXb7ULNwUi+04HmCw== +"@safe-global/safe-gateway-typescript-sdk@3.21.8": + version "3.21.8" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.21.8.tgz#f90eb668dd620d0c5578f02f7040169610a0eda1" + integrity sha512-n/fYgiqbuzAQuK0bgny6GBYvb585ETxKURa5Kb9hBV3fa47SvJo/dpGq275fJUn0e3Hh1YqETiLGj4HVJjHiTA== "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.21.2" From c3190665ebac9e5091d575094dbcbc3d94d839e3 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:24:47 +0200 Subject: [PATCH 114/154] Tests: Add swap tests (#3884) * Add swap tests --- cypress/e2e/pages/swaps.pages.js | 71 ++++++++- cypress/e2e/regression/swaps.cy.js | 58 ++++++++ cypress/e2e/regression/swaps_history.cy.js | 17 +-- cypress/e2e/regression/swaps_history_2.cy.js | 149 +++++++++++++++++++ cypress/fixtures/swaps_data.json | 32 +++- 5 files changed, 307 insertions(+), 20 deletions(-) create mode 100644 cypress/e2e/regression/swaps_history_2.cy.js diff --git a/cypress/e2e/pages/swaps.pages.js b/cypress/e2e/pages/swaps.pages.js index a09b1e3d83..1567ac6f55 100644 --- a/cypress/e2e/pages/swaps.pages.js +++ b/cypress/e2e/pages/swaps.pages.js @@ -1,5 +1,6 @@ import * as constants from '../../support/constants.js' import * as main from '../pages/main.page.js' +import * as create_tx from '../pages/create_tx.pages.js' // Incoming from CowSwap export const inputCurrencyInput = '[id="input-currency-input"]' @@ -10,21 +11,53 @@ const exceedFeesChkbox = 'input[id="fees-exceed-checkbox"]' const settingsBtn = 'button[id="open-settings-dialog-button"]' 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 orderTypeMenuItem = 'div[class*="MenuItem"]' const confirmSwapStr = 'Confirm Swap' const swapBtnStr = /Confirm Swap|Swap|Confirm (Approve COW and Swap)|Confirm/ const orderSubmittedStr = 'Order Submitted' +export const blockedAddress = '0x8576acc5c05d6ce88f4e49bf65bdf0c62f91353c' +export const blockedAddressStr = 'Blocked address' +const swapStr = 'Swap' +const limitStr = 'Limit' + export const swapTokens = { cow: 'COW', dai: 'DAI', eth: 'ETH', } +export const orderTypes = { + swap: 'Swap', + limit: 'Limit', +} + const swapOrders = '**/api/v1/orders/*' const surplus = '**/users/*/total_surplus' const nativePrice = '**/native_price' const quote = '**/quote/*' +export const limitOrderSafe = 'sep:0x8f4A19C85b39032A37f7a6dCc65234f966F72551' + +export const swapTxs = { + sell1Action: + '&id=multisig_0x03042B890b99552b60A073F808100517fb148F60_0xd033466000a40227fba7a7deb1a668371c213fec90bac9f2583096be2e0fd959', + buy2actions: + '&id=multisig_0x03042B890b99552b60A073F808100517fb148F60_0x135ff0282653d4c2a62c76cd247764b1abd4c0daa9201a72964feac2acaa7b44', + sellCancelled: + '&id=multisig_0x2a73e61bd15b25B6958b4DA3bfc759ca4db249b9_0xbe159adaa7fb0f7e80ad4bab33a2bb341043818478c96916cfa3877303d22a3d', + sell3Actions: + '&id=multisig_0x140663Cb76e4c4e97621395fc118912fa674150B_0x9f3d2c9c9879fb7eee7005d57b2b5c9006d7c8b98241aa49a0b9e769411c58ef', + sellLimitOrder: + '&id=multisig_0x03042B890b99552b60A073F808100517fb148F60_0xf7093c3e87e3b703a0df4d9360cd38254ed69d0dc4f7ff5399a194bd92e9014c', + sellLimitOrderFilled: + '&id=multisig_0x8f4A19C85b39032A37f7a6dCc65234f966F72551_0xd3d13db9fc438d0674819f81be62fcd9c74a8ed7c101a8249b8895e55ee80d76', + safeAppSwapOrder: + '&id=multisig_0x03042B890b99552b60A073F808100517fb148F60_0x5f08e05edb210a8990791e9df2f287a5311a8137815ec85856a2477a36552f1e', +} + export function clickOnAssetSwapBtn(index) { cy.get(assetsSwapBtn).eq(index).as('btn') cy.get('@btn').click() @@ -42,6 +75,10 @@ export function setExpiry(value) { cy.get('div').contains('Swap deadline').parent().next().find('input').clear().type(value) } +export function enterRecipient(address) { + cy.get(customRecipient).find('input').clear().type(address) +} + export function setSlippage(value) { cy.contains('button', 'Auto').next('button').find('input').clear().type(value) } @@ -127,7 +164,7 @@ export function verifySelectedInputCurrancy(option) { } export function selectInputCurrency(option) { cy.get(inputCurrencyInput).within(() => { - cy.get('button').trigger('mouseover').trigger('click') + cy.get('button').eq(0).trigger('mouseover').trigger('click') }) cy.get(tokenList).find('span').contains(option).click() } @@ -151,6 +188,14 @@ export function setOutputValue(value) { }) } +export function enableCustomRecipient(option) { + if (!option) cy.get(recipientToggle).click() +} + +export function disableCustomRecipient(option) { + if (option) cy.get(recipientToggle).click() +} + export function isInputGreaterZero(inputSelector) { return cy .get(inputSelector) @@ -161,3 +206,27 @@ export function isInputGreaterZero(inputSelector) { return n > 0 }) } + +export function selectOrderType(type) { + cy.get('a').contains(swapStr).click() + cy.get(orderTypeMenuItem).contains(type).click() +} + +export function createRegex(pattern, placeholder) { + const pattern_ = pattern.replace(placeholder, `\\s*\\d*\\.?\\d*\\s*${placeholder}`) + return new RegExp(pattern_, 'i') +} + +export function checkTokenOrder(regexPattern, option) { + cy.get(create_tx.txRowTitle) + .filter(`:contains("${option}")`) + .parent('div') + .then(($div) => { + const text = $div.text() + const regex = new RegExp(regexPattern, 'i') + + cy.wrap($div).should(($div) => { + expect(text).to.match(regex) + }) + }) +} diff --git a/cypress/e2e/regression/swaps.cy.js b/cypress/e2e/regression/swaps.cy.js index 9d99618d7a..216f736ef4 100644 --- a/cypress/e2e/regression/swaps.cy.js +++ b/cypress/e2e/regression/swaps.cy.js @@ -5,6 +5,7 @@ import * as tx from '../pages/transactions.page.js' import * as create_tx from '../pages/create_tx.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as owner from '../pages/owners.pages' +import * as wallet from '../../support/utils/wallet.js' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY @@ -20,6 +21,8 @@ describe('Swaps tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.swapUrl + staticSafes.SEP_STATIC_SAFE_1) + main.waitForHistoryCallToComplete() + wallet.connectSigner(signer) main.acceptCookies() iframeSelector = `iframe[src*="${constants.swapWidget}"]` }) @@ -65,4 +68,59 @@ describe('Swaps tests', () => { create_tx.deleteTx() main.verifyElementsCount(create_tx.transactionItem, 0) }) + + it( + 'Verify entering a blocked address in the custom recipient input blocks the form', + { defaultCommandTimeout: 30000 }, + () => { + let isCustomRecipientFound + swaps.acceptLegalDisclaimer() + swaps.waitForOrdersCallToComplete() + cy.wait(2000) + main + .getIframeBody(iframeSelector) + .then(($frame) => { + isCustomRecipientFound = (customRecipient) => { + const element = $frame.find(customRecipient) + return element.length > 0 + } + }) + .within(() => { + swaps.clickOnSettingsBtn() + swaps.enableCustomRecipient(isCustomRecipientFound(swaps.customRecipient)) + swaps.clickOnSettingsBtn() + swaps.enterRecipient(swaps.blockedAddress) + }) + cy.contains(swaps.blockedAddressStr) + }, + ) + + it('Verify enabling custom recipient adds that field to the form', { defaultCommandTimeout: 30000 }, () => { + swaps.acceptLegalDisclaimer() + swaps.waitForOrdersCallToComplete() + cy.wait(2000) + + const isCustomRecipientFound = ($frame, customRecipient) => { + const element = $frame.find(customRecipient) + return element.length > 0 + } + + main.getIframeBody(iframeSelector).then(($frame) => { + cy.wrap($frame).within(() => { + swaps.clickOnSettingsBtn() + + if (isCustomRecipientFound($frame, swaps.customRecipient)) { + swaps.disableCustomRecipient(true) + cy.wait(1000) + swaps.enableCustomRecipient(!isCustomRecipientFound($frame, swaps.customRecipient)) + } else { + swaps.enableCustomRecipient(isCustomRecipientFound($frame, swaps.customRecipient)) + cy.wait(1000) + } + + swaps.clickOnSettingsBtn() + swaps.enterRecipient('1') + }) + }) + }) }) diff --git a/cypress/e2e/regression/swaps_history.cy.js b/cypress/e2e/regression/swaps_history.cy.js index 46da373ee2..95d4ffb527 100644 --- a/cypress/e2e/regression/swaps_history.cy.js +++ b/cypress/e2e/regression/swaps_history.cy.js @@ -11,7 +11,7 @@ const limitOrder = '&id=multisig_0x8f4A19C85b39032A37f7a6dCc65234f966F72551_0x3faf510142c9ade7ac2a701fb697b95f321fd51f5eb9b17e7e534a8abe472b07' const limitOrderSafe = 'sep:0x8f4A19C85b39032A37f7a6dCc65234f966F72551' -describe('[SMOKE] Swaps history tests', () => { +describe('Swaps history tests', () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) }) @@ -22,21 +22,6 @@ describe('[SMOKE] Swaps history tests', () => { main.acceptCookies() }) - it('Verify sawp buying operation with approve and swap', { defaultCommandTimeout: 30000 }, () => { - // approve, preSignature actions - create_tx.clickOnTransactionItemByName('8:05 AM') - create_tx.verifyExpandedDetails([ - swapsHistory.buyOrder, - swapsHistory.buy, - swapsHistory.oneGNO, - swapsHistory.forAtMost, - swapsHistory.cow, - swapsHistory.expired, - swapsHistory.actionApprove, - swapsHistory.actionPreSignature, - ]) - }) - it('Verify swap selling operation with one action', { defaultCommandTimeout: 30000 }, () => { create_tx.clickOnTransactionItemByName('14') create_tx.verifyExpandedDetails([ diff --git a/cypress/e2e/regression/swaps_history_2.cy.js b/cypress/e2e/regression/swaps_history_2.cy.js new file mode 100644 index 0000000000..452c94a57d --- /dev/null +++ b/cypress/e2e/regression/swaps_history_2.cy.js @@ -0,0 +1,149 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as create_tx from '../pages/create_tx.pages.js' +import * as swaps_data from '../../fixtures/swaps_data.json' +import * as swaps from '../pages/swaps.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let staticSafes = [] + +const swapsHistory = swaps_data.type.history + +describe('Swaps history tests 2', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + }) + + it('Verify swap sell order with one action', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.sell1Action) + main.acceptCookies() + + const dai = swaps.createRegex(swapsHistory.forAtLeastFullDai, 'DAI') + const eq = swaps.createRegex(swapsHistory.DAIeqCOW, 'COW') + + create_tx.verifyExpandedDetails([ + swapsHistory.sellFull, + dai, + eq, + swapsHistory.dai, + swapsHistory.filled, + swapsHistory.gGpV2, + ]) + }) + + it('Verify swap buy operation with 2 actions: approve & swap', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.buy2actions) + main.acceptCookies() + + const eq = swaps.createRegex(swapsHistory.oneGNOFull, 'COW') + const atMost = swaps.createRegex(swapsHistory.forAtMostCow, 'COW') + + create_tx.verifyExpandedDetails([ + swapsHistory.buyOrder, + swapsHistory.buy, + eq, + atMost, + swapsHistory.cow, + swapsHistory.expired, + swapsHistory.actionApprove, + swapsHistory.actionPreSignature, + ]) + }) + + it('Verify "Cancelled" status for manually cancelled limit orders', { defaultCommandTimeout: 30000 }, () => { + const safe = '0x2a73e61bd15b25B6958b4DA3bfc759ca4db249b9' + cy.visit(constants.transactionUrl + safe + swaps.swapTxs.sellCancelled) + main.acceptCookies() + + const uni = swaps.createRegex(swapsHistory.forAtLeastFullUni, 'UNI') + const eq = swaps.createRegex(swapsHistory.UNIeqCOW, 'K COW') + + create_tx.verifyExpandedDetails([ + swapsHistory.sellOrder, + swapsHistory.sell, + uni, + eq, + swapsHistory.cow, + swapsHistory.cancelled, + swapsHistory.gGpV2, + ]) + }) + + it('Verify swap operation with 3 actions: wrap & approve & swap', { defaultCommandTimeout: 30000 }, () => { + const safe = '0x140663Cb76e4c4e97621395fc118912fa674150B' + cy.visit(constants.transactionUrl + safe + swaps.swapTxs.sell3Actions) + main.acceptCookies() + + const dai = swaps.createRegex(swapsHistory.forAtLeastFullDai, 'DAI') + const eq = swaps.createRegex(swapsHistory.DAIeqWETH, 'WETH') + + create_tx.verifyExpandedDetails([ + swapsHistory.sellOrder, + swapsHistory.sell, + dai, + eq, + swapsHistory.actionApproveEth, + swapsHistory.actionPreSignature, + swapsHistory.actionDepositEth, + ]) + }) + + it('Verify "Expired" field in the tx details for limit orders', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.sellLimitOrder) + main.acceptCookies() + + const dai = swaps.createRegex(swapsHistory.forAtLeastFullDai, 'DAI') + const eq = swaps.createRegex(swapsHistory.DAIeqCOW, 'COW') + + create_tx.verifyExpandedDetails([swapsHistory.sellOrder, swapsHistory.sell, dai, eq, swapsHistory.expired]) + }) + + it('Verify "Filled" field in the tx details for limit orders', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + swaps.limitOrderSafe + swaps.swapTxs.sellLimitOrderFilled) + main.acceptCookies() + + const usdc = swaps.createRegex(swapsHistory.forAtLeastFullUSDT, 'USDT') + const eq = swaps.createRegex(swapsHistory.USDTeqUSDC, 'USDC') + + create_tx.verifyExpandedDetails([swapsHistory.sellOrder, swapsHistory.sell, usdc, eq, swapsHistory.filled]) + }) + + it( + 'Verify no decoding if tx was created using CowSwap safe-app in the history', + { defaultCommandTimeout: 30000 }, + () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.safeAppSwapOrder) + main.acceptCookies() + main.verifyValuesDoNotExist('div', [ + swapsHistory.actionApproveG, + swapsHistory.actionDepositG, + swapsHistory.amount, + swapsHistory.executionPrice, + swapsHistory.surplus, + swapsHistory.expiry, + swapsHistory.oderId, + swapsHistory.status, + swapsHistory.forAtLeast, + swapsHistory.forAtMost, + ]) + main.verifyValuesDoNotExist(create_tx.transactionItem, [swapsHistory.title, swapsHistory.cow, swapsHistory.dai]) + main.verifyValuesExist(create_tx.transactionItem, [swapsHistory.actionPreSignatureG, swapsHistory.safeAppTitile]) + }, + ) + + it('Verify token order in sell and buy operations', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.sell1Action) + main.acceptCookies() + const eq = swaps.createRegex(swapsHistory.DAIeqCOW, 'COW') + swaps.checkTokenOrder(eq, swapsHistory.executionPrice) + + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.buy2actions) + main.acceptCookies() + const eq2 = swaps.createRegex(swapsHistory.oneGNOFull, 'COW') + swaps.checkTokenOrder(eq2, swapsHistory.limitPrice) + }) +}) diff --git a/cypress/fixtures/swaps_data.json b/cypress/fixtures/swaps_data.json index 7ca9a17d54..5ca3844ecb 100644 --- a/cypress/fixtures/swaps_data.json +++ b/cypress/fixtures/swaps_data.json @@ -3,25 +3,51 @@ "queue": { "contractName": "GPv2Settlement", "action": "setPreSignature", - "oneOfOne": "1 out of 1" + "oneOfOne": "1 out of 1", + "title": "Swap order" }, "history": { "buyOrder": "Buy order", "buy": "Buy", "oneGNO": "1 GNO", + "oneGNOFull": "1 GNO = 8.4747 COW", "forAtMost": "For at most", + "forAtMostCow": "For at most COW", "cow": "COW", "expired": "Expired", + "cancelled": "Cancelled", "actionApprove": "CoW Protocol Token: approve", "actionPreSignature": "GPv2Settlement: setPreSignature", + "actionApproveEth": "Wrapped Ether: approve", + "actionDepositEth": "Wrapped Ether: deposit", "sellOrder": "Sell order", - + "actionApproveG": "approve", + "actionPreSignatureG": "setPreSignature", + "actionDepositG": "deposit", + "amount": "Amount", + "executionPrice": "Execution price", + "limitPrice": "Limit price", + "surplus": "Surplus", + "expiry": "Expiry", + "oderId": "Order ID", + "status": "Status", + "sellFull": "Sell 1 COW", "sell": "Sell", "oneCOW": "1 COW", "forAtLeast": "for at least", + "forAtLeastFullDai": "for at least DAI", + "forAtLeastFullUni": "for at least UNI", + "forAtLeastFullUSDT": "for at least USDT", + "DAIeqCOW": "1 DAI = COW", + "UNIeqCOW": "1 UNI = K COW", + "DAIeqWETH": "1 DAI = WETH", + "USDTeqUSDC": "1 USDT = 0.19342 USDC", "dai": "DAI", "filled": "Filled", - "partiallyFilled": "Partially filled" + "partiallyFilled": "Partially filled", + "gGpV2": "GPv2Settlement", + "safeAppTitile": "CowSwap", + "title": "Swap order" } } } From 80029b6306064f654b92b11348dcdd2e32a96fb0 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:24:26 +0200 Subject: [PATCH 115/154] Tests: Increase waiters in swap tests (#3886) --- cypress/e2e/regression/swaps.cy.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cypress/e2e/regression/swaps.cy.js b/cypress/e2e/regression/swaps.cy.js index 216f736ef4..f90e564315 100644 --- a/cypress/e2e/regression/swaps.cy.js +++ b/cypress/e2e/regression/swaps.cy.js @@ -30,8 +30,7 @@ describe('Swaps tests', () => { // TODO: Waiting for signer connection issue be resolved it.skip('Verify an order can be created, signed and appear in tx queue', { defaultCommandTimeout: 30000 }, () => { swaps.acceptLegalDisclaimer() - swaps.waitForOrdersCallToComplete() - cy.wait(2000) + cy.wait(4000) main.getIframeBody(iframeSelector).within(() => { swaps.clickOnSettingsBtn() swaps.setSlippage('0.30') @@ -75,8 +74,7 @@ describe('Swaps tests', () => { () => { let isCustomRecipientFound swaps.acceptLegalDisclaimer() - swaps.waitForOrdersCallToComplete() - cy.wait(2000) + cy.wait(4000) main .getIframeBody(iframeSelector) .then(($frame) => { @@ -97,8 +95,7 @@ describe('Swaps tests', () => { it('Verify enabling custom recipient adds that field to the form', { defaultCommandTimeout: 30000 }, () => { swaps.acceptLegalDisclaimer() - swaps.waitForOrdersCallToComplete() - cy.wait(2000) + cy.wait(4000) const isCustomRecipientFound = ($frame, customRecipient) => { const element = $frame.find(customRecipient) From 15be4c9d9503a5246fbcda1201ae91355516dc3b Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:24:15 +0200 Subject: [PATCH 116/154] fix: Display cow fallback handler message [SW-33] (#3882) * fix: Display cow fallback handler message * fix: Remove alert icon and adjust style --- .../settings/FallbackHandler/index.tsx | 18 ++++++++++++++++-- src/features/swap/helpers/utils.ts | 2 ++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/settings/FallbackHandler/index.tsx b/src/components/settings/FallbackHandler/index.tsx index 28bd7e09ea..fff64f9edc 100644 --- a/src/components/settings/FallbackHandler/index.tsx +++ b/src/components/settings/FallbackHandler/index.tsx @@ -1,3 +1,4 @@ +import { TWAP_FALLBACK_HANDLER } from '@/features/swap/helpers/utils' import NextLink from 'next/link' import { Typography, Box, Grid, Paper, Link, Alert } from '@mui/material' import semverSatisfies from 'semver/functions/satisfies' @@ -30,6 +31,7 @@ export const FallbackHandler = (): ReactElement | null => { const hasFallbackHandler = !!safe.fallbackHandler const isOfficial = hasFallbackHandler && safe.fallbackHandler?.value === fallbackHandlerDeployment?.networkAddresses[safe.chainId] + const isTWAPFallbackHandler = safe.fallbackHandler?.value === TWAP_FALLBACK_HANDLER const warning = !hasFallbackHandler ? ( <> @@ -45,6 +47,8 @@ export const FallbackHandler = (): ReactElement | null => { </> )} </> + ) : isTWAPFallbackHandler ? ( + <>This is CoW's fallback handler. It is needed for this Safe to be able to use the TWAP feature for Swaps.</> ) : !isOfficial ? ( <> An <b>unofficial</b> fallback handler is currently set. @@ -61,6 +65,8 @@ export const FallbackHandler = (): ReactElement | null => { </> ) : undefined + console.log(safe.fallbackHandler, fallbackHandlerDeployment) + return ( <Paper sx={{ padding: 4 }}> <Grid container direction="row" justifyContent="space-between" spacing={3}> @@ -78,8 +84,16 @@ export const FallbackHandler = (): ReactElement | null => { <ExternalLink href={HelpCenterArticle.FALLBACK_HANDLER}>here</ExternalLink> </Typography> - <Alert severity={!hasFallbackHandler ? 'warning' : isOfficial ? 'success' : 'info'} sx={{ mt: 2 }}> - {warning && <Typography mb={hasFallbackHandler ? 2 : 0}>{warning}</Typography>} + <Alert + severity={!hasFallbackHandler ? 'warning' : isOfficial || isTWAPFallbackHandler ? 'success' : 'info'} + icon={false} + sx={{ mt: 2 }} + > + {warning && ( + <Typography mb={hasFallbackHandler ? 1 : 0} variant="body2"> + {warning} + </Typography> + )} {safe.fallbackHandler && ( <EthHashInfo diff --git a/src/features/swap/helpers/utils.ts b/src/features/swap/helpers/utils.ts index 87286e7c4f..5252a525f2 100644 --- a/src/features/swap/helpers/utils.ts +++ b/src/features/swap/helpers/utils.ts @@ -20,6 +20,8 @@ function asDecimal(amount: number | bigint, decimals: number): number { return Number(formatUnits(amount, decimals)) } +export const TWAP_FALLBACK_HANDLER = '0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5' + export const getExecutionPrice = ( order: Pick<SwapOrder, 'executedSellAmount' | 'executedBuyAmount' | 'buyToken' | 'sellToken'>, ): number => { From b69aac7413ebe4a1a59c5e4e0a8f274ea2ba1ed0 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:13:31 +0200 Subject: [PATCH 117/154] fix: Remove console-log (#3888) --- src/components/settings/FallbackHandler/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/settings/FallbackHandler/index.tsx b/src/components/settings/FallbackHandler/index.tsx index fff64f9edc..5a9d0c63be 100644 --- a/src/components/settings/FallbackHandler/index.tsx +++ b/src/components/settings/FallbackHandler/index.tsx @@ -65,8 +65,6 @@ export const FallbackHandler = (): ReactElement | null => { </> ) : undefined - console.log(safe.fallbackHandler, fallbackHandlerDeployment) - return ( <Paper sx={{ padding: 4 }}> <Grid container direction="row" justifyContent="space-between" spacing={3}> From 7a7c500e371d04214fae6bf0d5d2f7a1028cb27c Mon Sep 17 00:00:00 2001 From: James Mealy <james@safe.global> Date: Wed, 3 Jul 2024 10:47:33 +0200 Subject: [PATCH 118/154] Feat: create deep link page for messages (#3863) * feat: create a route for diplaying a sinlge message * feat: create share message link component * feat: add text to share link for sign message modal * only show link when message has been been created * move share link button to bottom of the page * Use previous gateway sdk version * add unit tests to sinlgeMsg page * Extract useOrigin hook * create info box for share message link * fix type, remove unused props * fix: only show message link after message exists * Change message after one signature is recevied * Update src/components/tx-flow/flows/SignMessage/SignMessage.tsx Co-authored-by: katspaugh <381895+katspaugh@users.noreply.github.com> * remove whitespace * create safe message bulder for tests * remove commented code * fix: unit tests error state * fix: cypress tests --------- Co-authored-by: katspaugh <381895+katspaugh@users.noreply.github.com> --- .../modals/message_confirmation.pages.js | 10 +-- public/images/messages/link.svg | 4 ++ .../safe-messages/InfoBox/index.tsx | 22 +++--- .../safe-messages/InfoBox/styles.module.css | 5 -- .../safe-messages/MsgDetails/index.tsx | 4 ++ .../MsgListItem/ExpandableMsgItem.tsx | 3 +- .../safe-messages/MsgShareLink/index.tsx | 35 ++++++++++ .../SingleMsg/SingleMsg.test.tsx | 67 +++++++++++++++++++ .../safe-messages/SingleMsg/index.tsx | 30 +++++++++ .../transactions/TxShareLink/index.tsx | 13 +--- .../flows/SignMessage/SignMessage.test.tsx | 18 ++--- .../tx-flow/flows/SignMessage/SignMessage.tsx | 30 +++++++-- src/config/routes.ts | 1 + src/hooks/messages/useSafeMessage.ts | 7 +- src/hooks/useOrigin.ts | 14 ++++ src/pages/transactions/msg.tsx | 25 +++++++ src/services/analytics/events/txList.ts | 4 ++ src/tests/builders/safeMessage.ts | 25 +++++++ 18 files changed, 269 insertions(+), 48 deletions(-) create mode 100644 public/images/messages/link.svg create mode 100644 src/components/safe-messages/MsgShareLink/index.tsx create mode 100644 src/components/safe-messages/SingleMsg/SingleMsg.test.tsx create mode 100644 src/components/safe-messages/SingleMsg/index.tsx create mode 100644 src/hooks/useOrigin.ts create mode 100644 src/pages/transactions/msg.tsx create mode 100644 src/tests/builders/safeMessage.ts diff --git a/cypress/e2e/pages/modals/message_confirmation.pages.js b/cypress/e2e/pages/modals/message_confirmation.pages.js index 67d78c4c56..99d19342f8 100644 --- a/cypress/e2e/pages/modals/message_confirmation.pages.js +++ b/cypress/e2e/pages/modals/message_confirmation.pages.js @@ -9,7 +9,7 @@ const messageInfobox = '[data-testid="message-infobox"]' const messageInfoBoxData = [ 'Collect all the confirmations', 'Confirmations (1 of 2)', - 'The signature will be submitted to the Safe App when the message is fully signed', + 'The signature will be submitted to the requesting app when the message is fully signed', ] export function verifyConfirmationWindowTitle(title) { @@ -36,9 +36,11 @@ export function verifyOffchainMessageHash(index) { } export function checkMessageInfobox() { - cy.get(messageInfobox).within(() => { - main.verifyTextVisibility(messageInfoBoxData) - }) + cy.get(messageInfobox) + .first() + .within(() => { + main.verifyTextVisibility(messageInfoBoxData) + }) } export function clickOnMessageDetails() { diff --git a/public/images/messages/link.svg b/public/images/messages/link.svg new file mode 100644 index 0000000000..4cb52591e4 --- /dev/null +++ b/public/images/messages/link.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M11.7947 4.22853L12.3936 3.64844C14.5921 1.45114 18.1551 1.45114 20.3526 3.64863C22.4917 5.78774 22.5495 9.21871 20.5262 11.4275L20.3527 11.6087L17.3344 14.628C15.1369 16.8243 11.5731 16.8243 9.37541 14.6278C9.16318 14.4156 8.96942 14.1883 8.79617 13.9487C8.47256 13.5011 8.57303 12.876 9.02057 12.5524C9.46812 12.2288 10.0933 12.3292 10.4169 12.7768C10.5278 12.9302 10.6525 13.0765 10.7894 13.2134C12.1554 14.5786 14.3411 14.6274 15.7653 13.36L15.9203 13.2137L18.9384 10.1946C20.3559 8.77716 20.3559 6.48032 18.9384 5.06284C17.5725 3.69693 15.3879 3.64814 13.9527 4.92645L13.7964 5.07394L13.1864 5.66494C12.7897 6.04924 12.1566 6.03922 11.7723 5.64257C11.4176 5.27643 11.3988 4.70883 11.71 4.32141L11.7947 4.22853Z" fill="#5FDDFF"/> +<path d="M6.6671 9.37566C8.8646 7.17817 12.4286 7.17817 14.6273 9.37566C14.7826 9.53091 14.9274 9.69368 15.0624 9.86421C15.4051 10.2973 15.3318 10.9262 14.8988 11.2689C14.4657 11.6116 13.8368 11.5384 13.4941 11.1053C13.407 10.9953 13.3136 10.8904 13.2133 10.7901C11.8464 9.42396 9.66084 9.37517 8.23627 10.6436L8.0812 10.79L5.06251 13.8077C3.64578 15.2252 3.64578 17.5223 5.06232 18.9397C6.42917 20.3065 8.61369 20.3553 10.0348 19.0903L10.1895 18.9443L10.9425 18.1813C11.3304 17.7882 11.9635 17.7841 12.3566 18.172C12.7195 18.5301 12.751 19.0971 12.4485 19.4914L12.366 19.5862L11.6083 20.3539C9.40979 22.5524 5.84663 22.5524 3.64791 20.3537C1.51003 18.2146 1.45225 14.7839 3.47486 12.5748L3.64822 12.3935L6.6671 9.37566Z" fill="#5FDDFF"/> +</svg> diff --git a/src/components/safe-messages/InfoBox/index.tsx b/src/components/safe-messages/InfoBox/index.tsx index cc7b3d5fa0..34bdff369f 100644 --- a/src/components/safe-messages/InfoBox/index.tsx +++ b/src/components/safe-messages/InfoBox/index.tsx @@ -1,6 +1,6 @@ +import type { ComponentType } from 'react' import { type ReactElement, type ReactNode } from 'react' import { Typography, SvgIcon, Divider } from '@mui/material' -import classNames from 'classnames' import InfoIcon from '@/public/images/notifications/info.svg' import css from './styles.module.css' @@ -8,17 +8,17 @@ const InfoBox = ({ title, message, children, - className, + icon = InfoIcon, }: { title: string - message: string - children: ReactNode - className?: string + message: ReactNode + children?: ReactNode + icon?: ComponentType }): ReactElement => { return ( - <div data-testid="message-infobox" className={classNames(css.container, className)}> + <div data-testid="message-infobox" className={css.container}> <div className={css.message}> - <SvgIcon component={InfoIcon} color="info" inheritViewBox fontSize="medium" /> + <SvgIcon component={icon} color="info" inheritViewBox fontSize="medium" /> <div> <Typography variant="subtitle1" fontWeight="bold"> {title} @@ -26,8 +26,12 @@ const InfoBox = ({ <Typography variant="body2">{message}</Typography> </div> </div> - <Divider className={css.divider} /> - <div>{children}</div> + {children && ( + <> + <Divider className={css.divider} /> + <div>{children}</div> + </> + )} </div> ) } diff --git a/src/components/safe-messages/InfoBox/styles.module.css b/src/components/safe-messages/InfoBox/styles.module.css index ae24d95400..58c139337f 100644 --- a/src/components/safe-messages/InfoBox/styles.module.css +++ b/src/components/safe-messages/InfoBox/styles.module.css @@ -13,11 +13,6 @@ gap: var(--space-1); } -.message button { - vertical-align: baseline; - text-decoration: underline; -} - .details { margin-top: var(--space-1); color: var(--color-primary-light); diff --git a/src/components/safe-messages/MsgDetails/index.tsx b/src/components/safe-messages/MsgDetails/index.tsx index 7a1dc3bbb7..fa2b1c7a78 100644 --- a/src/components/safe-messages/MsgDetails/index.tsx +++ b/src/components/safe-messages/MsgDetails/index.tsx @@ -20,6 +20,7 @@ import infoDetailsCss from '@/components/transactions/InfoDetails/styles.module. import { DecodedMsg } from '../DecodedMsg' import CopyButton from '@/components/common/CopyButton' import NamedAddressInfo from '@/components/common/NamedAddressInfo' +import MsgShareLink from '../MsgShareLink' const MsgDetails = ({ msg }: { msg: SafeMessage }): ReactElement => { const wallet = useWallet() @@ -32,6 +33,9 @@ const MsgDetails = ({ msg }: { msg: SafeMessage }): ReactElement => { return ( <div className={txDetailsCss.container}> <div className={txDetailsCss.details}> + <div className={txDetailsCss.shareLink}> + <MsgShareLink safeMessageHash={msg.messageHash} /> + </div> <div className={txDetailsCss.txData}> <InfoDetails title="Created by:"> <EthHashInfo diff --git a/src/components/safe-messages/MsgListItem/ExpandableMsgItem.tsx b/src/components/safe-messages/MsgListItem/ExpandableMsgItem.tsx index 0aee1bf5e7..22e5637f07 100644 --- a/src/components/safe-messages/MsgListItem/ExpandableMsgItem.tsx +++ b/src/components/safe-messages/MsgListItem/ExpandableMsgItem.tsx @@ -8,9 +8,10 @@ import MsgSummary from '@/components/safe-messages/MsgSummary' import txListItemCss from '@/components/transactions/TxListItem/styles.module.css' -const ExpandableMsgItem = ({ msg }: { msg: SafeMessage }): ReactElement => { +const ExpandableMsgItem = ({ msg, expanded = false }: { msg: SafeMessage; expanded?: boolean }): ReactElement => { return ( <Accordion + defaultExpanded={expanded} disableGutters elevation={0} className={txListItemCss.accordion} diff --git a/src/components/safe-messages/MsgShareLink/index.tsx b/src/components/safe-messages/MsgShareLink/index.tsx new file mode 100644 index 0000000000..3321ee16a6 --- /dev/null +++ b/src/components/safe-messages/MsgShareLink/index.tsx @@ -0,0 +1,35 @@ +import type { ReactElement } from 'react' +import { Button, IconButton, Link, SvgIcon } from '@mui/material' +import ShareIcon from '@/public/images/common/share.svg' +import { AppRoutes } from '@/config/routes' +import { useRouter } from 'next/router' +import Track from '@/components/common/Track' +import { MESSAGE_EVENTS } from '@/services/analytics/events/txList' +import React from 'react' +import CopyTooltip from '@/components/common/CopyTooltip' +import useOrigin from '@/hooks/useOrigin' + +const MsgShareLink = ({ safeMessageHash, button }: { safeMessageHash: string; button?: boolean }): ReactElement => { + const router = useRouter() + const { safe = '' } = router.query + const href = `${AppRoutes.transactions.msg}?safe=${safe}&messageHash=${safeMessageHash}` + const txUrl = useOrigin() + href + + return ( + <Track {...MESSAGE_EVENTS.COPY_DEEPLINK}> + <CopyTooltip text={txUrl} initialToolTipText="Copy the message URL"> + {button ? ( + <Button data-testid="share-btn" aria-label="Share" variant="contained" size="small" onClick={() => {}}> + Copy link + </Button> + ) : ( + <IconButton data-testid="share-btn" component={Link} aria-label="Share"> + <SvgIcon component={ShareIcon} inheritViewBox fontSize="small" color="border" /> + </IconButton> + )} + </CopyTooltip> + </Track> + ) +} + +export default MsgShareLink diff --git a/src/components/safe-messages/SingleMsg/SingleMsg.test.tsx b/src/components/safe-messages/SingleMsg/SingleMsg.test.tsx new file mode 100644 index 0000000000..a7421945e7 --- /dev/null +++ b/src/components/safe-messages/SingleMsg/SingleMsg.test.tsx @@ -0,0 +1,67 @@ +import { extendedSafeInfoBuilder } from '@/tests/builders/safe' +import { fireEvent, render, waitFor } from '@/tests/test-utils' +import * as useSafeInfo from '@/hooks/useSafeInfo' +import * as syncSafeMessageSigner from '@/hooks/messages/useSyncSafeMessageSigner' + +import SingleMsg from '.' +import { safeMsgBuilder } from '@/tests/builders/safeMessage' + +const safeMessage = safeMsgBuilder().build() +const extendedSafeInfo = extendedSafeInfoBuilder().build() + +jest.mock('next/router', () => ({ + useRouter() { + return { + pathname: '/transactions/msg', + query: { + safe: extendedSafeInfo.address.value, + messageHash: safeMessage.messageHash, + }, + } + }, +})) + +jest.spyOn(useSafeInfo, 'default').mockImplementation(() => ({ + safeAddress: extendedSafeInfo.address.value, + safe: { + ...extendedSafeInfo, + chainId: '5', + }, + safeError: undefined, + safeLoading: false, + safeLoaded: true, +})) + +describe('SingleMsg', () => { + beforeAll(() => { + jest.clearAllMocks() + }) + afterEach(() => { + jest.clearAllMocks() + }) + + it('renders <SingleMsg />', async () => { + jest + .spyOn(syncSafeMessageSigner, 'fetchSafeMessage') + .mockImplementation(() => Promise.resolve(safeMsgBuilder().build())) + const screen = render(<SingleMsg />) + expect(await screen.findByText('Signature')).toBeInTheDocument() + }) + + it('shows an error when the transaction has failed to load', async () => { + jest + .spyOn(syncSafeMessageSigner, 'fetchSafeMessage') + .mockImplementation(() => Promise.reject(new Error('Server error'))) + + const screen = render(<SingleMsg />) + + await waitFor(() => { + expect(screen.getByText('Failed to load message')).toBeInTheDocument() + }) + + await waitFor(() => { + fireEvent.click(screen.getByText('Details')) + expect(screen.getByText('Server error')).toBeInTheDocument() + }) + }) +}) diff --git a/src/components/safe-messages/SingleMsg/index.tsx b/src/components/safe-messages/SingleMsg/index.tsx new file mode 100644 index 0000000000..dc5c181659 --- /dev/null +++ b/src/components/safe-messages/SingleMsg/index.tsx @@ -0,0 +1,30 @@ +import { useRouter } from 'next/router' +import { TxListGrid } from '@/components/transactions/TxList' +import { TransactionSkeleton } from '@/components/transactions/TxListItem/ExpandableTransactionItem' +import ExpandableMsgItem from '../MsgListItem/ExpandableMsgItem' +import useSafeMessage from '@/hooks/messages/useSafeMessage' +import ErrorMessage from '@/components/tx/ErrorMessage' + +const SingleMsg = () => { + const router = useRouter() + const { messageHash } = router.query + const safeMessageHash = Array.isArray(messageHash) ? messageHash[0] : messageHash + const [safeMessage, _, messageError] = useSafeMessage(safeMessageHash) + + if (safeMessage) { + return ( + <TxListGrid> + <ExpandableMsgItem msg={safeMessage} expanded /> + </TxListGrid> + ) + } + + if (messageError) { + return <ErrorMessage error={messageError}>Failed to load message</ErrorMessage> + } + + // Loading skeleton + return <TransactionSkeleton /> +} + +export default SingleMsg diff --git a/src/components/transactions/TxShareLink/index.tsx b/src/components/transactions/TxShareLink/index.tsx index 4ac7a327cb..10cb9a326c 100644 --- a/src/components/transactions/TxShareLink/index.tsx +++ b/src/components/transactions/TxShareLink/index.tsx @@ -1,5 +1,4 @@ import type { ReactElement } from 'react' -import { useEffect, useState } from 'react' import { IconButton, Link, SvgIcon } from '@mui/material' import ShareIcon from '@/public/images/common/share.svg' import { AppRoutes } from '@/config/routes' @@ -8,17 +7,7 @@ import Track from '@/components/common/Track' import { TX_LIST_EVENTS } from '@/services/analytics/events/txList' import React from 'react' import CopyTooltip from '@/components/common/CopyTooltip' - -const useOrigin = () => { - const [origin, setOrigin] = useState('') - - useEffect(() => { - if (typeof location !== 'undefined') { - setOrigin(location.origin) - } - }, []) - return origin -} +import useOrigin from '@/hooks/useOrigin' const TxShareLink = ({ id }: { id: string }): ReactElement => { const router = useRouter() diff --git a/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx b/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx index ca6d202f98..c43fd49297 100644 --- a/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx +++ b/src/components/tx-flow/flows/SignMessage/SignMessage.test.tsx @@ -292,7 +292,7 @@ describe('SignMessage', () => { confirmationsSubmitted: 1, } as unknown as SafeMessage - jest.spyOn(useSafeMessage, 'default').mockReturnValueOnce([msg, jest.fn]) + jest.spyOn(useSafeMessage, 'default').mockReturnValueOnce([msg, jest.fn, undefined]) const { getByText, rerender } = render( <SignMessage logoUri="www.fake.com/test.png" name="Test App" message={messageText} requestId="123" />, @@ -330,7 +330,7 @@ describe('SignMessage', () => { fireEvent.click(button) }) - jest.spyOn(useSafeMessage, 'default').mockReturnValue([newMsg, jest.fn]) + jest.spyOn(useSafeMessage, 'default').mockReturnValue([newMsg, jest.fn, undefined]) rerender(<SignMessage logoUri="www.fake.com/test.png" name="Test App" message={messageText} requestId="123" />) @@ -350,7 +350,7 @@ describe('SignMessage', () => { it('displays an error if no wallet is connected', () => { jest.spyOn(useWalletHook, 'default').mockReturnValue(null) jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => false) - jest.spyOn(useSafeMessage, 'default').mockImplementation(() => [undefined, jest.fn()]) + jest.spyOn(useSafeMessage, 'default').mockImplementation(() => [undefined, jest.fn(), undefined]) const { getByText } = render( <SignMessage @@ -372,7 +372,7 @@ describe('SignMessage', () => { jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => true) jest.spyOn(useIsWrongChainHook, 'default').mockImplementation(() => true) jest.spyOn(useChainsHook, 'useCurrentChain').mockReturnValue(chainBuilder().build()) - jest.spyOn(useSafeMessage, 'default').mockImplementation(() => [undefined, jest.fn()]) + jest.spyOn(useSafeMessage, 'default').mockImplementation(() => [undefined, jest.fn(), undefined]) const { getByText } = render( <SignMessage @@ -398,7 +398,7 @@ describe('SignMessage', () => { } as ConnectedWallet), ) jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => false) - jest.spyOn(useSafeMessage, 'default').mockImplementation(() => [undefined, jest.fn()]) + jest.spyOn(useSafeMessage, 'default').mockImplementation(() => [undefined, jest.fn(), undefined]) const { getByText } = render( <SignMessage @@ -451,7 +451,7 @@ describe('SignMessage', () => { confirmationsSubmitted: 1, } as unknown as SafeMessage - jest.spyOn(useSafeMessage, 'default').mockReturnValue([msg, jest.fn]) + jest.spyOn(useSafeMessage, 'default').mockReturnValue([msg, jest.fn, undefined]) const { getByText } = render( <SignMessage logoUri="www.fake.com/test.png" name="Test App" message={messageText} requestId="123" />, @@ -473,7 +473,7 @@ describe('SignMessage', () => { } as ConnectedWallet), ) - jest.spyOn(useSafeMessage, 'default').mockReturnValue([undefined, jest.fn()]) + jest.spyOn(useSafeMessage, 'default').mockReturnValue([undefined, jest.fn(), undefined]) jest.spyOn(useIsSafeOwnerHook, 'default').mockImplementation(() => true) ;(getSafeMessage as jest.Mock).mockRejectedValue(new Error('SafeMessage not found')) @@ -541,7 +541,7 @@ describe('SignMessage', () => { } as unknown as SafeMessage ;(getSafeMessage as jest.Mock).mockResolvedValue(msg) - jest.spyOn(useSafeMessage, 'default').mockReturnValue([msg, jest.fn()]) + jest.spyOn(useSafeMessage, 'default').mockReturnValue([msg, jest.fn(), undefined]) const { getByText } = render( <SignMessage logoUri="www.fake.com/test.png" name="Test App" message={messageText} requestId="123" />, @@ -610,7 +610,7 @@ describe('SignMessage', () => { preparedSignature: '0x678', } as unknown as SafeMessage - jest.spyOn(useSafeMessage, 'default').mockReturnValue([msg, jest.fn()]) + jest.spyOn(useSafeMessage, 'default').mockReturnValue([msg, jest.fn(), undefined]) ;(getSafeMessage as jest.Mock).mockResolvedValue(msg) const { getByText } = render( diff --git a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx index 7531d2a3dc..f67a8b9ed8 100644 --- a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx +++ b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx @@ -54,6 +54,8 @@ import { selectBlindSigning } from '@/store/settingsSlice' import NextLink from 'next/link' import { AppRoutes } from '@/config/routes' import { useRouter } from 'next/router' +import MsgShareLink from '@/components/safe-messages/MsgShareLink' +import LinkIcon from '@/public/images/messages/link.svg' const createSkeletonMessage = (confirmationsRequired: number): SafeMessage => { return { @@ -232,12 +234,14 @@ const SignMessage = ({ message, safeAppId, requestId }: ProposeProps | ConfirmPr const [safeMessage, setSafeMessage] = useSafeMessage(safeMessageHash) const isPlainTextMessage = typeof decodedMessage === 'string' const decodedMessageAsString = isPlainTextMessage ? decodedMessage : JSON.stringify(decodedMessage, null, 2) - const hasSigned = !!safeMessage?.confirmations.some(({ owner }) => owner.value === wallet?.address) + const signedByCurrentSafe = !!safeMessage?.confirmations.some(({ owner }) => owner.value === wallet?.address) + const hasSignature = safeMessage?.confirmations && safeMessage.confirmations.length > 0 const isFullySigned = !!safeMessage?.preparedSignature const isEip712 = isEIP712TypedData(decodedMessage) const isBlindSigningRequest = isBlindSigningPayload(decodedMessage) const isBlindSigningEnabled = useAppSelector(selectBlindSigning) - const isDisabled = !isOwner || hasSigned || !safe.deployed || (!isBlindSigningEnabled && isBlindSigningRequest) + const isDisabled = + !isOwner || signedByCurrentSafe || !safe.deployed || (!isBlindSigningEnabled && isBlindSigningRequest) const { onSign, submitError } = useSyncSafeMessageSigner( safeMessage, @@ -320,14 +324,14 @@ const SignMessage = ({ message, safeAppId, requestId }: ProposeProps | ConfirmPr ) : ( <> <TxCard> - <AlreadySignedByOwnerMessage hasSigned={hasSigned} /> + <AlreadySignedByOwnerMessage hasSigned={signedByCurrentSafe} /> <InfoBox title="Collect all the confirmations" message={ - requestId + requestId && !hasSignature ? 'Please keep this modal open until all signers confirm this message. Closing the modal will abort the signing request.' - : 'The signature will be submitted to the Safe App when the message is fully signed.' + : 'The signature will be submitted to the requesting app when the message is fully signed.' } > <MsgSigners @@ -338,6 +342,22 @@ const SignMessage = ({ message, safeAppId, requestId }: ProposeProps | ConfirmPr /> </InfoBox> + {hasSignature && ( + <InfoBox + title="Share the link with other owners" + message={ + <> + <Typography mb={2}> + The owners will receive a notification about signing the message. You can also share the link with + them to speed up the process. + </Typography> + <MsgShareLink safeMessageHash={safeMessageHash} button /> + </> + } + icon={LinkIcon} + /> + )} + <WrongChainWarning /> <MessageDialogError isOwner={isOwner} submitError={submitError} /> diff --git a/src/config/routes.ts b/src/config/routes.ts index 3edbdd0773..d841873f49 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -45,6 +45,7 @@ export const AppRoutes = { }, transactions: { tx: '/transactions/tx', + msg: '/transactions/msg', queue: '/transactions/queue', messages: '/transactions/messages', index: '/transactions', diff --git a/src/hooks/messages/useSafeMessage.ts b/src/hooks/messages/useSafeMessage.ts index fe5ec4eeb0..e1ae7a4b0c 100644 --- a/src/hooks/messages/useSafeMessage.ts +++ b/src/hooks/messages/useSafeMessage.ts @@ -6,7 +6,7 @@ import useAsync from '../useAsync' import useSafeInfo from '../useSafeInfo' import { fetchSafeMessage } from './useSyncSafeMessageSigner' -const useSafeMessage = (safeMessageHash: string) => { +const useSafeMessage = (safeMessageHash: string | undefined) => { const [safeMessage, setSafeMessage] = useState<SafeMessage | undefined>() const { safe } = useSafeInfo() @@ -17,7 +17,8 @@ const useSafeMessage = (safeMessageHash: string) => { ?.filter(isSafeMessageListItem) .find((msg) => msg.messageHash === safeMessageHash) - const [updatedMessage] = useAsync(async () => { + const [updatedMessage, messageError] = useAsync(async () => { + if (!safeMessageHash) return return fetchSafeMessage(safeMessageHash, safe.chainId) // eslint-disable-next-line react-hooks/exhaustive-deps }, [safeMessageHash, safe.chainId, safe.messagesTag]) @@ -26,7 +27,7 @@ const useSafeMessage = (safeMessageHash: string) => { setSafeMessage(updatedMessage ?? ongoingMessage) }, [ongoingMessage, updatedMessage]) - return [safeMessage, setSafeMessage] as const + return [safeMessage, setSafeMessage, messageError] as const } export default useSafeMessage diff --git a/src/hooks/useOrigin.ts b/src/hooks/useOrigin.ts new file mode 100644 index 0000000000..d2710581ea --- /dev/null +++ b/src/hooks/useOrigin.ts @@ -0,0 +1,14 @@ +import { useEffect, useState } from 'react' + +const useOrigin = () => { + const [origin, setOrigin] = useState('') + + useEffect(() => { + if (typeof location !== 'undefined') { + setOrigin(location.origin) + } + }, []) + return origin +} + +export default useOrigin diff --git a/src/pages/transactions/msg.tsx b/src/pages/transactions/msg.tsx new file mode 100644 index 0000000000..2db44e1e61 --- /dev/null +++ b/src/pages/transactions/msg.tsx @@ -0,0 +1,25 @@ +import type { NextPage } from 'next' +import Head from 'next/head' + +import Typography from '@mui/material/Typography' +import SingleMsg from '@/components/safe-messages/SingleMsg' + +const SingleTransaction: NextPage = () => { + return ( + <> + <Head> + <title>{'Safe{Wallet} – Message details'} + + +

      + + Message details + + + +
      + + ) +} + +export default SingleTransaction diff --git a/src/services/analytics/events/txList.ts b/src/services/analytics/events/txList.ts index 7cdcc687b4..12b0ea586d 100644 --- a/src/services/analytics/events/txList.ts +++ b/src/services/analytics/events/txList.ts @@ -67,4 +67,8 @@ export const MESSAGE_EVENTS = { action: 'Sign message', category: TX_LIST_CATEGORY, }, + COPY_DEEPLINK: { + action: 'Copy message deeplink', + category: TX_LIST_CATEGORY, + }, } diff --git a/src/tests/builders/safeMessage.ts b/src/tests/builders/safeMessage.ts new file mode 100644 index 0000000000..b3a9da6a34 --- /dev/null +++ b/src/tests/builders/safeMessage.ts @@ -0,0 +1,25 @@ +import { Builder, type IBuilder } from '@/tests/Builder' +import { faker } from '@faker-js/faker' +import { SafeMessageListItemType, SafeMessageStatus, type SafeMessage } from '@safe-global/safe-gateway-typescript-sdk' + +export function safeMsgBuilder(): IBuilder { + return Builder.new().with({ + type: SafeMessageListItemType.MESSAGE, + messageHash: faker.string.hexadecimal(), + status: SafeMessageStatus.NEEDS_CONFIRMATION, + logoUri: null, + name: null, + message: 'Message text', + creationTimestamp: faker.date.past().getTime(), + modifiedTimestamp: faker.date.past().getTime(), + confirmationsSubmitted: 1, + confirmationsRequired: 2, + proposedBy: { value: faker.finance.ethereumAddress() }, + confirmations: [ + { + owner: { value: faker.finance.ethereumAddress() }, + signature: '', + }, + ], + }) +} From 26a99fb504181402b2482494585810a997b378a2 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Wed, 3 Jul 2024 11:57:51 +0200 Subject: [PATCH 119/154] Feat swaps fees [SW-34] (#3880) * feat: dynamic swap fee * fix: change tooltip text * fix: use functional params update * fix: define constants for the tiers * fix: switching between swap and twap was broken * feat: display the widget fee next to the order * feat: add new legaldisclaimer for swaps * fix: failing tests * fix: typo * chore: change fee recipient * fix: hide fee data in queue * fix: wrong styling for link * fix: check for stablecoin was not working * fix: wrong total fee description * feat: add most used stable-coins --- src/config/constants.ts | 1 + .../swap/components/HelpIconTooltip/index.tsx | 25 + .../swap/components/LegalDisclaimer/index.tsx | 35 ++ .../LegalDisclaimer/styles.module.css | 8 + .../swap/components/SwapOrder/index.tsx | 5 +- .../components/SwapOrder/rows/SurplusFee.tsx | 44 ++ .../OrderFeeConfirmationView.tsx | 39 ++ .../SwapOrderConfirmationView/index.tsx | 2 + src/features/swap/constants.ts | 2 + .../swap/helpers/__tests__/fee.test.ts | 131 +++++ src/features/swap/helpers/data/stablecoins.ts | 504 ++++++++++++++++++ src/features/swap/helpers/fee.ts | 52 ++ src/features/swap/helpers/utils.ts | 23 +- src/features/swap/index.tsx | 135 +++-- src/features/swap/store/swapParamsSlice.ts | 18 +- src/features/swap/types.ts | 16 + src/features/swap/useSwapConsent.ts | 2 +- src/pages/swap.tsx | 5 +- 18 files changed, 982 insertions(+), 65 deletions(-) create mode 100644 src/features/swap/components/HelpIconTooltip/index.tsx create mode 100644 src/features/swap/components/LegalDisclaimer/index.tsx create mode 100644 src/features/swap/components/LegalDisclaimer/styles.module.css create mode 100644 src/features/swap/components/SwapOrder/rows/SurplusFee.tsx create mode 100644 src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx create mode 100644 src/features/swap/helpers/__tests__/fee.test.ts create mode 100644 src/features/swap/helpers/data/stablecoins.ts create mode 100644 src/features/swap/helpers/fee.ts create mode 100644 src/features/swap/types.ts diff --git a/src/config/constants.ts b/src/config/constants.ts index 881d7f3643..60a9eb1f8f 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -85,6 +85,7 @@ export const HelpCenterArticle = { UNEXPECTED_DELEGATE_CALL: `${HELP_CENTER_URL}/en/articles/40794-why-do-i-see-an-unexpected-delegate-call-warning-in-my-transaction`, DELEGATES: `${HELP_CENTER_URL}/en/articles/40799-what-is-a-delegate-key`, PUSH_NOTIFICATIONS: `${HELP_CENTER_URL}/en/articles/99197-how-to-start-receiving-web-push-notifications-in-the-web-wallet`, + SWAP_WIDGET_FEES: `${HELP_CENTER_URL}/en/articles/178530-how-does-the-widget-fee-work-for-native-swaps`, } as const export const HelperCenterArticleTitles = { RECOVERY: 'Learn more about the Account recovery process', diff --git a/src/features/swap/components/HelpIconTooltip/index.tsx b/src/features/swap/components/HelpIconTooltip/index.tsx new file mode 100644 index 0000000000..83ec96c0a4 --- /dev/null +++ b/src/features/swap/components/HelpIconTooltip/index.tsx @@ -0,0 +1,25 @@ +import { SvgIcon, Tooltip } from '@mui/material' +import InfoIcon from '@/public/images/notifications/info.svg' +import type { ReactNode } from 'react' + +type Props = { + title: ReactNode +} +export const HelpIconTooltip = ({ title }: Props) => { + return ( + + + + + + ) +} diff --git a/src/features/swap/components/LegalDisclaimer/index.tsx b/src/features/swap/components/LegalDisclaimer/index.tsx new file mode 100644 index 0000000000..3caad26deb --- /dev/null +++ b/src/features/swap/components/LegalDisclaimer/index.tsx @@ -0,0 +1,35 @@ +import ExternalLink from '@/components/common/ExternalLink' +import { AppRoutes } from '@/config/routes' +import { Typography } from '@mui/material' + +import css from './styles.module.css' + +const LegalDisclaimerContent = () => ( +
      +
      + + You are now accessing a third party widget! + + + + Please note that we do not own, control, maintain or audit the CoW Swap Widget. Use of the widget is subject to + third party terms & conditions. We are not liable for any loss you may suffer in connection with interacting + with the widget, which is at your own risk. + + + + Our{' '} + + terms + {' '} + contain more detailed provisions binding on you relating to such third party content. + + + By clicking "continue" you re-confirm to have read and understood our terms and this message, and + agree to them. + +
      +
      +) + +export default LegalDisclaimerContent diff --git a/src/features/swap/components/LegalDisclaimer/styles.module.css b/src/features/swap/components/LegalDisclaimer/styles.module.css new file mode 100644 index 0000000000..926ac0a491 --- /dev/null +++ b/src/features/swap/components/LegalDisclaimer/styles.module.css @@ -0,0 +1,8 @@ +.disclaimerContainer p, +.disclaimerContainer h3 { + line-height: 24px; +} + +.disclaimerInner p { + text-align: justify; +} diff --git a/src/features/swap/components/SwapOrder/index.tsx b/src/features/swap/components/SwapOrder/index.tsx index a2fe88d3ca..8610f97072 100644 --- a/src/features/swap/components/SwapOrder/index.tsx +++ b/src/features/swap/components/SwapOrder/index.tsx @@ -8,8 +8,8 @@ import Stack from '@mui/material/Stack' import type { ReactElement } from 'react' import type { TwapOrder as SwapTwapOrder } from '@safe-global/safe-gateway-typescript-sdk' import { - type SwapOrder as SwapOrderType, type Order, + type SwapOrder as SwapOrderType, type TransactionData, } from '@safe-global/safe-gateway-typescript-sdk' import { DataRow } from '@/components/common/Table/DataRow' @@ -34,6 +34,7 @@ import { EmptyRow } from '@/components/common/Table/EmptyRow' import { PartDuration } from '@/features/swap/components/SwapOrder/rows/PartDuration' import { PartSellAmount } from '@/features/swap/components/SwapOrder/rows/PartSellAmount' import { PartBuyAmount } from '@/features/swap/components/SwapOrder/rows/PartBuyAmount' +import { SurplusFee } from '@/features/swap/components/SwapOrder/rows/SurplusFee' type SwapOrderProps = { txData?: TransactionData @@ -200,6 +201,7 @@ export const SellOrder = ({ order }: { order: SwapOrderType }) => { , , , + , ]} /> ) @@ -222,6 +224,7 @@ export const TwapOrder = ({ order }: { order: SwapTwapOrder }) => { , , , + , , {numberOfParts} diff --git a/src/features/swap/components/SwapOrder/rows/SurplusFee.tsx b/src/features/swap/components/SwapOrder/rows/SurplusFee.tsx new file mode 100644 index 0000000000..b03af41bef --- /dev/null +++ b/src/features/swap/components/SwapOrder/rows/SurplusFee.tsx @@ -0,0 +1,44 @@ +import type { Order } from '@safe-global/safe-gateway-typescript-sdk' +import { getOrderFeeBps } from '@/features/swap/helpers/utils' +import { DataRow } from '@/components/common/Table/DataRow' +import { formatVisualAmount } from '@/utils/formatters' +import { HelpIconTooltip } from '@/features/swap/components/HelpIconTooltip' + +export const SurplusFee = ({ + order, +}: { + order: Pick +}) => { + const bps = getOrderFeeBps(order) + const { executedSurplusFee, status, sellToken, buyToken, kind } = order + let token = sellToken + + if (kind === 'buy') { + token = buyToken + } + + if (executedSurplusFee === null || typeof executedSurplusFee === 'undefined' || executedSurplusFee === '0') { + return null + } + + return ( + + Total fees + + The amount of fees paid for this order. + {bps > 0 && `This includes a Widget fee of ${bps / 100} % and network fees.`} + + } + /> + + } + key="widget_fee" + > + {formatVisualAmount(BigInt(executedSurplusFee), token.decimals)} {token.symbol} + + ) +} diff --git a/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx new file mode 100644 index 0000000000..dd283a1c3c --- /dev/null +++ b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx @@ -0,0 +1,39 @@ +import type { OrderConfirmationView } from '@safe-global/safe-gateway-typescript-sdk' +import { getOrderFeeBps } from '@/features/swap/helpers/utils' +import { DataRow } from '@/components/common/Table/DataRow' +import { HelpCenterArticle } from '@/config/constants' +import { HelpIconTooltip } from '@/features/swap/components/HelpIconTooltip' +import MUILink from '@mui/material/Link' + +export const OrderFeeConfirmationView = ({ + order, +}: { + order: Pick + hideWhenNonFulfilled?: boolean +}) => { + const bps = getOrderFeeBps(order) + + const title = ( + <> + Widget fee{' '} + + The tiered widget fees incurred here will contribute to a license fee that supports the Safe community. + Neither Safe Ecosystem Foundation nor {`Safe{Wallet}`} + operate the CoW Swap Widget and/or CoW Swap.{` `} + + Learn more + + + } + /> + + ) + + return ( + + {Number(bps) / 100} % + + ) +} diff --git a/src/features/swap/components/SwapOrderConfirmationView/index.tsx b/src/features/swap/components/SwapOrderConfirmationView/index.tsx index 029bec33f3..515763aad8 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/index.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/index.tsx @@ -19,6 +19,7 @@ import NamedAddress from '@/components/common/NamedAddressInfo' import { PartDuration } from '@/features/swap/components/SwapOrder/rows/PartDuration' import { PartSellAmount } from '@/features/swap/components/SwapOrder/rows/PartSellAmount' import { PartBuyAmount } from '@/features/swap/components/SwapOrder/rows/PartBuyAmount' +import { OrderFeeConfirmationView } from '@/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView' type SwapOrderProps = { order: OrderConfirmationView @@ -92,6 +93,7 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd ) : ( <> ), + , , diff --git a/src/features/swap/constants.ts b/src/features/swap/constants.ts index f1ecfba7c1..c6245fac90 100644 --- a/src/features/swap/constants.ts +++ b/src/features/swap/constants.ts @@ -2,3 +2,5 @@ export const SWAP_TITLE = 'Safe Swap' export const SWAP_ORDER_TITLE = 'Swap order' export const LIMIT_ORDER_TITLE = 'Limit order' export const TWAP_ORDER_TITLE = 'TWAP order' + +export const SWAP_FEE_RECIPIENT = '0x63695Eee2c3141BDE314C5a6f89B98E62808d716' diff --git a/src/features/swap/helpers/__tests__/fee.test.ts b/src/features/swap/helpers/__tests__/fee.test.ts new file mode 100644 index 0000000000..003bc289bf --- /dev/null +++ b/src/features/swap/helpers/__tests__/fee.test.ts @@ -0,0 +1,131 @@ +import { calculateFeePercentageInBps } from '@/features/swap/helpers/fee' +import { type OnTradeParamsPayload } from '@cowprotocol/events' +import { stableCoinAddresses } from '@/features/swap/helpers/data/stablecoins' + +describe('calculateFeePercentageInBps', () => { + it('returns correct fee for non-stablecoin and sell order', () => { + let orderParams: OnTradeParamsPayload = { + sellToken: { address: 'non-stablecoin-address' }, + buyToken: { address: 'non-stablecoin-address' }, + buyTokenFiatAmount: '50000', + sellTokenFiatAmount: '50000', + orderKind: 'sell', + } as OnTradeParamsPayload + + const result = calculateFeePercentageInBps(orderParams) + expect(result).toBe(35) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '100000', + sellTokenFiatAmount: '100000', + } + + const result2 = calculateFeePercentageInBps(orderParams) + expect(result2).toBe(20) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '1000000', + sellTokenFiatAmount: '1000000', + } + + const result3 = calculateFeePercentageInBps(orderParams) + expect(result3).toBe(10) + }) + + it('returns correct fee for non-stablecoin and buy order', () => { + let orderParams: OnTradeParamsPayload = { + sellToken: { address: 'non-stablecoin-address' }, + buyToken: { address: 'non-stablecoin-address' }, + buyTokenFiatAmount: '50000', + sellTokenFiatAmount: '50000', + orderKind: 'buy', + } as OnTradeParamsPayload + + const result = calculateFeePercentageInBps(orderParams) + expect(result).toBe(35) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '100000', + sellTokenFiatAmount: '100000', + } + + const result2 = calculateFeePercentageInBps(orderParams) + expect(result2).toBe(20) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '1000000', + sellTokenFiatAmount: '1000000', + } + + const result3 = calculateFeePercentageInBps(orderParams) + expect(result3).toBe(10) + }) + + it('returns correct fee for stablecoin and sell order', () => { + const stableCoinAddressesKeys = Object.keys(stableCoinAddresses) + let orderParams: OnTradeParamsPayload = { + sellToken: { address: stableCoinAddressesKeys[0] }, + buyToken: { address: stableCoinAddressesKeys[1] }, + buyTokenFiatAmount: '50000', + sellTokenFiatAmount: '50000', + orderKind: 'sell', + } as OnTradeParamsPayload + + const result = calculateFeePercentageInBps(orderParams) + expect(result).toBe(10) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '100000', + sellTokenFiatAmount: '100000', + } + + const result2 = calculateFeePercentageInBps(orderParams) + expect(result2).toBe(7) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '1000000', + sellTokenFiatAmount: '1000000', + } + + const result3 = calculateFeePercentageInBps(orderParams) + expect(result3).toBe(5) + }) + + it('returns correct fee for stablecoin and buy order', () => { + const stableCoinAddressesKeys = Object.keys(stableCoinAddresses) + let orderParams: OnTradeParamsPayload = { + sellToken: { address: stableCoinAddressesKeys[0] }, + buyToken: { address: stableCoinAddressesKeys[1] }, + buyTokenFiatAmount: '50000', + sellTokenFiatAmount: '50000', + orderKind: 'buy', + } as OnTradeParamsPayload + + const result = calculateFeePercentageInBps(orderParams) + expect(result).toBe(10) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '100000', + sellTokenFiatAmount: '100000', + } + + const result2 = calculateFeePercentageInBps(orderParams) + expect(result2).toBe(7) + + orderParams = { + ...orderParams, + buyTokenFiatAmount: '1000000', + sellTokenFiatAmount: '1000000', + } + + const result3 = calculateFeePercentageInBps(orderParams) + expect(result3).toBe(5) + }) +}) diff --git a/src/features/swap/helpers/data/stablecoins.ts b/src/features/swap/helpers/data/stablecoins.ts new file mode 100644 index 0000000000..818ef01d31 --- /dev/null +++ b/src/features/swap/helpers/data/stablecoins.ts @@ -0,0 +1,504 @@ +export const stableCoinAddresses: { + [address: string]: { + name: string + symbol: string + chains: Array<'gnosis' | 'ethereum' | 'arbitrum-one' | 'sepolia'> + } +} = { + '0xdd96b45877d0e8361a4ddb732da741e97f3191ff': { + name: 'BUSD Token from BSC', + symbol: 'BUSD', + chains: ['gnosis'], + }, + '0x44fa8e6f47987339850636f88629646662444217': { + name: 'Dai Stablecoin on Gnosis', + symbol: 'DAI', + chains: ['gnosis'], + }, + '0x1e37e5b504f7773460d6eb0e24d2e7c223b66ec7': { + name: 'HUSD on Gnosis', + symbol: 'HUSD', + chains: ['gnosis'], + }, + '0xb714654e905edad1ca1940b7790a8239ece5a9ff': { + name: 'TrueUSD on Gnosis', + symbol: 'TUSD', + chains: ['gnosis'], + }, + '0xddafbb505ad214d7b80b1f830fccc89b60fb7a83': { + name: 'USD//C on Gnosis', + symbol: 'USDC', + chains: ['gnosis'], + }, + '0x4ecaba5870353805a9f068101a40e0f32ed605c6': { + name: 'Tether on Gnosis', + symbol: 'USDT', + chains: ['gnosis'], + }, + '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063': { + name: 'Dai Stablecoin', + symbol: 'DAI', + chains: ['gnosis'], + }, + '0x104592a158490a9228070E0A8e5343B499e125D0': { + name: 'Frax', + symbol: 'FRAX', + chains: ['gnosis'], + }, + '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174': { + name: 'USD Coin', + symbol: 'USDC', + chains: ['gnosis'], + }, + '0xc2132D05D31c914a87C6611C10748AEb04B58e8F': { + name: 'Tether USD', + symbol: 'USDT', + chains: ['gnosis'], + }, + '0x3e7676937A7E96CFB7616f255b9AD9FF47363D4b': { + name: 'Dai Stablecoin', + symbol: 'DAI', + chains: ['gnosis'], + }, + '0x0faF6df7054946141266420b43783387A78d82A9': { + name: 'USDC from Ethereum', + symbol: 'USDC', + chains: ['gnosis'], + }, + '0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1': { + name: 'Dai Stablecoin', + symbol: 'DAI', + chains: ['gnosis'], + }, + '0x3813e82e6f7098b9583FC0F33a962D02018B6803': { + name: 'Tether USD', + symbol: 'USDT', + chains: ['gnosis'], + }, + + '0xdac17f958d2ee523a2206206994597c13d831ec7': { + name: 'Tether', + symbol: 'usdt', + chains: ['ethereum'], + }, + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { + name: 'USDC', + symbol: 'usdc', + chains: ['ethereum'], + }, + '0xaf88d065e77c8cc2239327c5edb3a432268e5831': { + name: 'USDC', + symbol: 'usdc', + chains: ['arbitrum-one'], + }, + '0x6b175474e89094c44da98b954eedeac495271d0f': { + name: 'Dai', + symbol: 'dai', + chains: ['ethereum'], + }, + '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1': { + name: 'Dai', + symbol: 'dai', + chains: ['arbitrum-one'], + }, + '0x4c9edd5852cd905f086c759e8383e09bff1e68b3': { + name: 'Ethena USDe', + symbol: 'usde', + chains: ['ethereum'], + }, + '0xc5f0f7b66764f6ec8c8dff7ba683102295e16409': { + name: 'First Digital USD', + symbol: 'fdusd', + chains: ['ethereum'], + }, + '0x0c10bf8fcb7bf5412187a595ab97a3609160b5c6': { + name: 'USDD', + symbol: 'usdd', + chains: ['ethereum'], + }, + '0x680447595e8b7b3aa1b43beb9f6098c79ac2ab3f': { + name: 'USDD', + symbol: 'usdd', + chains: ['arbitrum-one'], + }, + '0x853d955acef822db058eb8505911ed77f175b99e': { + name: 'Frax', + symbol: 'frax', + chains: ['ethereum'], + }, + '0x17fc002b466eec40dae837fc4be5c67993ddbd6f': { + name: 'Frax', + symbol: 'frax', + chains: ['arbitrum-one'], + }, + '0x68749665ff8d2d112fa859aa293f07a622782f38': { + name: 'Tether Gold', + symbol: 'xaut', + chains: ['ethereum'], + }, + '0x0000000000085d4780b73119b644ae5ecd22b376': { + name: 'TrueUSD', + symbol: 'tusd', + chains: ['ethereum'], + }, + '0x45804880de22913dafe09f4980848ece6ecbaf78': { + name: 'PAX Gold', + symbol: 'paxg', + chains: ['ethereum'], + }, + '0x6c3ea9036406852006290770bedfcaba0e23a0e8': { + name: 'PayPal USD', + symbol: 'pyusd', + chains: ['ethereum'], + }, + '0xbc6da0fe9ad5f3b0d58160288917aa56653660e9': { + name: 'Alchemix USD', + symbol: 'alusd', + chains: ['ethereum'], + }, + '0xdb25f211ab05b1c97d595516f45794528a807ad8': { + name: 'STASIS EURO', + symbol: 'eurs', + chains: ['ethereum'], + }, + '0x8e870d67f660d95d5be530380d0ec0bd388289e1': { + name: 'Pax Dollar', + symbol: 'usdp', + chains: ['ethereum'], + }, + '0xf939e0a03fb07f59a73314e73794be0e57ac1b4e': { + name: 'crvUSD', + symbol: 'crvusd', + chains: ['ethereum'], + }, + '0x498bf2b1e120fed3ad3d42ea2165e9b73f99c1e5': { + name: 'crvUSD', + symbol: 'crvusd', + chains: ['arbitrum-one'], + }, + '0x056fd409e1d7a124bd7017459dfea2f387b6d5cd': { + name: 'Gemini Dollar', + symbol: 'gusd', + chains: ['ethereum'], + }, + '0x865377367054516e17014ccded1e7d814edc9ce4': { + name: 'DOLA', + symbol: 'dola', + chains: ['ethereum'], + }, + '0x6a7661795c374c0bfc635934efaddff3a7ee23b6': { + name: 'DOLA', + symbol: 'dola', + chains: ['arbitrum-one'], + }, + '0x40d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f': { + name: 'GHO', + symbol: 'gho', + chains: ['ethereum'], + }, + '0x5f98805a4e8be255a32880fdec7f6728c6568ba0': { + name: 'Liquity USD', + symbol: 'lusd', + chains: ['ethereum'], + }, + '0x93b346b6bc2548da6a1e7d98e9a421b42541425b': { + name: 'Liquity USD', + symbol: 'lusd', + chains: ['arbitrum-one'], + }, + '0x4fabb145d64652a948d72533023f6e7a623c7c53': { + name: 'BUSD', + symbol: 'busd', + chains: ['ethereum'], + }, + '0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3': { + name: 'Magic Internet Money (Ethereum)', + symbol: 'mim', + chains: ['ethereum'], + }, + '0x59d9356e565ab3a36dd77763fc0d87feaf85508c': { + name: 'Mountain Protocol USD', + symbol: 'usdm', + chains: ['ethereum', 'arbitrum-one'], + }, + '0xbea0000029ad1c77d3d5d23ba2d8893db9d1efab': { + name: 'Bean', + symbol: 'bean', + chains: ['ethereum'], + }, + '0x57ab1ec28d129707052df4df418d58a2d46d5f51': { + name: 'sUSD', + symbol: 'susd', + chains: ['ethereum'], + }, + '0xa970af1a584579b618be4d69ad6f73459d112f95': { + name: 'sUSD', + symbol: 'susd', + chains: ['arbitrum-one'], + }, + '0xc581b735a1688071a1746c968e0798d642ede491': { + name: 'Euro Tether', + symbol: 'eurt', + chains: ['ethereum'], + }, + '0xa774ffb4af6b0a91331c084e1aebae6ad535e6f3': { + name: 'flexUSD', + symbol: 'flexusd', + chains: ['ethereum'], + }, + '0x1a7e4e63778b4f12a199c062f3efdd288afcbce8': { + name: 'EURA', + symbol: 'eura', + chains: ['ethereum'], + }, + '0xfa5ed56a203466cbbc2430a43c66b9d8723528e7': { + name: 'EURA', + symbol: 'eura', + chains: ['arbitrum-one'], + }, + '0xe07f9d810a48ab5c3c914ba3ca53af14e4491e8a': { + name: 'Gyroscope GYD', + symbol: 'gyd', + chains: ['ethereum'], + }, + '0xca5d8f8a8d49439357d3cf46ca2e720702f132b8': { + name: 'Gyroscope GYD', + symbol: 'gyd', + chains: ['arbitrum-one'], + }, + '0x2c537e5624e4af88a7ae4060c022609376c8d0eb': { + name: 'BiLira', + symbol: 'tryb', + chains: ['ethereum'], + }, + '0x0e573ce2736dd9637a0b21058352e1667925c7a8': { + name: 'Verified USD', + symbol: 'usdv', + chains: ['ethereum'], + }, + '0x323665443cef804a3b5206103304bd4872ea4253': { + name: 'Verified USD', + symbol: 'usdv', + chains: ['arbitrum-one'], + }, + '0x956f47f50a910163d8bf957cf5846d573e7f87ca': { + name: 'Fei USD', + symbol: 'fei', + chains: ['ethereum'], + }, + '0x0a5e677a6a24b2f1a2bf4f3bffc443231d2fdec8': { + name: 'dForce USD', + symbol: 'usx', + chains: ['ethereum'], + }, + '0x641441c631e2f909700d2f41fd87f0aa6a6b4edb': { + name: 'dForce USD', + symbol: 'usx', + chains: ['arbitrum-one'], + }, + '0x4591dbff62656e7859afe5e45f6f47d3669fbb28': { + name: 'Prisma mkUSD', + symbol: 'mkusd', + chains: ['ethereum'], + }, + '0xc08512927d12348f6620a698105e1baac6ecd911': { + name: 'GYEN', + symbol: 'gyen', + chains: ['ethereum'], + }, + '0x589d35656641d6ab57a545f08cf473ecd9b6d5f7': { + name: 'GYEN', + symbol: 'gyen', + chains: ['arbitrum-one'], + }, + '0xdf3ac4f479375802a821f7b7b46cd7eb5e4262cc': { + name: 'eUSD', + symbol: 'eusd', + chains: ['ethereum'], + }, + '0x2a8e1e676ec238d8a992307b495b45b3feaa5e86': { + name: 'Origin Dollar', + symbol: 'ousd', + chains: ['ethereum'], + }, + '0x1b3c515f58857e141a966b33182f2f3feecc10e9': { + name: 'USK', + symbol: 'usk', + chains: ['ethereum'], + }, + '0xdf574c24545e5ffecb9a659c229253d4111d87e1': { + name: 'HUSD', + symbol: 'husd', + chains: ['ethereum'], + }, + '0xe2f2a5c287993345a840db3b0845fbc70f5935a5': { + name: 'mStable USD', + symbol: 'musd', + chains: ['ethereum'], + }, + '0x6e109e9dd7fa1a58bc3eff667e8e41fc3cc07aef': { + name: 'CNH Tether', + symbol: 'cnht', + chains: ['ethereum'], + }, + '0x6ba75d640bebfe5da1197bb5a2aff3327789b5d3': { + name: 'VNX EURO', + symbol: 'veur', + chains: ['ethereum'], + }, + '0x70e8de73ce538da2beed35d14187f6959a8eca96': { + name: 'XSGD', + symbol: 'xsgd', + chains: ['ethereum'], + }, + '0x97de57ec338ab5d51557da3434828c5dbfada371': { + name: 'eUSD (OLD)', + symbol: 'eusd', + chains: ['ethereum'], + }, + '0x68037790a0229e9ce6eaa8a99ea92964106c4703': { + name: 'Parallel', + symbol: 'par', + chains: ['ethereum'], + }, + '0x1cfa5641c01406ab8ac350ded7d735ec41298372': { + name: 'Convertible JPY Token', + symbol: 'cjpy', + chains: ['ethereum'], + }, + '0xd74f5255d557944cf7dd0e45ff521520002d5748': { + name: 'Sperax USD', + symbol: 'usds', + chains: ['arbitrum-one'], + }, + '0xd71ecff9342a5ced620049e616c5035f1db98620': { + name: 'sEUR', + symbol: 'seur', + chains: ['ethereum'], + }, + '0x38547d918b9645f2d94336b6b61aeb08053e142c': { + name: 'USC', + symbol: 'usc', + chains: ['ethereum'], + }, + '0x45fdb1b92a649fb6a64ef1511d3ba5bf60044838': { + name: 'SpiceUSD', + symbol: 'usds', + chains: ['ethereum'], + }, + '0xebf2096e01455108badcbaf86ce30b6e5a72aa52': { + name: 'XIDR', + symbol: 'xidr', + chains: ['ethereum'], + }, + '0xb0b195aefa3650a6908f15cdac7d92f8a5791b0b': { + name: 'BOB', + symbol: 'bob', + chains: ['ethereum', 'arbitrum-one'], + }, + '0x86b4dbe5d203e634a12364c0e428fa242a3fba98': { + name: 'poundtoken', + symbol: 'gbpt', + chains: ['ethereum'], + }, + '0xd90e69f67203ebe02c917b5128629e77b4cd92dc': { + name: 'One Cash', + symbol: 'onc', + chains: ['ethereum'], + }, + '0x3449fc1cd036255ba1eb19d65ff4ba2b8903a69a': { + name: 'Basis Cash', + symbol: 'bac', + chains: ['ethereum'], + }, + '0xc285b7e09a4584d027e5bc36571785b515898246': { + name: 'Coin98 Dollar', + symbol: 'cusd', + chains: ['ethereum'], + }, + '0x64343594ab9b56e99087bfa6f2335db24c2d1f17': { + name: 'Vesta Stable', + symbol: 'vst', + chains: ['arbitrum-one'], + }, + '0x2370f9d504c7a6e775bf6e14b3f12846b594cd53': { + name: 'JPY Coin v1', + symbol: 'jpyc', + chains: ['ethereum'], + }, + '0x53dfea0a8cc2a2a2e425e1c174bc162999723ea0': { + name: 'Jarvis Synthetic Swiss Franc', + symbol: 'jchf', + chains: ['ethereum'], + }, + '0x0f17bc9a994b87b5225cfb6a2cd4d667adb4f20b': { + name: 'Jarvis Synthetic Euro', + symbol: 'jeur', + chains: ['ethereum'], + }, + '0x3231cb76718cdef2155fc47b5286d82e6eda273f': { + name: 'Monerium EUR emoney', + symbol: 'eure', + chains: ['ethereum'], + }, + '0x65d72aa8da931f047169112fcf34f52dbaae7d18': { + name: 'f(x) rUSD', + symbol: 'rusd', + chains: ['ethereum'], + }, + '0x085780639cc2cacd35e474e71f4d000e2405d8f6': { + name: 'f(x) Protocol fxUSD', + symbol: 'fxusd', + chains: ['ethereum'], + }, + '0xa663b02cf0a4b149d2ad41910cb81e23e1c41c32': { + name: 'Staked FRAX', + symbol: 'sfrax', + chains: ['ethereum'], + }, + '0xe3b3fe7bca19ca77ad877a5bebab186becfad906': { + name: 'Staked FRAX', + symbol: 'sfrax', + chains: ['arbitrum-one'], + }, + '0xcfc5bd99915aaa815401c5a41a927ab7a38d29cf': { + name: 'Threshold USD', + symbol: 'thusd', + chains: ['ethereum'], + }, + '0xa47c8bf37f92abed4a126bda807a7b7498661acd': { + name: 'Wrapped USTC', + symbol: 'ustc', + chains: ['ethereum'], + }, + '0x3509f19581afedeff07c53592bc0ca84e4855475': { + name: 'xDollar Stablecoin', + symbol: 'xusd', + chains: ['arbitrum-one'], + }, + '0x431d5dff03120afa4bdf332c61a6e1766ef37bdb': { + name: 'JPY Coin', + symbol: 'jpyc', + chains: ['ethereum'], + }, + '0xb6667b04cb61aa16b59617f90ffa068722cf21da': { + name: 'Worldwide USD', + symbol: 'wusd', + chains: ['ethereum'], + }, + '0xB4F1737Af37711e9A5890D9510c9bB60e170CB0D': { + name: 'COW Dai Stablecoin', + symbol: 'DAI', + chains: ['sepolia'], + }, + '0xbe72E441BF55620febc26715db68d3494213D8Cb': { + name: 'COW USD Coin', + symbol: 'USDC', + chains: ['sepolia'], + }, + '0x58eb19ef91e8a6327fed391b51ae1887b833cc91': { + name: 'COW Tether USD', + symbol: 'USDT', + chains: ['sepolia'], + }, +} diff --git a/src/features/swap/helpers/fee.ts b/src/features/swap/helpers/fee.ts new file mode 100644 index 0000000000..0a7cc79d32 --- /dev/null +++ b/src/features/swap/helpers/fee.ts @@ -0,0 +1,52 @@ +import type { OnTradeParamsPayload } from '@cowprotocol/events' +import { stableCoinAddresses } from '@/features/swap/helpers/data/stablecoins' + +const FEE_PERCENTAGE_BPS = { + REGULAR: { + TIER_1: 35, + TIER_2: 20, + TIER_3: 10, + }, + STABLE: { + TIER_1: 10, + TIER_2: 7, + TIER_3: 5, + }, +} + +const FEE_TIERS = { + TIER_1: 100_000, // 0 - 100k + TIER_2: 1_000_000, // 100k - 1m +} + +const getLowerCaseStableCoinAddresses = () => { + const lowerCaseStableCoinAddresses = Object.keys(stableCoinAddresses).reduce((result, key) => { + result[key.toLowerCase()] = stableCoinAddresses[key] + return result + }, {} as typeof stableCoinAddresses) + + return lowerCaseStableCoinAddresses +} +/** + * Function to calculate the fee % in bps to apply for a trade. + * The fee % should be applied based on the fiat value of the buy or sell token. + * + * @param orderParams + */ +export const calculateFeePercentageInBps = (orderParams: OnTradeParamsPayload) => { + const { sellToken, buyToken, buyTokenFiatAmount, sellTokenFiatAmount, orderKind } = orderParams + const stableCoins = getLowerCaseStableCoinAddresses() + const isStableCoin = stableCoins[sellToken?.address?.toLowerCase()] && stableCoins[buyToken?.address.toLowerCase()] + + const fiatAmount = Number(orderKind == 'sell' ? sellTokenFiatAmount : buyTokenFiatAmount) || 0 + + if (fiatAmount < FEE_TIERS.TIER_1) { + return isStableCoin ? FEE_PERCENTAGE_BPS.STABLE.TIER_1 : FEE_PERCENTAGE_BPS.REGULAR.TIER_1 + } + + if (fiatAmount < FEE_TIERS.TIER_2) { + return isStableCoin ? FEE_PERCENTAGE_BPS.STABLE.TIER_2 : FEE_PERCENTAGE_BPS.REGULAR.TIER_2 + } + + return isStableCoin ? FEE_PERCENTAGE_BPS.STABLE.TIER_3 : FEE_PERCENTAGE_BPS.REGULAR.TIER_3 +} diff --git a/src/features/swap/helpers/utils.ts b/src/features/swap/helpers/utils.ts index 5252a525f2..1501b66dc3 100644 --- a/src/features/swap/helpers/utils.ts +++ b/src/features/swap/helpers/utils.ts @@ -1,13 +1,15 @@ import type { Order as SwapOrder } from '@safe-global/safe-gateway-typescript-sdk' import { formatUnits } from 'ethers' -import type { AnyAppDataDocVersion, latest } from '@cowprotocol/app-data' +import type { AnyAppDataDocVersion, latest, LatestAppDataDocVersion } from '@cowprotocol/app-data' + +import { TradeType, UiOrderType } from '@/features/swap/types' type Quantity = { amount: string | number | bigint decimals: number } -enum OrderKind { +export enum OrderKind { SELL = 'sell', BUY = 'buy', } @@ -159,6 +161,13 @@ export const getOrderClass = (order: Pick): latest.Ord return orderClass || 'market' } +export const getOrderFeeBps = (order: Pick): number => { + const fullAppData = order.fullAppData as unknown as LatestAppDataDocVersion + const basisPoints = (fullAppData?.metadata?.partnerFee as latest.PartnerFee)?.bps + + return Number(basisPoints) || 0 +} + export const isOrderPartiallyFilled = ( order: Pick, ): boolean => { @@ -173,3 +182,13 @@ export const isOrderPartiallyFilled = ( return BigInt(executedSellAmount) !== 0n && executedSellAmount < sellAmount } +export const UiOrderTypeToOrderType = (orderType: UiOrderType): TradeType => { + switch (orderType) { + case UiOrderType.SWAP: + return TradeType.SWAP + case UiOrderType.LIMIT: + return TradeType.LIMIT + case UiOrderType.TWAP: + return TradeType.ADVANCED + } +} diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 6b59e418a6..03c4d22e4d 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -21,7 +21,7 @@ import useWallet from '@/hooks/wallets/useWallet' import BlockedAddress from '@/components/common/BlockedAddress' import useSwapConsent from './useSwapConsent' import Disclaimer from '@/components/common/Disclaimer' -import LegalDisclaimerContent from '@/components/common/LegalDisclaimerContent' +import LegalDisclaimerContent from '@/features/swap/components/LegalDisclaimer' import { isBlockedAddress } from '@/services/ofac' import { selectSwapParams, setSwapParams, type SwapState } from './store/swapParamsSlice' import { setSwapOrder } from '@/store/swapOrderSlice' @@ -29,7 +29,15 @@ import useChainId from '@/hooks/useChainId' import { type BaseTransaction } from '@safe-global/safe-apps-sdk' import { APPROVAL_SIGNATURE_HASH } from '@/components/tx/ApprovalEditor/utils/approvals' import { id } from 'ethers' -import { LIMIT_ORDER_TITLE, SWAP_TITLE, SWAP_ORDER_TITLE, TWAP_ORDER_TITLE } from '@/features/swap/constants' +import { + LIMIT_ORDER_TITLE, + SWAP_TITLE, + SWAP_ORDER_TITLE, + TWAP_ORDER_TITLE, + SWAP_FEE_RECIPIENT, +} from '@/features/swap/constants' +import { calculateFeePercentageInBps } from '@/features/swap/helpers/fee' +import { UiOrderTypeToOrderType } from '@/features/swap/helpers/utils' const BASE_URL = typeof window !== 'undefined' && window.location.origin ? window.location.origin : '' @@ -69,19 +77,63 @@ const SwapWidget = ({ sell }: Params) => { const dispatch = useAppDispatch() const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) const swapParams = useAppSelector(selectSwapParams) - const { tradeType } = swapParams const { safeAddress, safeLoading } = useSafeInfo() const [blockedAddress, setBlockedAddress] = useState('') const wallet = useWallet() const { isConsentAccepted, onAccept } = useSwapConsent() - // useRefs as they don't trigger re-renders - const tradeTypeRef = useRef(tradeType === 'twap' ? TradeType.ADVANCED : TradeType.SWAP) - const sellTokenRef = useRef( - sell || { + + const [params, setParams] = useState({ + appCode: 'Safe Wallet Swaps', // Name of your app (max 50 characters) + width: '100%', // Width in pixels (or 100% to use all available space) + height: '860px', + chainId, + standaloneMode: false, + disableToastMessages: true, + disablePostedOrderConfirmationModal: true, + hideLogo: true, + hideNetworkSelector: true, + sounds: { + orderError: null, + orderExecuted: null, + postOrder: null, + }, + tradeType: swapParams.tradeType, + sell: sell || { asset: '', amount: '0', }, - ) + buy: { + asset: '', + amount: '0', + }, + images: { + emptyOrders: darkMode + ? BASE_URL + '/images/common/swap-empty-dark.svg' + : BASE_URL + '/images/common/swap-empty-light.svg', + }, + enabledTradeTypes: [TradeType.SWAP, TradeType.LIMIT, TradeType.ADVANCED], + theme: { + baseTheme: darkMode ? 'dark' : 'light', + primary: palette.primary.main, + background: palette.background.main, + paper: palette.background.paper, + text: palette.text.primary, + danger: palette.error.dark, + info: palette.info.main, + success: palette.success.main, + warning: palette.warning.main, + alert: palette.warning.main, + }, + partnerFee: { + bps: 35, + recipient: SWAP_FEE_RECIPIENT, + }, + content: { + feeLabel: 'Widget Fee', + feeTooltipMarkdown: + 'The [tiered widget fee](https://help.safe.global/en/articles/178530-how-does-the-widget-fee-work-for-native-swaps) incurred here and charged by CoW DAO for the operation of the CoW Swap Widget is automatically calculated into this quote. It will contribute to a license fee that supports the Safe Community. Neither the Safe Ecosystem Foundation nor Safe (Wallet) operate the CoW Swap Widget and/or CoW Swap.', + }, + }) useEffect(() => { if (isBlockedAddress(safeAddress)) { @@ -163,47 +215,39 @@ const SwapWidget = ({ sell }: Params) => { { event: CowEvents.ON_CHANGE_TRADE_PARAMS, handler: (newTradeParams: OnTradeParamsPayload) => { - const { orderType: tradeType, recipient, sellToken, sellTokenAmount } = newTradeParams - dispatch(setSwapParams({ tradeType })) + const { orderType: tradeType, recipient, sellToken, buyToken } = newTradeParams + + const newFeeBps = calculateFeePercentageInBps(newTradeParams) + + setParams((params) => ({ + ...params, + tradeType: UiOrderTypeToOrderType(tradeType), + partnerFee: { + recipient: SWAP_FEE_RECIPIENT, + bps: newFeeBps, + }, + sell: { + asset: sellToken?.symbol, + }, + buy: { + asset: buyToken?.symbol, + }, + })) - tradeTypeRef.current = tradeType - sellTokenRef.current = { - asset: sellToken?.symbol || '', - amount: sellTokenAmount?.units || '0', - } if (recipient && isBlockedAddress(recipient)) { setBlockedAddress(recipient) } + + dispatch(setSwapParams({ tradeType })) }, }, ] }, [dispatch]) - const [params, setParams] = useState(null) useEffect(() => { - setParams({ - appCode: 'Safe Wallet Swaps', // Name of your app (max 50 characters) - width: '100%', // Width in pixels (or 100% to use all available space) - height: '860px', + setParams((params) => ({ + ...params, chainId, - standaloneMode: false, - disableToastMessages: true, - disablePostedOrderConfirmationModal: true, - hideLogo: true, - hideNetworkSelector: true, - sounds: { - orderError: null, - orderExecuted: null, - postOrder: null, - }, - tradeType: tradeTypeRef.current, - sell: sellTokenRef.current, - images: { - emptyOrders: darkMode - ? BASE_URL + '/images/common/swap-empty-dark.svg' - : BASE_URL + '/images/common/swap-empty-light.svg', - }, - enabledTradeTypes: [TradeType.SWAP, TradeType.LIMIT, TradeType.ADVANCED], theme: { baseTheme: darkMode ? 'dark' : 'light', primary: palette.primary.main, @@ -216,13 +260,8 @@ const SwapWidget = ({ sell }: Params) => { warning: palette.warning.main, alert: palette.warning.main, }, - content: { - feeLabel: 'No fee for one month', - feeTooltipMarkdown: - 'Any future transaction fee incurred by CoW Protocol here will contribute to a license fee that supports the Safe Community. Neither Safe Ecosystem Foundation nor Core Contributors GmbH operate the CoW Swap Widget and/or CoW Swap.', - }, - }) - }, [sell, palette, darkMode, chainId]) + })) + }, [palette, darkMode, chainId]) const chain = useCurrentChain() @@ -237,10 +276,6 @@ const SwapWidget = ({ sell }: Params) => { useCustomAppCommunicator(iframeRef, appData, chain) - if (!params) { - return null - } - if (blockedAddress) { return } @@ -249,7 +284,7 @@ const SwapWidget = ({ sell }: Params) => { return ( } + content={} onAccept={onAccept} buttonText="Continue" /> diff --git a/src/features/swap/store/swapParamsSlice.ts b/src/features/swap/store/swapParamsSlice.ts index d8a7910170..137a8f6abe 100644 --- a/src/features/swap/store/swapParamsSlice.ts +++ b/src/features/swap/store/swapParamsSlice.ts @@ -1,13 +1,8 @@ import type { RootState } from '@/store' import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' - -// Using TradeType from the cow widget library results in lint errors -enum TradeType { - SWAP = 'swap', - LIMIT = 'limit', - TWAP = 'twap', -} +import { UiOrderTypeToOrderType } from '@/features/swap/helpers/utils' +import { TradeType, type UiOrderType } from '@/features/swap/types' export type SwapState = { tradeType: TradeType @@ -21,9 +16,14 @@ export const swapParamsSlice = createSlice({ name: 'swapParams', initialState, reducers: { - setSwapParams: (_, action: PayloadAction) => { + setSwapParams: ( + _, + action: PayloadAction<{ + tradeType: UiOrderType + }>, + ) => { return { - tradeType: action.payload.tradeType.toLowerCase() as TradeType, + tradeType: UiOrderTypeToOrderType(action.payload.tradeType), } }, }, diff --git a/src/features/swap/types.ts b/src/features/swap/types.ts new file mode 100644 index 0000000000..8df4dfc4e6 --- /dev/null +++ b/src/features/swap/types.ts @@ -0,0 +1,16 @@ +// Jest tests fail if TradeType is imported from widget-lib, so we duplicate them here +export enum TradeType { + SWAP = 'swap', + LIMIT = 'limit', + /** + * Currently it means only TWAP orders. + * But in the future it can be extended to support other order types. + */ + ADVANCED = 'advanced', +} + +export enum UiOrderType { + SWAP = 'SWAP', + LIMIT = 'LIMIT', + TWAP = 'TWAP', +} diff --git a/src/features/swap/useSwapConsent.ts b/src/features/swap/useSwapConsent.ts index 38e85d0fe2..050f1c2041 100644 --- a/src/features/swap/useSwapConsent.ts +++ b/src/features/swap/useSwapConsent.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react' import useLocalStorage from '@/services/local-storage/useLocalStorage' -const SWAPS_CONSENT_STORAGE_KEY = 'swapDisclaimerAccepted' +const SWAPS_CONSENT_STORAGE_KEY = 'swapDisclaimerAcceptedV1' const useSwapConsent = (): { isConsentAccepted: boolean diff --git a/src/pages/swap.tsx b/src/pages/swap.tsx index 1d2f918806..aad3eb0ce0 100644 --- a/src/pages/swap.tsx +++ b/src/pages/swap.tsx @@ -1,8 +1,9 @@ import type { NextPage } from 'next' import Head from 'next/head' import { useRouter } from 'next/router' -import SwapWidget from '@/features/swap' +import dynamic from 'next/dynamic' +const SwapWidgetNoSSR = dynamic(() => import('@/features/swap'), { ssr: false }) const Swap: NextPage = () => { const router = useRouter() const { token, amount } = router.query @@ -22,7 +23,7 @@ const Swap: NextPage = () => {
      - +
      ) From 397c268b69e72b31f6df5cf0186fcbfcca5fa8e8 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Wed, 3 Jul 2024 12:03:17 +0200 Subject: [PATCH 120/154] fix: swapTxInfo for buy orders shows wrong value (#3895) --- .../swap/components/SwapTxInfo/SwapTx.tsx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/features/swap/components/SwapTxInfo/SwapTx.tsx b/src/features/swap/components/SwapTxInfo/SwapTx.tsx index 0b3805784a..e973a29807 100644 --- a/src/features/swap/components/SwapTxInfo/SwapTx.tsx +++ b/src/features/swap/components/SwapTxInfo/SwapTx.tsx @@ -1,13 +1,11 @@ import type { Order } from '@safe-global/safe-gateway-typescript-sdk' import type { ReactElement } from 'react' -import { capitalize } from '@/hooks/useMnemonicName' import { Box, Typography } from '@mui/material' import TokenIcon from '@/components/common/TokenIcon' import { formatVisualAmount } from '@/utils/formatters' export const SwapTx = ({ info }: { info: Order }): ReactElement => { - const { kind, sellToken, sellAmount, buyToken } = info - const orderKindLabel = capitalize(kind) + const { kind, sellToken, sellAmount, buyToken, buyAmount } = info const isSellOrder = kind === 'sell' let from = ( @@ -31,9 +29,23 @@ export const SwapTx = ({ info }: { info: Order }): ReactElement => { ) + if (!isSellOrder) { - // switch them around for buy order - ;[from, to] = [to, from] + from = ( + + + + ) + to = ( + <> + + + {' '} + + {formatVisualAmount(buyAmount, buyToken.decimals)} {buyToken.symbol}{' '} + + + ) } return ( From 11634a7f7feb313696e49e7378f0e3ed3eb7efbb Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:04:08 +0200 Subject: [PATCH 121/154] fix: Hide native swaps card from custom apps list (#3896) --- src/components/safe-apps/SafeAppList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/safe-apps/SafeAppList/index.tsx b/src/components/safe-apps/SafeAppList/index.tsx index 1f57227405..936129dff6 100644 --- a/src/components/safe-apps/SafeAppList/index.tsx +++ b/src/components/safe-apps/SafeAppList/index.tsx @@ -72,7 +72,7 @@ const SafeAppList = ({ ))} - {!isFiltered && } + {!isFiltered && !addCustomApp && } {/* Flat list filtered by search query */} {safeAppsList.map((safeApp) => ( From 054e169dc237ff1612ef76537aa9b48a9f28335d Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Wed, 3 Jul 2024 12:09:55 +0200 Subject: [PATCH 122/154] feat: add a warning if changing the fallback handler (#3889) --- src/components/tx/SignOrExecuteForm/index.tsx | 5 + .../TwapFallbackHandlerWarning/index.tsx | 15 +++ .../swap/helpers/__tests__/utils.test.ts | 94 +++++++++++++++++++ src/features/swap/helpers/utils.ts | 17 +++- 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 src/features/swap/components/TwapFallbackHandlerWarning/index.tsx diff --git a/src/components/tx/SignOrExecuteForm/index.tsx b/src/components/tx/SignOrExecuteForm/index.tsx index 344962ceac..ef3c4377c9 100644 --- a/src/components/tx/SignOrExecuteForm/index.tsx +++ b/src/components/tx/SignOrExecuteForm/index.tsx @@ -29,6 +29,8 @@ import useChainId from '@/hooks/useChainId' import PermissionsCheck from './PermissionsCheck' import { isConfirmationViewOrder } from '@/utils/transaction-guards' import SwapOrderConfirmationView from '@/features/swap/components/SwapOrderConfirmationView' +import { isSettingTwapFallbackHandler } from '@/features/swap/helpers/utils' +import { TwapFallbackHandlerWarning } from '@/features/swap/components/TwapFallbackHandlerWarning' export type SubmitCallback = (txId: string, isExecuted?: boolean) => void @@ -80,6 +82,7 @@ export const SignOrExecuteForm = ({ const { safe } = useSafeInfo() const isCounterfactualSafe = !safe.deployed + const isChangingFallbackHandler = isSettingTwapFallbackHandler(decodedData) // If checkbox is checked and the transaction is executable, execute it, otherwise sign it const canExecute = isCorrectNonce && (props.isExecutable || isNewExecutableTx) @@ -100,6 +103,8 @@ export const SignOrExecuteForm = ({ {props.children} + {isChangingFallbackHandler && } + {isSwapOrder && ( }> diff --git a/src/features/swap/components/TwapFallbackHandlerWarning/index.tsx b/src/features/swap/components/TwapFallbackHandlerWarning/index.tsx new file mode 100644 index 0000000000..470bb515b4 --- /dev/null +++ b/src/features/swap/components/TwapFallbackHandlerWarning/index.tsx @@ -0,0 +1,15 @@ +import { Alert, Box, SvgIcon } from '@mui/material' +import InfoOutlinedIcon from '@/public/images/notifications/info.svg' + +export const TwapFallbackHandlerWarning = () => { + return ( + + }> + Enable TWAPs and submit order. + {` `} + To enable TWAP orders you need to set a custom fallback handler. This software is developed by CoW Swap and Safe + will not be responsible for any possible issues with it. + + + ) +} diff --git a/src/features/swap/helpers/__tests__/utils.test.ts b/src/features/swap/helpers/__tests__/utils.test.ts index 4119775add..17ebb380f5 100644 --- a/src/features/swap/helpers/__tests__/utils.test.ts +++ b/src/features/swap/helpers/__tests__/utils.test.ts @@ -5,7 +5,10 @@ import { getPartiallyFilledSurplus, getSurplusPrice, isOrderPartiallyFilled, + isSettingTwapFallbackHandler, + TWAP_FALLBACK_HANDLER, } from '../utils' +import type { DecodedDataResponse } from '@safe-global/safe-gateway-typescript-sdk' import { type SwapOrder } from '@safe-global/safe-gateway-typescript-sdk' describe('Swap helpers', () => { @@ -314,4 +317,95 @@ describe('Swap helpers', () => { expect(result).toEqual(5) }) }) + + describe('isSettingTwapFallbackHandler', () => { + it('should return true when handler is TWAP_FALLBACK_HANDLER', () => { + const decodedData = { + parameters: [ + { + valueDecoded: [ + { + dataDecoded: { + method: 'setFallbackHandler', + parameters: [{ name: 'handler', value: TWAP_FALLBACK_HANDLER }], + }, + }, + ], + }, + ], + } as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(true) + }) + + it('should return false when handler is not TWAP_FALLBACK_HANDLER', () => { + const decodedData = { + parameters: [ + { + valueDecoded: [ + { + dataDecoded: { + method: 'setFallbackHandler', + parameters: [{ name: 'handler', value: '0xDifferentHandler' }], + }, + }, + ], + }, + ], + } as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(false) + }) + + it('should return false when method is not setFallbackHandler', () => { + const decodedData = { + parameters: [ + { + valueDecoded: [ + { + dataDecoded: { + method: 'differentMethod', + parameters: [{ name: 'handler', value: TWAP_FALLBACK_HANDLER }], + }, + }, + ], + }, + ], + } as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(false) + }) + + it('should return false when decodedData is undefined', () => { + expect(isSettingTwapFallbackHandler(undefined)).toBe(false) + }) + + it('should return false when parameters are missing', () => { + const decodedData = {} as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(false) + }) + + it('should return false when valueDecoded is missing', () => { + const decodedData = { + parameters: [ + { + valueDecoded: null, + }, + ], + } as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(false) + }) + + it('should return false when dataDecoded is missing', () => { + const decodedData = { + parameters: [ + { + valueDecoded: [ + { + dataDecoded: null, + }, + ], + }, + ], + } as unknown as DecodedDataResponse + expect(isSettingTwapFallbackHandler(decodedData)).toBe(false) + }) + }) }) diff --git a/src/features/swap/helpers/utils.ts b/src/features/swap/helpers/utils.ts index 1501b66dc3..83d2c7b791 100644 --- a/src/features/swap/helpers/utils.ts +++ b/src/features/swap/helpers/utils.ts @@ -1,4 +1,4 @@ -import type { Order as SwapOrder } from '@safe-global/safe-gateway-typescript-sdk' +import type { DecodedDataResponse, Order as SwapOrder } from '@safe-global/safe-gateway-typescript-sdk' import { formatUnits } from 'ethers' import type { AnyAppDataDocVersion, latest, LatestAppDataDocVersion } from '@cowprotocol/app-data' @@ -182,6 +182,7 @@ export const isOrderPartiallyFilled = ( return BigInt(executedSellAmount) !== 0n && executedSellAmount < sellAmount } + export const UiOrderTypeToOrderType = (orderType: UiOrderType): TradeType => { switch (orderType) { case UiOrderType.SWAP: @@ -192,3 +193,17 @@ export const UiOrderTypeToOrderType = (orderType: UiOrderType): TradeType => { return TradeType.ADVANCED } } + +export const isSettingTwapFallbackHandler = (decodedData: DecodedDataResponse | undefined) => { + return ( + decodedData?.parameters?.some((item) => + item.valueDecoded?.some( + (decoded) => + decoded.dataDecoded?.method === 'setFallbackHandler' && + decoded.dataDecoded.parameters?.some( + (parameter) => parameter.name === 'handler' && parameter.value === TWAP_FALLBACK_HANDLER, + ), + ), + ) || false + ) +} From 3ba8dac1e8e5343fff49c6309543a10e7b7654f5 Mon Sep 17 00:00:00 2001 From: James Mealy Date: Wed, 3 Jul 2024 13:42:46 +0200 Subject: [PATCH 123/154] Feat: Do not show swaps feature to users from blocked counties [SW-48] (#3892) * Feat: Don't show swaps entrypoints if swaps page returns a 403 * feat: add custom 403 error page * redirect to 403 page from the swaps page if country is blocked * add terms link * Update comment in GeoblockingProvider --- src/components/balances/AssetsTable/index.tsx | 5 ++-- .../common/GeoblockingProvider/index.tsx | 29 +++++++++++++++++++ src/components/dashboard/Assets/index.tsx | 5 ++-- .../dashboard/Overview/Overview.tsx | 5 ++-- .../safe-apps/NativeSwapsCard/index.tsx | 5 ++-- .../sidebar/SidebarNavigation/index.tsx | 8 +++-- src/components/tx-flow/common/TxButton.tsx | 6 ++-- src/config/routes.ts | 1 + src/features/counterfactual/FirstTxFlow.tsx | 5 ++-- .../swap/components/SwapWidget/index.tsx | 5 ++-- .../swap/hooks/useIsSwapFeatureEnabled.ts | 11 +++++++ src/features/swap/index.tsx | 6 ++-- src/pages/403.tsx | 24 +++++++++++++++ src/pages/_app.tsx | 5 +++- src/pages/swap.tsx | 8 +++++ 15 files changed, 100 insertions(+), 28 deletions(-) create mode 100644 src/components/common/GeoblockingProvider/index.tsx create mode 100644 src/features/swap/hooks/useIsSwapFeatureEnabled.ts create mode 100644 src/pages/403.tsx diff --git a/src/components/balances/AssetsTable/index.tsx b/src/components/balances/AssetsTable/index.tsx index 3b52de7ac1..dd9ddc61d9 100644 --- a/src/components/balances/AssetsTable/index.tsx +++ b/src/components/balances/AssetsTable/index.tsx @@ -1,6 +1,4 @@ import CheckBalance from '@/features/counterfactual/CheckBalance' -import { useHasFeature } from '@/hooks/useChains' -import { FEATURES } from '@/utils/chains' import { type ReactElement } from 'react' import { Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material' import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' @@ -23,6 +21,7 @@ import SwapButton from '@/features/swap/components/SwapButton' import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' import { SWAP_LABELS } from '@/services/analytics/events/swaps' import SendButton from './SendButton' +import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' const skeletonCells: EnhancedTableProps['rows'][0]['cells'] = { asset: { @@ -99,7 +98,7 @@ const AssetsTable = ({ }): ReactElement => { const { balances, loading } = useBalances() const isCounterfactualSafe = useIsCounterfactualSafe() - const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) && !isCounterfactualSafe + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() && !isCounterfactualSafe const { isAssetSelected, toggleAsset, hidingAsset, hideAsset, cancel, deselectAll, saveChanges } = useHideAssets(() => setShowHiddenAssets(false), diff --git a/src/components/common/GeoblockingProvider/index.tsx b/src/components/common/GeoblockingProvider/index.tsx new file mode 100644 index 0000000000..b6451de004 --- /dev/null +++ b/src/components/common/GeoblockingProvider/index.tsx @@ -0,0 +1,29 @@ +import { AppRoutes } from '@/config/routes' +import { createContext, type ReactElement, type ReactNode, useEffect, useState } from 'react' + +export const GeoblockingContext = createContext(null) + +/** + * Endpoint returns a 403 if the requesting user is from one of the OFAC sanctioned countries + */ +const GeoblockingProvider = ({ children }: { children: ReactNode }): ReactElement => { + const [isBlockedCountry, setIsBlockedCountry] = useState(null) + + useEffect(() => { + const fetchSwaps = async () => { + await fetch(AppRoutes.swap, { method: 'HEAD' }).then((res) => { + if (res.status === 403) { + setIsBlockedCountry(true) + } else { + setIsBlockedCountry(false) + } + }) + } + + fetchSwaps() + }, []) + + return {children} +} + +export default GeoblockingProvider diff --git a/src/components/dashboard/Assets/index.tsx b/src/components/dashboard/Assets/index.tsx index 45f867893a..8df89c1644 100644 --- a/src/components/dashboard/Assets/index.tsx +++ b/src/components/dashboard/Assets/index.tsx @@ -9,12 +9,11 @@ import { AppRoutes } from '@/config/routes' import { WidgetContainer, WidgetBody, ViewAllLink } from '../styled' import css from '../PendingTxs/styles.module.css' import { useRouter } from 'next/router' -import { useHasFeature } from '@/hooks/useChains' -import { FEATURES } from '@/utils/chains' import { SWAP_LABELS } from '@/services/analytics/events/swaps' import { useVisibleAssets } from '@/components/balances/AssetsTable/useHideAssets' import BuyCryptoButton from '@/components/common/BuyCryptoButton' import SendButton from '@/components/balances/AssetsTable/SendButton' +import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' const MAX_ASSETS = 5 @@ -70,7 +69,7 @@ const AssetRow = ({ item, showSwap }: { item: SafeBalanceResponse['items'][numbe ) const AssetList = ({ items }: { items: SafeBalanceResponse['items'] }) => { - const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() return ( diff --git a/src/components/dashboard/Overview/Overview.tsx b/src/components/dashboard/Overview/Overview.tsx index 87b1740fac..f0b069ea4e 100644 --- a/src/components/dashboard/Overview/Overview.tsx +++ b/src/components/dashboard/Overview/Overview.tsx @@ -4,7 +4,6 @@ import Track from '@/components/common/Track' import QrCodeButton from '@/components/sidebar/QrCodeButton' import { TxModalContext } from '@/components/tx-flow' import { NewTxFlow } from '@/components/tx-flow/flows' -import { useHasFeature } from '@/hooks/useChains' import SwapIcon from '@/public/images/common/swap.svg' import { OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' import Link from 'next/link' @@ -14,13 +13,13 @@ import ArrowIconNW from '@/public/images/common/arrow-top-right.svg' import ArrowIconSE from '@/public/images/common/arrow-se.svg' import FiatValue from '@/components/common/FiatValue' import { AppRoutes } from '@/config/routes' -import { FEATURES } from '@/utils/chains' import { Button, Grid, Skeleton, Typography, useMediaQuery } from '@mui/material' import { useRouter } from 'next/router' import { type ReactElement, useContext } from 'react' import { WidgetBody, WidgetContainer } from '../styled' import { useTheme } from '@mui/material/styles' import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' +import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' const SkeletonOverview = ( <> @@ -47,7 +46,7 @@ const Overview = (): ReactElement => { const router = useRouter() const theme = useTheme() const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')) - const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() const isInitialState = !safeLoaded && !safeLoading const isLoading = safeLoading || balancesLoading || isInitialState diff --git a/src/components/safe-apps/NativeSwapsCard/index.tsx b/src/components/safe-apps/NativeSwapsCard/index.tsx index 25f029ae0d..b95e687b72 100644 --- a/src/components/safe-apps/NativeSwapsCard/index.tsx +++ b/src/components/safe-apps/NativeSwapsCard/index.tsx @@ -10,14 +10,13 @@ import Link from 'next/link' import { AppRoutes } from '@/config/routes' import { useRouter } from 'next/router' import useLocalStorage from '@/services/local-storage/useLocalStorage' -import { useHasFeature } from '@/hooks/useChains' -import { FEATURES } from '@/utils/chains' +import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' const SWAPS_APP_CARD_STORAGE_KEY = 'showSwapsAppCard' const NativeSwapsCard = () => { const router = useRouter() - const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() const [isSwapsCardVisible = true, setIsSwapsCardVisible] = useLocalStorage(SWAPS_APP_CARD_STORAGE_KEY) if (!isSwapFeatureEnabled || !isSwapsCardVisible) return null diff --git a/src/components/sidebar/SidebarNavigation/index.tsx b/src/components/sidebar/SidebarNavigation/index.tsx index 7c6fd9eba1..f8dd01768a 100644 --- a/src/components/sidebar/SidebarNavigation/index.tsx +++ b/src/components/sidebar/SidebarNavigation/index.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, type ReactElement } from 'react' +import React, { useContext, useMemo, type ReactElement } from 'react' import { useRouter } from 'next/router' import ListItem from '@mui/material/ListItem' import { ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' @@ -19,6 +19,7 @@ import { isRouteEnabled } from '@/utils/chains' import { trackEvent } from '@/services/analytics' import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' +import { GeoblockingContext } from '@/components/common/GeoblockingProvider' const getSubdirectory = (pathname: string): string => { return pathname.split('/')[1] @@ -31,16 +32,17 @@ const Navigation = (): ReactElement => { const currentSubdirectory = getSubdirectory(router.pathname) const queueSize = useQueuedTxsLength() const isCounterFactualSafe = useIsCounterfactualSafe() + const isBlockedCountry = useContext(GeoblockingContext) const enabledNavItems = useMemo(() => { return navItems.filter((item) => { const enabled = isRouteEnabled(item.href, chain) - if (item.href === AppRoutes.swap && isCounterFactualSafe) { + if (item.href === AppRoutes.swap && (isCounterFactualSafe || isBlockedCountry)) { return false } return enabled }) - }, [chain, isCounterFactualSafe]) + }, [chain, isBlockedCountry, isCounterFactualSafe]) const getBadge = (item: NavItem) => { // Indicate whether the current Safe needs an upgrade diff --git a/src/components/tx-flow/common/TxButton.tsx b/src/components/tx-flow/common/TxButton.tsx index 5071070a72..2288f08571 100644 --- a/src/components/tx-flow/common/TxButton.tsx +++ b/src/components/tx-flow/common/TxButton.tsx @@ -12,6 +12,7 @@ import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' import SwapIcon from '@/public/images/common/swap.svg' import AssetsIcon from '@/public/images/sidebar/assets.svg' +import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' const buttonSx = { height: '58px', @@ -86,9 +87,8 @@ export const TxBuilderButton = () => { export const MakeASwapButton = () => { const router = useRouter() const { setTxFlow } = useContext(TxModalContext) - const isEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) - - if (!isEnabled) return null + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() + if (!isSwapFeatureEnabled) return null const isSwapPage = router.pathname === AppRoutes.swap const onClick = isSwapPage ? () => setTxFlow(undefined) : undefined diff --git a/src/config/routes.ts b/src/config/routes.ts index d841873f49..11c78d471d 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -1,5 +1,6 @@ export const AppRoutes = { '404': '/404', + '403': '/403', wc: '/wc', terms: '/terms', swap: '/swap', diff --git a/src/features/counterfactual/FirstTxFlow.tsx b/src/features/counterfactual/FirstTxFlow.tsx index c20fa8498d..8e9d386074 100644 --- a/src/features/counterfactual/FirstTxFlow.tsx +++ b/src/features/counterfactual/FirstTxFlow.tsx @@ -18,9 +18,8 @@ import RecoveryPlus from '@/public/images/common/recovery-plus.svg' import SwapIcon from '@/public/images/common/swap.svg' import SafeLogo from '@/public/images/logo-no-text.svg' import HandymanOutlinedIcon from '@mui/icons-material/HandymanOutlined' -import { useHasFeature } from '@/hooks/useChains' -import { FEATURES } from '@/utils/chains' import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' +import useIsSwapFeatureEnabled from '../swap/hooks/useIsSwapFeatureEnabled' const FirstTxFlow = ({ open, onClose }: { open: boolean; onClose: () => void }) => { const txBuilder = useTxBuilderApp() @@ -29,7 +28,7 @@ const FirstTxFlow = ({ open, onClose }: { open: boolean; onClose: () => void }) const supportsRecovery = useIsRecoverySupported() const [recovery] = useRecovery() const isCounterfactualSafe = useIsCounterfactualSafe() - const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) && !isCounterfactualSafe + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() && !isCounterfactualSafe const handleClick = (onClick: () => void) => { onClose() diff --git a/src/features/swap/components/SwapWidget/index.tsx b/src/features/swap/components/SwapWidget/index.tsx index 7e7d8eb206..b0dc22ac75 100644 --- a/src/features/swap/components/SwapWidget/index.tsx +++ b/src/features/swap/components/SwapWidget/index.tsx @@ -11,14 +11,13 @@ import { useRouter } from 'next/router' import useLocalStorage from '@/services/local-storage/useLocalStorage' import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' import Track from '@/components/common/Track' -import { useHasFeature } from '@/hooks/useChains' -import { FEATURES } from '@/utils/chains' +import useIsSwapFeatureEnabled from '../../hooks/useIsSwapFeatureEnabled' const SWAPS_PROMO_WIDGET_IS_HIDDEN = 'swapsPromoWidgetIsHidden' function SwapWidget(): ReactElement | null { const [isHidden = false, setIsHidden] = useLocalStorage(SWAPS_PROMO_WIDGET_IS_HIDDEN) - const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() const onClick = useCallback(() => { setIsHidden(true) diff --git a/src/features/swap/hooks/useIsSwapFeatureEnabled.ts b/src/features/swap/hooks/useIsSwapFeatureEnabled.ts new file mode 100644 index 0000000000..d43d02a3fd --- /dev/null +++ b/src/features/swap/hooks/useIsSwapFeatureEnabled.ts @@ -0,0 +1,11 @@ +import { GeoblockingContext } from '@/components/common/GeoblockingProvider' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' +import { useContext } from 'react' + +const useIsSwapFeatureEnabled = () => { + const isBlockedCountry = useContext(GeoblockingContext) + return useHasFeature(FEATURES.NATIVE_SWAPS) && !isBlockedCountry +} + +export default useIsSwapFeatureEnabled diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 03c4d22e4d..1ec8ed89a6 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -1,4 +1,3 @@ -import { FEATURES } from '@/utils/chains' import { CowSwapWidget } from '@cowprotocol/widget-react' import { type CowSwapWidgetParams, TradeType } from '@cowprotocol/widget-lib' import type { OnTradeParamsPayload } from '@cowprotocol/events' @@ -10,7 +9,7 @@ import { type SafeAppData, SafeAppFeatures, } from '@safe-global/safe-gateway-typescript-sdk/dist/types/safe-apps' -import { useCurrentChain, useHasFeature } from '@/hooks/useChains' +import { useCurrentChain } from '@/hooks/useChains' import { useDarkMode } from '@/hooks/useDarkMode' import { useCustomAppCommunicator } from '@/hooks/safe-apps/useCustomAppCommunicator' import { useAppDispatch, useAppSelector } from '@/store' @@ -29,6 +28,7 @@ import useChainId from '@/hooks/useChainId' import { type BaseTransaction } from '@safe-global/safe-apps-sdk' import { APPROVAL_SIGNATURE_HASH } from '@/components/tx/ApprovalEditor/utils/approvals' import { id } from 'ethers' +import useIsSwapFeatureEnabled from './hooks/useIsSwapFeatureEnabled' import { LIMIT_ORDER_TITLE, SWAP_TITLE, @@ -75,7 +75,7 @@ const SwapWidget = ({ sell }: Params) => { const darkMode = useDarkMode() const chainId = useChainId() const dispatch = useAppDispatch() - const isSwapFeatureEnabled = useHasFeature(FEATURES.NATIVE_SWAPS) + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() const swapParams = useAppSelector(selectSwapParams) const { safeAddress, safeLoading } = useSafeInfo() const [blockedAddress, setBlockedAddress] = useState('') diff --git a/src/pages/403.tsx b/src/pages/403.tsx new file mode 100644 index 0000000000..fd6307a76d --- /dev/null +++ b/src/pages/403.tsx @@ -0,0 +1,24 @@ +import { AppRoutes } from '@/config/routes' +import type { NextPage } from 'next' +import Link from 'next/link' +import MUILink from '@mui/material/Link' + +const Custom403: NextPage = () => { + return ( +
      +

      403 - Access Restricted

      +

      + We regret to inform you that access to this service is currently unavailable in your region. For further + information, you may refer to our{' '} + + + terms + + + . We apologize for any inconvenience this may cause. Thank you for your understanding. +

      +
      + ) +} + +export default Custom403 diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 605929f139..1f9dee17c9 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -43,6 +43,7 @@ import Recovery from '@/features/recovery/components/Recovery' import WalletProvider from '@/components/common/WalletProvider' import CounterfactualHooks from '@/features/counterfactual/CounterfactualHooks' import PkModulePopup from '@/services/private-key-module/PkModulePopup' +import GeoblockingProvider from '@/components/common/GeoblockingProvider' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -84,7 +85,9 @@ export const AppProviders = ({ children }: { children: ReactNode | ReactNode[] } - {children} + + {children} + diff --git a/src/pages/swap.tsx b/src/pages/swap.tsx index aad3eb0ce0..297affc64e 100644 --- a/src/pages/swap.tsx +++ b/src/pages/swap.tsx @@ -1,13 +1,21 @@ import type { NextPage } from 'next' import Head from 'next/head' import { useRouter } from 'next/router' +import { GeoblockingContext } from '@/components/common/GeoblockingProvider' +import { useContext } from 'react' +import { AppRoutes } from '@/config/routes' import dynamic from 'next/dynamic' const SwapWidgetNoSSR = dynamic(() => import('@/features/swap'), { ssr: false }) const Swap: NextPage = () => { const router = useRouter() + const isBlockedCountry = useContext(GeoblockingContext) const { token, amount } = router.query + if (isBlockedCountry) { + router.replace(AppRoutes['403']) + } + let sell = undefined if (token && amount) { sell = { From a71d605df84f0357a1465cb748523770330d66df Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Wed, 3 Jul 2024 14:58:16 +0200 Subject: [PATCH 124/154] 1.39.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8bd9a38026..195a9f3b99 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.38.1", + "version": "1.39.0", "type": "module", "scripts": { "dev": "next dev", From 831e68a02b484458f60e7faf4f841183b1723744 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:58:43 +0200 Subject: [PATCH 125/154] Tests: Add sidebar tests (#3898) * Add tests * Update tests --- cypress/e2e/pages/main.page.js | 4 + cypress/e2e/pages/sidebar.pages.js | 37 +++++- cypress/e2e/pages/swaps.pages.js | 13 ++- cypress/e2e/regression/sidebar_2.cy.js | 3 +- cypress/e2e/regression/sidebar_3.cy.js | 117 +++++++++++++++++++ cypress/e2e/regression/swaps_history_2.cy.js | 6 + cypress/support/localstorage_data.js | 15 +++ 7 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 cypress/e2e/regression/sidebar_3.cy.js diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index 5e05873b53..214be21e3e 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -304,3 +304,7 @@ export function verifyTextVisibility(stringsArray) { export function getIframeBody(iframe) { return cy.get(iframe).its('0.contentDocument.body').should('not.be.empty').then(cy.wrap) } + +export const checkButtonByTextExists = (buttonText) => { + cy.get('button').contains(buttonText).should('exist') +} diff --git a/cypress/e2e/pages/sidebar.pages.js b/cypress/e2e/pages/sidebar.pages.js index 882f872abe..56c32db19c 100644 --- a/cypress/e2e/pages/sidebar.pages.js +++ b/cypress/e2e/pages/sidebar.pages.js @@ -33,12 +33,25 @@ const queuedTxInfo = '[data-testid="queued-tx-info"]' const showMoreBtn = '[data-testid="show-more-btn" ]' const importBtn = '[data-testid="import-btn"]' export const pendingActivationIcon = '[data-testid="pending-activation-icon"]' +const safeItemMenuIcon = '[data-testid="MoreVertIcon"]' +export const importBtnStr = 'Import' +export const exportBtnStr = 'Export' export const addedSafesEth = ['0x8675...a19b'] export const addedSafesSepolia = ['0x6d0b...6dC1', '0x5912...fFdb', '0x0637...708e', '0xD157...DE9a'] export const sideBarListItems = ['Home', 'Assets', 'Transactions', 'Address book', 'Apps', 'Settings'] +export const sideBarSafes = { + safe1: '0xBb26E3717172d5000F87DeFd391994f789D80aEB', + safe2: '0x905934aA8758c06B2422F0C90D97d2fbb6677811', + safe1short: '0xBb26...0aEB', + safe2short: '0x9059...7811', + safe3short: '0x86Cb...2C27', +} export const testSafeHeaderDetails = ['2/2', safes.SEP_STATIC_SAFE_9_SHORT] const receiveAssetsStr = 'Receive assets' +const emptyWatchListStr = 'Watch any Safe Account to keep an eye on its activity' +const emptySafeListStr = "You don't have any Safe Accounts yet" +const myAccountsStr = 'My accounts' export function getImportBtn() { return cy.get(importBtn).scrollIntoView().should('be.visible') @@ -141,6 +154,11 @@ export function verifyAddedSafesExist(safes) { main.verifyValuesExist(sideSafeListItem, safes) } +export function verifyAddedSafesExistByIndex(index, safe) { + cy.get(sideSafeListItem).eq(index).should('contain', safe) + cy.get(sideSafeListItem).eq(index).should('contain', 'sep:') +} + export function verifySafesByNetwork(netwrok, safes) { cy.get(sidebarSafeContainer).within(() => { cy.get(chainLogo) @@ -186,7 +204,7 @@ export function renameSafeItem(oldName, newName) { clickOnRenameBtn() typeSafeName(newName) } -// + export function removeSafeItem(name) { clickOnSafeItemOptionsBtn(name) clickOnRemoveBtn() @@ -238,3 +256,20 @@ export function checkCurrencyInHeader(currency) { export function checkSafeAddressInHeader(address) { main.verifyValuesExist(sidebarSafeHeader, address) } + +export function verifyWatchlistIsEmpty() { + main.verifyValuesExist(sidebarSafeContainer, [emptyWatchListStr]) +} + +export function verifySafeListIsEmpty() { + main.verifyValuesExist(sidebarSafeContainer, [emptySafeListStr]) +} + +export function verifySafeGiveNameOptionExists(index) { + cy.get(safeItemMenuIcon).eq(index).click() + clickOnRenameBtn() +} + +export function checkMyAccountCounter(value) { + cy.contains(myAccountsStr).should('contain', value) +} diff --git a/cypress/e2e/pages/swaps.pages.js b/cypress/e2e/pages/swaps.pages.js index 1567ac6f55..d0074961fe 100644 --- a/cypress/e2e/pages/swaps.pages.js +++ b/cypress/e2e/pages/swaps.pages.js @@ -2,7 +2,6 @@ import * as constants from '../../support/constants.js' import * as main from '../pages/main.page.js' import * as create_tx from '../pages/create_tx.pages.js' -// Incoming from CowSwap export const inputCurrencyInput = '[id="input-currency-input"]' export const outputurrencyInput = '[id="output-currency-input"]' const tokenList = '[id="tokens-list"]' @@ -14,9 +13,12 @@ export const dashboardSwapBtn = '[data-testid="overview-swap-btn"]' export const customRecipient = 'div[id="recipient"]' const recipientToggle = 'button[id="toggle-recipient-mode-button"]' const orderTypeMenuItem = 'div[class*="MenuItem"]' +const explorerBtn = '[data-testid="explorer-btn"]' const confirmSwapStr = 'Confirm Swap' const swapBtnStr = /Confirm Swap|Swap|Confirm (Approve COW and Swap)|Confirm/ const orderSubmittedStr = 'Order Submitted' +const orderIdStr = 'Order ID' +const cowOrdersUrl = 'https://explorer.cow.fi/orders' export const blockedAddress = '0x8576acc5c05d6ce88f4e49bf65bdf0c62f91353c' export const blockedAddressStr = 'Blocked address' @@ -230,3 +232,12 @@ export function checkTokenOrder(regexPattern, option) { }) }) } + +export function verifyOrderIDUrl() { + cy.get(create_tx.txRowTitle) + .contains(orderIdStr) + .parent() + .within(() => { + cy.get(explorerBtn).should('have.attr', 'href').and('include', cowOrdersUrl) + }) +} diff --git a/cypress/e2e/regression/sidebar_2.cy.js b/cypress/e2e/regression/sidebar_2.cy.js index 1daae9f126..a24156d4ef 100644 --- a/cypress/e2e/regression/sidebar_2.cy.js +++ b/cypress/e2e/regression/sidebar_2.cy.js @@ -37,8 +37,7 @@ describe('Sidebar added sidebar tests', () => { sideBar.verifySafeNameExists(newSafeName) }) - // TODO: Update to remove from watch list - it.skip('Verify a safe can be removed', () => { + it('Verify a safe can be removed', () => { sideBar.openSidebar() sideBar.removeSafeItem(addedSafe900) sideBar.verifySafeRemoved([addedSafe900]) diff --git a/cypress/e2e/regression/sidebar_3.cy.js b/cypress/e2e/regression/sidebar_3.cy.js new file mode 100644 index 0000000000..c1fca4d40f --- /dev/null +++ b/cypress/e2e/regression/sidebar_3.cy.js @@ -0,0 +1,117 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as sideBar from '../pages/sidebar.pages.js' +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 * as create_wallet from '../pages/create_wallet.pages.js' + +let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY +const signer1 = walletCredentials.OWNER_1_PRIVATE_KEY + +describe('Sidebar tests 3', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.clearLocalStorage() + }) + + it('Verify that users with no accounts see the empty state in "My accounts" block', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, { 1: [], 100: [], 137: [], 11155111: [] }) + wallet.connectSigner(signer) + sideBar.openSidebar() + sideBar.verifySafeListIsEmpty() + }) + + it('Verify empty state of the Watchlist', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, {}) + wallet.connectSigner(signer) + sideBar.openSidebar() + sideBar.verifyWatchlistIsEmpty() + }) + + it('Verify connected user is redirected from welcome page to accounts page', () => { + cy.visit(constants.welcomeUrl + '?chain=sep') + main.acceptCookies() + wallet.connectSigner(signer) + create_wallet.clickOnContinueWithWalletBtn() + + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/welcome/accounts') + }) + }) + + it('Verify that the user see safes that he owns in the list', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafes.safe1, sideBar.sideBarSafes.safe2], + }) + + wallet.connectSigner(signer) + sideBar.openSidebar() + sideBar.verifyAddedSafesExistByIndex(0, sideBar.sideBarSafes.safe1short) + sideBar.verifyAddedSafesExistByIndex(1, sideBar.sideBarSafes.safe2short) + }) + + it('Verify there is an option to name an unnamed safe', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafes.safe1, sideBar.sideBarSafes.safe2], + }) + wallet.connectSigner(signer) + sideBar.openSidebar() + sideBar.verifySafeGiveNameOptionExists(0) + }) + + it('Verify Import/export buttons are present', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.addedSafes) + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafes.safe1, sideBar.sideBarSafes.safe2], + }) + wallet.connectSigner(signer) + sideBar.openSidebar() + main.checkButtonByTextExists(sideBar.importBtnStr) + main.checkButtonByTextExists(sideBar.exportBtnStr) + }) + + it('Verify the "My accounts" counter at the top is counting all safes the user owns', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafes.safe1, sideBar.sideBarSafes.safe2], + }) + wallet.connectSigner(signer) + sideBar.openSidebar() + sideBar.checkMyAccountCounter(2) + }) + + it('Verify that safes the user do not owns show in the watchlist after adding them', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addedSafes, ls.addedSafes.set4) + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + wallet.connectSigner(signer1) + sideBar.openSidebar() + sideBar.verifyAddedSafesExist([sideBar.sideBarSafes.safe3short]) + }) + + it('Verify that safes that the user owns do show in the watchlist after adding them', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addedSafes, ls.addedSafes.set4) + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + main.acceptCookies() + wallet.connectSigner(signer1) + sideBar.openSidebar() + sideBar.verifyAddedSafesExist([sideBar.sideBarSafes.safe3short]) + }) +}) diff --git a/cypress/e2e/regression/swaps_history_2.cy.js b/cypress/e2e/regression/swaps_history_2.cy.js index 452c94a57d..1f97be155a 100644 --- a/cypress/e2e/regression/swaps_history_2.cy.js +++ b/cypress/e2e/regression/swaps_history_2.cy.js @@ -146,4 +146,10 @@ describe('Swaps history tests 2', () => { const eq2 = swaps.createRegex(swapsHistory.oneGNOFull, 'COW') swaps.checkTokenOrder(eq2, swapsHistory.limitPrice) }) + + it('Verify OrderID url on cowswap explorer', { defaultCommandTimeout: 30000 }, () => { + cy.visit(constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.sell1Action) + main.acceptCookies() + swaps.verifyOrderIDUrl() + }) }) diff --git a/cypress/support/localstorage_data.js b/cypress/support/localstorage_data.js index a2f56569a3..65add75614 100644 --- a/cypress/support/localstorage_data.js +++ b/cypress/support/localstorage_data.js @@ -626,6 +626,21 @@ export const addedSafes = { }, }, }, + set4: { + 11155111: { + '0x86Cb401afF6A25A335c440C25954A70b3c232C27': { + owners: [ + { + value: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', + }, + { + value: '0x12d0Ad7d21bdbe7E05AB0aDd973C58fB48b52Ae5', + }, + ], + threshold: 1, + }, + }, + }, } export const pinnedApps = { From 7a1018a21fbe7c9d9d2466aecc6d5578820d322e Mon Sep 17 00:00:00 2001 From: James Mealy Date: Wed, 3 Jul 2024 20:59:47 +0200 Subject: [PATCH 126/154] chore: update terms and conditions (#3902) --- src/pages/terms.tsx | 355 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 291 insertions(+), 64 deletions(-) diff --git a/src/pages/terms.tsx b/src/pages/terms.tsx index d226cde9b9..050fe45dc5 100644 --- a/src/pages/terms.tsx +++ b/src/pages/terms.tsx @@ -12,7 +12,7 @@ const SafeTerms = () => ( Terms and Conditions -

      Last updated: January 2024.

      +

      Last updated: April 2024.

      1. What is the scope of the Terms?

        @@ -161,8 +161,12 @@ const SafeTerms = () => (
      1. the responsibility to monitor authorized Transactions or to check the correctness or completeness of - Transactions before you are authorizing them. + Transactions before you are authorizing them;
      2. +
      3. notifications about events occurring in or connection with your Safe Account;
      4. +
      5. recovery of your Safe Account;
      6. +
      7. flagging malicious transactions;
      8. +
      9. issuance of the Safe Token and any related functionalities or reward programs.

      5. What do you need to know about Third-Party Services?

      @@ -268,25 +272,118 @@ const SafeTerms = () => (
    -

    8. Can we terminate or limit your right to use our Services?

    -
      +

      8. Are we responsible for recovering your Safe Account?

      +
        +
      1. We shall not be responsible for recovering your Safe Account.
      2. +
      3. You are solely responsible for securing a back-up of your Safe Account access as you see fit.
      4. +
      5. + Any recovery feature we provide access to within the Safe App is a mechanism controlled by your Safe Account on + the Blockchain, both of which we don't have any influence over once you have set it up. We will never act + as a recoverer ourselves and don't offer recovery services. The Self Custodial Recovery feature allows you + to determine your own recovery setup and nominate anyone including yourself as your recoverer. The recoverer can + start the recovery process at any time. Please note that we are not responsible for notifying you of this + process (see Section 7 above). Furthermore we reserve the right to cease the access to the Self Custodial + Recovery feature via our Safe App taking the user's reasonable interests into account and providing due + notification. +
      6. +
      7. The recovery feature is provided free of charge and liability is limited pursuant to Section 17.4 below.
      8. +
      + +

      9. Are we responsible for notifying you about events occuring in your Safe Account?

      +
        +
      1. + We shall not be responsible for notifying you of any interactions or events occurring in your Safe Account, be + it on the Blockchain, third-party interfaces, within any other infrastructure, or our Services. +
      2. +
      3. You are responsible for monitoring Safe Account as you see fit.
      4. +
      5. + Any notification service we provide or offer for subscription within the Safe App via e-mail or push + notifications or any other means of communication is provided free of charge and liability is limited pursuant + to Section 17.4 below. Furthermore we reserve the right to change the notification feature from time to time or + cease to provide them without notice. +
      6. +
      + +

      10. Are we responsible for flagging malicious transactions?

      +
        +
      1. We shall not be responsible for flagging malicious transactions in our Safe App.
      2. +
      3. + You are solely responsible for checking any transaction, address, Token or other item you interact with via your + Smart Account in our Safe App.{' '} +
      4. - We may terminate the Agreement and refuse access to the Safe Apps at any time giving 30 days’ prior - notice. The right of the parties to terminate the Agreement for cause remains unaffected. In case of our - termination of the Agreement, you may no longer access your Safe Account via our Services. However, you may + Any security flagging or warning service we provide or offer for subscription within the Safe App is provided + free of charge and liability is limited pursuant to Section 17.4 below. Furthermore we reserve the right to + change the feature from time to time or cease to provide them without notice. +
      5. +
      + +

      + 11. Are we responsible for the issuance of the Safe Token and any related functionalities or reward programs? +

      +
        +
      1. + The Safe Token is issued by the Safe Ecosystem Foundation. We are not the issuer or in any way responsible for + the Safe Token. Furthermore, we do not provide any functionalities to the Safe Token or Safe Token reward + programs. +
      2. +
      3. + You are solely responsible for managing your Safe Tokens just like any other Token in your Safe Account and + solely responsible for your eligibility for any reward programs. +
      4. +
      5. + Any interface we provide that allows you to claim or delegate your Safe Tokens or to participate in any third + party program related to Safe Tokens is provided free of charge and we exclude any and all liability for the + correctness, completeness, speed or timeliness of these services. Furthermore we reserve the right to change the + feature from time to time or cease to provide them without notice. +
      6. +
      + +

      12. Are we responsible for third-party content and services?

      +
        +
      1. + You may view, have access to, and use third-party content and services, for example widget integrations within + the Safe App (“Third-Party Features”). You view, access or use Third-Party Features at your own election. Your + reliance on Third-Party Features is subject to separate terms and conditions set forth by the applicable third + party content and/or service provider (“Third-Party Terms”). Third-Party Terms may, amongst other things, +
          +
        1. involve separate fees and charges,
        2. +
        3. include disclaimers or risk warnings,
        4. +
        5. apply a different terms and privacy policy.
        6. +
        +
      2. +
      3. + Third Party Features are provided for your convenience only. We do not verify, curate, or control Third Party + Features.{' '} +
      4. +
      5. + If we offer access to Third-Party Features in the Safe App free of charge by us (Third-Parties may charge + separate fees), the liability for providing access to such Third-Party Feature is limited pursuant to Section + 17.1 below. Furthermore we reserve the right to cease to provide access to those Third-Party Features through + the Safe App without notice. +
      6. +
      + +

      13. Can we terminate or limit your right to use our Services?

      +
        +
      1. + We may cease offering our Services and/or terminate the Agreement and refuse access to the Safe Apps at any + time. The right of the parties to terminate the Agreement at any time for cause remains unaffected. In case of + our termination of the Agreement, you may no longer access your Safe Account via our Services. However, you may continue to access your Safe Account and any Tokens via a third-party wallet provider using your Recovery Phrase and Private Keys.
      2. - We reserve the right to limit the use of the Safe Apps to a specified number of Users if necessary to - protect or ensure the stability and integrity of the Services. We will only be able to limit access to the - Services. At no time will we be able to limit or block access to or transfer your funds without your consent. + We reserve the right to limit the use of the Safe Apps to a specified number of Users if necessary to protect or + ensure the stability and integrity of the Services. We will only be able to limit access to the Services. At no + time will we be able to limit or block access to or transfer your funds without your consent.
      -

      9. Can you terminate your Agreement with us?

      +

      14. Can you terminate your Agreement with us?

      You may terminate the Agreement at any time without notice.

      -

      10. What licenses and access do we grant to you?

      + +

      15. What licenses and access do we grant to you?

      1. All intellectual property rights in Safe Accounts and the Services throughout the world belong to us as owner or @@ -300,7 +397,7 @@ const SafeTerms = () => (
      -

      11. What can you expect from the Services and can we make changes to them?

      +

      16. What can you expect from the Services and can we make changes to them?

      1. Without limiting your mandatory warranties, we provide the Services to you “as is” and “as @@ -327,7 +424,7 @@ const SafeTerms = () => (
      -

      12. What do you agree, warrant and represent?

      +

      17. What do you agree, warrant and represent?

      By using our Services you hereby agree, represent and warrant that:

      1. @@ -387,49 +484,179 @@ const SafeTerms = () => (
      2. You are using the Services at your own risk.
      -

      13. What about our liability to you?

      -

      All our liability is excluded, except for the following:

      - +

      18. What about our liability to you?

      1. - In the event of intent and gross negligence on our part, we are liable for damages – regardless of the legal - grounds. -
      2. -
      3. - In the event of negligence on our part, we are liable for damages resulting from injury to life, body or health. -
      4. -
      5. - In the event of simple negligence on our part, we are only liable for damages resulting from the breach of an - essential contractual duty (e.g. a duty, the performance of which enables the proper execution of the contract - in the first place and on the compliance of which the contractual partner regularly relies and may rely), - whereby our liability shall be limited to compensation of the foreseeable, typically occurring damage. Liability - for the violation of a non-essential contractual duty is excluded. -
      6. -
      7. - The liability for simple negligence only applies to the extent that we do not offer the Safe App and the - Services free of charge (please note, in this context, that any service, network and/or transaction fees may be - charged by third parties via the Blockchain and not necessarily by us). Conversely, this means that we are not - liable in cases of simple negligence, when you obtain the Safe App or the service from us free of charge. -
      8. -
      9. - The limitations of liability according to Clauses 13.2 to 13.4. do not apply as far as we have assumed a - guarantee or we have fraudulently concealed a defect in the Services. These limitations of liability also do not - apply to your claims according to the Product Liability Act (”Produkthaftungsgesetz”) and any applicable data - privacy laws. -
      10. -
      11. - If you suffer damages from the loss of data, we are not liable for this, as far as the damages would have been - avoided by your regular and complete backup of all relevant data. -
      12. -
      13. - In the event of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain - that we are not responsible for, we shall be exempt from our obligation to perform. This also applies if we are - prevented from performing due to force majeure or other circumstances, the elimination of which is not possible - or cannot be economically expected of CC. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      14. +
      15. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      16. +
      17. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      18. +
      19. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      20. +
      21. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      22. +
      23. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC. +
      24. +
      25. + If the Safe App or Services are provided to the User free of charge (please note, in this context, that any + service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to + the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases + of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the + performance of which enables the proper execution of this Agreement in the first place and on the compliance of + which the User regularly relies and may rely, whereby CC's liability shall be limited to the compensation + of the foreseeable, typically occurring damage. The Parties agree that the typical foreseeable damage equals the + sum of the annual Fees paid or agreed to be paid by the User to CC during the course of the calendar year in + which the event giving rise to the damage claim occurred. Liability in cases of simple negligence for damages + resulting from the breach of a non-essential contractual duty are excluded. The limitations of liability + according to Clause 17.1 and Clause 17.2 do not apply (i) to damages resulting from injury to life, body or + health, (ii) insofar as CC has assumed a guarantee, (iii) to claims of the User according to the Product + Liability Act and (iv) to claims of the User according to the applicable data protection law. The limitation of + liability also applies to the personal liability of the organs, legal representatives, employees and vicarious + agents of CC. If the User suffers damages due to the loss of data, CC is not liable for this, insofar as the + damage would have been avoided by a regular and complete backup of all relevant data by the User. In the event + of disruptions to the technical infrastructure, the internet connection or a relevant Blockchain that we are not + responsible for, we shall be exempt from our obligation to perform. This also applies if we are prevented from + performing due to force majeure or other circumstances, the elimination of which is not possible or cannot be + economically expected of CC.
      -

      14. What about viruses, bugs and security vulnerabilities?

      +

      19. What about viruses, bugs and security vulnerabilities?

      1. We endeavor to provide our Service free from material bugs, security vulnerabilities or viruses.
      2. @@ -443,7 +670,7 @@ const SafeTerms = () => (
      -

      15. What if an event outside our control happens that affects our Services?

      +

      20. What if an event outside our control happens that affects our Services?

      1. We may update and change our Services from time to time. We may suspend or withdraw or restrict the availability @@ -485,7 +712,7 @@ const SafeTerms = () => (
      -

      16. Who is responsible for your tax liabilities?

      +

      21. Who is responsible for your tax liabilities?

      You are solely responsible to determine if your use of the Services have tax implications, in particular income tax and capital gains tax relating to the purchase or sale of Tokens, for you. By using the Services you agree not @@ -493,7 +720,7 @@ const SafeTerms = () => ( action or transaction related thereto.

      -

      17. What if a court disagrees with part of this Agreement?

      +

      22. What if a court disagrees with part of this Agreement?

      Should individual provisions of these Terms be or become invalid or unenforceable in whole or in part, this shall not affect the validity of the remaining provisions. The invalid or unenforceable provision shall be replaced by @@ -502,20 +729,20 @@ const SafeTerms = () => ( valid provision that comes as close as possible to the economic purpose of the invalid or unenforceable provision.

      -

      18. What if we do not enforce certain rights under this Agreement?

      +

      23. What if we do not enforce certain rights under this Agreement?

      Our failure to exercise or enforce any right or remedy provided under this Agreement or by law shall not constitute a waiver of that or any other right or remedy, nor shall it prevent or restrict any further exercise of that or any other right or remedy.

      -

      19. Do third parties have rights?

      +

      24. Do third parties have rights?

      Unless it expressly states otherwise, this Agreement does not give rise to any third-party rights, which may be enforced against us.

      -

      20. Can this Agreement be assigned?

      +

      25. Can this Agreement be assigned?

      1. We are entitled to transfer our rights and obligations under the Agreement in whole or in part to third parties @@ -526,13 +753,13 @@ const SafeTerms = () => (
      -

      21. Which Clauses of this Agreement survive termination?

      +

      26. Which Clauses of this Agreement survive termination?

      All covenants, agreements, representations and warranties made in this Agreement shall survive your acceptance of this Agreement and its termination.

      -

      22. Data Protection

      +

      27. Data Protection

      We inform you about our processing of personal data, including the disclosure to third parties and your rights as an affected party, in the{' '} @@ -542,7 +769,7 @@ const SafeTerms = () => ( .

      -

      23. Which laws apply to the Agreement?

      +

      28. Which laws apply to the Agreement?

      The Agreement including these Terms shall be governed by German law. The application of the UN Convention on Contracts for the International Sale of Goods is excluded. For consumers domiciled in another European country but @@ -551,7 +778,7 @@ const SafeTerms = () => ( German law.

      -

      24. How can you get support for Safe Accounts and tell us about any problems?

      +

      29. How can you get support for Safe Accounts and tell us about any problems?

      If you want to learn more about Safe Accounts or the Service or have any problems using them or have any complaints please get in touch via any of the following channels: @@ -583,14 +810,14 @@ const SafeTerms = () => (

    -

    25. Where is the place of legal proceedings?

    +

    30. Where is the place of legal proceedings?

    For users who are merchants within the meaning of the German Commercial Code (Handelsgesetzbuch), a special fund (Sondervermögen) under public law or a legal person under public law, Berlin shall be the exclusive place of jurisdiction for all disputes arising from the contractual relationship.

    -

    26. Is this all?

    +

    31. Is this all?

    These Terms constitute the entire agreement between you and us in relation to the Agreement’s subject matter. It replaces and extinguishes any and all prior agreements, draft agreements, arrangements, warranties, From 7a3714bef56dc9bdfeb8c8e02571d3a945eda6b3 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Thu, 4 Jul 2024 15:45:40 +0200 Subject: [PATCH 127/154] chore: update terms and conditions request [SW-57] (#3906) * chore: users need to reconfirm cookie and terms policy * refactor: rename CookieBanner to CookiesAndTermsBanner --- .../index.tsx | 48 ++++++++++--------- .../styles.module.css | 0 .../sidebar/SidebarFooter/index.tsx | 6 +-- src/hooks/Beamer/useBeamer.ts | 4 +- src/pages/_app.tsx | 4 +- src/pages/settings/cookies.tsx | 4 +- src/services/analytics/useGtm.ts | 4 +- src/store/cookiesAndTermsSlice.ts | 31 ++++++++++++ src/store/cookiesSlice.ts | 29 ----------- src/store/index.ts | 4 +- src/store/popupSlice.ts | 6 +-- src/store/slices.ts | 2 +- 12 files changed, 74 insertions(+), 68 deletions(-) rename src/components/common/{CookieBanner => CookieAndTermBanner}/index.tsx (69%) rename src/components/common/{CookieBanner => CookieAndTermBanner}/styles.module.css (100%) create mode 100644 src/store/cookiesAndTermsSlice.ts delete mode 100644 src/store/cookiesSlice.ts diff --git a/src/components/common/CookieBanner/index.tsx b/src/components/common/CookieAndTermBanner/index.tsx similarity index 69% rename from src/components/common/CookieBanner/index.tsx rename to src/components/common/CookieAndTermBanner/index.tsx index 742a2fb84f..aea7da3c34 100644 --- a/src/components/common/CookieBanner/index.tsx +++ b/src/components/common/CookieAndTermBanner/index.tsx @@ -6,17 +6,18 @@ import WarningIcon from '@/public/images/notifications/warning.svg' import { useForm } from 'react-hook-form' import { useAppDispatch, useAppSelector } from '@/store' -import { selectCookies, CookieType, saveCookieConsent } from '@/store/cookiesSlice' +import { selectCookies, CookieAndTermType, saveCookieAndTermConsent } from '@/store/cookiesAndTermsSlice' import { selectCookieBanner, openCookieBanner, closeCookieBanner } from '@/store/popupSlice' import css from './styles.module.css' import { AppRoutes } from '@/config/routes' import ExternalLink from '../ExternalLink' -const COOKIE_WARNING: Record = { - [CookieType.NECESSARY]: '', - [CookieType.UPDATES]: `You attempted to open the "What's new" section but need to accept the "Beamer" cookies first.`, - [CookieType.ANALYTICS]: '', +const COOKIE_AND_TERM_WARNING: Record = { + [CookieAndTermType.TERMS]: '', + [CookieAndTermType.NECESSARY]: '', + [CookieAndTermType.UPDATES]: `You attempted to open the "What's new" section but need to accept the "Beamer" cookies first.`, + [CookieAndTermType.ANALYTICS]: '', } const CookieCheckbox = ({ @@ -29,34 +30,35 @@ const CookieCheckbox = ({ checkboxProps: CheckboxProps }) => } sx={{ mt: '-9px' }} /> -export const CookieBanner = ({ +export const CookieAndTermBanner = ({ warningKey, inverted, }: { - warningKey?: CookieType + warningKey?: CookieAndTermType inverted?: boolean }): ReactElement => { - const warning = warningKey ? COOKIE_WARNING[warningKey] : undefined + const warning = warningKey ? COOKIE_AND_TERM_WARNING[warningKey] : undefined const dispatch = useAppDispatch() const cookies = useAppSelector(selectCookies) const { register, watch, getValues, setValue } = useForm({ defaultValues: { - [CookieType.NECESSARY]: true, - [CookieType.UPDATES]: cookies[CookieType.UPDATES] ?? false, - [CookieType.ANALYTICS]: cookies[CookieType.ANALYTICS] ?? false, + [CookieAndTermType.TERMS]: true, + [CookieAndTermType.NECESSARY]: true, + [CookieAndTermType.UPDATES]: cookies[CookieAndTermType.UPDATES] ?? false, + [CookieAndTermType.ANALYTICS]: cookies[CookieAndTermType.ANALYTICS] ?? false, ...(warningKey ? { [warningKey]: true } : {}), }, }) const handleAccept = () => { - dispatch(saveCookieConsent(getValues())) + dispatch(saveCookieAndTermConsent(getValues())) dispatch(closeCookieBanner()) } const handleAcceptAll = () => { - setValue(CookieType.UPDATES, true) - setValue(CookieType.ANALYTICS, true) + setValue(CookieAndTermType.UPDATES, true) + setValue(CookieAndTermType.ANALYTICS, true) setTimeout(handleAccept, 300) } @@ -72,8 +74,10 @@ export const CookieBanner = ({ - By clicking "Accept all" you agree to the use of the tools listed below and their corresponding - cookies. Cookie policy + By browsing this page, you accept our{' '} + Terms & Conditions 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 @@ -86,9 +90,9 @@ export const CookieBanner = ({
    New features and product announcements @@ -96,9 +100,9 @@ export const CookieBanner = ({
    @@ -136,7 +140,7 @@ const CookieBannerPopup = (): ReactElement | null => { const dispatch = useAppDispatch() // Open the banner if cookie preferences haven't been set - const shouldOpen = cookies[CookieType.NECESSARY] === undefined + const shouldOpen = cookies[CookieAndTermType.NECESSARY] === undefined useEffect(() => { if (shouldOpen) { @@ -148,7 +152,7 @@ const CookieBannerPopup = (): ReactElement | null => { return cookiePopup?.open ? (

    - +
    ) : null } diff --git a/src/components/common/CookieBanner/styles.module.css b/src/components/common/CookieAndTermBanner/styles.module.css similarity index 100% rename from src/components/common/CookieBanner/styles.module.css rename to src/components/common/CookieAndTermBanner/styles.module.css diff --git a/src/components/sidebar/SidebarFooter/index.tsx b/src/components/sidebar/SidebarFooter/index.tsx index 318061e940..31b0486e49 100644 --- a/src/components/sidebar/SidebarFooter/index.tsx +++ b/src/components/sidebar/SidebarFooter/index.tsx @@ -9,7 +9,7 @@ import { } from '@/components/sidebar/SidebarList' import { BEAMER_SELECTOR, loadBeamer } from '@/services/beamer' import { useAppDispatch, useAppSelector } from '@/store' -import { selectCookies, CookieType } from '@/store/cookiesSlice' +import { selectCookies, CookieAndTermType } from '@/store/cookiesAndTermsSlice' import { openCookieBanner } from '@/store/popupSlice' import BeamerIcon from '@/public/images/sidebar/whats-new.svg' import HelpCenterIcon from '@/public/images/sidebar/help-center.svg' @@ -25,7 +25,7 @@ const SidebarFooter = (): ReactElement => { const cookies = useAppSelector(selectCookies) const chain = useCurrentChain() - const hasBeamerConsent = useCallback(() => cookies[CookieType.UPDATES], [cookies]) + const hasBeamerConsent = useCallback(() => cookies[CookieAndTermType.UPDATES], [cookies]) useEffect(() => { // Initialise Beamer when consent was previously given @@ -36,7 +36,7 @@ const SidebarFooter = (): ReactElement => { const handleBeamer = () => { if (!hasBeamerConsent()) { - dispatch(openCookieBanner({ warningKey: CookieType.UPDATES })) + dispatch(openCookieBanner({ warningKey: CookieAndTermType.UPDATES })) } } diff --git a/src/hooks/Beamer/useBeamer.ts b/src/hooks/Beamer/useBeamer.ts index 96c774fd68..52afabba1e 100644 --- a/src/hooks/Beamer/useBeamer.ts +++ b/src/hooks/Beamer/useBeamer.ts @@ -1,13 +1,13 @@ import { useEffect } from 'react' import { useAppSelector } from '@/store' -import { CookieType, selectCookies } from '@/store/cookiesSlice' +import { CookieAndTermType, selectCookies } from '@/store/cookiesAndTermsSlice' import { loadBeamer, unloadBeamer, updateBeamer } from '@/services/beamer' import { useCurrentChain } from '@/hooks/useChains' const useBeamer = () => { const cookies = useAppSelector(selectCookies) - const isBeamerEnabled = cookies[CookieType.UPDATES] + const isBeamerEnabled = cookies[CookieAndTermType.UPDATES] const chain = useCurrentChain() useEffect(() => { diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 1f9dee17c9..e7dac080d1 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -23,7 +23,7 @@ import useSafeNotifications from '@/hooks/useSafeNotifications' import useTxPendingStatuses from '@/hooks/useTxPendingStatuses' import { useInitSession } from '@/hooks/useInitSession' import Notifications from '@/components/common/Notifications' -import CookieBanner from '@/components/common/CookieBanner' +import CookieAndTermBanner from 'src/components/common/CookieAndTermBanner' import { useDarkMode } from '@/hooks/useDarkMode' import { cgwDebugStorage } from '@/components/sidebar/DebugToggle' import { useTxTracking } from '@/hooks/useTxTracking' @@ -125,7 +125,7 @@ const WebCoreApp = ({ - + diff --git a/src/pages/settings/cookies.tsx b/src/pages/settings/cookies.tsx index 1436fe878f..4fb0c830ae 100644 --- a/src/pages/settings/cookies.tsx +++ b/src/pages/settings/cookies.tsx @@ -1,4 +1,4 @@ -import { CookieBanner } from '@/components/common/CookieBanner' +import { CookieAndTermBanner } from 'src/components/common/CookieAndTermBanner' import SettingsHeader from '@/components/settings/SettingsHeader' import { Grid, Paper, Typography } from '@mui/material' import type { NextPage } from 'next' @@ -23,7 +23,7 @@ const Cookies: NextPage = () => {
    - +
    diff --git a/src/services/analytics/useGtm.ts b/src/services/analytics/useGtm.ts index 3893d82e21..95f546f6d5 100644 --- a/src/services/analytics/useGtm.ts +++ b/src/services/analytics/useGtm.ts @@ -16,7 +16,7 @@ import { } from '@/services/analytics/gtm' import { spindlInit, spindlAttribute } from './spindl' import { useAppSelector } from '@/store' -import { CookieType, selectCookies } from '@/store/cookiesSlice' +import { CookieAndTermType, selectCookies } from '@/store/cookiesAndTermsSlice' import useChainId from '@/hooks/useChainId' import { useRouter } from 'next/router' import { AppRoutes } from '@/config/routes' @@ -30,7 +30,7 @@ import { OVERVIEW_EVENTS } from './events' const useGtm = () => { const chainId = useChainId() const cookies = useAppSelector(selectCookies) - const isAnalyticsEnabled = cookies[CookieType.ANALYTICS] || false + const isAnalyticsEnabled = cookies[CookieAndTermType.ANALYTICS] || false const [, setPrevAnalytics] = useState(isAnalyticsEnabled) const router = useRouter() const theme = useTheme() diff --git a/src/store/cookiesAndTermsSlice.ts b/src/store/cookiesAndTermsSlice.ts new file mode 100644 index 0000000000..2c87b0f1f6 --- /dev/null +++ b/src/store/cookiesAndTermsSlice.ts @@ -0,0 +1,31 @@ +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' +import type { RootState } from '.' + +export enum CookieAndTermType { + TERMS = 'terms', + NECESSARY = 'necessary', + UPDATES = 'updates', + ANALYTICS = 'analytics', +} + +export type CookiesAndTermsState = Record + +const initialState: CookiesAndTermsState = { + [CookieAndTermType.TERMS]: undefined, + [CookieAndTermType.NECESSARY]: undefined, + [CookieAndTermType.UPDATES]: undefined, + [CookieAndTermType.ANALYTICS]: undefined, +} + +export const cookiesAndTermsSlice = createSlice({ + name: 'cookies_terms_v1', + initialState, + reducers: { + saveCookieAndTermConsent: (_, { payload }: PayloadAction) => payload, + }, +}) + +export const { saveCookieAndTermConsent } = cookiesAndTermsSlice.actions + +export const selectCookies = (state: RootState) => state[cookiesAndTermsSlice.name] diff --git a/src/store/cookiesSlice.ts b/src/store/cookiesSlice.ts deleted file mode 100644 index 70bbc35662..0000000000 --- a/src/store/cookiesSlice.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit' -import { createSlice } from '@reduxjs/toolkit' -import type { RootState } from '.' - -export enum CookieType { - NECESSARY = 'necessary', - UPDATES = 'updates', - ANALYTICS = 'analytics', -} - -export type CookiesState = Record - -const initialState: CookiesState = { - [CookieType.NECESSARY]: undefined, - [CookieType.UPDATES]: undefined, - [CookieType.ANALYTICS]: undefined, -} - -export const cookiesSlice = createSlice({ - name: 'cookies', - initialState, - reducers: { - saveCookieConsent: (_, { payload }: PayloadAction) => payload, - }, -}) - -export const { saveCookieConsent } = cookiesSlice.actions - -export const selectCookies = (state: RootState) => state[cookiesSlice.name] diff --git a/src/store/index.ts b/src/store/index.ts index de091020b3..e6b2f8f81c 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -34,7 +34,7 @@ const rootReducer = combineReducers({ [slices.pendingTxsSlice.name]: slices.pendingTxsSlice.reducer, [slices.addedSafesSlice.name]: slices.addedSafesSlice.reducer, [slices.settingsSlice.name]: slices.settingsSlice.reducer, - [slices.cookiesSlice.name]: slices.cookiesSlice.reducer, + [slices.cookiesAndTermsSlice.name]: slices.cookiesAndTermsSlice.reducer, [slices.popupSlice.name]: slices.popupSlice.reducer, [slices.spendingLimitSlice.name]: slices.spendingLimitSlice.reducer, [slices.safeAppsSlice.name]: slices.safeAppsSlice.reducer, @@ -51,7 +51,7 @@ const persistedSlices: (keyof PreloadedState)[] = [ slices.pendingTxsSlice.name, slices.addedSafesSlice.name, slices.settingsSlice.name, - slices.cookiesSlice.name, + slices.cookiesAndTermsSlice.name, slices.safeAppsSlice.name, slices.pendingSafeMessagesSlice.name, slices.batchSlice.name, diff --git a/src/store/popupSlice.ts b/src/store/popupSlice.ts index c077d558a7..831fcc5c90 100644 --- a/src/store/popupSlice.ts +++ b/src/store/popupSlice.ts @@ -1,6 +1,6 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' -import type { CookieType } from './cookiesSlice' +import type { CookieAndTermType } from './cookiesAndTermsSlice' import type { RootState } from '.' export enum PopupType { @@ -10,7 +10,7 @@ export enum PopupType { type PopupState = { [PopupType.COOKIES]: { open: boolean - warningKey?: CookieType + warningKey?: CookieAndTermType } } @@ -24,7 +24,7 @@ export const popupSlice = createSlice({ name: 'popups', initialState, reducers: { - openCookieBanner: (state, { payload }: PayloadAction<{ warningKey?: CookieType }>) => { + openCookieBanner: (state, { payload }: PayloadAction<{ warningKey?: CookieAndTermType }>) => { state[PopupType.COOKIES] = { ...payload, open: true, diff --git a/src/store/slices.ts b/src/store/slices.ts index b186894502..210bd7d7c5 100644 --- a/src/store/slices.ts +++ b/src/store/slices.ts @@ -9,7 +9,7 @@ export * from './notificationsSlice' export * from './pendingTxsSlice' export * from './addedSafesSlice' export * from './settingsSlice' -export * from './cookiesSlice' +export * from './cookiesAndTermsSlice' export * from './popupSlice' export * from './spendingLimitsSlice' export * from './safeAppsSlice' From f920e90ad07dbb6d5d20d444c517fbdcc0c50053 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Thu, 4 Jul 2024 16:10:01 +0200 Subject: [PATCH 128/154] feat: add feature flag for swap fees (#3905) --- src/features/swap/index.tsx | 10 ++++++---- src/utils/chains.ts | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 1ec8ed89a6..c6c0e23957 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -9,7 +9,7 @@ import { type SafeAppData, SafeAppFeatures, } from '@safe-global/safe-gateway-typescript-sdk/dist/types/safe-apps' -import { useCurrentChain } from '@/hooks/useChains' +import { useCurrentChain, useHasFeature } from '@/hooks/useChains' import { useDarkMode } from '@/hooks/useDarkMode' import { useCustomAppCommunicator } from '@/hooks/safe-apps/useCustomAppCommunicator' import { useAppDispatch, useAppSelector } from '@/store' @@ -38,6 +38,7 @@ import { } from '@/features/swap/constants' import { calculateFeePercentageInBps } from '@/features/swap/helpers/fee' import { UiOrderTypeToOrderType } from '@/features/swap/helpers/utils' +import { FEATURES } from '@/utils/chains' const BASE_URL = typeof window !== 'undefined' && window.location.origin ? window.location.origin : '' @@ -81,6 +82,7 @@ const SwapWidget = ({ sell }: Params) => { const [blockedAddress, setBlockedAddress] = useState('') const wallet = useWallet() const { isConsentAccepted, onAccept } = useSwapConsent() + const feeEnabled = useHasFeature(FEATURES.NATIVE_SWAPS_FEE_ENABLED) const [params, setParams] = useState({ appCode: 'Safe Wallet Swaps', // Name of your app (max 50 characters) @@ -125,7 +127,7 @@ const SwapWidget = ({ sell }: Params) => { alert: palette.warning.main, }, partnerFee: { - bps: 35, + bps: feeEnabled ? 35 : 0, recipient: SWAP_FEE_RECIPIENT, }, content: { @@ -217,7 +219,7 @@ const SwapWidget = ({ sell }: Params) => { handler: (newTradeParams: OnTradeParamsPayload) => { const { orderType: tradeType, recipient, sellToken, buyToken } = newTradeParams - const newFeeBps = calculateFeePercentageInBps(newTradeParams) + const newFeeBps = feeEnabled ? calculateFeePercentageInBps(newTradeParams) : 0 setParams((params) => ({ ...params, @@ -242,7 +244,7 @@ const SwapWidget = ({ sell }: Params) => { }, }, ] - }, [dispatch]) + }, [dispatch, feeEnabled]) useEffect(() => { setParams((params) => ({ diff --git a/src/utils/chains.ts b/src/utils/chains.ts index 2cebd45e8b..6df14d92ca 100644 --- a/src/utils/chains.ts +++ b/src/utils/chains.ts @@ -22,6 +22,7 @@ export enum FEATURES { SPEED_UP_TX = 'SPEED_UP_TX', SAP_BANNER = 'SAP_BANNER', NATIVE_SWAPS = 'NATIVE_SWAPS', + NATIVE_SWAPS_FEE_ENABLED = 'NATIVE_SWAPS_FEE_ENABLED', RELAY_NATIVE_SWAPS = 'RELAY_NATIVE_SWAPS', ZODIAC_ROLES = 'ZODIAC_ROLES', } From f3de1c85db172f567b2b96927ff872de9a793ce2 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Thu, 4 Jul 2024 16:10:19 +0200 Subject: [PATCH 129/154] chore: update ofac list addresses (#3903) add latest addresses up to 2024-05-01 https://github.com/ultrasoundmoney/ofac-ethereum-addresses/blob/main/data.csv --- src/services/ofac/blockedAddressList.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/ofac/blockedAddressList.json b/src/services/ofac/blockedAddressList.json index e8df17f077..2a75c8a50b 100644 --- a/src/services/ofac/blockedAddressList.json +++ b/src/services/ofac/blockedAddressList.json @@ -149,5 +149,10 @@ "0x530a64c0ce595026a4a556b703644228179e2d57", "0xfac583c0cf07ea434052c49115a4682172ab6b4f", "0x961c5be54a2ffc17cf4cb021d863c42dacd47fc1", - "0x983a81ca6fb1e441266d2fbcb7d8e530ac2e05a2" + "0x983a81ca6fb1e441266d2fbcb7d8e530ac2e05a2", + "0xf3701f445b6bdafedbca97d1e477357839e4120d", + "0xe950dc316b836e4eefb8308bf32bf7c72a1358ff", + "0x21b8d56bda776bbe68655a16895afd96f5534fed", + "0x175d44451403edf28469df03a9280c1197adb92c", + "0x19f8f2b0915daa12a3f5c9cf01df9e24d53794f7" ] From 7e10cbc6382058fafb5e83b70b11d6abfaf66af0 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Thu, 4 Jul 2024 18:00:57 +0200 Subject: [PATCH 130/154] fix: address poison copy confirmation modal (#3891) --- .../TxDetails/TxData/Transfer/index.test.tsx | 276 ++++++++++++++++++ .../TxDetails/TxData/Transfer/index.tsx | 2 +- 2 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/components/transactions/TxDetails/TxData/Transfer/index.test.tsx diff --git a/src/components/transactions/TxDetails/TxData/Transfer/index.test.tsx b/src/components/transactions/TxDetails/TxData/Transfer/index.test.tsx new file mode 100644 index 0000000000..d904372959 --- /dev/null +++ b/src/components/transactions/TxDetails/TxData/Transfer/index.test.tsx @@ -0,0 +1,276 @@ +import { render } from '@/tests/test-utils' +import TransferTxInfo from '.' +import { + TransactionInfoType, + TransactionStatus, + TransactionTokenType, + TransferDirection, +} from '@safe-global/safe-gateway-typescript-sdk' +import { faker } from '@faker-js/faker' +import { parseUnits } from 'ethers' +import { chainBuilder } from '@/tests/builders/chains' + +jest.mock('@/hooks/useChains', () => ({ + __esModule: true, + useChainId: () => '1', + useChain: () => chainBuilder().with({ chainId: '1' }).build(), + useCurrentChain: () => chainBuilder().with({ chainId: '1' }).build(), + default: () => ({ + loading: false, + error: undefined, + configs: [chainBuilder().with({ chainId: '1' }).build()], + }), +})) + +describe('TransferTxInfo', () => { + describe('should render non-malicious', () => { + it('outgoing tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('1 TST')).toBeInTheDocument() + expect(result.getByText(recipient)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + + it('incoming tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + }) + + describe('should render untrusted', () => { + it('outgoing tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('1 TST')).toBeInTheDocument() + expect(result.getByText(recipient)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.getByLabelText('This token is unfamiliar', { exact: false })).toBeInTheDocument() + }) + + it('incoming tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeInTheDocument() + }) + }) + + describe('should render imitations', () => { + it('outgoing tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('1 TST')).toBeInTheDocument() + expect(result.getByText(recipient)).toBeInTheDocument() + expect(result.getByText('malicious', { exact: false })).toBeInTheDocument() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + + it('incoming tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.getByText('malicious', { exact: false })).toBeInTheDocument() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + + it('untrusted and imitation tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.getByText('malicious', { exact: false })).toBeInTheDocument() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + }) +}) diff --git a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx index 7254c81657..9971388af1 100644 --- a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx +++ b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx @@ -49,7 +49,7 @@ const TransferTxInfo = ({ txInfo, txStatus, trusted, imitation }: TransferTxInfo shortAddress={false} hasExplorer showCopyButton - trusted={trusted} + trusted={trusted && !imitation} > From 88eb33f0b4f3a0dba0a762173fc6609b3b7e1567 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Thu, 4 Jul 2024 18:00:57 +0200 Subject: [PATCH 131/154] fix: address poison copy confirmation modal (#3891) --- .../TxDetails/TxData/Transfer/index.test.tsx | 276 ++++++++++++++++++ .../TxDetails/TxData/Transfer/index.tsx | 2 +- 2 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/components/transactions/TxDetails/TxData/Transfer/index.test.tsx diff --git a/src/components/transactions/TxDetails/TxData/Transfer/index.test.tsx b/src/components/transactions/TxDetails/TxData/Transfer/index.test.tsx new file mode 100644 index 0000000000..d904372959 --- /dev/null +++ b/src/components/transactions/TxDetails/TxData/Transfer/index.test.tsx @@ -0,0 +1,276 @@ +import { render } from '@/tests/test-utils' +import TransferTxInfo from '.' +import { + TransactionInfoType, + TransactionStatus, + TransactionTokenType, + TransferDirection, +} from '@safe-global/safe-gateway-typescript-sdk' +import { faker } from '@faker-js/faker' +import { parseUnits } from 'ethers' +import { chainBuilder } from '@/tests/builders/chains' + +jest.mock('@/hooks/useChains', () => ({ + __esModule: true, + useChainId: () => '1', + useChain: () => chainBuilder().with({ chainId: '1' }).build(), + useCurrentChain: () => chainBuilder().with({ chainId: '1' }).build(), + default: () => ({ + loading: false, + error: undefined, + configs: [chainBuilder().with({ chainId: '1' }).build()], + }), +})) + +describe('TransferTxInfo', () => { + describe('should render non-malicious', () => { + it('outgoing tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('1 TST')).toBeInTheDocument() + expect(result.getByText(recipient)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + + it('incoming tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + }) + + describe('should render untrusted', () => { + it('outgoing tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('1 TST')).toBeInTheDocument() + expect(result.getByText(recipient)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.getByLabelText('This token is unfamiliar', { exact: false })).toBeInTheDocument() + }) + + it('incoming tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.queryByText('malicious', { exact: false })).toBeNull() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeInTheDocument() + }) + }) + + describe('should render imitations', () => { + it('outgoing tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('1 TST')).toBeInTheDocument() + expect(result.getByText(recipient)).toBeInTheDocument() + expect(result.getByText('malicious', { exact: false })).toBeInTheDocument() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + + it('incoming tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.getByText('malicious', { exact: false })).toBeInTheDocument() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + + it('untrusted and imitation tx', () => { + const recipient = faker.finance.ethereumAddress() + const sender = faker.finance.ethereumAddress() + const tokenAddress = faker.finance.ethereumAddress() + + const result = render( + , + ) + + expect(result.getByText('12.34 TST')).toBeInTheDocument() + expect(result.getByText(sender)).toBeInTheDocument() + expect(result.getByText('malicious', { exact: false })).toBeInTheDocument() + expect(result.queryByLabelText('This token is unfamiliar', { exact: false })).toBeNull() + }) + }) +}) diff --git a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx index 7254c81657..9971388af1 100644 --- a/src/components/transactions/TxDetails/TxData/Transfer/index.tsx +++ b/src/components/transactions/TxDetails/TxData/Transfer/index.tsx @@ -49,7 +49,7 @@ const TransferTxInfo = ({ txInfo, txStatus, trusted, imitation }: TransferTxInfo shortAddress={false} hasExplorer showCopyButton - trusted={trusted} + trusted={trusted && !imitation} > From 90b0a6d2c603c38aba299683ceff450ce970b214 Mon Sep 17 00:00:00 2001 From: Den Smalonski Date: Thu, 4 Jul 2024 18:42:52 +0200 Subject: [PATCH 132/154] fix: add transparent bg for logo on home --- src/components/welcome/WelcomeLogin/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/welcome/WelcomeLogin/index.tsx b/src/components/welcome/WelcomeLogin/index.tsx index 910e573302..41bde3cb9f 100644 --- a/src/components/welcome/WelcomeLogin/index.tsx +++ b/src/components/welcome/WelcomeLogin/index.tsx @@ -45,7 +45,11 @@ const WelcomeLogin = () => { return ( - + Get started From 0b8525429c0333481041f6485a7956aca0d6b79f Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Fri, 5 Jul 2024 09:50:29 +0200 Subject: [PATCH 133/154] Tests: Add relay check (#3909) --- cypress/e2e/happypath/sendfunds_relay.cy.js | 104 +++++++++++--------- cypress/e2e/pages/main.page.js | 25 +++++ cypress/support/constants.js | 1 + 3 files changed, 85 insertions(+), 45 deletions(-) diff --git a/cypress/e2e/happypath/sendfunds_relay.cy.js b/cypress/e2e/happypath/sendfunds_relay.cy.js index 3048ce3934..5cb76405b9 100644 --- a/cypress/e2e/happypath/sendfunds_relay.cy.js +++ b/cypress/e2e/happypath/sendfunds_relay.cy.js @@ -14,7 +14,7 @@ import { contracts, abi_qtrust, abi_nft_pc2 } from '../../support/api/contracts' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' -const safeBalanceEth = 405240000000000000n +const safeBalanceEth = 405250000000000000n const qtrustBanance = 60000000000000000000n const transferAmount = '1' @@ -97,15 +97,19 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 3000 return main.fetchCurrentNonce(network_pref + originatingSafe) }) .then(async (currentNonce) => { - executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount).then(async () => { - main.checkTokenBalanceIsNull(network_pref + originatingSafe, constants.tokenAbbreviation.tpcc) - const contractWithWallet = nftContract.connect(owner1Signer) - const tx = await contractWithWallet.safeTransferFrom(walletAddress.toString(), originatingSafe, 2, { - gasLimit: 200000, + return main.getRelayRemainingAttempts(originatingSafe).then((remainingAttempts) => { + if (remainingAttempts < 1) { + throw new Error(main.noRelayAttemptsError) + } + executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount).then(async () => { + main.checkTokenBalanceIsNull(network_pref + originatingSafe, constants.tokenAbbreviation.tpcc) + const contractWithWallet = nftContract.connect(owner1Signer) + const tx = await contractWithWallet.safeTransferFrom(walletAddress.toString(), originatingSafe, 2, { + gasLimit: 200000, + }) + await tx.wait() + main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) }) - - await tx.wait() - main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) }) }) }) @@ -129,37 +133,42 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 3000 return main.fetchCurrentNonce(network_pref + targetSafe) }) .then(async (currentNonce) => { - executeTransactionFlow(targetSafe, walletAddress.toString(), tokenAmount2) - const amount = ethers.parseUnits(tokenAmount2, unit_eth).toString() - const safeTransactionData = { - to: targetSafe, - data: '0x', - value: amount.toString(), - } - - const safeTransaction = await protocolKitOwner1_S3.createTransaction({ transactions: [safeTransactionData] }) - const safeTxHash = await protocolKitOwner1_S3.getTransactionHash(safeTransaction) - const senderSignature = await protocolKitOwner1_S3.signHash(safeTxHash) - const safeAddress = outgoingSafeAddress - - await apiKit.proposeTransaction({ - safeAddress, - safeTransactionData: safeTransaction.data, - safeTxHash, - senderAddress: await owner1Signer.getAddress(), - senderSignature: senderSignature.data, - }) + return main.getRelayRemainingAttempts(targetSafe).then(async (remainingAttempts) => { + if (remainingAttempts < 1) { + throw new Error(main.noRelayAttemptsError) + } + executeTransactionFlow(targetSafe, walletAddress.toString(), tokenAmount2) + const amount = ethers.parseUnits(tokenAmount2, unit_eth).toString() + const safeTransactionData = { + to: targetSafe, + data: '0x', + value: amount.toString(), + } + + const safeTransaction = await protocolKitOwner1_S3.createTransaction({ transactions: [safeTransactionData] }) + const safeTxHash = await protocolKitOwner1_S3.getTransactionHash(safeTransaction) + const senderSignature = await protocolKitOwner1_S3.signHash(safeTxHash) + const safeAddress = outgoingSafeAddress + + await apiKit.proposeTransaction({ + safeAddress, + safeTransactionData: safeTransaction.data, + safeTxHash, + senderAddress: await owner1Signer.getAddress(), + senderSignature: senderSignature.data, + }) - const pendingTransactions = await apiKit.getPendingTransactions(safeAddress) - const safeTxHashofExistingTx = pendingTransactions.results[0].safeTxHash + const pendingTransactions = await apiKit.getPendingTransactions(safeAddress) + const safeTxHashofExistingTx = pendingTransactions.results[0].safeTxHash - const signature = await protocolKitOwner2_S3.signHash(safeTxHashofExistingTx) - await apiKit.confirmTransaction(safeTxHashofExistingTx, signature.data) + const signature = await protocolKitOwner2_S3.signHash(safeTxHashofExistingTx) + await apiKit.confirmTransaction(safeTxHashofExistingTx, signature.data) - const safeTx = await apiKit.getTransaction(safeTxHashofExistingTx) - await protocolKitOwner2_S3.executeTransaction(safeTx) - main.verifyNonceChange(network_pref + targetSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + targetSafe, constants.tokenAbbreviation.eth, safeBalanceEth) + const safeTx = await apiKit.getTransaction(safeTxHashofExistingTx) + await protocolKitOwner2_S3.executeTransaction(safeTx) + main.verifyNonceChange(network_pref + targetSafe, currentNonce + 1) + main.checkTokenBalance(network_pref + targetSafe, constants.tokenAbbreviation.eth, safeBalanceEth) + }) }) }) @@ -186,16 +195,21 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 3000 return main.fetchCurrentNonce(network_pref + originatingSafe) }) .then(async (currentNonce) => { - executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount) + return main.getRelayRemainingAttempts(originatingSafe).then(async (remainingAttempts) => { + if (remainingAttempts < 1) { + throw new Error(main.noRelayAttemptsError) + } + executeTransactionFlow(originatingSafe, walletAddress.toString(), transferAmount) + + const contractWithWallet = tokenContract.connect(signers[0]) + const tx = await contractWithWallet.transfer(originatingSafe, amount, { + gasLimit: 200000, + }) - const contractWithWallet = tokenContract.connect(signers[0]) - const tx = await contractWithWallet.transfer(originatingSafe, amount, { - gasLimit: 200000, + await tx.wait() + main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) + main.checkTokenBalance(network_pref + originatingSafe, constants.tokenAbbreviation.qtrust, qtrustBanance) }) - - await tx.wait() - main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + originatingSafe, constants.tokenAbbreviation.qtrust, qtrustBanance) }) }) }) diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index 214be21e3e..5643720ef9 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -4,6 +4,7 @@ const acceptSelection = 'Save settings' const executeStr = 'Execute' const connectedOwnerBlock = '[data-testid="open-account-center"]' export const modalDialogCloseBtn = '[data-testid="modal-dialog-close-btn"]' +export const noRelayAttemptsError = 'Not enough relay attempts remaining' export function checkElementBackgroundColor(element, color) { cy.get(element).should('have.css', 'background-color', color) @@ -84,6 +85,23 @@ export function fetchCurrentNonce(safeAddress) { ) } +export const getRelayRemainingAttempts = (safeAddress) => { + const chain = constants.networkKeys.sepolia + + return cy + .request({ + method: 'GET', + url: `${constants.stagingCGWUrlv1}${constants.stagingCGWChains}${chain}${constants.relayPath}${safeAddress}`, + headers: { + accept: 'application/json', + }, + }) + .then((response) => { + console.log('Remaining relay attempts: ', response.body.remaining) + return response.body.remaining + }) +} + export function verifyNonceChange(safeAddress, expectedNonce) { fetchCurrentNonce(safeAddress).then((newNonce) => { expect(newNonce).to.equal(expectedNonce) @@ -98,6 +116,13 @@ export function checkTokenBalance(safeAddress, tokenSymbol, expectedBalance) { }) } +export function getTokenBalance(safeAddress, tokenSymbol) { + getSafeBalance(safeAddress.substring(4), constants.networkKeys.sepolia).then((response) => { + const targetToken = response.body.items.find((token) => token.tokenInfo.symbol === tokenSymbol) + console.log('**** TOKEN BALANCE', targetToken.balance) + }) +} + export function checkNFTBalance(safeAddress, tokenSymbol, expectedBalance) { getSafeNFTs(safeAddress.substring(4), constants.networkKeys.sepolia).then((response) => { const targetToken = response.body.results.find((token) => token.tokenSymbol === tokenSymbol) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 1e83251268..0bba9e43a4 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -81,6 +81,7 @@ export const stagingCGWChains = '/chains/' export const stagingCGWSafes = '/safes/' export const stagingCGWNone = '/nonces/' export const stagingCGWCollectibles = '/collectibles/' +export const relayPath = '/relay/' export const stagingCGWAllTokensBalances = '/balances/USD?trusted=false&exclude_spam=false' export const proposeEndpoint = '/**/propose' From c5d65fd6bcbff74de6ba7cd831b4e6e7978d34ba Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Fri, 5 Jul 2024 10:35:24 +0200 Subject: [PATCH 134/154] =?UTF-8?q?fix:=20don=E2=80=99t=20display=20widget?= =?UTF-8?q?=20fee,=20if=20it=20is=20not=20on=20(#3910)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OrderFeeConfirmationView.tsx | 11 +++++------ .../components/SwapOrderConfirmationView/index.tsx | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx index dd283a1c3c..f823509f9f 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx @@ -5,14 +5,13 @@ import { HelpCenterArticle } from '@/config/constants' import { HelpIconTooltip } from '@/features/swap/components/HelpIconTooltip' import MUILink from '@mui/material/Link' -export const OrderFeeConfirmationView = ({ - order, -}: { - order: Pick - hideWhenNonFulfilled?: boolean -}) => { +export const OrderFeeConfirmationView = ({ order }: { order: Pick }) => { const bps = getOrderFeeBps(order) + if (Number(bps) === 0) { + return null + } + const title = ( <> Widget fee{' '} diff --git a/src/features/swap/components/SwapOrderConfirmationView/index.tsx b/src/features/swap/components/SwapOrderConfirmationView/index.tsx index 515763aad8..39edb319f2 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/index.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/index.tsx @@ -93,7 +93,7 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd ) : ( <> ), - , + , , From 17e05478a895383bebc679fc943b36f85ab96124 Mon Sep 17 00:00:00 2001 From: James Mealy Date: Fri, 5 Jul 2024 11:58:13 +0200 Subject: [PATCH 135/154] Fix: Add title for limit order cancellations [SW-68] (#3911) * Fix: Add title for limit order cancellations * Make title more general --- src/features/swap/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index c6c0e23957..c96c61c7c6 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -45,7 +45,8 @@ const BASE_URL = typeof window !== 'undefined' && window.location.origin ? windo const PRE_SIGN_SIGHASH = id('setPreSignature(bytes,bool)').slice(0, 10) const WRAP_SIGHASH = id('deposit()').slice(0, 10) const UNWRAP_SIGHASH = id('withdraw(uint256)').slice(0, 10) -const CREATE_WITH_CONTEXT = id('createWithContext((address,bytes32,bytes),address,bytes,bool)').slice(0, 10) +const CREATE_WITH_CONTEXT_SIGHASH = id('createWithContext((address,bytes32,bytes),address,bytes,bool)').slice(0, 10) +const CANCEL_ORDER_SIGHASH = id('invalidateOrder(bytes)').slice(0, 10) type Params = { sell?: { @@ -60,7 +61,8 @@ export const getSwapTitle = (tradeType: SwapState['tradeType'], txs: BaseTransac [APPROVAL_SIGNATURE_HASH]: 'Approve', [WRAP_SIGHASH]: 'Wrap', [UNWRAP_SIGHASH]: 'Unwrap', - [CREATE_WITH_CONTEXT]: TWAP_ORDER_TITLE, + [CREATE_WITH_CONTEXT_SIGHASH]: TWAP_ORDER_TITLE, + [CANCEL_ORDER_SIGHASH]: 'Cancel Order', } const swapTitle = txs From 3a28a1f76cb9254e2daff13bfd635773044a2a9c Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Fri, 5 Jul 2024 12:28:56 +0200 Subject: [PATCH 136/154] fix: mark imitation txs opaque (#3908) --- src/components/transactions/TxSummary/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/transactions/TxSummary/index.tsx b/src/components/transactions/TxSummary/index.tsx index 611a10f03d..3763d60fb8 100644 --- a/src/components/transactions/TxSummary/index.tsx +++ b/src/components/transactions/TxSummary/index.tsx @@ -44,7 +44,7 @@ const TxSummary = ({ item, isConflictGroup, isBulkGroup }: TxSummaryProps): Reac [css.history]: !isQueue, [css.conflictGroup]: isConflictGroup, [css.bulkGroup]: isBulkGroup, - [css.untrusted]: !isTrusted, + [css.untrusted]: !isTrusted || isImitationTransaction, })} id={tx.id} > From 7de64fab7e03acf3cb762dfff05083a926822c0b Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Fri, 5 Jul 2024 12:28:56 +0200 Subject: [PATCH 137/154] fix: mark imitation txs opaque (#3908) --- src/components/transactions/TxSummary/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/transactions/TxSummary/index.tsx b/src/components/transactions/TxSummary/index.tsx index 611a10f03d..3763d60fb8 100644 --- a/src/components/transactions/TxSummary/index.tsx +++ b/src/components/transactions/TxSummary/index.tsx @@ -44,7 +44,7 @@ const TxSummary = ({ item, isConflictGroup, isBulkGroup }: TxSummaryProps): Reac [css.history]: !isQueue, [css.conflictGroup]: isConflictGroup, [css.bulkGroup]: isBulkGroup, - [css.untrusted]: !isTrusted, + [css.untrusted]: !isTrusted || isImitationTransaction, })} id={tx.id} > From 2e4acea2e18b17cb18aa1014928e8bb1cc1f01af Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Fri, 5 Jul 2024 14:09:23 +0200 Subject: [PATCH 138/154] chore: update cowprotocol/widget-react (#3914) The CoW widget was not properly updating AppData in some situations. This widget release includes the fix for this: https://github.com/cowprotocol/cowswap/pull/4670 --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 195a9f3b99..2f36b56772 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@gnosis.pm/zodiac/**/ethers": "^6.11.1" }, "dependencies": { - "@cowprotocol/widget-react": "^0.9.1", + "@cowprotocol/widget-react": "^0.9.3", "@ducanh2912/next-pwa": "^9.7.1", "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.0", diff --git a/yarn.lock b/yarn.lock index 77bd445e20..69ff8a11fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1210,19 +1210,19 @@ resolved "https://registry.yarnpkg.com/@cowprotocol/events/-/events-1.3.0.tgz#44c175689d0f9bbd769901e7b0e4ecd33ec55697" integrity sha512-gYTbm8stIXJI/a+jWcjAdDtIo5NX2Q88Q90XETJE2MaLIR7LQL0Ao4mW0J9LsYIvE7Zb7QEdduJ56pNXmQh5/g== -"@cowprotocol/widget-lib@^0.11.1": - version "0.11.1" - resolved "https://registry.yarnpkg.com/@cowprotocol/widget-lib/-/widget-lib-0.11.1.tgz#df2906ba33215931582172bfa5b2ab5c512f2d83" - integrity sha512-xMD3D3hw3HrRX5jpF5LEziopnPjKTd28FQcZ4f7p0SStzDgYoiGAHNGDxcefafklS6Osi22IjamDZ6bGp/Ul3g== +"@cowprotocol/widget-lib@^0.13.2": + version "0.13.2" + resolved "https://registry.yarnpkg.com/@cowprotocol/widget-lib/-/widget-lib-0.13.2.tgz#f554773ccdae4549be1f856fc59b9de4b8a7af4c" + integrity sha512-PS5phDCaQcB+6DmXyRaWfATh5K8PXFkRvt6jBhLPhNR/JOnuPyBLzECzWrnWkD+dDJwFwALid1j22Z5h0H84Bw== dependencies: "@cowprotocol/events" "^1.3.0" -"@cowprotocol/widget-react@^0.9.1": - version "0.9.1" - resolved "https://registry.yarnpkg.com/@cowprotocol/widget-react/-/widget-react-0.9.1.tgz#d4c82028c4a02361984ed892ee677a96a7669320" - integrity sha512-EWq/5EVRFf2kGUJNHeJ7c2JedFSioXrHAbf+4I25+g+pAkUbrzwjxJSeppovLwXpv7umqHqEQaw7jsctpacjEw== +"@cowprotocol/widget-react@^0.9.3": + version "0.9.3" + resolved "https://registry.yarnpkg.com/@cowprotocol/widget-react/-/widget-react-0.9.3.tgz#57ff027e8e49dc1399bae2afe51b3fa4430fad48" + integrity sha512-kVTdheXAU8gLa1x+lH6CYJAsTRAzZvfaCx6iJrftNnSIl4oFiI70k/kxndC+jewMR01auo4+EQQzX8//b39m0g== dependencies: - "@cowprotocol/widget-lib" "^0.11.1" + "@cowprotocol/widget-lib" "^0.13.2" "@cypress/request@2.88.12": version "2.88.12" From 08973ef3fc937ea0deca0b42001db0c8a66e83cb Mon Sep 17 00:00:00 2001 From: James Mealy Date: Fri, 5 Jul 2024 14:11:22 +0200 Subject: [PATCH 139/154] Fix: display swap fees using the correct token [SW-67] (#3913) * fix: show calculated fee in the correct token * fix: tooltip formatting --- .../swap/components/SwapOrder/rows/SurplusFee.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/features/swap/components/SwapOrder/rows/SurplusFee.tsx b/src/features/swap/components/SwapOrder/rows/SurplusFee.tsx index b03af41bef..91e566f0c1 100644 --- a/src/features/swap/components/SwapOrder/rows/SurplusFee.tsx +++ b/src/features/swap/components/SwapOrder/rows/SurplusFee.tsx @@ -10,13 +10,9 @@ export const SurplusFee = ({ order: Pick }) => { const bps = getOrderFeeBps(order) - const { executedSurplusFee, status, sellToken, buyToken, kind } = order + const { executedSurplusFee, sellToken } = order let token = sellToken - if (kind === 'buy') { - token = buyToken - } - if (executedSurplusFee === null || typeof executedSurplusFee === 'undefined' || executedSurplusFee === '0') { return null } @@ -30,7 +26,7 @@ export const SurplusFee = ({ title={ <> The amount of fees paid for this order. - {bps > 0 && `This includes a Widget fee of ${bps / 100} % and network fees.`} + {bps > 0 && ` This includes a Widget fee of ${bps / 100}% and network fees.`} } /> From 4c90f768d92899eb73520642e3dfedcebf48f5b3 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Fri, 5 Jul 2024 14:26:41 +0200 Subject: [PATCH 140/154] fix: terms was not a link (#3915) --- src/features/swap/components/LegalDisclaimer/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/features/swap/components/LegalDisclaimer/index.tsx b/src/features/swap/components/LegalDisclaimer/index.tsx index 3caad26deb..8c4e7393a4 100644 --- a/src/features/swap/components/LegalDisclaimer/index.tsx +++ b/src/features/swap/components/LegalDisclaimer/index.tsx @@ -25,8 +25,11 @@ const LegalDisclaimerContent = () => ( contain more detailed provisions binding on you relating to such third party content. - By clicking "continue" you re-confirm to have read and understood our terms and this message, and - agree to them. + By clicking "continue" you re-confirm to have read and understood our{' '} + + terms + {' '} + and this message, and agree to them.
    From fb9d9db99383755bcb2c1b19c17d3b8c8d5c9765 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:56:50 +0200 Subject: [PATCH 141/154] Tests: Add sidebar (pending tx) tests (#3912) * Add sidebar tests * Update tests --- cypress/e2e/pages/sidebar.pages.js | 20 +++++++++ cypress/e2e/regression/pending_actions.cy.js | 43 -------------------- cypress/e2e/regression/sidebar_3.cy.js | 38 +++++++++++++++++ cypress/e2e/smoke/tx_history_filter.cy.js | 6 +-- cypress/fixtures/txhistory_data_data.json | 6 +-- 5 files changed, 64 insertions(+), 49 deletions(-) delete mode 100644 cypress/e2e/regression/pending_actions.cy.js diff --git a/cypress/e2e/pages/sidebar.pages.js b/cypress/e2e/pages/sidebar.pages.js index 56c32db19c..854394159e 100644 --- a/cypress/e2e/pages/sidebar.pages.js +++ b/cypress/e2e/pages/sidebar.pages.js @@ -47,11 +47,17 @@ export const sideBarSafes = { safe2short: '0x9059...7811', safe3short: '0x86Cb...2C27', } +export const sideBarSafesPendingActions = { + safe1: '0x5912f6616c84024cD1aff0D5b55bb36F5180fFdb', + safe1short: '0x5912...fFdb', +} export const testSafeHeaderDetails = ['2/2', safes.SEP_STATIC_SAFE_9_SHORT] const receiveAssetsStr = 'Receive assets' const emptyWatchListStr = 'Watch any Safe Account to keep an eye on its activity' const emptySafeListStr = "You don't have any Safe Accounts yet" const myAccountsStr = 'My accounts' +const confirmTxStr = (number) => `${number} to confirm` +export const confirmGenStr = 'to confirm' export function getImportBtn() { return cy.get(importBtn).scrollIntoView().should('be.visible') @@ -273,3 +279,17 @@ export function verifySafeGiveNameOptionExists(index) { export function checkMyAccountCounter(value) { cy.contains(myAccountsStr).should('contain', value) } + +export function checkTxToConfirm(numberOfTx) { + const str = confirmTxStr(numberOfTx) + main.verifyValuesExist(sideSafeListItem, [str]) +} + +export function verifyTxToConfirmDoesNotExist() { + main.verifyValuesDoNotExist(sideSafeListItem, [confirmGenStr]) +} + +export function checkBalanceExists() { + const balance = new RegExp(`\\s*\\d*\\.?\\d*\\s*`, 'i') + const element = cy.get(chainLogo).prev().contains(balance) +} diff --git a/cypress/e2e/regression/pending_actions.cy.js b/cypress/e2e/regression/pending_actions.cy.js deleted file mode 100644 index c7f2399954..0000000000 --- a/cypress/e2e/regression/pending_actions.cy.js +++ /dev/null @@ -1,43 +0,0 @@ -import * as constants from '../../support/constants' -import * as safe from '../pages/load_safe.pages' - -describe('Pending actions tests', () => { - before(() => { - cy.visit(constants.welcomeUrl) - // main.acceptCookies() - }) - - //TODO: Discuss test logic - - beforeEach(() => { - // Uses the previously saved local storage - // to preserve the wallet connection between tests - cy.restoreLocalStorageCache() - }) - - afterEach(() => { - cy.saveLocalStorageCache() - }) - - it.skip('should add the Safe with the pending actions', () => { - safe.openLoadSafeForm() - safe.inputAddress(constants.TEST_SAFE) - safe.clickOnNextBtn() - safe.verifyOwnersModalIsVisible() - safe.clickOnNextBtn() - safe.clickOnAddBtn() - }) - - it.skip('should display the pending actions in the Safe list sidebar', () => { - safe.openSidebar() - safe.verifyAddressInsidebar(constants.SIDEBAR_ADDRESS) - safe.verifySidebarIconNumber(1) - safe.clickOnPendingActions() - //cy.get('img[alt="E2E Wallet logo"]').next().contains('2').should('exist') - }) - - it.skip('should have the right number of queued and signable transactions', () => { - safe.verifyTransactionSectionIsVisible() - safe.verifyNumberOfTransactions(1, 1) - }) -}) diff --git a/cypress/e2e/regression/sidebar_3.cy.js b/cypress/e2e/regression/sidebar_3.cy.js index c1fca4d40f..b1ff34d2c2 100644 --- a/cypress/e2e/regression/sidebar_3.cy.js +++ b/cypress/e2e/regression/sidebar_3.cy.js @@ -5,11 +5,14 @@ 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 * as create_wallet from '../pages/create_wallet.pages.js' +import * as navigation from '../pages/navigation.page.js' +import * as owner from '../pages/owners.pages.js' let staticSafes = [] const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY const signer1 = walletCredentials.OWNER_1_PRIVATE_KEY +const signer2 = walletCredentials.OWNER_3_PRIVATE_KEY describe('Sidebar tests 3', () => { before(async () => { @@ -114,4 +117,39 @@ describe('Sidebar tests 3', () => { sideBar.openSidebar() sideBar.verifyAddedSafesExist([sideBar.sideBarSafes.safe3short]) }) + + it('Verify pending signature is displayed in sidebar for unsigned tx', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_7) + main.acceptCookies() + wallet.connectSigner(signer) + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafesPendingActions.safe1], + }) + sideBar.openSidebar() + sideBar.verifyTxToConfirmDoesNotExist() + + cy.get('body').click() + + owner.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafesPendingActions.safe1], + }) + wallet.connectSigner(signer2) + sideBar.openSidebar() + sideBar.verifyAddedSafesExist([sideBar.sideBarSafesPendingActions.safe1short]) + sideBar.checkTxToConfirm(1) + }) + + it('Verify balance exists in a tx in sidebar', () => { + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_7) + main.acceptCookies() + wallet.connectSigner(signer) + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafesPendingActions.safe1], + }) + sideBar.openSidebar() + sideBar.verifyTxToConfirmDoesNotExist() + sideBar.checkBalanceExists() + }) }) diff --git a/cypress/e2e/smoke/tx_history_filter.cy.js b/cypress/e2e/smoke/tx_history_filter.cy.js index 97cf73cc47..44d5b3dd1b 100644 --- a/cypress/e2e/smoke/tx_history_filter.cy.js +++ b/cypress/e2e/smoke/tx_history_filter.cy.js @@ -70,7 +70,7 @@ describe('[SMOKE] API Tx history filter tests', () => { }) }) - it('Verify that when the date range filter is set to only one day with no transactions, it returns no results', () => { + it('Verify that when the incoming date range filter is set to only one day with no transactions, it returns no results', () => { const params = { transactionType: txType_incoming, startDate: '2023-12-31T23:00:00.000Z', @@ -180,10 +180,10 @@ describe('[SMOKE] API Tx history filter tests', () => { }) }) - it('Verify that when the date range filter is set to only one day with no transactions, it returns no results', () => { + it('Verify that when the outgoing date range filter is set to only one day with no transactions, it returns no results', () => { const params = { transactionType: txType_outgoing, - startDate: '2023-12-31T23:00:00.000Z', + startDate: '2024-07-16T23:00:00.000Z', token_address: constants.RECIPIENT_ADDRESS, } const url = buildQueryUrl({ chainId, safeAddress, ...params }) diff --git a/cypress/fixtures/txhistory_data_data.json b/cypress/fixtures/txhistory_data_data.json index 1397385c76..8a276ccf8b 100644 --- a/cypress/fixtures/txhistory_data_data.json +++ b/cypress/fixtures/txhistory_data_data.json @@ -34,11 +34,11 @@ "receive": { "title": "Receive", "summaryTitle": "Received", - "summaryTxInfo": "< 0.00001 ETH", + "summaryTxInfo": "1,000 QTRUST", "summaryTime": "11:00 AM", - "receivedFrom": "Received 0.00000000001 ETH from:", + "receivedFrom": "Received 1,000 QTRUST from", "senderAddress": "sep:0x96D4c6fFC338912322813a77655fCC926b9A5aC5", - "transactionHash": "0x4159...3e7d", + "transactionHash": "0xd89d...9136", "transactionHashCopied": "0x415977f4e4912e22a5cabc4116f7e8f8984996e00a641dcccf8cbe1eb3db3e7d", "altImage": "Received", "altToken": "ETH" From fb5ab2b1a6008a130acbcf6568de9be23a78d654 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Fri, 5 Jul 2024 16:04:38 +0200 Subject: [PATCH 142/154] feat: add a remote config for cow server (#3917) Sometimes there are bugs in the production version of CoW. Having the option to switch to staging would allow us to test fixes for those bugs. --- src/features/swap/index.tsx | 2 ++ src/utils/chains.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index c96c61c7c6..13d42b5ed7 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -85,12 +85,14 @@ const SwapWidget = ({ sell }: Params) => { const wallet = useWallet() const { isConsentAccepted, onAccept } = useSwapConsent() const feeEnabled = useHasFeature(FEATURES.NATIVE_SWAPS_FEE_ENABLED) + const useStagingCowServer = useHasFeature(FEATURES.NATIVE_SWAPS_USE_COW_STAGING_SERVER) const [params, setParams] = useState({ appCode: 'Safe Wallet Swaps', // Name of your app (max 50 characters) width: '100%', // Width in pixels (or 100% to use all available space) height: '860px', chainId, + baseUrl: useStagingCowServer ? 'https://staging.swap.cow.fi' : 'https://swap.cow.fi', standaloneMode: false, disableToastMessages: true, disablePostedOrderConfirmationModal: true, diff --git a/src/utils/chains.ts b/src/utils/chains.ts index 6df14d92ca..ae181213cc 100644 --- a/src/utils/chains.ts +++ b/src/utils/chains.ts @@ -22,6 +22,7 @@ export enum FEATURES { SPEED_UP_TX = 'SPEED_UP_TX', SAP_BANNER = 'SAP_BANNER', NATIVE_SWAPS = 'NATIVE_SWAPS', + NATIVE_SWAPS_USE_COW_STAGING_SERVER = 'NATIVE_SWAPS_USE_COW_STAGING_SERVER', NATIVE_SWAPS_FEE_ENABLED = 'NATIVE_SWAPS_FEE_ENABLED', RELAY_NATIVE_SWAPS = 'RELAY_NATIVE_SWAPS', ZODIAC_ROLES = 'ZODIAC_ROLES', From 3e5257fe1c557e4259a1f5bb1c4871e46eb51217 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Mon, 8 Jul 2024 15:02:02 +0200 Subject: [PATCH 143/154] fix: sell token symbol was not being shown (#3920) --- src/features/swap/components/SwapTxInfo/SwapTx.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/features/swap/components/SwapTxInfo/SwapTx.tsx b/src/features/swap/components/SwapTxInfo/SwapTx.tsx index e973a29807..08f861a8d0 100644 --- a/src/features/swap/components/SwapTxInfo/SwapTx.tsx +++ b/src/features/swap/components/SwapTxInfo/SwapTx.tsx @@ -32,9 +32,14 @@ export const SwapTx = ({ info }: { info: Order }): ReactElement => { if (!isSellOrder) { from = ( - - - + <> + + + + + {sellToken.symbol} + + ) to = ( <> From ab14e14a8c0f732c311b01c821a97968fce336c8 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:12:03 +0200 Subject: [PATCH 144/154] Update hp tests (#3919) --- cypress/e2e/happypath/sendfunds_connected_wallet.cy.js | 5 ----- cypress/e2e/happypath/sendfunds_relay.cy.js | 4 ---- 2 files changed, 9 deletions(-) diff --git a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js index a7cd2d52c0..532b72ccea 100644 --- a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js +++ b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js @@ -14,9 +14,6 @@ import { contracts, abi_qtrust, abi_nft_pc2 } from '../../support/api/contracts' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' -const safeBalanceEth = 505360000000000000n -const safeBalanceEth = 505320000000000000n -const qtrustBanance = 99000000000000000025n const transferAmount = '1' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) @@ -160,7 +157,6 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi const safeTx = await apiKit.getTransaction(safeTxHashofExistingTx) await protocolKitOwner2_S3.executeTransaction(safeTx) main.verifyNonceChange(network_pref + targetSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + targetSafe, constants.tokenAbbreviation.eth, safeBalanceEth) }) }) @@ -194,7 +190,6 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi await tx.wait() main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + originatingSafe, constants.tokenAbbreviation.qtrust, qtrustBanance) }) }) }) diff --git a/cypress/e2e/happypath/sendfunds_relay.cy.js b/cypress/e2e/happypath/sendfunds_relay.cy.js index 5cb76405b9..1f4a65bb51 100644 --- a/cypress/e2e/happypath/sendfunds_relay.cy.js +++ b/cypress/e2e/happypath/sendfunds_relay.cy.js @@ -14,8 +14,6 @@ import { contracts, abi_qtrust, abi_nft_pc2 } from '../../support/api/contracts' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' -const safeBalanceEth = 405250000000000000n -const qtrustBanance = 60000000000000000000n const transferAmount = '1' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) @@ -167,7 +165,6 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 3000 const safeTx = await apiKit.getTransaction(safeTxHashofExistingTx) await protocolKitOwner2_S3.executeTransaction(safeTx) main.verifyNonceChange(network_pref + targetSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + targetSafe, constants.tokenAbbreviation.eth, safeBalanceEth) }) }) }) @@ -208,7 +205,6 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 3000 await tx.wait() main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) - main.checkTokenBalance(network_pref + originatingSafe, constants.tokenAbbreviation.qtrust, qtrustBanance) }) }) }) From 66130ce1a6cda11028ca3d3b01dbad7073f226d5 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Mon, 8 Jul 2024 20:28:45 +0200 Subject: [PATCH 145/154] fix: re-enable swaps for counterfactual safes [SW-23] (#3757) --- src/components/balances/AssetsTable/index.tsx | 4 +--- src/components/sidebar/SidebarNavigation/index.tsx | 6 ++---- src/features/counterfactual/CounterfactualForm.tsx | 4 +++- src/features/counterfactual/FirstTxFlow.tsx | 4 +--- .../counterfactual/hooks/usePendingSafeStatuses.ts | 7 ++----- src/features/counterfactual/utils.ts | 9 +++++---- 6 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/components/balances/AssetsTable/index.tsx b/src/components/balances/AssetsTable/index.tsx index dd9ddc61d9..df1f069b5d 100644 --- a/src/components/balances/AssetsTable/index.tsx +++ b/src/components/balances/AssetsTable/index.tsx @@ -18,7 +18,6 @@ import useBalances from '@/hooks/useBalances' import { useHideAssets, useVisibleAssets } from './useHideAssets' import AddFundsCTA from '@/components/common/AddFunds' import SwapButton from '@/features/swap/components/SwapButton' -import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' import { SWAP_LABELS } from '@/services/analytics/events/swaps' import SendButton from './SendButton' import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' @@ -97,8 +96,7 @@ const AssetsTable = ({ setShowHiddenAssets: (hidden: boolean) => void }): ReactElement => { const { balances, loading } = useBalances() - const isCounterfactualSafe = useIsCounterfactualSafe() - const isSwapFeatureEnabled = useIsSwapFeatureEnabled() && !isCounterfactualSafe + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() const { isAssetSelected, toggleAsset, hidingAsset, hideAsset, cancel, deselectAll, saveChanges } = useHideAssets(() => setShowHiddenAssets(false), diff --git a/src/components/sidebar/SidebarNavigation/index.tsx b/src/components/sidebar/SidebarNavigation/index.tsx index f8dd01768a..1a5632c803 100644 --- a/src/components/sidebar/SidebarNavigation/index.tsx +++ b/src/components/sidebar/SidebarNavigation/index.tsx @@ -18,7 +18,6 @@ import { useCurrentChain } from '@/hooks/useChains' import { isRouteEnabled } from '@/utils/chains' import { trackEvent } from '@/services/analytics' import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' -import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' import { GeoblockingContext } from '@/components/common/GeoblockingProvider' const getSubdirectory = (pathname: string): string => { @@ -31,18 +30,17 @@ const Navigation = (): ReactElement => { const { safe } = useSafeInfo() const currentSubdirectory = getSubdirectory(router.pathname) const queueSize = useQueuedTxsLength() - const isCounterFactualSafe = useIsCounterfactualSafe() const isBlockedCountry = useContext(GeoblockingContext) const enabledNavItems = useMemo(() => { return navItems.filter((item) => { const enabled = isRouteEnabled(item.href, chain) - if (item.href === AppRoutes.swap && (isCounterFactualSafe || isBlockedCountry)) { + if (item.href === AppRoutes.swap && isBlockedCountry) { return false } return enabled }) - }, [chain, isBlockedCountry, isCounterFactualSafe]) + }, [chain, isBlockedCountry]) const getBadge = (item: NavItem) => { // Indicate whether the current Safe needs an upgrade diff --git a/src/features/counterfactual/CounterfactualForm.tsx b/src/features/counterfactual/CounterfactualForm.tsx index 3097f5f87d..cf20d005e5 100644 --- a/src/features/counterfactual/CounterfactualForm.tsx +++ b/src/features/counterfactual/CounterfactualForm.tsx @@ -3,6 +3,7 @@ import useDeployGasLimit from '@/features/counterfactual/hooks/useDeployGasLimit import { deploySafeAndExecuteTx } from '@/features/counterfactual/utils' import useChainId from '@/hooks/useChainId' import { getTotalFeeFormatted } from '@/hooks/useGasPrice' +import useSafeInfo from '@/hooks/useSafeInfo' import useWalletCanPay from '@/hooks/useWalletCanPay' import useOnboard from '@/hooks/wallets/useOnboard' import useWallet from '@/hooks/wallets/useWallet' @@ -50,6 +51,7 @@ export const CounterfactualForm = ({ const onboard = useOnboard() const chain = useCurrentChain() const chainId = useChainId() + const { safeAddress } = useSafeInfo() // Form state const [isSubmittable, setIsSubmittable] = useState(true) @@ -83,7 +85,7 @@ export const CounterfactualForm = ({ trackEvent({ ...OVERVIEW_EVENTS.PROCEED_WITH_TX, label: TX_TYPES.activate_with_tx }) onboard && (await assertWalletChain(onboard, chainId)) - await deploySafeAndExecuteTx(txOptions, wallet, safeTx, wallet?.provider) + await deploySafeAndExecuteTx(txOptions, wallet, safeAddress, safeTx, wallet?.provider) trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.activate_with_tx }) trackEvent({ ...TX_EVENTS.EXECUTE, label: TX_TYPES.activate_with_tx }) diff --git a/src/features/counterfactual/FirstTxFlow.tsx b/src/features/counterfactual/FirstTxFlow.tsx index 8e9d386074..a30fa14218 100644 --- a/src/features/counterfactual/FirstTxFlow.tsx +++ b/src/features/counterfactual/FirstTxFlow.tsx @@ -18,7 +18,6 @@ import RecoveryPlus from '@/public/images/common/recovery-plus.svg' import SwapIcon from '@/public/images/common/swap.svg' import SafeLogo from '@/public/images/logo-no-text.svg' import HandymanOutlinedIcon from '@mui/icons-material/HandymanOutlined' -import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' import useIsSwapFeatureEnabled from '../swap/hooks/useIsSwapFeatureEnabled' const FirstTxFlow = ({ open, onClose }: { open: boolean; onClose: () => void }) => { @@ -27,8 +26,7 @@ const FirstTxFlow = ({ open, onClose }: { open: boolean; onClose: () => void }) const { setTxFlow } = useContext(TxModalContext) const supportsRecovery = useIsRecoverySupported() const [recovery] = useRecovery() - const isCounterfactualSafe = useIsCounterfactualSafe() - const isSwapFeatureEnabled = useIsSwapFeatureEnabled() && !isCounterfactualSafe + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() const handleClick = (onClick: () => void) => { onClose() diff --git a/src/features/counterfactual/hooks/usePendingSafeStatuses.ts b/src/features/counterfactual/hooks/usePendingSafeStatuses.ts index 208a8f0cea..d9f6077734 100644 --- a/src/features/counterfactual/hooks/usePendingSafeStatuses.ts +++ b/src/features/counterfactual/hooks/usePendingSafeStatuses.ts @@ -16,7 +16,7 @@ import useSafeInfo from '@/hooks/useSafeInfo' import { isSmartContract, useWeb3ReadOnly } from '@/hooks/wallets/web3' import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' import { useAppDispatch, useAppSelector } from '@/store' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef } from 'react' export const safeCreationPendingStatuses: Partial> = { [SafeCreationEvent.PROCESSING]: PendingSafeStatus.PROCESSING, @@ -76,9 +76,8 @@ const usePendingSafeMonitor = (): void => { } const usePendingSafeStatus = (): void => { - const [safeAddress, setSafeAddress] = useState('') const dispatch = useAppDispatch() - const { safe } = useSafeInfo() + const { safe, safeAddress } = useSafeInfo() const chainId = useChainId() const provider = useWeb3ReadOnly() @@ -107,8 +106,6 @@ const usePendingSafeStatus = (): void => { useEffect(() => { const unsubFns = Object.entries(safeCreationPendingStatuses).map(([event, status]) => safeCreationSubscribe(event as SafeCreationEvent, async (detail) => { - setSafeAddress(detail.safeAddress) - if (event === SafeCreationEvent.SUCCESS) { // TODO: Possible to add a label with_tx, without_tx? trackEvent(CREATE_SAFE_EVENTS.ACTIVATED_SAFE) diff --git a/src/features/counterfactual/utils.ts b/src/features/counterfactual/utils.ts index 8b33717818..8b3a16d6d6 100644 --- a/src/features/counterfactual/utils.ts +++ b/src/features/counterfactual/utils.ts @@ -49,6 +49,7 @@ export const dispatchTxExecutionAndDeploySafe = async ( safeTx: SafeTransaction, txOptions: TransactionOptions, provider: Eip1193Provider, + safeAddress: string, ) => { const sdkUnchecked = await getUncheckedSafeSDK(provider) const eventParams = { groupKey: CF_TX_GROUP_KEY } @@ -68,12 +69,11 @@ export const dispatchTxExecutionAndDeploySafe = async ( // @ts-ignore TODO: Check why TransactionResponse type doesn't work result = await signer.sendTransaction({ ...deploymentTx, gasLimit: gas }) } catch (error) { - safeCreationDispatch(SafeCreationEvent.FAILED, { ...eventParams, error: asError(error), safeAddress: '' }) + safeCreationDispatch(SafeCreationEvent.FAILED, { ...eventParams, error: asError(error), safeAddress }) throw error } - // TODO: Probably need to pass the actual safe address - safeCreationDispatch(SafeCreationEvent.PROCESSING, { ...eventParams, txHash: result!.hash, safeAddress: '' }) + safeCreationDispatch(SafeCreationEvent.PROCESSING, { ...eventParams, txHash: result!.hash, safeAddress }) return result!.hash } @@ -81,6 +81,7 @@ export const dispatchTxExecutionAndDeploySafe = async ( export const deploySafeAndExecuteTx = async ( txOptions: TransactionOptions, wallet: ConnectedWallet | null, + safeAddress: string, safeTx?: SafeTransaction, provider?: Eip1193Provider, ) => { @@ -88,7 +89,7 @@ export const deploySafeAndExecuteTx = async ( assertWallet(wallet) assertProvider(provider) - return dispatchTxExecutionAndDeploySafe(safeTx, txOptions, provider) + return dispatchTxExecutionAndDeploySafe(safeTx, txOptions, provider, safeAddress) } export const { getStore: getNativeBalance, setStore: setNativeBalance } = new ExternalStore(0n) From 84050cc88bf353d06f5b794bf49ffa81e85fed9a Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:20:41 +0200 Subject: [PATCH 146/154] Order details tests (#3922) --- cypress/e2e/pages/swaps.pages.js | 30 +++++++++++ cypress/e2e/regression/swaps.cy.js | 52 +++++++++++++++++++ cypress/fixtures/swaps_data.json | 5 ++ .../OrderFeeConfirmationView.tsx | 2 +- .../SwapOrderConfirmationView/index.tsx | 14 ++--- 5 files changed, 95 insertions(+), 8 deletions(-) diff --git a/cypress/e2e/pages/swaps.pages.js b/cypress/e2e/pages/swaps.pages.js index d0074961fe..efb734acaf 100644 --- a/cypress/e2e/pages/swaps.pages.js +++ b/cypress/e2e/pages/swaps.pages.js @@ -14,7 +14,15 @@ export const customRecipient = 'div[id="recipient"]' const recipientToggle = 'button[id="toggle-recipient-mode-button"]' const orderTypeMenuItem = 'div[class*="MenuItem"]' const explorerBtn = '[data-testid="explorer-btn"]' +const limitPriceFld = '[data-testid="limit-price"]' +const expiryFld = '[data-testid="expiry"]' +const slippageFld = '[data-testid="slippage"]' +const orderIDFld = '[data-testid="order-id"]' +const widgetFeeFld = '[data-testid="widget-fee"]' +const interactWithFld = '[data-testid="interact-wth"]' +const recipientAlert = '[data-testid="recipient-alert"]' const confirmSwapStr = 'Confirm Swap' + const swapBtnStr = /Confirm Swap|Swap|Confirm (Approve COW and Swap)|Confirm/ const orderSubmittedStr = 'Order Submitted' const orderIdStr = 'Order ID' @@ -22,6 +30,7 @@ const cowOrdersUrl = 'https://explorer.cow.fi/orders' export const blockedAddress = '0x8576acc5c05d6ce88f4e49bf65bdf0c62f91353c' export const blockedAddressStr = 'Blocked address' + const swapStr = 'Swap' const limitStr = 'Limit' @@ -219,6 +228,14 @@ export function createRegex(pattern, placeholder) { return new RegExp(pattern_, 'i') } +export function getOrderID() { + return new RegExp(`[a-fA-F0-9]{8}`, 'i') +} + +export function getWidgetFee() { + return new RegExp(`\\s*\\d*\\.?\\d+\\s*%\\s*`, 'i') +} + export function checkTokenOrder(regexPattern, option) { cy.get(create_tx.txRowTitle) .filter(`:contains("${option}")`) @@ -241,3 +258,16 @@ export function verifyOrderIDUrl() { cy.get(explorerBtn).should('have.attr', 'href').and('include', cowOrdersUrl) }) } + +export function verifyOrderDetails(limitPrice, expiry, slippage, interactWith, oderID, widgetFee) { + cy.get(limitPriceFld).contains(limitPrice) + cy.get(expiryFld).contains(expiry) + cy.get(slippageFld).contains(slippage) + cy.get(orderIDFld).contains(oderID) + cy.get(widgetFeeFld).contains(widgetFee) + cy.get(interactWithFld).contains(interactWith) +} + +export function verifyRecipientAlertIsDisplayed() { + main.verifyElementsIsVisible([recipientAlert]) +} diff --git a/cypress/e2e/regression/swaps.cy.js b/cypress/e2e/regression/swaps.cy.js index f90e564315..c8994fdc4a 100644 --- a/cypress/e2e/regression/swaps.cy.js +++ b/cypress/e2e/regression/swaps.cy.js @@ -6,13 +6,17 @@ import * as create_tx from '../pages/create_tx.pages.js' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as owner from '../pages/owners.pages' import * as wallet from '../../support/utils/wallet.js' +import * as swaps_data from '../../fixtures/swaps_data.json' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY +const signer2 = walletCredentials.OWNER_3_WALLET_ADDRESS let staticSafes = [] let iframeSelector +const swapOrder = swaps_data.type.orderDetails + describe('Swaps tests', () => { before(async () => { staticSafes = await getSafes(CATEGORIES.static) @@ -120,4 +124,52 @@ describe('Swaps tests', () => { }) }) }) + + it('Verify order deails are displayed in swap confirmation', { defaultCommandTimeout: 30000 }, () => { + const limitPrice = swaps.createRegex(swapOrder.DAIeqCOW, 'COW') + const widgetFee = swaps.getWidgetFee() + const orderID = swaps.getOrderID() + const slippage = swaps.getWidgetFee() + + swaps.acceptLegalDisclaimer() + cy.wait(4000) + main.getIframeBody(iframeSelector).within(() => { + swaps.clickOnSettingsBtn() + swaps.setSlippage('0.30') + swaps.setExpiry('2') + swaps.clickOnSettingsBtn() + swaps.setInputValue(4) + swaps.checkSwapBtnIsVisible() + swaps.isInputGreaterZero(swaps.outputurrencyInput).then((isGreaterThanZero) => { + cy.wrap(isGreaterThanZero).should('be.true') + }) + swaps.clickOnExceeFeeChkbox() + swaps.clickOnSwapBtn() + swaps.clickOnSwapBtn() + }) + + swaps.verifyOrderDetails(limitPrice, swapOrder.expiry2Mins, slippage, swapOrder.interactWith, orderID, widgetFee) + }) + + it( + 'Verify recipient address alert is displayed in order details if the recipient is not owner of the order', + { defaultCommandTimeout: 30000 }, + () => { + const limitPrice = swaps.createRegex(swapOrder.DAIeqCOW, 'COW') + const widgetFee = swaps.getWidgetFee() + const orderID = swaps.getOrderID() + + swaps.acceptLegalDisclaimer() + cy.wait(4000) + main.getIframeBody(iframeSelector).within(() => { + swaps.setInputValue(4) + swaps.checkSwapBtnIsVisible() + swaps.enterRecipient(signer2) + swaps.clickOnExceeFeeChkbox() + swaps.clickOnSwapBtn() + swaps.clickOnSwapBtn() + }) + swaps.verifyRecipientAlertIsDisplayed() + }, + ) }) diff --git a/cypress/fixtures/swaps_data.json b/cypress/fixtures/swaps_data.json index 5ca3844ecb..10418befb0 100644 --- a/cypress/fixtures/swaps_data.json +++ b/cypress/fixtures/swaps_data.json @@ -6,6 +6,11 @@ "oneOfOne": "1 out of 1", "title": "Swap order" }, + "orderDetails": { + "expiry2Mins": "in 2 minutes", + "interactWith": "GPv2Settlement", + "DAIeqCOW": "1 DAI = COW" + }, "history": { "buyOrder": "Buy order", "buy": "Buy", diff --git a/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx index dd283a1c3c..083e5c1378 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx @@ -32,7 +32,7 @@ export const OrderFeeConfirmationView = ({ ) return ( - + {Number(bps) / 100} % ) diff --git a/src/features/swap/components/SwapOrderConfirmationView/index.tsx b/src/features/swap/components/SwapOrderConfirmationView/index.tsx index 515763aad8..1d52c70d22 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/index.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/index.tsx @@ -61,12 +61,12 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd /> , - + 1 {buyToken.symbol} = {formatAmount(limitPrice)} {sellToken.symbol} , compareAsc(now, expires) !== 1 ? ( - + {formatTimeInWords(validUntil * 1000)} @@ -80,30 +80,30 @@ export const SwapOrderConfirmationView = ({ order, settlementContract }: SwapOrd ), orderClass !== 'limit' ? ( - + {slippage}% ) : ( ), !isTwapOrder ? ( - + ) : ( <> ), , - + , receiver && owner !== receiver ? ( <> - +
    - + Order recipient address differs from order owner. From b7d81204996e96c4fcd44e0dcdd5532482f08815 Mon Sep 17 00:00:00 2001 From: James Mealy Date: Tue, 9 Jul 2024 19:10:25 +0200 Subject: [PATCH 147/154] chore: update terms page with corrections (#3916) --- src/pages/terms.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/terms.tsx b/src/pages/terms.tsx index 050fe45dc5..1b3b3df005 100644 --- a/src/pages/terms.tsx +++ b/src/pages/terms.tsx @@ -12,7 +12,7 @@ const SafeTerms = () => ( Terms and Conditions -

    Last updated: April 2024.

    +

    Last updated: July 2024.

    1. What is the scope of the Terms?

      @@ -342,8 +342,8 @@ const SafeTerms = () => (

      12. Are we responsible for third-party content and services?

      1. - You may view, have access to, and use third-party content and services, for example widget integrations within - the Safe App (“Third-Party Features”). You view, access or use Third-Party Features at your own election. Your + You may view, have access to, and use third-party content and services, for example widget integrations, within + the Safe App (“Third-Party Features”). You view, access, or use Third-Party Features at your own election. Your reliance on Third-Party Features is subject to separate terms and conditions set forth by the applicable third party content and/or service provider (“Third-Party Terms”). Third-Party Terms may, amongst other things,
          @@ -488,8 +488,8 @@ const SafeTerms = () => (
          1. If the Safe App or Services are provided to the User free of charge (please note, in this context, that any - service, network and/or transaction fees may be charged by third parties via the Blockchain and not necessarily - by us), CC shall be liable only in cases of intent, gross negligence or if CC has fraudulently concealed a + service, network, and/or transaction fees may be charged by third parties via the Blockchain and not necessarily + by us), CC shall be liable only in cases of intent, gross negligence, or if CC has fraudulently concealed a possible material or legal defect of the Safe App or Services. If the Safe App or Services are not provided to the User free of charge, CC shall be liable only (i) in cases pursuant to Clause 17.1 as well as (ii) in cases of simple negligence for damages resulting from the breach of an essential contractual duty, a duty, the From eb7b007d6c38d2da29b4a753188189072074310e Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Wed, 10 Jul 2024 08:16:27 +0200 Subject: [PATCH 148/154] Fix: wording changes (#3921) * fix: last update of t&cs, reword the legal disclaimer title --------- Co-authored-by: Daniel Dimitrov --- src/components/common/CookieAndTermBanner/index.tsx | 6 +++--- .../swap/components/LegalDisclaimer/index.tsx | 2 +- src/features/swap/index.tsx | 11 ++--------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/components/common/CookieAndTermBanner/index.tsx b/src/components/common/CookieAndTermBanner/index.tsx index aea7da3c34..559b22343b 100644 --- a/src/components/common/CookieAndTermBanner/index.tsx +++ b/src/components/common/CookieAndTermBanner/index.tsx @@ -75,9 +75,9 @@ export const CookieAndTermBanner = ({ By browsing this page, you accept our{' '} - Terms & Conditions 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 + Terms & Conditions (last updated July 2024) 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/features/swap/components/LegalDisclaimer/index.tsx b/src/features/swap/components/LegalDisclaimer/index.tsx index 8c4e7393a4..a324af6889 100644 --- a/src/features/swap/components/LegalDisclaimer/index.tsx +++ b/src/features/swap/components/LegalDisclaimer/index.tsx @@ -8,7 +8,7 @@ const LegalDisclaimerContent = () => (
            - You are now accessing a third party widget! + You are now accessing a third party widget. diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 13d42b5ed7..295da8ec66 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -137,7 +137,7 @@ const SwapWidget = ({ sell }: Params) => { content: { feeLabel: 'Widget Fee', feeTooltipMarkdown: - 'The [tiered widget fee](https://help.safe.global/en/articles/178530-how-does-the-widget-fee-work-for-native-swaps) incurred here and charged by CoW DAO for the operation of the CoW Swap Widget is automatically calculated into this quote. It will contribute to a license fee that supports the Safe Community. Neither the Safe Ecosystem Foundation nor Safe (Wallet) operate the CoW Swap Widget and/or CoW Swap.', + 'The [tiered widget fee](https://help.safe.global/en/articles/178530-how-does-the-widget-fee-work-for-native-swaps) incurred here is charged by CoW Protocol for the operation of this widget. The fee is automatically calculated into this quote. Part of the fee will contribute to a license fee that supports the Safe Community. Neither the Safe Ecosystem Foundation nor Safe{Wallet} operate the CoW Swap Widget and/or CoW Swap', }, }) @@ -287,14 +287,7 @@ const SwapWidget = ({ sell }: Params) => { } if (!isConsentAccepted) { - return ( - } - onAccept={onAccept} - buttonText="Continue" - /> - ) + return } onAccept={onAccept} buttonText="Continue" /> } if (!isSwapFeatureEnabled) { From 51c068e605ea2de87a0771978657ee15cfd38183 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Wed, 10 Jul 2024 10:34:29 +0200 Subject: [PATCH 149/154] fix: tooltip in swap order view (#3923) --- .../SwapOrderConfirmationView/OrderFeeConfirmationView.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx index f823509f9f..40254c527d 100644 --- a/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx +++ b/src/features/swap/components/SwapOrderConfirmationView/OrderFeeConfirmationView.tsx @@ -18,9 +18,10 @@ export const OrderFeeConfirmationView = ({ order }: { order: Pick - The tiered widget fees incurred here will contribute to a license fee that supports the Safe community. - Neither Safe Ecosystem Foundation nor {`Safe{Wallet}`} - operate the CoW Swap Widget and/or CoW Swap.{` `} + The tiered widget fee incurred here is charged by CoW Protocol for the operation of this widget. The fee is + automatically calculated into this quote. Part of the fee will contribute to a license fee that supports the + Safe Community. Neither the Safe Ecosystem Foundation nor {`Safe{Wallet}`} operate the CoW Swap Widget + and/or CoW Swap. Learn more From 0cd1d890640a8e7e3bd0520c74066a7f623f2121 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Wed, 10 Jul 2024 14:07:07 +0200 Subject: [PATCH 150/154] fix: usdt missing from stablecoins array on arbitrum (#3927) --- src/features/swap/helpers/data/stablecoins.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/features/swap/helpers/data/stablecoins.ts b/src/features/swap/helpers/data/stablecoins.ts index 818ef01d31..b818b11d60 100644 --- a/src/features/swap/helpers/data/stablecoins.ts +++ b/src/features/swap/helpers/data/stablecoins.ts @@ -91,6 +91,11 @@ export const stableCoinAddresses: { symbol: 'usdc', chains: ['arbitrum-one'], }, + '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9': { + name: 'USDT', + symbol: 'usdt', + chains: ['arbitrum-one'], + }, '0x6b175474e89094c44da98b954eedeac495271d0f': { name: 'Dai', symbol: 'dai', From e2e13ffd4684fb0a9ba50b65bd5e80e00852a9ec Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Wed, 10 Jul 2024 15:07:56 +0200 Subject: [PATCH 151/154] chore: bump package.json (#3930) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f36b56772..12f287beca 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.39.0", + "version": "1.39.1", "type": "module", "scripts": { "dev": "next dev", From 480b9af4c6542b2d6dbd32c8b65feaebcf6908a0 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:12:28 +0200 Subject: [PATCH 152/154] Fix: valueDecoded must be an array (#3940) --- src/components/tx/SignOrExecuteForm/index.tsx | 20 +++++++++---------- src/features/swap/helpers/utils.ts | 18 +++++++++-------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/components/tx/SignOrExecuteForm/index.tsx b/src/components/tx/SignOrExecuteForm/index.tsx index ef3c4377c9..84e2397e2e 100644 --- a/src/components/tx/SignOrExecuteForm/index.tsx +++ b/src/components/tx/SignOrExecuteForm/index.tsx @@ -113,17 +113,17 @@ export const SignOrExecuteForm = ({ Error parsing data
            }> - - + + {!isCounterfactualSafe && } diff --git a/src/features/swap/helpers/utils.ts b/src/features/swap/helpers/utils.ts index 83d2c7b791..318fad0b79 100644 --- a/src/features/swap/helpers/utils.ts +++ b/src/features/swap/helpers/utils.ts @@ -196,14 +196,16 @@ export const UiOrderTypeToOrderType = (orderType: UiOrderType): TradeType => { export const isSettingTwapFallbackHandler = (decodedData: DecodedDataResponse | undefined) => { return ( - decodedData?.parameters?.some((item) => - item.valueDecoded?.some( - (decoded) => - decoded.dataDecoded?.method === 'setFallbackHandler' && - decoded.dataDecoded.parameters?.some( - (parameter) => parameter.name === 'handler' && parameter.value === TWAP_FALLBACK_HANDLER, - ), - ), + decodedData?.parameters?.some( + (item) => + Array.isArray(item?.valueDecoded) && + item.valueDecoded.some( + (decoded) => + decoded.dataDecoded?.method === 'setFallbackHandler' && + decoded.dataDecoded.parameters?.some( + (parameter) => parameter.name === 'handler' && parameter.value === TWAP_FALLBACK_HANDLER, + ), + ), ) || false ) } From 283ec49d7d1a406a7711652d3286c71abbe9bac4 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:37:40 +0200 Subject: [PATCH 153/154] Fix: prompt -> confirm (#3939) * Fix: prompt -> confirm * Update unit test --- .../safe-wallet-provider/useSafeWalletProvider.test.tsx | 2 +- src/services/safe-wallet-provider/useSafeWalletProvider.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx index 13043011f7..2a60e2df28 100644 --- a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx +++ b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx @@ -300,7 +300,7 @@ describe('useSafeWalletProvider', () => { } as unknown as router.NextRouter) // @ts-expect-error - auto accept prompt - jest.spyOn(window, 'prompt').mockReturnValue(true) + jest.spyOn(window, 'confirm').mockReturnValue(true) const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000'), { initialReduxState: { diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.tsx index cd70840652..6bb82c0934 100644 --- a/src/services/safe-wallet-provider/useSafeWalletProvider.tsx +++ b/src/services/safe-wallet-provider/useSafeWalletProvider.tsx @@ -170,7 +170,7 @@ export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK | throw new Error(`Chain ${chainId} not supported`) } - if (prompt(`${appInfo.name} wants to switch to ${cfg.shortName}. Do you want to continue?`)) { + if (confirm(`${appInfo.name} wants to switch to ${cfg.shortName}. Do you want to continue?`)) { router.push({ pathname: AppRoutes.index, query: { From ccdcfb2ff996f196a9c4c4a921f9fd1c0d38474c Mon Sep 17 00:00:00 2001 From: katspaugh Date: Fri, 12 Jul 2024 14:07:31 +0200 Subject: [PATCH 154/154] 1.39.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 12f287beca..0a6dc2bf06 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.39.1", + "version": "1.39.2", "type": "module", "scripts": { "dev": "next dev",