diff --git a/.env.custom b/.env.custom index b35cf9192..ede540940 100644 --- a/.env.custom +++ b/.env.custom @@ -1 +1 @@ -APPLICATION_VERSION=1.36.4 \ No newline at end of file +APPLICATION_VERSION=1.44.3 \ No newline at end of file diff --git a/.env.example b/.env.example index ab53f2b04..b4c47be00 100644 --- a/.env.example +++ b/.env.example @@ -38,10 +38,15 @@ NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION= NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING= NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING= -# Redefine -NEXT_PUBLIC_REDEFINE_API= +# Blockaid +NEXT_PUBLIC_BLOCKAID_CLIENT_ID # Social Login NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING= NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION= +# Cypress wallet private keys +CYPRESS_WALLET_CREDENTIALS= + +# Beamer keys for e2e tests +BEAMER_DATA_E2E= \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index d3c2987cb..a8dc812ac 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,11 +1,9 @@ { - "extends": [ - "next", - "prettier", - "plugin:prettier/recommended", - "plugin:storybook/recommended" - ], + "extends": ["next", "prettier", "plugin:prettier/recommended", "plugin:storybook/recommended"], "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": ["./tsconfig.json"] + }, "rules": { "@next/next/no-img-element": "off", "@next/next/google-font-display": "off", @@ -13,7 +11,9 @@ "@next/next/no-page-custom-font": "off", "unused-imports/no-unused-imports-ts": "off", "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/await-thenable": "error", "no-constant-condition": "warn", + "no-unused-vars": ["off", { "varsIgnorePattern": "^_" }], "react-hooks/exhaustive-deps": [ "warn", { @@ -25,14 +25,6 @@ "jsx-quotes": ["error", "prefer-double"], "react/jsx-curly-brace-presence": ["off", { "props": "never", "children": "never" }] }, - "ignorePatterns": [ - "node_modules/", - ".next/", - ".github/" - ], - "plugins": [ - "unused-imports", - "@typescript-eslint", - "no-only-tests" - ] + "ignorePatterns": ["node_modules/", ".next/", ".github/", "cypress/", "src/types/contracts/"], + "plugins": ["unused-imports", "@typescript-eslint", "no-only-tests"] } diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..4bae3ef5f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'weekly' + + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index fd3d7998f..ba6ad94d7 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -51,7 +51,7 @@ runs: NEXT_PUBLIC_SAFE_RELAY_SERVICE_URL_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_PRODUCTION }} NEXT_PUBLIC_SAFE_RELAY_SERVICE_URL_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING }} NEXT_PUBLIC_IS_OFFICIAL_HOST: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_IS_OFFICIAL_HOST }} - NEXT_PUBLIC_REDEFINE_API: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_REDEFINE_API }} + NEXT_PUBLIC_BLOCKAID_CLIENT_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_BLOCKAID_CLIENT_ID }} NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING }} NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION }} NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION }} @@ -59,3 +59,4 @@ runs: NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION }} NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING }} NEXT_PUBLIC_SPINDL_SDK_KEY: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SPINDL_SDK_KEY }} + NEXT_PUBLIC_ECOSYSTEM_ID_ADDRESS: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_ECOSYSTEM_ID_ADDRESS }} diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 26ffce42c..f578a7653 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -13,7 +13,7 @@ jobs: - name: 'CLA Assistant' if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' # Beta Release - uses: contributor-assistant/github-action@v2.2.0 + uses: contributor-assistant/github-action@v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # the below token should have repo scope and must be manually added by you in the repository's secret @@ -24,7 +24,7 @@ jobs: # branch should not be protected branch: 'main' # user names of users allowed to contribute without CLA - allowlist: lukasschor,rmeissner,germartinez,Uxio0,dasanra,francovenica,tschubotz,luarx,DaniSomoza,iamacook,yagopv,usame-algan,schmanu,DiogoSoaress,JagoFigueroa,fmrsabino,mike10ca,jmealy,compojoom,TanyaEfremova,bot* + allowlist: clovisdasilvaneto,lukasschor,rmeissner,germartinez,Uxio0,dasanra,francovenica,tschubotz,luarx,DaniSomoza,iamacook,yagopv,usame-algan,schmanu,DiogoSoaress,JagoFigueroa,fmrsabino,mike10ca,jmealy,compojoom,TanyaEfremova,bot* # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken # enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) diff --git a/.github/workflows/cypress/action.yml b/.github/workflows/cypress/action.yml index bcccd875b..c549e1c5f 100644 --- a/.github/workflows/cypress/action.yml +++ b/.github/workflows/cypress/action.yml @@ -23,6 +23,10 @@ inputs: description: 'Cypress cloud record key' required: false + tag: + description: 'Cypress cloud tag key' + required: false + runs: using: 'composite' steps: @@ -34,6 +38,10 @@ runs: curl -O 'https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb' sudo apt-get install ./google-chrome-stable_current_amd64.deb + - name: Install Cypress 13.13.1 + shell: bash + run: npm install cypress@13.13.1 --legacy-peer-deps + - uses: ./.github/workflows/build with: secrets: ${{ inputs.secrets }} @@ -43,16 +51,18 @@ runs: shell: bash run: yarn serve & - - uses: cypress-io/github-action@v4 + - uses: cypress-io/github-action@v6 with: spec: ${{ inputs.spec }} group: ${{ inputs.group }} parallel: true browser: chrome record: true + tag: ${{ inputs.tag }} config: baseUrl=http://localhost:8080 env: CYPRESS_RECORD_KEY: ${{ inputs.record_key || fromJSON(inputs.secrets).CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).GITHUB_TOKEN }} CYPRESS_PROJECT_ID: ${{ inputs.project_id }} CYPRESS_WALLET_CREDENTIALS: ${{ fromJSON(inputs.secrets).CYPRESS_WALLET_CREDENTIALS }} + BEAMER_DATA_E2E: ${{ fromJSON(inputs.secrets).BEAMER_DATA_E2E }} diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index a56a220e1..e6ef646e0 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -43,10 +43,10 @@ jobs: secrets: ${{ toJSON(secrets) }} if: startsWith(github.ref, 'refs/heads/main') - - uses: ./.github/workflows/build-storybook + #- uses: ./.github/workflows/build-storybook - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v3 + uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE }} aws-region: ${{ secrets.AWS_REGION }} diff --git a/.github/workflows/deploy-dockerhub.yml b/.github/workflows/deploy-dockerhub.yml index a565ca877..3559ae754 100644 --- a/.github/workflows/deploy-dockerhub.yml +++ b/.github/workflows/deploy-dockerhub.yml @@ -27,7 +27,7 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - name: Deploy Main if: github.ref == 'refs/heads/main' - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: push: true tags: safeglobal/safe-wallet-web:staging @@ -38,7 +38,7 @@ jobs: cache-to: type=gha,mode=max - name: Deploy Develop if: github.ref == 'refs/heads/dev' - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: push: true tags: safeglobal/safe-wallet-web:dev @@ -49,7 +49,7 @@ jobs: cache-to: type=gha,mode=max - name: Deploy Tag if: (github.event_name == 'release' && github.event.action == 'released') - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: push: true tags: | diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 84a0e6ba6..32a2f2ff6 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -33,7 +33,7 @@ jobs: run: sha256sum "$ARCHIVE_NAME".tar.gz > ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v3 + uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE }} aws-region: ${{ secrets.AWS_REGION }} diff --git a/.github/workflows/e2e-hp-ondemand.yml b/.github/workflows/e2e-hp-ondemand.yml index e898380e1..bd1f2e95a 100644 --- a/.github/workflows/e2e-hp-ondemand.yml +++ b/.github/workflows/e2e-hp-ondemand.yml @@ -12,6 +12,7 @@ concurrency: jobs: e2e: runs-on: ubuntu-20.04 + timeout-minutes: 60 name: Cypress Happy path on demand tests strategy: fail-fast: false @@ -26,10 +27,11 @@ jobs: spec: | cypress/e2e/happypath/*.cy.js group: 'Happy path on demand tests' + tag: 'happypath' - name: Python setup if: always() - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.x' diff --git a/.github/workflows/e2e-ondemand.yml b/.github/workflows/e2e-ondemand.yml index 906195d4a..704f42f86 100644 --- a/.github/workflows/e2e-ondemand.yml +++ b/.github/workflows/e2e-ondemand.yml @@ -12,6 +12,7 @@ concurrency: jobs: e2e: runs-on: ubuntu-20.04 + timeout-minutes: 40 name: Cypress Regression on demand tests strategy: fail-fast: false @@ -27,11 +28,13 @@ jobs: cypress/e2e/regression/*.cy.js cypress/e2e/safe-apps/*.cy.js cypress/e2e/smoke/*.cy.js + cypress/e2e/prodhealthcheck/*.cy.js group: 'Regression on demand tests' + tag: 'regression' - name: Python setup if: always() - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.x' @@ -49,7 +52,7 @@ jobs: if: always() run: | pip install trcli - trcli -y \ + if ! trcli -y \ -h https://gno.testrail.io/ \ --project "Safe- Web App" \ --username ${{ secrets.TESTRAIL_USERNAME }} \ @@ -57,4 +60,6 @@ jobs: parse_junit \ --title "Automated Tests, branch: ${GITHUB_REF_NAME}" \ --run-description ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} \ - -f "reports/junit-report.xml" + -f "reports/junit-report.xml"; then + echo -e "\e[41;32mTestRail upload failed. Pipeline will continue, please check the upload process.\e[0m" + fi diff --git a/.github/workflows/e2e-prod-ondemand.yml b/.github/workflows/e2e-prod-ondemand.yml new file mode 100644 index 000000000..76fd8c5db --- /dev/null +++ b/.github/workflows/e2e-prod-ondemand.yml @@ -0,0 +1,59 @@ +name: Production health check tests + +on: + workflow_dispatch: + schedule: + - cron: '0 4 * * 1-5' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + runs-on: ubuntu-20.04 + name: Cypress production health check tests + strategy: + fail-fast: false + matrix: + containers: [1] + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/workflows/cypress + with: + secrets: ${{ toJSON(secrets) }} + spec: | + cypress/e2e/prodhealthcheck/*.cy.js + group: 'Production health check tests' + tag: 'production' + + - name: Python setup + if: always() + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install junitparser + if: always() + run: | + pip install junitparser + + - name: Merge JUnit reports for TestRail + if: always() + run: | + junitparser merge --suite-name "Root Suite" --glob "reports/junit-*" "reports/junit-report.xml" + + - name: TestRail CLI upload results + if: always() + run: | + pip install trcli + trcli -y \ + -h https://gno.testrail.io/ \ + --project "Safe- Web App" \ + --username ${{ secrets.TESTRAIL_USERNAME }} \ + --password ${{ secrets.TESTRAIL_PASSWORD }} \ + parse_junit \ + --title "Automated Tests, branch: ${GITHUB_REF_NAME}" \ + --run-description ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} \ + -f "reports/junit-report.xml" diff --git a/.github/workflows/e2e-regression.yml b/.github/workflows/e2e-regression.yml index 104147064..e8dd34ffd 100644 --- a/.github/workflows/e2e-regression.yml +++ b/.github/workflows/e2e-regression.yml @@ -26,3 +26,4 @@ jobs: secrets: ${{ toJSON(secrets) }} spec: cypress/e2e/**/*.cy.js group: 'Regression tests' + tag: 'regression' diff --git a/.github/workflows/e2e-safe-apps.yml b/.github/workflows/e2e-safe-apps.yml index 2843bdfb1..88c0d5d32 100644 --- a/.github/workflows/e2e-safe-apps.yml +++ b/.github/workflows/e2e-safe-apps.yml @@ -26,4 +26,4 @@ jobs: group: 'Safe Apps tests' project_id: okn21k record_key: ${{ secrets.CYPRESS_SAFE_APPS_RECORD_KEY }} - + tag: 'safeapps' diff --git a/.github/workflows/e2e-smoke.yml b/.github/workflows/e2e-smoke.yml index e9cb5a6ab..cb7abeabb 100644 --- a/.github/workflows/e2e-smoke.yml +++ b/.github/workflows/e2e-smoke.yml @@ -10,6 +10,7 @@ concurrency: jobs: e2e: runs-on: ubuntu-20.04 + timeout-minutes: 30 name: Cypress Smoke tests strategy: @@ -25,3 +26,4 @@ jobs: secrets: ${{ toJSON(secrets) }} spec: cypress/e2e/smoke/*.cy.js group: 'Smoke tests' + tag: 'smoke' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1f43f7b41..bdbe5727f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,12 +7,21 @@ concurrency: jobs: eslint: + permissions: + checks: write + pull-requests: read + statuses: write + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/workflows/yarn - - uses: Maggi64/eslint-plus-action@master + - uses: CatChen/eslint-suggestion-action@v2 with: - npmInstall: false + request-changes: true # optional + fail-check: true # optional + github-token: ${{ secrets.GITHUB_TOKEN }} # optional + directory: './' # optional + targets: 'src' # optional diff --git a/.github/workflows/nextjs-bundle-analysis.yml b/.github/workflows/nextjs-bundle-analysis.yml index 1e3ad27f2..49d08b009 100644 --- a/.github/workflows/nextjs-bundle-analysis.yml +++ b/.github/workflows/nextjs-bundle-analysis.yml @@ -32,7 +32,7 @@ jobs: run: npx -p nextjs-bundle-analysis report - name: Upload bundle - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bundle path: .next/analyze/__bundle_analysis.json diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index 3821d33ed..d389a5286 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -28,7 +28,7 @@ jobs: - name: GitHub release if: steps.version.outputs.version - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 id: create_release with: draft: true diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c05edb116..54aed4878 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -12,6 +12,11 @@ concurrency: jobs: test: + permissions: + contents: read + checks: write + pull-requests: write + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -19,7 +24,7 @@ jobs: - uses: ./.github/workflows/yarn - name: Annotations and coverage report - uses: ArtiomTr/jest-coverage-report-action@v2 + uses: ArtiomTr/jest-coverage-report-action@v2.3.1 with: skip-step: install annotations: failed-tests diff --git a/README.md b/README.md index 9a2b59261..97e85fa55 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ ![GitHub package.json version (branch)](https://img.shields.io/github/package-json/v/safe-global/safe-wallet-web) [![GitPOAP Badge](https://public-api.gitpoap.io/v1/repo/safe-global/safe-wallet-web/badge)](https://www.gitpoap.io/gh/safe-global/safe-wallet-web) -The default Safe web interface. +Safe{Wallet} is a smart contract wallet for Ethereum and other EVM chains. Based on Gnosis Safe multisig contracts. + +This repository is the frontend of the Safe{Wallet} app. ## Contributing @@ -19,31 +21,31 @@ Create a `.env` file with environment variables. You can use the `.env.example` Here's the list of all the environment variables: -| Env variable | Description -| ------------------------------------------------------ | ----------- -| `NEXT_PUBLIC_INFURA_TOKEN` | [Infura](https://docs.infura.io/infura/networks/ethereum/how-to/secure-a-project/project-id) RPC API token -| `NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN` | Infura token for Safe Apps, falls back to `NEXT_PUBLIC_INFURA_TOKEN` -| `NEXT_PUBLIC_IS_PRODUCTION` | Set to `true` to build a minified production app -| `NEXT_PUBLIC_GATEWAY_URL_PRODUCTION` | The base URL for the [Safe Client Gateway](https://github.com/safe-global/safe-client-gateway) -| `NEXT_PUBLIC_GATEWAY_URL_STAGING` | The base CGW URL on staging -| `NEXT_PUBLIC_SAFE_VERSION` | The latest version of the Safe contract, defaults to 1.3.0 -| `NEXT_PUBLIC_WC_PROJECT_ID` | [WalletConnect v2](https://docs.walletconnect.com/2.0/cloud/relay) project ID -| `NEXT_PUBLIC_TENDERLY_ORG_NAME` | [Tenderly](https://tenderly.co) org name for Transaction Simulation -| `NEXT_PUBLIC_TENDERLY_PROJECT_NAME` | Tenderly project name -| `NEXT_PUBLIC_TENDERLY_SIMULATE_ENDPOINT_URL` | Tenderly simulation URL -| `NEXT_PUBLIC_BEAMER_ID` | [Beamer](https://www.getbeamer.com) is a news feed for in-app announcements -| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID` | [GTM](https://tagmanager.google.com) project id -| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_DEVELOPMENT_AUTH` | Dev GTM key -| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LATEST_AUTH` | Preview GTM key -| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LIVE_AUTH` | Production GTM key -| `NEXT_PUBLIC_SENTRY_DSN` | [Sentry](https://sentry.io) id for tracking runtime errors -| `NEXT_PUBLIC_IS_OFFICIAL_HOST` | Whether it's the official distribution of the app, or a fork; has legal implications. Set to true only if you also update the legal pages like Imprint and Terms of use -| `NEXT_PUBLIC_REDEFINE_API` | Redefine API base URL -| `NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION` | Firebase Cloud Messaging (FCM) `initializeApp` options on production -| `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_SPINDL_SDK_KEY` | [Spindl](http://spindl.xyz) SDK key +| Env variable | Description | +| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `NEXT_PUBLIC_INFURA_TOKEN` | [Infura](https://docs.infura.io/infura/networks/ethereum/how-to/secure-a-project/project-id) RPC API token | +| `NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN` | Infura token for Safe Apps, falls back to `NEXT_PUBLIC_INFURA_TOKEN` | +| `NEXT_PUBLIC_IS_PRODUCTION` | Set to `true` to build a minified production app | +| `NEXT_PUBLIC_GATEWAY_URL_PRODUCTION` | The base URL for the [Safe Client Gateway](https://github.com/safe-global/safe-client-gateway) | +| `NEXT_PUBLIC_GATEWAY_URL_STAGING` | The base CGW URL on staging | +| `NEXT_PUBLIC_SAFE_VERSION` | The latest version of the Safe contract, defaults to 1.4.1 | +| `NEXT_PUBLIC_WC_PROJECT_ID` | [WalletConnect v2](https://docs.walletconnect.com/2.0/cloud/relay) project ID | +| `NEXT_PUBLIC_TENDERLY_ORG_NAME` | [Tenderly](https://tenderly.co) org name for Transaction Simulation | +| `NEXT_PUBLIC_TENDERLY_PROJECT_NAME` | Tenderly project name | +| `NEXT_PUBLIC_TENDERLY_SIMULATE_ENDPOINT_URL` | Tenderly simulation URL | +| `NEXT_PUBLIC_BEAMER_ID` | [Beamer](https://www.getbeamer.com) is a news feed for in-app announcements | +| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID` | [GTM](https://tagmanager.google.com) project id | +| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_DEVELOPMENT_AUTH` | Dev GTM key | +| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LATEST_AUTH` | Preview GTM key | +| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LIVE_AUTH` | Production GTM key | +| `NEXT_PUBLIC_SENTRY_DSN` | [Sentry](https://sentry.io) id for tracking runtime errors | +| `NEXT_PUBLIC_IS_OFFICIAL_HOST` | Whether it's the official distribution of the app, or a fork; has legal implications. Set to true only if you also update the legal pages like Imprint and Terms of use | +| `NEXT_PUBLIC_REDEFINE_API` | Redefine API base URL | +| `NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION` | Firebase Cloud Messaging (FCM) `initializeApp` options on production | +| `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_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. @@ -56,6 +58,7 @@ yarn ``` Generate types: + ```bash yarn postinstall ``` @@ -111,6 +114,7 @@ yarn cypress:open ``` You can then choose which e2e tests to run. +Some tests will require signer private keys, please include them in your .env file ## Component template diff --git a/cypress.config.js b/cypress.config.js index b03786f20..f04876345 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,6 +1,12 @@ import { defineConfig } from 'cypress' import 'dotenv/config' import * as fs from 'fs' +import path, { dirname } from 'path' +import { fileURLToPath } from 'url' +import matter from 'gray-matter' +import { configureVisualRegression } from 'cypress-visual-regression' + +const __dirname = dirname(fileURLToPath(import.meta.url)) export default defineConfig({ projectId: 'exhdra', @@ -14,7 +20,30 @@ export default defineConfig({ openMode: 0, }, e2e: { + screenshotsFolder: './cypress/snapshots/actual', setupNodeEvents(on, config) { + // Read and parse the terms Markdown file + try { + const filePath = path.resolve(__dirname, './src/markdown/terms/terms.md') + + const content = fs.readFileSync(filePath, 'utf8') + const parsed = matter(content) + const frontmatter = parsed.data + + // Set Cookie term version on the cypress env - this way we can access it in the tests + config.env.CURRENT_COOKIE_TERMS_VERSION = frontmatter.version + } catch (error) { + console.error('Error reading or parsing terms.md file:', error) + } + + configureVisualRegression(on), + on('task', { + log(message) { + console.log(message) + return null + }, + }) + on('after:spec', (spec, results) => { if (results && results.video) { const failures = results.tests.some((test) => test.attempts.some((attempt) => attempt.state === 'failed')) @@ -23,9 +52,15 @@ export default defineConfig({ } } }) + + return config }, env: { ...process.env, + visualRegressionType: 'regression', + visualRegressionBaseDirectory: 'cypress/snapshots/actual', + visualRegressionDiffDirectory: 'cypress/snapshots/diff', + visualRegressionGenerateDiff: 'fail', }, baseUrl: 'http://localhost:3000', testIsolation: false, diff --git a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js index 532b72cce..f0236a53a 100644 --- a/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js +++ b/cypress/e2e/happypath/sendfunds_connected_wallet.cy.js @@ -8,7 +8,7 @@ import * as nfts from '../pages/nfts.pages' import * as ls from '../../support/localstorage_data.js' import { ethers } from 'ethers' import SafeApiKit from '@safe-global/api-kit' -import { createEthersAdapter, createSigners } from '../../support/api/utils_ether' +import { createSigners } from '../../support/api/utils_ether' 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' @@ -40,10 +40,6 @@ const tokenContract = new ethers.Contract(contractAddress, abi_qtrust, provider) const nftContract = new ethers.Contract(nftContractAddress, abi_nft_pc2, provider) const owner1Signer = signers[0] -const owner2Signer = signers[1] - -const ethAdapterOwner1 = createEthersAdapter(owner1Signer) -const ethAdapterOwner2 = createEthersAdapter(owner2Signer) function visit(url) { cy.visit(url) @@ -51,12 +47,14 @@ function visit(url) { describe('Send funds with connected signer happy path tests', { defaultCommandTimeout: 60000 }, () => { before(async () => { + cy.clearLocalStorage().then(() => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2_cookies, ls.cookies.acceptedCookies) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__tokenlist_onboarding, + ls.cookies.acceptedTokenListOnboarding, + ) + }) safesData = await getSafes(CATEGORIES.funds) - main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__cookies, ls.cookies.acceptedCookies) - main.addToLocalStorage( - constants.localStorageKeys.SAFE_v2__tokenlist_onboarding, - ls.cookies.acceptedTokenListOnboarding, - ) apiKit = new SafeApiKit({ chainId: BigInt(1), txServiceUrl: constants.stagingTxServiceUrl, @@ -65,8 +63,8 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi outgoingSafeAddress = safesData.SEP_FUNDS_SAFE_6.substring(4) const safeConfigurations = [ - { ethAdapter: ethAdapterOwner1, safeAddress: outgoingSafeAddress }, - { ethAdapter: ethAdapterOwner2, safeAddress: outgoingSafeAddress }, + { signer: walletCredentials.OWNER_1_PRIVATE_KEY, safeAddress: outgoingSafeAddress, provider }, + { signer: walletCredentials.OWNER_2_PRIVATE_KEY, safeAddress: outgoingSafeAddress, provider }, ] safes = await createSafes(safeConfigurations) @@ -104,6 +102,8 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi }) await tx.wait() main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() }) }) }) @@ -157,6 +157,8 @@ 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) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() }) }) @@ -190,6 +192,8 @@ describe('Send funds with connected signer happy path tests', { defaultCommandTi await tx.wait() main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() }) }) }) diff --git a/cypress/e2e/happypath/sendfunds_queue_1.cy.js b/cypress/e2e/happypath/sendfunds_queue_1.cy.js index f0a6d5624..b1f6e1bd4 100644 --- a/cypress/e2e/happypath/sendfunds_queue_1.cy.js +++ b/cypress/e2e/happypath/sendfunds_queue_1.cy.js @@ -4,10 +4,12 @@ import * as assets from '../pages/assets.pages' import * as tx from '../pages/transactions.page' import { ethers } from 'ethers' import SafeApiKit from '@safe-global/api-kit' -import { createEthersAdapter, createSigners } from '../../support/api/utils_ether' +import { 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' +import * as ls from '../../support/localstorage_data.js' +import * as navigation from '../pages/navigation.page.js' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const receiver = walletCredentials.OWNER_2_WALLET_ADDRESS @@ -37,13 +39,8 @@ const signers = createSigners(privateKeys, provider) const owner1Signer = signers[0] const owner2Signer = signers[1] -const ethAdapterOwner1 = createEthersAdapter(owner1Signer) -const ethAdapterOwner2 = createEthersAdapter(owner2Signer) - function visit(url) { cy.visit(url) - cy.clearLocalStorage() - main.acceptCookies() } function executeTransactionFlow(fromSafe) { @@ -56,6 +53,14 @@ function executeTransactionFlow(fromSafe) { describe('Send funds from queue happy path tests 1', () => { before(async () => { + cy.clearLocalStorage().then(() => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2_cookies, ls.cookies.acceptedCookies) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__tokenlist_onboarding, + ls.cookies.acceptedTokenListOnboarding, + ) + }) + safesData = await getSafes(CATEGORIES.funds) apiKit = new SafeApiKit({ chainId: BigInt(1), @@ -67,10 +72,10 @@ describe('Send funds from queue happy path tests 1', () => { existingSafeAddress3 = safesData.SEP_FUNDS_SAFE_5.substring(4) const safeConfigurations = [ - { ethAdapter: ethAdapterOwner1, safeAddress: existingSafeAddress1 }, - { ethAdapter: ethAdapterOwner1, safeAddress: existingSafeAddress2 }, - { ethAdapter: ethAdapterOwner1, safeAddress: existingSafeAddress3 }, - { ethAdapter: ethAdapterOwner2, safeAddress: existingSafeAddress3 }, + { signer: privateKeys[0], safeAddress: existingSafeAddress1, provider }, + { signer: privateKeys[0], safeAddress: existingSafeAddress2, provider }, + { signer: privateKeys[0], safeAddress: existingSafeAddress3, provider }, + { signer: privateKeys[1], safeAddress: existingSafeAddress3, provider }, ] safes = await createSafes(safeConfigurations) @@ -81,41 +86,39 @@ describe('Send funds from queue happy path tests 1', () => { protocolKitOwner2_S3 = safes[3] }) - it( - 'Verify confirmation and execution of native token queued tx by second signer with connected wallet', - { defaultCommandTimeout: 300000 }, - () => { - cy.wrap(null) - .then(() => { - return main.fetchCurrentNonce(network_pref + existingSafeAddress1) - }) - .then(async (currentNonce) => { - const amount = ethers.parseUnits(tokenAmount, unit_eth).toString() - const safeTransactionData = { - to: receiver, - data: '0x', - value: amount.toString(), - } - - const safeTransaction = await protocolKitOwnerS1.createTransaction({ transactions: [safeTransactionData] }) - const safeTxHash = await protocolKitOwnerS1.getTransactionHash(safeTransaction) - const senderSignature = await protocolKitOwnerS1.signHash(safeTxHash) - const safeAddress = existingSafeAddress1 - - await apiKit.proposeTransaction({ - safeAddress, - safeTransactionData: safeTransaction.data, - safeTxHash, - senderAddress: await owner1Signer.getAddress(), - senderSignature: senderSignature.data, - }) - - executeTransactionFlow(safeAddress) - cy.wait(5000) - main.verifyNonceChange(network_pref + safeAddress, currentNonce + 1) + it('Verify confirmation and execution of native token queued tx by second signer with connected wallet', () => { + cy.wrap(null) + .then(() => { + return main.fetchCurrentNonce(network_pref + existingSafeAddress1) + }) + .then(async (currentNonce) => { + const amount = ethers.parseUnits(tokenAmount, unit_eth).toString() + const safeTransactionData = { + to: receiver, + data: '0x', + value: amount.toString(), + } + + const safeTransaction = await protocolKitOwnerS1.createTransaction({ transactions: [safeTransactionData] }) + const safeTxHash = await protocolKitOwnerS1.getTransactionHash(safeTransaction) + const senderSignature = await protocolKitOwnerS1.signHash(safeTxHash) + const safeAddress = existingSafeAddress1 + + await apiKit.proposeTransaction({ + safeAddress, + safeTransactionData: safeTransaction.data, + safeTxHash, + senderAddress: await owner1Signer.getAddress(), + senderSignature: senderSignature.data, }) - }, - ) + + executeTransactionFlow(safeAddress) + cy.wait(5000) + main.verifyNonceChange(network_pref + safeAddress, currentNonce + 1) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + }) it.skip('Verify confirmation and execution of native token queued tx by second signer with relayer', () => { function executeTransactionFlow(fromSafe) { @@ -198,6 +201,8 @@ describe('Send funds from queue happy path tests 1', () => { executeTransaction(safeAddress) cy.wait(5000) main.verifyNonceChange(network_pref + safeAddress, currentNonce + 1) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() }) }) }) diff --git a/cypress/e2e/happypath/sendfunds_relay.cy.js b/cypress/e2e/happypath/sendfunds_relay.cy.js index 1f4a65bb5..fcf34d798 100644 --- a/cypress/e2e/happypath/sendfunds_relay.cy.js +++ b/cypress/e2e/happypath/sendfunds_relay.cy.js @@ -8,11 +8,11 @@ import * as nfts from '../pages/nfts.pages' import * as ls from '../../support/localstorage_data.js' import { ethers } from 'ethers' import SafeApiKit from '@safe-global/api-kit' -import { createEthersAdapter, createSigners } from '../../support/api/utils_ether' +import { createSigners } from '../../support/api/utils_ether' 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' +import * as fundSafes from '../../fixtures/safes/funds.json' const transferAmount = '1' @@ -41,9 +41,6 @@ const nftContract = new ethers.Contract(nftContractAddress, abi_nft_pc2, provide const owner1Signer = signers[0] const owner2Signer = signers[1] -const ethAdapterOwner1 = createEthersAdapter(owner1Signer) -const ethAdapterOwner2 = createEthersAdapter(owner2Signer) - function visit(url) { cy.visit(url) } @@ -51,12 +48,14 @@ function visit(url) { // TODO: Relay only allows 5 txs per hour. describe('Send funds with relay happy path tests', { defaultCommandTimeout: 300000 }, () => { before(async () => { - safesData = await getSafes(CATEGORIES.funds) - main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__cookies, ls.cookies.acceptedCookies) - main.addToLocalStorage( - constants.localStorageKeys.SAFE_v2__tokenlist_onboarding, - ls.cookies.acceptedTokenListOnboarding, - ) + cy.clearLocalStorage().then(() => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2_cookies, ls.cookies.acceptedCookies) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__tokenlist_onboarding, + ls.cookies.acceptedTokenListOnboarding, + ) + }) + safesData = fundSafes apiKit = new SafeApiKit({ chainId: BigInt(1), txServiceUrl: constants.stagingTxServiceUrl, @@ -65,8 +64,8 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 3000 outgoingSafeAddress = safesData.SEP_FUNDS_SAFE_8.substring(4) const safeConfigurations = [ - { ethAdapter: ethAdapterOwner1, safeAddress: outgoingSafeAddress }, - { ethAdapter: ethAdapterOwner2, safeAddress: outgoingSafeAddress }, + { signer: privateKeys[0], safeAddress: outgoingSafeAddress, provider }, + { signer: privateKeys[1], safeAddress: outgoingSafeAddress, provider }, ] safes = await createSafes(safeConfigurations) @@ -107,6 +106,8 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 3000 }) await tx.wait() main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() }) }) }) @@ -165,6 +166,8 @@ 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) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() }) }) }) @@ -205,6 +208,8 @@ describe('Send funds with relay happy path tests', { defaultCommandTimeout: 3000 await tx.wait() main.verifyNonceChange(network_pref + originatingSafe, currentNonce + 1) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() }) }) }) diff --git a/cypress/e2e/happypath/tx_history_filter_hp_2.cy.js b/cypress/e2e/happypath/tx_history_filter_hp_2.cy.js index 98e0f5d98..39c322cce 100644 --- a/cypress/e2e/happypath/tx_history_filter_hp_2.cy.js +++ b/cypress/e2e/happypath/tx_history_filter_hp_2.cy.js @@ -2,18 +2,24 @@ 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 ls from '../../support/localstorage_data.js' let staticSafes = [] describe('Tx history happy path tests 2', () => { before(async () => { + cy.clearLocalStorage().then(() => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2_cookies, ls.cookies.acceptedCookies) + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__tokenlist_onboarding, + ls.cookies.acceptedTokenListOnboarding, + ) + }) 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', () => { @@ -25,6 +31,6 @@ describe('Tx history happy path tests 2', () => { createTx.fillFilterForm({ address: moduleAddress }) createTx.clickOnApplyBtn() createTx.verifyNumberOfTransactions(1) - createTx.checkTxItemDate(0, uiDate) + createTx.checkTxItemDate(1, uiDate) }) }) diff --git a/cypress/e2e/pages/assets.pages.js b/cypress/e2e/pages/assets.pages.js index b92af57eb..a19f57b9b 100644 --- a/cypress/e2e/pages/assets.pages.js +++ b/cypress/e2e/pages/assets.pages.js @@ -241,6 +241,8 @@ export function checkHiddenTokenBtnCounter(value) { } export function verifyEachRowHasCheckbox(state) { + const tokens = [currencyTestTokenB, currencyTestTokenA] + main.verifyTextVisibility(tokens) cy.get(tokenListTable).within(() => { cy.get('tbody').within(() => { cy.get('tr').each(($row) => { diff --git a/cypress/e2e/pages/batches.pages.js b/cypress/e2e/pages/batches.pages.js index 85e98e635..c06fc5d91 100644 --- a/cypress/e2e/pages/batches.pages.js +++ b/cypress/e2e/pages/batches.pages.js @@ -3,12 +3,13 @@ import * as constants from '../../support/constants' const tokenSelectorText = 'G(รถ|oe)rli Ether' const noLaterString = 'No, later' const yesExecuteString = 'Yes, execute' -const newTransactionTitle = 'New transaction' +export const newTransactionBtnStr = 'New transaction' const sendTokensButn = 'Send tokens' const nextBtn = 'Next' const executeBtn = 'Execute' -const addToBatchBtn = 'Add to batch' +export const addToBatchBtn = 'Add to batch' const confirmBatchBtn = 'Confirm batch' +export const batchedTxs = 'Batched transactions' export const closeModalBtnBtn = '[data-testid="CloseIcon"]' export const deleteTransactionbtn = '[title="Delete transaction"]' @@ -61,7 +62,7 @@ function executeTransaction() { } function addToBatchButton() { - cy.contains(addToBatchBtn).should('be.visible').and('not.be.disabled').click() + cy.get('button').contains(addToBatchBtn).click() } export function openBatchtransactionsModal() { @@ -106,3 +107,15 @@ export function verifyTransactionAdded() { export function verifyBatchIconCount(count) { cy.get(batchTxCounter).contains(count) } + +export function verifyNewTxButtonStatus(param) { + cy.get('button').contains(newTransactionBtnStr).should(param) +} + +export function isTxExpanded(index, option) { + cy.contains(batchedTxs) + .parent() + .within(() => { + cy.get('li').eq(index).find(`div[aria-expanded="${option}"]`) + }) +} diff --git a/cypress/e2e/pages/create_tx.pages.js b/cypress/e2e/pages/create_tx.pages.js index 221959716..315211344 100644 --- a/cypress/e2e/pages/create_tx.pages.js +++ b/cypress/e2e/pages/create_tx.pages.js @@ -1,23 +1,21 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' import * as wallet from '../pages/create_wallet.pages' +import * as modal from '../pages/modals.page' export const delegateCallWarning = '[data-testid="delegate-call-warning"]' export const policyChangeWarning = '[data-testid="threshold-warning"]' const newTransactionBtnStr = 'New transaction' const recepientInput = 'input[name="recipient"]' -const sendTokensBtnStr = 'Send tokens' const tokenAddressInput = 'input[name="tokenAddress"]' const amountInput = 'input[name="amount"]' const nonceInput = 'input[name="nonce"]' -const nonceTxValue = '[data-testid="nonce"]' const gasLimitInput = '[name="gasLimit"]' const rotateLeftIcon = '[data-testid="RotateLeftIcon"]' export const transactionItem = '[data-testid="transaction-item"]' export const connectedWalletExecMethod = '[data-testid="connected-wallet-execution-method"]' const addToBatchBtn = '[data-track="batching: Add to batch"]' const accordionDetails = '[data-testid="accordion-details"]' -const accordionMessageDetails = '[data-testid="accordion-msg-details"]' const copyIcon = '[data-testid="copy-btn-icon"]' const transactionSideList = '[data-testid="transaction-actions-list"]' const confirmationVisibilityBtn = '[data-testid="confirmation-visibility-btn"]' @@ -53,7 +51,7 @@ const transactionsPerHrStr = 'free transactions left this hour' const maxAmountBtnStr = 'Max' const nextBtnStr = 'Next' -const nativeTokenTransferStr = 'Native token transfer' +const nativeTokenTransferStr = 'ETH' const yesStr = 'Yes, ' const estimatedFeeStr = 'Estimated fee' const executeStr = 'Execute' @@ -61,11 +59,24 @@ const editBtnStr = 'Edit' const executionParamsStr = 'Execution parameters' const noLaterStr = 'No, later' const signBtnStr = 'Sign' +const confirmBtnStr = 'Confirm' const expandAllBtnStr = 'Expand all' const collapseAllBtnStr = 'Collapse all' export const messageNestedStr = `"nestedString": "Test message 3 off-chain"` const noTxFoundStr = (type) => `0 ${type} transactions found` const deleteFromQueueStr = 'Delete from the queue' +const bulkExecuteBtn = (tx) => `Bulk execute ${tx} transactions` +const bulkConfirmationText = (tx) => + `This transaction batches a total of ${tx} transactions from your queue into a single Ethereum transaction` + +const disabledBultExecuteBtnTooltip = + 'Batch execution is only available for transactions that have been fully signed and are strictly sequential in Safe Account nonce' +const enabledBulkExecuteBtnTooltip = 'All highlighted transactions will be included in the batch execution' + +const bulkExecuteBtnStr = 'Bulk execute' + +const batchModalTitle = 'Batch' +const bulkTxStr = 'Bulk transactions' export const filterTypes = { incoming: 'Incoming', @@ -77,6 +88,15 @@ function clickOnRejectBtn() { cy.get(rejectTxBtn).click() } +export function verifyBulkExecuteBtnIsEnabled(txs) { + return cy.get('button').contains(bulkExecuteBtn(txs)).should('be.enabled') +} + +export function verifyEnabledBulkExecuteBtnTooltip() { + cy.get('button').contains(bulkExecuteBtnStr).trigger('mouseover', { force: true }) + cy.contains(enabledBulkExecuteBtnTooltip).should('exist') +} + export function deleteTx() { clickOnRejectBtn() cy.get(wallet.choiceBtn).contains(deleteFromQueueStr).click() @@ -274,6 +294,10 @@ export function expandAllActions(actions) { main.checkTextsExistWithinElement(accordionDetails, actions) } +export function clickOnExpandAllActionsBtn() { + cy.get(expandAllBtn).click() +} + export function collapseAllActions(data) { cy.get(collapseAllBtn).click() data.forEach((action) => { @@ -461,7 +485,6 @@ export function openExecutionParamsModal() { export function verifyAndSubmitExecutionParams() { cy.contains(executionParamsStr).parents('form').as('Paramsform') - const arrayNames = ['Wallet nonce', 'Max priority fee (Gwei)', 'Max fee (Gwei)', 'Gas limit'] arrayNames.forEach((element) => { cy.get('@Paramsform').find('label').contains(`${element}`).next().find('input').should('not.be.disabled') @@ -482,6 +505,10 @@ export function clickOnSignTransactionBtn() { cy.get('button').contains(signBtnStr).click() } +export function clickOnConfirmTransactionBtn() { + cy.get('button').contains(confirmBtnStr).click() +} + export function waitForProposeRequest() { cy.intercept('POST', constants.proposeEndpoint).as('ProposeTx') cy.wait('@ProposeTx') @@ -540,3 +567,35 @@ export function verifyTxDestinationAddress(receivedAddress) { export function verifyReplacedSigner(newSignerName) { cy.get(replacementNewSigner).should('exist').contains(newSignerName) } + +function verifyBulkActions(actions) { + actions.forEach((action) => { + cy.contains(action).should('exist') + }) +} + +export function verifyBulkConfirmationScreen(tx, actions) { + cy.contains(bulkConfirmationText(tx)) + verifyBulkActions(actions) + cy.get(modal.modalHeader).within(() => { + cy.contains(batchModalTitle).should('exist') + cy.get('svg').should('exist') + }) +} + +export function verifyBulkTxHistoryBlock(tx, actions) { + cy.contains(bulkTxStr) + .parent('div') + .parent() + .eq(0) + .within(() => { + cy.contains(tx) + verifyBulkActions(actions) + }) +} + +export function verifyBulkExecuteBtnIsDisabled() { + cy.get('button').contains(bulkExecuteBtnStr).should('be.disabled') + cy.get('button').contains(bulkExecuteBtnStr).trigger('mouseover', { force: true }) + cy.contains(disabledBultExecuteBtnTooltip).should('exist') +} diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index 813ba6d61..1c79e430c 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -32,7 +32,7 @@ export 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"]' +export const activateAccountBtn = '[data-testid="activate-account-btn-cf"]' const notificationsSwitch = '[data-testid="notifications-switch"]' export const addFundsSection = '[data-testid="add-funds-section"]' export const noTokensAlert = '[data-testid="no-tokens-alert"]' @@ -49,8 +49,14 @@ const initialSteps = '0 of 2 steps completed' export const addSignerStr = 'Add signer' export const accountRecoveryStr = 'Account recovery' export const sendTokensStr = 'Send tokens' +const noWalletConnectedMsg = 'No wallet connected' +export const deployWalletStr = 'about to deploy this Safe Account' const connectWalletBtn = '[data-testid="connect-wallet-btn"]' + +export function waitForConnectionMsgDisappear() { + cy.contains(noWalletConnectedMsg).should('not.exist') +} export function checkNotificationsSwitchIs(status) { cy.get(notificationsSwitch).find('input').should(`be.${status}`) } @@ -175,7 +181,7 @@ export function selectNetwork(network) { cy.get(expandMoreIcon).parents('div').eq(1).click() cy.wait(1000) let regex = new RegExp(`^${network}$`) - cy.get('li').contains(regex).click() + cy.get('li').parents('ul').contains(regex).click() } export function clickOnNextBtn() { diff --git a/cypress/e2e/pages/dashboard.pages.js b/cypress/e2e/pages/dashboard.pages.js index 746106e15..ce1675e7c 100644 --- a/cypress/e2e/pages/dashboard.pages.js +++ b/cypress/e2e/pages/dashboard.pages.js @@ -4,20 +4,16 @@ import * as main from './main.page.js' import * as createtx from './create_tx.pages.js' import staticSafes from '../../fixtures/safes/static.json' -const connectAndTransactStr = 'Connect & transact' const transactionQueueStr = 'Pending transactions' const noTransactionStr = 'This Safe has no queued transactions' const overviewStr = 'Total asset value' const sendStr = 'Send' const receiveStr = 'Receive' const viewAllStr = 'View all' -const transactionBuilderStr = 'Use Transaction Builder' const safeAppStr = 'Safe Apps' const exploreSafeApps = 'Explore Safe Apps' export const copiedAppUrl = 'share/safe-app?appUrl' -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"][href^="/transactions/queue"]' @@ -108,10 +104,6 @@ export function verifyShareBtnWorks(index, data) { ) } -export function verifyConnectTransactStrIsVisible() { - cy.contains(connectAndTransactStr).should('be.visible') -} - export function verifyOverviewWidgetData() { // Alias for the Overview section cy.contains('div', overviewStr).parents('section').as('overviewSection') @@ -144,21 +136,6 @@ export function verifyTxQueueWidget() { }) } -export function verifyFeaturedAppsSection() { - // Alias for the featured Safe Apps section - cy.contains('h2', connectAndTransactStr).parents('section').as('featuredSafeAppsSection') - - // Tx Builder app - cy.get('@featuredSafeAppsSection').within(() => { - // Transaction Builder - cy.contains(transactionBuilderStr) - cy.get(txBuilder).should('exist') - - // Featured apps have a Safe-specific link - cy.get(safeSpecificLink).should('have.length', 1) - }) -} - export function verifySafeAppsSection() { cy.contains('h2', safeAppStr).parents('section').as('safeAppsSection') cy.get('@safeAppsSection').contains(exploreSafeApps) diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index 5643720ef..0288673d5 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -19,7 +19,7 @@ export function clickOnSideMenuItem(item) { export function waitForHistoryCallToComplete() { cy.intercept('GET', constants.transactionHistoryEndpoint).as('History') - cy.wait('@History') + cy.wait('@History', { timeout: 20000 }) } export const fetchSafeData = (safeAddress) => { @@ -333,3 +333,12 @@ export function getIframeBody(iframe) { export const checkButtonByTextExists = (buttonText) => { cy.get('button').contains(buttonText).should('exist') } + +export function getAddedSafeAddressFromLocalStorage(chainId, index) { + return cy.window().then((win) => { + const addedSafes = win.localStorage.getItem(constants.localStorageKeys.SAFE_v2__addedSafes) + const addedSafesObj = JSON.parse(addedSafes) + const safeAddress = Object.keys(addedSafesObj[chainId])[index] + return safeAddress + }) +} diff --git a/cypress/e2e/pages/modals.page.js b/cypress/e2e/pages/modals.page.js index d2b99a68e..6a4fe0b8e 100644 --- a/cypress/e2e/pages/modals.page.js +++ b/cypress/e2e/pages/modals.page.js @@ -1,5 +1,6 @@ export const modalTitle = '[data-testid="modal-title"]' export const modal = '[data-testid="modal-view"]' +export const modalHeader = '[data-testid="modal-header"]' export const modalTitiles = { editEntry: 'Edit entry', diff --git a/cypress/e2e/pages/modules.page.js b/cypress/e2e/pages/modules.page.js new file mode 100644 index 000000000..d886f5987 --- /dev/null +++ b/cypress/e2e/pages/modules.page.js @@ -0,0 +1 @@ +export const moduleRemoveIcon = '[data-testid="module-remove-btn"]' diff --git a/cypress/e2e/pages/navigation.page.js b/cypress/e2e/pages/navigation.page.js index ff672497e..18971fd50 100644 --- a/cypress/e2e/pages/navigation.page.js +++ b/cypress/e2e/pages/navigation.page.js @@ -3,7 +3,7 @@ export const setupSection = '[data-testid="setup-section"]' export const modalBackBtn = '[data-testid="modal-back-btn"]' export const newTxBtn = '[data-testid="new-tx-btn"]' const modalCloseIcon = '[data-testid="CloseIcon"]' -const expandMoreIcon = 'svg[data-testid="ExpandMoreIcon"]' +export const expandMoreIcon = 'svg[data-testid="ExpandMoreIcon"]' const sentinelStart = 'div[data-testid="sentinelStart"]' const disconnectBtnStr = 'Disconnect' @@ -16,8 +16,8 @@ export function clickOnSideNavigation(option) { cy.get(option).should('exist').click() } -export function clickOnModalCloseBtn() { - cy.get(modalCloseIcon).eq(0).trigger('click') +export function clickOnModalCloseBtn(index) { + cy.get(modalCloseIcon).eq(index).trigger('click') } export function clickOnNewTxBtn() { diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index afde4caa6..039cecaee 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -125,8 +125,8 @@ export function getAddressToBeRemoved() { return removedAddress } -export function openReplaceOwnerWindow() { - cy.get(replaceOwnerBtn).click({ force: true }) +export function openReplaceOwnerWindow(index) { + cy.get(replaceOwnerBtn).eq(index).click({ force: true }) cy.get(newOwnerName).should('be.visible') cy.get(newOwnerAddress).should('be.visible') } @@ -191,7 +191,7 @@ export function verifyNonceInputValue(value) { } export function verifyErrorMsgInvalidAddress(errorMsg) { - cy.get('label').contains(errorMsg).should('be.visible') + cy.get('label').contains(errorMsg).should('exist') } export function verifyValidWalletName(errorMsg) { diff --git a/cypress/e2e/pages/recovery.pages.js b/cypress/e2e/pages/recovery.pages.js index 60537a806..9c84ea3fc 100644 --- a/cypress/e2e/pages/recovery.pages.js +++ b/cypress/e2e/pages/recovery.pages.js @@ -4,23 +4,51 @@ import * as safe from '../pages/load_safe.pages' import * as tx from '../pages/transactions.page' import { tableContainer } from '../pages/address_book.page' import { txDate } from '../pages/create_tx.pages' +import { modalHeader } from '../pages/modals.page' -const setupRecoveryBtn = '[data-testid="setup-recovery-btn"]' -const setupRecoveryModalBtn = '[data-testid="setup-btn"]' +export const setupRecoveryBtn = '[data-testid="setup-recovery-btn"]' +export const setupRecoveryModalBtn = '[data-testid="setup-btn"]' const recoveryNextBtn = '[data-testid="next-btn"]' const warningSection = '[data-testid="warning-section"]' const termsCheckbox = 'input[type="checkbox"]' -const removeRecovererBtn = '[data-testid="remove-recoverer-btn"]' +export const removeRecovererBtn = '[data-testid="remove-recoverer-btn"]' +export const editRecovererBtn = '[data-testid="edit-recoverer-btn"]' const removeRecovererSection = '[data-testid="remove-recoverer-section"]' const startRecoveryBtn = '[data-testid="start-recovery-btn"]' const recoveryDelaySelect = '[data-testid="recovery-delay-select"]' +const recoveryExpirySelect = '[data-testid="recovery-expiry-select"]' const postponeRecoveryBtn = '[data-testid="postpone-recovery-btn"]' const goToQueueBtn = '[data-testid="queue-btn"]' const executeBtn = '[data-testid="execute-btn"]' const cancelRecoveryBtn = '[data-testid="cancel-recovery-btn"]' const cancelProposalBtn = '[data-testid="cancel-proposal-btn"]' const executeFormBtn = '[data-testid="execute-form-btn"]' +const advancedBtn = '[data-testid="advanced-btn"]' +const recoveryProposalModal = '[data-testid="recovery-proposal"]' +const recoveryProposalHorizontal = '[data-testid="recovery-proposal-hr"]' +export const recoveryOptions = { + fiveMin: '5 minutes', + oneHr: '1 hour', + fiveSixDays: '56 days', + never: 'never', +} +export function clickOnEditRecoverer() { + cy.get(editRecovererBtn).click() +} +export function verifyRecovererSettings(data) { + main.checkTextsExistWithinElement(tableContainer, data) +} + +export function verifyRecovererConfirmationData(data) { + data.forEach((item) => { + cy.get(modalHeader).next('div').contains(item) + }) +} + +export function verifyRecoveryTableDisplayed() { + cy.get(tableContainer).should('be.visible') +} export function clickOnExecuteRecoveryCancelBtn() { cy.get(executeFormBtn).click() } @@ -46,6 +74,27 @@ export function setRecoveryDelay(option) { cy.contains(option).click() } +export function verifyRecoveryDelayOptions(options) { + cy.get(recoveryDelaySelect).click() + options.forEach((item) => { + cy.contains(item) + }) +} + +export function setRecoveryExpiry(option) { + cy.get(advancedBtn).click() + cy.get(recoveryExpirySelect).click() + cy.contains(option).click() +} + +export function verifyRecoveryExpiryOptions(options) { + cy.get(advancedBtn).click() + cy.get(recoveryExpirySelect).click() + options.forEach((item) => { + cy.contains(item) + }) +} + export function getSetupRecoveryBtn() { return cy.get(setupRecoveryBtn) } @@ -116,3 +165,21 @@ export function postponeRecovery() { cy.get(postponeRecoveryBtn).should('not.exist') }) } + +export function clickOnRecoverLaterBtn() { + cy.get(postponeRecoveryBtn).click() + cy.get(postponeRecoveryBtn).should('not.exist') +} + +export function verifyNonceState(state) { + if (state === constants.elementExistanceStates.exist) { + cy.get(nonceFld).should(constants.elementExistanceStates.exist) + } + cy.get(nonceFld).should(constants.elementExistanceStates.not_exist) +} + +export function verifyRecoveryProposalModalState(option, horizontal = false) { + let modal = recoveryProposalModal + if (horizontal) modal = recoveryProposalHorizontal + cy.get(modal).should(option) +} diff --git a/cypress/e2e/pages/safeapps.pages.js b/cypress/e2e/pages/safeapps.pages.js index 1810656cb..a673a864c 100644 --- a/cypress/e2e/pages/safeapps.pages.js +++ b/cypress/e2e/pages/safeapps.pages.js @@ -27,7 +27,7 @@ const acceptBtnStr = /accept/i const clearAllBtnStr = /clear all/i const allowAllPermissions = /allow all/i export const enterAddressStr = /enter address or ens name/i -export const addTransactionStr = /add transaction/i +export const addTransactionStr = /add new transaction/i export const createBatchStr = /create batch/i export const sendBatchStr = /send batch/i export const transactionDetailsStr = /transaction details/i @@ -218,12 +218,10 @@ export function verifyAppDescription(descr) { export function clickOnOpenSafeAppBtn() { cy.get(openSafeAppBtn).click() - cy.wait(2000) } export function verifyDisclaimerIsDisplayed() { verifyDisclaimerIsVisible() - cy.wait(500) } function verifyDisclaimerIsVisible() { @@ -249,20 +247,17 @@ export function verifyMicrofoneCheckBoxExists() { return cy.findByRole('checkbox', { name: microfoneCheckBoxStr }).should('exist') } -export function storeAndVerifyPermissions() { +export function verifyInfoModalAcceptance() { cy.waitForSelector(() => { return cy .findByRole('button', { name: continueBtnStr }) .click() - .wait(500) + .wait(2000) .should(() => { - const storedBrowserPermissions = JSON.parse(localStorage.getItem(constants.BROWSER_PERMISSIONS_KEY)) - const browserPermissions = Object.values(storedBrowserPermissions)[0][0] - const storedInfoModal = JSON.parse(localStorage.getItem(constants.INFO_MODAL_KEY)) - - expect(browserPermissions.feature).to.eq('camera') - expect(browserPermissions.status).to.eq('granted') - expect(storedInfoModal['11155111'].consentsAccepted).to.eq(true) + const storedInfoModal = JSON.parse( + localStorage.getItem(constants.localStorageKeys.SAFE_v2__SafeApps__infoModal), + ) + expect(storedInfoModal[constants.networkKeys.sepolia].consentsAccepted).to.eq(true) }) }) } diff --git a/cypress/e2e/pages/sidebar.pages.js b/cypress/e2e/pages/sidebar.pages.js index 854394159..4729e6f97 100644 --- a/cypress/e2e/pages/sidebar.pages.js +++ b/cypress/e2e/pages/sidebar.pages.js @@ -57,8 +57,13 @@ 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` +const pedningTxStr = (n) => `${n} pending transaction` export const confirmGenStr = 'to confirm' +export function verifyNumberOfPendingTxTag(tx) { + cy.contains(pedningTxStr(tx)) +} + export function getImportBtn() { return cy.get(importBtn).scrollIntoView().should('be.visible') } diff --git a/cypress/e2e/pages/spending_limits.pages.js b/cypress/e2e/pages/spending_limits.pages.js index d6178686b..a7b844282 100644 --- a/cypress/e2e/pages/spending_limits.pages.js +++ b/cypress/e2e/pages/spending_limits.pages.js @@ -33,6 +33,9 @@ const oldTokenAmount = '[data-testid="old-token-amount"]' const oldResetTime = '[data-testid="old-reset-time"]' const slimitReplacementWarning = '[data-testid="limit-replacement-warning"]' const addressItem = '[data-testid="address-item"]' +const allActionsSection = '[data-testid="all-actions"]' +const actionItem = '[data-testid="action-item"]' +const decodedTxSummary = '[data-testid="decoded-tx-summary"]' const actionSectionItem = () => { return cy.get('[data-testid="CodeIcon"]').parent() @@ -48,7 +51,12 @@ export const timePeriodOptions = { const getBeneficiaryInput = () => cy.get(beneficiarySection).find('input').should('be.enabled') const automationOwner = ls.addressBookData.sepoliaAddress2[11155111]['0xC16Db0251654C0a72E91B190d81eAD367d2C6fED'] -const expectedSpendOptions = ['0 of 0.17 ETH', '0.00001 of 0.05 ETH', '0 of 0.01 ETH'] +export const actionNames = { + resetAllowance: 'resetAllowance', + setAllowance: 'setAllowance', +} + +const expectedSpendOptions = ['0.02 of 0.17 ETH', '0.00001 of 0.05 ETH', '0 of 0.01 ETH'] const expectedResetOptions = new Array(3).fill('One-time') const newTransactionStr = 'New transaction' @@ -205,3 +213,25 @@ export function verifyCharErrorValidation() { export function verifyNumberAmountEntered(amount) { cy.get(tokenAmountFld).find('input').should('have.value', amount) } + +export function verifyActionCount(count) { + main.verifyElementsCount(actionItem, count) +} + +export function verifyActionNames(names) { + cy.get(allActionsSection) + .parent() + .within(() => { + names.forEach((item) => { + cy.contains(item) + }) + }) +} + +export function verifyDecodedTxSummary(names) { + cy.get(decodedTxSummary).within(() => { + names.forEach((item) => { + cy.contains(item) + }) + }) +} diff --git a/cypress/e2e/pages/swaps.pages.js b/cypress/e2e/pages/swaps.pages.js index efb734aca..75cbc3d5d 100644 --- a/cypress/e2e/pages/swaps.pages.js +++ b/cypress/e2e/pages/swaps.pages.js @@ -3,7 +3,7 @@ import * as main from '../pages/main.page.js' import * as create_tx from '../pages/create_tx.pages.js' export const inputCurrencyInput = '[id="input-currency-input"]' -export const outputurrencyInput = '[id="output-currency-input"]' +export const outputCurrencyInput = '[id="output-currency-input"]' const tokenList = '[id="tokens-list"]' export const swapBtn = '[id="swap-button"]' const exceedFeesChkbox = 'input[id="fees-exceed-checkbox"]' @@ -173,18 +173,38 @@ export function verifySelectedInputCurrancy(option) { cy.get('span').contains(option).should('be.visible') }) } -export function selectInputCurrency(option) { - cy.get(inputCurrencyInput).within(() => { - cy.get('button').eq(0).trigger('mouseover').trigger('click') + +function selectCurrency(inputSelector, option) { + cy.get(inputSelector).within(() => { + cy.get('button') + .eq(0) + .invoke('text') + .then(($value) => { + cy.log('*** Currency value ' + $value) + if (!$value.includes(option)) { + cy.log('*** Currency value is different from specified') + cy.get('button').eq(0).trigger('mouseover').trigger('click') + cy.wrap(true).as('isAction') + } else { + cy.wrap(false).as('isAction') + } + }) }) - cy.get(tokenList).find('span').contains(option).click() + + cy.get('@isAction').then((isAction) => { + if (isAction) { + cy.log('*** Clicking on token option') + cy.get(tokenList).find('span').contains(option).click() + } + }) +} + +export function selectInputCurrency(option) { + selectCurrency(inputCurrencyInput, option) } export function selectOutputCurrency(option) { - cy.get(outputurrencyInput).within(() => { - cy.get('button').trigger('mouseover').trigger('click') - }) - cy.get(tokenList).find('span').contains(option).click() + selectCurrency(outputCurrencyInput, option) } export function setInputValue(value) { @@ -194,7 +214,7 @@ export function setInputValue(value) { } export function setOutputValue(value) { - cy.get(outputurrencyInput).within(() => { + cy.get(outputCurrencyInput).within(() => { cy.get('input').type(value) }) } @@ -254,18 +274,19 @@ export function verifyOrderIDUrl() { cy.get(create_tx.txRowTitle) .contains(orderIdStr) .parent() + .parent() .within(() => { 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) + cy.contains(limitPrice) + cy.contains(expiry) + cy.contains(slippage) + cy.contains(oderID) + cy.contains(widgetFee) + cy.contains(interactWith) } export function verifyRecipientAlertIsDisplayed() { diff --git a/cypress/e2e/pages/tables.page.js b/cypress/e2e/pages/tables.page.js new file mode 100644 index 000000000..de71b5632 --- /dev/null +++ b/cypress/e2e/pages/tables.page.js @@ -0,0 +1 @@ +export const dataRow = '[data-testid="tx-data-row"]' diff --git a/cypress/e2e/prodhealthcheck/add_owner.cy.js b/cypress/e2e/prodhealthcheck/add_owner.cy.js new file mode 100644 index 000000000..539ecc8ce --- /dev/null +++ b/cypress/e2e/prodhealthcheck/add_owner.cy.js @@ -0,0 +1,31 @@ +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('[PROD] Add Owners tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.visit(constants.prodbaseUrl + constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) + cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) + }) + + // TODO: Added to prod + it('Verify add owner button is disabled for disconnected user', () => { + owner.verifyAddOwnerBtnIsDisabled() + }) + + // TODO: Added to prod + it.skip('Verify the Add New Owner Form can be opened', () => { + wallet.connectSigner(signer) + owner.openAddOwnerWindow() + }) +}) diff --git a/cypress/e2e/prodhealthcheck/create_tx.cy.js b/cypress/e2e/prodhealthcheck/create_tx.cy.js new file mode 100644 index 000000000..9f3df5161 --- /dev/null +++ b/cypress/e2e/prodhealthcheck/create_tx.cy.js @@ -0,0 +1,46 @@ +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() + createtx.setSendValue(sendValue) + createtx.clickOnNextBtn() +} + +describe.skip('[PROD] Create transactions tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.visit(constants.prodbaseUrl + constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_6) + wallet.connectSigner(signer) + createtx.clickOnNewtransactionBtn() + createtx.clickOnSendTokensBtn() + }) + + // TODO: Added to prod + it('Verify submitting a tx and that clicking on notification shows the transaction in queue', () => { + happyPathToStepTwo() + createtx.verifySubmitBtnIsEnabled() + createtx.changeNonce(14) + cy.wait(1000) + createtx.clickOnSignTransactionBtn() + createtx.waitForProposeRequest() + createtx.clickViewTransaction() + createtx.verifySingleTxPage() + createtx.verifyQueueLabel() + createtx.verifyTransactionSummary(sendValue) + }) +}) diff --git a/cypress/e2e/prodhealthcheck/load_safe.cy.js b/cypress/e2e/prodhealthcheck/load_safe.cy.js new file mode 100644 index 000000000..8d1e630c3 --- /dev/null +++ b/cypress/e2e/prodhealthcheck/load_safe.cy.js @@ -0,0 +1,43 @@ +import 'cypress-file-upload' +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as safe from '../pages/load_safe.pages' +import * as createwallet from '../pages/create_wallet.pages' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let staticSafes = [] + +const testSafeName = 'Test safe name' +const testOwnerName = 'Test Owner Name' +// TODO +const SAFE_ENS_NAME = 'test20.eth' +const SAFE_ENS_NAME_TRANSLATED = constants.EOA + +const EOA_ADDRESS = constants.EOA + +const INVALID_ADDRESS_ERROR_MSG = 'Address given is not a valid Safe address' + +// TODO +const OWNER_ENS_DEFAULT_NAME = 'test20.eth' +const OWNER_ADDRESS = constants.EOA + +describe('[PROD] Load Safe tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.visit(constants.prodbaseUrl + constants.loadNewSafeSepoliaUrl) + cy.wait(2000) + }) + + // TODO: Added to prod + it('Verify Safe and owner names are displayed in the Review step', () => { + safe.inputNameAndAddress(testSafeName, staticSafes.SEP_STATIC_SAFE_4) + safe.clickOnNextBtn() + createwallet.typeOwnerName(testOwnerName, 0) + safe.clickOnNextBtn() + safe.verifyDataInReviewSection(testSafeName, testOwnerName) + safe.clickOnAddBtn() + }) +}) diff --git a/cypress/e2e/prodhealthcheck/messages_onchain.cy.js b/cypress/e2e/prodhealthcheck/messages_onchain.cy.js new file mode 100644 index 000000000..5b4f0928c --- /dev/null +++ b/cypress/e2e/prodhealthcheck/messages_onchain.cy.js @@ -0,0 +1,28 @@ +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 * as msg_data from '../../fixtures/txmessages_data.json' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let staticSafes = [] + +const typeMessagesOnchain = msg_data.type.onChain + +describe('[PROD] Onchain Messages tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.visit(constants.prodbaseUrl + constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_10) + }) + + // TODO: Added to prod + it('Verify summary for signed on-chain message', () => { + createTx.verifySummaryByName( + typeMessagesOnchain.contractName, + [typeMessagesOnchain.success, typeMessagesOnchain.signMessage], + typeMessagesOnchain.altTmage, + ) + }) +}) diff --git a/cypress/e2e/prodhealthcheck/nfts.cy.js b/cypress/e2e/prodhealthcheck/nfts.cy.js new file mode 100644 index 000000000..dcc484f39 --- /dev/null +++ b/cypress/e2e/prodhealthcheck/nfts.cy.js @@ -0,0 +1,87 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as nfts from '../pages/nfts.pages' +import * as createTx from '../pages/create_tx.pages' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' + +const singleNFT = ['safeTransferFrom'] +const multipleNFT = ['multiSend'] +const multipleNFTAction = 'safeTransferFrom' +const NFTSentName = 'GTT #22' + +let nftsSafes, + staticSafes = [] + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + +describe.skip('[PROD] NFTs tests', () => { + before(() => { + getSafes(CATEGORIES.nfts) + .then((nfts) => { + nftsSafes = nfts + return getSafes(CATEGORIES.static) + }) + .then((statics) => { + staticSafes = statics + }) + }) + + beforeEach(() => { + cy.visit(constants.prodbaseUrl + constants.balanceNftsUrl + staticSafes.SEP_STATIC_SAFE_2) + wallet.connectSigner(signer) + nfts.waitForNftItems(2) + }) + + // TODO: Added to prod + // TODO: Add Sign action + it('Verify multipls NFTs can be selected and reviewed', () => { + nfts.verifyInitialNFTData() + nfts.selectNFTs(3) + nfts.deselectNFTs([2], 3) + nfts.sendNFT() + nfts.verifyNFTModalData() + nfts.typeRecipientAddress(staticSafes.SEP_STATIC_SAFE_1) + nfts.clikOnNextBtn() + nfts.verifyReviewModalData(2) + }) + + // TODO: Added to prod + it('Verify that when 2 NFTs are selected, actions and tx details are correct in Review step', () => { + nfts.verifyInitialNFTData() + nfts.selectNFTs(2) + nfts.sendNFT() + nfts.typeRecipientAddress(staticSafes.SEP_STATIC_SAFE_1) + nfts.clikOnNextBtn() + nfts.verifyTxDetails(multipleNFT) + nfts.verifyCountOfActions(2) + nfts.verifyActionName(0, multipleNFTAction) + nfts.verifyActionName(1, multipleNFTAction) + }) + + // TODO: Added to prod + it('Verify Send button is disabled for non-owner', () => { + cy.visit(constants.balanceNftsUrl + nftsSafes.SEP_NFT_SAFE_2) + nfts.verifyInitialNFTData() + nfts.selectNFTs(1) + nfts.verifySendNFTBtnDisabled() + }) + + // TODO: Added to prod + it('Verify Send NFT transaction has been created', () => { + cy.visit(constants.balanceNftsUrl + nftsSafes.SEP_NFT_SAFE_1) + nfts.verifyInitialNFTData() + nfts.selectNFTs(1) + nfts.sendNFT() + nfts.typeRecipientAddress(staticSafes.SEP_STATIC_SAFE_1) + createTx.changeNonce(2) + nfts.clikOnNextBtn() + createTx.clickOnSignTransactionBtn() + createTx.waitForProposeRequest() + createTx.clickViewTransaction() + createTx.verifySingleTxPage() + createTx.verifyQueueLabel() + createTx.verifyTransactionStrExists(NFTSentName) + }) +}) diff --git a/cypress/e2e/prodhealthcheck/recovery.cy.js b/cypress/e2e/prodhealthcheck/recovery.cy.js new file mode 100644 index 000000000..e201a66a3 --- /dev/null +++ b/cypress/e2e/prodhealthcheck/recovery.cy.js @@ -0,0 +1,49 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as recovery from '../pages/recovery.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let recoverySafes, + staticSafes = [] + +describe('[PROD] Production recovery health check tests', { defaultCommandTimeout: 50000 }, () => { + before(() => { + getSafes(CATEGORIES.recovery) + .then((recoveries) => { + recoverySafes = recoveries + return getSafes(CATEGORIES.static) + }) + .then((statics) => { + staticSafes = statics + }) + }) + + it('Verify that the Security section contains Account recovery block on supported netwroks', () => { + const safes = [ + staticSafes.ETH_STATIC_SAFE_15, + staticSafes.GNO_STATIC_SAFE_16, + staticSafes.MATIC_STATIC_SAFE_17, + staticSafes.SEP_STATIC_SAFE_13, + ] + + safes.forEach((safe) => { + cy.visit(constants.prodbaseUrl + constants.securityUrl + safe) + recovery.getSetupRecoveryBtn() + }) + }) + + it('Verify that the Security and Login section does not contain Account recovery block on unsupported networks', () => { + const safes = [ + staticSafes.BNB_STATIC_SAFE_18, + staticSafes.AURORA_STATIC_SAFE_19, + staticSafes.AVAX_STATIC_SAFE_20, + staticSafes.LINEA_STATIC_SAFE_21, + staticSafes.ZKSYNC_STATIC_SAFE_22, + ] + + safes.forEach((safe) => { + cy.visit(constants.prodbaseUrl + constants.securityUrl + safe) + main.verifyElementsCount(recovery.setupRecoveryBtn, 0) + }) + }) +}) diff --git a/cypress/e2e/prodhealthcheck/remove_owner.cy.js b/cypress/e2e/prodhealthcheck/remove_owner.cy.js new file mode 100644 index 000000000..755f320db --- /dev/null +++ b/cypress/e2e/prodhealthcheck/remove_owner.cy.js @@ -0,0 +1,41 @@ +import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +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('[PROD] Remove Owners tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.visit(constants.prodbaseUrl + constants.setupUrl + staticSafes.SEP_STATIC_SAFE_13) + main.waitForHistoryCallToComplete() + cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) + }) + + // TODO: Added to prod + it.skip('Verify owner deletion transaction has been created', () => { + wallet.connectSigner(signer) + owner.waitForConnectionStatus() + owner.openRemoveOwnerWindow(1) + cy.wait(3000) + createwallet.clickOnNextBtn() + //This method creates the @removedAddress alias + owner.getAddressToBeRemoved() + owner.verifyOwnerDeletionWindowDisplayed() + createTx.changeNonce(10) + createTx.clickOnSignTransactionBtn() + createTx.waitForProposeRequest() + createTx.clickViewTransaction() + createTx.clickOnTransactionItemByName('removeOwner') + createTx.verifyTxDestinationAddress('@removedAddress') + }) +}) diff --git a/cypress/e2e/prodhealthcheck/sidebar.cy.js b/cypress/e2e/prodhealthcheck/sidebar.cy.js new file mode 100644 index 000000000..edb534286 --- /dev/null +++ b/cypress/e2e/prodhealthcheck/sidebar.cy.js @@ -0,0 +1,43 @@ +import * as constants from '../../support/constants' +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('[PROD] Sidebar tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.visit(constants.prodbaseUrl + constants.homeUrl + staticSafes.SEP_STATIC_SAFE_9) + }) + + // TODO: Added to prod + it('Verify current safe details', () => { + sideBar.verifySafeHeaderDetails(sideBar.testSafeHeaderDetails) + }) + + // TODO: Added to prod + it.skip('Verify New transaction button enabled for owners', () => { + wallet.connectSigner(signer) + sideBar.verifyNewTxBtnStatus(constants.enabledStates.enabled) + }) + + // TODO: Added to prod + it.skip('Verify New transaction button enabled for beneficiaries who are non-owners', () => { + cy.visit(constants.prodbaseUrl + constants.homeUrl + staticSafes.SEP_STATIC_SAFE_11) + wallet.connectSigner(signer) + sideBar.verifyNewTxBtnStatus(constants.enabledStates.enabled) + }) + + // TODO: Added to prod + it('Verify New Transaction button disabled for non-owners', () => { + main.verifyElementsCount(navigation.newTxBtn, 0) + }) +}) diff --git a/cypress/e2e/prodhealthcheck/sidebar_3.cy.js b/cypress/e2e/prodhealthcheck/sidebar_3.cy.js new file mode 100644 index 000000000..218d5f747 --- /dev/null +++ b/cypress/e2e/prodhealthcheck/sidebar_3.cy.js @@ -0,0 +1,66 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.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' +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.skip('[PROD] Sidebar tests 3', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + // TODO: Added to prod + it('Verify the "My accounts" counter at the top is counting all safes the user owns', () => { + cy.visit(constants.prodbaseUrl + constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafes.safe1, sideBar.sideBarSafes.safe2], + }) + wallet.connectSigner(signer) + sideBar.openSidebar() + sideBar.checkMyAccountCounter(2) + }) + + // TODO: Added to prod + it('Verify pending signature is displayed in sidebar for unsigned tx', () => { + cy.visit(constants.prodbaseUrl + constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_7) + 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) + }) + + // TODO: Added to prod + it('Verify balance exists in a tx in sidebar', () => { + cy.visit(constants.prodbaseUrl + constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_7) + wallet.connectSigner(signer) + owner.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + wallet.connectSigner(signer) + cy.intercept('GET', constants.safeListEndpoint, { + 11155111: [sideBar.sideBarSafesPendingActions.safe1], + }) + sideBar.openSidebar() + sideBar.verifyTxToConfirmDoesNotExist() + sideBar.checkBalanceExists() + }) +}) diff --git a/cypress/e2e/prodhealthcheck/spending_limits.cy.js b/cypress/e2e/prodhealthcheck/spending_limits.cy.js new file mode 100644 index 000000000..e1f663f6d --- /dev/null +++ b/cypress/e2e/prodhealthcheck/spending_limits.cy.js @@ -0,0 +1,55 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as spendinglimit from '../pages/spending_limits.pages' +import * as navigation from '../pages/navigation.page' +import * as tx from '../pages/create_tx.pages' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' + +let staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY +const signerAddress = walletCredentials.OWNER_4_WALLET_ADDRESS + +const tokenAmount = 0.1 +const newTokenAmount = 0.001 +const spendingLimitBalance = '(0.15 ETH)' + +describe('[PROD] Spending limits tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.visit(constants.prodbaseUrl + constants.setupUrl + staticSafes.SEP_STATIC_SAFE_8) + cy.get(spendinglimit.spendingLimitsSection).should('be.visible') + }) + + // TODO: Added to prod + it.skip('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) + spendinglimit.clickOnNextBtn() + spendinglimit.checkReviewData( + tokenAmount, + staticSafes.SEP_STATIC_SAFE_6, + spendinglimit.timePeriodOptions.oneTime.split(' ').join('-'), + ) + }) + + // TODO: Added to prod + it('Verify values and trash icons are displayed in Beneficiary table', () => { + spendinglimit.verifyBeneficiaryTable() + }) + + // TODO: Added to prod + it.skip('Verify Spending limit option is available when selecting the corresponding token', () => { + wallet.connectSigner(signer) + navigation.clickOnNewTxBtn() + tx.clickOnSendTokensBtn() + spendinglimit.verifyTxOptionExist([spendinglimit.spendingLimitTxOption]) + }) +}) diff --git a/cypress/e2e/prodhealthcheck/swaps_history_2.cy.js b/cypress/e2e/prodhealthcheck/swaps_history_2.cy.js new file mode 100644 index 000000000..96f76e9f4 --- /dev/null +++ b/cypress/e2e/prodhealthcheck/swaps_history_2.cy.js @@ -0,0 +1,65 @@ +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('[PROD] Swaps history tests 2', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + // TODO: Added to prod + it('Verify swap buy operation with 2 actions: approve & swap', { defaultCommandTimeout: 30000 }, () => { + cy.visit( + constants.prodbaseUrl + constants.transactionUrl + staticSafes.SEP_STATIC_SAFE_1 + swaps.swapTxs.buy2actions, + ) + 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, + ]) + }) + + // TODO: Added to prod + // TODO: Unskip after next release due to changes in design tx + it.skip( + 'Verify no decoding if tx was created using CowSwap safe-app in the history', + { defaultCommandTimeout: 30000 }, + () => { + cy.visit( + constants.prodbaseUrl + + constants.transactionUrl + + staticSafes.SEP_STATIC_SAFE_1 + + swaps.swapTxs.safeAppSwapOrder, + ) + 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]) + }, + ) +}) diff --git a/cypress/e2e/prodhealthcheck/swaps_tokens.cy.js b/cypress/e2e/prodhealthcheck/swaps_tokens.cy.js new file mode 100644 index 000000000..acc2f9a5f --- /dev/null +++ b/cypress/e2e/prodhealthcheck/swaps_tokens.cy.js @@ -0,0 +1,49 @@ +import * as constants from '../../support/constants.js' +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}"]` + +describe('[PROD] Swaps token tests', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.visit(constants.prodbaseUrl + constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_1) + }) + + // TODO: Added to prod + it.skip( + '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) + swaps.acceptLegalDisclaimer() + cy.wait(2000) + main.getIframeBody(iframeSelector).within(() => { + swaps.verifySelectedInputCurrancy(swaps.swapTokens.eth) + }) + }, + ) + + // TODO: Added to prod + // TODO: Check why expected number of buttons not displayed sometimes + it.skip('Verify swap button are displayed in assets table and dashboard', () => { + assets.selectTokenList(assets.tokenListOptions.allTokens) + main.verifyElementsCount(swaps.assetsSwapBtn, 4) + cy.visit(constants.homeUrl + staticSafes.SEP_STATIC_SAFE_1) + main.verifyElementsCount(swaps.assetsSwapBtn, 4) + main.verifyElementsCount(swaps.dashboardSwapBtn, 1) + }) +}) diff --git a/cypress/e2e/prodhealthcheck/tokens.cy.js b/cypress/e2e/prodhealthcheck/tokens.cy.js new file mode 100644 index 000000000..14b18ab68 --- /dev/null +++ b/cypress/e2e/prodhealthcheck/tokens.cy.js @@ -0,0 +1,95 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as assets from '../pages/assets.pages' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +const ASSET_NAME_COLUMN = 0 +const TOKEN_AMOUNT_COLUMN = 1 +const FIAT_AMOUNT_COLUMN = 2 + +let staticSafes = [] + +describe('[PROD] Prod tokens tests', () => { + const fiatRegex = assets.fiatRegex + + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + beforeEach(() => { + cy.visit(constants.prodbaseUrl + constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_2) + }) + + // TODO: Added to prod + it('Verify that non-native tokens are present and have balance', () => { + assets.selectTokenList(assets.tokenListOptions.allTokens) + assets.verifyBalance(assets.currencyDaiCap, TOKEN_AMOUNT_COLUMN, assets.currencyDaiAlttext) + assets.verifyTokenBalanceFormat( + assets.currencyDaiCap, + assets.currencyDaiFormat_2, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + assets.verifyBalance(assets.currencyAave, TOKEN_AMOUNT_COLUMN, assets.currencyAaveAlttext) + assets.verifyTokenBalanceFormat( + assets.currencyAave, + assets.currentcyAaveFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + assets.verifyBalance(assets.currencyLink, TOKEN_AMOUNT_COLUMN, assets.currencyLinkAlttext) + assets.verifyTokenBalanceFormat( + assets.currencyLink, + assets.currentcyLinkFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + assets.verifyBalance(assets.currencyTestTokenA, TOKEN_AMOUNT_COLUMN, assets.currencyTestTokenAAlttext) + assets.verifyTokenBalanceFormat( + assets.currencyTestTokenA, + assets.currentcyTestTokenAFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + assets.verifyBalance(assets.currencyTestTokenB, TOKEN_AMOUNT_COLUMN, assets.currencyTestTokenBAlttext) + assets.verifyTokenBalanceFormat( + assets.currencyTestTokenB, + assets.currentcyTestTokenBFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + assets.verifyBalance(assets.currencyUSDC, TOKEN_AMOUNT_COLUMN, assets.currencyTestUSDCAlttext) + assets.verifyTokenBalanceFormat( + assets.currencyUSDC, + assets.currentcyTestUSDCFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + }) + + // TODO: Added to prod + //Include in smoke. + it('Verify that when owner is disconnected, Send button is disabled', () => { + assets.selectTokenList(assets.tokenListOptions.allTokens) + assets.showSendBtn(0) + assets.VerifySendButtonIsDisabled() + }) + + // TODO: Added to prod + it('Verify that when connected user is not owner, Send button is disabled', () => { + cy.visit(constants.prodbaseUrl + constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_3) + assets.selectTokenList(assets.tokenListOptions.allTokens) + assets.showSendBtn(0) + assets.VerifySendButtonIsDisabled() + }) +}) diff --git a/cypress/e2e/prodhealthcheck/tx_history.cy.js b/cypress/e2e/prodhealthcheck/tx_history.cy.js new file mode 100644 index 000000000..25bef3d1c --- /dev/null +++ b/cypress/e2e/prodhealthcheck/tx_history.cy.js @@ -0,0 +1,134 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as createTx from '../pages/create_tx.pages' +import * as data from '../../fixtures/txhistory_data_data.json' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let staticSafes = [] + +const typeCreateAccount = data.type.accountCreation +const typeReceive = data.type.receive +const typeSend = data.type.send +const typeSpendingLimits = data.type.spendingLimits +const typeDeleteAllowance = data.type.deleteSpendingLimit +const typeSideActions = data.type.sideActions +const typeGeneral = data.type.general + +describe('[PROD] Tx history tests 1', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.intercept( + 'GET', + `**${constants.stagingCGWChains}${constants.networkKeys.sepolia}/${ + constants.stagingCGWSafes + }${staticSafes.SEP_STATIC_SAFE_7.substring(4)}/transactions/history**`, + (req) => { + req.url = `https://safe-client.safe.global/v1/chains/11155111/safes/0x5912f6616c84024cD1aff0D5b55bb36F5180fFdb/transactions/history?timezone=Europe/Berlin&trusted=false&cursor=limit=100&offset=1` + req.continue() + }, + ).as('allTransactions') + + cy.visit(constants.prodbaseUrl + constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_7) + cy.wait('@allTransactions') + }) + + // TODO: Added to prod + // Account creation + it('Verify summary for account creation', () => { + createTx.verifySummaryByName( + typeCreateAccount.title, + [typeCreateAccount.actionsSummary, typeGeneral.statusOk], + typeCreateAccount.altTmage, + ) + }) + + // TODO: Added to prod + it('Verify exapanded details for account creation', () => { + createTx.clickOnTransactionItemByName(typeCreateAccount.title) + createTx.verifyExpandedDetails([ + typeCreateAccount.creator.actionTitle, + typeCreateAccount.creator.address, + typeCreateAccount.factory.actionTitle, + typeCreateAccount.factory.name, + typeCreateAccount.factory.address, + typeCreateAccount.masterCopy.actionTitle, + typeCreateAccount.masterCopy.name, + typeCreateAccount.masterCopy.address, + typeCreateAccount.transactionHash, + ]) + }) + + // TODO: Added to prod + // Token send + it('Verify exapanded details for token send', () => { + createTx.clickOnTransactionItemByName(typeSend.title, typeSend.summaryTxInfo) + createTx.verifyExpandedDetails([typeSend.sentTo, typeSend.recipientAddress, typeSend.transactionHash]) + createTx.verifyActionListExists([ + typeSideActions.created, + typeSideActions.confirmations, + typeSideActions.executedBy, + ]) + }) + + // TODO: Added to prod + // Spending limits + // TODO: Unskip after next release due to design tx + it.skip('Verify summary for setting spend limits', () => { + createTx.verifySummaryByName( + typeSpendingLimits.title, + typeSpendingLimits.summaryTxInfo, + [typeGeneral.statusOk], + typeSpendingLimits.altTmage, + ) + }) + + // TODO: Added to prod + // TODO: Unskip after next release due to design tx + it.skip('Verify exapanded details for initial spending limits setup', () => { + createTx.clickOnTransactionItemByName(typeSpendingLimits.title, typeSpendingLimits.summaryTxInfo) + createTx.verifyExpandedDetails( + [ + typeSpendingLimits.contractTitle, + typeSpendingLimits.call_multiSend, + typeSpendingLimits.transactionHash, + typeSpendingLimits.safeTxHash, + ], + createTx.delegateCallWarning, + ) + }) + + // TODO: Added to prod + // TODO: Unskip after next release due to design tx + it.skip('Verify that 3 actions exist in initial spending limits setup', () => { + createTx.clickOnTransactionItemByName(typeSpendingLimits.title, typeSpendingLimits.summaryTxInfo) + createTx.verifyActions([ + typeSpendingLimits.enableModule.title, + typeSpendingLimits.addDelegate.title, + typeSpendingLimits.setAllowance.title, + ]) + }) + + // Spending limit deletion + it('Verify exapanded details for allowance deletion', () => { + createTx.clickOnTransactionItemByName(typeDeleteAllowance.title, typeDeleteAllowance.summaryTxInfo) + createTx.verifyExpandedDetails([ + typeDeleteAllowance.description, + typeDeleteAllowance.beneficiary, + typeDeleteAllowance.beneficiaryAddress, + typeDeleteAllowance.transactionHash, + typeDeleteAllowance.safeTxHash, + typeDeleteAllowance.token, + typeDeleteAllowance.tokenName, + ]) + }) + + // TODO: Added to prod + it('Verify advanced details displayed in exapanded details for allowance deletion', () => { + createTx.clickOnTransactionItemByName(typeDeleteAllowance.title, typeDeleteAllowance.summaryTxInfo) + createTx.expandAdvancedDetails([typeDeleteAllowance.baseGas]) + createTx.collapseAdvancedDetails([typeDeleteAllowance.baseGas]) + }) +}) diff --git a/cypress/e2e/prodhealthcheck/tx_history_2.cy.js b/cypress/e2e/prodhealthcheck/tx_history_2.cy.js new file mode 100644 index 000000000..2532190e3 --- /dev/null +++ b/cypress/e2e/prodhealthcheck/tx_history_2.cy.js @@ -0,0 +1,140 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as createTx from '../pages/create_tx.pages' +import * as data from '../../fixtures/txhistory_data_data.json' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' + +let staticSafes = [] + +const typeOnchainRejection = data.type.onchainRejection +const typeBatch = data.type.batchNativeTransfer +const typeAddOwner = data.type.addOwner +const typeChangeOwner = data.type.swapOwner +const typeRemoveOwner = data.type.removeOwner +const typeDisableOwner = data.type.disableModule +const typeChangeThreshold = data.type.changeThreshold +const typeSideActions = data.type.sideActions +const typeGeneral = data.type.general +const typeUntrustedToken = data.type.untrustedReceivedToken + +describe('[PROD] Tx history tests 2', () => { + before(async () => { + staticSafes = await getSafes(CATEGORIES.static) + }) + + beforeEach(() => { + cy.intercept( + 'GET', + `**${constants.stagingCGWChains}${constants.networkKeys.sepolia}/${ + constants.stagingCGWSafes + }${staticSafes.SEP_STATIC_SAFE_7.substring(4)}/transactions/history**`, + (req) => { + req.url = `https://safe-client.safe.global/v1/chains/11155111/safes/0x5912f6616c84024cD1aff0D5b55bb36F5180fFdb/transactions/history?timezone=Europe/Berlin&trusted=false&cursor=limit=100&offset=1` + req.continue() + }, + ).as('allTransactions') + + cy.visit(constants.prodbaseUrl + constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_7) + }) + + it('Verify number of transactions is correct', () => { + createTx.verifyNumberOfTransactions(20) + }) + + // TODO: Added to prod + // On-chain rejection + it('Verify exapanded details for on-chain rejection', () => { + createTx.clickOnTransactionItemByName(typeOnchainRejection.title) + createTx.verifyExpandedDetails([ + typeOnchainRejection.description, + typeOnchainRejection.transactionHash, + typeOnchainRejection.safeTxHash, + ]) + createTx.verifyActionListExists([ + typeSideActions.rejectionCreated, + typeSideActions.confirmations, + typeSideActions.executedBy, + ]) + }) + + // TODO: Added to prod + // Batch transaction + // TODO: Unskip after next release due to design tx + it.skip('Verify exapanded details for batch', () => { + createTx.clickOnTransactionItemByName(typeBatch.title, typeBatch.summaryTxInfo) + createTx.verifyExpandedDetails( + [typeBatch.contractTitle, typeBatch.transactionHash, typeBatch.safeTxHash], + createTx.delegateCallWarning, + ) + createTx.verifyActions([typeBatch.nativeTransfer.title]) + }) + + // TODO: Added to prod + // Add owner + it('Verify summary for adding owner', () => { + createTx.verifySummaryByName(typeAddOwner.title, [typeGeneral.statusOk], typeAddOwner.altImage) + }) + + // TODO: Added to prod + // Change owner + it('Verify summary for changing owner', () => { + createTx.verifySummaryByName(typeChangeOwner.title, [typeGeneral.statusOk], typeChangeOwner.altImage) + }) + + // TODO: Added to prod + it('Verify exapanded details for changing owner', () => { + createTx.clickOnTransactionItemByName(typeChangeOwner.title) + createTx.verifyExpandedDetails([ + typeChangeOwner.description, + typeChangeOwner.newOwner.actionTitile, + typeChangeOwner.newOwner.ownerAddress, + typeChangeOwner.oldOwner.actionTitile, + typeChangeOwner.oldOwner.ownerAddress, + + typeChangeOwner.transactionHash, + typeChangeOwner.safeTxHash, + ]) + }) + + // TODO: Added to prod + // Remove owner + it('Verify summary for removing owner', () => { + createTx.verifySummaryByName(typeRemoveOwner.title, [typeGeneral.statusOk], typeRemoveOwner.altImage) + }) + + // TODO: Added to prod + // Disbale module + it('Verify summary for disable module', () => { + createTx.verifySummaryByName(typeDisableOwner.title, [typeGeneral.statusOk], typeDisableOwner.altImage) + }) + + // TODO: Added to prod + // Change threshold + it('Verify summary for changing threshold', () => { + createTx.verifySummaryByName( + typeChangeThreshold.title, + [typeChangeThreshold.summaryTxInfo, typeGeneral.statusOk], + typeChangeThreshold.altImage, + ) + }) + + // TODO: Added to prod + it('Verify exapanded details for changing threshold', () => { + createTx.clickOnTransactionItemByName(typeChangeThreshold.title) + createTx.verifyExpandedDetails( + [ + typeChangeThreshold.requiredConfirmationsTitle, + typeChangeThreshold.transactionHash, + typeChangeThreshold.safeTxHash, + ], + createTx.policyChangeWarning, + ) + createTx.checkRequiredThreshold(2) + }) + + // TODO: Added to prod + it('Verify that sender address of untrusted token will not be copied until agreed in warning popup', () => { + createTx.clickOnTransactionItemByName(typeUntrustedToken.summaryTitle, typeUntrustedToken.summaryTxInfo) + createTx.verifyAddressNotCopied(0, typeUntrustedToken.senderAddress) + }) +}) diff --git a/cypress/e2e/regression/add_owner.cy.js b/cypress/e2e/regression/add_owner.cy.js index 6129ab561..3bd525cfc 100644 --- a/cypress/e2e/regression/add_owner.cy.js +++ b/cypress/e2e/regression/add_owner.cy.js @@ -4,10 +4,14 @@ import * as owner from '../pages/owners.pages' import * as addressBook from '../pages/address_book.page' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' import * as wallet from '../../support/utils/wallet.js' +import * as createTx from '../pages/create_tx.pages.js' +import * as navigation from '../pages/navigation.page' +import { getEvents, events, checkDataLayerEvents } from '../../support/utils/gtag.js' let staticSafes = [] const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY +const signer2 = walletCredentials.OWNER_1_PRIVATE_KEY describe('Add Owners tests', () => { before(async () => { @@ -16,15 +20,15 @@ describe('Add Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) - cy.clearLocalStorage() - main.acceptCookies() cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) + // TODO: Added to prod it('Verify add owner button is disabled for disconnected user', () => { owner.verifyAddOwnerBtnIsDisabled() }) + // TODO: Added to prod it('Verify the Add New Owner Form can be opened', () => { wallet.connectSigner(signer) owner.openAddOwnerWindow() @@ -58,4 +62,45 @@ describe('Add Owners tests', () => { owner.clickOnNextBtn() owner.verifyConfirmTransactionWindowDisplayed() }) + + it( + 'Verify creation, confirmation and deletion of Add owner tx. GA tx_confirm', + { defaultCommandTimeout: 30000 }, + () => { + const tx_confirmed = [ + { + eventLabel: events.txConfirmedAddOwner.eventLabel, + eventCategory: events.txConfirmedAddOwner.category, + eventType: events.txConfirmedAddOwner.eventType, + safeAddress: staticSafes.SEP_STATIC_SAFE_24.slice(6), + }, + ] + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_24) + wallet.connectSigner(signer2) + owner.waitForConnectionStatus() + owner.openAddOwnerWindow() + owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) + createTx.changeNonce(1) + owner.clickOnNextBtn() + createTx.clickOnSignTransactionBtn() + createTx.clickViewTransaction() + + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + wallet.connectSigner(signer) + + createTx.clickOnConfirmTransactionBtn() + createTx.clickOnNoLaterOption() + createTx.clickOnSignTransactionBtn() + + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + wallet.connectSigner(signer2) + + createTx.deleteTx() + + getEvents() + checkDataLayerEvents(tx_confirmed) + }, + ) }) diff --git a/cypress/e2e/regression/address_book.cy.js b/cypress/e2e/regression/address_book.cy.js index 1413b95a0..16d37bcb3 100644 --- a/cypress/e2e/regression/address_book.cy.js +++ b/cypress/e2e/regression/address_book.cy.js @@ -24,8 +24,6 @@ describe('Address book tests', () => { beforeEach(() => { cy.visit(constants.addressBookUrl + staticSafes.SEP_STATIC_SAFE_4) - cy.clearLocalStorage() - main.acceptCookies() }) it('Verify owners name can be edited', () => { @@ -45,25 +43,6 @@ describe('Address book tests', () => { }) }) - //TODO: Rework to use Polygon. Replace Verify csv file can be imported (Goerli) with this test - it.skip('Verify that Sepolia and Polygon addresses can be imported', () => { - // Go to a Safe on Gnosis Chain - cy.get('header') - .contains(/^G(รถ|oe)rli$/) - .click() - cy.contains('Gnosis Chain').click() - - // Navigate to the Address Book page - cy.visit(`/address-book?safe=${constants.GNO_TEST_SAFE}`) - - // Waits for the Address Book table to be in the page - cy.contains('p', 'Address book').should('be.visible') - - // Finds the imported Gnosis Chain address - cy.contains(constants.GNO_CSV_ENTRY.name).should('exist') - cy.contains(constants.GNO_CSV_ENTRY.address).should('exist') - }) - it('Verify the address book file can be exported', () => { cy.wrap(null) .then(() => main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.dataSet)) @@ -79,8 +58,38 @@ describe('Address book tests', () => { addressBook.verifyExportMessage(12) addressBook.confirmExport() const downloadsFolder = Cypress.config('downloadsFolder') - //File reading is failing in the CI. Can be tested locally - cy.readFile(path.join(downloadsFolder, fileName)).should('exist') + + cy.readFile(path.join(downloadsFolder, fileName), 'utf-8').then((content) => { + const lines = content + .replace(/^\uFEFF/, '') + .trim() + .split('\r\n') + + const [header, ...dataLines] = lines + const actualData = dataLines.reduce((acc, line) => { + const [address, name, chainId] = line.split(',') + acc[chainId] = acc[chainId] || {} + acc[chainId][address] = name + return acc + }, {}) + + Object.keys(ls.addressBookData.dataSet).forEach((chainId) => { + cy.log(`Checking chainId: ${chainId}`) + + const actualChainData = actualData[chainId] || {} + const expectedChainData = ls.addressBookData.dataSet[chainId] + + Object.keys(expectedChainData).forEach((address) => { + const actualName = actualChainData[address] + const expectedName = expectedChainData[address] + + cy.log( + `ChainId: ${chainId}, Address: ${address}, Actual Name: ${actualName}, Expected Name: ${expectedName}`, + ) + expect(actualName).to.equal(expectedName) + }) + }) + }) }) }) diff --git a/cypress/e2e/regression/address_book_2.cy.js b/cypress/e2e/regression/address_book_2.cy.js index 9f49706fa..39859b85f 100644 --- a/cypress/e2e/regression/address_book_2.cy.js +++ b/cypress/e2e/regression/address_book_2.cy.js @@ -21,9 +21,6 @@ describe('Address book tests - 2', () => { beforeEach(() => { cy.visit(constants.addressBookUrl + staticSafes.SEP_STATIC_SAFE_4) - cy.clearLocalStorage() - cy.wait(1000) - main.acceptCookies() }) it('Verify Name and Address columns sorting works', () => { @@ -67,14 +64,6 @@ describe('Address book tests - 2', () => { addressBook.verifyNameWasChanged(owner1, onwer3) }) - it.skip('Verify copy to clipboard/Etherscan work as expected', () => { - main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sepoliaAddress1) - cy.wait(1000) - cy.reload() - createtx.verifyCopyIconWorks(0, constants.RECIPIENT_ADDRESS) - createtx.verifyNumberOfExternalLinks(1) - }) - it('Verify by default there 25 rows shown per page', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.pagination) cy.wait(1000) diff --git a/cypress/e2e/regression/balances_pagination.cy.js b/cypress/e2e/regression/balances_pagination.cy.js index b110310b9..8b81050e7 100644 --- a/cypress/e2e/regression/balances_pagination.cy.js +++ b/cypress/e2e/regression/balances_pagination.cy.js @@ -6,9 +6,7 @@ const ASSETS_LENGTH = 8 describe('Balance pagination tests', () => { before(() => { - cy.clearLocalStorage() 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 b9ecb1374..b8f1c5d3f 100644 --- a/cypress/e2e/regression/batch_tx.cy.js +++ b/cypress/e2e/regression/batch_tx.cy.js @@ -4,6 +4,8 @@ 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' +import * as ls from '../../support/localstorage_data.js' +import * as navigation from '../pages/navigation.page.js' const currentNonce = 3 const funds_first_tx = '0.001' @@ -12,6 +14,7 @@ const funds_second_tx = '0.002' let staticSafes = [] const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY +const signer2 = walletCredentials.OWNER_3_PRIVATE_KEY describe('Batch transaction tests', () => { before(async () => { @@ -19,15 +22,11 @@ describe('Batch transaction tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_2) wallet.connectSigner(signer) owner.waitForConnectionStatus() - main.acceptCookies() }) - // TODO: Check if localstorage can be used to add batches - // Rework test it('Verify the Add batch button is present in a transaction form', () => { //The "true" is to validate that the add to batch button is not visible if "Yes, execute" is selected batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) @@ -41,4 +40,51 @@ describe('Batch transaction tests', () => { batch.clickOnBatchCounter() batch.verifyAmountTransactionsInBatch(2) }) + + it('Verify that clicking on "Confirm batch" button opens confirm batch modal with listed transactions', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__batch, ls.batchData.entry0) + cy.reload() + batch.clickOnBatchCounter() + batch.clickOnConfirmBatchBtn() + cy.contains(funds_first_tx).parents('ul').as('TransactionList') + cy.get('@TransactionList').find('li').eq(0).contains(funds_first_tx) + cy.get('@TransactionList').find('li').eq(1).contains(funds_second_tx) + cy.contains(batch.addToBatchBtn).should('have.length', 0) + }) + + it('Verify the "New transaction" button in Add batch modal is enabled/disabled for different users types', () => { + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + wallet.connectSigner(signer) + batch.openBatchtransactionsModal() + batch.verifyNewTxButtonStatus(constants.enabledStates.enabled) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + wallet.connectSigner(signer2) + owner.waitForConnectionStatus() + batch.verifyNewTxButtonStatus(constants.enabledStates.disabled) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + batch.verifyNewTxButtonStatus(constants.enabledStates.disabled) + }) + + it('Verify a batched tx can be expanded and collapsed', () => { + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__batch, ls.batchData.entry0) + cy.reload() + batch.clickOnBatchCounter() + cy.contains(funds_first_tx).parents('ul').as('TransactionList') + cy.get('@TransactionList').find('li').eq(0).contains(funds_first_tx).click() + batch.isTxExpanded(0, true) + cy.get('@TransactionList').find('li').eq(0).contains(funds_first_tx).click() + batch.isTxExpanded(0, false) + }) + + it('Verify that the Add batch button is not present on non-safe pages', () => { + const urls = [constants.welcomeUrl, constants.appSettingsUrl, constants.appsUrl] + + urls.forEach((url) => { + cy.visit(url) + cy.get(batch.batchTxTopBar).should('not.exist') + }) + }) }) diff --git a/cypress/e2e/regression/beamer.cy.js b/cypress/e2e/regression/beamer.cy.js deleted file mode 100644 index 7dd046e3d..000000000 --- a/cypress/e2e/regression/beamer.cy.js +++ /dev/null @@ -1,26 +0,0 @@ -import * as constants from '../../support/constants' -import * as addressbook from '../pages/address_book.page' -import * as main from '../../e2e/pages/main.page' -import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' - -let staticSafes = [] - -describe('Beamer tests', () => { - before(async () => { - staticSafes = await getSafes(CATEGORIES.static) - cy.clearLocalStorage() - cy.visit(constants.addressBookUrl + staticSafes.SEP_STATIC_SAFE_4) - main.acceptCookies() - }) - - it.skip('Verify "Updates" cookie acceptance is required before displaying Beamer', () => { - addressbook.clickOnWhatsNewBtn() - addressbook.acceptBeamerCookies() - addressbook.verifyBeamerIsChecked() - main.acceptCookies() - // wait for Beamer cookies to be set - cy.wait(1000) - addressbook.clickOnWhatsNewBtn(true) // clicks through the "lastPostTitle" - addressbook.verifyBeameriFrameExists() - }) -}) diff --git a/cypress/e2e/regression/bulk_execution.cy.js b/cypress/e2e/regression/bulk_execution.cy.js new file mode 100644 index 000000000..c48467aab --- /dev/null +++ b/cypress/e2e/regression/bulk_execution.cy.js @@ -0,0 +1,86 @@ +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 { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' +import * as data from '../../fixtures/txhistory_data_data.json' + +let staticSafes, + fundsSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + +const typeBulkTx = data.type.bulkTransaction + +describe('Bulk execution', () => { + before(() => { + getSafes(CATEGORIES.funds) + .then((funds) => { + fundsSafes = funds + return getSafes(CATEGORIES.static) + }) + .then((statics) => { + staticSafes = statics + }) + }) + + it('Verify that Bulk Execution is available for a few fully signed txs located one by one', () => { + cy.visit(constants.transactionQueueUrl + fundsSafes.SEP_FUNDS_SAFE_14) + main.acceptCookies() + wallet.connectSigner(signer) + create_tx.verifyBulkExecuteBtnIsEnabled(2) + create_tx.verifyEnabledBulkExecuteBtnTooltip() + }) + + it( + 'Verify that "Confirm bulk execution" screen contains only available for execution txs in the actions list', + { defaultCommandTimeout: 30000 }, + () => { + const actions = ['1transfer', '2removeOwner'] + + cy.visit(constants.transactionQueueUrl + fundsSafes.SEP_FUNDS_SAFE_14) + wallet.connectSigner(signer) + main.acceptCookies() + create_tx.verifyBulkExecuteBtnIsEnabled(2).click() + create_tx.verifyBulkConfirmationScreen(2, actions) + }, + ) + + it( + 'Verify bulk view for the txs with the same tx hash in the History (tx executed via bulk feature)', + { defaultCommandTimeout: 30000 }, + () => { + const actions = ['Wrapped Ether', 'addOwnerWithThreshold', 'Sent'] + const tx = '3 transactions' + + cy.visit(constants.transactionsHistoryUrl + fundsSafes.SEP_FUNDS_SAFE_14) + wallet.connectSigner(signer) + main.acceptCookies() + create_tx.verifyBulkTxHistoryBlock(tx, actions) + }, + ) + + it( + 'Verify bulk view for the outgoing and incoming txs in the History after swap', + { defaultCommandTimeout: 30000 }, + () => { + const data = [typeBulkTx.receive, typeBulkTx.send, typeBulkTx.COW, typeBulkTx.DAI] + const tx = typeBulkTx.twoTx + + cy.visit(constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_1) + main.acceptCookies() + + create_tx.verifyBulkTxHistoryBlock(tx, data) + }, + ) + + it( + 'Verify that Bulk Execution button is disabled if the tx in Next is not fully signed', + { defaultCommandTimeout: 30000 }, + () => { + cy.visit(constants.transactionQueueUrl + fundsSafes.SEP_FUNDS_SAFE_15) + main.acceptCookies() + create_tx.verifyBulkExecuteBtnIsDisabled() + }, + ) +}) diff --git a/cypress/e2e/regression/create_safe_cf.cy.js b/cypress/e2e/regression/create_safe_cf.cy.js index 4b936701d..c1acac991 100644 --- a/cypress/e2e/regression/create_safe_cf.cy.js +++ b/cypress/e2e/regression/create_safe_cf.cy.js @@ -10,7 +10,8 @@ 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 +// DO NOT use OWNER_2_PRIVATE_KEY for safe creation. Used for CF safes. +const signer = walletCredentials.OWNER_2_PRIVATE_KEY const txOrder = [ 'Activate Safe now', @@ -27,9 +28,7 @@ describe('CF Safe regression tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() - cy.visit(constants.homeUrl + staticSafes.SEP_STATIC_SAFE_14) - main.acceptCookies() + cy.visit(constants.homeUrl + staticSafes.SEP_STATIC_SAFE_0) }) it('Verify Add native assets and Create tx modals can be opened', () => { @@ -39,10 +38,10 @@ describe('CF Safe regression tests', () => { owner.waitForConnectionStatus() createwallet.clickOnAddFundsBtn() main.verifyElementsIsVisible([createwallet.qrCode]) - navigation.clickOnModalCloseBtn() + navigation.clickOnModalCloseBtn(0) createwallet.clickOnCreateTxBtn() - navigation.clickOnModalCloseBtn() + navigation.clickOnModalCloseBtn(0) }) it('Verify "0 out of 2 step completed" is shown in the dashboard', () => { @@ -87,7 +86,7 @@ describe('CF Safe regression tests', () => { owner.waitForConnectionStatus() createwallet.clickOnCreateTxBtn() createwallet.clickOnTxType(txOrder[0]) - main.verifyElementsExist([createwallet.activateAccountBtn]) + cy.contains(createwallet.deployWalletStr) }) it('Verify "Add another Owner" takes to a tx Add owner', () => { @@ -123,10 +122,6 @@ describe('CF Safe regression tests', () => { 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() wallet.connectSigner(signer) owner.waitForConnectionStatus() @@ -140,21 +135,22 @@ describe('CF Safe regression tests', () => { it('Verify "Notifications" in the settings are disabled', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() - cy.visit(constants.notificationsUrl + staticSafes.SEP_STATIC_SAFE_14) + cy.visit(constants.notificationsUrl + staticSafes.SEP_STATIC_SAFE_0) createwallet.checkNotificationsSwitchIs(constants.enabledStates.disabled) }) it('Verify in assets, that a "Add funds" block is present', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) cy.reload() - cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_14) + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_0) main.verifyElementsIsVisible([createwallet.addFundsSection, createwallet.noTokensAlert]) }) it('Verify clicking on "Activate now" button opens safe activation flow', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__undeployedSafes, ls.undeployedSafe.safe1) - cy.reload() - cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_14) + cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_0) + wallet.connectSigner(signer) + owner.waitForConnectionStatus() createwallet.clickOnActivateAccountBtn() main.verifyElementsIsVisible([createwallet.activateAccountBtn]) }) diff --git a/cypress/e2e/regression/create_safe_simple.cy.js b/cypress/e2e/regression/create_safe_simple.cy.js index 2580029ba..620ad6e31 100644 --- a/cypress/e2e/regression/create_safe_simple.cy.js +++ b/cypress/e2e/regression/create_safe_simple.cy.js @@ -11,8 +11,6 @@ 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() }) @@ -134,9 +132,7 @@ describe('Safe creation tests', () => { main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.sameOwnerName), ) .then(() => { - wallet.connectSigner(signer) - createwallet.clickOnContinueWithWalletBtn() - createwallet.clickOnCreateNewSafeBtn() + createwallet.waitForConnectionMsgDisappear() createwallet.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() createwallet.clickOnSignerAddressInput(1) diff --git a/cypress/e2e/regression/create_safe_simple_2.cy.js b/cypress/e2e/regression/create_safe_simple_2.cy.js index f0ad5bf2b..0a10dda33 100644 --- a/cypress/e2e/regression/create_safe_simple_2.cy.js +++ b/cypress/e2e/regression/create_safe_simple_2.cy.js @@ -15,8 +15,6 @@ const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('Safe creation tests 2', () => { beforeEach(() => { cy.visit(constants.welcomeUrl + '?chain=sep') - cy.clearLocalStorage() - main.acceptCookies() }) it('Cancel button cancels safe creation', () => { diff --git a/cypress/e2e/regression/create_tx.cy.js b/cypress/e2e/regression/create_tx.cy.js index 914828302..f7dfd03f7 100644 --- a/cypress/e2e/regression/create_tx.cy.js +++ b/cypress/e2e/regression/create_tx.cy.js @@ -24,21 +24,19 @@ describe('Create transactions tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_6) - main.acceptCookies() wallet.connectSigner(signer) createtx.clickOnNewtransactionBtn() createtx.clickOnSendTokensBtn() }) + // TODO: Added to prod it('Verify submitting a tx and that clicking on notification shows the transaction in queue', () => { happyPathToStepTwo() createtx.verifySubmitBtnIsEnabled() createtx.changeNonce(14) cy.wait(1000) createtx.clickOnSignTransactionBtn() - createtx.waitForProposeRequest() createtx.clickViewTransaction() createtx.verifySingleTxPage() createtx.verifyQueueLabel() diff --git a/cypress/e2e/regression/load_safe.cy.js b/cypress/e2e/regression/load_safe.cy.js index 87533108d..83f3f80d0 100644 --- a/cypress/e2e/regression/load_safe.cy.js +++ b/cypress/e2e/regression/load_safe.cy.js @@ -27,9 +27,7 @@ describe('Load Safe tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.loadNewSafeSepoliaUrl) - main.acceptCookies() cy.wait(2000) }) @@ -40,6 +38,7 @@ describe('Load Safe tests', () => { safe.clickOnNextBtn() }) + // TODO: Added to prod it('Verify Safe and owner names are displayed in the Review step', () => { safe.inputNameAndAddress(testSafeName, staticSafes.SEP_STATIC_SAFE_4) safe.clickOnNextBtn() diff --git a/cypress/e2e/regression/load_safe_2.cy.js b/cypress/e2e/regression/load_safe_2.cy.js index 0cd807c43..e5750ec3d 100644 --- a/cypress/e2e/regression/load_safe_2.cy.js +++ b/cypress/e2e/regression/load_safe_2.cy.js @@ -26,9 +26,7 @@ describe('Load Safe tests 2', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.loadNewSafeSepoliaUrl) - main.acceptCookies() cy.wait(2000) }) diff --git a/cypress/e2e/regression/messages_onchain.cy.js b/cypress/e2e/regression/messages_onchain.cy.js index 492e38fba..88039368f 100644 --- a/cypress/e2e/regression/messages_onchain.cy.js +++ b/cypress/e2e/regression/messages_onchain.cy.js @@ -14,28 +14,18 @@ describe('Onchain Messages tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_10) - main.acceptCookies() }) it('Verify exapanded details for signed on-chain message', () => { createTx.clickOnTransactionItemByName(typeMessagesOnchain.contractName) - createTx.verifyExpandedDetails([ - typeMessagesOnchain.contractName, - typeMessagesOnchain.contractAddress, - typeMessagesOnchain.delegateCall, - ]) + createTx.verifyExpandedDetails([typeMessagesOnchain.contractName, typeMessagesOnchain.delegateCall]) }) it('Verify exapanded details for unsigned on-chain message', () => { cy.visit(constants.transactionQueueUrl + staticSafes.SEP_STATIC_SAFE_10) createTx.clickOnTransactionItemByName(typeMessagesOnchain.contractName) - createTx.verifyExpandedDetails([ - typeMessagesOnchain.contractName, - typeMessagesOnchain.contractAddress, - typeMessagesOnchain.delegateCall, - ]) + createTx.verifyExpandedDetails([typeMessagesOnchain.contractName, typeMessagesOnchain.delegateCall]) }) it('Verify summary for unsigned on-chain message', () => { @@ -47,6 +37,7 @@ describe('Onchain Messages tests', () => { ) }) + // TODO: Added to prod it('Verify summary for signed on-chain message', () => { createTx.verifySummaryByName( typeMessagesOnchain.contractName, diff --git a/cypress/e2e/regression/messages_popup.cy.js b/cypress/e2e/regression/messages_popup.cy.js index 441debb12..a679e6cc9 100644 --- a/cypress/e2e/regression/messages_popup.cy.js +++ b/cypress/e2e/regression/messages_popup.cy.js @@ -18,9 +18,7 @@ describe('Messages popup window tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.appsCustomUrl + staticSafes.SEP_STATIC_SAFE_10) - main.acceptCookies() iframeSelector = `iframe[id="iframe-${constants.safeTestAppurl}"]` }) @@ -33,10 +31,6 @@ describe('Messages popup window tests', () => { 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) apps.clickOnOpenSafeAppBtn() @@ -56,10 +50,7 @@ describe('Messages popup window tests', () => { 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) apps.clickOnOpenSafeAppBtn() diff --git a/cypress/e2e/regression/nfts.cy.js b/cypress/e2e/regression/nfts.cy.js index 386af6267..429ae3a23 100644 --- a/cypress/e2e/regression/nfts.cy.js +++ b/cypress/e2e/regression/nfts.cy.js @@ -30,13 +30,12 @@ describe('NFTs tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.balanceNftsUrl + staticSafes.SEP_STATIC_SAFE_2) - main.acceptCookies() wallet.connectSigner(signer) nfts.waitForNftItems(2) }) + // TODO: Added to prod // TODO: Add Sign action it('Verify multipls NFTs can be selected and reviewed', () => { nfts.verifyInitialNFTData() @@ -59,6 +58,7 @@ describe('NFTs tests', () => { nfts.verifyCountOfActions(0) }) + // TODO: Added to prod it('Verify that when 2 NFTs are selected, actions and tx details are correct in Review step', () => { nfts.verifyInitialNFTData() nfts.selectNFTs(2) @@ -71,6 +71,7 @@ describe('NFTs tests', () => { nfts.verifyActionName(1, multipleNFTAction) }) + // TODO: Added to prod it('Verify Send button is disabled for non-owner', () => { cy.visit(constants.balanceNftsUrl + nftsSafes.SEP_NFT_SAFE_2) nfts.verifyInitialNFTData() @@ -85,9 +86,9 @@ describe('NFTs tests', () => { nfts.verifySendNFTBtnDisabled() }) + // TODO: Added to prod 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/recovery.cy.js b/cypress/e2e/regression/recovery.cy.js new file mode 100644 index 000000000..37d585cc9 --- /dev/null +++ b/cypress/e2e/regression/recovery.cy.js @@ -0,0 +1,242 @@ +import * as constants from '../../support/constants.js' +import * as main from '../pages/main.page.js' +import * as owner from '../pages/owners.pages.js' +import * as recovery from '../pages/recovery.pages.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as wallet from '../../support/utils/wallet.js' +import * as modules from '../pages/modules.page.js' +import * as navigation from '../pages/navigation.page.js' + +let recoverySafes, + staticSafes = [] +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY +const guardian = walletCredentials.OWNER_2_PRIVATE_KEY + +describe('Recovery regression tests', { defaultCommandTimeout: 50000 }, () => { + before(() => { + getSafes(CATEGORIES.recovery) + .then((recoveries) => { + recoverySafes = recoveries + return getSafes(CATEGORIES.static) + }) + .then((statics) => { + staticSafes = statics + }) + }) + + it('Verify there is no account recovery section in the global settings', () => { + cy.visit(constants.setupUrl + recoverySafes.SEP_RECOVERY_SAFE_1) + cy.clearLocalStorage() + main.acceptCookies() + main.verifyElementsCount(recovery.setupRecoveryModalBtn, 0) + }) + + it('Verify that non-owner can not edit and delete recovery set up on Security and Login', () => { + cy.visit(constants.securityUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + cy.clearLocalStorage() + main.acceptCookies() + recovery.verifyRecoveryTableDisplayed() + main.verifyElementsCount(recovery.removeRecovererBtn, 0) + main.verifyElementsCount(recovery.editRecovererBtn, 0) + }) + + it('Verify that non-owner can not delete recovery set up on Modules', () => { + cy.visit(constants.modulesUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + cy.clearLocalStorage() + main.acceptCookies() + main.verifyElementsStatus([modules.moduleRemoveIcon], constants.enabledStates.disabled) + }) + + it('Verify that guardian can not delete or edit recovery set up on Security and Login', () => { + cy.visit(constants.securityUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + cy.clearLocalStorage() + wallet.connectSigner(guardian) + main.acceptCookies() + recovery.postponeRecovery() + recovery.verifyRecoveryTableDisplayed() + main.verifyElementsCount(recovery.removeRecovererBtn, 0) + main.verifyElementsCount(recovery.editRecovererBtn, 0) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + + it('Verify that during the first connection to the safe "Proposal to recover account" modal is displayed for the guardian', () => { + cy.visit(constants.securityUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + cy.clearLocalStorage() + wallet.connectSigner(guardian) + main.acceptCookies() + recovery.verifyRecoveryProposalModalState(constants.elementExistanceStates.exist) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + + it('Verify that "Account recovery" widget is displayed in the header for the Guardian', () => { + cy.visit(constants.homeUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + cy.clearLocalStorage() + wallet.connectSigner(guardian) + main.acceptCookies() + recovery.clickOnRecoverLaterBtn() + recovery.verifyRecoveryProposalModalState(constants.elementExistanceStates.exist, true) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + + it('Verify that recover later option is cached and "Proposal to account recovery" modal is not displayed on next safe opening', () => { + cy.visit(constants.securityUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + cy.clearLocalStorage() + wallet.connectSigner(guardian) + main.acceptCookies() + recovery.clickOnRecoverLaterBtn() + cy.reload() + recovery.verifyRecoveryProposalModalState(constants.elementExistanceStates.not_exist) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + + it('Verify that "Proposal to account recovery" modal is not displayed if the user is not guardian', () => { + cy.visit(constants.securityUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + cy.clearLocalStorage() + wallet.connectSigner(signer) + main.acceptCookies() + recovery.verifyRecoveryProposalModalState(constants.elementExistanceStates.not_exist) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + + it('Verify that the guardian can not delete recovery set up on Modules', () => { + cy.visit(constants.modulesUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + cy.clearLocalStorage() + wallet.connectSigner(guardian) + main.acceptCookies() + recovery.postponeRecovery() + main.verifyElementsStatus([modules.moduleRemoveIcon], constants.enabledStates.disabled) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + + it('Verify initial and edited recovery settings', () => { + const address = '0x9445...F1BA' + const settings = [address, recovery.recoveryOptions.fiveSixDays, recovery.recoveryOptions.never] + const confirmationData = [recovery.recoveryOptions.fiveMin, recovery.recoveryOptions.oneHr] + cy.visit(constants.securityUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + cy.clearLocalStorage() + wallet.connectSigner(signer) + main.acceptCookies() + recovery.verifyRecoveryTableDisplayed() + recovery.verifyRecovererSettings(settings) + recovery.clickOnEditRecoverer() + recovery.clickOnNextBtn() + recovery.setRecoveryDelay(recovery.recoveryOptions.fiveMin) + recovery.setRecoveryExpiry(recovery.recoveryOptions.oneHr) + recovery.agreeToTerms() + recovery.clickOnNextBtn() + recovery.verifyRecovererConfirmationData(confirmationData) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + + it('Verify that set up recovery flow can be canceled before submitting tx', () => { + cy.visit(constants.securityUrl + staticSafes.SEP_STATIC_SAFE_13) + cy.clearLocalStorage() + wallet.connectSigner(signer) + main.acceptCookies() + recovery.clickOnSetupRecoveryBtn() + recovery.clickOnSetupRecoveryModalBtn() + recovery.clickOnNextBtn() + recovery.enterRecovererAddress(constants.SEPOLIA_OWNER_2) + recovery.agreeToTerms() + recovery.clickOnNextBtn() + navigation.clickOnModalCloseBtn(0) + recovery.getSetupRecoveryBtn() + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + + it('Verify Recovery delay and Expiry options are present during recovery setup', () => { + const options = [ + recovery.recoveryOptions.fiveMin, + recovery.recoveryOptions.fiveSixDays, + recovery.recoveryOptions.oneHr, + ] + cy.visit(constants.securityUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + cy.clearLocalStorage() + wallet.connectSigner(signer) + main.acceptCookies() + recovery.verifyRecoveryTableDisplayed() + recovery.clickOnEditRecoverer() + recovery.clickOnNextBtn() + recovery.verifyRecoveryDelayOptions(options) + cy.get('body').click() + recovery.verifyRecoveryExpiryOptions(options) + cy.get('body').click() + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + + it('Verify that there is validation for the Guardian address field', () => { + cy.visit(constants.securityUrl + staticSafes.SEP_STATIC_SAFE_13) + cy.clearLocalStorage() + wallet.connectSigner(signer) + main.acceptCookies() + recovery.clickOnSetupRecoveryBtn() + recovery.clickOnSetupRecoveryModalBtn() + recovery.clickOnNextBtn() + + recovery.enterRecovererAddress(main.generateRandomString(10), 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) + + recovery.enterRecovererAddress(constants.DEFAULT_OWNER_ADDRESS.toUpperCase(), 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) + + recovery.enterRecovererAddress(constants.ENS_TEST_SEPOLIA_INVALID, 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.failedResolve) + + recovery.enterRecovererAddress(staticSafes.SEP_STATIC_SAFE_13, 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.ownSafeGuardian) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + + it('Verify that recovery tx is opened after clicking on "Start recovery" button in the widget', () => { + cy.visit(constants.securityUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + cy.clearLocalStorage() + wallet.connectSigner(guardian) + main.acceptCookies() + recovery.clickOnRecoverLaterBtn() + cy.visit(constants.homeUrl + recoverySafes.SEP_RECOVERY_SAFE_4) + recovery.clickOnStartRecoveryBtn() + recovery.enterRecovererAddress(constants.SEPOLIA_OWNER_2) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + }) + + it('Verify that the Security section contains Account recovery block on supported netwroks', () => { + const safes = [ + staticSafes.ETH_STATIC_SAFE_15, + staticSafes.GNO_STATIC_SAFE_16, + staticSafes.MATIC_STATIC_SAFE_17, + staticSafes.SEP_STATIC_SAFE_13, + ] + + safes.forEach((safe) => { + cy.visit(constants.prodbaseUrl + constants.securityUrl + safe) + recovery.getSetupRecoveryBtn() + }) + }) + + it('Verify that the Security and Login section does not contain Account recovery block on unsupported networks', () => { + const safes = [ + staticSafes.BNB_STATIC_SAFE_18, + staticSafes.AURORA_STATIC_SAFE_19, + staticSafes.AVAX_STATIC_SAFE_20, + staticSafes.LINEA_STATIC_SAFE_21, + staticSafes.ZKSYNC_STATIC_SAFE_22, + ] + + safes.forEach((safe) => { + cy.visit(constants.prodbaseUrl + constants.securityUrl + safe) + main.verifyElementsCount(recovery.setupRecoveryBtn, 0) + }) + }) +}) diff --git a/cypress/e2e/regression/remove_owner.cy.js b/cypress/e2e/regression/remove_owner.cy.js index 15b0e6963..41e0b7371 100644 --- a/cypress/e2e/regression/remove_owner.cy.js +++ b/cypress/e2e/regression/remove_owner.cy.js @@ -18,8 +18,6 @@ describe('Remove Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_13) main.waitForHistoryCallToComplete() - cy.clearLocalStorage() - main.acceptCookies() cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) @@ -49,6 +47,7 @@ describe('Remove Owners tests', () => { owner.getThresholdOptions().should('have.length', 1) }) + // TODO: Added to prod it('Verify owner deletion transaction has been created', () => { wallet.connectSigner(signer) owner.waitForConnectionStatus() diff --git a/cypress/e2e/regression/replace_owner.cy.js b/cypress/e2e/regression/replace_owner.cy.js index f332faded..4f7db44a7 100644 --- a/cypress/e2e/regression/replace_owner.cy.js +++ b/cypress/e2e/regression/replace_owner.cy.js @@ -1,14 +1,16 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' 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' +import * as ls from '../../support/localstorage_data.js' +import { getEvents, events, checkDataLayerEvents } from '../../support/utils/gtag.js' let staticSafes = [] const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY +const signer2 = walletCredentials.OWNER_1_PRIVATE_KEY const ownerName = 'Replacement Signer Name' @@ -19,8 +21,6 @@ describe('Replace Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) - cy.clearLocalStorage() - main.acceptCookies() cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) @@ -32,23 +32,17 @@ describe('Replace Owners tests', () => { it('Verify max characters in name field', () => { wallet.connectSigner(signer) owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() + owner.openReplaceOwnerWindow(0) owner.typeOwnerName(main.generateRandomString(51)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) }) - // TODO: Rework with localstorage it('Verify that Address input auto-fills with related value', () => { - cy.visit(constants.addressBookUrl + staticSafes.SEP_STATIC_SAFE_4) - addressBook.clickOnCreateEntryBtn() - addressBook.typeInName(constants.addresBookContacts.user1.name) - addressBook.typeInAddress(constants.addresBookContacts.user1.address) - addressBook.clickOnSaveEntryBtn() - addressBook.verifyNewEntryAdded(constants.addresBookContacts.user1.name, constants.addresBookContacts.user1.address) + main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.autofillData) cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) wallet.connectSigner(signer) owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() + owner.openReplaceOwnerWindow(0) owner.typeOwnerAddress(constants.addresBookContacts.user1.address) owner.verifyNewOwnerName(constants.addresBookContacts.user1.name) }) @@ -56,7 +50,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.openReplaceOwnerWindow(0) owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) owner.clickOnNextBtn() owner.verifyConfirmTransactionWindowDisplayed() @@ -65,7 +59,7 @@ describe('Replace Owners tests', () => { it('Verify relevant error messages are displayed in Address input', () => { wallet.connectSigner(signer) owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() + owner.openReplaceOwnerWindow(0) owner.typeOwnerAddress(main.generateRandomString(10)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) @@ -82,19 +76,29 @@ describe('Replace Owners tests', () => { owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.alreadyAdded) }) - it("Verify 'Replace' tx is created", () => { - cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) + it("Verify 'Replace' tx is created. GA tx_created", () => { + const tx_created = [ + { + eventLabel: events.txCreatedSwapOwner.eventLabel, + eventCategory: events.txCreatedSwapOwner.category, + eventAction: events.txCreatedSwapOwner.action, + event: events.txCreatedSwapOwner.eventName, + safeAddress: staticSafes.SEP_STATIC_SAFE_25.slice(6), + }, + ] + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_25) wallet.connectSigner(signer) owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() + owner.openReplaceOwnerWindow(1) cy.wait(1000) owner.typeOwnerName(ownerName) owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) - createTx.changeNonce(2) + createTx.changeNonce(0) owner.clickOnNextBtn() createTx.clickOnSignTransactionBtn() - createTx.waitForProposeRequest() createTx.clickViewTransaction() createTx.verifyReplacedSigner(ownerName) + getEvents() + checkDataLayerEvents(tx_created) }) }) diff --git a/cypress/e2e/regression/sidebar.cy.js b/cypress/e2e/regression/sidebar.cy.js index 597b8e381..668c9ed88 100644 --- a/cypress/e2e/regression/sidebar.cy.js +++ b/cypress/e2e/regression/sidebar.cy.js @@ -16,14 +16,13 @@ describe('Sidebar tests', () => { beforeEach(() => { cy.visit(constants.homeUrl + staticSafes.SEP_STATIC_SAFE_9) - cy.clearLocalStorage() - main.acceptCookies() }) it('Verify Current network is displayed at the top', () => { sideBar.verifyNetworkIsDisplayed(constants.networks.sepolia) }) + // TODO: Added to prod it('Verify current safe details', () => { sideBar.verifySafeHeaderDetails(sideBar.testSafeHeaderDetails) }) @@ -33,25 +32,24 @@ describe('Sidebar tests', () => { sideBar.verifyQRModalDisplayed() }) - it.skip('Verify Copy button copies the address', () => { - sideBar.verifyCopyAddressBtn(staticSafes.SEP_STATIC_SAFE_9.substring(4)) - }) - it('Verify Open blockexplorer button contain etherscan link', () => { sideBar.verifyEtherscanLinkExists() }) + // TODO: Added to prod it('Verify New transaction button enabled for owners', () => { wallet.connectSigner(signer) sideBar.verifyNewTxBtnStatus(constants.enabledStates.enabled) }) + // TODO: Added to prod 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) }) + // TODO: Added to prod it('Verify New Transaction button disabled for non-owners', () => { main.verifyElementsCount(navigation.newTxBtn, 0) }) diff --git a/cypress/e2e/regression/sidebar_2.cy.js b/cypress/e2e/regression/sidebar_2.cy.js index a24156d4e..b0e56b43b 100644 --- a/cypress/e2e/regression/sidebar_2.cy.js +++ b/cypress/e2e/regression/sidebar_2.cy.js @@ -19,8 +19,6 @@ describe('Sidebar added sidebar tests', () => { beforeEach(() => { cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_9) cy.wait(2000) - cy.clearLocalStorage() - main.acceptCookies() main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addedSafes, ls.addedSafes.set2) main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.addedSafes) }) @@ -48,15 +46,8 @@ describe('Sidebar added sidebar tests', () => { sideBar.checkCurrencyInHeader(assets.currency$) }) - // Waiting for endpoint from CGW - it.skip('Verify "wallet" tag counter if the safe has tx ready for execution', () => { + it('Verify "wallet" tag counter if the safe has tx ready for execution', () => { sideBar.openSidebar() - sideBar.verifyMissingSignature(staticSafe200) - }) - - // Waiting for endpoint from CGW - it.skip('Verify "Wallet" tag counter only shows for owners', () => { - sideBar.openSidebar() - sideBar.verifyQueuedTx(staticSafe200) + sideBar.verifyNumberOfPendingTxTag(1) }) }) diff --git a/cypress/e2e/regression/sidebar_3.cy.js b/cypress/e2e/regression/sidebar_3.cy.js index b1ff34d2c..a760e5ba0 100644 --- a/cypress/e2e/regression/sidebar_3.cy.js +++ b/cypress/e2e/regression/sidebar_3.cy.js @@ -19,10 +19,6 @@ describe('Sidebar tests 3', () => { 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() @@ -89,6 +85,7 @@ describe('Sidebar tests 3', () => { main.checkButtonByTextExists(sideBar.exportBtnStr) }) + // TODO: Added to prod 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() @@ -118,6 +115,7 @@ describe('Sidebar tests 3', () => { sideBar.verifyAddedSafesExist([sideBar.sideBarSafes.safe3short]) }) + // TODO: Added to prod it('Verify pending signature is displayed in sidebar for unsigned tx', () => { cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_7) main.acceptCookies() @@ -141,10 +139,14 @@ describe('Sidebar tests 3', () => { sideBar.checkTxToConfirm(1) }) + // TODO: Added to prod it('Verify balance exists in a tx in sidebar', () => { cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_7) main.acceptCookies() wallet.connectSigner(signer) + owner.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + wallet.connectSigner(signer) cy.intercept('GET', constants.safeListEndpoint, { 11155111: [sideBar.sideBarSafesPendingActions.safe1], }) diff --git a/cypress/e2e/regression/sidebar_nonowner.cy.js b/cypress/e2e/regression/sidebar_nonowner.cy.js index 629bbbea4..3104c1174 100644 --- a/cypress/e2e/regression/sidebar_nonowner.cy.js +++ b/cypress/e2e/regression/sidebar_nonowner.cy.js @@ -21,8 +21,6 @@ describe('Sidebar non-owner tests', () => { beforeEach(() => { cy.visit(constants.homeUrl + staticSafes.SEP_STATIC_SAFE_11) cy.wait(2000) - cy.clearLocalStorage() - main.acceptCookies() main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addedSafes, ls.addedSafes.set3) main.addToLocalStorage(constants.localStorageKeys.SAFE_v2__addressBook, ls.addressBookData.addedSafes) }) diff --git a/cypress/e2e/regression/spending_limits.cy.js b/cypress/e2e/regression/spending_limits.cy.js index 91513b135..527a83604 100644 --- a/cypress/e2e/regression/spending_limits.cy.js +++ b/cypress/e2e/regression/spending_limits.cy.js @@ -10,10 +10,11 @@ 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 signerAddress = walletCredentials.OWNER_4_WALLET_ADDRESS const tokenAmount = 0.1 const newTokenAmount = 0.001 -const spendingLimitBalance = '(0.17 ETH)' +const spendingLimitBalance = '(0.15 ETH)' describe('Spending limits tests', () => { before(async () => { @@ -22,11 +23,31 @@ describe('Spending limits tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_8) - cy.clearLocalStorage() - main.acceptCookies() cy.get(spendinglimit.spendingLimitsSection).should('be.visible') }) + it('Verify resetAllowance and setAllowance actions are shown if a part of allowance was used', () => { + wallet.connectSigner(signer) + spendinglimit.clickOnNewSpendingLimitBtn() + spendinglimit.enterBeneficiaryAddress(signerAddress) + spendinglimit.enterSpendingLimitAmount(0.1) + spendinglimit.clickOnNextBtn() + spendinglimit.verifyActionCount(2) + spendinglimit.verifyActionNames([spendinglimit.actionNames.resetAllowance, spendinglimit.actionNames.setAllowance]) + }) + + it('Verify only setAllowance action is shown if allowance was not used', () => { + cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_23) + wallet.connectSigner(signer) + spendinglimit.clickOnNewSpendingLimitBtn() + spendinglimit.enterBeneficiaryAddress(signerAddress) + spendinglimit.enterSpendingLimitAmount(0.1) + spendinglimit.clickOnNextBtn() + spendinglimit.verifyActionCount(0) + spendinglimit.verifyDecodedTxSummary([spendinglimit.actionNames.setAllowance]) + }) + + // TODO: Added to prod 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) @@ -41,10 +62,12 @@ describe('Spending limits tests', () => { ) }) + // TODO: Added to prod it('Verify values and trash icons are displayed in Beneficiary table', () => { spendinglimit.verifyBeneficiaryTable() }) + // TODO: Added to prod it('Verify Spending limit option is available when selecting the corresponding token', () => { wallet.connectSigner(signer) navigation.clickOnNewTxBtn() @@ -147,11 +170,6 @@ describe('Spending limits tests', () => { }) }) - it.skip('Verify that clicking on copy icon of a beneficiary works', () => { - tx.verifyNumberOfCopyIcons(3) - tx.verifyCopyIconWorks(0, constants.DEFAULT_OWNER_ADDRESS) - }) - it('Verify explorer links contain Sepolia link', () => { tx.verifyNumberOfExternalLinks(3) }) diff --git a/cypress/e2e/regression/spending_limits_nonowner.cy.js b/cypress/e2e/regression/spending_limits_nonowner.cy.js index e64bda33f..d6fe2ede2 100644 --- a/cypress/e2e/regression/spending_limits_nonowner.cy.js +++ b/cypress/e2e/regression/spending_limits_nonowner.cy.js @@ -12,8 +12,6 @@ describe('Spending limits non-owner tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_3) - cy.clearLocalStorage() - main.acceptCookies() cy.get(spendinglimit.spendingLimitsSection).should('be.visible') }) diff --git a/cypress/e2e/regression/swaps.cy.js b/cypress/e2e/regression/swaps.cy.js index c8994fdc4..e2251c7c9 100644 --- a/cypress/e2e/regression/swaps.cy.js +++ b/cypress/e2e/regression/swaps.cy.js @@ -7,10 +7,14 @@ 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' +import * as navigation from '../pages/navigation.page' +import { getEvents, events, checkDataLayerEvents } from '../../support/utils/gtag.js' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) const signer = walletCredentials.OWNER_4_PRIVATE_KEY const signer2 = walletCredentials.OWNER_3_WALLET_ADDRESS +const signer3 = walletCredentials.OWNER_1_PRIVATE_KEY + let staticSafes = [] let iframeSelector @@ -23,11 +27,9 @@ 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}"]` }) @@ -42,10 +44,10 @@ describe('Swaps tests', () => { swaps.clickOnSettingsBtn() swaps.selectInputCurrency(swaps.swapTokens.cow) swaps.checkTokenBalance(staticSafes.SEP_STATIC_SAFE_1.substring(4), swaps.swapTokens.cow) - swaps.setInputValue(4) + swaps.setInputValue(20) swaps.selectOutputCurrency(swaps.swapTokens.dai) swaps.checkSwapBtnIsVisible() - swaps.isInputGreaterZero(swaps.outputurrencyInput).then((isGreaterThanZero) => { + swaps.isInputGreaterZero(swaps.outputCurrencyInput).then((isGreaterThanZero) => { cy.wrap(isGreaterThanZero).should('be.true') }) swaps.clickOnExceeFeeChkbox() @@ -88,6 +90,7 @@ describe('Swaps tests', () => { } }) .within(() => { + swaps.selectInputCurrency(swaps.swapTokens.cow) swaps.clickOnSettingsBtn() swaps.enableCustomRecipient(isCustomRecipientFound(swaps.customRecipient)) swaps.clickOnSettingsBtn() @@ -108,6 +111,7 @@ describe('Swaps tests', () => { main.getIframeBody(iframeSelector).then(($frame) => { cy.wrap($frame).within(() => { + swaps.selectInputCurrency(swaps.swapTokens.cow) swaps.clickOnSettingsBtn() if (isCustomRecipientFound($frame, swaps.customRecipient)) { @@ -134,13 +138,15 @@ describe('Swaps tests', () => { swaps.acceptLegalDisclaimer() cy.wait(4000) main.getIframeBody(iframeSelector).within(() => { + swaps.selectInputCurrency(swaps.swapTokens.cow) swaps.clickOnSettingsBtn() swaps.setSlippage('0.30') swaps.setExpiry('2') swaps.clickOnSettingsBtn() - swaps.setInputValue(4) + swaps.setInputValue(200) + swaps.selectOutputCurrency(swaps.swapTokens.dai) swaps.checkSwapBtnIsVisible() - swaps.isInputGreaterZero(swaps.outputurrencyInput).then((isGreaterThanZero) => { + swaps.isInputGreaterZero(swaps.outputCurrencyInput).then((isGreaterThanZero) => { cy.wrap(isGreaterThanZero).should('be.true') }) swaps.clickOnExceeFeeChkbox() @@ -159,17 +165,93 @@ describe('Swaps tests', () => { const widgetFee = swaps.getWidgetFee() const orderID = swaps.getOrderID() + const isCustomRecipientFound = ($frame, customRecipient) => { + const element = $frame.find(customRecipient) + return element.length > 0 + } + + swaps.acceptLegalDisclaimer() + cy.wait(4000) + main.getIframeBody(iframeSelector).then(($frame) => { + cy.wrap($frame).within(() => { + swaps.selectInputCurrency(swaps.swapTokens.cow) + swaps.setInputValue(200) + swaps.selectOutputCurrency(swaps.swapTokens.dai) + swaps.checkSwapBtnIsVisible() + 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(signer2) + swaps.clickOnExceeFeeChkbox() + swaps.clickOnSwapBtn() + swaps.clickOnSwapBtn() + }) + swaps.verifyRecipientAlertIsDisplayed() + }) + }, + ) + + it( + 'Verify an order can be created, signed by second signer and deleted. GA tx_confirm, tx_created', + { defaultCommandTimeout: 30000 }, + () => { + const tx_created = [ + { + eventLabel: events.txCreatedSwap.eventLabel, + eventCategory: events.txCreatedSwap.category, + eventType: events.txCreatedSwap.eventType, + safeAddress: staticSafes.SEP_STATIC_SAFE_1.slice(6), + }, + ] + const tx_confirmed = [ + { + eventLabel: events.txConfirmedSwap.eventLabel, + eventCategory: events.txConfirmedSwap.category, + eventType: events.txConfirmedSwap.eventType, + safeAddress: staticSafes.SEP_STATIC_SAFE_1.slice(6), + }, + ] swaps.acceptLegalDisclaimer() cy.wait(4000) main.getIframeBody(iframeSelector).within(() => { - swaps.setInputValue(4) - swaps.checkSwapBtnIsVisible() - swaps.enterRecipient(signer2) + swaps.clickOnSettingsBtn() + swaps.setSlippage('0.30') + swaps.setExpiry('2') + swaps.clickOnSettingsBtn() + swaps.selectInputCurrency(swaps.swapTokens.cow) + swaps.checkTokenBalance(staticSafes.SEP_STATIC_SAFE_1.substring(4), swaps.swapTokens.cow) + swaps.setInputValue(100) + swaps.selectOutputCurrency(swaps.swapTokens.dai) swaps.clickOnExceeFeeChkbox() swaps.clickOnSwapBtn() swaps.clickOnSwapBtn() }) - swaps.verifyRecipientAlertIsDisplayed() + create_tx.changeNonce(22) + create_tx.clickOnSignTransactionBtn() + create_tx.clickViewTransaction() + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + wallet.connectSigner(signer3) + create_tx.clickOnConfirmTransactionBtn() + create_tx.clickOnNoLaterOption() + create_tx.clickOnSignTransactionBtn() + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() + wallet.connectSigner(signer) + create_tx.deleteTx() + + getEvents() + checkDataLayerEvents(tx_created) + checkDataLayerEvents(tx_confirmed) }, ) }) diff --git a/cypress/e2e/regression/swaps_history.cy.js b/cypress/e2e/regression/swaps_history.cy.js index 95d4ffb52..9d743e254 100644 --- a/cypress/e2e/regression/swaps_history.cy.js +++ b/cypress/e2e/regression/swaps_history.cy.js @@ -17,9 +17,7 @@ describe('Swaps history tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_1) - main.acceptCookies() }) it('Verify swap selling operation with one action', { defaultCommandTimeout: 30000 }, () => { diff --git a/cypress/e2e/regression/swaps_history_2.cy.js b/cypress/e2e/regression/swaps_history_2.cy.js index 1f97be155..4239df212 100644 --- a/cypress/e2e/regression/swaps_history_2.cy.js +++ b/cypress/e2e/regression/swaps_history_2.cy.js @@ -14,10 +14,6 @@ describe('Swaps history tests 2', () => { 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() @@ -25,16 +21,10 @@ describe('Swaps history tests 2', () => { 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, - ]) + create_tx.verifyExpandedDetails([swapsHistory.sellFull, dai, eq, swapsHistory.dai, swapsHistory.filled]) }) + // TODO: Added to prod 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() @@ -69,7 +59,6 @@ describe('Swaps history tests 2', () => { eq, swapsHistory.cow, swapsHistory.cancelled, - swapsHistory.gGpV2, ]) }) @@ -112,6 +101,7 @@ describe('Swaps history tests 2', () => { create_tx.verifyExpandedDetails([swapsHistory.sellOrder, swapsHistory.sell, usdc, eq, swapsHistory.filled]) }) + // TODO: Added to prod it( 'Verify no decoding if tx was created using CowSwap safe-app in the history', { defaultCommandTimeout: 30000 }, @@ -131,7 +121,7 @@ describe('Swaps history tests 2', () => { swapsHistory.forAtMost, ]) main.verifyValuesDoNotExist(create_tx.transactionItem, [swapsHistory.title, swapsHistory.cow, swapsHistory.dai]) - main.verifyValuesExist(create_tx.transactionItem, [swapsHistory.actionPreSignatureG, swapsHistory.safeAppTitile]) + main.verifyValuesExist(create_tx.transactionItem, [swapsHistory.actionPreSignatureG, swapsHistory.gGpV2]) }, ) diff --git a/cypress/e2e/regression/swaps_tokens.cy.js b/cypress/e2e/regression/swaps_tokens.cy.js index b642b8f15..effe894ea 100644 --- a/cypress/e2e/regression/swaps_tokens.cy.js +++ b/cypress/e2e/regression/swaps_tokens.cy.js @@ -17,11 +17,10 @@ describe('[SMOKE] Swaps token tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_1) - main.acceptCookies() }) + // TODO: Added to prod it( 'Verify that clicking the swap from assets tab, autofills that token automatically in the form', { defaultCommandTimeout: 30000 }, @@ -38,6 +37,7 @@ describe('[SMOKE] Swaps token tests', () => { }, ) + // TODO: Added to prod it('Verify swap button are displayed in assets table and dashboard', () => { assets.selectTokenList(assets.tokenListOptions.allTokens) main.verifyElementsCount(swaps.assetsSwapBtn, 4) diff --git a/cypress/e2e/regression/tokens.cy.js b/cypress/e2e/regression/tokens.cy.js index e941adeca..3f696c1d1 100644 --- a/cypress/e2e/regression/tokens.cy.js +++ b/cypress/e2e/regression/tokens.cy.js @@ -2,6 +2,7 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' import * as assets from '../pages/assets.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as ls from '../../support/localstorage_data.js' const ASSET_NAME_COLUMN = 0 const TOKEN_AMOUNT_COLUMN = 1 @@ -16,11 +17,14 @@ describe('Tokens tests', () => { staticSafes = await getSafes(CATEGORIES.static) }) beforeEach(() => { + main.addToLocalStorage( + constants.localStorageKeys.SAFE_v2__tokenlist_onboarding, + ls.cookies.acceptedTokenListOnboarding, + ) cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_2) - cy.clearLocalStorage() - main.acceptCookies() }) + // TODO: Added to prod it('Verify that non-native tokens are present and have balance', () => { assets.selectTokenList(assets.tokenListOptions.allTokens) assets.verifyBalance(assets.currencyDaiCap, TOKEN_AMOUNT_COLUMN, assets.currencyDaiAlttext) @@ -170,6 +174,7 @@ describe('Tokens tests', () => { assets.verifyTokenBalanceOrder('descending') }) + // TODO: Added to prod //Include in smoke. it('Verify that when owner is disconnected, Send button is disabled', () => { assets.selectTokenList(assets.tokenListOptions.allTokens) @@ -177,6 +182,7 @@ describe('Tokens tests', () => { assets.VerifySendButtonIsDisabled() }) + // TODO: Added to prod it('Verify that when connected user is not owner, Send button is disabled', () => { cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_3) assets.selectTokenList(assets.tokenListOptions.allTokens) diff --git a/cypress/e2e/regression/tx_decoding.cy.js b/cypress/e2e/regression/tx_decoding.cy.js new file mode 100644 index 000000000..9bf12f199 --- /dev/null +++ b/cypress/e2e/regression/tx_decoding.cy.js @@ -0,0 +1,17 @@ +import * as main from '../pages/main.page.js' +import * as createTx from '../pages/create_tx.pages.js' +import * as constants from '../../support/constants.js' + +const safe = 'sep:0x2a73e61bd15b25B6958b4DA3bfc759ca4db249b9' +const decodedTx = + '&id=multisig_0x2a73e61bd15b25B6958b4DA3bfc759ca4db249b9_0xa3e73a212d7025c08048a05dcd829a88d1bf8a7c0d9eaf453b3b6039ad6156f3' + +//TODO: Check file error +describe('Tx decoding tests', () => { + it.skip('Check visual tx', () => { + cy.visit(constants.transactionUrl + safe + decodedTx) + createTx.clickOnExpandAllActionsBtn() + cy.wait(1000) + cy.compareSnapshot('tx_decoding', { errorThreshold: 0, failSilently: false }) + }) +}) diff --git a/cypress/e2e/regression/tx_history.cy.js b/cypress/e2e/regression/tx_history.cy.js index d18372001..f5e39f5d4 100644 --- a/cypress/e2e/regression/tx_history.cy.js +++ b/cypress/e2e/regression/tx_history.cy.js @@ -20,11 +20,22 @@ describe('Tx history tests 1', () => { }) beforeEach(() => { - cy.clearLocalStorage() + cy.intercept( + 'GET', + `**${constants.stagingCGWChains}${constants.networkKeys.sepolia}/${ + constants.stagingCGWSafes + }${staticSafes.SEP_STATIC_SAFE_7.substring(4)}/transactions/history**`, + (req) => { + req.url = `https://safe-client.staging.5afe.dev/v1/chains/11155111/safes/0x5912f6616c84024cD1aff0D5b55bb36F5180fFdb/transactions/history?timezone=Europe/Berlin&trusted=false&cursor=limit=100&offset=1` + req.continue() + }, + ).as('allTransactions') + cy.visit(constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_7) - main.acceptCookies() + cy.wait('@allTransactions') }) + // TODO: Added to prod // Account creation it('Verify summary for account creation', () => { createTx.verifySummaryByName( @@ -34,6 +45,7 @@ describe('Tx history tests 1', () => { ) }) + // TODO: Added to prod it('Verify exapanded details for account creation', () => { createTx.clickOnTransactionItemByName(typeCreateAccount.title) createTx.verifyExpandedDetails([ @@ -49,24 +61,12 @@ describe('Tx history tests 1', () => { ]) }) - it.skip('Verify copy bottons work as expected for account creation', () => { - createTx.clickOnTransactionItemByName(typeCreateAccount.title) - createTx.verifyNumberOfCopyIcons(4) - createTx.verifyCopyIconWorks(0, typeCreateAccount.creator.address) - }) - it('Verify external links exist for account creation', () => { createTx.clickOnTransactionItemByName(typeCreateAccount.title) createTx.verifyNumberOfExternalLinks(4) }) - // Token receipt - it.skip('Verify copy button copies tx hash', () => { - createTx.clickOnTransactionItemByName(typeReceive.summaryTitle, typeReceive.summaryTxInfo) - createTx.verifyNumberOfCopyIcons(2) - createTx.verifyCopyIconWorks(1, typeReceive.transactionHashCopied) - }) - + // TODO: Added to prod // Token send it('Verify exapanded details for token send', () => { createTx.clickOnTransactionItemByName(typeSend.title, typeSend.summaryTxInfo) @@ -78,6 +78,7 @@ describe('Tx history tests 1', () => { ]) }) + // TODO: Added to prod // Spending limits it('Verify summary for setting spend limits', () => { createTx.verifySummaryByName( @@ -88,12 +89,13 @@ describe('Tx history tests 1', () => { ) }) + // TODO: Added to prod it('Verify exapanded details for initial spending limits setup', () => { createTx.clickOnTransactionItemByName(typeSpendingLimits.title, typeSpendingLimits.summaryTxInfo) createTx.verifyExpandedDetails( [ - typeSpendingLimits.title, - typeSpendingLimits.description, + typeSpendingLimits.contractTitle, + typeSpendingLimits.call_multiSend, typeSpendingLimits.transactionHash, typeSpendingLimits.safeTxHash, ], @@ -101,6 +103,7 @@ describe('Tx history tests 1', () => { ) }) + // TODO: Added to prod it('Verify that 3 actions exist in initial spending limits setup', () => { createTx.clickOnTransactionItemByName(typeSpendingLimits.title, typeSpendingLimits.summaryTxInfo) createTx.verifyActions([ @@ -145,6 +148,7 @@ describe('Tx history tests 1', () => { ]) }) + // TODO: Added to prod it('Verify advanced details displayed in exapanded details for allowance deletion', () => { createTx.clickOnTransactionItemByName(typeDeleteAllowance.title, typeDeleteAllowance.summaryTxInfo) createTx.expandAdvancedDetails([typeDeleteAllowance.baseGas]) diff --git a/cypress/e2e/regression/tx_history_2.cy.js b/cypress/e2e/regression/tx_history_2.cy.js index db98c415c..5b4be913b 100644 --- a/cypress/e2e/regression/tx_history_2.cy.js +++ b/cypress/e2e/regression/tx_history_2.cy.js @@ -23,15 +23,25 @@ describe('Tx history tests 2', () => { }) beforeEach(() => { - cy.clearLocalStorage() + cy.intercept( + 'GET', + `**${constants.stagingCGWChains}${constants.networkKeys.sepolia}/${ + constants.stagingCGWSafes + }${staticSafes.SEP_STATIC_SAFE_7.substring(4)}/transactions/history**`, + (req) => { + req.url = `https://safe-client.staging.5afe.dev/v1/chains/11155111/safes/0x5912f6616c84024cD1aff0D5b55bb36F5180fFdb/transactions/history?timezone=Europe/Berlin&trusted=false&cursor=limit=100&offset=1` + req.continue() + }, + ).as('allTransactions') + cy.visit(constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_7) - main.acceptCookies() }) it('Verify number of transactions is correct', () => { createTx.verifyNumberOfTransactions(20) }) + // TODO: Added to prod // On-chain rejection it('Verify exapanded details for on-chain rejection', () => { createTx.clickOnTransactionItemByName(typeOnchainRejection.title) @@ -47,22 +57,18 @@ describe('Tx history tests 2', () => { ]) }) + // TODO: Added to prod // Batch transaction it('Verify exapanded details for batch', () => { createTx.clickOnTransactionItemByName(typeBatch.title, typeBatch.summaryTxInfo) createTx.verifyExpandedDetails( - [ - typeBatch.description, - typeBatch.contractTitle, - typeBatch.contractAddress, - typeBatch.transactionHash, - typeBatch.safeTxHash, - ], + [typeBatch.contractTitle, typeBatch.transactionHash, typeBatch.safeTxHash], createTx.delegateCallWarning, ) createTx.verifyActions([typeBatch.nativeTransfer.title]) }) + // TODO: Added to prod // Add owner it('Verify summary for adding owner', () => { createTx.verifySummaryByName(typeAddOwner.title, [typeGeneral.statusOk], typeAddOwner.altImage) @@ -82,11 +88,13 @@ describe('Tx history tests 2', () => { ) }) + // TODO: Added to prod // Change owner it('Verify summary for changing owner', () => { createTx.verifySummaryByName(typeChangeOwner.title, [typeGeneral.statusOk], typeChangeOwner.altImage) }) + // TODO: Added to prod it('Verify exapanded details for changing owner', () => { createTx.clickOnTransactionItemByName(typeChangeOwner.title) createTx.verifyExpandedDetails([ @@ -101,6 +109,7 @@ describe('Tx history tests 2', () => { ]) }) + // TODO: Added to prod // Remove owner it('Verify summary for removing owner', () => { createTx.verifySummaryByName(typeRemoveOwner.title, [typeGeneral.statusOk], typeRemoveOwner.altImage) @@ -121,6 +130,7 @@ describe('Tx history tests 2', () => { createTx.checkRequiredThreshold(1) }) + // TODO: Added to prod // Disbale module it('Verify summary for disable module', () => { createTx.verifySummaryByName(typeDisableOwner.title, [typeGeneral.statusOk], typeDisableOwner.altImage) @@ -136,6 +146,7 @@ describe('Tx history tests 2', () => { ]) }) + // TODO: Added to prod // Change threshold it('Verify summary for changing threshold', () => { createTx.verifySummaryByName( @@ -145,6 +156,7 @@ describe('Tx history tests 2', () => { ) }) + // TODO: Added to prod it('Verify exapanded details for changing threshold', () => { createTx.clickOnTransactionItemByName(typeChangeThreshold.title) createTx.verifyExpandedDetails( @@ -158,6 +170,7 @@ describe('Tx history tests 2', () => { createTx.checkRequiredThreshold(2) }) + // TODO: Added to prod it('Verify that sender address of untrusted token will not be copied until agreed in warning popup', () => { createTx.clickOnTransactionItemByName(typeUntrustedToken.summaryTitle, typeUntrustedToken.summaryTxInfo) createTx.verifyAddressNotCopied(0, typeUntrustedToken.senderAddress) diff --git a/cypress/e2e/safe-apps/apps_list.cy.js b/cypress/e2e/safe-apps/apps_list.cy.js index 7d3e4750e..cf9682c42 100644 --- a/cypress/e2e/safe-apps/apps_list.cy.js +++ b/cypress/e2e/safe-apps/apps_list.cy.js @@ -2,6 +2,7 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' import * as safeapps from '../pages/safeapps.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as ls from '../../support/localstorage_data.js' const myCustomAppTitle = 'Cypress Test App' const myCustomAppDescrAdded = 'Cypress Test App Description' @@ -14,11 +15,9 @@ describe('Safe Apps list tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(`${constants.appsUrl}?safe=${staticSafes.SEP_STATIC_SAFE_1}`, { failOnStatusCode: false, }) - main.acceptCookies() }) it('Verify app list can be filtered by app name', () => { diff --git a/cypress/e2e/safe-apps/browser_permissions.cy.js b/cypress/e2e/safe-apps/browser_permissions.cy.js index fc08642fd..f42f30af6 100644 --- a/cypress/e2e/safe-apps/browser_permissions.cy.js +++ b/cypress/e2e/safe-apps/browser_permissions.cy.js @@ -4,7 +4,6 @@ import * as safeapps from '../pages/safeapps.pages' describe('Browser permissions tests', () => { beforeEach(() => { - cy.clearLocalStorage() cy.fixture('safe-app').then((html) => { cy.intercept('GET', `${constants.testAppUrl}/*`, html) cy.intercept('GET', `*/manifest.json`, { @@ -15,7 +14,6 @@ describe('Browser permissions tests', () => { }) }) cy.visitSafeApp(`${constants.testAppUrl}/app`) - main.acceptCookies() }) // @TODO: unknown apps don't have permissions diff --git a/cypress/e2e/safe-apps/drain_account.spec.cy.js b/cypress/e2e/safe-apps/drain_account.spec.cy.js index 032e75157..a5a01c84e 100644 --- a/cypress/e2e/safe-apps/drain_account.spec.cy.js +++ b/cypress/e2e/safe-apps/drain_account.spec.cy.js @@ -4,11 +4,16 @@ import * as main from '../pages/main.page' import * as safeapps from '../pages/safeapps.pages' import * as navigation from '../pages/navigation.page' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as ls from '../../support/localstorage_data.js' +import * as wallet from '../../support/utils/wallet.js' let safeAppSafes = [] let iframeSelector -describe('Drain Account tests', { defaultCommandTimeout: 12000 }, () => { +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY + +describe('Drain Account tests', () => { before(async () => { safeAppSafes = await getSafes(CATEGORIES.safeapps) }) @@ -17,27 +22,26 @@ describe('Drain Account tests', { defaultCommandTimeout: 12000 }, () => { const appUrl = constants.drainAccount_url iframeSelector = `iframe[id="iframe-${appUrl}"]` const visitUrl = `/apps/open?safe=${safeAppSafes.SEP_SAFEAPP_SAFE_1}&appUrl=${encodeURIComponent(appUrl)}` - cy.intercept(`**//v1/chains/11155111/safes/${safeAppSafes.SEP_SAFEAPP_SAFE_1.substring(4)}/balances/**`, { fixture: 'balances.json', }) - - cy.clearLocalStorage() cy.visit(visitUrl) - main.acceptCookies() - safeapps.clickOnContinueBtn() }) it('Verify drain can be created', () => { + wallet.connectSigner(signer) cy.enter(iframeSelector).then((getBody) => { getBody().findByLabelText(safeapps.recipientStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) getBody().findAllByText(safeapps.transferEverythingStr).click() }) cy.findByRole('button', { name: safeapps.testTransfer1 }) cy.findByRole('button', { name: safeapps.nativeTransfer2 }) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() }) it('Verify partial drain can be created', () => { + wallet.connectSigner(signer) cy.enter(iframeSelector).then((getBody) => { getBody().findByLabelText(safeapps.selectAllRowsChbxStr).click() getBody().findAllByLabelText(safeapps.selectRowChbxStr).eq(1).click() @@ -47,6 +51,8 @@ describe('Drain Account tests', { defaultCommandTimeout: 12000 }, () => { }) cy.findByRole('button', { name: safeapps.testTransfer2 }) cy.findByRole('button', { name: safeapps.nativeTransfer1 }) + navigation.clickOnWalletExpandMoreIcon() + navigation.clickOnDisconnectBtn() }) // TODO: ENS does not resolve @@ -64,7 +70,7 @@ describe('Drain Account tests', { defaultCommandTimeout: 12000 }, () => { getBody().findByLabelText(safeapps.recipientStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) getBody().findAllByText(safeapps.transferEverythingStr).click() }) - navigation.clickOnModalCloseBtn() + navigation.clickOnModalCloseBtn(1) cy.enter(iframeSelector).then((getBody) => { getBody().findAllByText(safeapps.transferEverythingStr).should('be.visible') }) diff --git a/cypress/e2e/safe-apps/info_modal.cy.js b/cypress/e2e/safe-apps/info_modal.cy.js index 3a361ebbf..30b912de9 100644 --- a/cypress/e2e/safe-apps/info_modal.cy.js +++ b/cypress/e2e/safe-apps/info_modal.cy.js @@ -11,28 +11,27 @@ describe('Info modal tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(`${constants.appsUrl}?safe=${staticSafes.SEP_STATIC_SAFE_2}`, { failOnStatusCode: false, }) - main.acceptCookies() }) it('Verify the disclaimer is displayed when a Safe App is opened', () => { + // Required to show disclaimer + cy.clearLocalStorage() + main.acceptCookies() safeapps.clickOnApp(safeapps.transactionBuilderStr) safeapps.clickOnOpenSafeAppBtn() safeapps.verifyDisclaimerIsDisplayed() }) - // Skip tests due to changed logic - // TODO: Discuss furthers - it.skip('Verify the permissions slide is shown if the app require permissions', () => { - safeapps.clickOnContinueBtn() - cy.wait(500) // wait for the animation to finish - safeapps.verifyCameraCheckBoxExists() - }) - - it.skip('Verify the permissions and consents decision are stored when accepted', () => { - safeapps.storeAndVerifyPermissions() + it('Verify info modal consent is stored when accepted', { defaultCommandTimeout: 20000 }, () => { + // Required to show disclaimer + cy.clearLocalStorage() + main.acceptCookies() + safeapps.clickOnApp(safeapps.transactionBuilderStr) + safeapps.clickOnOpenSafeAppBtn() + safeapps.verifyDisclaimerIsDisplayed() + safeapps.verifyInfoModalAcceptance() }) }) diff --git a/cypress/e2e/safe-apps/permissions_settings.cy.js b/cypress/e2e/safe-apps/permissions_settings.cy.js index 53df8b6ce..ee1d65e32 100644 --- a/cypress/e2e/safe-apps/permissions_settings.cy.js +++ b/cypress/e2e/safe-apps/permissions_settings.cy.js @@ -13,7 +13,6 @@ describe.skip('Permissions settings tests', () => { before(() => { getSafes(CATEGORIES.static).then((statics) => { staticSafes = statics - cy.clearLocalStorage() cy.on('window:before:load', (window) => { window.localStorage.setItem( constants.BROWSER_PERMISSIONS_KEY, @@ -52,7 +51,6 @@ describe.skip('Permissions settings tests', () => { cy.visit(`${constants.appSettingsUrl}?safe=${staticSafes.SEP_STATIC_SAFE_2}`, { failOnStatusCode: false, }) - main.acceptCookies() }) }) diff --git a/cypress/e2e/safe-apps/preview_drawer.cy.js b/cypress/e2e/safe-apps/preview_drawer.cy.js index e24c590fa..d20ee5fca 100644 --- a/cypress/e2e/safe-apps/preview_drawer.cy.js +++ b/cypress/e2e/safe-apps/preview_drawer.cy.js @@ -2,6 +2,7 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' import * as safeapps from '../pages/safeapps.pages' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as ls from '../../support/localstorage_data.js' let staticSafes = [] @@ -11,11 +12,9 @@ describe('Preview drawer tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(`${constants.appsUrl}?safe=${staticSafes.SEP_STATIC_SAFE_2}`, { failOnStatusCode: false, }) - main.acceptCookies() }) it('Verify the preview drawer is displayed when opening a Safe App from the app list', () => { diff --git a/cypress/e2e/safe-apps/safe_permissions.cy.js b/cypress/e2e/safe-apps/safe_permissions.cy.js index 0b64e41ad..d91082cbf 100644 --- a/cypress/e2e/safe-apps/safe_permissions.cy.js +++ b/cypress/e2e/safe-apps/safe_permissions.cy.js @@ -1,10 +1,10 @@ import * as constants from '../../support/constants' import * as safeapps from '../pages/safeapps.pages' import * as main from '../pages/main.page' +import * as ls from '../../support/localstorage_data.js' describe('Safe permissions system tests', () => { beforeEach(() => { - cy.clearLocalStorage() cy.fixture('safe-app').then((html) => { cy.intercept('GET', `${constants.testAppUrl}/*`, html) cy.intercept('GET', `*/manifest.json`, { @@ -17,11 +17,6 @@ describe('Safe permissions system tests', () => { it('Verify that requesting permissions with wallet_requestPermissions shows the permissions prompt and return the permissions on accept', () => { cy.visitSafeApp(constants.testAppUrl + constants.requestPermissionsUrl) - main.acceptCookies() - safeapps.clickOnContinueBtn() - safeapps.verifyWarningDefaultAppMsgIsDisplayed() - safeapps.clickOnContinueBtn() - safeapps.verifyPermissionsRequestExists() safeapps.verifyAccessToAddressBookExists() safeapps.clickOnAcceptBtn() @@ -56,11 +51,6 @@ describe('Safe permissions system tests', () => { }) cy.visitSafeApp(constants.testAppUrl + constants.getPermissionsUrl) - main.acceptCookies() - safeapps.clickOnContinueBtn() - safeapps.verifyWarningDefaultAppMsgIsDisplayed() - safeapps.clickOnContinueBtn() - cy.get('@safeAppsMessage').should('have.been.calledWithMatch', { data: [ { diff --git a/cypress/e2e/safe-apps/tx-builder.2spec.cy.js b/cypress/e2e/safe-apps/tx-builder.2spec.cy.js new file mode 100644 index 000000000..7f059a65a --- /dev/null +++ b/cypress/e2e/safe-apps/tx-builder.2spec.cy.js @@ -0,0 +1,172 @@ +import 'cypress-file-upload' +import * as constants from '../../support/constants.js' +import * as safeapps from '../pages/safeapps.pages.js' +import * as createtx from '../pages/create_tx.pages.js' +import * as navigation from '../pages/navigation.page.js' +import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as ls from '../../support/localstorage_data.js' +import { getEvents, events, checkDataLayerEvents } from '../../support/utils/gtag.js' +import * as wallet from '../../support/utils/wallet.js' +import * as utils from '../../support/utils/checkers.js' + +let safeAppSafes = [] +let iframeSelector + +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY +const signer2 = walletCredentials.OWNER_1_PRIVATE_KEY + +describe('Transaction Builder 2 tests', { defaultCommandTimeout: 20000 }, () => { + before(async () => { + safeAppSafes = await getSafes(CATEGORIES.safeapps) + }) + + beforeEach(() => { + const appUrl = constants.TX_Builder_url + iframeSelector = `iframe[id="iframe-${appUrl}"]` + const visitUrl = `/apps/open?safe=${safeAppSafes.SEP_SAFEAPP_SAFE_1}&appUrl=${encodeURIComponent(appUrl)}` + cy.visit(visitUrl) + }) + + it('Verify a batch cannot be created without method data', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().findByText(safeapps.addTransactionStr).click() + getBody() + .findAllByText(safeapps.requiredStr) + .then(($element) => { + const color = $element.css('color') + expect(utils.isInRedRange(color), 'Element color is ').to.be.true + }) + }) + }) + + it('Verify a batch can be uploaded, saved to library, downloaded and removed', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText('choose a file').attachFile('test-working-batch.json', { subjectType: 'drag-n-drop' }) + getBody().findAllByText('uploaded').wait(300) + getBody().find(safeapps.saveToLibraryBtn).click() + getBody().findByLabelText(safeapps.batchNameStr).type(safeapps.e3eTestStr) + getBody().findAllByText(safeapps.createBtnStr).should('not.be.disabled').click() + getBody().findByText(safeapps.transactionLibraryStr).click() + getBody().find(safeapps.downloadBatchBtn).click() + getBody().find(safeapps.deleteBatchBtn).click() + getBody().findAllByText(safeapps.confirmDeleteBtnStr).should('not.be.disabled').click() + getBody().findByText(safeapps.noSavedBatchesStr).should('be.visible') + getBody().findByText(safeapps.backToTransactionStr).should('be.visible') + }) + cy.readFile('cypress/downloads/E2E test.json').should('exist') + }) + + it('Verify there is notification if uploaded batch is from a different chain', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText('choose a file').attachFile('test-mainnet-batch.json', { subjectType: 'drag-n-drop' }) + getBody().findAllByText(safeapps.warningStr).should('be.visible') + getBody().findAllByText(safeapps.anotherChainStr).should('be.visible') + }) + }) + + it('Verify there is error message when a modified batch is uploaded', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText('choose a file').attachFile('test-modified-batch.json', { subjectType: 'drag-n-drop' }) + getBody().findAllByText(safeapps.changedPropertiesStr) + getBody().findAllByText('choose a file').should('be.visible') + }) + }) + + it('Verify an invalid batch cannot be uploaded', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody() + .findAllByText('choose a file') + .attachFile('test-invalid-batch.json', { subjectType: 'drag-n-drop' }) + .findAllByText('choose a file') + .should('be.visible') + }) + }) + + it('Verify an empty batch cannot be uploaded', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody() + .findAllByText('choose a file') + .attachFile('test-empty-batch.json', { subjectType: 'drag-n-drop' }) + .findAllByText('choose a file') + .should('be.visible') + }) + }) + + it('Verify a valid batch as successful can be simulated', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) + getBody().findByText(safeapps.keepProxiABIStr).click() + getBody().findByLabelText(safeapps.tokenAmount).type('0') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.simulateBtnStr).click() + getBody().findByText(safeapps.transferStr).should('be.visible') + getBody().findByText(safeapps.successStr).should('be.visible') + }) + }) + + it('Verify an invalid batch as failed can be simulated', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) + getBody().findByText(safeapps.keepProxiABIStr).click() + getBody().findByLabelText(safeapps.tokenAmount).type('100') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.simulateBtnStr).click() + getBody().findByText(safeapps.failedStr).should('be.visible') + }) + }) + + // TODO: Fix visibility element + it('Verify a simple batch can be created, signed by second signer and deleted. GA tx_confirm, tx_created', () => { + const tx_created = [ + { + eventLabel: events.txCreatedTxBuilder.eventLabel, + eventCategory: events.txCreatedTxBuilder.category, + eventType: events.txCreatedTxBuilder.eventType, + event: events.txCreatedTxBuilder.event, + safeAddress: safeAppSafes.SEP_SAFEAPP_SAFE_1.slice(6), + }, + ] + const tx_confirmed = [ + { + eventLabel: events.txConfirmedTxBuilder.eventLabel, + eventCategory: events.txConfirmedTxBuilder.category, + eventType: events.txConfirmedTxBuilder.eventType, + safeAddress: safeAppSafes.SEP_SAFEAPP_SAFE_1.slice(6), + }, + ] + // wallet.connectSigner(signer) + // cy.enter(iframeSelector).then((getBody) => { + // getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + // getBody().find(safeapps.contractMethodIndex).parent().click() + // getBody().findByRole('option', { name: safeapps.testAddressValue2 }).click() + // getBody().findByLabelText(safeapps.newAddressValueStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) + // getBody().findByText(safeapps.addTransactionStr).click() + // getBody().findAllByText(constants.SEPOLIA_CONTRACT_SHORT).should('have.length', 1) + // getBody().findByText(safeapps.testAddressValueStr).should('exist') + // getBody().findByText(safeapps.createBatchStr).click() + // getBody().findByText(safeapps.sendBatchStr).click() + // }) + + // createtx.clickOnSignTransactionBtn() + // createtx.clickViewTransaction() + // navigation.clickOnWalletExpandMoreIcon() + // navigation.clickOnDisconnectBtn() + // wallet.connectSigner(signer2) + + // createtx.clickOnConfirmTransactionBtn() + // createtx.clickOnNoLaterOption() + // createtx.clickOnSignTransactionBtn() + // navigation.clickOnWalletExpandMoreIcon() + // navigation.clickOnDisconnectBtn() + // wallet.connectSigner(signer) + // createtx.deleteTx() + // createtx.verifyNumberOfTransactions(0) + // getEvents() + // checkDataLayerEvents(tx_created) + // checkDataLayerEvents(tx_confirmed) + }) +}) diff --git a/cypress/e2e/safe-apps/tx-builder.spec.cy.js b/cypress/e2e/safe-apps/tx-builder.spec.cy.js index 7dd84ba19..8bf6f67ce 100644 --- a/cypress/e2e/safe-apps/tx-builder.spec.cy.js +++ b/cypress/e2e/safe-apps/tx-builder.spec.cy.js @@ -1,14 +1,21 @@ import 'cypress-file-upload' import * as constants from '../../support/constants' -import * as main from '../pages/main.page' import * as safeapps from '../pages/safeapps.pages' import * as createtx from '../../e2e/pages/create_tx.pages' import * as navigation from '../pages/navigation.page' import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js' +import * as ls from '../../support/localstorage_data.js' +import { getEvents, events, checkDataLayerEvents } from '../../support/utils/gtag.js' +import * as wallet from '../../support/utils/wallet.js' +import * as utils from '../../support/utils/checkers.js' let safeAppSafes = [] let iframeSelector +const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) +const signer = walletCredentials.OWNER_4_PRIVATE_KEY +const signer2 = walletCredentials.OWNER_1_PRIVATE_KEY + describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { before(async () => { safeAppSafes = await getSafes(CATEGORIES.safeapps) @@ -18,14 +25,11 @@ describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { const appUrl = constants.TX_Builder_url iframeSelector = `iframe[id="iframe-${appUrl}"]` const visitUrl = `/apps/open?safe=${safeAppSafes.SEP_SAFEAPP_SAFE_1}&appUrl=${encodeURIComponent(appUrl)}` - - cy.clearLocalStorage() cy.visit(visitUrl) - main.acceptCookies() - safeapps.clickOnContinueBtn() }) - it('Verify a simple batch can be created', () => { + // TODO: Check if we still need this test as we now create complete flow of creating, signing and deleting a tx + it.skip('Verify a simple batch can be created', () => { cy.enter(iframeSelector).then((getBody) => { getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) getBody().find(safeapps.contractMethodIndex).parent().click() @@ -69,8 +73,8 @@ describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { getBody().findByText(safeapps.sendBatchStr).click() }) cy.get('h4').contains(safeapps.transactionBuilderStr).should('be.visible') - cy.findAllByText(safeapps.testBooleanValue).should('have.length', 3) - navigation.clickOnModalCloseBtn() + cy.findAllByText(safeapps.testBooleanValue).should('have.length', 6) + navigation.clickOnModalCloseBtn(0) cy.enter(iframeSelector).then((getBody) => { getBody().findAllByText(constants.SEPOLIA_CONTRACT_SHORT).should('have.length', 3) getBody().findAllByText(safeapps.testBooleanValue).should('have.length', 3) @@ -96,7 +100,7 @@ describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { cy.findByText(safeapps.thresholdStr2).should('exist') }) - it('Verify a batch can be created from an ABI', () => { + it.skip('Verify a batch can be created from an ABI', () => { cy.enter(iframeSelector).then((getBody) => { getBody().findByLabelText(safeapps.enterABIStr).type(safeapps.abi) getBody().findByLabelText(safeapps.toAddressStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) @@ -111,7 +115,7 @@ describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { getBody().findByText(safeapps.sendBatchStr).click() }) cy.get('h4').contains(safeapps.transactionBuilderStr).should('be.visible') - navigation.clickOnModalCloseBtn() + navigation.clickOnModalCloseBtn(0) cy.enter(iframeSelector).then((getBody) => { getBody().findAllByText(constants.SEPOLIA_RECIPIENT_ADDR_SHORT).should('have.length', 1) getBody().findAllByText(safeapps.testFallback).should('have.length', 1) @@ -133,7 +137,7 @@ describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { getBody().findByText(safeapps.sendBatchStr).click() }) cy.get('h4').contains(safeapps.transactionBuilderStr).should('be.visible') - navigation.clickOnModalCloseBtn() + navigation.clickOnModalCloseBtn(0) cy.enter(iframeSelector).then((getBody) => { getBody().findAllByText(constants.SEPOLIA_CONTRACT_SHORT).should('have.length', 1) getBody().findAllByText(safeapps.customData).should('have.length', 1) @@ -188,8 +192,8 @@ describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { getBody().findByText(safeapps.sendBatchStr).click() }) cy.get('h4').contains(safeapps.transactionBuilderStr).should('be.visible') - cy.findAllByText(safeapps.testAddressValueStr).should('have.length', 2) - navigation.clickOnModalCloseBtn() + cy.findAllByText(safeapps.testAddressValueStr).should('have.length', 4) + navigation.clickOnModalCloseBtn(0) cy.enter(iframeSelector).then((getBody) => { getBody().findAllByText(constants.SEPOLIA_CONTRACT_SHORT).should('have.length', 2) getBody().findAllByText(safeapps.testAddressValueStr).should('have.length', 2) @@ -201,103 +205,24 @@ describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { getBody().findByLabelText(safeapps.enterAddressStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2.substring(5)) getBody() .findAllByText(safeapps.addressNotValidStr) - .should('have.css', 'color', 'rgb(244, 67, 54)') - .and('be.visible') + .then(($element) => { + const color = $element.css('color') + expect(utils.isInRedRange(color), 'Element color is ').to.be.true + }) }) }) - it.skip('Verify a batch cannot be created without asset amount', () => { + it('Verify a batch cannot be created without asset amount', () => { cy.enter(iframeSelector).then((getBody) => { getBody().findByLabelText(safeapps.enterAddressStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) getBody().findByText(safeapps.keepProxiABIStr).click() getBody().findByText(safeapps.addTransactionStr).click() - getBody().findAllByText(safeapps.requiredStr).should('have.css', 'color', 'rgb(244, 67, 54)') - }) - }) - - it('Verify a batch cannot be created without method data', () => { - cy.enter(iframeSelector).then((getBody) => { - getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) - getBody().findByText(safeapps.addTransactionStr).click() - getBody().findAllByText(safeapps.requiredStr).should('have.css', 'color', 'rgb(244, 67, 54)') - }) - }) - - it('Verify a batch can be uploaded, saved to library, downloaded and removed', () => { - cy.enter(iframeSelector).then((getBody) => { - getBody().findAllByText('choose a file').attachFile('test-working-batch.json', { subjectType: 'drag-n-drop' }) - getBody().findAllByText('uploaded').wait(300) - getBody().find(safeapps.saveToLibraryBtn).click() - getBody().findByLabelText(safeapps.batchNameStr).type(safeapps.e3eTestStr) - getBody().findAllByText(safeapps.createBtnStr).should('not.be.disabled').click() - getBody().findByText(safeapps.transactionLibraryStr).click() - getBody().find(safeapps.downloadBatchBtn).click() - getBody().find(safeapps.deleteBatchBtn).click() - getBody().findAllByText(safeapps.confirmDeleteBtnStr).should('not.be.disabled').click() - getBody().findByText(safeapps.noSavedBatchesStr).should('be.visible') - getBody().findByText(safeapps.backToTransactionStr).should('be.visible') - }) - cy.readFile('cypress/downloads/E2E test.json').should('exist') - }) - - it('Verify there is notification if uploaded batch is from a different chain', () => { - cy.enter(iframeSelector).then((getBody) => { - getBody().findAllByText('choose a file').attachFile('test-mainnet-batch.json', { subjectType: 'drag-n-drop' }) - getBody().findAllByText(safeapps.warningStr).should('be.visible') - getBody().findAllByText(safeapps.anotherChainStr).should('be.visible') - }) - }) - - it('Verify there is error message when a modified batch is uploaded', () => { - cy.enter(iframeSelector).then((getBody) => { - getBody().findAllByText('choose a file').attachFile('test-modified-batch.json', { subjectType: 'drag-n-drop' }) - getBody().findAllByText(safeapps.changedPropertiesStr) - getBody().findAllByText('choose a file').should('be.visible') - }) - }) - - it('Verify an invalid batch cannot be uploaded', () => { - cy.enter(iframeSelector).then((getBody) => { - getBody() - .findAllByText('choose a file') - .attachFile('test-invalid-batch.json', { subjectType: 'drag-n-drop' }) - .findAllByText('choose a file') - .should('be.visible') - }) - }) - - it('Verify an empty batch cannot be uploaded', () => { - cy.enter(iframeSelector).then((getBody) => { getBody() - .findAllByText('choose a file') - .attachFile('test-empty-batch.json', { subjectType: 'drag-n-drop' }) - .findAllByText('choose a file') - .should('be.visible') - }) - }) - - it.skip('Verify a valid batch as successful can be simulated', () => { - cy.enter(iframeSelector).then((getBody) => { - getBody().findByLabelText(safeapps.enterAddressStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) - getBody().findByText(safeapps.keepProxiABIStr).click() - getBody().findByLabelText(safeapps.tokenAmount).type('0') - getBody().findByText(safeapps.addTransactionStr).click() - getBody().findByText(safeapps.createBatchStr).click() - getBody().findByText(safeapps.simulateBtnStr).click() - getBody().findByText(safeapps.transferStr).should('be.visible') - getBody().findByText(safeapps.successStr).should('be.visible') - }) - }) - - it.skip('Verify an invalid batch as failed can be simulated', () => { - cy.enter(iframeSelector).then((getBody) => { - getBody().findByLabelText(safeapps.enterAddressStr).type(safeAppSafes.SEP_SAFEAPP_SAFE_2) - getBody().findByText(safeapps.keepProxiABIStr).click() - getBody().findByLabelText(safeapps.tokenAmount).type('100') - getBody().findByText(safeapps.addTransactionStr).click() - getBody().findByText(safeapps.createBatchStr).click() - getBody().findByText(safeapps.simulateBtnStr).click() - getBody().findByText(safeapps.failedStr).should('be.visible') + .findAllByText(safeapps.requiredStr) + .then(($element) => { + const color = $element.css('color') + expect(utils.isInRedRange(color), 'Element color is ').to.be.true + }) }) }) }) diff --git a/cypress/e2e/safe-apps/tx_modal.cy.js b/cypress/e2e/safe-apps/tx_modal.cy.js index 2549e3aa6..3b33dc219 100644 --- a/cypress/e2e/safe-apps/tx_modal.cy.js +++ b/cypress/e2e/safe-apps/tx_modal.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 ls from '../../support/localstorage_data.js' const testAppName = 'Cypress Test App' const testAppDescr = 'Cypress Test App Description' @@ -9,7 +9,6 @@ const confirmTx = 'Confirm transaction' describe('Transaction modal tests', () => { beforeEach(() => { - cy.clearLocalStorage() cy.fixture('safe-app').then((html) => { cy.intercept('GET', `${constants.testAppUrl}/*`, html) cy.intercept('GET', `*/manifest.json`, { @@ -25,10 +24,6 @@ describe('Transaction modal tests', () => { { defaultCommandTimeout: 12000 }, () => { cy.visitSafeApp(`${constants.testAppUrl}/dummy`) - main.acceptCookies() - safeapps.clickOnContinueBtn() - safeapps.verifyWarningDefaultAppMsgIsDisplayed() - safeapps.clickOnContinueBtn() cy.findByRole('dialog').within(() => { cy.findByText(confirmTx) cy.findByText(unknownApp) diff --git a/cypress/e2e/smoke/add_owner.cy.js b/cypress/e2e/smoke/add_owner.cy.js index 4539f4cb4..0d5429298 100644 --- a/cypress/e2e/smoke/add_owner.cy.js +++ b/cypress/e2e/smoke/add_owner.cy.js @@ -16,9 +16,7 @@ describe('[SMOKE] Add Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) - cy.clearLocalStorage() main.waitForHistoryCallToComplete() - main.acceptCookies() main.verifyElementsExist([navigation.setupSection]) }) diff --git a/cypress/e2e/smoke/address_book.cy.js b/cypress/e2e/smoke/address_book.cy.js index 842369c38..326a33ea9 100644 --- a/cypress/e2e/smoke/address_book.cy.js +++ b/cypress/e2e/smoke/address_book.cy.js @@ -25,9 +25,7 @@ describe('[SMOKE] Address book tests', () => { beforeEach(() => { cy.visit(constants.addressBookUrl + staticSafes.SEP_STATIC_SAFE_4) - cy.clearLocalStorage() main.waitForHistoryCallToComplete() - main.acceptCookies() }) it('[SMOKE] Verify entry can be added', () => { diff --git a/cypress/e2e/smoke/assets.cy.js b/cypress/e2e/smoke/assets.cy.js index 368b266f3..c81721bc2 100644 --- a/cypress/e2e/smoke/assets.cy.js +++ b/cypress/e2e/smoke/assets.cy.js @@ -18,8 +18,6 @@ describe('[SMOKE] Assets tests', () => { beforeEach(() => { cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_2) - cy.clearLocalStorage() - main.acceptCookies() }) it('[SMOKE] Verify that the native token is visible', () => { diff --git a/cypress/e2e/smoke/batch_tx.cy.js b/cypress/e2e/smoke/batch_tx.cy.js index 93f459c7e..c0432a042 100644 --- a/cypress/e2e/smoke/batch_tx.cy.js +++ b/cypress/e2e/smoke/batch_tx.cy.js @@ -20,9 +20,7 @@ describe('[SMOKE] Batch transaction tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_2) - main.acceptCookies() }) it('[SMOKE] Verify empty batch list can be opened', () => { @@ -46,14 +44,15 @@ describe('[SMOKE] Batch transaction tests', () => { .then(() => main.isItemInLocalstorage(constants.localStorageKeys.SAFE_v2__batch, ls.batchData.entry0)) .then(() => { cy.reload() - batch.clickOnBatchCounter() wallet.connectSigner(signer) + batch.clickOnBatchCounter() + batch.clickOnConfirmBatchBtn() batch.verifyBatchTransactionsCount(2) batch.clickOnBatchCounter() cy.contains(funds_first_tx).parents('ul').as('TransactionList') cy.get('@TransactionList').find('li').eq(0).contains(funds_first_tx) - cy.get('@TransactionList').find('li').eq(1).contains(funds_first_tx) + cy.get('@TransactionList').find('li').eq(1).contains(funds_second_tx) }) }) @@ -63,12 +62,12 @@ describe('[SMOKE] Batch transaction tests', () => { .then(() => main.isItemInLocalstorage(constants.localStorageKeys.SAFE_v2__batch, ls.batchData.entry0)) .then(() => { cy.reload() - batch.clickOnBatchCounter() wallet.connectSigner(signer) + batch.clickOnBatchCounter() 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) - cy.get('@BatchList').contains(funds_second_tx).should('not.exist') + cy.get('@BatchList').contains(funds_first_tx).should('not.exist') }) }) }) diff --git a/cypress/e2e/smoke/create_safe_cf.cy.js b/cypress/e2e/smoke/create_safe_cf.cy.js index 6eba4ed3e..3a9205383 100644 --- a/cypress/e2e/smoke/create_safe_cf.cy.js +++ b/cypress/e2e/smoke/create_safe_cf.cy.js @@ -3,17 +3,22 @@ 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' +import { getEvents, events, checkDataLayerEvents } from '../../support/utils/gtag.js' const walletCredentials = JSON.parse(Cypress.env('CYPRESS_WALLET_CREDENTIALS')) -const signer = walletCredentials.OWNER_4_PRIVATE_KEY +// DO NOT use OWNER_2_PRIVATE_KEY for safe creation. Used for CF safes. +const signer = walletCredentials.OWNER_2_PRIVATE_KEY describe('[SMOKE] CF Safe creation tests', () => { beforeEach(() => { cy.visit(constants.welcomeUrl + '?chain=sep') + // Required for data layer cy.clearLocalStorage() main.acceptCookies() + getEvents() }) - it('[SMOKE] CF creation happy path', () => { + + it('[SMOKE] CF creation happy path. GA safe_created', () => { wallet.connectSigner(signer) owner.waitForConnectionStatus() createwallet.clickOnContinueWithWalletBtn() @@ -22,6 +27,20 @@ describe('[SMOKE] CF Safe creation tests', () => { createwallet.clickOnNextBtn() createwallet.selectPayLaterOption() createwallet.clickOnReviewStepNextBtn() - createwallet.verifyCFSafeCreated() + cy.wait(1000) + main.getAddedSafeAddressFromLocalStorage(constants.networkKeys.sepolia, 0).then((address) => { + const safe_created = [ + { + eventLabel: events.safeCreatedCF.eventLabel, + eventCategory: events.safeCreatedCF.category, + eventAction: events.safeCreatedCF.action, + eventType: events.safeCreatedCF.eventType, + event: events.safeCreatedCF.eventName, + safeAddress: address.slice(2), + }, + ] + checkDataLayerEvents(safe_created) + createwallet.verifyCFSafeCreated() + }) }) }) diff --git a/cypress/e2e/smoke/create_safe_simple.cy.js b/cypress/e2e/smoke/create_safe_simple.cy.js index 3b749b1e8..2ba907a99 100644 --- a/cypress/e2e/smoke/create_safe_simple.cy.js +++ b/cypress/e2e/smoke/create_safe_simple.cy.js @@ -10,8 +10,6 @@ const signer = walletCredentials.OWNER_4_PRIVATE_KEY describe('[SMOKE] Safe creation tests', () => { beforeEach(() => { cy.visit(constants.welcomeUrl + '?chain=sep') - cy.clearLocalStorage() - main.acceptCookies() }) it('[SMOKE] Verify a Wallet can be connected', () => { wallet.connectSigner(signer) diff --git a/cypress/e2e/smoke/create_tx.cy.js b/cypress/e2e/smoke/create_tx.cy.js index 0a7dd1c6f..af0b9ba05 100644 --- a/cypress/e2e/smoke/create_tx.cy.js +++ b/cypress/e2e/smoke/create_tx.cy.js @@ -17,9 +17,7 @@ describe('[SMOKE] Create transactions tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_10) - main.acceptCookies() wallet.connectSigner(signer) createtx.clickOnNewtransactionBtn() createtx.clickOnSendTokensBtn() diff --git a/cypress/e2e/smoke/create_tx_2.cy.js b/cypress/e2e/smoke/create_tx_2.cy.js index 7030235f6..330cabe92 100644 --- a/cypress/e2e/smoke/create_tx_2.cy.js +++ b/cypress/e2e/smoke/create_tx_2.cy.js @@ -24,9 +24,7 @@ describe('[SMOKE] Create transactions tests 2', () => { }) beforeEach(() => { - 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/smoke/dashboard.cy.js b/cypress/e2e/smoke/dashboard.cy.js index 626625d70..cfdb9b9c1 100644 --- a/cypress/e2e/smoke/dashboard.cy.js +++ b/cypress/e2e/smoke/dashboard.cy.js @@ -9,8 +9,8 @@ let staticSafes = [] const txData = ['14', 'Send', '-0.00002 ETH', '1 out of 1'] const txaddOwner = ['5', 'addOwnerWithThreshold', '1 out of 2'] -const txMultiSendCall3 = ['4', 'Safe: MultiSendCallOnly 1.3.0', '3 actions', '1 out of 2'] -const txMultiSendCall2 = ['6', 'Safe: MultiSendCallOnly 1.3.0', '2 actions', '1 out of 2'] +const txMultiSendCall3 = ['4', 'Batch', '3 actions', '1 out of 2'] +const txMultiSendCall2 = ['6', 'Batch', '2 actions', '1 out of 2'] describe('[SMOKE] Dashboard tests', { defaultCommandTimeout: 20000 }, () => { before(async () => { @@ -18,10 +18,7 @@ describe('[SMOKE] Dashboard tests', { defaultCommandTimeout: 20000 }, () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.homeUrl + staticSafes.SEP_STATIC_SAFE_2) - main.acceptCookies() - dashboard.verifyConnectTransactStrIsVisible() }) it('[SMOKE] Verify the overview widget is displayed', () => { @@ -32,18 +29,10 @@ describe('[SMOKE] Dashboard tests', { defaultCommandTimeout: 20000 }, () => { dashboard.verifyTxQueueWidget() }) - it('[SMOKE] Verify the featured Safe Apps are displayed', () => { - dashboard.verifyFeaturedAppsSection() - }) - it('[SMOKE] Verify the Safe Apps Section is displayed', () => { dashboard.verifySafeAppsSection() }) - it.skip('[SMOKE] Verify clicking on the share icon copies the app URL to the clipboard', () => { - dashboard.verifyShareBtnWorks(0, dashboard.copiedAppUrl) - }) - it('[SMOKE] Verify clicking on Explore Safe apps button opens list of all apps', () => { dashboard.clickOnExploreAppsBtn() }) diff --git a/cypress/e2e/smoke/import_export_data.cy.js b/cypress/e2e/smoke/import_export_data.cy.js index 6ecc9d6dd..b862c91ad 100644 --- a/cypress/e2e/smoke/import_export_data.cy.js +++ b/cypress/e2e/smoke/import_export_data.cy.js @@ -15,9 +15,7 @@ describe('[SMOKE] Import Export Data tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.dataSettingsUrl).then(() => { - main.acceptCookies() createwallet.selectNetwork(constants.networks.sepolia) }) }) diff --git a/cypress/e2e/smoke/import_export_data_2.cy.js b/cypress/e2e/smoke/import_export_data_2.cy.js index 7aa5ee3dc..7d52747f6 100644 --- a/cypress/e2e/smoke/import_export_data_2.cy.js +++ b/cypress/e2e/smoke/import_export_data_2.cy.js @@ -25,8 +25,6 @@ describe('[SMOKE] Import Export Data tests 2', () => { beforeEach(() => { cy.visit(constants.BALANCE_URL + staticSafes.SEP_STATIC_SAFE_13) - cy.clearLocalStorage() - main.acceptCookies() }) it('[SMOKE] Verify that the Sidebar Import button opens an import modal', () => { diff --git a/cypress/e2e/smoke/landing.cy.js b/cypress/e2e/smoke/landing.cy.js index f56883364..fe61ffea7 100644 --- a/cypress/e2e/smoke/landing.cy.js +++ b/cypress/e2e/smoke/landing.cy.js @@ -1,7 +1,6 @@ import * as constants from '../../support/constants' describe('[SMOKE] Landing page tests', () => { it('[SMOKE] Verify a user will be redirected to welcome page', () => { - cy.clearLocalStorage() cy.visit('/') cy.url().should('include', constants.welcomeUrl) }) diff --git a/cypress/e2e/smoke/load_safe.cy.js b/cypress/e2e/smoke/load_safe.cy.js index 1b7c7d569..ff05d98de 100644 --- a/cypress/e2e/smoke/load_safe.cy.js +++ b/cypress/e2e/smoke/load_safe.cy.js @@ -16,9 +16,7 @@ describe('[SMOKE] Load Safe tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.loadNewSafeSepoliaUrl) - main.acceptCookies() cy.wait(2000) }) diff --git a/cypress/e2e/smoke/messages_offchain.cy.js b/cypress/e2e/smoke/messages_offchain.cy.js index 78839b83b..408d66f46 100644 --- a/cypress/e2e/smoke/messages_offchain.cy.js +++ b/cypress/e2e/smoke/messages_offchain.cy.js @@ -23,12 +23,11 @@ describe('[SMOKE] Offchain Messages tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.transactionsMessagesUrl + staticSafes.SEP_STATIC_SAFE_10) - main.acceptCookies() }) - it('[SMOKE] Verify summary for off-chain unsigned messages', () => { + // TODO: Clarify changes + it.skip('[SMOKE] Verify summary for off-chain unsigned messages', () => { createTx.verifySummaryByIndex(0, [ typeMessagesGeneral.sign, typeMessagesGeneral.oneOftwo, @@ -41,7 +40,8 @@ describe('[SMOKE] Offchain Messages tests', () => { ]) }) - it('[SMOKE] Verify summary for off-chain signed messages', () => { + // TODO: Clarify changes + it.skip('[SMOKE] Verify summary for off-chain signed messages', () => { createTx.verifySummaryByIndex(1, [ typeMessagesGeneral.confirmed, typeMessagesGeneral.twoOftwo, @@ -80,7 +80,8 @@ describe('[SMOKE] Offchain Messages tests', () => { main.verifyTextVisibility(values) }) - it('[SMOKE] Verify confirmation window is displayed for unsigned message', () => { + // TODO: Clarify changes + it.skip('[SMOKE] Verify confirmation window is displayed for unsigned message', () => { wallet.connectSigner(signer) messages.clickOnMessageSignBtn(2) msg_confirmation_modal.verifyConfirmationWindowTitle(modal.modalTitiles.confirmMsg) diff --git a/cypress/e2e/smoke/nfts.cy.js b/cypress/e2e/smoke/nfts.cy.js index 30fa5c8d5..1ad6a72e8 100644 --- a/cypress/e2e/smoke/nfts.cy.js +++ b/cypress/e2e/smoke/nfts.cy.js @@ -15,9 +15,7 @@ describe('[SMOKE] NFTs tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.balanceNftsUrl + staticSafes.SEP_STATIC_SAFE_2) - main.acceptCookies() nfts.waitForNftItems(2) }) diff --git a/cypress/e2e/smoke/replace_owner.cy.js b/cypress/e2e/smoke/replace_owner.cy.js index ca931b895..b05e400da 100644 --- a/cypress/e2e/smoke/replace_owner.cy.js +++ b/cypress/e2e/smoke/replace_owner.cy.js @@ -15,8 +15,6 @@ describe('[SMOKE] Replace Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + staticSafes.SEP_STATIC_SAFE_4) - cy.clearLocalStorage() - main.acceptCookies() cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) @@ -33,6 +31,6 @@ describe('[SMOKE] Replace Owners tests', () => { it('[SMOKE] Verify that the owner replacement form is opened', () => { wallet.connectSigner(signer) owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() + owner.openReplaceOwnerWindow(0) }) }) diff --git a/cypress/e2e/smoke/spending_limits.cy.js b/cypress/e2e/smoke/spending_limits.cy.js index 63451967e..ac080d65d 100644 --- a/cypress/e2e/smoke/spending_limits.cy.js +++ b/cypress/e2e/smoke/spending_limits.cy.js @@ -16,8 +16,6 @@ describe('[SMOKE] Spending limits tests', () => { beforeEach(() => { 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') diff --git a/cypress/e2e/smoke/tx_history.cy.js b/cypress/e2e/smoke/tx_history.cy.js index 7559ed74a..32911d795 100644 --- a/cypress/e2e/smoke/tx_history.cy.js +++ b/cypress/e2e/smoke/tx_history.cy.js @@ -20,9 +20,7 @@ describe('[SMOKE] Tx history tests', () => { }) beforeEach(() => { - cy.clearLocalStorage() cy.visit(constants.transactionsHistoryUrl + staticSafes.SEP_STATIC_SAFE_7) - main.acceptCookies() }) // Token receipt diff --git a/cypress/fixtures/safes/funds.json b/cypress/fixtures/safes/funds.json index 66cda0772..2d51a4d13 100644 --- a/cypress/fixtures/safes/funds.json +++ b/cypress/fixtures/safes/funds.json @@ -11,5 +11,7 @@ "SEP_FUNDS_SAFE_10": "sep:0xE72d4D7E87672c14Df3d449C6b79f20151c18fC1", "SEP_FUNDS_SAFE_11": "sep:0x74D5228112a9652a9825a6A285Fb39e290269172", "SEP_FUNDS_SAFE_12": "sep:0xe5DC58EfDA6ebe93014AaE7A5a673C5F80118171", - "ETH_FUNDS_SAFE_13": "eth:0x8675B754342754A30A2AeF474D114d8460bca19b" + "ETH_FUNDS_SAFE_13": "eth:0x8675B754342754A30A2AeF474D114d8460bca19b", + "SEP_FUNDS_SAFE_14": "sep:0xF9e21491A1FccD40c9B658b8cA5e25018BA9105b", + "SEP_FUNDS_SAFE_15": "sep:0x1b412E4E47e3199c96d4544FD15875eA6886D4F0" } diff --git a/cypress/fixtures/safes/recovery.json b/cypress/fixtures/safes/recovery.json index f04d8c5d7..5c39e72e4 100644 --- a/cypress/fixtures/safes/recovery.json +++ b/cypress/fixtures/safes/recovery.json @@ -1,5 +1,6 @@ { "SEP_RECOVERY_SAFE_1": "sep:0x702E067A0015F1b835d9c631Cb28A9F617314F27", "SEP_RECOVERY_SAFE_2": "sep:0xb791302040DB5Ab4Ade0b5295cecCaeF07AF07a1", - "SEP_RECOVERY_SAFE_3": "sep:0xAE1E3f93fda95eEbb857Ee06325f6F1e45EF3CBE" + "SEP_RECOVERY_SAFE_3": "sep:0xAE1E3f93fda95eEbb857Ee06325f6F1e45EF3CBE", + "SEP_RECOVERY_SAFE_4": "sep:0xe41D568F5040FD9adeE8B64200c6B7C363C68c41" } diff --git a/cypress/fixtures/safes/static.json b/cypress/fixtures/safes/static.json index 3a470ab92..6e02c7541 100644 --- a/cypress/fixtures/safes/static.json +++ b/cypress/fixtures/safes/static.json @@ -1,4 +1,5 @@ { + "SEP_STATIC_SAFE_0": "sep:0x926186108f74dB20BFeb2b6c888E523C78cb7E00", "SEP_STATIC_SAFE_1": "sep:0x03042B890b99552b60A073F808100517fb148F60", "SEP_STATIC_SAFE_2": "sep:0xBd69b0a9DC90eB6F9bAc3E4a5875f437348b6415", "SEP_STATIC_SAFE_3": "sep:0x33C4AA5729D91FfB3B87AEf8a324bb6304Fb905c", @@ -13,5 +14,16 @@ "SEP_STATIC_SAFE_11": "sep:0x10B45a24640E2170B6AA63ea3A289D723a0C9cba", "SEP_STATIC_SAFE_12": "sep:0xFFfaC243A24EecE6553f0Da278322aCF1Fb6CeF1", "SEP_STATIC_SAFE_13": "sep:0x027bBe128174F0e5e5d22ECe9623698E01cd3970", - "SEP_STATIC_SAFE_14": "sep:0xe41D568F5040FD9adeE8B64200c6B7C363C68c41" + "SEP_STATIC_SAFE_14": "sep:0xe41D568F5040FD9adeE8B64200c6B7C363C68c41", + "ETH_STATIC_SAFE_15": "eth:0xfF501B324DC6d78dC9F983f140B9211c3EdB4dc7", + "GNO_STATIC_SAFE_16": "gno:0xB8d760a90a5ed54D3c2b3EFC231277e99188642A", + "MATIC_STATIC_SAFE_17": "matic:0x6D04edC44F7C88faa670683036edC2F6FC10b86e", + "BNB_STATIC_SAFE_18": "bnb:0x1D28a316431bAFf410Fe53398c6C5BD566032Eec", + "AURORA_STATIC_SAFE_19": "aurora:0xCEA454dD3d76Da856E72C3CBaDa8ee6A789aD167", + "AVAX_STATIC_SAFE_20": "avax:0x480e5A3E90a3fF4a16AECCB5d638fAba96a15c28", + "LINEA_STATIC_SAFE_21": "linea:0x95934e67299E0B3DD277907acABB512802f3536E", + "ZKSYNC_STATIC_SAFE_22": "zksync:0x49136c0270c5682FFbb38Cb29Ecf0563b2E1F9f6", + "SEP_STATIC_SAFE_23": "sep:0x589d862CE2d519d5A862066bB923da0564c3D2EA", + "SEP_STATIC_SAFE_24": "sep:0x49DC5764961DA4864DC5469f16BC68a0F765f2F2", + "SEP_STATIC_SAFE_25": "sep:0x4ECFAa2E8cb4697bCD27bdC9Ce3E16f03F73124F" } diff --git a/cypress/fixtures/txhistory_data_data.json b/cypress/fixtures/txhistory_data_data.json index 8a276ccf8..e80972788 100644 --- a/cypress/fixtures/txhistory_data_data.json +++ b/cypress/fixtures/txhistory_data_data.json @@ -10,7 +10,7 @@ "executedBy": "Executed" }, "accountCreation": { - "actionsSummary": "Safe Account created by 0xC16D...6fED", + "actionsSummary": "Created by 0xC16D...6fED", "transactionSafehash": {}, "summaryTime": "10:30 AM", "title": "Safe Account created", @@ -62,11 +62,11 @@ "safeTxHash": "0x1303...0fe2" }, "batchNativeTransfer": { - "title": "Safe: MultiSendCallOnly 1.3.0", + "title": "Batch", "summaryTxInfo": "2 actions", "summaryTime": "11:24 AM", - "description": "MultiSend contract", - "altImage": "Safe: MultiSendCallOnly 1.3.0", + "description": "Batch transaction with 2 actions", + "altImage": "Batch", "contractTitle": "Safe: MultiSendCallOnly 1.3.0", "contractAddress": "sep:0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", "transactionHash": "0xa5dd...b064", @@ -82,9 +82,9 @@ "description": "Add signer", "altImage": "addOwnerWithThreshold", "requiredConfirmationsTitle": "Required confirmations for new transactions", - "ownerAddress": "sep:0x01A9F68e339da12565cfBc47fe7D6EdEcB11C46f", - "transactionHash": "0x51d5...da62", - "safeTxHash": "0xdcc5...e1b2" + "ownerAddress": "sep:0x4fe7164d7cA511Ab35520bb14065F1693240dC90", + "transactionHash": "0xfcad...ab35", + "safeTxHash": "0x8583...3d2d" }, "removeOwner": { "title": "removeOwner", @@ -145,28 +145,29 @@ "baseGas": "baseGas" }, "spendingLimits": { - "title": "Safe: MultiSendCallOnly 1.3.0", + "title": "Batch", "summaryTxInfo": "3 actions", "summaryTime": "11:06 AM", - "description": "MultiSend contract", - "altImage": "Safe: MultiSendCallOnly 1.3.0", + "description": "Batch transaction with 3 actions", + "altImage": "Batch", "contractTitle": "Safe: MultiSendCallOnly 1.3.0", "contractAddress": "sep:0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B", "transactionHash": "0x69c3...bc37", "safeTxHash": "0xf81c...243e", + "call_multiSend": "CallmultiSend", "enableModule": { "title": "enableModule", "description": "Interact with", "interactionAddress": "sep:0x5912f6616c84024cD1aff0D5b55bb36F5180fFdb", "moduleAddress": "sep:0xCFbF...3134", - "moduleAddressTitle": "module(address)" + "moduleAddressTitle": "module address" }, "addDelegate": { "title": "addDelegate", "description": "Interact with", "interactionAddress": "sep:0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134", "delegateAddress": "sep:0xC16D...6fED", - "delegateAddressTitle": "delegate(address)" + "delegateAddressTitle": "delegate address" }, "setAllowance": { "title": "setAllowance", @@ -175,7 +176,7 @@ "allowanceAmount": "100000000000", "resetTimeMin": "0", "resetBaseMin": "0", - "delegateAddressTitle": "delegate(address)" + "delegateAddressTitle": "delegate address" } }, "untrustedReceivedToken": { @@ -188,6 +189,18 @@ "transactionHashCopied": "0x54e7e766b08d4210bc1cfcc84d84ca4782a0cc1efe9e7d9c032d305060ed2a7c", "altImage": "Received", "altToken": "" + }, + "bulkTransaction": { + "send": "Sent", + "receive": "Received", + "twoTx": "2 transactions", + "threeTx": "3 transactions", + "wrappedEther": "Wrapped Ether", + "addOwnerWithThreshold": "addOwnerWithThreshold", + "1transfer": "1transfer", + "2removeOwner": "2removeOwner", + "COW": "-10 COW", + "DAI": "363.19846 DAI" } } } diff --git a/cypress/fixtures/txmessages_data.json b/cypress/fixtures/txmessages_data.json index fdd609947..71816eddf 100644 --- a/cypress/fixtures/txmessages_data.json +++ b/cypress/fixtures/txmessages_data.json @@ -3,6 +3,7 @@ "general": { "confirmed": "Confirmed", "sign": "Sign", + "zeroOftwo": "0 out of 2", "oneOftwo": "1 out of 2", "twoOftwo": "2 out of 2" }, diff --git a/cypress/snapshots/actual/cypress/e2e/regression/tx_decoding.cy.js/tx_decoding.png b/cypress/snapshots/actual/cypress/e2e/regression/tx_decoding.cy.js/tx_decoding.png new file mode 100644 index 000000000..08188e23a Binary files /dev/null and b/cypress/snapshots/actual/cypress/e2e/regression/tx_decoding.cy.js/tx_decoding.png differ diff --git a/cypress/support/api/utils_ether.js b/cypress/support/api/utils_ether.js index 4a065ffd0..367b675fb 100644 --- a/cypress/support/api/utils_ether.js +++ b/cypress/support/api/utils_ether.js @@ -1,13 +1,5 @@ import { ethers } from 'ethers' -import { EthersAdapter } from '@safe-global/protocol-kit' export function createSigners(privateKeys, provider) { return privateKeys.map((privateKey) => new ethers.Wallet(privateKey, provider)) } - -export function createEthersAdapter(signer) { - return new EthersAdapter({ - ethers, - signerOrProvider: signer, - }) -} diff --git a/cypress/support/api/utils_protocolkit.js b/cypress/support/api/utils_protocolkit.js index 500b0d5a0..61b56e5ab 100644 --- a/cypress/support/api/utils_protocolkit.js +++ b/cypress/support/api/utils_protocolkit.js @@ -3,8 +3,11 @@ import Safe from '@safe-global/protocol-kit' export async function createSafes(safeConfigurations) { const safes = [] for (const config of safeConfigurations) { - const safe = await Safe.create({ - ethAdapter: config.ethAdapter, + const providerUrl = config.provider._getConnection().url + + const safe = await Safe.init({ + provider: providerUrl, + signer: config.signer, safeAddress: config.safeAddress, }) safes.push(safe) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 0bba9e43a..b49298c6b 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -37,6 +37,7 @@ export const goerlySafeName = /g(รถ|oe)rli-safe/ export const sepoliaSafeName = 'sepolia-safe' export const goerliToken = /G(รถ|oe)rli Ether/ +export const prodbaseUrl = 'https://app.safe.global' export const swapWidget = 'https://swap.cow.fi/#/11155111/widget/swap/' export const safeTestAppurl = 'https://safe-apps-test-app.pages.dev' export const TX_Builder_url = 'https://safe-apps.dev.5afe.dev/tx-builder' @@ -65,6 +66,7 @@ export const appSettingsUrl = '/settings/safe-apps' export const setupUrl = '/settings/setup?safe=' export const dataSettingsUrl = '/settings/data?safe=' export const securityUrl = '/settings/security?safe=' +export const modulesUrl = '/settings/modules?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' @@ -196,6 +198,7 @@ export const addressBookErrrMsg = { emptyAddress: 'Owner', safeAlreadyAdded: 'Safe Account is already added', prefixMismatch: "doesn't match the current chain", + ownSafeGuardian: 'The Safe Account cannot be a Recoverer of itself', invalidPrefix(prefix) { return `"${prefix}" doesn't match the current chain` }, @@ -225,16 +228,19 @@ export const addresBookContacts = { }, } +export const CURRENT_COOKIE_TERMS_VERSION = Cypress.env('CURRENT_COOKIE_TERMS_VERSION') + export const localStorageKeys = { SAFE_v2__addressBook: 'SAFE_v2__addressBook', SAFE_v2__batch: 'SAFE_v2__batch', SAFE_v2__settings: 'SAFE_v2__settings', SAFE_v2__addedSafes: 'SAFE_v2__addedSafes', SAFE_v2__safeApps: 'SAFE_v2__safeApps', - SAFE_v2__cookies: 'SAFE_v2__cookies', + SAFE_v2_cookies: 'SAFE_v2__cookies_terms', 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', SAFE_v2__undeployedSafes: 'SAFE_v2__undeployedSafes', + SAFE_v2__batch: 'SAFE_v2__batch', } diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index fb98bb629..14693ec95 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -17,6 +17,8 @@ import '@testing-library/cypress/add-commands' import './commands' import './safe-apps-commands' +import * as constants from './constants' +import * as ls from './localstorage_data' // Alternatively you can use CommonJS syntax: // require('./commands') @@ -25,7 +27,16 @@ import './safe-apps-commands' However, in cypress the cookie banner state is evaluated after the banner has been dismissed not before which displays the terms banner even though it shouldn't so we need to globally hide it in our tests. */ +const { addCompareSnapshotCommand } = require('cypress-visual-regression/dist/command') +addCompareSnapshotCommand() + +const beamer = JSON.parse(Cypress.env('BEAMER_DATA_E2E')) +const productID = beamer.PRODUCT_ID + before(() => { + Cypress.on('uncaught:exception', (err, runnable) => { + return false + }) cy.on('log:added', (ev) => { if (Cypress.config('hideXHR')) { const app = window.top @@ -41,4 +52,20 @@ before(() => { beforeEach(() => { cy.setupInterceptors() + cy.clearAllSessionStorage() + cy.clearLocalStorage() + cy.clearCookies() + cy.window().then((window) => { + const getDate = () => new Date().toISOString() + const beamerKey1 = `_BEAMER_FIRST_VISIT_${productID}` + const beamerKey2 = `_BEAMER_BOOSTED_ANNOUNCEMENT_DATE_${productID}` + const cookiesKey = 'SAFE_v2__cookies_terms' + window.localStorage.setItem(beamerKey1, getDate()) + window.localStorage.setItem(beamerKey2, getDate()) + window.localStorage.setItem(cookiesKey, ls.cookies.acceptedCookies) + window.localStorage.setItem( + constants.localStorageKeys.SAFE_v2__SafeApps__infoModal, + ls.appPermissions(constants.safeTestAppurl).infoModalAccepted, + ) + }) }) diff --git a/cypress/support/localstorage_data.js b/cypress/support/localstorage_data.js index 65add7561..986afb30c 100644 --- a/cypress/support/localstorage_data.js +++ b/cypress/support/localstorage_data.js @@ -1,4 +1,15 @@ /* eslint-disable */ + +import { CURRENT_COOKIE_TERMS_VERSION } from './constants.js' + +const cookieState = { + necessary: true, + updates: true, + analytics: true, + terms: true, + termsVersion: CURRENT_COOKIE_TERMS_VERSION, +} + export const batchData = { entry0: { 11155111: { @@ -111,7 +122,7 @@ export const batchData = { logoUri: null, }, direction: 'OUTGOING', - transferInfo: { type: 'NATIVE_COIN', value: '1000000000000000' }, + transferInfo: { type: 'NATIVE_COIN', value: '2000000000000000' }, }, txData: { hexData: null, @@ -335,6 +346,11 @@ export const addressBookData = { '0x6a5602335a878ADDCa4BF63a050E34946B56B5bC': 'BB Safe', }, }, + autofillData: { + 11155111: { + '0x01A9F68e339da12565cfBc47fe7D6EdEcB11C46f': 'David', + }, + }, sameOwnerName: { 11155111: { '0xC16Db0251654C0a72E91B190d81eAD367d2C6fED': 'Automation owner Sepolia', @@ -374,7 +390,7 @@ export const addressBookData = { '0xc2F3645bfd395516d1a18CA6ad9298299d328C01': 'Safe 27', }, }, - cookies: { necessary: true, updates: true, analytics: true }, + cookies: cookieState, } export const safeSettings = { @@ -657,6 +673,13 @@ export const customApps = (url) => ({ }, }) +const infoModalAccepted = { + 11155111: { + consentsAccepted: true, + warningCheckedCustomApps: [], + }, +} + export const appPermissions = (url) => ({ grantedPermissions: { [url]: [ @@ -664,27 +687,33 @@ export const appPermissions = (url) => ({ { feature: 'microphone', status: 'granted' }, ], }, - infoModalAccepted: { 11155111: { consentsAccepted: true, warningCheckedCustomApps: [] } }, + infoModalAccepted: JSON.stringify(infoModalAccepted), }) export const cookies = { - acceptedCookies: { necessary: true, updates: true, analytics: true }, + acceptedCookies: JSON.stringify(cookieState), acceptedTokenListOnboarding: true, } export const undeployedSafe = { safe1: { 11155111: { - '0xe41D568F5040FD9adeE8B64200c6B7C363C68c41': { + '0x926186108f74dB20BFeb2b6c888E523C78cb7E00': { props: { safeAccountConfig: { threshold: 1, - owners: ['0xC16Db0251654C0a72E91B190d81eAD367d2C6fED'], + owners: ['0x9445deb174C1eCbbfce8d31D33F438B8e7a0F1BA'], fallbackHandler: '0x017062a1dE2FE6b99BE3d9d37841FeD19F573804', }, - safeDeploymentConfig: { saltNonce: '20', safeVersion: '1.3.0' }, + safeDeploymentConfig: { + saltNonce: '21', + safeVersion: '1.3.0', + }, + }, + status: { + status: 'AWAITING_EXECUTION', + type: 'PayLater', }, - status: { status: 'AWAITING_EXECUTION' }, }, }, }, diff --git a/cypress/support/safe-apps-commands.js b/cypress/support/safe-apps-commands.js index ddbf3d826..798eb50d3 100644 --- a/cypress/support/safe-apps-commands.js +++ b/cypress/support/safe-apps-commands.js @@ -8,7 +8,7 @@ Cypress.Commands.add('visitSafeApp', (appUrl) => { window.localStorage.setItem( INFO_MODAL_KEY, JSON.stringify({ - 5: { consentsAccepted: true, warningCheckedCustomApps: allowedApps }, + 11155111: { consentsAccepted: true, warningCheckedCustomApps: allowedApps }, }), ) }) diff --git a/cypress/support/utils/checkers.js b/cypress/support/utils/checkers.js index 112e11b09..492071bb6 100644 --- a/cypress/support/utils/checkers.js +++ b/cypress/support/utils/checkers.js @@ -2,3 +2,8 @@ export function startsWith0x(str) { const pattern = /^0x/ return pattern.test(str) } + +export const isInRedRange = (rgbColor) => { + const [r, g, b] = rgbColor.match(/\d+/g).map(Number) + return r >= 200 && r <= 255 && g >= 0 && g <= 95 && b >= 0 && b <= 120 +} diff --git a/cypress/support/utils/gtag.js b/cypress/support/utils/gtag.js new file mode 100644 index 000000000..fe6e9fd07 --- /dev/null +++ b/cypress/support/utils/gtag.js @@ -0,0 +1,72 @@ +export function getEvents() { + cy.window().then((win) => { + cy.wrap(win.dataLayer).as('dataLayer') + }) +} + +export const checkDataLayerEvents = (expectedEvents) => { + cy.get('@dataLayer').should((dataLayer) => { + expectedEvents.forEach((expectedEvent) => { + const eventExists = dataLayer.some((event) => { + return Object.keys(expectedEvent).every((key) => { + return event[key] === expectedEvent[key] + }) + }) + expect(eventExists, `Expected event matching fields: ${JSON.stringify(expectedEvent)} not found`).to.be.true + }) + }) +} + +export const events = { + safeCreatedCF: { + category: 'create-safe', + action: 'Created Safe', + eventName: 'safe_created', + eventLabel: 'counterfactual', + eventType: 'safe_created', + }, + + txCreatedSwapOwner: { + category: 'transactions', + action: 'Create transaction', + eventName: 'tx_created', + eventLabel: 'owner_swap', + }, + + txConfirmedAddOwner: { + category: 'transactions', + action: 'Confirm transaction', + eventLabel: 'owner_add', + eventType: 'tx_confirmed', + event: 'tx_confirmed', + }, + txCreatedSwap: { + category: 'transactions', + action: 'Confirm transaction', + eventLabel: 'native_swap', + eventType: 'tx_created', + }, + + txConfirmedSwap: { + category: 'transactions', + action: 'Confirm transaction', + eventLabel: 'native_swap', + eventType: 'tx_confirmed', + }, + + txCreatedTxBuilder: { + category: 'transactions', + action: 'Confirm transaction', + eventLabel: 'https://safe-apps.dev.5afe.dev/tx-builder', + eventType: 'tx_created', + event: 'tx_created', + }, + + txConfirmedTxBuilder: { + category: 'transactions', + action: 'Confirm transaction', + eventLabel: 'https://safe-apps.dev.5afe.dev/tx-builder', + eventType: 'tx_confirmed', + event: 'tx_confirmed', + }, +} diff --git a/cypress/support/utils/txquery.js b/cypress/support/utils/txquery.js index c4845de5f..844501610 100644 --- a/cypress/support/utils/txquery.js +++ b/cypress/support/utils/txquery.js @@ -9,7 +9,7 @@ function buildQueryUrl({ chainId, safeAddress, transactionType, ...params }) { const defaultParams = { safe: `sep:${safeAddress}`, - timezone_offset: '7200000', + timezone: 'Europe/Berlin', trusted: 'false', } diff --git a/cypress/support/utils/wallet.js b/cypress/support/utils/wallet.js index a9ea82fd8..957b229f1 100644 --- a/cypress/support/utils/wallet.js +++ b/cypress/support/utils/wallet.js @@ -23,7 +23,7 @@ export function connectSigner(signer) { function handlePkConnect() { cy.get('body').then(($body) => { if ($body.find(pkConnectBtn).length > 0) { - cy.get(pkInput).find('input').clear().type(signer) + cy.get(pkInput).find('input').clear().type(signer, { log: false, force: true }) cy.get(pkConnectBtn).click() } }) @@ -32,6 +32,7 @@ export function connectSigner(signer) { function enterPrivateKey() { cy.wait(1000) cy.get(connectWalletBtn) + .eq(0) .should('be.enabled') .and('be.visible') .click() diff --git a/docs/update-terms.md b/docs/update-terms.md new file mode 100644 index 000000000..5e69fac01 --- /dev/null +++ b/docs/update-terms.md @@ -0,0 +1,28 @@ +# How to update Terms & Conditions + +To update the terms and conditions, follow these steps: + +1. Export the terms and conditions from Google Docs as a Markdown file. +2. Replace the content of the src/markdown/terms/terms.md file with the exported content. +3. Update the frontmatter of the file with the new version number and date. + +Thatโ€™s it! + +The updated terms and conditions will be displayed in the app with the correct version number and date. A popup banner +will automatically appear for users who havenโ€™t accepted the new terms. + +## How does this work? + +We rely on the version number from the frontmatter. When the Redux store is rehydrated, we check the version stored in +the store against the version in the frontmatter. If they differ, we reset the accepted terms, forcing the user to +accept the new version. + +The Markdown file is automatically converted to HTML and displayed in the app. Note that because the Markdown was +generated +from Google Docs, we require the remark-heading-id plugin. Additionally, since Google Docs uses {# ...} syntax, it will +fail in an MDX file. + +For Cypress, we follow a similar process. We read the version from the frontmatter and pass it as an environment +variable. + +For Jest tests, we mock the file and read the version from the mock. diff --git a/jest.config.cjs b/jest.config.cjs index 8eb1dc0ed..ebf77f7fc 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -13,6 +13,7 @@ const customJestConfig = { // Handle module aliases (this will be automatically configured for you soon) '^@/(.*)$': '/src/$1', '^.+\\.(svg)$': '/mocks/svg.js', + '^.+/markdown/terms/terms\\.md$': '/mocks/terms.md.js', isows: '/node_modules/isows/_cjs/index.js', }, testEnvironment: 'jest-environment-jsdom', diff --git a/jest.setup.js b/jest.setup.js index 702bee613..3a44ca67c 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -43,7 +43,7 @@ const NumberFormat = Intl.NumberFormat const englishTestLocale = 'en' // `viem` used by the `safe-apps-sdk` uses `TextEncoder` and `TextDecoder` which are not available in jsdom for some reason -Object.assign(global, { TextDecoder, TextEncoder }) +Object.assign(global, { TextDecoder, TextEncoder, fetch: jest.fn() }) jest.spyOn(Intl, 'NumberFormat').mockImplementation((locale, ...rest) => new NumberFormat([englishTestLocale], ...rest)) diff --git a/mocks/terms.md.js b/mocks/terms.md.js new file mode 100644 index 000000000..cadf08493 --- /dev/null +++ b/mocks/terms.md.js @@ -0,0 +1,6 @@ +export const metadata = { + version: 'test-version', + last_update_date: 'test-date', +} + +export default metadata diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03dc..a4a7b3f5c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/next.config.mjs b/next.config.mjs index f3fc7f9e8..b867dc004 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,11 @@ import path from 'path' import withBundleAnalyzer from '@next/bundle-analyzer' import withPWAInit from '@ducanh2912/next-pwa' +import remarkGfm from 'remark-gfm' +import remarkHeadingId from 'remark-heading-id' +import createMDX from '@next/mdx' +import remarkFrontmatter from 'remark-frontmatter' +import remarkMdxFrontmatter from 'remark-mdx-frontmatter' const SERVICE_WORKERS_PATH = './src/service-workers' @@ -26,13 +31,21 @@ const nextConfig = { unoptimized: true, }, + pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], reactStrictMode: false, productionBrowserSourceMaps: true, eslint: { dirs: ['src', 'cypress'], }, experimental: { - optimizePackageImports: ['@mui/material', '@mui/icons-material', 'lodash', 'date-fns', '@sentry/react', '@gnosis.pm/zodiac'], + optimizePackageImports: [ + '@mui/material', + '@mui/icons-material', + 'lodash', + 'date-fns', + '@sentry/react', + '@gnosis.pm/zodiac', + ], }, webpack(config) { config.module.rules.push({ @@ -69,7 +82,19 @@ const nextConfig = { return config }, } +const withMDX = createMDX({ + extension: /\.(md|mdx)?$/, + jsx: true, + options: { + remarkPlugins: [ + remarkFrontmatter, + [remarkMdxFrontmatter, { name: 'metadata' }], + remarkHeadingId, remarkGfm], + rehypePlugins: [], + }, +}) + export default withBundleAnalyzer({ enabled: process.env.ANALYZE === 'true', -})(withPWA(nextConfig)) +})(withPWA(withMDX(nextConfig))) diff --git a/package.json b/package.json index bdc61714f..788afb52c 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.2", + "version": "1.44.3", "type": "module", "scripts": { "dev": "next dev", @@ -31,7 +31,7 @@ "update-wc": "yarn add @walletconnect/web3wallet@latest @walletconnect/utils@latest; yarn add -D @walletconnect/types@latest", "prepare": "husky", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build --quiet" }, "engines": { "node": ">=16" @@ -45,64 +45,67 @@ "dependencies": { "@cowprotocol/widget-react": "^0.9.3", "@ducanh2912/next-pwa": "^9.7.1", - "@emotion/cache": "^11.11.0", + "@emotion/cache": "^11.13.1", "@emotion/react": "^11.11.0", "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.0", - "@gnosis.pm/zodiac": "^4.0.1", + "@gnosis.pm/zodiac": "^4.0.3", "@mui/icons-material": "^5.14.20", "@mui/material": "^5.14.20", "@mui/x-date-pickers": "^5.0.20", - "@reduxjs/toolkit": "^1.9.5", - "@safe-global/api-kit": "^2.3.2", - "@safe-global/protocol-kit": "^3.1.1", + "@reduxjs/toolkit": "^2.2.6", + "@safe-global/api-kit": "^2.4.6", + "@safe-global/protocol-kit": "^4.1.1", "@safe-global/safe-apps-sdk": "^9.1.0", - "@safe-global/safe-deployments": "^1.36.0", - "@safe-global/safe-gateway-typescript-sdk": "3.21.8", - "@safe-global/safe-modules-deployments": "^1.2.0", + "@safe-global/safe-deployments": "^1.37.8", + "@safe-global/safe-gateway-typescript-sdk": "3.22.3-beta.15", + "@safe-global/safe-modules-deployments": "^2.2.1", "@sentry/react": "^7.91.0", "@spindl-xyz/attribution-lite": "^1.4.0", - "@walletconnect/utils": "^2.13.1", - "@walletconnect/web3wallet": "^1.12.1", + "@walletconnect/utils": "^2.16.1", + "@walletconnect/web3wallet": "^1.15.1", "@web3-onboard/coinbase": "^2.2.6", "@web3-onboard/core": "^2.21.4", - "@web3-onboard/injected-wallets": "^2.10.14", + "@web3-onboard/injected-wallets": "^2.11.2", "@web3-onboard/keystone": "^2.3.7", "@web3-onboard/ledger": "2.3.2", "@web3-onboard/trezor": "^2.4.2", "@web3-onboard/walletconnect": "^2.5.4", "blo": "^1.1.1", - "classnames": "^2.3.1", + "classnames": "^2.5.1", "date-fns": "^2.30.0", "ethers": "^6.11.1", "exponential-backoff": "^3.1.0", "firebase": "^10.3.1", - "fuse.js": "^6.6.2", + "fuse.js": "^7.0.0", "idb-keyval": "^6.2.1", "js-cookie": "^3.0.1", "lodash": "^4.17.21", - "next": "^14.1.1", + "next": "^14.2.13", "papaparse": "^5.3.2", "qrcode.react": "^3.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-gtm-module": "^2.0.11", "react-hook-form": "7.41.1", "react-papaparse": "^4.0.2", - "react-redux": "^8.0.5", - "semver": "^7.5.2", - "zodiac-roles-deployments": "^2.2.2" + "react-redux": "^9.1.2", + "semver": "^7.6.3", + "zodiac-roles-deployments": "^2.2.5" }, "devDependencies": { "@chromatic-com/storybook": "^1.3.1", "@cowprotocol/app-data": "^2.1.0", "@faker-js/faker": "^8.1.0", + "@mdx-js/loader": "^3.0.1", + "@mdx-js/react": "^3.0.1", "@next/bundle-analyzer": "^13.5.6", - "@openzeppelin/contracts": "^4.9.2", - "@safe-global/safe-core-sdk-types": "^4.1.1", + "@next/mdx": "^14.2.11", + "@openzeppelin/contracts": "^4.9.6", + "@safe-global/safe-core-sdk-types": "^5.0.1", "@sentry/types": "^7.74.0", - "@storybook/addon-designs": "^8.0.0", + "@storybook/addon-designs": "^8.0.3", "@storybook/addon-essentials": "^8.0.6", "@storybook/addon-interactions": "^8.0.6", "@storybook/addon-links": "^8.0.6", @@ -119,19 +122,21 @@ "@testing-library/user-event": "^14.4.2", "@typechain/ethers-v6": "^0.5.1", "@types/jest": "^29.5.4", - "@types/js-cookie": "^3.0.2", + "@types/js-cookie": "^3.0.6", "@types/lodash": "^4.14.182", + "@types/mdx": "^2.0.13", "@types/node": "18.11.18", "@types/qrcode": "^1.5.5", - "@types/react": "^18.2.75", - "@types/react-dom": "^18.2.24", + "@types/react": "^18.3.10", + "@types/react-dom": "^18.3.0", "@types/react-gtm-module": "^2.0.3", "@types/semver": "^7.3.10", "@typescript-eslint/eslint-plugin": "^7.6.0", - "@walletconnect/types": "^2.13.1", + "@walletconnect/types": "^2.16.1", "cross-env": "^7.0.3", "cypress": "^12.15.0", "cypress-file-upload": "^5.0.8", + "cypress-visual-regression": "^5.0.2", "eslint": "^8.57.0", "eslint-config-next": "^14.1.0", "eslint-config-prettier": "^8.5.0", @@ -140,6 +145,7 @@ "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-unused-imports": "^2.0.0", "fake-indexeddb": "^4.0.2", + "gray-matter": "^4.0.3", "husky": "^9.0.11", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", @@ -147,12 +153,16 @@ "postinstall-postinstall": "^2.1.0", "mockdate": "^3.0.5", "prettier": "^2.7.0", - "storybook": "^8.0.6", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "remark-heading-id": "^1.0.1", + "remark-mdx-frontmatter": "^5.0.0", + "storybook": "^8.3.4", "ts-prune": "^0.10.3", "typechain": "^8.3.2", "typescript": "^5.4.5", "typescript-plugin-css-modules": "^4.2.2", - "webpack": "^5.88.2" + "webpack": "^5.94.0" }, "nextBundleAnalysis": { "budget": null, diff --git a/patches/@safe-global+safe-modules-deployments+1.2.0.patch b/patches/@safe-global+safe-modules-deployments+2.2.1.patch similarity index 96% rename from patches/@safe-global+safe-modules-deployments+1.2.0.patch rename to patches/@safe-global+safe-modules-deployments+2.2.1.patch index ce7a8e398..758793ce1 100644 --- a/patches/@safe-global+safe-modules-deployments+1.2.0.patch +++ b/patches/@safe-global+safe-modules-deployments+2.2.1.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@safe-global/safe-modules-deployments/dist/assets/allowance-module/v0.1.0/allowance-module.json b/node_modules/@safe-global/safe-modules-deployments/dist/assets/allowance-module/v0.1.0/allowance-module.json -index 1324cfa..5c8a9ba 100644 +index 25a409d..4d53ae7 100644 --- a/node_modules/@safe-global/safe-modules-deployments/dist/assets/allowance-module/v0.1.0/allowance-module.json +++ b/node_modules/@safe-global/safe-modules-deployments/dist/assets/allowance-module/v0.1.0/allowance-module.json @@ -6,6 +6,8 @@ @@ -12,7 +12,7 @@ index 1324cfa..5c8a9ba 100644 "56": "0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134", "100": "0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134", diff --git a/node_modules/@safe-global/safe-modules-deployments/src/assets/allowance-module/v0.1.0/allowance-module.json b/node_modules/@safe-global/safe-modules-deployments/src/assets/allowance-module/v0.1.0/allowance-module.json -index b873a40..5e49d1c 100644 +index 70e4643..6fc893a 100644 --- a/node_modules/@safe-global/safe-modules-deployments/src/assets/allowance-module/v0.1.0/allowance-module.json +++ b/node_modules/@safe-global/safe-modules-deployments/src/assets/allowance-module/v0.1.0/allowance-module.json @@ -6,6 +6,8 @@ diff --git a/public/images/common/download-cloud.svg b/public/images/common/download-cloud.svg new file mode 100644 index 000000000..b6dd6403c --- /dev/null +++ b/public/images/common/download-cloud.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/images/common/multisend.svg b/public/images/common/multisend.svg new file mode 100644 index 000000000..17a4cd2b8 --- /dev/null +++ b/public/images/common/multisend.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/common/recovery_coincover.svg b/public/images/common/recovery_coincover.svg deleted file mode 100644 index 94303918f..000000000 --- a/public/images/common/recovery_coincover.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/public/images/common/recovery_sygnum.svg b/public/images/common/recovery_sygnum.svg index b13483a96..2644602e5 100644 --- a/public/images/common/recovery_sygnum.svg +++ b/public/images/common/recovery_sygnum.svg @@ -1,11 +1 @@ - - - - - - - - - - - + diff --git a/public/images/common/safe-pass-star.svg b/public/images/common/safe-pass-star.svg new file mode 100644 index 000000000..2cff53958 --- /dev/null +++ b/public/images/common/safe-pass-star.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/images/common/stake.svg b/public/images/common/stake.svg new file mode 100644 index 000000000..41469d1e8 --- /dev/null +++ b/public/images/common/stake.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/protofire-logo.svg b/public/images/protofire-logo.svg new file mode 100644 index 000000000..928d8dbaa --- /dev/null +++ b/public/images/protofire-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/sidebar/lightbulb_icon.svg b/public/images/sidebar/lightbulb_icon.svg new file mode 100644 index 000000000..717dd8c3e --- /dev/null +++ b/public/images/sidebar/lightbulb_icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/transactions/blockaid-icon.svg b/public/images/transactions/blockaid-icon.svg new file mode 100644 index 000000000..7c66a098f --- /dev/null +++ b/public/images/transactions/blockaid-icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/transactions/zodiac-roles.svg b/public/images/transactions/zodiac-roles.svg new file mode 100644 index 000000000..5eeb888df --- /dev/null +++ b/public/images/transactions/zodiac-roles.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/address-book/ImportDialog/index.tsx b/src/components/address-book/ImportDialog/index.tsx index d031893ed..9c5b047ab 100644 --- a/src/components/address-book/ImportDialog/index.tsx +++ b/src/components/address-book/ImportDialog/index.tsx @@ -114,7 +114,7 @@ const ImportDialog = ({ handleClose }: { handleClose: () => void }): ReactElemen }} > {/* https://github.com/Bunlong/react-papaparse/blob/master/src/useCSVReader.tsx */} - {({ getRootProps, acceptedFile, ProgressBar, getRemoveFileProps, Remove }: any) => { + {({ getRootProps, acceptedFile, getRemoveFileProps }: any) => { const { onClick } = getRemoveFileProps() const onRemove = (e: MouseEvent) => { diff --git a/src/components/balances/AssetsTable/index.tsx b/src/components/balances/AssetsTable/index.tsx index df1f069b5..077bfed57 100644 --- a/src/components/balances/AssetsTable/index.tsx +++ b/src/components/balances/AssetsTable/index.tsx @@ -1,6 +1,6 @@ import CheckBalance from '@/features/counterfactual/CheckBalance' import { type ReactElement } from 'react' -import { Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material' +import { Box, IconButton, Checkbox, Skeleton, SvgIcon, Tooltip, Typography } 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' @@ -21,6 +21,9 @@ import SwapButton from '@/features/swap/components/SwapButton' import { SWAP_LABELS } from '@/services/analytics/events/swaps' import SendButton from './SendButton' import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' +import useIsStakingFeatureEnabled from '@/features/stake/hooks/useIsSwapFeatureEnabled' +import { STAKE_LABELS } from '@/services/analytics/events/stake' +import StakeButton from '@/features/stake/components/StakeButton' const skeletonCells: EnhancedTableProps['rows'][0]['cells'] = { asset: { @@ -97,6 +100,7 @@ const AssetsTable = ({ }): ReactElement => { const { balances, loading } = useBalances() const isSwapFeatureEnabled = useIsSwapFeatureEnabled() + const isStakingFeatureEnabled = useIsStakingFeatureEnabled() const { isAssetSelected, toggleAsset, hidingAsset, hideAsset, cancel, deselectAll, saveChanges } = useHideAssets(() => setShowHiddenAssets(false), @@ -130,6 +134,10 @@ const AssetsTable = ({ {item.tokenInfo.name} + {isStakingFeatureEnabled && item.tokenInfo.type === TokenType.NATIVE_TOKEN && ( + + )} + {!isNative && } ), @@ -164,7 +172,7 @@ const AssetsTable = ({ inheritViewBox color="error" fontSize="small" - sx={{ verticalAlign: 'middle', marginLeft: 0.5 }} + sx={{ verticalAlign: 'middle', ml: 0.5, mr: [0, '-20px'] }} /> diff --git a/src/components/common/AddressBookInput/index.test.tsx b/src/components/common/AddressBookInput/index.test.tsx index 449e0eead..ee910e6dc 100644 --- a/src/components/common/AddressBookInput/index.test.tsx +++ b/src/components/common/AddressBookInput/index.test.tsx @@ -1,4 +1,5 @@ -import { act, fireEvent, render, waitFor } from '@/tests/test-utils' +import { act } from 'react' +import { fireEvent, render, waitFor } from '@/tests/test-utils' import { FormProvider, useForm } from 'react-hook-form' import AddressBookInput from '.' import type { AddressInputProps } from '../AddressInput' @@ -147,12 +148,12 @@ describe('AddressBookInput', () => { expect(input).toHaveAttribute('aria-expanded', 'false') - await act(() => { + act(() => { fireEvent.mouseDown(input) fireEvent.mouseUp(input) }) - await act(() => { + act(() => { fireEvent.change(input, { target: { value: invalidAddress } }) jest.advanceTimersByTime(1000) }) @@ -160,7 +161,8 @@ describe('AddressBookInput', () => { await waitFor(() => expect(utils.getByLabelText(validationError, { exact: false })).toBeDefined()) const address = checksumAddress(faker.finance.ethereumAddress()) - await act(() => { + + act(() => { fireEvent.change(input, { target: { value: address } }) jest.advanceTimersByTime(1000) }) @@ -187,14 +189,14 @@ describe('AddressBookInput', () => { expect(input).toHaveAttribute('aria-expanded', 'false') - await act(() => { + act(() => { fireEvent.mouseDown(input) fireEvent.mouseUp(input) }) expect(input).toHaveAttribute('aria-expanded', 'true') - await act(() => { + act(() => { fireEvent.click(utils.getByText('InvalidAddress')) fireEvent.blur(input) jest.advanceTimersByTime(1000) @@ -206,7 +208,7 @@ describe('AddressBookInput', () => { }) // Clear the input by clicking on the readonly input - await act(() => { + act(() => { // first click clears input fireEvent.click(utils.getByLabelText(validationError, { exact: false })) }) @@ -215,13 +217,13 @@ describe('AddressBookInput', () => { const newInput = utils.getByLabelText(validationError, { exact: false }) expect(newInput).toBeVisible() - await act(() => { + act(() => { // mousedown opens autocompletion again fireEvent.mouseDown(newInput) fireEvent.mouseUp(newInput) }) - await act(() => { + act(() => { fireEvent.click(utils.getByText('ValidAddress')) fireEvent.blur(newInput) @@ -239,7 +241,7 @@ describe('AddressBookInput', () => { const { input, utils } = setup('', {}, undefined, true) const newAddress = checksumAddress(faker.finance.ethereumAddress()) - await act(() => { + act(() => { fireEvent.change(input, { target: { value: newAddress } }) jest.advanceTimersByTime(1000) }) @@ -253,7 +255,7 @@ describe('AddressBookInput', () => { }) const nameInput = utils.getByLabelText('Name', { exact: false }) - await act(() => { + act(() => { fireEvent.change(nameInput, { target: { value: 'Tim Testermann' } }) fireEvent.submit(nameInput) }) @@ -265,7 +267,7 @@ describe('AddressBookInput', () => { const { input, utils } = setup('', {}, undefined, false) const newAddress = checksumAddress(faker.finance.ethereumAddress()) - await act(() => { + act(() => { fireEvent.change(input, { target: { value: newAddress } }) jest.advanceTimersByTime(1000) }) diff --git a/src/components/common/AddressBookInput/index.tsx b/src/components/common/AddressBookInput/index.tsx index c49de00ef..77be19a66 100644 --- a/src/components/common/AddressBookInput/index.tsx +++ b/src/components/common/AddressBookInput/index.tsx @@ -62,7 +62,8 @@ const AddressBookInput = ({ name, canAdd, ...props }: AddressInputProps & { canA ( + // eslint-disable-next-line + render={({ field: { ref, ...field } }) => ( )} /> diff --git a/src/components/common/BlockedAddress/index.tsx b/src/components/common/BlockedAddress/index.tsx index 4e2dc6d8e..3d271d422 100644 --- a/src/components/common/BlockedAddress/index.tsx +++ b/src/components/common/BlockedAddress/index.tsx @@ -5,7 +5,7 @@ import { useRouter } from 'next/router' import Disclaimer from '@/components/common/Disclaimer' import { AppRoutes } from '@/config/routes' -export const BlockedAddress = ({ address }: { address?: string }): ReactElement => { +export const BlockedAddress = ({ address, featureTitle }: { address: string; featureTitle: string }): ReactElement => { const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down('sm')) const displayAddress = address && isMobile ? shortenAddress(address) : address @@ -19,7 +19,7 @@ export const BlockedAddress = ({ address }: { address?: string }): ReactElement ) diff --git a/src/components/common/ChainIndicator/styles.module.css b/src/components/common/ChainIndicator/styles.module.css index c80681292..e1ed054b6 100644 --- a/src/components/common/ChainIndicator/styles.module.css +++ b/src/components/common/ChainIndicator/styles.module.css @@ -30,6 +30,12 @@ .indicator { min-width: 35px; } + .responsive { + min-width: 0; + } + .responsive .name { + display: none; + } } @container my-accounts-container (max-width: 500px) { diff --git a/src/components/common/ChainSwitcher/index.tsx b/src/components/common/ChainSwitcher/index.tsx index 8c70c4f4a..f0955d13c 100644 --- a/src/components/common/ChainSwitcher/index.tsx +++ b/src/components/common/ChainSwitcher/index.tsx @@ -1,30 +1,57 @@ import type { ReactElement } from 'react' -import { useCallback } from 'react' -import { Box, Button } from '@mui/material' +import { useCallback, useState } from 'react' +import { Button, CircularProgress, Typography } from '@mui/material' import { useCurrentChain } from '@/hooks/useChains' import useOnboard from '@/hooks/wallets/useOnboard' import useIsWrongChain from '@/hooks/useIsWrongChain' -import css from './styles.module.css' import { switchWalletChain } from '@/services/tx/tx-sender/sdk' -const ChainSwitcher = ({ fullWidth }: { fullWidth?: boolean }): ReactElement | null => { +const ChainSwitcher = ({ + fullWidth, + primaryCta = false, +}: { + fullWidth?: boolean + primaryCta?: boolean +}): ReactElement | null => { const chain = useCurrentChain() const onboard = useOnboard() const isWrongChain = useIsWrongChain() + const [loading, setIsLoading] = useState(false) const handleChainSwitch = useCallback(async () => { if (!onboard || !chain) return - + setIsLoading(true) await switchWalletChain(onboard, chain.chainId) + setIsLoading(false) }, [chain, onboard]) if (!isWrongChain) return null return ( - ) } diff --git a/src/components/common/CheckWallet/index.test.tsx b/src/components/common/CheckWallet/index.test.tsx index 7172f1e92..6d2806617 100644 --- a/src/components/common/CheckWallet/index.test.tsx +++ b/src/components/common/CheckWallet/index.test.tsx @@ -2,6 +2,7 @@ import { render } from '@/tests/test-utils' import CheckWallet from '.' import useIsOnlySpendingLimitBeneficiary from '@/hooks/useIsOnlySpendingLimitBeneficiary' import useIsSafeOwner from '@/hooks/useIsSafeOwner' +import useIsWrongChain from '@/hooks/useIsWrongChain' import useWallet from '@/hooks/wallets/useWallet' import { chainBuilder } from '@/tests/builders/chains' @@ -31,7 +32,14 @@ jest.mock('@/hooks/useChains', () => ({ useCurrentChain: jest.fn(() => chainBuilder().build()), })) -const renderButton = () => render({(isOk) => }) +// mock useIsWrongChain +jest.mock('@/hooks/useIsWrongChain', () => ({ + __esModule: true, + default: jest.fn(() => false), +})) + +const renderButton = () => + render({(isOk) => }) describe('CheckWallet', () => { beforeEach(() => { @@ -69,6 +77,18 @@ describe('CheckWallet', () => { ) }) + it('should be disabled when connected to the wrong network', () => { + ;(useIsWrongChain as jest.MockedFunction).mockReturnValue(true) + ;(useIsSafeOwner as jest.MockedFunction).mockReturnValueOnce(true) + + const renderButtonWithNetworkCheck = () => + render({(isOk) => }) + + const { container } = renderButtonWithNetworkCheck() + + expect(container.querySelector('button')).toBeDisabled() + }) + it('should not disable the button for non-owner spending limit benificiaries', () => { ;(useIsSafeOwner as jest.MockedFunction).mockReturnValueOnce(false) ;( diff --git a/src/components/common/CheckWallet/index.tsx b/src/components/common/CheckWallet/index.tsx index 60ad9fc52..b354fb8f5 100644 --- a/src/components/common/CheckWallet/index.tsx +++ b/src/components/common/CheckWallet/index.tsx @@ -1,37 +1,58 @@ +import { useIsWalletDelegate } from '@/hooks/useDelegates' import { type ReactElement } from 'react' -import { Tooltip } from '@mui/material' import useIsOnlySpendingLimitBeneficiary from '@/hooks/useIsOnlySpendingLimitBeneficiary' import useIsSafeOwner from '@/hooks/useIsSafeOwner' import useWallet from '@/hooks/wallets/useWallet' import useConnectWallet from '../ConnectWallet/useConnectWallet' +import useIsWrongChain from '@/hooks/useIsWrongChain' +import { Tooltip } from '@mui/material' +import useSafeInfo from '@/hooks/useSafeInfo' type CheckWalletProps = { children: (ok: boolean) => ReactElement allowSpendingLimit?: boolean allowNonOwner?: boolean noTooltip?: boolean + checkNetwork?: boolean } enum Message { WalletNotConnected = 'Please connect your wallet', NotSafeOwner = 'Your connected wallet is not a signer of this Safe Account', + CounterfactualMultisig = 'You need to activate the Safe before transacting', } -const CheckWallet = ({ children, allowSpendingLimit, allowNonOwner, noTooltip }: CheckWalletProps): ReactElement => { +const CheckWallet = ({ + children, + allowSpendingLimit, + allowNonOwner, + noTooltip, + checkNetwork = false, +}: CheckWalletProps): ReactElement => { const wallet = useWallet() const isSafeOwner = useIsSafeOwner() const isSpendingLimit = useIsOnlySpendingLimitBeneficiary() const connectWallet = useConnectWallet() + const isWrongChain = useIsWrongChain() + const isDelegate = useIsWalletDelegate() + + const { safe } = useSafeInfo() + + const isCounterfactualMultiSig = !allowNonOwner && !safe.deployed && safe.threshold > 1 const message = - wallet && (isSafeOwner || allowNonOwner || (isSpendingLimit && allowSpendingLimit)) + wallet && + (isSafeOwner || allowNonOwner || (isSpendingLimit && allowSpendingLimit) || isDelegate) && + !isCounterfactualMultiSig ? '' : !wallet ? Message.WalletNotConnected + : isCounterfactualMultiSig + ? Message.CounterfactualMultisig : Message.NotSafeOwner + if (checkNetwork && isWrongChain) return children(false) if (!message) return children(true) - if (noTooltip) return children(false) return ( diff --git a/src/components/common/Chip/index.tsx b/src/components/common/Chip/index.tsx index 81bb62368..f87237f24 100644 --- a/src/components/common/Chip/index.tsx +++ b/src/components/common/Chip/index.tsx @@ -1,8 +1,32 @@ -import { Chip as MuiChip } from '@mui/material' -import type { ChipProps } from '@mui/material' -import type { ReactElement } from 'react' -import React from 'react' +import { Typography, Chip as MuiChip, type ChipProps } from '@mui/material' -export function Chip(props: ChipProps): ReactElement { - return +type Props = { + label?: string + sx?: ChipProps['sx'] +} + +export function Chip({ sx, label = 'New' }: Props) { + return ( + + {label} + + } + /> + ) } diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx deleted file mode 100644 index 8bab108de..000000000 --- a/src/components/common/ConnectWallet/WalletDetails.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Box, Divider, SvgIcon, Typography } from '@mui/material' -import type { ReactElement } from 'react' - -import LockIcon from '@/public/images/common/lock.svg' - -import WalletLogin from '@/components/welcome/WelcomeLogin/WalletLogin' - -const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement => { - return ( - <> - - - - - - - - - - - or - - - - ) -} - -export default WalletDetails diff --git a/src/components/common/CookieAndTermBanner/index.tsx b/src/components/common/CookieAndTermBanner/index.tsx index be809a48b..5c80c3005 100644 --- a/src/components/common/CookieAndTermBanner/index.tsx +++ b/src/components/common/CookieAndTermBanner/index.tsx @@ -4,9 +4,15 @@ import type { CheckboxProps } from '@mui/material' import { Grid, Button, Checkbox, FormControlLabel, Typography, Paper, SvgIcon, Box } from '@mui/material' import WarningIcon from '@/public/images/notifications/warning.svg' import { useForm } from 'react-hook-form' +import { metadata } from '@/markdown/terms/terms.md' import { useAppDispatch, useAppSelector } from '@/store' -import { selectCookies, CookieAndTermType, saveCookieAndTermConsent } from '@/store/cookiesAndTermsSlice' +import { + selectCookies, + CookieAndTermType, + saveCookieAndTermConsent, + hasAcceptedTerms, +} from '@/store/cookiesAndTermsSlice' import { selectCookieBanner, openCookieBanner, closeCookieBanner } from '@/store/popupSlice' import css from './styles.module.css' @@ -52,7 +58,13 @@ export const CookieAndTermBanner = ({ }) const handleAccept = () => { - dispatch(saveCookieAndTermConsent(getValues())) + const values = getValues() + dispatch( + saveCookieAndTermConsent({ + ...values, + termsVersion: metadata.version, + }), + ) dispatch(closeCookieBanner()) } @@ -74,9 +86,11 @@ export const CookieAndTermBanner = ({ - By browsing this page, you accept our 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 + By browsing this page, you accept our{' '} + Terms & Conditions (last updated{' '} + {metadata.last_update_date}) and the use of necessary cookies. By clicking "Accept all" you + additionally agree to the use of Beamer and Analytics cookies as listed below.{' '} + Cookie policy @@ -135,11 +149,10 @@ export const CookieAndTermBanner = ({ const CookieBannerPopup = (): ReactElement | null => { const cookiePopup = useAppSelector(selectCookieBanner) - const cookies = useAppSelector(selectCookies) const dispatch = useAppDispatch() - // Open the banner if cookie preferences haven't been set - const shouldOpen = cookies[CookieAndTermType.NECESSARY] === undefined + const hasAccepted = useAppSelector(hasAcceptedTerms) + const shouldOpen = !hasAccepted useEffect(() => { if (shouldOpen) { @@ -155,5 +168,4 @@ const CookieBannerPopup = (): ReactElement | null => { ) : null } - export default CookieBannerPopup diff --git a/src/components/common/CopyTooltip/ConfirmCopyModal.tsx b/src/components/common/CopyTooltip/ConfirmCopyModal.tsx index b2c039898..d659346f0 100644 --- a/src/components/common/CopyTooltip/ConfirmCopyModal.tsx +++ b/src/components/common/CopyTooltip/ConfirmCopyModal.tsx @@ -16,6 +16,8 @@ import { type ReactElement, useEffect, type SyntheticEvent } from 'react' import { trackEvent, TX_LIST_EVENTS } from '@/services/analytics' import Track from '../Track' +import css from './styles.module.css' + export type ConfirmCopyModalProps = { open: boolean onClose: () => void @@ -47,11 +49,18 @@ const ConfirmCopyModal = ({ open, onClose, onCopy, children }: ConfirmCopyModalP {children} - - - + + + + + + + + ) diff --git a/src/components/common/CopyTooltip/styles.module.css b/src/components/common/CopyTooltip/styles.module.css new file mode 100644 index 000000000..6edc2a146 --- /dev/null +++ b/src/components/common/CopyTooltip/styles.module.css @@ -0,0 +1,15 @@ +.dialogActions { + display: flex; + flex-direction: row; + align-items: center; +} + +@media (max-width: 599.95px) { + .dialogActions { + flex-direction: column; + width: 100%; + } + .dialogActions > span { + width: 100%; + } +} diff --git a/src/components/common/DatePickerInput/index.tsx b/src/components/common/DatePickerInput/index.tsx index ed760e712..1d8ca6137 100644 --- a/src/components/common/DatePickerInput/index.tsx +++ b/src/components/common/DatePickerInput/index.tsx @@ -53,7 +53,7 @@ const DatePickerInput = ({ inputFormat="dd/MM/yyyy" {...field} disableFuture={disableFuture} - renderInput={({ label, error: _, ...params }) => ( + renderInput={({ label, ...params }) => ( )} PaperProps={{ diff --git a/src/components/common/DateTime/DateTime.tsx b/src/components/common/DateTime/DateTime.tsx index 6ce5776e2..b29017a6a 100644 --- a/src/components/common/DateTime/DateTime.tsx +++ b/src/components/common/DateTime/DateTime.tsx @@ -9,8 +9,10 @@ type DateTimeProps = { } export const DateTime = ({ value, showDateTime, showTime }: DateTimeProps): ReactElement => { + const showTooltip = !showDateTime || showTime + return ( - + {showTime ? formatTime(value) : showDateTime ? formatDateTime(value) : formatTimeInWords(value)} ) diff --git a/src/components/common/DateTime/index.test.tsx b/src/components/common/DateTime/index.test.tsx index a749c21d7..d0e07ad03 100644 --- a/src/components/common/DateTime/index.test.tsx +++ b/src/components/common/DateTime/index.test.tsx @@ -70,13 +70,11 @@ describe('DateTime', () => { date.setDate(date.getDate() - days) - const { queryByText } = render(, { + const { getByText } = render(, { routerProps: { pathname: '/transactions/history' }, }) - const expected = formatDateTime(date.getTime()) - - expect(queryByText('3 days ago')).toBeInTheDocument() + expect(getByText('3 days ago')).toBeInTheDocument() }) it('should render the full date and time after threshold on the filter', () => { diff --git a/src/components/common/EnhancedTable/index.tsx b/src/components/common/EnhancedTable/index.tsx index 0874f4beb..1be254118 100644 --- a/src/components/common/EnhancedTable/index.tsx +++ b/src/components/common/EnhancedTable/index.tsx @@ -88,6 +88,7 @@ function EnhancedTableHead(props: EnhancedTableHeadProps) { active={orderBy === headCell.id} direction={orderBy === headCell.id ? order : 'asc'} onClick={createSortHandler(headCell.id)} + sx={{ mr: [0, '-26px'] }} > {headCell.label} {orderBy === headCell.id ? ( diff --git a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx index d21b9a830..06701edc7 100644 --- a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx @@ -83,7 +83,7 @@ const SrcEthHashInfo = ({ )} - + {name && ( @@ -104,7 +104,12 @@ const SrcEthHashInfo = ({ {(!onlyName || !name) && ( {copyAddress ? ( - + {addressElement} ) : ( diff --git a/src/components/common/EthHashInfo/SrcEthHashInfo/styles.module.css b/src/components/common/EthHashInfo/SrcEthHashInfo/styles.module.css index 9dc2b92c1..1f755478a 100644 --- a/src/components/common/EthHashInfo/SrcEthHashInfo/styles.module.css +++ b/src/components/common/EthHashInfo/SrcEthHashInfo/styles.module.css @@ -19,7 +19,6 @@ .addressContainer { display: flex; align-items: center; - gap: 0.25em; white-space: nowrap; } diff --git a/src/components/common/EthHashInfo/index.test.tsx b/src/components/common/EthHashInfo/index.test.tsx index 0facb02d3..3ae9e9282 100644 --- a/src/components/common/EthHashInfo/index.test.tsx +++ b/src/components/common/EthHashInfo/index.test.tsx @@ -1,7 +1,8 @@ import { blo } from 'blo' +import { act } from 'react' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { act, fireEvent, render, waitFor } from '@/tests/test-utils' +import { fireEvent, render, waitFor } from '@/tests/test-utils' import * as useAllAddressBooks from '@/hooks/useAllAddressBooks' import * as useChainId from '@/hooks/useChainId' import * as store from '@/store' @@ -258,7 +259,7 @@ describe('EthHashInfo', () => { const button = container.querySelector('button') - await act(() => { + act(() => { fireEvent.click(button!) }) @@ -288,7 +289,7 @@ describe('EthHashInfo', () => { const button = container.querySelector('button') - await act(() => { + act(() => { fireEvent.click(button!) }) @@ -320,7 +321,7 @@ describe('EthHashInfo', () => { const button = container.querySelector('button') - await act(() => { + act(() => { fireEvent.click(button!) }) @@ -349,7 +350,7 @@ describe('EthHashInfo', () => { const button = container.querySelector('button') - await act(() => { + act(() => { fireEvent.click(button!) }) @@ -375,7 +376,7 @@ describe('EthHashInfo', () => { const button = container.querySelector('button') - await act(() => { + act(() => { fireEvent.click(button!) }) diff --git a/src/components/common/FiatValue/FiatValue.test.tsx b/src/components/common/FiatValue/FiatValue.test.tsx new file mode 100644 index 000000000..c4d6e8ce2 --- /dev/null +++ b/src/components/common/FiatValue/FiatValue.test.tsx @@ -0,0 +1,37 @@ +import { render } from '@/tests/test-utils' + +const normalizer = (text: string) => text.replace(/\u200A/g, ' ') + +describe('FiatValue', () => { + beforeEach(() => { + Object.defineProperty(window, 'navigator', { + value: { + language: 'en-US', + }, + writable: true, + }) + }) + + it('should render fiat value', () => { + const FiatValue = require('.').default + const { getByText } = render() + const span = getByText((content) => normalizer(content) === '$ 100', { normalizer }) + expect(span).toBeInTheDocument() + expect(span).toHaveAttribute('aria-label', '$โ€Š100.00') + }) + + it('should render a big fiat value', () => { + const FiatValue = require('.').default + const { getByText } = render() + const span = getByText((content) => normalizer(content) === '$ 100.29M', { normalizer }) + expect(span).toBeInTheDocument() + expect(span).toHaveAttribute('aria-label', '$โ€Š100,285,367.00') + }) + + it('should render fiat value with precise=true', () => { + const FiatValue = require('.').default + const { getByText } = render() + expect(getByText((content) => normalizer(content) === '$ 100', { normalizer })).toBeInTheDocument() + expect(getByText('.35')).toBeInTheDocument() + }) +}) diff --git a/src/components/common/FiatValue/index.tsx b/src/components/common/FiatValue/index.tsx index 9cc09f1ea..d934a7b28 100644 --- a/src/components/common/FiatValue/index.tsx +++ b/src/components/common/FiatValue/index.tsx @@ -1,22 +1,54 @@ import type { CSSProperties, ReactElement } from 'react' import { useMemo } from 'react' +import { Tooltip, Typography } from '@mui/material' import { useAppSelector } from '@/store' import { selectCurrency } from '@/store/settingsSlice' -import { formatCurrency } from '@/utils/formatNumber' +import { formatCurrency, formatCurrencyPrecise } from '@/utils/formatNumber' const style = { whiteSpace: 'nowrap' } as CSSProperties -const FiatValue = ({ value, maxLength }: { value: string | number; maxLength?: number }): ReactElement => { +const FiatValue = ({ + value, + maxLength, + precise, +}: { + value: string | number + maxLength?: number + precise?: boolean +}): ReactElement => { const currency = useAppSelector(selectCurrency) const fiat = useMemo(() => { return formatCurrency(value, currency, maxLength) }, [value, currency, maxLength]) + const preciseFiat = useMemo(() => { + return formatCurrencyPrecise(value, currency) + }, [value, currency]) + + const [whole, decimals, endCurrency] = useMemo(() => { + const match = preciseFiat.match(/(.+)(\D\d+)(\D+)?$/) + return match ? match.slice(1) : ['', preciseFiat, '', ''] + }, [preciseFiat]) + return ( - - {fiat} - + + + {precise ? ( + <> + {whole} + {decimals && ( + + {decimals} + + )} + {endCurrency} + + ) : ( + fiat + )} + + ) } diff --git a/src/components/common/Footer/index.tsx b/src/components/common/Footer/index.tsx index 2692e42e0..f37691d65 100644 --- a/src/components/common/Footer/index.tsx +++ b/src/components/common/Footer/index.tsx @@ -10,6 +10,8 @@ import packageJson from '../../../../package.json' import ExternalLink from '../ExternalLink' import MUILink from '@mui/material/Link' import { HELP_CENTER_URL, IS_DEV, IS_OFFICIAL_HOST } from '@/config/constants' +import darkPalette from '@/components/theme/darkPalette' +import ProtofireLogo from '@/public/images/protofire-logo.svg' const footerPages = [ AppRoutes.welcome.index, @@ -75,9 +77,22 @@ const Footer = (): ReactElement | null => { ) : ( -
  • - Preferences -
  • + <> +
  • + Terms +
  • +
  • + Cookie policy +
  • +
  • + Preferences +
  • +
  • + + Help + +
  • + )}
  • @@ -85,9 +100,20 @@ const Footer = (): ReactElement | null => { v{packageJson.version}
  • - {/*
  • - -
  • */} +
  • + + Supported by{' '} + + + Protofire + + +
  • ) diff --git a/src/components/common/GeoblockingProvider/index.tsx b/src/components/common/GeoblockingProvider/index.tsx index b6451de00..47e660508 100644 --- a/src/components/common/GeoblockingProvider/index.tsx +++ b/src/components/common/GeoblockingProvider/index.tsx @@ -1,27 +1,19 @@ import { AppRoutes } from '@/config/routes' -import { createContext, type ReactElement, type ReactNode, useEffect, useState } from 'react' +import useAsync from '@/hooks/useAsync' +import { createContext, type ReactElement, type ReactNode } from 'react' export const GeoblockingContext = createContext(null) +const checkBlocked = async () => { + const res = await fetch(AppRoutes.swap, { method: 'HEAD' }) + return res.status === 403 +} + /** * 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() - }, []) + const [isBlockedCountry = null] = useAsync(checkBlocked, []) return {children} } diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index e08289543..cfa880fd6 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -8,11 +8,11 @@ import classnames from 'classnames' import css from './styles.module.css' import ConnectWallet from '@/components/common/ConnectWallet' import NetworkSelector from '@/components/common/NetworkSelector' -import SafeTokenWidget, { getSafeTokenAddress } from '@/components/common/SafeTokenWidget' +import SafeTokenWidget from '@/components/common/SafeTokenWidget' import NotificationCenter from '@/components/notification-center/NotificationCenter' import { AppRoutes } from '@/config/routes' -import useChainId from '@/hooks/useChainId' import SafeLogo from '@/public/images/logo.svg' +import SafeLogoMobile from '@/public/images/logo-no-text.svg' import Link from 'next/link' import useSafeAddress from '@/hooks/useSafeAddress' import BatchIndicator from '@/components/batch/BatchIndicator' @@ -21,6 +21,7 @@ import { FEATURES } from '@/utils/chains' import { useHasFeature } from '@/hooks/useChains' import Track from '@/components/common/Track' import { OVERVIEW_EVENTS, OVERVIEW_LABELS } from '@/services/analytics' +import { useSafeTokenEnabled } from '@/hooks/useSafeTokenEnabled' type HeaderProps = { onMenuToggle?: Dispatch> @@ -36,9 +37,8 @@ function getLogoLink(router: ReturnType): Url { } const Header = ({ onMenuToggle, onBatchToggle }: HeaderProps): ReactElement => { - const chainId = useChainId() const safeAddress = useSafeAddress() - const showSafeToken = safeAddress && !!getSafeTokenAddress(chainId) + const showSafeToken = useSafeTokenEnabled() const router = useRouter() const enableWc = useHasFeature(FEATURES.NATIVE_WALLETCONNECT) @@ -61,10 +61,18 @@ const Header = ({ onMenuToggle, onBatchToggle }: HeaderProps): ReactElement => { return ( -
    - - - +
    + {onMenuToggle && ( + + + + )} +
    + +
    + + +
    diff --git a/src/components/common/Header/styles.module.css b/src/components/common/Header/styles.module.css index 3051c3ba1..e2a572a9f 100644 --- a/src/components/common/Header/styles.module.css +++ b/src/components/common/Header/styles.module.css @@ -36,7 +36,12 @@ align-items: flex-start; } -.logo svg { +.logoMobile { + display: none; +} + +.logo svg, +.logoMobile svg { width: auto; display: block; color: var(--color-logo-main); @@ -64,8 +69,17 @@ display: none; } + .logoMobile { + display: flex; + flex: 1; + border: none; + align-items: flex-start; + margin-left: var(--space-2); + } + .menuButton { display: flex; + flex: 0; } } @@ -73,8 +87,4 @@ .hideMobile { display: none; } - - .hideSidebarMobile { - visibility: hidden; - } } diff --git a/src/components/common/Identicon/index.tsx b/src/components/common/Identicon/index.tsx index 6b3d4d762..fb78a42e7 100644 --- a/src/components/common/Identicon/index.tsx +++ b/src/components/common/Identicon/index.tsx @@ -4,6 +4,7 @@ import { blo } from 'blo' import Skeleton from '@mui/material/Skeleton' import css from './styles.module.css' +import { isAddress } from 'ethers' export interface IdenticonProps { address: string @@ -13,6 +14,9 @@ export interface IdenticonProps { const Identicon = ({ address, size = 40 }: IdenticonProps): ReactElement => { const style = useMemo(() => { try { + if (!isAddress(address)) { + return null + } const blockie = blo(address as `0x${string}`) return { backgroundImage: `url(${blockie})`, diff --git a/src/components/common/ModalDialog/styles.module.css b/src/components/common/ModalDialog/styles.module.css index 526648940..dc1b74134 100644 --- a/src/components/common/ModalDialog/styles.module.css +++ b/src/components/common/ModalDialog/styles.module.css @@ -1,5 +1,5 @@ .dialog :global .MuiDialogActions-root { - border-top: 2px solid var(--color-border-light); + border-top: 1px solid var(--color-border-light); padding: var(--space-3); } @@ -14,7 +14,7 @@ } .dialog :global .MuiDialogTitle-root { - border-bottom: 2px solid var(--color-border-light); + border-bottom: 1px solid var(--color-border-light); } @media (min-width: 600px) { diff --git a/src/components/common/NameInput/index.tsx b/src/components/common/NameInput/index.tsx index 09289fc8a..ac3a1ed27 100644 --- a/src/components/common/NameInput/index.tsx +++ b/src/components/common/NameInput/index.tsx @@ -1,17 +1,15 @@ import type { TextFieldProps } from '@mui/material' import { TextField } from '@mui/material' import get from 'lodash/get' -import { type FieldError, type Validate, useFormContext } from 'react-hook-form' +import { type FieldError, useFormContext } from 'react-hook-form' import inputCss from '@/styles/inputs.module.css' const NameInput = ({ name, - validate, required = false, ...props }: Omit & { name: string - validate?: Validate required?: boolean }) => { const { register, formState } = useFormContext() || {} diff --git a/src/components/common/NamedAddressInfo/index.tsx b/src/components/common/NamedAddressInfo/index.tsx index 7ebd1852e..575f8fa78 100644 --- a/src/components/common/NamedAddressInfo/index.tsx +++ b/src/components/common/NamedAddressInfo/index.tsx @@ -11,14 +11,10 @@ const NamedAddressInfo = ({ address, name, customAvatar, ...props }: EthHashInfo [address, chainId, name, customAvatar], ) - return ( - - ) + const finalName = name || contract?.displayName || contract?.name + const finalAvatar = customAvatar || contract?.logoUri + + return } export default NamedAddressInfo diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index 296dabd33..22ea17309 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -1,7 +1,6 @@ import ChainIndicator from '@/components/common/ChainIndicator' import { useDarkMode } from '@/hooks/useDarkMode' import { useTheme } from '@mui/material/styles' -import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import Link from 'next/link' import type { SelectChangeEvent } from '@mui/material' import { ListSubheader, MenuItem, Select, Skeleton } from '@mui/material' @@ -16,6 +15,8 @@ import { useCallback } from 'react' import { AppRoutes } from '@/config/routes' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' import useWallet from '@/hooks/wallets/useWallet' +import { useAppSelector } from '@/store' +import { selectChains } from '@/store/chainsSlice' const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => { const isDarkMode = useDarkMode() @@ -24,8 +25,8 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => const chainId = useChainId() const router = useRouter() const isWalletConnected = !!useWallet() - const [testNets, prodNets] = useMemo(() => partition(configs, (config) => config.isTestnet), [configs]) + const chains = useAppSelector(selectChains) const getNetworkLink = useCallback( (shortName: string) => { @@ -67,16 +68,18 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => } const renderMenuItem = useCallback( - (value: string, chain: ChainInfo) => { + (chainId: string, isSelected: boolean) => { + const chain = chains.data.find((chain) => chain.chainId === chainId) + if (!chain) return null return ( - + - + ) }, - [getNetworkLink, props.onChainSelect], + [chains.data, getNetworkLink, props.onChainSelect], ) return configs.length ? ( @@ -87,6 +90,7 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => className={css.select} variant="standard" IconComponent={ExpandMoreIcon} + renderValue={(value) => renderMenuItem(value, true)} MenuProps={{ transitionDuration: 0, sx: { @@ -108,11 +112,11 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => }, }} > - {prodNets.map((chain) => renderMenuItem(chain.chainId, chain))} + {prodNets.map((chain) => renderMenuItem(chain.chainId, false))} Testnets - {testNets.map((chain) => renderMenuItem(chain.chainId, chain))} + {testNets.map((chain) => renderMenuItem(chain.chainId, false))} ) : ( diff --git a/src/components/common/NetworkSelector/styles.module.css b/src/components/common/NetworkSelector/styles.module.css index a2ab95a39..1703446ac 100644 --- a/src/components/common/NetworkSelector/styles.module.css +++ b/src/components/common/NetworkSelector/styles.module.css @@ -28,9 +28,8 @@ pointer-events: none; } -.menuItem { - padding-top: var(--space-1); - padding-bottom: var(--space-1); +.select :global .MuiMenuItem-root { + padding: 0; } .listSubHeader { diff --git a/src/components/common/OnboardingTooltip/index.tsx b/src/components/common/OnboardingTooltip/index.tsx index 7a08e26f7..c5868791d 100644 --- a/src/components/common/OnboardingTooltip/index.tsx +++ b/src/components/common/OnboardingTooltip/index.tsx @@ -33,6 +33,7 @@ export const OnboardingTooltip = ({ - {text} +
    {text}
    - - )} diff --git a/src/components/common/Table/DataRow.tsx b/src/components/common/Table/DataRow.tsx index be49473c6..67a8886d4 100644 --- a/src/components/common/Table/DataRow.tsx +++ b/src/components/common/Table/DataRow.tsx @@ -1,6 +1,5 @@ import type { ReactElement, ReactNode } from 'react' -import { Typography } from '@mui/material' -import css from './styles.module.css' +import FieldsGrid from '@/components/tx/FieldsGrid' type DataRowProps = { datatestid?: String @@ -10,19 +9,10 @@ type DataRowProps = { export const DataRow = ({ datatestid, title, children }: DataRowProps): ReactElement | null => { if (children == undefined) return null - return ( -
    -
    - {title} -
    - {typeof children === 'string' ? ( - - {children} - - ) : ( - children - )} -
    + return ( + + {children} + ) } diff --git a/src/components/common/Table/styles.module.css b/src/components/common/Table/styles.module.css index ccc68717d..33e2c2fd9 100644 --- a/src/components/common/Table/styles.module.css +++ b/src/components/common/Table/styles.module.css @@ -4,6 +4,12 @@ gap: var(--space-1); justify-content: flex-start; max-width: 900px; + overflow-x: auto; + margin-top: 4px; +} + +.gridRow:first-of-type { + margin-bottom: 0; } .gridEmptyRow { @@ -16,12 +22,17 @@ margin-bottom: var(--space-1); border-top: 1px solid var(--color-border-light); } + .title { color: var(--color-primary-light); font-weight: 400; word-break: break-all; } +.title span:nth-child(2) { + word-break: normal; +} + .gridRow > * { flex-shrink: 0; } diff --git a/src/components/common/TokenAmount/index.tsx b/src/components/common/TokenAmount/index.tsx index 0e8f87440..ed818ec7b 100644 --- a/src/components/common/TokenAmount/index.tsx +++ b/src/components/common/TokenAmount/index.tsx @@ -1,4 +1,5 @@ import { type ReactElement } from 'react' +import { Tooltip } from '@mui/material' import { TransferDirection } from '@safe-global/safe-gateway-typescript-sdk' import css from './styles.module.css' import { formatVisualAmount } from '@/utils/formatters' @@ -27,15 +28,19 @@ const TokenAmount = ({ const sign = direction === TransferDirection.OUTGOING ? '-' : '' const amount = decimals !== undefined ? formatVisualAmount(value, decimals, preciseAmount ? PRECISION : undefined) : value + const fullAmount = + decimals !== undefined ? sign + formatVisualAmount(value, decimals, PRECISION) + ' ' + tokenSymbol : value return ( - - {logoUri && } - - {sign} - {amount} {tokenSymbol} - - + + + {logoUri && } + + {sign} + {amount} {tokenSymbol} + + + ) } diff --git a/src/components/common/TxModalDialog/index.tsx b/src/components/common/TxModalDialog/index.tsx index 904c15175..62f597867 100644 --- a/src/components/common/TxModalDialog/index.tsx +++ b/src/components/common/TxModalDialog/index.tsx @@ -1,23 +1,16 @@ -import { type ReactElement } from 'react' -import { IconButton, Dialog, DialogTitle, type DialogProps } from '@mui/material' +import { Dialog, DialogContent, DialogContentText, DialogTitle, IconButton, type DialogProps } from '@mui/material' import classnames from 'classnames' +import type { ReactElement } from 'react' import CloseIcon from '@mui/icons-material/Close' import css from './styles.module.css' -interface ModalDialogProps extends DialogProps { - dialogTitle?: React.ReactNode - hideChainIndicator?: boolean -} - const TxModalDialog = ({ - dialogTitle, - hideChainIndicator, children, onClose, fullScreen = false, fullWidth = false, ...restProps -}: ModalDialogProps): ReactElement => { +}: DialogProps): ReactElement => { return (
    - - {children} + + + {children} + + ) } diff --git a/src/components/common/WalletOverview/styles.module.css b/src/components/common/WalletOverview/styles.module.css index 14dd5f668..46f6e86b0 100644 --- a/src/components/common/WalletOverview/styles.module.css +++ b/src/components/common/WalletOverview/styles.module.css @@ -37,12 +37,14 @@ font-size: 12px; } - .walletDetails { - display: none; - } - .imageContainer img { width: 22px; height: auto; } } + +@media (max-width: 899.95px) { + .walletDetails { + display: none; + } +} diff --git a/src/components/common/WalletProvider/index.tsx b/src/components/common/WalletProvider/index.tsx index f1c360c68..6e14266d1 100644 --- a/src/components/common/WalletProvider/index.tsx +++ b/src/components/common/WalletProvider/index.tsx @@ -13,6 +13,7 @@ const WalletProvider = ({ children }: { children: ReactNode }): ReactElement => const walletSubscription = onboard.state.select('wallets').subscribe((wallets) => { const newWallet = getConnectedWallet(wallets) + setWallet(newWallet) }) diff --git a/src/features/swap/components/LegalDisclaimer/index.tsx b/src/components/common/WidgetDisclaimer/index.tsx similarity index 77% rename from src/features/swap/components/LegalDisclaimer/index.tsx rename to src/components/common/WidgetDisclaimer/index.tsx index a324af688..3c5969d78 100644 --- a/src/features/swap/components/LegalDisclaimer/index.tsx +++ b/src/components/common/WidgetDisclaimer/index.tsx @@ -4,7 +4,11 @@ import { Typography } from '@mui/material' import css from './styles.module.css' -const LegalDisclaimerContent = () => ( +const linkSx = { + textDecoration: 'none', +} + +const WidgetDisclaimer = ({ widgetName }: { widgetName: string }) => (
    @@ -12,21 +16,21 @@ const LegalDisclaimerContent = () => ( - Please note that we do not own, control, maintain or audit the CoW Swap Widget. Use of the widget is subject to + Please note that we do not own, control, maintain or audit the {widgetName}. 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. @@ -35,4 +39,4 @@ const LegalDisclaimerContent = () => (
    ) -export default LegalDisclaimerContent +export default WidgetDisclaimer diff --git a/src/features/swap/components/LegalDisclaimer/styles.module.css b/src/components/common/WidgetDisclaimer/styles.module.css similarity index 100% rename from src/features/swap/components/LegalDisclaimer/styles.module.css rename to src/components/common/WidgetDisclaimer/styles.module.css diff --git a/src/components/dashboard/ActivityRewardsSection/index.tsx b/src/components/dashboard/ActivityRewardsSection/index.tsx index 6d4d31ca5..0dd0035ed 100644 --- a/src/components/dashboard/ActivityRewardsSection/index.tsx +++ b/src/components/dashboard/ActivityRewardsSection/index.tsx @@ -14,6 +14,8 @@ import NextLink from 'next/link' import { OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import ExternalLink from '@/components/common/ExternalLink' const Step = ({ active, title }: { active: boolean; title: ReactNode }) => { return ( @@ -34,15 +36,19 @@ const Step = ({ active, title }: { active: boolean; title: ReactNode }) => { ) } +const LOCAL_STORAGE_KEY_HIDE_WIDGET = 'hideActivityRewardsBanner' + const ActivityRewardsSection = () => { const [matchingApps] = useRemoteSafeApps(SafeAppsTag.SAFE_GOVERNANCE_APP) const isDarkMode = useDarkMode() const router = useRouter() + const [widgetHidden = false, setWidgetHidden] = useLocalStorage(LOCAL_STORAGE_KEY_HIDE_WIDGET) + const isSAPBannerEnabled = useHasFeature(FEATURES.SAP_BANNER) const governanceApp = matchingApps?.[0] - if (!governanceApp || !governanceApp?.url || !isSAPBannerEnabled) return null + if (!governanceApp || !governanceApp?.url || !isSAPBannerEnabled || widgetHidden) return null const appUrl = getSafeAppUrl(router, governanceApp?.url) @@ -50,6 +56,15 @@ const ActivityRewardsSection = () => { trackEvent(OVERVIEW_EVENTS.OPEN_ACTIVITY_APP) } + const onHide = () => { + setWidgetHidden(true) + trackEvent(OVERVIEW_EVENTS.HIDE_ACTIVITY_APP_WIDGET) + } + + const onLearnMore = () => { + trackEvent(OVERVIEW_EVENTS.OPEN_LEARN_MORE_ACTIVITY_APP) + } + return ( <> @@ -82,14 +97,6 @@ const ActivityRewardsSection = () => { > Interact with Safe and get rewards - - - - - - - - @@ -100,6 +107,22 @@ const ActivityRewardsSection = () => {
    + + Learn more + + + + + + + + + + diff --git a/src/components/dashboard/ActivityRewardsSection/styles.module.css b/src/components/dashboard/ActivityRewardsSection/styles.module.css index 92c6ac163..eda8e4a38 100644 --- a/src/components/dashboard/ActivityRewardsSection/styles.module.css +++ b/src/components/dashboard/ActivityRewardsSection/styles.module.css @@ -67,16 +67,24 @@ .links { display: flex; - flex-wrap: nowrap; + flex-wrap: wrap; align-items: center; margin-top: var(--space-3); text-wrap: nowrap; + width: 100%; } @media (max-width: 899.99px) { .header { padding: 0; } + .links { + flex-direction: column; + } + + .links a { + width: 100%; + } .widgetWrapper { padding: 32px; diff --git a/src/components/dashboard/Assets/index.tsx b/src/components/dashboard/Assets/index.tsx index 8df89c164..12aca6161 100644 --- a/src/components/dashboard/Assets/index.tsx +++ b/src/components/dashboard/Assets/index.tsx @@ -43,7 +43,7 @@ const NoAssets = () => ( ) -const AssetRow = ({ item, showSwap }: { item: SafeBalanceResponse['items'][number]; showSwap: boolean }) => ( +const AssetRow = ({ item, showSwap }: { item: SafeBalanceResponse['items'][number]; showSwap?: boolean }) => ( ( - - - - - - - - - {description} - - - - Use {name} - - - - -) - -const onWcWidgetClick = (e: SyntheticEvent) => { - e.preventDefault() - openWalletConnect() -} - -export const FeaturedApps = ({ stackedLayout }: { stackedLayout: boolean }): ReactElement | null => { - const txBuilder = useTxBuilderApp() - const isWcEnabled = useHasFeature(FEATURES.NATIVE_WALLETCONNECT) - - return ( - - - - Connect & transact - - - - {txBuilder?.app && ( - - - - - - )} - {isWcEnabled && ( - - - - - - )} - - - - - ) -} diff --git a/src/components/dashboard/FirstSteps/index.tsx b/src/components/dashboard/FirstSteps/index.tsx index 82eb34a5e..cf4dde16b 100644 --- a/src/components/dashboard/FirstSteps/index.tsx +++ b/src/components/dashboard/FirstSteps/index.tsx @@ -24,6 +24,7 @@ import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded' import CheckCircleOutlineRoundedIcon from '@mui/icons-material/CheckCircleOutlineRounded' import LightbulbOutlinedIcon from '@mui/icons-material/LightbulbOutlined' import css from './styles.module.css' +import ActivateAccountButton from '@/features/counterfactual/ActivateAccountButton' const calculateProgress = (items: boolean[]) => { const totalNumberOfItems = items.length @@ -133,22 +134,17 @@ const AddFundsWidget = ({ completed }: { completed: boolean }) => { {!completed && ( <> - - {(isOk) => ( - - - - )} - + + + { ) } +const ActivateSafeWidget = () => { + const [open, setOpen] = useState(false) + + const title = 'Activate your Safe account.' + + return ( + <> + + Activate your Safe + + } + title={title} + completed={false} + content="" + > + + + setOpen(false)} /> + + ) +} + const AccountReadyWidget = () => { return ( @@ -283,6 +303,8 @@ const FirstSteps = () => { const chain = useCurrentChain() const undeployedSafe = useAppSelector((state) => selectUndeployedSafe(state, safe.chainId, safeAddress)) + const isMultiSig = safe.threshold > 1 + const hasNonZeroBalance = balances && (balances.items.length > 1 || BigInt(balances.items[0]?.balance || 0) > 0) const hasOutgoingTransactions = !!outgoingTransactions && outgoingTransactions.length > 0 const completedItems = [hasNonZeroBalance, hasOutgoingTransactions] @@ -359,7 +381,13 @@ const FirstSteps = () => { - {isActivating ? : } + {isActivating ? ( + + ) : isMultiSig ? ( + + ) : ( + + )} diff --git a/src/components/dashboard/Overview/Overview.tsx b/src/components/dashboard/Overview/Overview.tsx index f0b069ea4..79f956849 100644 --- a/src/components/dashboard/Overview/Overview.tsx +++ b/src/components/dashboard/Overview/Overview.tsx @@ -72,7 +72,7 @@ const Overview = (): ReactElement => { {safe.deployed ? ( - + ) : ( { const router = useRouter() const { id } = transaction - const { safe } = useSafeInfo() const url = useMemo( () => ({ @@ -36,17 +34,21 @@ const PendingTx = ({ transaction }: PendingTxType): ReactElement => { return ( - {isMultisigExecutionInfo(transaction.executionInfo) && transaction.executionInfo.nonce} + + + {isMultisigExecutionInfo(transaction.executionInfo) && transaction.executionInfo.nonce} + - - - - - + + + - + + + + - + {isMultisigExecutionInfo(transaction.executionInfo) && ( ({ ...jest.requireActual('@safe-global/safe-gateway-typescript-sdk'), - getSafeApps: (chainId: string): Promise => + getSafeApps: (): Promise => Promise.resolve([ { id: 13, diff --git a/src/components/dashboard/index.tsx b/src/components/dashboard/index.tsx index fa480c931..300998424 100644 --- a/src/components/dashboard/index.tsx +++ b/src/components/dashboard/index.tsx @@ -1,31 +1,31 @@ import FirstSteps from '@/components/dashboard/FirstSteps' import useSafeInfo from '@/hooks/useSafeInfo' -import type { ReactElement } from 'react' +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' import GovernanceSection from '@/components/dashboard/GovernanceSection/GovernanceSection' -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' import css from './styles.module.css' import SwapWidget from '@/features/swap/components/SwapWidget' +import useIsSwapFeatureEnabled from '@/features/swap/hooks/useIsSwapFeatureEnabled' +import { useSafeTokenEnabled } from '@/hooks/useSafeTokenEnabled' const RecoveryHeader = dynamic(() => import('@/features/recovery/components/RecoveryHeader')) const Dashboard = (): ReactElement => { const { safe } = useSafeInfo() const showSafeApps = useHasFeature(FEATURES.SAFE_APPS) - const isSAPBannerEnabled = useHasFeature(FEATURES.SAP_BANNER) + const isSafeTokenEnabled = useSafeTokenEnabled() + const isSwapFeatureEnabled = useIsSwapFeatureEnabled() + const isSAPBannerEnabled = useHasFeature(FEATURES.SAP_BANNER) && isSafeTokenEnabled const supportsRecovery = useIsRecoverySupported() - const [recovery] = useRecovery() - const showRecoveryWidget = supportsRecovery && !recovery return ( <> @@ -42,13 +42,17 @@ const Dashboard = (): ReactElement => { {safe.deployed && ( <> - - - + {isSwapFeatureEnabled && ( + + + + )} - - - + {isSAPBannerEnabled && ( + + + + )} @@ -60,12 +64,6 @@ const Dashboard = (): ReactElement => { - {showSafeApps && ( - - - - )} - {showSafeApps && ( diff --git a/src/components/new-safe/create/AdvancedCreateSafe.tsx b/src/components/new-safe/create/AdvancedCreateSafe.tsx new file mode 100644 index 000000000..3c434e3bf --- /dev/null +++ b/src/components/new-safe/create/AdvancedCreateSafe.tsx @@ -0,0 +1,127 @@ +import { Container, Typography, Grid } from '@mui/material' +import { useRouter } from 'next/router' + +import useWallet from '@/hooks/wallets/useWallet' +import OverviewWidget from '@/components/new-safe/create/OverviewWidget' +import type { TxStepperProps } from '@/components/new-safe/CardStepper/useCardStepper' +import SetNameStep from '@/components/new-safe/create/steps/SetNameStep' +import OwnerPolicyStep from '@/components/new-safe/create/steps/OwnerPolicyStep' +import ReviewStep from '@/components/new-safe/create/steps/ReviewStep' +import { CreateSafeStatus } from '@/components/new-safe/create/steps/StatusStep' +import { CardStepper } from '@/components/new-safe/CardStepper' +import { AppRoutes } from '@/config/routes' +import { CREATE_SAFE_CATEGORY } from '@/services/analytics' +import type { CreateSafeInfoItem } from '@/components/new-safe/create/CreateSafeInfos' +import CreateSafeInfos from '@/components/new-safe/create/CreateSafeInfos' +import { useState } from 'react' +import { type NewSafeFormData } from '.' +import AdvancedOptionsStep from './steps/AdvancedOptionsStep' +import { getLatestSafeVersion } from '@/utils/chains' +import { useCurrentChain } from '@/hooks/useChains' + +const AdvancedCreateSafe = () => { + const router = useRouter() + const wallet = useWallet() + const chain = useCurrentChain() + + const [safeName, setSafeName] = useState('') + const [dynamicHint, setDynamicHint] = useState() + const [activeStep, setActiveStep] = useState(0) + + const CreateSafeSteps: TxStepperProps['steps'] = [ + { + title: 'Select network and name of your Safe Account', + subtitle: 'Select the network on which to create your Safe Account', + render: (data, onSubmit, onBack, setStep) => ( + + ), + }, + { + title: 'Signers and confirmations', + subtitle: + 'Set the signer wallets of your Safe Account and how many need to confirm to execute a valid transaction.', + render: (data, onSubmit, onBack, setStep) => ( + + ), + }, + { + title: 'Advanced settings', + subtitle: 'Choose the Safe version and optionally a specific salt nonce', + render: (data, onSubmit, onBack, setStep) => ( + + ), + }, + { + title: 'Review', + subtitle: + "You're about to create a new Safe Account and will have to confirm the transaction with your connected wallet.", + render: (data, onSubmit, onBack, setStep) => ( + + ), + }, + { + title: '', + subtitle: '', + render: (data, onSubmit, onBack, setStep, setProgressColor, setStepData) => ( + + ), + }, + ] + + const initialStep = 0 + const initialData: NewSafeFormData = { + name: '', + owners: [], + threshold: 1, + saltNonce: 0, + safeVersion: getLatestSafeVersion(chain), + } + + const onClose = () => { + router.push(AppRoutes.welcome.index) + } + + return ( + + + + + Create new Safe Account + + + + + + + + + {activeStep < 2 && } + {wallet?.address && } + + + + + ) +} + +export default AdvancedCreateSafe diff --git a/src/components/new-safe/create/NetworkWarning/index.tsx b/src/components/new-safe/create/NetworkWarning/index.tsx index 1a7aa0156..d0a87b5c7 100644 --- a/src/components/new-safe/create/NetworkWarning/index.tsx +++ b/src/components/new-safe/create/NetworkWarning/index.tsx @@ -1,17 +1,19 @@ import { Alert, AlertTitle, Box } from '@mui/material' import { useCurrentChain } from '@/hooks/useChains' import ChainSwitcher from '@/components/common/ChainSwitcher' +import useIsWrongChain from '@/hooks/useIsWrongChain' -const NetworkWarning = () => { +const NetworkWarning = ({ action }: { action?: string }) => { const chain = useCurrentChain() + const isWrongChain = useIsWrongChain() - if (!chain) return null + if (!chain || !isWrongChain) return null return ( - + Change your wallet network - You are trying to create a Safe Account on {chain.chainName}. Make sure that your wallet is set to the same - network. + You are trying to {action || 'sign or execute a transaction'} on {chain.chainName}. Make sure that your wallet is + set to the same network. diff --git a/src/components/new-safe/create/NoWalletConnectedWarning/index.tsx b/src/components/new-safe/create/NoWalletConnectedWarning/index.tsx new file mode 100644 index 000000000..e81293b48 --- /dev/null +++ b/src/components/new-safe/create/NoWalletConnectedWarning/index.tsx @@ -0,0 +1,23 @@ +import { Alert, AlertTitle, Box } from '@mui/material' +import useWallet from '@/hooks/wallets/useWallet' +import ConnectWalletButton from '@/components/common/ConnectWallet/ConnectWalletButton' + +const NoWalletConnectedWarning = () => { + const wallet = useWallet() + + if (wallet) { + return null + } + + return ( + + No wallet connected + You need to connect a wallet to create a Safe account. + + + + + ) +} + +export default NoWalletConnectedWarning diff --git a/src/components/new-safe/create/OverviewWidget/index.tsx b/src/components/new-safe/create/OverviewWidget/index.tsx index 2d0a5f858..a9eb6f8ac 100644 --- a/src/components/new-safe/create/OverviewWidget/index.tsx +++ b/src/components/new-safe/create/OverviewWidget/index.tsx @@ -2,11 +2,12 @@ import ChainIndicator from '@/components/common/ChainIndicator' import WalletOverview from 'src/components/common/WalletOverview' import { useCurrentChain } from '@/hooks/useChains' import useWallet from '@/hooks/wallets/useWallet' -import { Card, Grid, Typography } from '@mui/material' +import { Box, Card, Grid, Typography } from '@mui/material' import type { ReactElement } from 'react' import SafeLogo from '@/public/images/logo-no-text.svg' import css from '@/components/new-safe/create/OverviewWidget/styles.module.css' +import ConnectWalletButton from '@/components/common/ConnectWallet/ConnectWalletButton' const LOGO_DIMENSIONS = '22px' @@ -34,11 +35,12 @@ const OverviewWidget = ({ safeName }: { safeName: string }): ReactElement | null
    )) ) : ( -
    - + + Connect your wallet to continue -
    + +
    )}
    diff --git a/src/components/new-safe/create/__tests__/useEstimateSafeCreationGas.test.ts b/src/components/new-safe/create/__tests__/useEstimateSafeCreationGas.test.ts index 6acc65087..172c33ebb 100644 --- a/src/components/new-safe/create/__tests__/useEstimateSafeCreationGas.test.ts +++ b/src/components/new-safe/create/__tests__/useEstimateSafeCreationGas.test.ts @@ -7,9 +7,9 @@ import * as web3 from '@/hooks/wallets/web3' import * as safeContracts from '@/services/contracts/safeContracts' import * as store from '@/store' import { renderHook } from '@/tests/test-utils' +import type { SafeProxyFactoryContractImplementationType } from '@safe-global/protocol-kit/dist/src/types/contracts' import { JsonRpcProvider } from 'ethers' import { EMPTY_DATA, ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' -import type { SafeProxyFactoryEthersContract } from '@safe-global/protocol-kit' import { waitFor } from '@testing-library/react' import { type EIP1193Provider } from '@web3-onboard/core' @@ -27,7 +27,7 @@ describe('useEstimateSafeCreationGas', () => { jest.spyOn(chainIdModule, 'useChainId').mockReturnValue('4') jest .spyOn(safeContracts, 'getReadOnlyProxyFactoryContract') - .mockResolvedValue({ getAddress: () => ZERO_ADDRESS } as unknown as SafeProxyFactoryEthersContract) + .mockResolvedValue({ getAddress: () => ZERO_ADDRESS } as unknown as SafeProxyFactoryContractImplementationType) jest.spyOn(sender, 'encodeSafeCreationTx').mockReturnValue(Promise.resolve(EMPTY_DATA)) jest.spyOn(wallet, 'default').mockReturnValue({} as ConnectedWallet) }) diff --git a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts index 1c0d1cb8c..ff9ff360d 100644 --- a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts +++ b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts @@ -1,15 +1,11 @@ -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 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', () => { beforeEach(() => { @@ -26,37 +22,10 @@ describe('useSyncSafeCreationStep', () => { renderHook(() => useSyncSafeCreationStep(mockSetStep)) - expect(mockSetStep).not.toHaveBeenCalled() - expect(mockPushRoute).toHaveBeenCalledWith({ pathname: AppRoutes.welcome.index, query: undefined }) - }) - - it('should go to the fourth step if there is a pending safe', async () => { - const mockPushRoute = jest.fn() - jest.spyOn(localStorage, 'default').mockReturnValue([{}, jest.fn()]) - jest.spyOn(wallet, 'default').mockReturnValue({ address: '0x1' } as ConnectedWallet) - jest.spyOn(useChainId, 'default').mockReturnValue('11155111') - jest.spyOn(useRouter, 'useRouter').mockReturnValue({ - push: mockPushRoute, - } as unknown as NextRouter) - - const mockSetStep = jest.fn() - - renderHook(() => useSyncSafeCreationStep(mockSetStep), { - initialReduxState: { - undeployedSafes: { - '11155111': { - '0x123': { status: { status: PendingSafeStatus.PROCESSING, type: PayMethod.PayNow }, props: {} as any }, - }, - }, - }, - }) - - expect(mockSetStep).toHaveBeenCalledWith(3) - - expect(mockPushRoute).not.toHaveBeenCalled() + expect(mockSetStep).toHaveBeenCalledWith(0) }) - it('should go to the second step if the wrong chain is connected', async () => { + it('should go to the first 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(useIsWrongChain, 'default').mockReturnValue(true) diff --git a/src/components/new-safe/create/index.tsx b/src/components/new-safe/create/index.tsx index 8f738c082..7683691eb 100644 --- a/src/components/new-safe/create/index.tsx +++ b/src/components/new-safe/create/index.tsx @@ -18,12 +18,16 @@ 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 { type SafeVersion } from '@safe-global/safe-core-sdk-types' +import { getLatestSafeVersion } from '@/utils/chains' +import { useCurrentChain } from '@/hooks/useChains' export type NewSafeFormData = { name: string threshold: number owners: NamedAddress[] saltNonce: number + safeVersion: SafeVersion safeAddress?: string willRelay?: boolean } @@ -97,6 +101,7 @@ const staticHints: Record< const CreateSafe = () => { const router = useRouter() const wallet = useWallet() + const chain = useCurrentChain() const [safeName, setSafeName] = useState('') const [dynamicHint, setDynamicHint] = useState() @@ -156,6 +161,7 @@ const CreateSafe = () => { owners: [], threshold: 1, saltNonce: Date.now(), + safeVersion: getLatestSafeVersion(chain) as SafeVersion, } const onClose = () => { diff --git a/src/components/new-safe/create/logic/index.test.ts b/src/components/new-safe/create/logic/index.test.ts index 6457eca09..8d93077d3 100644 --- a/src/components/new-safe/create/logic/index.test.ts +++ b/src/components/new-safe/create/logic/index.test.ts @@ -1,8 +1,12 @@ import { JsonRpcProvider } from 'ethers' +import * as contracts from '@/services/contracts/safeContracts' +import type { SafeProvider } from '@safe-global/protocol-kit' +import type { CompatibilityFallbackHandlerContractImplementationType } from '@safe-global/protocol-kit/dist/src/types' import { EMPTY_DATA, ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' import * as web3 from '@/hooks/wallets/web3' -import { relaySafeCreation } from '@/components/new-safe/create/logic/index' -import { relayTransaction, type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import * as sdkHelpers from '@/services/tx/tx-sender/sdk' +import { getRedirect, relaySafeCreation } from '@/components/new-safe/create/logic/index' +import { relayTransaction } from '@safe-global/safe-gateway-typescript-sdk' import { toBeHex } from 'ethers' import { Gnosis_safe__factory, @@ -13,50 +17,30 @@ import { getReadOnlyGnosisSafeContract, getReadOnlyProxyFactoryContract, } from '@/services/contracts/safeContracts' -import { LATEST_SAFE_VERSION } from '@/config/constants' import * as gateway from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES, getLatestSafeVersion } from '@/utils/chains' +import { type FEATURES as GatewayFeatures } from '@safe-global/safe-gateway-typescript-sdk' +import { chainBuilder } from '@/tests/builders/chains' -const provider = new JsonRpcProvider(undefined, { name: 'rinkeby', chainId: 4 }) - -const mockTransaction = { - data: EMPTY_DATA, - nonce: 1, - from: '0x10', - to: '0x11', - value: BigInt(0), -} - -const mockPendingTx = { - data: EMPTY_DATA, - from: ZERO_ADDRESS, - to: ZERO_ADDRESS, - nonce: 0, - startBlock: 0, - value: BigInt(0), -} - -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, - } -}) +const provider = new JsonRpcProvider(undefined, { name: 'ethereum', chainId: 1 }) + +const latestSafeVersion = getLatestSafeVersion( + chainBuilder() + .with({ chainId: '1', features: [FEATURES.SAFE_141 as unknown as GatewayFeatures] }) + .build(), +) describe('createNewSafeViaRelayer', () => { const owner1 = toBeHex('0x1', 20) const owner2 = toBeHex('0x2', 20) - const mockChainInfo = { - chainId: '5', - l2: false, - } as ChainInfo + const mockChainInfo = chainBuilder() + .with({ + chainId: '1', + l2: false, + features: [FEATURES.SAFE_141 as unknown as GatewayFeatures], + }) + .build() beforeAll(() => { jest.resetAllMocks() @@ -64,13 +48,26 @@ describe('createNewSafeViaRelayer', () => { }) it('returns taskId if create Safe successfully relayed', async () => { + const mockSafeProvider = { + getExternalProvider: jest.fn(), + getExternalSigner: jest.fn(), + getChainId: jest.fn().mockReturnValue(BigInt(1)), + } as unknown as SafeProvider + jest.spyOn(gateway, 'relayTransaction').mockResolvedValue({ taskId: '0x123' }) + jest.spyOn(sdkHelpers, 'getSafeProvider').mockImplementation(() => mockSafeProvider) + + jest.spyOn(contracts, 'getReadOnlyFallbackHandlerContract').mockResolvedValue({ + getAddress: () => '0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4', + } as unknown as CompatibilityFallbackHandlerContractImplementationType) const expectedSaltNonce = 69 const expectedThreshold = 1 - const proxyFactoryAddress = await (await getReadOnlyProxyFactoryContract('5', LATEST_SAFE_VERSION)).getAddress() - const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract('5', LATEST_SAFE_VERSION) - const safeContractAddress = await (await getReadOnlyGnosisSafeContract(mockChainInfo)).getAddress() + const proxyFactoryAddress = await (await getReadOnlyProxyFactoryContract(latestSafeVersion)).getAddress() + const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(latestSafeVersion) + const safeContractAddress = await ( + await getReadOnlyGnosisSafeContract(mockChainInfo, latestSafeVersion) + ).getAddress() const expectedInitializer = Gnosis_safe__factory.createInterface().encodeFunctionData('setup', [ [owner1, owner2], @@ -93,10 +90,10 @@ describe('createNewSafeViaRelayer', () => { expect(taskId).toEqual('0x123') expect(relayTransaction).toHaveBeenCalledTimes(1) - expect(relayTransaction).toHaveBeenCalledWith('5', { + expect(relayTransaction).toHaveBeenCalledWith('1', { to: proxyFactoryAddress, data: expectedCallData, - version: LATEST_SAFE_VERSION, + version: latestSafeVersion, }) }) @@ -106,4 +103,27 @@ describe('createNewSafeViaRelayer', () => { expect(relaySafeCreation(mockChainInfo, [owner1, owner2], 1, 69)).rejects.toEqual(relayFailedError) }) + + describe('getRedirect', () => { + it("should redirect to home for any redirect that doesn't start with /apps", () => { + const expected = { + pathname: '/home', + query: { + safe: 'sep:0x1234', + }, + } + expect(getRedirect('sep', '0x1234', 'https://google.com')).toEqual(expected) + expect(getRedirect('sep', '0x1234', '/queue')).toEqual(expected) + }) + + it('should redirect to an app if an app URL is passed', () => { + expect(getRedirect('sep', '0x1234', '/apps?appUrl=https://safe-eth.everstake.one/?chain=eth')).toEqual( + '/apps?appUrl=https://safe-eth.everstake.one/?chain=eth&safe=sep:0x1234', + ) + + expect(getRedirect('sep', '0x1234', '/apps?appUrl=https://safe-eth.everstake.one')).toEqual( + '/apps?appUrl=https://safe-eth.everstake.one&safe=sep:0x1234', + ) + }) + }) }) diff --git a/src/components/new-safe/create/logic/index.ts b/src/components/new-safe/create/logic/index.ts index 9c15781a5..e5713f5b5 100644 --- a/src/components/new-safe/create/logic/index.ts +++ b/src/components/new-safe/create/logic/index.ts @@ -1,5 +1,5 @@ import type { SafeVersion } from '@safe-global/safe-core-sdk-types' -import { type BrowserProvider, type Provider } from 'ethers' +import { type Eip1193Provider, type Provider } from 'ethers' import { getSafeInfo, type SafeInfo, type ChainInfo, relayTransaction } from '@safe-global/safe-gateway-typescript-sdk' import { @@ -10,16 +10,17 @@ import { import type { UrlObject } from 'url' import { AppRoutes } from '@/config/routes' import { SAFE_APPS_EVENTS, trackEvent } from '@/services/analytics' -import { predictSafeAddress, SafeFactory } from '@safe-global/protocol-kit' +import { predictSafeAddress, SafeFactory, SafeProvider } 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' +import { isValidSafeVersion } from '@/hooks/coreSDK/safeCoreSDK' 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 { sponsoredCall } from '@/services/tx/relaying' import { checksumAddress } from '@/utils/addresses' +import { getLatestSafeVersion } from '@/utils/chains' +import { ECOSYSTEM_ID_ADDRESS } from '@/config/constants' export type SafeCreationProps = { owners: string[] @@ -27,27 +28,22 @@ export type SafeCreationProps = { saltNonce: number } -const getSafeFactory = async ( - ethersProvider: BrowserProvider, - safeVersion = LATEST_SAFE_VERSION, -): Promise => { +const getSafeFactory = async (provider: Eip1193Provider, safeVersion: SafeVersion): Promise => { if (!isValidSafeVersion(safeVersion)) { throw new Error('Invalid Safe version') } - const ethAdapter = await createEthersAdapter(ethersProvider) - const safeFactory = await SafeFactory.create({ ethAdapter, safeVersion }) - return safeFactory + return SafeFactory.init({ provider, safeVersion }) } /** * Create a Safe creation transaction via Core SDK and submits it to the wallet */ export const createNewSafe = async ( - ethersProvider: BrowserProvider, + provider: Eip1193Provider, props: DeploySafeProps, - safeVersion?: SafeVersion, + safeVersion: SafeVersion, ): Promise => { - const safeFactory = await getSafeFactory(ethersProvider, safeVersion) + const safeFactory = await getSafeFactory(provider, safeVersion) return safeFactory.deploySafe(props) } @@ -55,19 +51,20 @@ export const createNewSafe = async ( * Compute the new counterfactual Safe address before it is actually created */ export const computeNewSafeAddress = async ( - ethersProvider: BrowserProvider, + provider: Eip1193Provider, props: DeploySafeProps, - chainId: string, + chain: ChainInfo, + safeVersion?: SafeVersion, ): Promise => { - const ethAdapter = await createEthersAdapter(ethersProvider) + const safeProvider = new SafeProvider({ provider }) return predictSafeAddress({ - ethAdapter, - chainId: BigInt(chainId), + safeProvider, + chainId: BigInt(chain.chainId), safeAccountConfig: props.safeAccountConfig, safeDeploymentConfig: { saltNonce: props.saltNonce, - safeVersion: LATEST_SAFE_VERSION as SafeVersion, + safeVersion: safeVersion ?? getLatestSafeVersion(chain), }, }) } @@ -81,26 +78,40 @@ export const encodeSafeCreationTx = async ({ threshold, saltNonce, chain, -}: SafeCreationProps & { chain: ChainInfo }) => { - const readOnlySafeContract = await getReadOnlyGnosisSafeContract(chain, LATEST_SAFE_VERSION) - const readOnlyProxyContract = await getReadOnlyProxyFactoryContract(chain.chainId, LATEST_SAFE_VERSION) - const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(chain.chainId, LATEST_SAFE_VERSION) + safeVersion, +}: SafeCreationProps & { chain: ChainInfo; safeVersion?: SafeVersion }) => { + const usedSafeVersion = safeVersion ?? getLatestSafeVersion(chain) + const readOnlySafeContract = await getReadOnlyGnosisSafeContract(chain, usedSafeVersion) + const readOnlyProxyContract = await getReadOnlyProxyFactoryContract(usedSafeVersion) + const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(usedSafeVersion) - const setupData = readOnlySafeContract.encode('setup', [ + const callData = { owners, threshold, - ZERO_ADDRESS, - EMPTY_DATA, - await readOnlyFallbackHandlerContract.getAddress(), - ZERO_ADDRESS, - '0', - ZERO_ADDRESS, + to: ZERO_ADDRESS, + data: EMPTY_DATA, + fallbackHandler: await readOnlyFallbackHandlerContract.getAddress(), + paymentToken: ZERO_ADDRESS, + payment: 0, + paymentReceiver: ECOSYSTEM_ID_ADDRESS, + } + + // @ts-ignore union type is too complex + const setupData = readOnlySafeContract.encode('setup', [ + callData.owners, + callData.threshold, + callData.to, + callData.data, + callData.fallbackHandler, + callData.paymentToken, + callData.payment, + callData.paymentReceiver, ]) return readOnlyProxyContract.encode('createProxyWithNonce', [ await readOnlySafeContract.getAddress(), setupData, - saltNonce, + BigInt(saltNonce), ]) } @@ -109,8 +120,9 @@ export const estimateSafeCreationGas = async ( provider: Provider, from: string, safeParams: SafeCreationProps, + safeVersion?: SafeVersion, ): Promise => { - const readOnlyProxyFactoryContract = await getReadOnlyProxyFactoryContract(chain.chainId, LATEST_SAFE_VERSION) + const readOnlyProxyFactoryContract = await getReadOnlyProxyFactoryContract(safeVersion ?? getLatestSafeVersion(chain)) const encodedSafeCreationTx = await encodeSafeCreationTx({ ...safeParams, chain }) const gas = await provider.estimateGas({ @@ -147,17 +159,14 @@ export const getRedirect = ( if (!chainPrefix) return AppRoutes.index // Go to the dashboard if no specific redirect is provided - if (!redirectUrl) { + if (!redirectUrl || !redirectUrl.startsWith(AppRoutes.apps.index)) { return { pathname: AppRoutes.home, query: { safe: address } } } // Otherwise, redirect to the provided URL (e.g. from a Safe App) // Track the redirect to Safe App - // TODO: Narrow this down to /apps only - if (redirectUrl.includes('apps')) { - trackEvent(SAFE_APPS_EVENTS.SHARED_APP_OPEN_AFTER_SAFE_CREATION) - } + trackEvent(SAFE_APPS_EVENTS.SHARED_APP_OPEN_AFTER_SAFE_CREATION) // We're prepending the safe address directly here because the `router.push` doesn't parse // The URL for already existing query params @@ -174,13 +183,15 @@ export const relaySafeCreation = async ( saltNonce: number, version?: SafeVersion, ) => { - const safeVersion = version ?? LATEST_SAFE_VERSION + const latestSafeVersion = getLatestSafeVersion(chain) + + const safeVersion = version ?? latestSafeVersion - const readOnlyProxyFactoryContract = await getReadOnlyProxyFactoryContract(chain.chainId, safeVersion) + const readOnlyProxyFactoryContract = await getReadOnlyProxyFactoryContract(safeVersion) const proxyFactoryAddress = await readOnlyProxyFactoryContract.getAddress() - const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(chain.chainId, safeVersion) + const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(safeVersion) const fallbackHandlerAddress = await readOnlyFallbackHandlerContract.getAddress() - const readOnlySafeContract = await getReadOnlyGnosisSafeContract(chain) + const readOnlySafeContract = await getReadOnlyGnosisSafeContract(chain, safeVersion) const safeContractAddress = await readOnlySafeContract.getAddress() const callData = { @@ -191,9 +202,10 @@ export const relaySafeCreation = async ( fallbackHandler: fallbackHandlerAddress, paymentToken: ZERO_ADDRESS, payment: 0, - paymentReceiver: ZERO_ADDRESS, + paymentReceiver: ECOSYSTEM_ID_ADDRESS, } + // @ts-ignore const initializer = readOnlySafeContract.encode('setup', [ callData.owners, callData.threshold, @@ -208,7 +220,7 @@ export const relaySafeCreation = async ( const createProxyWithNonceCallData = readOnlyProxyFactoryContract.encode('createProxyWithNonce', [ safeContractAddress, initializer, - saltNonce, + BigInt(saltNonce), ]) const relayResponse = await relayTransaction(chain.chainId, { diff --git a/src/components/new-safe/create/logic/utils.test.ts b/src/components/new-safe/create/logic/utils.test.ts index 91b5c4c54..0a9f54378 100644 --- a/src/components/new-safe/create/logic/utils.test.ts +++ b/src/components/new-safe/create/logic/utils.test.ts @@ -1,19 +1,17 @@ import * as creationUtils from '@/components/new-safe/create/logic/index' import { getAvailableSaltNonce } from '@/components/new-safe/create/logic/utils' -import * as web3Utils from '@/hooks/wallets/web3' +import * as walletUtils from '@/utils/wallets' import { faker } from '@faker-js/faker' import type { DeploySafeProps } from '@safe-global/protocol-kit' -import { BrowserProvider } from 'ethers' import { MockEip1193Provider } from '@/tests/mocks/providers' +import { chainBuilder } from '@/tests/builders/chains' describe('getAvailableSaltNonce', () => { jest.spyOn(creationUtils, 'computeNewSafeAddress').mockReturnValue(Promise.resolve(faker.finance.ethereumAddress())) - let mockProvider: BrowserProvider let mockDeployProps: DeploySafeProps beforeAll(() => { - mockProvider = new BrowserProvider(MockEip1193Provider) mockDeployProps = { safeAccountConfig: { threshold: 1, @@ -28,31 +26,31 @@ 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)) + jest.spyOn(walletUtils, 'isSmartContract').mockReturnValue(Promise.resolve(false)) const initialNonce = faker.string.numeric() - const mockChainId = faker.string.numeric() + const mockChain = chainBuilder().build() const result = await getAvailableSaltNonce( - mockProvider, + MockEip1193Provider, { ...mockDeployProps, saltNonce: initialNonce }, - mockChainId, + mockChain, ) expect(result).toEqual(initialNonce) }) it('should return an increased nonce if a contract is deployed to the computed address', async () => { - jest.spyOn(web3Utils, 'isSmartContract').mockReturnValueOnce(Promise.resolve(true)) + jest.spyOn(walletUtils, 'isSmartContract').mockReturnValueOnce(Promise.resolve(true)) const initialNonce = faker.string.numeric() - const mockChainId = faker.string.numeric() + const mockChain = chainBuilder().build() const result = await getAvailableSaltNonce( - mockProvider, + MockEip1193Provider, { ...mockDeployProps, saltNonce: initialNonce }, - mockChainId, + mockChain, ) - jest.spyOn(web3Utils, 'isSmartContract').mockReturnValueOnce(Promise.resolve(false)) + jest.spyOn(walletUtils, 'isSmartContract').mockReturnValueOnce(Promise.resolve(false)) const increasedNonce = (Number(initialNonce) + 1).toString() diff --git a/src/components/new-safe/create/logic/utils.ts b/src/components/new-safe/create/logic/utils.ts index 5d616c2d1..ff6305875 100644 --- a/src/components/new-safe/create/logic/utils.ts +++ b/src/components/new-safe/create/logic/utils.ts @@ -1,19 +1,27 @@ import { computeNewSafeAddress } from '@/components/new-safe/create/logic/index' -import { isSmartContract } from '@/hooks/wallets/web3' +import { isSmartContract } from '@/utils/wallets' import type { DeploySafeProps } from '@safe-global/protocol-kit' -import type { BrowserProvider } from 'ethers' +import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { type SafeVersion } from '@safe-global/safe-core-sdk-types' +import type { Eip1193Provider } from 'ethers' export const getAvailableSaltNonce = async ( - provider: BrowserProvider, + provider: Eip1193Provider, props: DeploySafeProps, - chainId: string, + chain: ChainInfo, + safeVersion?: SafeVersion, ): Promise => { - const safeAddress = await computeNewSafeAddress(provider, props, chainId) - const isContractDeployed = await isSmartContract(provider, safeAddress) + const safeAddress = await computeNewSafeAddress(provider, props, chain, safeVersion) + const isContractDeployed = await isSmartContract(safeAddress) // Safe is already deployed so we try the next saltNonce if (isContractDeployed) { - return getAvailableSaltNonce(provider, { ...props, saltNonce: (Number(props.saltNonce) + 1).toString() }, chainId) + return getAvailableSaltNonce( + provider, + { ...props, saltNonce: (Number(props.saltNonce) + 1).toString() }, + chain, + safeVersion, + ) } // We know that there will be a saltNonce but the type has it as optional diff --git a/src/components/new-safe/create/steps/AdvancedOptionsStep/index.tsx b/src/components/new-safe/create/steps/AdvancedOptionsStep/index.tsx new file mode 100644 index 000000000..f7627ef66 --- /dev/null +++ b/src/components/new-safe/create/steps/AdvancedOptionsStep/index.tsx @@ -0,0 +1,201 @@ +import { Button, MenuItem, Divider, Box, TextField, Stack, Skeleton, SvgIcon, Tooltip, Typography } from '@mui/material' +import { Controller, FormProvider, useForm } from 'react-hook-form' +import type { ReactElement } from 'react' + +import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' +import type { NewSafeFormData } from '@/components/new-safe/create' +import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCreationStep' +import ArrowBackIcon from '@mui/icons-material/ArrowBack' +import layoutCss from '@/components/new-safe/create/styles.module.css' +import { type SafeVersion } from '@safe-global/safe-core-sdk-types' +import NumberField from '@/components/common/NumberField' +import { useCurrentChain } from '@/hooks/useChains' +import useAsync from '@/hooks/useAsync' +import { computeNewSafeAddress } from '../../logic' +import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeContracts' +import EthHashInfo from '@/components/common/EthHashInfo' +import InfoIcon from '@/public/images/notifications/info.svg' +import useWallet from '@/hooks/wallets/useWallet' +import { isSmartContract } from '@/utils/wallets' + +enum AdvancedOptionsFields { + safeVersion = 'safeVersion', + saltNonce = 'saltNonce', +} + +export type AdvancedOptionsStepForm = { + [AdvancedOptionsFields.safeVersion]: SafeVersion + [AdvancedOptionsFields.saltNonce]: number +} + +const ADVANCED_OPTIONS_STEP_FORM_ID = 'create-safe-advanced-options-step-form' + +const AdvancedOptionsStep = ({ onSubmit, onBack, data, setStep }: StepRenderProps): ReactElement => { + const wallet = useWallet() + useSyncSafeCreationStep(setStep) + const chain = useCurrentChain() + + const formMethods = useForm({ + mode: 'onChange', + defaultValues: data, + }) + + const { handleSubmit, control, watch, formState, getValues, register } = formMethods + + const selectedSafeVersion = watch(AdvancedOptionsFields.safeVersion) + const selectedSaltNonce = watch(AdvancedOptionsFields.saltNonce) + + const [readOnlyFallbackHandlerContract] = useAsync( + () => (chain ? getReadOnlyFallbackHandlerContract(selectedSafeVersion) : undefined), + [chain, selectedSafeVersion], + ) + + const [predictedSafeAddress] = useAsync(async () => { + if (!chain || !readOnlyFallbackHandlerContract || !wallet) { + return undefined + } + return computeNewSafeAddress( + wallet.provider, + { + safeAccountConfig: { + owners: data.owners.map((owner) => owner.address), + threshold: data.threshold, + fallbackHandler: await readOnlyFallbackHandlerContract.getAddress(), + }, + saltNonce: selectedSaltNonce.toString(), + }, + chain, + selectedSafeVersion, + ) + }, [ + chain, + data.owners, + data.threshold, + wallet, + readOnlyFallbackHandlerContract, + selectedSafeVersion, + selectedSaltNonce, + ]) + + const [isDeployed] = useAsync( + async () => (predictedSafeAddress ? await isSmartContract(predictedSafeAddress) : false), + [predictedSafeAddress], + ) + + const isDisabled = !formState.isValid || Boolean(isDeployed) + + const handleBack = () => { + const formData = getValues() + onBack(formData) + } + + const onFormSubmit = handleSubmit((data) => { + onSubmit(data) + + // TODO: Tracking of advanced setup + }) + + return ( +
    + + + + + Safe version + + + + + + + + Changes the used master copy and fallback handler of the Safe. + + ( + + 1.4.1 (latest) + 1.3.0 + + )} + /> + + + + + + Salt nonce + + + + + + + + Impacts the derived Safe address + + { + if (isNaN(value)) { + return 'Salt nonce must be a number' + } + if (value < 0) { + return 'Salt nonce must be positive' + } + }, + required: true, + })} + label="Salt nonce" + error={Boolean(formState.errors[AdvancedOptionsFields.saltNonce]) || Boolean(isDeployed)} + helperText={ + formState.errors[AdvancedOptionsFields.saltNonce]?.message ?? Boolean(isDeployed) + ? 'The Safe is already deployed. Use a different salt nonce.' + : undefined + } + /> + + + + New Safe address + + {predictedSafeAddress ? ( + + ) : ( + + )} + + + + + + + + + + +
    + ) +} + +export default AdvancedOptionsStep 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 9ab659c24..23a7a6703 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.test.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.test.tsx @@ -8,6 +8,8 @@ import ReviewStep, { NetworkFee } from '@/components/new-safe/create/steps/Revie import * as useWallet from '@/hooks/wallets/useWallet' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import { act, fireEvent, screen } from '@testing-library/react' +import { LATEST_SAFE_VERSION } from '@/config/constants' +import { type SafeVersion } from '@safe-global/safe-core-sdk-types' const mockChainInfo = { chainId: '100', @@ -22,7 +24,7 @@ describe('NetworkFee', () => { it('should display the total fee', () => { jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'MetaMask' } as unknown as ConnectedWallet) const mockTotalFee = '0.0123' - const result = render() + const result = render() expect(result.getByText(`โ‰ˆ ${mockTotalFee} ${mockChainInfo.nativeCurrency.symbol}`)).toBeInTheDocument() }) @@ -39,6 +41,7 @@ describe('ReviewStep', () => { threshold: 1, owners: [{ name: '', address: '0x1' }], saltNonce: 0, + safeVersion: LATEST_SAFE_VERSION as SafeVersion, } jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) @@ -55,6 +58,7 @@ describe('ReviewStep', () => { threshold: 1, owners: [{ name: '', address: '0x1' }], saltNonce: 0, + safeVersion: LATEST_SAFE_VERSION as SafeVersion, } jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) @@ -70,6 +74,7 @@ describe('ReviewStep', () => { threshold: 1, owners: [{ name: '', address: '0x1' }], saltNonce: 0, + safeVersion: LATEST_SAFE_VERSION as SafeVersion, } jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) @@ -86,6 +91,7 @@ describe('ReviewStep', () => { threshold: 1, owners: [{ name: '', address: '0x1' }], saltNonce: 0, + safeVersion: LATEST_SAFE_VERSION as SafeVersion, } jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) @@ -102,6 +108,7 @@ describe('ReviewStep', () => { threshold: 1, owners: [{ name: '', address: '0x1' }], saltNonce: 0, + safeVersion: LATEST_SAFE_VERSION as SafeVersion, } jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) @@ -124,6 +131,7 @@ describe('ReviewStep', () => { threshold: 1, owners: [{ name: '', address: '0x1' }], saltNonce: 0, + safeVersion: LATEST_SAFE_VERSION as SafeVersion, } jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) jest.spyOn(relay, 'hasRemainingRelays').mockReturnValue(true) diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index e7bfa4a4f..2eea468a2 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -2,7 +2,6 @@ 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' @@ -16,7 +15,6 @@ 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 { LATEST_SAFE_VERSION } from '@/config/constants' import PayNowPayLater, { PayMethod } from '@/features/counterfactual/PayNowPayLater' import { CF_TX_GROUP_KEY, createCounterfactualSafe } from '@/features/counterfactual/utils' import { useCurrentChain, useHasFeature } from '@/hooks/useChains' @@ -25,7 +23,6 @@ import useIsWrongChain from '@/hooks/useIsWrongChain' 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' @@ -37,29 +34,27 @@ 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 { checksumAddress } from '@/utils/addresses' +import { ECOSYSTEM_ID_ADDRESS } from '@/config/constants' export const NetworkFee = ({ totalFee, chain, - willRelay, + isWaived, inline = false, }: { totalFee: string chain: ChainInfo | undefined - willRelay: boolean + isWaived: boolean inline?: boolean }) => { - const wallet = useWallet() - return ( - + ≈ {totalFee} {chain?.nativeCurrency.symbol} @@ -119,7 +114,6 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps { - if (!wallet || !provider || !chain) return + if (!wallet || !chain) return setIsCreating(true) try { - const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract( - chain.chainId, - LATEST_SAFE_VERSION, - ) + const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(data.safeVersion) const props: DeploySafeProps = { safeAccountConfig: { threshold: data.threshold, owners: data.owners.map((owner) => checksumAddress(owner.address)), fallbackHandler: await readOnlyFallbackHandlerContract.getAddress(), + paymentReceiver: ECOSYSTEM_ID_ADDRESS, }, } - const saltNonce = await getAvailableSaltNonce(provider, { ...props, saltNonce: '0' }, chain.chainId) - const safeAddress = await computeNewSafeAddress(provider, { ...props, saltNonce }, chain.chainId) + const saltNonce = await getAvailableSaltNonce( + wallet.provider, + { ...props, saltNonce: '0' }, + chain, + data.safeVersion, + ) + const safeAddress = await computeNewSafeAddress(wallet.provider, { ...props, saltNonce }, chain, data.safeVersion) if (isCounterfactual && payMethod === PayMethod.PayLater) { gtmSetSafeAddress(safeAddress) trackEvent({ ...OVERVIEW_EVENTS.PROCEED_WITH_TX, label: 'counterfactual', category: CREATE_SAFE_CATEGORY }) - await createCounterfactualSafe(chain, safeAddress, saltNonce, data, dispatch, props, router) + createCounterfactualSafe(chain, safeAddress, saltNonce, data, dispatch, props, PayMethod.PayLater, router) trackEvent({ ...CREATE_SAFE_EVENTS.CREATED_SAFE, label: 'counterfactual' }) return } @@ -200,21 +197,9 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps { - dispatch(addUndeployedSafe(undeployedSafe)) + // Create a counterfactual Safe + createCounterfactualSafe(chain, safeAddress, saltNonce, data, dispatch, props, PayMethod.PayNow) if (taskId) { safeCreationDispatch(SafeCreationEvent.RELAYING, { groupKey: CF_TX_GROUP_KEY, taskId, safeAddress }) @@ -240,17 +225,22 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps { - onSubmitCallback(undefined, txHash) + await createNewSafe( + wallet.provider, + { + safeAccountConfig: props.safeAccountConfig, + saltNonce, + options, + callback: (txHash) => { + onSubmitCallback(undefined, txHash) + }, }, - }) + data.safeVersion, + ) } } catch (_err) { const error = asError(_err) @@ -299,7 +289,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps You will have to confirm a transaction and pay an estimated fee of{' '} - with your connected + with your connected wallet
    @@ -332,7 +322,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps - + {!willRelay && ( @@ -344,7 +334,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps
    - {isWrongChain && } + {!walletCanPay && !willRelay && ( diff --git a/src/components/new-safe/create/steps/ReviewStep/styles.module.css b/src/components/new-safe/create/steps/ReviewStep/styles.module.css index 47bb71ae8..f495e3d66 100644 --- a/src/components/new-safe/create/steps/ReviewStep/styles.module.css +++ b/src/components/new-safe/create/steps/ReviewStep/styles.module.css @@ -5,7 +5,7 @@ font-size: 14px; } -.sponsoredFee { +.strikethrough { text-decoration: line-through; color: var(--color-text-secondary); } diff --git a/src/components/new-safe/create/steps/SetNameStep/index.tsx b/src/components/new-safe/create/steps/SetNameStep/index.tsx index 703cafc30..62c9b1b01 100644 --- a/src/components/new-safe/create/steps/SetNameStep/index.tsx +++ b/src/components/new-safe/create/steps/SetNameStep/index.tsx @@ -5,7 +5,6 @@ import InfoIcon from '@/public/images/notifications/info.svg' import NetworkSelector from '@/components/common/NetworkSelector' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' import type { NewSafeFormData } from '@/components/new-safe/create' -import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCreationStep' import css from '@/components/new-safe/create/steps/SetNameStep/styles.module.css' import layoutCss from '@/components/new-safe/create/styles.module.css' @@ -17,13 +16,20 @@ import { AppRoutes } from '@/config/routes' import MUILink from '@mui/material/Link' import Link from 'next/link' import { useRouter } from 'next/router' +import NoWalletConnectedWarning from '../../NoWalletConnectedWarning' +import { type SafeVersion } from '@safe-global/safe-core-sdk-types' +import { useCurrentChain } from '@/hooks/useChains' +import { useEffect } from 'react' +import { getLatestSafeVersion } from '@/utils/chains' type SetNameStepForm = { name: string + safeVersion: SafeVersion } enum SetNameStepFields { name = 'name', + safeVersion = 'safeVersion', } const SET_NAME_STEP_FORM_ID = 'create-safe-set-name-step-form' @@ -31,23 +37,22 @@ const SET_NAME_STEP_FORM_ID = 'create-safe-set-name-step-form' function SetNameStep({ data, onSubmit, - setStep, setSafeName, }: StepRenderProps & { setSafeName: (name: string) => void }) { const router = useRouter() const fallbackName = useMnemonicSafeName() const isWrongChain = useIsWrongChain() - useSyncSafeCreationStep(setStep) + + const chain = useCurrentChain() const formMethods = useForm({ mode: 'all', - defaultValues: { - [SetNameStepFields.name]: data.name, - }, + defaultValues: data, }) const { handleSubmit, + setValue, formState: { errors, isValid }, } = formMethods @@ -66,6 +71,11 @@ function SetNameStep({ router.push(AppRoutes.welcome.index) } + // whenever the chain switches we need to update the latest Safe version + useEffect(() => { + setValue(SetNameStepFields.safeVersion, getLatestSafeVersion(chain)) + }, [chain, setValue]) + const isDisabled = isWrongChain || !isValid return ( @@ -112,7 +122,11 @@ function SetNameStep({ . - {isWrongChain && } + + + + + diff --git a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx index 3776d953f..fc0b04589 100644 --- a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx +++ b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx @@ -72,7 +72,9 @@ const StatusMessage = ({ {stepInfo.instruction} )} - {!isError && explorerLink && Check Status} + {!isError && explorerLink && ( + Check status on block explorer + )} ) diff --git a/src/components/new-safe/create/steps/StatusStep/index.tsx b/src/components/new-safe/create/steps/StatusStep/index.tsx index 2a6b01ac0..cd896b796 100644 --- a/src/components/new-safe/create/steps/StatusStep/index.tsx +++ b/src/components/new-safe/create/steps/StatusStep/index.tsx @@ -17,7 +17,7 @@ import { Alert, AlertTitle, Box, Button, Paper, Stack, SvgIcon, Typography } fro import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' -import useSyncSafeCreationStep from '../../useSyncSafeCreationStep' +import { getLatestSafeVersion } from '@/utils/chains' const SPEED_UP_THRESHOLD_IN_SECONDS = 15 @@ -37,8 +37,6 @@ export const CreateSafeStatus = ({ const isError = status === SafeCreationEvent.FAILED || status === SafeCreationEvent.REVERTED - useSyncSafeCreationStep(setStep) - useEffect(() => { const unsubFns = Object.entries(safeCreationPendingStatuses).map(([event]) => safeCreationSubscribe(event as SafeCreationEvent, async () => { @@ -56,7 +54,10 @@ export const CreateSafeStatus = ({ if (status === SafeCreationEvent.SUCCESS) { dispatch(updateAddressBook(chain.chainId, safeAddress, data.name, data.owners, data.threshold)) - router.push(getRedirect(chain.shortName, safeAddress, router.query?.safeViewRedirectURL)) + const redirect = getRedirect(chain.shortName, safeAddress, router.query?.safeViewRedirectURL) + if (typeof redirect !== 'string' || redirect.startsWith('/')) { + router.push(redirect) + } } }, [dispatch, chain, data.name, data.owners, data.threshold, router, safeAddress, status]) @@ -86,6 +87,7 @@ export const CreateSafeStatus = ({ threshold: pendingSafe.props.safeAccountConfig.threshold, saltNonce: Number(pendingSafe.props.safeDeploymentConfig?.saltNonce), safeAddress, + safeVersion: pendingSafe.props.safeDeploymentConfig?.safeVersion ?? getLatestSafeVersion(chain), }) } diff --git a/src/components/new-safe/create/useEstimateSafeCreationGas.ts b/src/components/new-safe/create/useEstimateSafeCreationGas.ts index b285f007c..4cbe4c04a 100644 --- a/src/components/new-safe/create/useEstimateSafeCreationGas.ts +++ b/src/components/new-safe/create/useEstimateSafeCreationGas.ts @@ -3,9 +3,11 @@ import useWallet from '@/hooks/wallets/useWallet' import useAsync from '@/hooks/useAsync' import { useCurrentChain } from '@/hooks/useChains' import { estimateSafeCreationGas, type SafeCreationProps } from '@/components/new-safe/create/logic' +import { type SafeVersion } from '@safe-global/safe-core-sdk-types' export const useEstimateSafeCreationGas = ( safeParams: SafeCreationProps, + safeVersion?: SafeVersion, ): { gasLimit?: bigint gasLimitError?: Error @@ -18,8 +20,8 @@ export const useEstimateSafeCreationGas = ( const [gasLimit, gasLimitError, gasLimitLoading] = useAsync(() => { if (!wallet?.address || !chain || !web3ReadOnly) return - return estimateSafeCreationGas(chain, web3ReadOnly, wallet.address, safeParams) - }, [wallet, chain, web3ReadOnly, safeParams]) + return estimateSafeCreationGas(chain, web3ReadOnly, wallet.address, safeParams, safeVersion) + }, [wallet, chain, web3ReadOnly, safeParams, safeVersion]) return { gasLimit, gasLimitError, gasLimitLoading } } diff --git a/src/components/new-safe/create/useSyncSafeCreationStep.ts b/src/components/new-safe/create/useSyncSafeCreationStep.ts index 1e645d6c9..5ba811a03 100644 --- a/src/components/new-safe/create/useSyncSafeCreationStep.ts +++ b/src/components/new-safe/create/useSyncSafeCreationStep.ts @@ -1,37 +1,20 @@ -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 useIsWrongChain from '@/hooks/useIsWrongChain' -import { useRouter } from 'next/router' -import { AppRoutes } from '@/config/routes' const useSyncSafeCreationStep = (setStep: StepRenderProps['setStep']) => { - 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 && pendingSafe.status.status !== 'AWAITING_EXECUTION') { - setStep(3) - return - } - - // Jump to the welcome page if there is no wallet - if (!wallet) { - router.push({ pathname: AppRoutes.welcome.index, query: router.query }) - } - // Jump to choose name and network step if the wallet is connected to the wrong chain and there is no pending Safe - if (isWrongChain) { + if (!wallet || isWrongChain) { setStep(0) return } - }, [wallet, setStep, pendingSafe, isWrongChain, router]) + }, [wallet, setStep, isWrongChain]) } export default useSyncSafeCreationStep diff --git a/src/components/notification-center/NotificationCenter/index.tsx b/src/components/notification-center/NotificationCenter/index.tsx index ebce3aee2..bd7c47371 100644 --- a/src/components/notification-center/NotificationCenter/index.tsx +++ b/src/components/notification-center/NotificationCenter/index.tsx @@ -110,6 +110,10 @@ const NotificationCenter = (): ReactElement => { ({ defaultValues: { riskAcknowledgement: false }, mode: 'onChange' }) - const onSubmit: SubmitHandler = (_, __) => { + const onSubmit: SubmitHandler = () => { if (safeApp) { onSave(safeApp) trackSafeAppEvent(SAFE_APPS_EVENTS.ADD_CUSTOM_APP, safeApp.url) diff --git a/src/components/safe-apps/AppFrame/index.tsx b/src/components/safe-apps/AppFrame/index.tsx index b2e1155ea..de48f57e0 100644 --- a/src/components/safe-apps/AppFrame/index.tsx +++ b/src/components/safe-apps/AppFrame/index.tsx @@ -29,15 +29,14 @@ import css from './styles.module.css' import SafeAppIframe from './SafeAppIframe' import { useCustomAppCommunicator } from '@/hooks/safe-apps/useCustomAppCommunicator' -const UNKNOWN_APP_NAME = 'Unknown Safe App' - type AppFrameProps = { appUrl: string allowedFeaturesList: string safeAppFromManifest: SafeAppDataWithPermissions + isNativeEmbed?: boolean } -const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrameProps): ReactElement => { +const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest, isNativeEmbed }: AppFrameProps): ReactElement => { const { safe, safeLoaded } = useSafeInfo() const addressBook = useAddressBook() const chainId = useChainId() @@ -98,11 +97,14 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame } setAppIsLoading(false) - gtmTrackPageview(`${router.pathname}?appUrl=${router.query.appUrl}`, router.asPath) - }, [appUrl, iframeRef, setAppIsLoading, router]) + + if (!isNativeEmbed) { + gtmTrackPageview(`${router.pathname}?appUrl=${router.query.appUrl}`, router.asPath) + } + }, [appUrl, iframeRef, setAppIsLoading, router, isNativeEmbed]) useEffect(() => { - if (!appIsLoading && !isBackendAppsLoading) { + if (!isNativeEmbed && !appIsLoading && !isBackendAppsLoading) { trackSafeAppEvent( { ...SAFE_APPS_EVENTS.OPEN_APP, @@ -110,7 +112,7 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame appName, ) } - }, [appIsLoading, isBackendAppsLoading, appName]) + }, [appIsLoading, isBackendAppsLoading, appName, isNativeEmbed]) if (!safeLoaded) { return
    @@ -118,9 +120,11 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame return ( <> - - {`Safe Apps - Viewer - ${remoteApp ? remoteApp.name : UNKNOWN_APP_NAME}`} - + {!isNativeEmbed && ( + + {`Safe{Wallet} - Safe Apps${remoteApp ? ' - ' + remoteApp.name : ''}`} + + )}
    {thirdPartyCookiesDisabled && setThirdPartyCookiesDisabled(false)} />} @@ -160,7 +164,7 @@ const AppFrame = ({ appUrl, allowedFeaturesList, safeAppFromManifest }: AppFrame transactions={transactions} /> - {permissionsRequest && ( + {!isNativeEmbed && permissionsRequest && ( { - it('should detect unsafe src', () => { - expect(_isSafeSrc('https://google.com/test.jpg')).toBe(false) - expect(_isSafeSrc('data:image/png;base64,')).toBe(false) - }) - - it('should detect safe src', () => { - expect(_isSafeSrc('https://safe-transaction-assets.safe.global/contracts/logos/0x34CfAC646f3.png')).toBe(true) - expect(_isSafeSrc('https://safe-transaction-assets.staging.5afe.dev/contracts/logos/0x34CfAC.png')).toBe(true) - expect(_isSafeSrc('/images/transactions/incoming.svg')).toBe(true) - }) -}) diff --git a/src/components/safe-apps/SafeAppIconCard/index.test.tsx b/src/components/safe-apps/SafeAppIconCard/index.test.tsx new file mode 100644 index 000000000..5997c6f67 --- /dev/null +++ b/src/components/safe-apps/SafeAppIconCard/index.test.tsx @@ -0,0 +1,18 @@ +import { render } from '@/tests/test-utils' +import SafeAppIconCard from '.' + +describe('SafeAppIconCard', () => { + it('should render an icon', () => { + const src = 'https://safe-transaction-assets.safe.global/safe_apps/160/icon.png' + const { queryByAltText } = render( + , + ) + + const img = queryByAltText('test') + expect(img).toBeInTheDocument() + expect(img).toHaveAttribute('src', src) + expect(img).toHaveAttribute('height', '100') + expect(img).toHaveAttribute('width', '100') + expect(img).not.toHaveAttribute('crossorigin') + }) +}) diff --git a/src/components/safe-apps/SafeAppIconCard/index.tsx b/src/components/safe-apps/SafeAppIconCard/index.tsx index b0333aa34..32b398fff 100644 --- a/src/components/safe-apps/SafeAppIconCard/index.tsx +++ b/src/components/safe-apps/SafeAppIconCard/index.tsx @@ -1,38 +1,7 @@ import ImageFallback from '@/components/common/ImageFallback' -import { type ReactElement, memo } from 'react' const APP_LOGO_FALLBACK_IMAGE = `/images/apps/app-placeholder.svg` -const getIframeContent = (url: string, width: number, height: number, fallback: string): string => { - return ` - - Safe App logo - - - ` -} - -export const _isSafeSrc = (src: string) => { - const allowedHosts = ['.safe.global', '.5afe.dev'] - const isRelative = src.startsWith('/') - - let hostname = '' - if (!isRelative) { - try { - hostname = new URL(src).hostname - } catch (e) { - return false - } - } - - return isRelative || allowedHosts.some((host) => hostname.endsWith(host)) -} - const SafeAppIconCard = ({ src, alt, @@ -45,24 +14,6 @@ const SafeAppIconCard = ({ width?: number height?: number fallback?: string -}): ReactElement => { - if (_isSafeSrc(src)) { - return - } - - return ( -