diff --git a/.env.example b/.env.example index 4ee43536d7..ab53f2b047 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -## Infura token (required) +## Infura token NEXT_PUBLIC_INFURA_TOKEN= NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN= @@ -22,7 +22,6 @@ NEXT_PUBLIC_SENTRY_DSN= NEXT_PUBLIC_BEAMER_ID= # Wallet-specific variables -NEXT_PUBLIC_WC_BRIDGE= NEXT_PUBLIC_WC_PROJECT_ID= # E2E tests diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index 8a8c32e0c1..347a646d4c 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -49,3 +49,4 @@ runs: NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING }} 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 }} diff --git a/.github/workflows/cypress/action.yml b/.github/workflows/cypress/action.yml new file mode 100644 index 0000000000..e0aedfa3a7 --- /dev/null +++ b/.github/workflows/cypress/action.yml @@ -0,0 +1,57 @@ +name: 'Cypress' + +description: 'Run Cypress' + +inputs: + secrets: + description: 'GitHub secrets as JSON' + required: true + + spec: + description: 'A glob pattern for which tests to run' + required: true + + group: + description: 'The name of the group (e.g. "smoke")' + required: true + + project_id: + description: 'Cypress cloud project id' + required: false + + record_key: + description: 'Cypress cloud record key' + required: false + +runs: + using: 'composite' + steps: + - uses: browser-actions/setup-chrome@v1 + + - uses: ./.github/workflows/yarn + + - name: Install Cypress + shell: bash + run: ./node_modules/.bin/cypress install + + - uses: ./.github/workflows/build + with: + secrets: ${{ inputs.secrets }} + e2e_mnemonic: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_CYPRESS_MNEMONIC }} + + - name: Serve + shell: bash + run: yarn serve & + + - uses: cypress-io/github-action@v4 + with: + spec: ${{ inputs.spec }} + group: ${{ inputs.group }} + parallel: true + browser: chrome + record: true + 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 }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy-dev.yml similarity index 98% rename from .github/workflows/deploy.yml rename to .github/workflows/deploy-dev.yml index f33c09fb11..a17a1b1c72 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy-dev.yml @@ -40,6 +40,7 @@ jobs: - uses: ./.github/workflows/build with: secrets: ${{ toJSON(secrets) }} + prod: ${{ github.ref == 'refs/heads/main' }} - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 diff --git a/.github/workflows/deploy-dockerhub.yml b/.github/workflows/deploy-dockerhub.yml index db4c810127..a565ca8779 100644 --- a/.github/workflows/deploy-dockerhub.yml +++ b/.github/workflows/deploy-dockerhub.yml @@ -13,25 +13,50 @@ on: jobs: dockerhub-push: runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || (github.event_name == 'release' && github.event.action == 'released') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + - uses: docker/setup-buildx-action@v3 - name: Dockerhub login - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Deploy Dockerhub main + - name: Deploy Main if: github.ref == 'refs/heads/main' - run: bash scripts/github/deploy_docker.sh staging - env: - DOCKERHUB_PROJECT: ${{ secrets.DOCKER_PROJECT }} - - name: Deploy Dockerhub dev + uses: docker/build-push-action@v5 + with: + push: true + tags: safeglobal/safe-wallet-web:staging + platforms: | + linux/amd64 + linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Deploy Develop if: github.ref == 'refs/heads/dev' - run: bash scripts/github/deploy_docker.sh dev - env: - DOCKERHUB_PROJECT: ${{ secrets.DOCKER_PROJECT }} - - name: Deploy Dockerhub tag - if: startsWith(github.ref, 'refs/tags/') - run: bash scripts/github/deploy_docker.sh ${GITHUB_REF##*/} - env: - DOCKERHUB_PROJECT: ${{ secrets.DOCKER_PROJECT }} + uses: docker/build-push-action@v5 + with: + push: true + tags: safeglobal/safe-wallet-web:dev + platforms: | + linux/amd64 + linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Deploy Tag + if: (github.event_name == 'release' && github.event.action == 'released') + uses: docker/build-push-action@v5 + with: + push: true + tags: | + safeglobal/safe-wallet-web:${{ github.event.release.tag_name }} + safeglobal/safe-wallet-web:latest + platforms: | + linux/amd64 + linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-production.yml similarity index 100% rename from .github/workflows/deploy-release.yml rename to .github/workflows/deploy-production.yml diff --git a/.github/workflows/e2e-ondemand.yml b/.github/workflows/e2e-ondemand.yml new file mode 100644 index 0000000000..b0f9355b55 --- /dev/null +++ b/.github/workflows/e2e-ondemand.yml @@ -0,0 +1,48 @@ +name: Regression on demand 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 Regression on demand tests + strategy: + fail-fast: false + matrix: + containers: [1, 2, 3, 4, 5] + steps: + - uses: actions/checkout@v3 + + - uses: ./.github/workflows/cypress + with: + command: npx cypress run --reporter "junit" --reporter-options "mochaFile=reports/junit-[hash].xml,testsuitesTitle=Root Suite" + secrets: ${{ toJSON(secrets) }} + spec: cypress/e2e/**/*.cy.js + group: 'Regression on demand tests' + + - name: Python setup + if: always() + uses: actions/setup-python@v3 + with: + python-version: '3.x' + + - 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*.xml" diff --git a/.github/workflows/e2e-regression.yml b/.github/workflows/e2e-regression.yml new file mode 100644 index 0000000000..f6bc878da5 --- /dev/null +++ b/.github/workflows/e2e-regression.yml @@ -0,0 +1,28 @@ +name: Regression tests + +on: + pull_request: + branches: + - 'release**' + - 'release/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + runs-on: ubuntu-20.04 + name: Cypress Regression tests + strategy: + fail-fast: false + matrix: + containers: [1, 2, 3] + steps: + - uses: actions/checkout@v3 + + - uses: ./.github/workflows/cypress + with: + secrets: ${{ toJSON(secrets) }} + spec: cypress/e2e/**/*.cy.js + group: 'Regression tests' diff --git a/.github/workflows/e2e-safe-apps.yml b/.github/workflows/e2e-safe-apps.yml new file mode 100644 index 0000000000..609fc34ab5 --- /dev/null +++ b/.github/workflows/e2e-safe-apps.yml @@ -0,0 +1,29 @@ +name: Safe Apps e2e + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + runs-on: ubuntu-latest + name: Cypress Safe Apps tests + strategy: + fail-fast: false + matrix: + containers: [1, 2] + steps: + - uses: actions/checkout@v3 + + - uses: ./.github/workflows/cypress + with: + secrets: ${{ toJSON(secrets) }} + spec: cypress/e2e/safe-apps/*.cy.js + group: 'Safe Apps tests' + project_id: okn21k + record_key: ${{ secrets.CYPRESS_SAFE_APPS_RECORD_KEY }} + diff --git a/.github/workflows/e2e-smoke.yml b/.github/workflows/e2e-smoke.yml new file mode 100644 index 0000000000..dbc80c27bb --- /dev/null +++ b/.github/workflows/e2e-smoke.yml @@ -0,0 +1,27 @@ +name: Smoke tests + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + runs-on: ubuntu-20.04 + name: Cypress Smoke tests + + strategy: + fail-fast: false + matrix: + containers: [1, 2, 3, 4, 5] + + steps: + - uses: actions/checkout@v3 + + - uses: ./.github/workflows/cypress + with: + secrets: ${{ toJSON(secrets) }} + spec: cypress/e2e/smoke/*.cy.js + group: 'Smoke tests' diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml deleted file mode 100644 index fc2c709f25..0000000000 --- a/.github/workflows/e2e.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: e2e - -on: - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - e2e: - runs-on: ubuntu-20.04 - name: Cypress Smoke tests - strategy: - fail-fast: false - matrix: - containers: [1, 2, 3] - steps: - - uses: actions/checkout@v3 - - - uses: ./.github/workflows/yarn - - - name: Install Cypress - run: | - ./node_modules/.bin/cypress install - - - uses: ./.github/workflows/build - with: - secrets: ${{ toJSON(secrets) }} - e2e_mnemonic: ${{ secrets.NEXT_PUBLIC_CYPRESS_MNEMONIC }} - - - name: Serve - run: yarn serve & - - - uses: cypress-io/github-action@v4 - with: - parallel: true - spec: cypress/e2e/smoke/*.cy.js - browser: chrome - record: true - config: baseUrl=http://localhost:8080 - group: 'Smoke tests' - env: - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/nextjs-bundle-analysis.yml b/.github/workflows/nextjs-bundle-analysis.yml new file mode 100644 index 0000000000..1840f8d380 --- /dev/null +++ b/.github/workflows/nextjs-bundle-analysis.yml @@ -0,0 +1,65 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +name: 'Next.js Bundle Analysis' + +on: + pull_request: + push: + branches: + - dev + +permissions: + contents: read # for checkout repository + actions: read # for fetching base branch bundle stats + pull-requests: write # for comments + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + uses: ./.github/workflows/yarn + + - name: Build next.js app + uses: ./.github/workflows/build + with: + secrets: ${{ toJSON(secrets) }} + + - name: Analyze bundle + run: npx -p nextjs-bundle-analysis report + + - name: Upload bundle + uses: actions/upload-artifact@v3 + with: + name: bundle + path: .next/analyze/__bundle_analysis.json + + - name: Download base branch bundle stats + uses: dawidd6/action-download-artifact@v2 + if: success() && github.event.number + with: + workflow: nextjs_bundle_analysis.yml + branch: ${{ github.event.pull_request.base.ref }} + path: .next/analyze/base + + - name: Compare with base branch bundle + if: success() && github.event.number + run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare + + - name: Get Comment Body + id: get-comment-body + if: success() && github.event.number + # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings + run: | + echo "body<> $GITHUB_OUTPUT + echo "$(cat .next/analyze/__bundle_analysis_comment.txt)" >> $GITHUB_OUTPUT + echo EOF >> $GITHUB_OUTPUT + + - name: Comment + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: next-bundle-analysis + message: ${{ steps.get-comment-body.outputs.body }} diff --git a/.github/workflows/safe-apps-e2e.yml b/.github/workflows/safe-apps-e2e.yml deleted file mode 100644 index 7053c80d4d..0000000000 --- a/.github/workflows/safe-apps-e2e.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Safe Apps e2e - -on: - pull_request: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - e2e: - runs-on: ubuntu-latest - name: Cypress Safe Apps tests - strategy: - fail-fast: false - matrix: - containers: [1, 2, 3] - steps: - - uses: actions/checkout@v3 - - - uses: ./.github/workflows/yarn - - - name: Install Cypress - run: | - ./node_modules/.bin/cypress install - - - uses: ./.github/workflows/build - with: - secrets: ${{ toJSON(secrets) }} - e2e_mnemonic: ${{ secrets.NEXT_PUBLIC_CYPRESS_MNEMONIC }} - - - name: Serve - run: yarn serve & - - - uses: cypress-io/github-action@v4 - with: - parallel: true - spec: cypress/e2e/safe-apps/*.cy.js - browser: chrome - record: true - config: baseUrl=http://localhost:8080 - group: 'Safe Apps tests' - env: - CYPRESS_PROJECT_ID: okn21k - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_SAFE_APPS_RECORD_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/unit-tests.yml similarity index 100% rename from .github/workflows/test.yml rename to .github/workflows/unit-tests.yml diff --git a/.gitignore b/.gitignore index 97916164f6..505387fd77 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,5 @@ yalc.lock /public/worker-*.js /public/workbox-*.js /public/workbox-*.js.map -/public/fallback* \ No newline at end of file +/public/fallback* +/public/*.js.LICENSE.txt \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 9e8cba5245..101be17060 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,24 @@ -FROM node:18-alpine AS base -ENV NEXT_TELEMETRY_DISABLED 1 - -FROM base AS builder - +FROM node:18-alpine RUN apk add --no-cache libc6-compat git python3 py3-pip make g++ libusb-dev eudev-dev linux-headers WORKDIR /app - -# Install dependencies -COPY package.json yarn.lock* ./ -RUN yarn --frozen-lockfile COPY . . -RUN yarn run after-install -RUN yarn build +# Fix arm64 timeouts +RUN yarn config set network-timeout 300000 && yarn global add node-gyp -# Production image -FROM base AS runner -WORKDIR /app +# install deps +RUN yarn install --frozen-lockfile +RUN yarn after-install ENV NODE_ENV production -ENV REVERSE_PROXY_UI_PORT 8080 - -RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs -COPY --from=builder /app/out ./out -# Set the correct permission for prerender cache -RUN mkdir .next && chown nextjs:nodejs .next +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +ENV NEXT_TELEMETRY_DISABLED 1 -USER nextjs +EXPOSE 3000 -EXPOSE ${REVERSE_PROXY_UI_PORT} +ENV PORT 3000 -CMD npx -y serve out -p ${REVERSE_PROXY_UI_PORT} +CMD ["yarn", "static-serve"] diff --git a/README.md b/README.md index c2bdf1146f..9ead08c0fe 100644 --- a/README.md +++ b/README.md @@ -17,37 +17,38 @@ Contributions, be it a bug report or a pull request, are very welcome. Please ch Create a `.env` file with environment variables. You can use the `.env.example` file as a reference. -Here's the list of all the required and optional variables: - -| Env variable | | Description -| ------------------------------------------------------ | ------------ | ----------- -| `NEXT_PUBLIC_INFURA_TOKEN` | optional | [Infura](https://docs.infura.io/infura/networks/ethereum/how-to/secure-a-project/project-id) RPC API token -| `NEXT_PUBLIC_SAFE_APPS_INFURA_TOKEN` | optional | Infura token for Safe Apps, falls back to `NEXT_PUBLIC_INFURA_TOKEN` -| `NEXT_PUBLIC_IS_PRODUCTION` | optional | Set to `true` to build a minified production app -| `NEXT_PUBLIC_GATEWAY_URL_PRODUCTION` | optional | The base URL for the [Safe Client Gateway](https://github.com/safe-global/safe-client-gateway) -| `NEXT_PUBLIC_GATEWAY_URL_STAGING` | optional | The base CGW URL on staging -| `NEXT_PUBLIC_SAFE_VERSION` | optional | The latest version of the Safe contract, defaults to 1.3.0 -| `NEXT_PUBLIC_WC_BRIDGE` | optional | [WalletConnect v1](https://docs.walletconnect.com/1.0/bridge-server) bridge URL, falls back to the public WC bridge -| `NEXT_PUBLIC_WC_PROJECT_ID` | optional | [WalletConnect v2](https://docs.walletconnect.com/2.0/cloud/relay) project ID -| `NEXT_PUBLIC_TENDERLY_ORG_NAME` | optional | [Tenderly](https://tenderly.co) org name for Transaction Simulation -| `NEXT_PUBLIC_TENDERLY_PROJECT_NAME` | optional | Tenderly project name -| `NEXT_PUBLIC_TENDERLY_SIMULATE_ENDPOINT_URL` | optional | Tenderly simulation URL -| `NEXT_PUBLIC_BEAMER_ID` | optional | [Beamer](https://www.getbeamer.com) is a news feed for in-app announcements -| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID` | optional | [GTM](https://tagmanager.google.com) project id -| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_DEVELOPMENT_AUTH` | optional | Dev GTM key -| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LATEST_AUTH` | optional | Preview GTM key -| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_LIVE_AUTH` | optional | Production GTM key -| `NEXT_PUBLIC_SENTRY_DSN` | optional | [Sentry](https://sentry.io) id for tracking runtime errors -| `NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_PRODUCTION` | optional | [Safe Gelato Relay Service](https://github.com/safe-global/safe-gelato-relay-service) URL to allow relaying transactions via Gelato -| `NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING` | optional | Relay URL on staging -| `NEXT_PUBLIC_IS_OFFICIAL_HOST` | optional | 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` | optional | Redefine API base URL -| `NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION` | optional | Firebase Cloud Messaging (FCM) `initializeApp` options on production -| `NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION` | optional | FCM vapid key on production -| `NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING` | optional | FCM `initializeApp` options on staging -| `NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING` | optional | FCM vapid key on staging - -If you don't provide some of the optional vars, the corresponding features will be disabled in the UI. +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_SAFE_GELATO_RELAY_SERVICE_URL_PRODUCTION` | [Safe Gelato Relay Service](https://github.com/safe-global/safe-gelato-relay-service) URL to allow relaying transactions via Gelato +| `NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING` | Relay URL on staging +| `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_SOCIAL_WALLET_OPTIONS_PRODUCTION` | Web3Auth and Google credentials (production) +| `NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING` | Web3Auth and Google credentials (staging) + +If you don't provide some of the variables, the corresponding features will be disabled in the UI. ### Running the app locally diff --git a/cypress.config.js b/cypress.config.js index ff3bf4baf8..3b57174ed1 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,15 +1,25 @@ import { defineConfig } from 'cypress' +import * as fs from 'fs' export default defineConfig({ projectId: 'exhdra', trashAssetsBeforeRuns: true, retries: { - runMode: 1, + runMode: 2, openMode: 0, }, - e2e: { + setupNodeEvents(on, config) { + on('after:spec', (spec, results) => { + if (results && results.video) { + const failures = results.tests.some((test) => test.attempts.some((attempt) => attempt.state === 'failed')) + if (!failures) { + fs.unlinkSync(results.video) + } + } + }) + }, baseUrl: 'http://localhost:3000', testIsolation: false, hideXHR: true, diff --git a/cypress/e2e/create_safe.cy.js b/cypress/e2e/create_safe.cy.js deleted file mode 100644 index 3906b002d2..0000000000 --- a/cypress/e2e/create_safe.cy.js +++ /dev/null @@ -1,21 +0,0 @@ -describe('Create Safe', () => { - it('should create a new Safe', () => { - cy.visit('/welcome') - - // Close cookie banner - cy.contains('button', 'Accept all').click() - - // Ensure wallet is connected to correct chain via header - cy.contains('E2E Wallet @ Goerli') - - cy.contains('Create new Safe').click() - - // Name - cy.wait(1000) // Wait for form default values to populate - cy.contains('button', 'Next').click() - - // Owners and confirmations - cy.wait(1000) // Wait for form default values to populate - cy.contains('button', 'Next').click() - }) -}) diff --git a/cypress/e2e/custom_apps.cy.js b/cypress/e2e/custom_apps.cy.js deleted file mode 100644 index 4c008309bc..0000000000 --- a/cypress/e2e/custom_apps.cy.js +++ /dev/null @@ -1,29 +0,0 @@ -const appUrl = 'https://safe-custom-app.com' - -describe('When visiting a custom Safe App', () => { - before(() => { - cy.fixture('safe-app').then((html) => { - cy.intercept('GET', `${appUrl}`, html) - cy.intercept('GET', `${appUrl}/manifest.json`, { - name: 'Cypress Test App', - description: 'Cypress Test App Description', - icons: [{ src: 'logo.svg', sizes: 'any', type: 'image/svg+xml' }], - }) - }) - - cy.visitSafeApp(`${appUrl}`) - cy.wait(1000) - cy.contains('button', 'Accept all').click() - }) - - it('should show the custom app warning', () => { - cy.findByRole('heading', { content: /warning/i }) - cy.findByText('https://safe-custom-app.com') - cy.findByText('Check the link you are using') - - cy.findByRole('checkbox').should('exist').click() - cy.findByRole('button', { name: /continue/i }).click() - cy.reload() - cy.findByRole('heading', { content: /warning/i }).should('not.exist') - }) -}) diff --git a/cypress/e2e/intercom.cy.js b/cypress/e2e/intercom.cy.js deleted file mode 100644 index 0067f6e812..0000000000 --- a/cypress/e2e/intercom.cy.js +++ /dev/null @@ -1,44 +0,0 @@ -const RINKEBY_TEST_SAFE = 'rin:0xFfDC1BcdeC18b1196e7FA04246295DE3A17972Ac' -const intercomIframe = 'iframe[id="intercom-frame"]' -const intercomButton = '[aria-label="Open Intercom Messenger"]' -const fakeIntercomButton = 'img[alt="Open Intercom"]' - -describe('Intercom and cookie prefs', () => { - it('should show the Intercom chat if cookies are enabled', () => { - cy.visit('/') - - // Don't accept Intercom cookies - cy.contains('a', 'Accept selection').click() - - // Click on the Intercom button - cy.get(fakeIntercomButton).click() - - // Cookie preferences appear - cy.contains( - 'You attempted to open the customer support chat. Please accept the community support & updates cookies', - ) - - cy.contains('a', 'Accept all').click() - - // Intercom is now active - cy.get(intercomIframe).should('exist') - cy.get(fakeIntercomButton).should('not.exist') - cy.get(intercomButton).click() - cy.get('#intercom-container').should('exist') - - // Intercom should be disabled on a Safe App page - cy.visit(`/${RINKEBY_TEST_SAFE}/apps?appUrl=https://safe-apps.dev.gnosisdev.com/drain-safe`) - cy.get(intercomButton).should('not.exist') - - // Go to Settings and change the cookie settings - cy.visit(`/${RINKEBY_TEST_SAFE}/settings`) - cy.get(intercomIframe).should('exist') - cy.contains('button', 'Preferences').click() - cy.contains('Community support & updates').click() - cy.contains('a', 'Accept selection').click() - - // Intercom should be now disabled - cy.get(intercomButton).should('not.exist') - cy.get(fakeIntercomButton).should('be.visible') - }) -}) diff --git a/cypress/e2e/non_owner_spending_limit.cy.js b/cypress/e2e/non_owner_spending_limit.cy.js deleted file mode 100644 index 581a9f426b..0000000000 --- a/cypress/e2e/non_owner_spending_limit.cy.js +++ /dev/null @@ -1,54 +0,0 @@ -const HW_WALLET = '0xff6E053fBf4E5895eDec09146Bc10f705E8c4b3D' -const SPENDING_LIMIT_SAFE = 'gor:0xBE3C5aFF7f66c23fe71c3047911f9Aa0026b281B' - -describe('Check non-owner spending limit beneficiary modal', () => { - before(() => { - cy.visit(`/${SPENDING_LIMIT_SAFE}/home`, { failOnStatusCode: false }) - - cy.contains('Accept selection').click() - }) - - it('should open the spending limit modal', () => { - // Assert that "New transaction" button is visible - cy.contains('New transaction', { - timeout: 60_000, // `lastWallet` takes a while initialize in CI - }).should('be.visible') - - // Open the new transaction modal - cy.contains('New transaction').click() - }) - - it('should draft a spending limit transaction', () => { - // Modal is open - cy.contains('h2', 'New transaction').should('be.visible') - - cy.contains('Send tokens').click() - - // Fill transaction data - cy.get('input[name="recipient"]').type(SPENDING_LIMIT_SAFE) - - // Click on the Token selector - cy.get('input[name="tokenAddress"]').prev().click() - cy.get('ul[role="listbox"]').contains('Görli Ether').click() - - // Insert max amount - cy.contains('Spending Limit Transaction (100 GOR)').click() - - // Insert max amount - cy.contains('Max').click() - - cy.contains('Next').click() - }) - - it('should review the spending limit transaction', () => { - cy.contains( - 'Spending limit transactions only appear in the interface once they are successfully processed and indexed. Pending transactions can only be viewed in your signer wallet application or under your wallet address on a Blockchain Explorer.', - ) - - // Alias for New transaction modal - cy.contains('h2', 'Review transaction').parents('div').as('modal') - - // Estimation is loaded - cy.get('button[type="submit"]').should('not.be.disabled') - }) -}) diff --git a/cypress/e2e/pages/balances.pages.js b/cypress/e2e/pages/balances.pages.js index 0cb82caeb1..ebd08d0de9 100644 --- a/cypress/e2e/pages/balances.pages.js +++ b/cypress/e2e/pages/balances.pages.js @@ -22,7 +22,7 @@ const hiddenTokenIcon = 'svg[data-testid="VisibilityOffOutlinedIcon"]' const hideTokenDefaultString = 'Hide tokens' const assetNameSortBtnStr = 'Asset' const assetBalanceSortBtnStr = 'Balance' -const sendBtnStr = 'Send' +export const sendBtnStr = 'Send' const sendTokensStr = 'Send tokens' const pageRowsDefault = '25' diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index ba4f9f8599..38ff86234e 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -1,4 +1,5 @@ import * as constants from '../../support/constants' +import * as main from '../pages/main.page' const welcomeLoginScreen = '[data-testid="welcome-login"]' const expandMoreIcon = 'svg[data-testid="ExpandMoreIcon"]' @@ -11,12 +12,69 @@ export const removeOwnerBtn = 'button[aria-label="Remove owner"]' const connectingContainer = 'div[class*="connecting-container"]' const createNewSafeBtn = 'span[data-track="create-safe: Continue to creation"]' const connectWalletBtn = 'Connect wallet' - +const googleConnectBtn = '[data-testid="google-connect-btn"]' +const googleSignedinBtn = '[data-testid="signed-in-account-btn"]' +export const accountInfoHeader = '[data-testid="open-account-center"]' +const reviewStepOwnerInfo = '[data-testid="review-step-owner-info"]' +const reviewStepNextBtn = '[data-testid="review-step-next-btn"]' +const safeCreationStatusInfo = '[data-testid="safe-status-info"]' +const startUsingSafeBtn = '[data-testid="start-using-safe-btn"]' +const sponsorIcon = '[data-testid="sponsor-icon"]' +const networkFeeSection = '[data-tetid="network-fee-section"]' +const nextBtn = '[data-testid="next-btn"]' +const backBtn = '[data-testid="back-btn"]' + +const sponsorStr = 'Your account is sponsored by Goerli' +const safeCreationProcessing = 'Transaction is being executed' +const safeCreationComplete = 'Your Safe Account is being indexed' const changeNetworkWarningStr = 'Change your wallet network' const safeAccountSetupStr = 'Safe Account setup' const policy1_2 = '1/1 policy' export const walletName = 'test1-sepolia-safe' export const defaltSepoliaPlaceholder = 'Sepolia Safe' +const welcomeToSafeStr = 'Welcome to Safe' + +export function clickOnBackBtn() { + cy.get(backBtn).should('be.enabled').click() +} +export function verifySafeIsBeingCreated() { + cy.get(safeCreationStatusInfo).should('have.text', safeCreationProcessing) +} + +export function verifySafeCreationIsComplete() { + cy.get(safeCreationStatusInfo).should('exist').and('have.text', safeCreationComplete) + cy.get(startUsingSafeBtn).should('exist').click() + cy.get(welcomeToSafeStr).should('exist') +} + +export function clickOnReviewStepNextBtn() { + cy.get(reviewStepNextBtn).click() +} +export function verifyOwnerInfoIsPresent() { + return cy.get(reviewStepOwnerInfo).shoul('exist') +} + +export function verifySponsorMessageIsPresent() { + main.verifyElementsExist([sponsorIcon, networkFeeSection]) + // Goerli is generated + cy.get(networkFeeSection).contains(sponsorStr).should('exist') +} + +export function verifyGoogleConnectBtnIsDisabled() { + cy.get(googleConnectBtn).should('be.disabled') +} + +export function verifyGoogleConnectBtnIsEnabled() { + cy.get(googleConnectBtn).should('not.be.disabled') +} + +export function verifyGoogleSignin() { + return cy.get(googleSignedinBtn).should('exist') +} + +export function verifyGoogleAccountInfoInHeader() { + return cy.get(accountInfoHeader).should('exist') +} export function verifyPolicy1_1() { cy.contains(policy1_2).should('exist') @@ -75,7 +133,7 @@ export function selectNetwork(network, regex = false) { } export function clickOnNextBtn() { - cy.contains('button', 'Next').click() + cy.get(nextBtn).should('be.enabled').click() } export function verifyOwnerName(name, index) { diff --git a/cypress/e2e/pages/main.page.js b/cypress/e2e/pages/main.page.js index 438797b93c..5cd87d4d05 100644 --- a/cypress/e2e/pages/main.page.js +++ b/cypress/e2e/pages/main.page.js @@ -1,11 +1,26 @@ import * as constants from '../../support/constants' const acceptSelection = 'Accept selection' +const executeStr = 'Execute' +export const modalDialogCloseBtn = '[data-testid="modal-dialog-close-btn"]' +export function clickOnExecuteBtn() { + cy.get('button').contains(executeStr).click() +} export function clickOnSideMenuItem(item) { cy.get('p').contains(item).click() } +export function waitForTrnsactionHistoryToComplete() { + cy.intercept('GET', constants.transactionHistoryEndpoint).as('History') + cy.wait('@History') +} + +export function waitForSafeListRequestToComplete() { + cy.intercept('GET', constants.safeListEndpoint).as('Safes') + cy.wait('@Safes') +} + export function acceptCookies(index = 0) { cy.wait(1000) diff --git a/cypress/e2e/pages/navigation.page.js b/cypress/e2e/pages/navigation.page.js new file mode 100644 index 0000000000..8dd0ab339f --- /dev/null +++ b/cypress/e2e/pages/navigation.page.js @@ -0,0 +1,7 @@ +export const sideNavSettingsIcon = '[data-testid="settings-nav-icon"]' +export const setupSection = '[data-testid="setup-section"]' +export const modalBackBtn = '[data-testid="modal-back-btn"]' + +export function clickOnSideNavigation(option) { + cy.get(option).should('exist').click() +} diff --git a/cypress/e2e/pages/nfts.pages.js b/cypress/e2e/pages/nfts.pages.js index 85775098cb..82796786f6 100644 --- a/cypress/e2e/pages/nfts.pages.js +++ b/cypress/e2e/pages/nfts.pages.js @@ -1,8 +1,22 @@ import * as constants from '../../support/constants' +import * as main from '../pages/main.page' -const nftModal = 'div[role="dialog"]' -const nftModalCloseBtn = 'button[aria-label="close"]' +const nftModalTitle = '[data-testid="modal-title"]' +const nftModal = '[data-testid="modal-view"]' + +const nftModalCloseBtn = main.modalDialogCloseBtn const recipientInput = 'input[name="recipient"]' +const nftsRow = '[data-testid^="nfts-table-row"]' +const inactiveNftIcon = '[data-testid="nft-icon-border"]' +const activeNftIcon = '[data-testid="nft-icon-primary"]' +const nftCheckBox = (index) => `[data-testid="nft-checkbox-${index}"] > input` +const activeSendNFTBtn = '[data-testid="nft-send-btn-false"]' +const modalTitle = '[data-testid="modal-title"]' +const modalHeader = '[data-testid="modal-header"]' +const modalSelectedNFTs = '[data-testid="selected-nfts"]' +const nftItemList = '[data-testid="nft-item-list"]' +const nftItemNane = '[data-testid="nft-item-name"]' +const signBtn = '[data-testid="sign-btn"]' const noneNFTSelected = '0 NFTs selected' const sendNFTStr = 'Send NFTs' @@ -19,7 +33,8 @@ export function clickOnNftsTab() { cy.get('p').contains('NFTs').click() } function verifyTableRows(number) { - cy.get('tbody tr').should('have.length', number) + cy.scrollTo('bottom').wait(500) + cy.get(nftsRow).should('have.length.at.least', number) } export function verifyNFTNumber(number) { @@ -27,44 +42,48 @@ export function verifyNFTNumber(number) { } export function verifyDataInTable(name, address, tokenID) { - cy.get('tbody tr:first-child').contains('td:first-child', name) - cy.get('tbody tr:first-child').contains('td:first-child', address) - cy.get('tbody tr:first-child').contains('td:nth-child(2)', tokenID) + cy.get(nftsRow).contains(name) + cy.get(nftsRow).contains(address) + cy.get(nftsRow).contains(tokenID) } -export function openNFT(index) { - cy.get('tbody').within(() => { - cy.get('tr').eq(index).click() - }) +export function waitForNftItems(count) { + cy.get(nftsRow).should('have.length.at.least', count) +} + +export function openActiveNFT(index) { + cy.get(activeNftIcon).eq(index).click() } export function verifyNameInNFTModal(name) { - cy.get(nftModal).contains(name) + cy.get(nftModalTitle).contains(name) } export function verifySelectedNetwrokSepolia() { - cy.get(nftModal).contains(constants.networks.sepolia) + cy.get(nftModal).within(() => { + cy.get(nftModalTitle).contains(constants.networks.sepolia) + }) } export function verifyNFTModalLink(link) { - cy.get(nftModal).contains(`a[href="${link}"]`, 'View on OpenSea') + cy.get(nftModalTitle).contains(`a[href="${link}"]`, 'View on OpenSea') } export function closeNFTModal() { cy.get(nftModalCloseBtn).click() - cy.get(nftModal).should('not.exist') + cy.get(nftModalTitle).should('not.exist') } -export function clickOn6thNFT() { - cy.get('tbody tr:nth-child(6) td:nth-child(2)').click() +export function clickOnInactiveNFT() { + cy.get(inactiveNftIcon).eq(0).click() } export function verifyNFTModalDoesNotExist() { - cy.get(nftModal).should('not.exist') + cy.get(nftModalTitle).should('not.exist') } export function selectNFTs(numberOfNFTs) { for (let i = 1; i <= numberOfNFTs; i++) { - cy.get(`tbody tr:nth-child(${i}) input[type="checkbox"]`).click() + cy.get(nftCheckBox(i)).click() cy.contains(`${i} NFT${i > 1 ? 's' : ''} selected`) } cy.contains('button', `Send ${numberOfNFTs} NFT${numberOfNFTs > 1 ? 's' : ''}`) @@ -73,8 +92,8 @@ export function selectNFTs(numberOfNFTs) { export function deselectNFTs(checkboxIndexes, checkedItems) { let total = checkedItems - checkboxIndexes.length - checkboxIndexes.forEach((index) => { - cy.get(`tbody tr:nth-child(${index}) input[type="checkbox"]`).uncheck() + checkboxIndexes.forEach((i) => { + cy.get(nftCheckBox(i)).uncheck() }) cy.contains(`${total} NFT${total !== 1 ? 's' : ''} selected`) @@ -88,14 +107,12 @@ export function verifyInitialNFTData() { cy.contains('button[disabled]', 'Send') } -export function sendNFT(numberOfCheckedNFTs) { - cy.contains('button', `Send ${numberOfCheckedNFTs} NFT${numberOfCheckedNFTs !== 1 ? 's' : ''}`).click() +export function sendNFT() { + cy.get(activeSendNFTBtn).click() } export function verifyNFTModalData() { - cy.contains(sendNFTStr) - cy.contains(recipientAddressStr) - cy.contains(selectedNFTStr) + main.verifyElementsExist([modalTitle, modalHeader, modalSelectedNFTs]) } export function typeRecipientAddress(address) { @@ -107,11 +124,10 @@ export function clikOnNextBtn() { } export function verifyReviewModalData(NFTcount) { - cy.contains(sendStr) - cy.contains(toStr) - cy.wait(1000) - cy.get(`b:contains(${transferFromStr})`).should('have.length', NFTcount) - cy.contains('button:not([disabled])', signBtnStr) + main.verifyElementsExist([nftItemList]) + main.verifyElementsCount(nftItemNane, NFTcount) + cy.get(signBtn).should('not.be.disabled') + if (NFTcount > 1) { const numbersArr = Array.from({ length: NFTcount }, (_, index) => index + 1) numbersArr.forEach((number) => { diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index 6e2e30aef3..f0276a325b 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -1,11 +1,12 @@ import * as constants from '../../support/constants' import * as main from '../pages/main.page' +import * as createWallet from '../pages/create_wallet.pages' +import * as navigation from '../pages/navigation.page' const copyToClipboardBtn = 'button[aria-label="Copy to clipboard"]' const tooltipLabel = (label) => `span[aria-label="${label}"]` const removeOwnerBtn = 'span[data-track="settings: Remove owner"] > span > button' const replaceOwnerBtn = 'span[data-track="settings: Replace owner"] > span > button' -const addOwnerBtn = 'span[data-track="settings: Add owner"] > button' const tooltip = 'div[role="tooltip"]' const expandMoreIcon = 'svg[data-testid="ExpandMoreIcon"]' const sentinelStart = 'div[data-testid="sentinelStart"]' @@ -20,22 +21,29 @@ const thresholdOption = 'li[role="option"]' const existingOwnerAddressInput = (index) => `input[name="owners.${index}.address"]` const existingOwnerNameInput = (index) => `input[name="owners.${index}.name"]` const singleOwnerNameInput = 'input[name="name"]' +const finishTransactionBtn = '[data-testid="finish-transaction-btn"]' +const addOwnerBtn = '[data-testid="add-owner-btn"]' +const addOwnerNextBtn = '[data-testid="add-owner-next-btn"]' const disconnectBtnStr = 'Disconnect' const notConnectedStatus = 'Connect' const e2eWalletStr = 'E2E Wallet' const max50charsLimitStr = 'Maximum 50 symbols' -const nextBtnStr = 'Next' const executeBtnStr = 'Execute' const backbtnStr = 'Back' const removeOwnerStr = 'Remove owner' const selectedOwnerStr = 'Selected owner' const addNewOwnerStr = 'Add new owner' +const processedTransactionStr = 'Transaction was successful' export const safeAccountNonceStr = 'Safe Account nonce' export const nonOwnerErrorMsg = 'Your connected wallet is not an owner of this Safe Account' export const disconnectedUserErrorMsg = 'Please connect your wallet' +export function verifyOwnerTransactionComplted() { + cy.get(processedTransactionStr).should('exist') + cy.get(finishTransactionBtn).should('exist') +} export function verifyNumberOfOwners(count) { const indices = Array.from({ length: count }, (_, index) => index) const names = indices.map(existingOwnerNameInput) @@ -154,12 +162,11 @@ export function clickOnConnectBtn() { } export function waitForConnectionStatus() { - cy.get('div').contains(e2eWalletStr) + cy.get(createWallet.accountInfoHeader).should('exist') } export function openAddOwnerWindow() { - cy.get('span').contains(addNewOwnerStr).click() - cy.wait(1000) + cy.get(addOwnerBtn).should('be.enabled').click() cy.get(newOwnerName).should('be.visible') cy.get(newOwnerAddress).should('be.visible') } @@ -197,11 +204,11 @@ export function verifyNewOwnerName(name) { } export function clickOnNextBtn() { - cy.get('button').contains(nextBtnStr).click() + cy.get(addOwnerNextBtn).should('be.enabled').click() } export function clickOnBackBtn() { - cy.get('button').contains(backbtnStr).click() + cy.get(navigation.modalBackBtn).should('be.enabled').click() } export function verifyConfirmTransactionWindowDisplayed() { diff --git a/cypress/e2e/pages/safeapps.pages.js b/cypress/e2e/pages/safeapps.pages.js index fb7401609b..f349d2034e 100644 --- a/cypress/e2e/pages/safeapps.pages.js +++ b/cypress/e2e/pages/safeapps.pages.js @@ -7,6 +7,7 @@ export const contractMethodIndex = '[name="contractMethodIndex"]' export const saveToLibraryBtn = 'button[title="Save to Library"]' export const downloadBatchBtn = 'button[title="Download batch"]' export const deleteBatchBtn = 'button[title="Delete Batch"]' +const appModal = '[data-testid="app-info-modal"]' const addBtnStr = /add/i const noAppsStr = /no Safe Apps found/i @@ -208,9 +209,16 @@ function verifyDisclaimerIsVisible() { } export function clickOnContinueBtn() { + cy.get(appModal).should('exist') return cy.findByRole('button', { name: continueBtnStr }).click().wait(1000) } +export function checkLocalStorage() { + clickOnContinueBtn().should(() => { + expect(window.localStorage.getItem(constants.BROWSER_PERMISSIONS_KEY)).to.eq(localStorageItem) + }) +} + export function verifyCameraCheckBoxExists() { cy.findByRole('checkbox', { name: cameraCheckBoxStr }).should('exist') } diff --git a/cypress/e2e/regression/add_owner.cy.js b/cypress/e2e/regression/add_owner.cy.js new file mode 100644 index 0000000000..2bb15fc757 --- /dev/null +++ b/cypress/e2e/regression/add_owner.cy.js @@ -0,0 +1,55 @@ +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' + +describe('Add Owners tests', () => { + beforeEach(() => { + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) + cy.clearLocalStorage() + main.acceptCookies() + cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) + }) + + it('Verify Tooltip displays correct message for disconnected user', () => { + owner.waitForConnectionStatus() + owner.clickOnWalletExpandMoreIcon() + owner.clickOnDisconnectBtn() + owner.verifyAddOwnerBtnIsDisabled() + }) + + it('Verify the Add New Owner Form can be opened', () => { + owner.waitForConnectionStatus() + owner.openAddOwnerWindow() + }) + + it('Verify error message displayed if character limit is exceeded in Name input', () => { + owner.waitForConnectionStatus() + owner.openAddOwnerWindow() + owner.typeOwnerName(main.generateRandomString(51)) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) + }) + + it('Verify that the "Name" field is auto-filled with the relevant name from Address Book', () => { + cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) + 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) + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) + owner.waitForConnectionStatus() + owner.openAddOwnerWindow() + owner.typeOwnerAddress(constants.addresBookContacts.user1.address) + owner.selectNewOwner(constants.addresBookContacts.user1.name) + owner.verifyNewOwnerName(constants.addresBookContacts.user1.name) + }) + + it('Verify that Name field not mandatory', () => { + owner.waitForConnectionStatus() + owner.openAddOwnerWindow() + owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) + owner.clickOnNextBtn() + owner.verifyConfirmTransactionWindowDisplayed() + }) +}) diff --git a/cypress/e2e/regression/address_book.cy.js b/cypress/e2e/regression/address_book.cy.js new file mode 100644 index 0000000000..bd7ac23a16 --- /dev/null +++ b/cypress/e2e/regression/address_book.cy.js @@ -0,0 +1,65 @@ +import 'cypress-file-upload' +const path = require('path') +import { format } from 'date-fns' +import * as constants from '../../support/constants' +import * as addressBook from '../../e2e/pages/address_book.page' +import * as main from '../../e2e/pages/main.page' + +const NAME = 'Owner1' +const EDITED_NAME = 'Edited Owner1' + +describe('Address book tests', () => { + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) + main.acceptCookies() + }) + + //TODO: Use localstorage for setting up/deleting entries + it('Verify entered entry in Name input can be saved', () => { + addressBook.clickOnCreateEntryBtn() + addressBook.addEntry(NAME, constants.RECIPIENT_ADDRESS) + addressBook.clickOnEditEntryBtn() + addressBook.typeInNameInput(EDITED_NAME) + addressBook.clickOnSaveButton() + addressBook.verifyNameWasChanged(NAME, EDITED_NAME) + }) + + //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') + }) + + // TODO: Rework with localstorage. Change title in Testrail. New title "...exported" + it('Verify the address book file can be downloaded', () => { + addressBook.clickOnImportFileBtn() + addressBook.importFile() + // Download the export file + const date = format(new Date(), 'yyyy-MM-dd', { timeZone: 'UTC' }) + const fileName = `safe-address-book-${date}.csv` //name that is given to the file automatically + + addressBook.clickOnExportFileBtn() + //This is the submit button for the Export modal. It requires an actuall class or testId to differentiate + //from the Export button at the top of the AB table + 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') + // TODO: Add verifications on address and chain from file, import and export files should be equal including all chains from origin + }) +}) diff --git a/cypress/e2e/regression/assets.cy.js b/cypress/e2e/regression/assets.cy.js new file mode 100644 index 0000000000..d50cb2be18 --- /dev/null +++ b/cypress/e2e/regression/assets.cy.js @@ -0,0 +1,185 @@ +import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +import * as balances from '../pages/balances.pages' +import * as owner from '../pages/owners.pages' + +const ASSET_NAME_COLUMN = 0 +const TOKEN_AMOUNT_COLUMN = 1 +const FIAT_AMOUNT_COLUMN = 2 + +describe('Assets tests', () => { + const fiatRegex = balances.fiatRegex + + beforeEach(() => { + cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) + cy.clearLocalStorage() + main.acceptCookies() + }) + + it('Verify that non-native tokens are present and have balance', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.verifyBalance(balances.currencyDaiCap, TOKEN_AMOUNT_COLUMN, balances.currencyDaiAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyDaiCap, + balances.currencyDaiFormat_2, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + balances.verifyBalance(balances.currencyAave, TOKEN_AMOUNT_COLUMN, balances.currencyAaveAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyAave, + balances.currentcyAaveFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + balances.verifyBalance(balances.currencyLink, TOKEN_AMOUNT_COLUMN, balances.currencyLinkAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyLink, + balances.currentcyLinkFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + balances.verifyBalance(balances.currencyTestTokenA, TOKEN_AMOUNT_COLUMN, balances.currencyTestTokenAAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyTestTokenA, + balances.currentcyTestTokenAFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + balances.verifyBalance(balances.currencyTestTokenB, TOKEN_AMOUNT_COLUMN, balances.currencyTestTokenBAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyTestTokenB, + balances.currentcyTestTokenBFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + + balances.verifyBalance(balances.currencyUSDC, TOKEN_AMOUNT_COLUMN, balances.currencyTestUSDCAlttext) + balances.verifyTokenBalanceFormat( + balances.currencyUSDC, + balances.currentcyTestUSDCFormat, + TOKEN_AMOUNT_COLUMN, + FIAT_AMOUNT_COLUMN, + fiatRegex, + ) + }) + + it('Verify that every token except the native token has a "go to blockexplorer link"', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + // Specifying true for Sepolia. Will delete the flag once completely migrate to Sepolia + balances.verifyAssetNameHasExplorerLink(balances.currencyUSDC, ASSET_NAME_COLUMN, true) + balances.verifyAssetNameHasExplorerLink(balances.currencyTestTokenB, ASSET_NAME_COLUMN, true) + balances.verifyAssetNameHasExplorerLink(balances.currencyTestTokenA, ASSET_NAME_COLUMN, true) + balances.verifyAssetNameHasExplorerLink(balances.currencyLink, ASSET_NAME_COLUMN, true) + balances.verifyAssetNameHasExplorerLink(balances.currencyAave, ASSET_NAME_COLUMN, true) + balances.verifyAssetNameHasExplorerLink(balances.currencyDaiCap, ASSET_NAME_COLUMN, true) + balances.verifyAssetExplorerLinkNotAvailable(constants.tokenNames.sepoliaEther, ASSET_NAME_COLUMN) + }) + + it('Verify the default Fiat currency and the effects after changing it', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.verifyFirstRowDoesNotContainCurrency(balances.currencyEUR, FIAT_AMOUNT_COLUMN) + balances.verifyFirstRowContainsCurrency(balances.currencyUSD, FIAT_AMOUNT_COLUMN) + balances.clickOnCurrencyDropdown() + balances.selectCurrency(balances.currencyEUR) + balances.verifyFirstRowDoesNotContainCurrency(balances.currencyUSD, FIAT_AMOUNT_COLUMN) + balances.verifyFirstRowContainsCurrency(balances.currencyEUR, FIAT_AMOUNT_COLUMN) + }) + + it('Verify that a tool tip is shown pointing to "Token list" dropdown', () => { + //Spam warning message is removed in beforeEach hook + cy.reload() + }) + + it('Verify that checking the checkboxes increases the token selected counter', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.checkTokenCounter(1) + }) + + it('Verify that selecting tokens and saving hides them from the table', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.saveHiddenTokenSelection() + main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink]) + }) + + it('Verify that Cancel closes the menu and does not change the table status', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.clickOnTokenCheckbox(balances.currencyAave) + balances.saveHiddenTokenSelection() + main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink, balances.currencyAave]) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.clickOnTokenCheckbox(balances.currencyAave) + balances.cancelSaveHiddenTokenSelection() + main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink, balances.currencyAave]) + }) + + it('Verify that Deselect All unchecks all tokens from the list', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.clickOnTokenCheckbox(balances.currencyAave) + balances.deselecAlltHiddenTokenSelection() + balances.verifyEachRowHasCheckbox(constants.checkboxStates.unchecked) + }) + + it('Verify the Hidden tokens counter works for spam tokens', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(balances.currencyLink) + balances.saveHiddenTokenSelection() + balances.checkHiddenTokenBtnCounter(1) + }) + + it('Verify the Hidden tokens counter works for native tokens', () => { + balances.openHideTokenMenu() + balances.clickOnTokenCheckbox(constants.tokenNames.sepoliaEther) + balances.saveHiddenTokenSelection() + balances.checkHiddenTokenBtnCounter(1) + }) + + it('Verify you can hide tokens from the eye icon in the table rows', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.hideAsset(balances.currencyLink) + }) + + it('Verify the sorting of "Assets" and "Balance" in the table', () => { + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.verifyTableRows(7) + balances.clickOnTokenNameSortBtn() + balances.verifyTokenNamesOrder() + balances.clickOnTokenNameSortBtn() + balances.verifyTokenNamesOrder('descending') + balances.clickOnTokenBalanceSortBtn() + balances.verifyTokenBalanceOrder() + balances.clickOnTokenBalanceSortBtn() + balances.verifyTokenBalanceOrder('descending') + }) + + // TODO: New title: "Verify that when owner is disconnected, Send button is disabled". + // Modify test accordingly. Include in smoke. + it('Verify that the Send button shows when hovering a row', () => { + owner.clickOnWalletExpandMoreIcon() + owner.clickOnDisconnectBtn() + balances.selectTokenList(balances.tokenListOptions.allTokens) + balances.showSendBtn(0) + owner.verifyTooltiptext(owner.disconnectedUserErrorMsg) + // Removed the part that checks for a non owner error message in the tooltip + // because the safe has no assets, and we don't have a safe with assets where e2e wallet is not an owner + }) +}) diff --git a/cypress/e2e/smoke/balances.cy.js b/cypress/e2e/regression/balances.cy.js similarity index 100% rename from cypress/e2e/smoke/balances.cy.js rename to cypress/e2e/regression/balances.cy.js diff --git a/cypress/e2e/smoke/balances_pagination.cy.js b/cypress/e2e/regression/balances_pagination.cy.js similarity index 100% rename from cypress/e2e/smoke/balances_pagination.cy.js rename to cypress/e2e/regression/balances_pagination.cy.js diff --git a/cypress/e2e/regression/batch_tx.cy.js b/cypress/e2e/regression/batch_tx.cy.js new file mode 100644 index 0000000000..7ae147fee7 --- /dev/null +++ b/cypress/e2e/regression/batch_tx.cy.js @@ -0,0 +1,33 @@ +import * as batch from '../pages/batches.pages' +import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +import * as owner from '../../e2e/pages/owners.pages.js' + +const currentNonce = 3 +const funds_first_tx = '0.001' +const funds_second_tx = '0.002' + +describe('Batch transaction tests', () => { + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) + 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) + }) + + it('Verify a second transaction can be added to the batch', () => { + batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) + cy.wait(1000) + batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) + batch.verifyBatchIconCount(2) + batch.clickOnBatchCounter() + batch.verifyAmountTransactionsInBatch(2) + }) +}) diff --git a/cypress/e2e/smoke/beamer.cy.js b/cypress/e2e/regression/beamer.cy.js similarity index 100% rename from cypress/e2e/smoke/beamer.cy.js rename to cypress/e2e/regression/beamer.cy.js diff --git a/cypress/e2e/regression/create_safe_google.cy.js b/cypress/e2e/regression/create_safe_google.cy.js new file mode 100644 index 0000000000..f29ded27e1 --- /dev/null +++ b/cypress/e2e/regression/create_safe_google.cy.js @@ -0,0 +1,50 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as createwallet from '../pages/create_wallet.pages' +import * as owner from '../pages/owners.pages' +import * as navigation from '../pages/navigation.page' + +describe('Safe creation Google tests', () => { + beforeEach(() => { + cy.visit(constants.welcomeUrl + '?chain=gor') + cy.clearLocalStorage() + main.acceptCookies() + // TODO: Need credentials to finish API Google login + // createwallet.loginGoogleAPI() + }) + + it('Verify that "Connect with Google" option is disabled for the networks without Relay on the Welcome page', () => { + owner.clickOnWalletExpandMoreIcon() + owner.clickOnDisconnectBtn() + createwallet.selectNetwork(constants.networks.polygon) + createwallet.verifyGoogleConnectBtnIsDisabled() + }) + + it.skip('Verify a successful connection with google', () => { + createwallet.verifyGoogleSignin() + }) + + it.skip('Verify Google account info in the header after account connection', () => { + createwallet.verifyGoogleAccountInfoInHeader() + }) + + it.skip('Verify a successful safe creation with a Google account', { defaultCommandTimeout: 90000 }, () => { + createwallet.verifyGoogleSignin().click() + createwallet.verifyOwnerInfoIsPresent() + createwallet.clickOnReviewStepNextBtn() + createwallet.verifySafeIsBeingCreated() + createwallet.verifySafeCreationIsComplete() + }) + + it.skip('Verify a successful transaction creation with Google account', { defaultCommandTimeout: 90000 }, () => { + createwallet.verifyGoogleSignin().click() + createwallet.clickOnReviewStepNextBtn() + createwallet.verifySafeCreationIsComplete() + navigation.clickOnSideNavigation(navigation.sideNavSettingsIcon) + owner.openAddOwnerWindow() + owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) + owner.clickOnNextBtn() + main.clickOnExecuteBtn() + owner.verifyOwnerTransactionComplted() + }) +}) diff --git a/cypress/e2e/regression/create_safe_simple.cy.js b/cypress/e2e/regression/create_safe_simple.cy.js new file mode 100644 index 0000000000..f8627e89d9 --- /dev/null +++ b/cypress/e2e/regression/create_safe_simple.cy.js @@ -0,0 +1,123 @@ +import * as constants from '../../support/constants' +import * as main from '../../e2e/pages/main.page' +import * as createwallet from '../pages/create_wallet.pages' +import * as owner from '../pages/owners.pages' + +describe('Safe creation tests', () => { + beforeEach(() => { + cy.visit(constants.welcomeUrl + '?chain=sep') + main.waitForSafeListRequestToComplete() + cy.clearLocalStorage() + main.acceptCookies() + }) + + it('Verify Next button is disabled until switching to network is done', () => { + owner.waitForConnectionStatus() + createwallet.selectNetwork(constants.networks.ethereum) + createwallet.clickOnCreateNewSafeBtn() + createwallet.checkNetworkChangeWarningMsg() + createwallet.verifyNextBtnIsDisabled() + createwallet.selectNetwork(constants.networks.sepolia) + createwallet.verifyNextBtnIsEnabled() + }) + + // TODO: Check unit tests + it('Verify error message is displayed if wallet name input exceeds 50 characters', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.typeWalletName(main.generateRandomString(51)) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) + createwallet.clearWalletName() + }) + + // TODO: Replace wallet with Safe + // TODO: Check unit tests + it('Verify there is no error message is displayed if wallet name input contains less than 50 characters', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.typeWalletName(main.generateRandomString(50)) + owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) + }) + + it('Verify current connected account is shown as default owner', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.clickOnNextBtn() + owner.verifyExistingOwnerAddress(0, constants.DEFAULT_OWNER_ADDRESS) + }) + + // TODO: Check unit tests + it('Verify error message is displayed if owner name input exceeds 50 characters', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + owner.typeExistingOwnerName(main.generateRandomString(51)) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) + }) + + // TODO: Check unit tests + it('Verify there is no error message is displayed if owner name input contains less than 50 characters', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + owner.typeExistingOwnerName(main.generateRandomString(50)) + owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) + }) + + it('Verify data persistence', () => { + const ownerName = 'David' + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.clickOnNextBtn() + createwallet.clickOnAddNewOwnerBtn() + createwallet.typeOwnerName(ownerName, 1) + createwallet.typeOwnerAddress(constants.SEPOLIA_OWNER_2, 1) + createwallet.clickOnBackBtn() + createwallet.clearWalletName() + createwallet.typeWalletName(createwallet.walletName) + createwallet.clickOnNextBtn() + createwallet.clickOnNextBtn() + createwallet.verifySafeNameInSummaryStep(createwallet.walletName) + createwallet.verifyOwnerNameInSummaryStep(ownerName) + createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) + createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) + createwallet.verifyThresholdStringInSummaryStep(1, 2) + createwallet.verifyNetworkInSummaryStep(constants.networks.sepolia) + createwallet.clickOnBackBtn() + createwallet.clickOnBackBtn() + cy.wait(1000) + createwallet.clickOnNextBtn() + createwallet.clickOnNextBtn() + createwallet.verifySafeNameInSummaryStep(createwallet.walletName) + createwallet.verifyOwnerNameInSummaryStep(ownerName) + createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) + createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) + createwallet.verifyThresholdStringInSummaryStep(1, 2) + createwallet.verifyNetworkInSummaryStep(constants.networks.sepolia) + createwallet.verifyEstimatedFeeInSummaryStep() + }) + + it('Verify tip is displayed on right side for threshold 1/1', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.clickOnNextBtn() + createwallet.verifyPolicy1_1() + }) + + // TODO: Check unit tests + it('Verify address input validation rules', () => { + owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() + createwallet.clickOnNextBtn() + createwallet.clickOnAddNewOwnerBtn() + createwallet.typeOwnerAddress(main.generateRandomString(10), 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) + + createwallet.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS, 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.ownerAdded) + + createwallet.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS.toUpperCase(), 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) + + createwallet.typeOwnerAddress(constants.ENS_TEST_SEPOLIA_INVALID, 1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.failedResolve) + }) +}) diff --git a/cypress/e2e/smoke/import_export_data.cy.js b/cypress/e2e/regression/import_export_data.cy.js similarity index 95% rename from cypress/e2e/smoke/import_export_data.cy.js rename to cypress/e2e/regression/import_export_data.cy.js index 2d99d5d9b7..379314b48a 100644 --- a/cypress/e2e/smoke/import_export_data.cy.js +++ b/cypress/e2e/regression/import_export_data.cy.js @@ -36,6 +36,7 @@ describe('Import Export Data tests', () => { file.verifyAppsAreVisible(appNames) }) + // TODO: Revisit logic it('Verify imported data in settings', () => { const unchecked = [file.prependChainPrefixStr, file.copyAddressStr] const checked = [file.darkModeStr] @@ -45,7 +46,7 @@ describe('Import Export Data tests', () => { file.verifyCheckboxes(checked, true) }) - it('Verifies data for export in Data tab', () => { + it('Verify data for export in Data tab', () => { file.clickOnShowMoreTabsBtn() file.verifDataTabBtnIsVisible() file.clickOnDataTab() diff --git a/cypress/e2e/regression/load_safe.cy.js b/cypress/e2e/regression/load_safe.cy.js new file mode 100644 index 0000000000..3099d130d6 --- /dev/null +++ b/cypress/e2e/regression/load_safe.cy.js @@ -0,0 +1,81 @@ +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' + +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('Load Safe tests', () => { + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.loadNewSafeSepoliaUrl) + main.acceptCookies() + cy.wait(2000) + }) + + it('Verify a network can be selected in the Safe', () => { + safe.clickNetworkSelector(constants.networks.sepolia) + safe.selectPolygon() + cy.wait(2000) + safe.clickNetworkSelector(constants.networks.polygon) + safe.selectSepolia() + }) + + // TODO: Devide by positive and negative tests + it('Verify only valid Safe name can be accepted', () => { + // alias the address input label + cy.get('input[name="address"]').parent().prev('label').as('addressLabel') + + createwallet.verifyDefaultWalletName(createwallet.defaltSepoliaPlaceholder) + safe.verifyIncorrectAddressErrorMessage() + safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) + + // Type an invalid address + // cy.get('input[name="address"]').clear().type(EOA_ADDRESS) + // cy.get('@addressLabel').contains(INVALID_ADDRESS_ERROR_MSG) + + // Type a ENS name + // TODO: register a goerli ENS name for the test Safe + // cy.get('input[name="address"]').clear().type(SAFE_ENS_NAME) + // giving time to the ENS name to be translated + // cy.get('input[name="address"]', { timeout: 10000 }).should('have.value', `rin:${SAFE_ENS_NAME_TRANSLATED}`) + + // Uploading a QR code + // TODO: fix this + // cy.findByTestId('QrCodeIcon').click() + // cy.contains('Upload an image').click() + // cy.get('[type="file"]').attachFile('../fixtures/goerli_safe_QR.png') + + safe.verifyAddressInputValue() + safe.clickOnNextBtn() + }) + + it('Verify custom name in the first owner can be set', () => { + safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) + safe.clickOnNextBtn() + createwallet.typeOwnerName(testOwnerName, 0) + safe.clickOnNextBtn() + }) + + it('Verify Safe and owner names are displayed in the Review step', () => { + safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) + safe.clickOnNextBtn() + createwallet.typeOwnerName(testOwnerName, 0) + safe.clickOnNextBtn() + safe.verifyDataInReviewSection(testSafeName, testOwnerName) + safe.clickOnAddBtn() + }) +}) diff --git a/cypress/e2e/regression/nfts.cy.js b/cypress/e2e/regression/nfts.cy.js new file mode 100644 index 0000000000..74f7791d24 --- /dev/null +++ b/cypress/e2e/regression/nfts.cy.js @@ -0,0 +1,28 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as nfts from '../pages/nfts.pages' + +const nftsName = 'CatFactory' +const nftsAddress = '0x373B...866c' +const nftsTokenID = 'CF' + +describe('NFTs tests', () => { + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(constants.balanceNftsUrl + constants.SEPOLIA_TEST_SAFE_5) + main.acceptCookies() + nfts.waitForNftItems(2) + }) + + // TODO: Add Sign action + it('Verify multipls NFTs can be selected and reviewed', () => { + nfts.verifyInitialNFTData() + nfts.selectNFTs(3) + nfts.deselectNFTs([2], 3) + nfts.sendNFT() + nfts.verifyNFTModalData() + nfts.typeRecipientAddress(constants.SEPOLIA_TEST_SAFE_4) + nfts.clikOnNextBtn() + nfts.verifyReviewModalData(2) + }) +}) diff --git a/cypress/e2e/smoke/pending_actions.cy.js b/cypress/e2e/regression/pending_actions.cy.js similarity index 100% rename from cypress/e2e/smoke/pending_actions.cy.js rename to cypress/e2e/regression/pending_actions.cy.js diff --git a/cypress/e2e/regression/remove_owner.cy.js b/cypress/e2e/regression/remove_owner.cy.js new file mode 100644 index 0000000000..84758dc45a --- /dev/null +++ b/cypress/e2e/regression/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' + +describe('Remove Owners tests', () => { + beforeEach(() => { + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) + main.waitForTrnsactionHistoryToComplete() + cy.clearLocalStorage() + main.acceptCookies() + owner.waitForConnectionStatus() + cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) + }) + + it('Verify that "Remove" icon is visible', () => { + owner.verifyRemoveBtnIsEnabled().should('have.length', 2) + }) + + it('Verify Tooltip displays correct message for Non-Owner', () => { + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_4) + main.waitForTrnsactionHistoryToComplete() + owner.waitForConnectionStatus() + owner.verifyRemoveBtnIsDisabled() + }) + + it('Verify Tooltip displays correct message for disconnected user', () => { + owner.clickOnWalletExpandMoreIcon() + owner.clickOnDisconnectBtn() + owner.verifyRemoveBtnIsDisabled() + }) + + it('Verify owner removal form can be opened', () => { + owner.openRemoveOwnerWindow(1) + }) + + it('Verify threshold input displays the upper limit as the current safe number of owners minus one', () => { + owner.openRemoveOwnerWindow(1) + owner.verifyThresholdLimit(1, 1) + owner.getThresholdOptions().should('have.length', 1) + }) +}) diff --git a/cypress/e2e/regression/replace_owner.cy.js b/cypress/e2e/regression/replace_owner.cy.js new file mode 100644 index 0000000000..4d3465983c --- /dev/null +++ b/cypress/e2e/regression/replace_owner.cy.js @@ -0,0 +1,71 @@ +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' + +describe('Replace Owners tests', () => { + beforeEach(() => { + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) + cy.clearLocalStorage() + main.acceptCookies() + cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) + }) + + it('Verify Tooltip displays correct message for disconnected user', () => { + owner.waitForConnectionStatus() + owner.clickOnWalletExpandMoreIcon() + owner.clickOnDisconnectBtn() + owner.verifyReplaceBtnIsDisabled() + }) + + // TODO: Check unit tests + it('Verify max characters in name field', () => { + owner.waitForConnectionStatus() + owner.openReplaceOwnerWindow() + 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 + constants.SEPOLIA_TEST_SAFE_1) + 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) + cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) + owner.waitForConnectionStatus() + owner.openReplaceOwnerWindow() + owner.typeOwnerAddress(constants.addresBookContacts.user1.address) + owner.selectNewOwner(constants.addresBookContacts.user1.name) + owner.verifyNewOwnerName(constants.addresBookContacts.user1.name) + }) + + it('Verify that Name field not mandatory. Verify confirmation for owner replacement is displayed', () => { + owner.waitForConnectionStatus() + owner.openReplaceOwnerWindow() + owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) + owner.clickOnNextBtn() + owner.verifyConfirmTransactionWindowDisplayed() + }) + + it('Verify relevant error messages are displayed in Address input', () => { + owner.waitForConnectionStatus() + owner.openReplaceOwnerWindow() + owner.typeOwnerAddress(main.generateRandomString(10)) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) + + owner.typeOwnerAddress(constants.addresBookContacts.user1.address.toUpperCase()) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) + + owner.typeOwnerAddress(constants.SEPOLIA_TEST_SAFE_1) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.ownSafe) + + owner.typeOwnerAddress(constants.addresBookContacts.user1.address.replace('F', 'f')) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) + + owner.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS) + owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.alreadyAdded) + }) +}) diff --git a/cypress/e2e/regression/tx_history.cy.js b/cypress/e2e/regression/tx_history.cy.js new file mode 100644 index 0000000000..ea29756d19 --- /dev/null +++ b/cypress/e2e/regression/tx_history.cy.js @@ -0,0 +1,31 @@ +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as createTx from '../pages/create_tx.pages' + +const OUTGOING = 'Sent' + +const str1 = 'Received' +const str2 = 'Executed' +const str3 = 'Transaction hash' + +describe('Transaction history tests', () => { + beforeEach(() => { + cy.clearLocalStorage() + // Go to the test Safe transaction history + cy.visit(constants.transactionsHistoryUrl + constants.SEPOLIA_TEST_SAFE_5) + + // So that tests that rely on this feature don't randomly fail + cy.window().then((win) => win.localStorage.setItem('SAFE_v2__AB_human-readable', true)) + + main.acceptCookies() + }) + + it('Verify transaction can be expanded/collapsed', () => { + createTx.clickOnTransactionExpandableItem('Oct 20, 2023', () => { + createTx.verifyTransactionStrExists(str1) + createTx.verifyTransactionStrExists(str2) + createTx.verifyTransactionStrExists(str3) + createTx.clickOnExpandIcon() + }) + }) +}) diff --git a/cypress/e2e/safe-apps/browser_permissions.cy.js b/cypress/e2e/safe-apps/browser_permissions.cy.js index 59cbceffed..97f816dc75 100644 --- a/cypress/e2e/safe-apps/browser_permissions.cy.js +++ b/cypress/e2e/safe-apps/browser_permissions.cy.js @@ -30,8 +30,6 @@ describe('Browser permissions tests', () => { safeapps.verifyWarningDefaultAppMsgIsDisplayed() safeapps.verifyCameraCheckBoxExists() safeapps.clickOnContinueBtn() - safeapps.clickOnContinueBtn().should(() => { - expect(window.localStorage.getItem(constants.BROWSER_PERMISSIONS_KEY)).to.eq(safeapps.localStorageItem) - }) + safeapps.checkLocalStorage() }) }) diff --git a/cypress/e2e/smoke/add_owner.cy.js b/cypress/e2e/smoke/add_owner.cy.js index 2397881b65..a4588e261c 100644 --- a/cypress/e2e/smoke/add_owner.cy.js +++ b/cypress/e2e/smoke/add_owner.cy.js @@ -1,68 +1,29 @@ 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 navigation from '../pages/navigation.page' -describe('Add Owners tests', () => { +describe('[SMOKE] Add Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) cy.clearLocalStorage() + main.waitForTrnsactionHistoryToComplete() main.acceptCookies() - cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) + main.verifyElementsExist([navigation.setupSection]) }) - it('Verify the presence of "Add Owner" button', () => { + it('[SMOKE] Verify the presence of "Add Owner" button', () => { owner.verifyAddOwnerBtnIsEnabled() }) - it('Verify “Add new owner” button tooltip displays correct message for Non-Owner', () => { + it('[SMOKE] Verify “Add new owner” button tooltip displays correct message for Non-Owner', () => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_2) + main.waitForTrnsactionHistoryToComplete() owner.verifyAddOwnerBtnIsDisabled() }) - it('Verify Tooltip displays correct message for disconnected user', () => { - owner.waitForConnectionStatus() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() - owner.verifyAddOwnerBtnIsDisabled() - }) - - it('Verify the Add New Owner Form can be opened', () => { - owner.waitForConnectionStatus() - owner.openAddOwnerWindow() - }) - - it('Verify error message displayed if character limit is exceeded in Name input', () => { - owner.waitForConnectionStatus() - owner.openAddOwnerWindow() - owner.typeOwnerName(main.generateRandomString(51)) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) - }) - - it('Verify that the "Name" field is auto-filled with the relevant name from Address Book', () => { - cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) - 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) - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) - owner.waitForConnectionStatus() - owner.openAddOwnerWindow() - owner.typeOwnerAddress(constants.addresBookContacts.user1.address) - owner.selectNewOwner(constants.addresBookContacts.user1.name) - owner.verifyNewOwnerName(constants.addresBookContacts.user1.name) - }) - - it('Verify that Name field not mandatory', () => { - owner.waitForConnectionStatus() - owner.openAddOwnerWindow() - owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) - owner.clickOnNextBtn() - owner.verifyConfirmTransactionWindowDisplayed() - }) - - it('Verify relevant error messages are displayed in Address input', () => { + // TODO: Check if this test is covered with unit tests + it('[SMOKE] Verify relevant error messages are displayed in Address input', () => { owner.waitForConnectionStatus() owner.openAddOwnerWindow() owner.typeOwnerAddress(main.generateRandomString(10)) @@ -81,22 +42,20 @@ describe('Add Owners tests', () => { owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.alreadyAdded) }) - it('Verify default threshold value. Verify correct threshold calculation', () => { + it('[SMOKE] Verify default threshold value. Verify correct threshold calculation', () => { owner.waitForConnectionStatus() owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS) owner.verifyThreshold(1, 2) }) - it('Verify valid Address validation', () => { + it('[SMOKE] Verify valid Address validation', () => { owner.waitForConnectionStatus() owner.openAddOwnerWindow() owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) owner.clickOnNextBtn() owner.verifyConfirmTransactionWindowDisplayed() - cy.reload() - owner.waitForConnectionStatus() - owner.openAddOwnerWindow() + owner.clickOnBackBtn() owner.typeOwnerAddress(constants.SEPOLIA_TEST_SAFE_2) owner.clickOnNextBtn() owner.verifyConfirmTransactionWindowDisplayed() diff --git a/cypress/e2e/smoke/address_book.cy.js b/cypress/e2e/smoke/address_book.cy.js index b0af66eecd..27093faa55 100644 --- a/cypress/e2e/smoke/address_book.cy.js +++ b/cypress/e2e/smoke/address_book.cy.js @@ -1,6 +1,4 @@ import 'cypress-file-upload' -const path = require('path') -import { format } from 'date-fns' import * as constants from '../../support/constants' import * as addressBook from '../../e2e/pages/address_book.page' import * as main from '../../e2e/pages/main.page' @@ -8,28 +6,21 @@ import * as main from '../../e2e/pages/main.page' const NAME = 'Owner1' const EDITED_NAME = 'Edited Owner1' -describe('Address book tests', () => { +describe('[SMOKE] Address book tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) + main.waitForTrnsactionHistoryToComplete() main.acceptCookies() }) - it('Verify entry can be added', () => { + it('[SMOKE] Verify entry can be added', () => { addressBook.clickOnCreateEntryBtn() addressBook.addEntry(NAME, constants.RECIPIENT_ADDRESS) }) - it('Verify entered entry in Name input can be saved', () => { - addressBook.clickOnCreateEntryBtn() - addressBook.addEntry(NAME, constants.RECIPIENT_ADDRESS) - addressBook.clickOnEditEntryBtn() - addressBook.typeInNameInput(EDITED_NAME) - addressBook.clickOnSaveButton() - addressBook.verifyNameWasChanged(NAME, EDITED_NAME) - }) - - it('Verify entry can be deleted', () => { + //TODO: Use localstorage for setting up/deleting entries + it('[SMOKE] Verify entry can be deleted', () => { addressBook.clickOnCreateEntryBtn() addressBook.addEntry(NAME, constants.RECIPIENT_ADDRESS) // Click the delete button in the first entry @@ -38,45 +29,10 @@ describe('Address book tests', () => { addressBook.verifyEditedNameNotExists(EDITED_NAME) }) - it('Verify csv file can be imported (Goerli)', () => { + it('[SMOKE] Verify csv file can be imported', () => { addressBook.clickOnImportFileBtn() addressBook.importFile() addressBook.verifyImportModalIsClosed() addressBook.verifyDataImported(constants.SEPOLIA_CSV_ENTRY.name, constants.SEPOLIA_CSV_ENTRY.address) }) - - it.skip('Verify Gnosis Chain imported address can be found', () => { - // 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 downloaded', () => { - addressBook.clickOnImportFileBtn() - addressBook.importFile() - // Download the export file - const date = format(new Date(), 'yyyy-MM-dd', { timeZone: 'UTC' }) - const fileName = `safe-address-book-${date}.csv` //name that is given to the file automatically - - addressBook.clickOnExportFileBtn() - //This is the submit button for the Export modal. It requires an actuall class or testId to differentiate - //from the Export button at the top of the AB table - 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') - }) }) diff --git a/cypress/e2e/smoke/assets.cy.js b/cypress/e2e/smoke/assets.cy.js index ebdf8cad3a..7eb9e311f2 100644 --- a/cypress/e2e/smoke/assets.cy.js +++ b/cypress/e2e/smoke/assets.cy.js @@ -1,13 +1,12 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as balances from '../pages/balances.pages' -import * as owner from '../pages/owners.pages' const ASSET_NAME_COLUMN = 0 const TOKEN_AMOUNT_COLUMN = 1 const FIAT_AMOUNT_COLUMN = 2 -describe('Assets tests', () => { +describe('[SMOKE] Assets tests', () => { const fiatRegex = balances.fiatRegex beforeEach(() => { @@ -16,99 +15,15 @@ describe('Assets tests', () => { main.acceptCookies() }) - it('Verify that the token tab is selected by default and the table is visible', () => { + it('[SMOKE] Verify that the token tab is selected by default and the table is visible', () => { balances.verifyTokensTabIsSelected('true') }) - it('Verify that the native token is visible', () => { + it('[SMOKE] Verify that the native token is visible', () => { balances.verifyTokenIsPresent(constants.tokenNames.sepoliaEther) }) - it('Verify that non-native tokens are present and have balance', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.verifyBalance(balances.currencyDaiCap, TOKEN_AMOUNT_COLUMN, balances.currencyDaiAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyDaiCap, - balances.currencyDaiFormat_2, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - - balances.verifyBalance(balances.currencyAave, TOKEN_AMOUNT_COLUMN, balances.currencyAaveAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyAave, - balances.currentcyAaveFormat, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - - balances.verifyBalance(balances.currencyLink, TOKEN_AMOUNT_COLUMN, balances.currencyLinkAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyLink, - balances.currentcyLinkFormat, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - - balances.verifyBalance(balances.currencyTestTokenA, TOKEN_AMOUNT_COLUMN, balances.currencyTestTokenAAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyTestTokenA, - balances.currentcyTestTokenAFormat, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - - balances.verifyBalance(balances.currencyTestTokenB, TOKEN_AMOUNT_COLUMN, balances.currencyTestTokenBAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyTestTokenB, - balances.currentcyTestTokenBFormat, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - - balances.verifyBalance(balances.currencyUSDC, TOKEN_AMOUNT_COLUMN, balances.currencyTestUSDCAlttext) - balances.verifyTokenBalanceFormat( - balances.currencyUSDC, - balances.currentcyTestUSDCFormat, - TOKEN_AMOUNT_COLUMN, - FIAT_AMOUNT_COLUMN, - fiatRegex, - ) - }) - - it('Verify that every token except the native token has a "go to blockexplorer link"', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - // Specifying true for Sepolia. Will delete the flag once completely migrate to Sepolia - balances.verifyAssetNameHasExplorerLink(balances.currencyUSDC, ASSET_NAME_COLUMN, true) - balances.verifyAssetNameHasExplorerLink(balances.currencyTestTokenB, ASSET_NAME_COLUMN, true) - balances.verifyAssetNameHasExplorerLink(balances.currencyTestTokenA, ASSET_NAME_COLUMN, true) - balances.verifyAssetNameHasExplorerLink(balances.currencyLink, ASSET_NAME_COLUMN, true) - balances.verifyAssetNameHasExplorerLink(balances.currencyAave, ASSET_NAME_COLUMN, true) - balances.verifyAssetNameHasExplorerLink(balances.currencyDaiCap, ASSET_NAME_COLUMN, true) - balances.verifyAssetExplorerLinkNotAvailable(constants.tokenNames.sepoliaEther, ASSET_NAME_COLUMN) - }) - - it('Verify the default Fiat currency and the effects after changing it', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.verifyFirstRowDoesNotContainCurrency(balances.currencyEUR, FIAT_AMOUNT_COLUMN) - balances.verifyFirstRowContainsCurrency(balances.currencyUSD, FIAT_AMOUNT_COLUMN) - balances.clickOnCurrencyDropdown() - balances.selectCurrency(balances.currencyEUR) - balances.verifyFirstRowDoesNotContainCurrency(balances.currencyUSD, FIAT_AMOUNT_COLUMN) - balances.verifyFirstRowContainsCurrency(balances.currencyEUR, FIAT_AMOUNT_COLUMN) - }) - - it('Verify that a tool tip is shown pointing to "Token list" dropdown', () => { - //Spam warning message is removed in beforeEach hook - cy.reload() - }) - - it('Verify that Token list dropdown down options show/hide spam tokens', () => { + it('[SMOKE] Verify that Token list dropdown down options show/hide spam tokens', () => { let spamTokens = [ balances.currencyAave, balances.currencyTestTokenA, @@ -124,95 +39,14 @@ describe('Assets tests', () => { main.verifyValuesExist(balances.tokenListTable, spamTokens) }) - it('Verify that "Hide token" button is present and opens the "Hide tokens menu"', () => { + it('[SMOKE] Verify that "Hide token" button is present and opens the "Hide tokens menu"', () => { balances.selectTokenList(balances.tokenListOptions.allTokens) balances.openHideTokenMenu() balances.verifyEachRowHasCheckbox() }) - it('Verify that checking the checkboxes increases the token selected counter', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.checkTokenCounter(1) - }) - - it('Verify that selecting tokens and saving hides them from the table', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.saveHiddenTokenSelection() - main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink]) - }) - - it('Verify that Cancel closes the menu and does not change the table status', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.clickOnTokenCheckbox(balances.currencyAave) - balances.saveHiddenTokenSelection() - main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink, balances.currencyAave]) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.clickOnTokenCheckbox(balances.currencyAave) - balances.cancelSaveHiddenTokenSelection() - main.verifyValuesDoNotExist(balances.tokenListTable, [balances.currencyLink, balances.currencyAave]) - }) - - it('Verify that Deselect All unchecks all tokens from the list', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.clickOnTokenCheckbox(balances.currencyAave) - balances.deselecAlltHiddenTokenSelection() - balances.verifyEachRowHasCheckbox(constants.checkboxStates.unchecked) - }) - - it('Verify the Hidden tokens counter works for spam tokens', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(balances.currencyLink) - balances.saveHiddenTokenSelection() - balances.checkHiddenTokenBtnCounter(1) - }) - - it('Verify the Hidden tokens counter works for native tokens', () => { - balances.openHideTokenMenu() - balances.clickOnTokenCheckbox(constants.tokenNames.sepoliaEther) - balances.saveHiddenTokenSelection() - balances.checkHiddenTokenBtnCounter(1) - }) - - it('Verify you can hide tokens from the eye icon in the table rows', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.hideAsset(balances.currencyLink) - }) - - it('Verify the sorting of "Assets" and "Balance" in the table', () => { - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.verifyTableRows(7) - balances.clickOnTokenNameSortBtn() - balances.verifyTokenNamesOrder() - balances.clickOnTokenNameSortBtn() - balances.verifyTokenNamesOrder('descending') - balances.clickOnTokenBalanceSortBtn() - balances.verifyTokenBalanceOrder() - balances.clickOnTokenBalanceSortBtn() - balances.verifyTokenBalanceOrder('descending') - }) - - it('Verify that clicking the button with an owner opens the Send funds form', () => { + it('[SMOKE] Verify that clicking the button with an owner opens the Send funds form', () => { balances.selectTokenList(balances.tokenListOptions.allTokens) balances.clickOnSendBtn(0) }) - - it('Verify that the Send button shows when hovering a row', () => { - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() - balances.selectTokenList(balances.tokenListOptions.allTokens) - balances.showSendBtn(0) - owner.verifyTooltiptext(owner.disconnectedUserErrorMsg) - // Removed the part that checks for a non owner error message in the tooltip - // because the safe has no assets, and we don't have a safe with assets where e2e wallet is not an owner - }) }) diff --git a/cypress/e2e/smoke/batch_tx.cy.js b/cypress/e2e/smoke/batch_tx.cy.js index c3e93d08fc..df4caff475 100644 --- a/cypress/e2e/smoke/batch_tx.cy.js +++ b/cypress/e2e/smoke/batch_tx.cy.js @@ -7,7 +7,7 @@ const currentNonce = 3 const funds_first_tx = '0.001' const funds_second_tx = '0.002' -describe('Batch transaction tests', () => { +describe('[SMOKE] Batch transaction tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) @@ -15,17 +15,12 @@ describe('Batch transaction tests', () => { main.acceptCookies() }) - it('Verify empty batch list can be opened', () => { + it('[SMOKE] Verify empty batch list can be opened', () => { batch.openBatchtransactionsModal() batch.openNewTransactionModal() }) - 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) - }) - - it('Verify a transaction can be added to the batch', () => { + it('[SMOKE] Verify a transaction can be added to the batch', () => { batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) cy.contains(batch.transactionAddedToBatchStr).should('be.visible') //The batch button in the header shows the transaction count @@ -34,16 +29,7 @@ describe('Batch transaction tests', () => { batch.verifyAmountTransactionsInBatch(1) }) - it('Verify a second transaction can be added to the batch', () => { - batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) - cy.wait(1000) - batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) - batch.verifyBatchIconCount(2) - batch.clickOnBatchCounter() - batch.verifyAmountTransactionsInBatch(2) - }) - - it('Verify the batch can be confirmed and related transactions exist in the form', () => { + it('[SMOKE] Verify the batch can be confirmed and related transactions exist in the form', () => { batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) cy.wait(1000) batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) @@ -58,7 +44,7 @@ describe('Batch transaction tests', () => { cy.get('@TransactionList').find('li').eq(1).find('span').eq(0).contains(funds_first_tx) }) - it('Verify a transaction can be removed from the batch', () => { + it('[SMOKE] Verify a transaction can be removed from the batch', () => { batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) cy.wait(1000) batch.addNewTransactionToBatch(constants.EOA, currentNonce, funds_first_tx) diff --git a/cypress/e2e/smoke/create_safe_simple.cy.js b/cypress/e2e/smoke/create_safe_simple.cy.js index 1b924cd743..3578417fce 100644 --- a/cypress/e2e/smoke/create_safe_simple.cy.js +++ b/cypress/e2e/smoke/create_safe_simple.cy.js @@ -3,13 +3,14 @@ import * as main from '../../e2e/pages/main.page' import * as createwallet from '../pages/create_wallet.pages' import * as owner from '../pages/owners.pages' -describe('Safe creation tests', () => { +describe('[SMOKE] Safe creation tests', () => { beforeEach(() => { cy.visit(constants.welcomeUrl + '?chain=sep') + main.waitForSafeListRequestToComplete() cy.clearLocalStorage() main.acceptCookies() }) - it('Verify a Wallet can be connected', () => { + it('[SMOKE] Verify a Wallet can be connected', () => { createwallet.clickOnCreateNewSafeBtn() owner.clickOnWalletExpandMoreIcon() owner.clickOnDisconnectBtn() @@ -17,62 +18,16 @@ describe('Safe creation tests', () => { createwallet.connectWallet() }) - it('Verify Next button is disabled until switching to network is done', () => { - owner.waitForConnectionStatus() - createwallet.selectNetwork(constants.networks.ethereum) - createwallet.clickOnCreateNewSafeBtn() - createwallet.checkNetworkChangeWarningMsg() - createwallet.verifyNextBtnIsDisabled() - createwallet.selectNetwork(constants.networks.sepolia) - createwallet.verifyNextBtnIsEnabled() - }) - - it('Verify that a new Wallet has default name related to the selected network', () => { + it('[SMOKE] Verify that a new Wallet has default name related to the selected network', () => { owner.waitForConnectionStatus() createwallet.clickOnCreateNewSafeBtn() createwallet.verifyDefaultWalletName(createwallet.defaltSepoliaPlaceholder) }) - it('Verify error message is displayed if wallet name input exceeds 50 characters', () => { + it('[SMOKE] Verify Add and Remove Owner Row works as expected', () => { owner.waitForConnectionStatus() createwallet.clickOnCreateNewSafeBtn() - createwallet.typeWalletName(main.generateRandomString(51)) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) - createwallet.clearWalletName() - }) - - it('Verify there is no error message is displayed if wallet name input contains less than 50 characters', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - createwallet.typeWalletName(main.generateRandomString(50)) - owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) - }) - - it('Verify current connected account is shown as default owner', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() - owner.verifyExistingOwnerAddress(0, constants.DEFAULT_OWNER_ADDRESS) - }) - - it('Verify error message is displayed if owner name input exceeds 50 characters', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.typeExistingOwnerName(main.generateRandomString(51)) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) - }) - - it('Verify there is no error message is displayed if owner name input contains less than 50 characters', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.typeExistingOwnerName(main.generateRandomString(50)) - owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) - }) - - it('Verify Add and Remove Owner Row works as expected', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() + createwallet.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() owner.verifyNumberOfOwners(2) owner.verifyExistingOwnerAddress(1, '') @@ -83,10 +38,10 @@ describe('Safe creation tests', () => { owner.verifyNumberOfOwners(2) }) - it('Verify Threshold Setup', () => { + it('[SMOKE] Verify Threshold Setup', () => { owner.waitForConnectionStatus() createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() + createwallet.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() createwallet.clickOnAddNewOwnerBtn() owner.verifyNumberOfOwners(3) @@ -100,62 +55,4 @@ describe('Safe creation tests', () => { owner.verifyThresholdLimit(1, 2) createwallet.updateThreshold(1) }) - - it('Verify data persistence', () => { - const ownerName = 'David' - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() - createwallet.clickOnAddNewOwnerBtn() - createwallet.typeOwnerName(ownerName, 1) - createwallet.typeOwnerAddress(constants.SEPOLIA_OWNER_2, 1) - owner.clickOnBackBtn() - createwallet.clearWalletName() - createwallet.typeWalletName(createwallet.walletName) - owner.clickOnNextBtn() - owner.clickOnNextBtn() - createwallet.verifySafeNameInSummaryStep(createwallet.walletName) - createwallet.verifyOwnerNameInSummaryStep(ownerName) - createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) - createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) - createwallet.verifyThresholdStringInSummaryStep(1, 2) - createwallet.verifyNetworkInSummaryStep(constants.networks.sepolia) - owner.clickOnBackBtn() - owner.clickOnBackBtn() - cy.wait(1000) - owner.clickOnNextBtn() - owner.clickOnNextBtn() - createwallet.verifySafeNameInSummaryStep(createwallet.walletName) - createwallet.verifyOwnerNameInSummaryStep(ownerName) - createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) - createwallet.verifyOwnerAddressInSummaryStep(constants.DEFAULT_OWNER_ADDRESS) - createwallet.verifyThresholdStringInSummaryStep(1, 2) - createwallet.verifyNetworkInSummaryStep(constants.networks.sepolia) - createwallet.verifyEstimatedFeeInSummaryStep() - }) - - it('Verify tip is displayed on right side for threshold 1/1', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() - createwallet.verifyPolicy1_1() - }) - - it('Verify address input validation rules', () => { - owner.waitForConnectionStatus() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnNextBtn() - createwallet.clickOnAddNewOwnerBtn() - createwallet.typeOwnerAddress(main.generateRandomString(10), 1) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) - - createwallet.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS, 1) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.ownerAdded) - - createwallet.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS.toUpperCase(), 1) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) - - createwallet.typeOwnerAddress(constants.ENS_TEST_SEPOLIA_INVALID, 1) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.failedResolve) - }) }) diff --git a/cypress/e2e/smoke/create_tx.cy.js b/cypress/e2e/smoke/create_tx.cy.js index 74718d6980..75014058c3 100644 --- a/cypress/e2e/smoke/create_tx.cy.js +++ b/cypress/e2e/smoke/create_tx.cy.js @@ -5,14 +5,14 @@ import * as createtx from '../../e2e/pages/create_tx.pages' const sendValue = 0.00002 const currentNonce = 11 -describe('Create transactions tests', () => { +describe('[SMOKE] Create transactions tests', () => { before(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) main.acceptCookies() }) - it('Verify a new send token transaction can be created', () => { + it('[SMOKE] Verify a new send token transaction can be created', () => { createtx.clickOnNewtransactionBtn() createtx.clickOnSendTokensBtn() createtx.typeRecipientAddress(constants.EOA) @@ -23,7 +23,7 @@ describe('Create transactions tests', () => { createtx.clickOnNextBtn() }) - it('Verify a transaction can be reviewed, edited and submitted', () => { + it('[SMOKE] Verify a transaction can be reviewed, edited and submitted', () => { createtx.verifySubmitBtnIsEnabled() cy.wait(1000) createtx.verifyNativeTokenTransfer() @@ -36,7 +36,7 @@ describe('Create transactions tests', () => { createtx.clickOnSignTransactionBtn() }) - it('Verify that clicking on notification shows the transaction in queue', () => { + it('[SMOKE] Verify that clicking on notification shows the transaction in queue', () => { createtx.waitForProposeRequest() createtx.clickViewTransaction() createtx.verifySingleTxPage() diff --git a/cypress/e2e/smoke/dashboard.cy.js b/cypress/e2e/smoke/dashboard.cy.js index d3712a5b07..03b239c733 100644 --- a/cypress/e2e/smoke/dashboard.cy.js +++ b/cypress/e2e/smoke/dashboard.cy.js @@ -2,7 +2,7 @@ import * as constants from '../../support/constants' import * as dashboard from '../pages/dashboard.pages' import * as main from '../pages/main.page' -describe('Dashboard tests', () => { +describe('[SMOKE] Dashboard tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) @@ -11,19 +11,19 @@ describe('Dashboard tests', () => { dashboard.verifyConnectTransactStrIsVisible() }) - it('Verify the overview widget is displayed', () => { + it('[SMOKE] Verify the overview widget is displayed', () => { dashboard.verifyOverviewWidgetData() }) - it('Verify the transaction queue widget is displayed', () => { + it('[SMOKE] Verify the transaction queue widget is displayed', () => { dashboard.verifyTxQueueWidget() }) - it('Verify the featured Safe Apps are displayed', () => { + it('[SMOKE] Verify the featured Safe Apps are displayed', () => { dashboard.verifyFeaturedAppsSection() }) - it('Verify the Safe Apps Section is displayed', () => { + it('[SMOKE] Verify the Safe Apps Section is displayed', () => { dashboard.verifySafeAppsSection() }) }) diff --git a/cypress/e2e/smoke/landing.cy.js b/cypress/e2e/smoke/landing.cy.js index 206732446f..f56883364a 100644 --- a/cypress/e2e/smoke/landing.cy.js +++ b/cypress/e2e/smoke/landing.cy.js @@ -1,6 +1,6 @@ import * as constants from '../../support/constants' -describe('Landing page tests', () => { - it('Verify a user will be redirected to welcome page', () => { +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 edce4b7f8b..1d9122eb47 100644 --- a/cypress/e2e/smoke/load_safe.cy.js +++ b/cypress/e2e/smoke/load_safe.cy.js @@ -18,7 +18,7 @@ const INVALID_ADDRESS_ERROR_MSG = 'Address given is not a valid Safe address' const OWNER_ENS_DEFAULT_NAME = 'test20.eth' const OWNER_ADDRESS = constants.EOA -describe('Load Safe tests', () => { +describe('[SMOKE] Load Safe tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.loadNewSafeSepoliaUrl) @@ -26,59 +26,7 @@ describe('Load Safe tests', () => { cy.wait(2000) }) - it('Verify a network can be selected in the Safe', () => { - safe.clickNetworkSelector(constants.networks.sepolia) - safe.selectPolygon() - cy.wait(2000) - safe.clickNetworkSelector(constants.networks.polygon) - safe.selectSepolia() - }) - - it('Verify only valid Safe name can be accepted', () => { - // alias the address input label - cy.get('input[name="address"]').parent().prev('label').as('addressLabel') - - createwallet.verifyDefaultWalletName(createwallet.defaltSepoliaPlaceholder) - safe.verifyIncorrectAddressErrorMessage() - safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) - - // Type an invalid address - // cy.get('input[name="address"]').clear().type(EOA_ADDRESS) - // cy.get('@addressLabel').contains(INVALID_ADDRESS_ERROR_MSG) - - // Type a ENS name - // TODO: register a goerli ENS name for the test Safe - // cy.get('input[name="address"]').clear().type(SAFE_ENS_NAME) - // giving time to the ENS name to be translated - // cy.get('input[name="address"]', { timeout: 10000 }).should('have.value', `rin:${SAFE_ENS_NAME_TRANSLATED}`) - - // Uploading a QR code - // TODO: fix this - // cy.findByTestId('QrCodeIcon').click() - // cy.contains('Upload an image').click() - // cy.get('[type="file"]').attachFile('../fixtures/goerli_safe_QR.png') - - safe.verifyAddressInputValue() - safe.clickOnNextBtn() - }) - - it('Verify custom name in the first owner an be set', () => { - safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) - safe.clickOnNextBtn() - createwallet.typeOwnerName(testOwnerName, 0) - safe.clickOnNextBtn() - }) - - it('Verify Safe and owner names are displayed in the Review step', () => { - safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) - safe.clickOnNextBtn() - createwallet.typeOwnerName(testOwnerName, 0) - safe.clickOnNextBtn() - safe.verifyDataInReviewSection(testSafeName, testOwnerName) - safe.clickOnAddBtn() - }) - - it('Verify the custom Safe name is successfully loaded', () => { + it('[SMOKE] Verify the custom Safe name is successfully loaded', () => { safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_2) safe.clickOnNextBtn() createwallet.typeOwnerName(testOwnerName, 0) diff --git a/cypress/e2e/smoke/nfts.cy.js b/cypress/e2e/smoke/nfts.cy.js index 01680d3f72..11cd2217d2 100644 --- a/cypress/e2e/smoke/nfts.cy.js +++ b/cypress/e2e/smoke/nfts.cy.js @@ -6,42 +6,31 @@ const nftsName = 'CatFactory' const nftsAddress = '0x373B...866c' const nftsTokenID = 'CF' -describe('NFTs tests', () => { +describe('[SMOKE] NFTs tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.balanceNftsUrl + constants.SEPOLIA_TEST_SAFE_5) main.acceptCookies() - nfts.clickOnNftsTab() + nfts.waitForNftItems(2) }) - it('Verify that NFTs exist in the table', () => { - nfts.verifyNFTNumber(20) + it('[SMOKE] Verify that NFTs exist in the table', () => { + nfts.verifyNFTNumber(10) }) - it('Verify NFT row contains data', () => { + it('[SMOKE] Verify NFT row contains data', () => { nfts.verifyDataInTable(nftsName, nftsAddress, nftsTokenID) }) - it('Verify NFT preview window can be opened', () => { - nfts.openNFT(0) + it('[SMOKE] Verify NFT preview window can be opened', () => { + nfts.openActiveNFT(0) nfts.verifyNameInNFTModal(nftsTokenID) nfts.verifySelectedNetwrokSepolia() nfts.closeNFTModal() }) - it('Verify NFT open does not open if no NFT exits', () => { - nfts.clickOn6thNFT() + it('[SMOKE] Verify NFT open does not open if no NFT exits', () => { + nfts.clickOnInactiveNFT() nfts.verifyNFTModalDoesNotExist() }) - - it('Verify multipls NFTs can be selected and reviewed', () => { - nfts.verifyInitialNFTData() - nfts.selectNFTs(3) - nfts.deselectNFTs([2], 3) - nfts.sendNFT(2) - nfts.verifyNFTModalData() - nfts.typeRecipientAddress(constants.SEPOLIA_TEST_SAFE_4) - nfts.clikOnNextBtn() - nfts.verifyReviewModalData(2) - }) }) diff --git a/cypress/e2e/smoke/remove_owner.cy.js b/cypress/e2e/smoke/remove_owner.cy.js index 4643b91e1a..d6127941ce 100644 --- a/cypress/e2e/smoke/remove_owner.cy.js +++ b/cypress/e2e/smoke/remove_owner.cy.js @@ -1,8 +1,9 @@ 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' -describe('Remove Owners tests', () => { +describe('[SMOKE] Remove Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) cy.clearLocalStorage() @@ -10,44 +11,12 @@ describe('Remove Owners tests', () => { cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) - it('Verify that "Remove" icon is visible', () => { - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) - owner.verifyRemoveBtnIsEnabled().should('have.length', 2) - }) - - it('Verify Tooltip displays correct message for Non-Owner', () => { - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_4) - owner.waitForConnectionStatus() - owner.verifyRemoveBtnIsDisabled() - }) - - it('Verify Tooltip displays correct message for disconnected user', () => { - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) - owner.waitForConnectionStatus() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() - owner.verifyRemoveBtnIsDisabled() - }) - - it('Verify owner removal form can be opened', () => { - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) - owner.waitForConnectionStatus() - owner.openRemoveOwnerWindow(1) - }) - - it('Verify threshold input displays the upper limit as the current safe number of owners minus one', () => { - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) - owner.waitForConnectionStatus() - owner.openRemoveOwnerWindow(1) - owner.verifyThresholdLimit(1, 1) - owner.getThresholdOptions().should('have.length', 1) - }) - - it('Verify owner deletion confirmation is displayed', () => { + // TODO: Add Sign action. Check there is no error before sign action on UI when nonce not loaded + it('[SMOKE] Verify owner deletion confirmation is displayed', () => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_3) owner.waitForConnectionStatus() owner.openRemoveOwnerWindow(1) - owner.clickOnNextBtn() + createwallet.clickOnNextBtn() owner.verifyOwnerDeletionWindowDisplayed() }) }) diff --git a/cypress/e2e/smoke/replace_owner.cy.js b/cypress/e2e/smoke/replace_owner.cy.js index ed48056206..6bccf10e28 100644 --- a/cypress/e2e/smoke/replace_owner.cy.js +++ b/cypress/e2e/smoke/replace_owner.cy.js @@ -1,9 +1,8 @@ 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' -describe('Replace Owners tests', () => { +describe('[SMOKE] Replace Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) cy.clearLocalStorage() @@ -11,74 +10,19 @@ describe('Replace Owners tests', () => { cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) - it('Verify that "Replace" icon is visible', () => { + it('[SMOKE] Verify that "Replace" icon is visible', () => { owner.verifyReplaceBtnIsEnabled() }) - it('Verify Tooltip displays correct message for Non-Owner', () => { + // TODO: Remove "tooltip" from title + it('[SMOKE] Verify Tooltip displays correct message for Non-Owner', () => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_2) owner.waitForConnectionStatus() owner.verifyReplaceBtnIsDisabled() }) - it('Verify Tooltip displays correct message for disconnected user', () => { - owner.waitForConnectionStatus() - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() - owner.verifyReplaceBtnIsDisabled() - }) - - it('Verify that the owner replacement form is opened', () => { - owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() - }) - - it('Verify max characters in name field', () => { - owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() - owner.typeOwnerName(main.generateRandomString(51)) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) - }) - - it('Verify that Address input auto-fills with related value', () => { - cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) - 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) - cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) + it('[SMOKE] Verify that the owner replacement form is opened', () => { owner.waitForConnectionStatus() owner.openReplaceOwnerWindow() - owner.typeOwnerAddress(constants.addresBookContacts.user1.address) - owner.selectNewOwner(constants.addresBookContacts.user1.name) - owner.verifyNewOwnerName(constants.addresBookContacts.user1.name) - }) - - it('Verify that Name field not mandatory. Verify confirmation for owner replacement is displayed', () => { - owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() - owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) - owner.clickOnNextBtn() - owner.verifyConfirmTransactionWindowDisplayed() - }) - - it('Verify relevant error messages are displayed in Address input', () => { - owner.waitForConnectionStatus() - owner.openReplaceOwnerWindow() - owner.typeOwnerAddress(main.generateRandomString(10)) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidFormat) - - owner.typeOwnerAddress(constants.addresBookContacts.user1.address.toUpperCase()) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) - - owner.typeOwnerAddress(constants.SEPOLIA_TEST_SAFE_1) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.ownSafe) - - owner.typeOwnerAddress(constants.addresBookContacts.user1.address.replace('F', 'f')) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.invalidChecksum) - - owner.typeOwnerAddress(constants.DEFAULT_OWNER_ADDRESS) - owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.alreadyAdded) }) }) diff --git a/cypress/e2e/smoke/tx_history.cy.js b/cypress/e2e/smoke/tx_history.cy.js index a9b8309c3f..af41ef7d45 100644 --- a/cypress/e2e/smoke/tx_history.cy.js +++ b/cypress/e2e/smoke/tx_history.cy.js @@ -8,7 +8,7 @@ const str1 = 'Received' const str2 = 'Executed' const str3 = 'Transaction hash' -describe('Transaction history tests', () => { +describe('[SMOKE] Transaction history tests', () => { beforeEach(() => { cy.clearLocalStorage() // Go to the test Safe transaction history @@ -20,7 +20,7 @@ describe('Transaction history tests', () => { main.acceptCookies() }) - it('Verify October 29th transactions are displayed', () => { + it('[SMOKE] Verify October 29th transactions are displayed', () => { const DATE = 'Oct 29, 2023' const NEXT_DATE_LABEL = 'Oct 20, 2023' const amount = '0.00001 ETH' @@ -44,13 +44,4 @@ describe('Transaction history tests', () => { createTx.verifyTransactionStrExists(success) }) }) - - it('Verify transaction can be expanded/collapsed', () => { - createTx.clickOnTransactionExpandableItem('Oct 20, 2023', () => { - createTx.verifyTransactionStrExists(str1) - createTx.verifyTransactionStrExists(str2) - createTx.verifyTransactionStrExists(str3) - createTx.clickOnExpandIcon() - }) - }) }) diff --git a/cypress/e2e/spending_limit.cy.js b/cypress/e2e/spending_limit.cy.js deleted file mode 100644 index 403d1b93ce..0000000000 --- a/cypress/e2e/spending_limit.cy.js +++ /dev/null @@ -1,54 +0,0 @@ -const HW_WALLET = '0xff6E053fBf4E5895eDec09146Bc10f705E8c4b3D' -const SPENDING_LIMIT_SAFE = 'gor:0x28F95E682D1dd632b54Dc61740575f49DB39Eb7F' - -describe('Check spending limit modal', () => { - before(() => { - cy.visit(`/${SPENDING_LIMIT_SAFE}/home`, { failOnStatusCode: false }) - - cy.contains('Accept selection').click() - }) - - it('should open the spending limit modal', () => { - // Assert that "New transaction" button is visible - cy.contains('New transaction', { - timeout: 60_000, // `lastWallet` takes a while initialize in CI - }).should('be.visible') - - // Open the new transaction modal - cy.contains('New transaction').click() - }) - - it('should draft a spending limit transaction', () => { - // Modal is open - cy.contains('h2', 'New transaction').should('be.visible') - - cy.contains('Send tokens').click() - - // Fill transaction data - cy.get('input[name="recipient"]').type(SPENDING_LIMIT_SAFE) - - // Click on the Token selector - cy.get('input[name="tokenAddress"]').prev().click() - cy.get('ul[role="listbox"]').contains('Görli Ether').click() - - // Insert max amount - cy.contains('Spending Limit Transaction (0.1 GOR)').click() - - // Insert max amount - cy.contains('Max').click() - - cy.contains('Next').click() - }) - - it('should review the spending limit transaction', () => { - cy.contains( - 'Spending limit transactions only appear in the interface once they are successfully processed and indexed. Pending transactions can only be viewed in your signer wallet application or under your wallet address on a Blockchain Explorer.', - ) - - // Alias for New transaction modal - cy.contains('h2', 'Review transaction').parents('div').as('modal') - - // Estimation is loaded - cy.get('button[type="submit"]').should('not.be.disabled') - }) -}) diff --git a/cypress/e2e/tx_modal.cy.js b/cypress/e2e/tx_modal.cy.js deleted file mode 100644 index f416144b47..0000000000 --- a/cypress/e2e/tx_modal.cy.js +++ /dev/null @@ -1,164 +0,0 @@ -import * as constants from '../support/constants' - -const TEST_SAFE = 'rin:0x11Df0fa87b30080d59eba632570f620e37f2a8f7' -const RECIPIENT_ENS = 'diogo.eth' -const SAFE_NONCE = '6' - -describe('Tx Modal', () => { - before(() => { - // Open the Safe used for testing - cy.visit(`/${TEST_SAFE}`) - cy.contains('a', 'Accept selection').click() - }) - - describe('Send funds modal', () => { - describe('Send Funds form', () => { - before(() => { - // Open Send Funds Modal - cy.contains('New Transaction').click() - cy.contains('Send funds').click() - }) - - it('should display Send Funds modal with all the form elements', () => { - // Modal header - cy.contains('Send funds') - .should('be.visible') - .next() - .contains('Step 1 of 2') - .should('be.visible') - .next() - // Current network is same as Safe - .contains('Rinkeby') - .should('be.visible') - - // It contains the form elements - cy.get('form').within(() => { - // Sending from the current Safe address - const [chainPrefix, safeAddress] = TEST_SAFE.split(':') - cy.contains(chainPrefix) - cy.contains(safeAddress) - - // Recipient field - cy.get('#address-book-input').should('be.visible') - - // Token selector - cy.contains('Select an asset*').should('be.visible') - - // Amount field - cy.contains('Amount').should('be.visible') - }) - - // Review button is disabled - cy.get('button[type="submit"]').should('be.disabled') - }) - - it('should resolve the ENS name', () => { - // Fills recipient with ens - cy.get('label[for="address-book-input"]').next().type(RECIPIENT_ENS) - - // Waits for resolving the ENS - cy.contains(constants.RECIPIENT_ADDRESS).should('be.visible') - }) - - it('should have all tokens available in the token selector', () => { - // Click on the Token selector - cy.contains('Select an asset*').click() - - const ownedTokens = ['Dai', 'Wrapped Ether', 'Ether', 'Uniswap', 'Gnosis', '0x', 'USD Coin'] - ownedTokens.forEach((token) => { - cy.get('ul[role="listbox"]').contains(token) - }) - }) - - it('should validate token amount', () => { - // Select a token - cy.get('ul[role="listbox"]').contains('Gnosis').click() - - // Insert an incorrect amount - cy.get('input[placeholder="Amount*"]').type('0.4') - - // Selecting more than the balance is not allowed - cy.contains('Maximum value is 0.000004') - - // Form field contains an error class - cy.get('input[placeholder="Amount*"]') - // Parent div is MuiInputBase-root - .parent('div') - .should(($div) => { - // Turn the classList into an array - const classList = Array.from($div[0].classList) - expect(classList).to.include('MuiInputBase-root').and.to.include('Mui-error') - }) - - // Insert a correct amount - cy.get('input[placeholder="Amount*"]').clear().type('0.000002') - - // Form field does not contain an error class - cy.get('input[placeholder="Amount*"]') - // Parent div is MuiInputBase-root - .parent('div') - .should(($div) => { - // Turn the classList into an array - const classList = Array.from($div[0].classList) - // Check if it contains the error class - expect(classList).to.include('MuiInputBase-root').and.not.to.include('Mui-error') - }) - - // Click Send max fills the input with token total amount - cy.contains('Send max').click() - cy.get('input[placeholder="Amount*"]').should('have.value', '0.000004') - }) - - it('should advance to the Review step', () => { - // Clicks Review - cy.contains('Review').click() - - // Modal step 2 - cy.contains('Step 2 of 2').should('be.visible') - }) - }) - - describe('Review modal', () => { - before(() => { - // Wait max 10s for estimate to finish - cy.contains('Submit', { timeout: 10000 }) - }) - - it('should have the same parameters as the previous step', () => { - // Sender - cy.contains('Sending from').parent().next().contains(TEST_SAFE) - // Recipient - cy.contains('Recipient').parent().next().contains(constants.RECIPIENT_ADDRESS) - - // Token value - cy.contains('0.000004 GNO') - }) - - it('should contain a correctly estimated gas limit value', () => { - const GAS_LIMIT = '79804' // gas limit is deterministic - - // Estimated gas price is loaded - cy.contains('Estimated fee price').next().should('not.have.text', '> 0.001 ETH') - - // Click Advanced parameters - cy.contains('Estimated fee price').click() - - // Find Gas limit - cy.contains('Gas limit').next().contains(GAS_LIMIT).should('be.visible') - - // Close info again - cy.contains('Estimated fee price').click() - }) - - it('should contain the Safe nonce upon clicking Advanced parameters', () => { - // Click Advanced parameters - cy.contains('Advanced parameters').click() - // Find Safe nonce - cy.contains('Safe nonce').next().contains(SAFE_NONCE).should('be.visible') - - // Close dialog again - cy.contains('Advanced parameters').click() - }) - }) - }) -}) diff --git a/cypress/e2e/tx_simulation.cy.js b/cypress/e2e/tx_simulation.cy.js deleted file mode 100644 index 4f219d41c3..0000000000 --- a/cypress/e2e/tx_simulation.cy.js +++ /dev/null @@ -1,62 +0,0 @@ -import * as constants from '../support/constants' - -const TEST_SAFE = 'rin:0x11Df0fa87b30080d59eba632570f620e37f2a8f7' - -describe('Tx Simulation', () => { - before(() => { - // Open the Safe used for testing - cy.visit(`/${TEST_SAFE}/home`, { failOnStatusCode: false }) - cy.contains('button', 'Accept selection').click() - - // Open Send Funds Modal - cy.contains('New transaction').click() - cy.contains('Send tokens').click() - - // Choose recipient - cy.get('input[name="recipient"]').should('be.visible') - cy.get('input[name="recipient"]').type(constants.RECIPIENT_ADDRESS, { force: true }) - - // Select asset and amount - cy.get('input[name="tokenAddress"]').parent().click() - cy.get('ul[role="listbox"]').contains('Gnosis').click() - cy.contains('Max').click() - - // go to review step - cy.contains('Next').click() - }) - it('should initially have a successful simulation', () => { - // Simulate - cy.contains('Simulate').click() - - // result exists after max 10 seconds - cy.contains('The transaction was successfully simulated', { timeout: 10000 }) - }) - - it('should show unexpected error for a very low gas limit', () => { - // Set Gas Limit to too low - cy.contains('Estimated fee').click() - cy.contains('Edit').click() - cy.get('input[name="gasLimit"]').clear().type('21000') - cy.contains('Confirm').click() - - // Simulate - cy.contains('Simulate').click() - - // error exists after max 10 seconds - cy.contains('An unexpected error occurred during simulation:', { timeout: 10000 }) - }) - - it('should simulate with failed transaction for a slightly too low gas limit', () => { - // Set Gas Limit to too low - cy.contains('Estimated fee').click() - cy.contains('Edit').click() - cy.get('input[name="gasLimit"]').clear().type('75000') - cy.contains('Confirm').click() - - // Simulate - cy.contains('Simulate').click() - - // failed tx exists after max 10 seconds - cy.contains('out of gas', { timeout: 10000 }) - }) -}) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index e9dce33964..a518c1c918 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -61,6 +61,8 @@ export const validAppUrl = 'https://my-valid-custom-app.com' export const proposeEndpoint = '/**/propose' export const appsEndpoint = '/**/safe-apps' +export const transactionHistoryEndpoint = '**/history' +export const safeListEndpoint = '**/safes' export const mainSideMenuOptions = { home: 'Home', diff --git a/docs/environments.md b/docs/environments.md index 0f43c8d43a..e11d262e76 100644 --- a/docs/environments.md +++ b/docs/environments.md @@ -7,7 +7,7 @@ We have several environments where the app can be deployed: |local|http://localhost:3000/app|local development|`yarn start`|staging| |PRs |`https://--walletweb.review-wallet-web.5afe.dev/`|peer review & feature QA|for all PRs on push|staging| |dev |https://safe-wallet-web.dev.5afe.dev/|preview of all WIP features|on push to the `dev` branch|staging| -|staging|https://safe-wallet-web.staging.5afe.dev/|preview of features before a release|on push to `main`|staging| +|staging|https://safe-wallet-web.staging.5afe.dev/|preview of features before a release|on push to `main`|**production** (for testing)| |production|https://app.safe.global/|live app|deployed by DevOps (see the [Release Procedure](release-procedure.md))|**production**| ## Lifecycle of a feature diff --git a/package.json b/package.json index 1150b1d199..588e07adb1 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "homepage": "https://github.com/safe-global/safe-wallet-web", "license": "GPL-3.0", "type": "module", - "version": "1.22.2", + "version": "1.23.0", "scripts": { "dev": "next dev", "start": "next dev", @@ -17,9 +17,9 @@ "test:coverage": "yarn test --coverage --watchAll=false", "cmp": "./scripts/cmp.sh", "routes": "node scripts/generate-routes.js > src/config/routes.ts && prettier -w src/config/routes.ts && cat src/config/routes.ts", - "css-vars": "ts-node-esm ./scripts/css-vars.ts > ./src/styles/vars.css && prettier -w src/styles/vars.css", + "css-vars": "npx -y tsx ./scripts/css-vars.ts > ./src/styles/vars.css && prettier -w src/styles/vars.css", "generate-types": "typechain --target ethers-v5 --out-dir src/types/contracts ./node_modules/@safe-global/safe-deployments/dist/assets/**/*.json ./node_modules/@safe-global/safe-modules-deployments/dist/assets/**/*.json ./node_modules/@openzeppelin/contracts/build/contracts/ERC20.json ./node_modules/@openzeppelin/contracts/build/contracts/ERC721.json", - "after-install": "yarn generate-types && yarn css-vars", + "after-install": "yarn generate-types", "postinstall": "yarn after-install", "analyze": "cross-env ANALYZE=true yarn build", "cypress:open": "cross-env TZ=UTC cypress open --e2e", @@ -52,11 +52,12 @@ "@safe-global/safe-core-sdk-utils": "^1.7.4", "@safe-global/safe-deployments": "1.25.0", "@safe-global/safe-ethers-lib": "^1.9.4", - "@safe-global/safe-gateway-typescript-sdk": "^3.12.0", + "@safe-global/safe-gateway-typescript-sdk": "^3.13.2", "@safe-global/safe-modules-deployments": "^1.0.0", "@safe-global/safe-react-components": "^2.0.6", "@sentry/react": "^7.74.0", "@sentry/tracing": "^7.74.0", + "@spindl-xyz/attribution-lite": "^1.4.0", "@tkey-mpc/common-types": "^8.2.2", "@truffle/hdwallet-provider": "^2.1.4", "@walletconnect/utils": "^2.10.2", @@ -67,7 +68,7 @@ "@web3-onboard/keystone": "^2.3.7", "@web3-onboard/ledger": "2.3.2", "@web3-onboard/trezor": "^2.4.2", - "@web3-onboard/walletconnect": "^2.4.7", + "@web3-onboard/walletconnect": "^2.5.0", "@web3auth/mpc-core-kit": "^1.1.3", "blo": "^1.1.1", "bn.js": "^5.2.1", @@ -130,11 +131,16 @@ "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", "prettier": "^2.7.0", - "ts-node": "^10.8.2", "ts-prune": "^0.10.3", "typechain": "^8.0.0", "typescript": "4.9.4", "typescript-plugin-css-modules": "^4.2.2", "webpack": "^5.88.2" + }, + "nextBundleAnalysis": { + "budget": null, + "budgetPercentIncreaseRed": 20, + "minimumChangeThreshold": 0, + "showDetails": true } } diff --git a/scripts/github/deploy_docker.sh b/scripts/github/deploy_docker.sh deleted file mode 100644 index 832a4878e5..0000000000 --- a/scripts/github/deploy_docker.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -export DOCKER_BUILDKIT=1 - -if [ "$1" = "dev" -o "$1" = "main" ]; then - # If image does not exist, don't use cache - docker pull safeglobal/$DOCKERHUB_PROJECT:$1 && \ - docker build -t $DOCKERHUB_PROJECT . --cache-from safeglobal/$DOCKERHUB_PROJECT:$1 --build-arg BUILDKIT_INLINE_CACHE=1 || \ - docker build -t $DOCKERHUB_PROJECT . --build-arg BUILDKIT_INLINE_CACHE=1 -else - # Building tag version from staging image (vX.X.X) - docker pull safeglobal/$DOCKERHUB_PROJECT:staging && \ - docker build -t $DOCKERHUB_PROJECT . --cache-from safeglobal/$DOCKERHUB_PROJECT:staging --build-arg BUILDKIT_INLINE_CACHE=1 || \ - docker build -t $DOCKERHUB_PROJECT . --build-arg BUILDKIT_INLINE_CACHE=1 - # Only push latest on release - case $1 in v*) - docker tag $DOCKERHUB_PROJECT safeglobal/$DOCKERHUB_PROJECT:latest - docker push safeglobal/$DOCKERHUB_PROJECT:latest - esac -fi -docker tag $DOCKERHUB_PROJECT safeglobal/$DOCKERHUB_PROJECT:$1 -docker push safeglobal/$DOCKERHUB_PROJECT:$1 diff --git a/src/components/common/AppStoreButton/index.tsx b/src/components/common/AppStoreButton/index.tsx index 6687afe3d2..65fe9eb30a 100644 --- a/src/components/common/AppStoreButton/index.tsx +++ b/src/components/common/AppStoreButton/index.tsx @@ -6,7 +6,6 @@ import { MOBILE_APP_EVENTS, trackEvent } from '@/services/analytics' // App Store campaigns track the user interaction enum LINKS { - pairing = 'https://apps.apple.com/app/apple-store/id1515759131?pt=119497694&ct=Web%20App%20Connect&mt=8', footer = 'https://apps.apple.com/app/apple-store/id1515759131?pt=119497694&ct=Web%20App%20Footer&mt=8', } diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx index 5f0442f50a..6a2ad4caf7 100644 --- a/src/components/common/ConnectWallet/WalletDetails.tsx +++ b/src/components/common/ConnectWallet/WalletDetails.tsx @@ -1,8 +1,13 @@ -import { Box, Divider, SvgIcon, Typography } from '@mui/material' +import { Box, Divider, Skeleton, SvgIcon, Typography } from '@mui/material' +import dynamic from 'next/dynamic' import type { ReactElement } from 'react' import LockIcon from '@/public/images/common/lock.svg' -import SocialSigner from '@/components/common/SocialSigner' + +const SocialSigner = dynamic(() => import('@/components/common/SocialSigner'), { + loading: () => , +}) + import WalletLogin from '@/components/welcome/WelcomeLogin/WalletLogin' const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement => { diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index b41bbc8399..71036103a6 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -64,13 +64,6 @@ border-bottom: 1px solid var(--color-border-light); } -.pairingDetails { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-2); -} - .loginButton { min-height: 42px; } diff --git a/src/components/common/Footer/index.tsx b/src/components/common/Footer/index.tsx index 545b1f43aa..59d519013a 100644 --- a/src/components/common/Footer/index.tsx +++ b/src/components/common/Footer/index.tsx @@ -1,5 +1,6 @@ import type { ReactElement, ReactNode } from 'react' -import { Typography } from '@mui/material' +import { SvgIcon, Typography } from '@mui/material' +import GitHubIcon from '@mui/icons-material/GitHub' import Link from 'next/link' import { useRouter } from 'next/router' import css from './styles.module.css' @@ -8,7 +9,7 @@ import packageJson from '../../../../package.json' import AppstoreButton from '../AppStoreButton' import ExternalLink from '../ExternalLink' import MUILink from '@mui/material/Link' -import { IS_DEV, IS_OFFICIAL_HOST } from '@/config/constants' +import { HELP_CENTER_URL, IS_DEV, IS_OFFICIAL_HOST } from '@/config/constants' const footerPages = [ AppRoutes.welcome.index, @@ -67,14 +68,19 @@ const Footer = (): ReactElement | null => {
  • Preferences
  • +
  • + + Help + +
  • ) : (
  • {'This is an unofficial distribution of Safe{Wallet}'}
  • )}
  • - - v{packageJson.version} + + v{packageJson.version}
  • diff --git a/src/components/common/ModalDialog/index.tsx b/src/components/common/ModalDialog/index.tsx index cede7fe2cf..c9d32eafe6 100644 --- a/src/components/common/ModalDialog/index.tsx +++ b/src/components/common/ModalDialog/index.tsx @@ -20,12 +20,13 @@ interface DialogTitleProps { export const ModalDialogTitle = ({ children, onClose, hideChainIndicator = false, ...other }: DialogTitleProps) => { return ( - + {children} {!hideChainIndicator && } {onClose ? ( { onClose(e, 'backdropClick') @@ -56,6 +57,7 @@ const ModalDialog = ({ return ( { - return ( - - The {'Safe{Wallet}'} web-mobile pairing feature will be discontinued from 15th November 2023. Please migrate to a - different signer wallet before this date. - - ) -} - -export default PairingDeprecationWarning diff --git a/src/components/common/PairingDetails/PairingDescription.tsx b/src/components/common/PairingDetails/PairingDescription.tsx deleted file mode 100644 index bdcad66d90..0000000000 --- a/src/components/common/PairingDetails/PairingDescription.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Typography } from '@mui/material' -import type { ReactElement } from 'react' - -import AppStoreButton from '@/components/common/AppStoreButton' -import ExternalLink from '../ExternalLink' -import { HelpCenterArticle } from '@/config/constants' - -const PairingDescription = (): ReactElement => { - return ( - <> - - Scan this code in the {'Safe{Wallet}'} mobile app to sign transactions with your mobile device. -
    - - Learn more about this feature. - -
    - - - - ) -} - -export default PairingDescription diff --git a/src/components/common/PairingDetails/PairingQRCode.tsx b/src/components/common/PairingDetails/PairingQRCode.tsx deleted file mode 100644 index a53f5dbce1..0000000000 --- a/src/components/common/PairingDetails/PairingQRCode.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect, useState } from 'react' -import { IconButton, Box } from '@mui/material' -import RefreshIcon from '@mui/icons-material/Refresh' -import type { ReactElement } from 'react' - -import { getPairingConnector, usePairingConnector, WalletConnectEvents } from '@/services/pairing/connector' -import usePairingUri from '@/services/pairing/hooks' -import useChainId from '@/hooks/useChainId' -import QRCode from '@/components/common/QRCode' - -const QR_CODE_SIZE = 100 - -const PairingQRCode = ({ size = QR_CODE_SIZE }: { size?: number }): ReactElement => { - const chainId = useChainId() - const uri = usePairingUri() - const connector = usePairingConnector() - const [displayRefresh, setDisplayRefresh] = useState(false) - - // Workaround because the disconnect listener in useInitPairing is not picking up the event - useEffect(() => { - connector?.on(WalletConnectEvents.DISCONNECT, () => { - setDisplayRefresh(true) - }) - }, [connector]) - - const handleRefresh = () => { - setDisplayRefresh(false) - getPairingConnector()?.createSession({ chainId: +chainId }) - } - - if (displayRefresh || (connector && !connector.handshakeTopic)) { - return ( - theme.palette.background.main, - }} - > - - - - - ) - } - - return -} - -export default PairingQRCode diff --git a/src/components/common/SocialSigner/index.tsx b/src/components/common/SocialSigner/index.tsx index c7f1138219..0282008027 100644 --- a/src/components/common/SocialSigner/index.tsx +++ b/src/components/common/SocialSigner/index.tsx @@ -1,4 +1,7 @@ +import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' +import { type ISocialWalletService } from '@/services/mpc/interfaces' import { Box, Button, LinearProgress, SvgIcon, Tooltip, Typography } from '@mui/material' +import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' import { useCallback, useContext, useMemo, useState } from 'react' import { PasswordRecovery } from '@/components/common/SocialSigner/PasswordRecovery' import GoogleLogo from '@/public/images/welcome/logo-google.svg' @@ -15,8 +18,6 @@ import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import { CGW_NAMES } from '@/hooks/wallets/consts' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import { TxModalContext } from '@/components/tx-flow' -import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' import madProps from '@/utils/mad-props' import { asError } from '@/services/exceptions/utils' import ErrorMessage from '@/components/tx/ErrorMessage' @@ -41,7 +42,7 @@ const useIsSocialWalletEnabled = () => { } type SocialSignerLoginProps = { - socialWalletService: ReturnType + socialWalletService: ISocialWalletService | undefined wallet: ReturnType supportedChains: ReturnType isMPCLoginEnabled: ReturnType @@ -113,6 +114,7 @@ export const SocialSigner = ({ {isSocialLogin && userInfo ? ( )} - + ) } diff --git a/src/components/common/WalletInfo/styles.module.css b/src/components/common/WalletInfo/styles.module.css index a8c3a97308..e12fa9a690 100644 --- a/src/components/common/WalletInfo/styles.module.css +++ b/src/components/common/WalletInfo/styles.module.css @@ -1,3 +1,10 @@ +.container { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--space-2); +} + .accountContainer { width: 100%; margin-bottom: var(--space-1); diff --git a/src/components/cookie-policy/index.tsx b/src/components/cookie-policy/index.tsx deleted file mode 100644 index 3cf91a4fd4..0000000000 --- a/src/components/cookie-policy/index.tsx +++ /dev/null @@ -1,576 +0,0 @@ -import css from './styles.module.css' -import Link from 'next/link' -import MUILink from '@mui/material/Link' -import { AppRoutes } from '@/config/routes' - -const SafeCookiePolicy = () => { - return ( -
    -

    Cookie Policy

    -

    Last updated on March 2023

    -

    - As described in our{' '} - - Privacy Policy - - , for general web-browsing of this website, your personal data is not revealed to us, although certain - statistical information is available to us via our internet service provider as well as through the use of - special tracking technologies. Such information tells us about the pages you are clicking on or the hardware you - are using, but not your name, age, address or anything we can use to identify you personally. We exclusively - process your personal data in pseudonymised form. -

    -

    - This Cookie Policy applies to our website at{' '} - - https://app.safe.global - -  and sets out some further detail on how and why we use these technologies on our website.{' '} -

    -

    - In this policy, "we", "us" and "our" refers to Core Contributors GmbH a company - incorporated in Germany with its registered address at Skalitzer Str. 85-86, ℅ Full Node, 10997 Berlin, Germany. - The terms “you” and “your” includes our clients, business partners and users of this - website.{' '} -

    -

    - By using our website, you consent to storage and access to cookies and other technologies on your device, in - accordance with this Cookie Policy. -

    -

    What are cookies?

    -

    - Cookies are a feature of web browser software that allows web servers to recognize the computer or device used - to access a website. A cookie is a small text file that a website saves on your computer or mobile device when - you visit the site. It enables the website to remember your actions and preferences (such as login, language, - font size and other display preferences) over a period of time, so you don't have to keep re-entering them - whenever you come back to the site or browse from one page to another. -

    -

    What are the different types of cookies?

    -

    A cookie can be classified by its lifespan and the domain to which it belongs.

    -

    By lifespan, a cookie is either a:

    -
      -
    1. session cookie which is erased when the user closes the browser; or
    2. -
    3. - persistent cookie which is saved to the hard drive and remains on the user's computer/device for a - pre-defined period of time. As for the domain to which it belongs, cookies are either: -
    4. -
    -
      -
    1. - first-party cookies which are set by the web server of the visited page and share the same domain (i.e. set by - us); or -
    2. -
    3. third-party cookies stored by a different domain to the visited page's domain.
    4. -
    -

    What cookies do we use and why?

    -

    We list all the cookies we use on this website in the APPENDIX below.

    -

    - We do not use cookies set by ourselves via our web developers (first-party cookies). We only have those set by - others (third-party cookies). -

    -

    - Cookies are also sometimes classified by reference to their purpose. We use the following cookies for the - following purposes: -

    -
      -
    1. - Analytical/performance cookies: They allow us to recognize and count the number of visitors and to see how - visitors move around our website when they are using it, as well as dates and times they visit. This helps us - to improve the way our website works, for example, by ensuring that users are finding what they are looking - for easily. -
    2. -
    3. - Targeting cookies: These cookies record your visit to our website, the pages you have visited and the links - you have followed, as well as time spent on our website, and the websites visited just before and just after - our website. We will use this information to make our website and the advertising displayed on it more - relevant to your interests. We may also share this information with third parties for this purpose. -
    4. -
    -

    - In general, we use cookies and other technologies (such as web server logs) on our website to enhance your - experience and to collect information about how our website is used.{' '} -

    -

    - We will retain and evaluate information on your recent visits to our website and how you move around different - sections of our website for analytics purposes to understand how people use our website so that we can make it - more intuitive. The information also helps us to understand which parts of this website are most popular and - generally to assess user behavior and characteristics to measure interest in and use of the various areas of our - website. This then allows us to improve our website and the way we market our business. -

    -

    - This information may also be used to help us to improve, administer and diagnose problems with our server and - website. The information also helps us monitor traffic on our website so that we can manage our website's - capacity and efficiency. -

    -

    Other Technologies

    -

    - We may allow others to provide analytics services and serve advertisements on our behalf. In addition to the - uses of cookies described above, these entities may use other methods, such as the technologies described below, - to collect information about your use of our website and other websites and online services. -

    -

    - Pixels tags. Pixel tags (which are also called clear GIFs, web beacons, or pixels), are small pieces of code - that can be embedded on websites and emails. Pixels tags may be used to learn how you interact with our website - pages and emails, and this information helps us, and our partners provide you with a more tailored experience. -

    -

    - Device Identifiers. A device identifier is a unique label that can be used to identify a mobile device. Device - identifiers may be used to track, analyze and improve the performance of the website and ads delivered. -

    -

    What data is collected by cookies and other technologies on our website?

    -

    This information may include:

    -
      -
    1. - the IP and logical address of the server you are using (but the last digits are anonymized so we cannot - identify you). -
    2. -
    3. the top level domain name from which you access the internet (for example .ie, .com, etc)
    4. -
    5. the type of browser you are using,
    6. -
    7. the date and time you access our website
    8. -
    9. the internet address linking to our website.
    10. -
    -

    This website also uses cookies to:

    -
      -
    1. remember you and your actions while navigating between pages;
    2. -
    3. remember if you have agreed (or not) to our use of cookies on our website;
    4. -
    5. ensure the security of the website;
    6. -
    7. monitor and improve the performance of servers hosting the site;
    8. -
    9. distinguish users and sessions;
    10. -
    11. Improving the speed of the site when you access content repeatedly;
    12. -
    13. determine new sessions and visits;
    14. -
    15. show the traffic source or campaign that explains how you may have reached our website; and
    16. -
    17. allow us to store any customization preferences where our website allows this
    18. -
    -

    - We may also use other services, such as{' '} - - - Google Analytics - - -  (described below) or other third-party cookies, to assist with analyzing performance on our website. As - part of providing these services, these service providers may use cookies and the technologies described below - to collect and store information about your device, such as time of visit, pages visited, time spent on each - page of our website, links clicked and conversion information, IP address, browser, mobile network information, - and type of operating system used. -

    -

    Google Analytics Cookies

    -

    - This website uses{' '} - - - Google Analytics - - - , a web analytics service provided by Google, Inc. ("Google"). -

    -

    - We use Google Analytics to track your preferences and also to identify popular sections of our website. Use of - Google Analytics in this way, enables us to adapt the content of our website more specifically to your needs and - thereby improve what we can offer to you. -

    -

    - Google will use this information for the purpose of evaluating your use of our website, compiling reports on - website activity for website operators and providing other services relating to website activity and internet - usage. Google may also transfer this information to third parties where required to do so by law, or where such - third parties process the information on Google's behalf. Google will not associate your IP address with - any other data held by Google. -

    -

    In particular Google Analytics tells us

    -
      -
    1. your IP address (last 3 digits are masked);
    2. -
    3. the number of pages visited;
    4. -
    5. the time and duration of the visit;
    6. -
    7. your location;
    8. -
    9. the website you came from (if any);
    10. -
    11. the type of hardware you use (i.e. whether you are browsing from a desktop or a mobile device);
    12. -
    13. the software used (type of browser); and
    14. -
    15. your general interaction with our website.
    16. -
    -

    - As stated above, cookie-related information is not used to identify you personally, and what is compiled is only - aggregate data that tells us, for example, what countries we are most popular in, but not that you live in a - particular country or your precise location when you visited our website (this is because we have only half the - information- we know the country the person is browsing from, but not the name of person who is browsing). In - such an example Google will analyze the number of users for us, but the relevant cookies do not reveal their - identities. -

    -

    - By using this website, you consent to the processing of data about you by Google in the manner and for the - purposes set out above. Google Analytics, its purpose and function is further explained on the{' '} - - - Google Analytics website - - - . -

    -

    - For more information about Google Analytics cookies, please see Google's help pages and privacy policy:{' '} - - - Google's Privacy Policy - - -  and{' '} - - - Google Analytics Help pages - - - . For further information about the use of these cookies by Google{' '} - - - click here - - - . -

    -

    - What if you don’t agree with us monitoring your use of our website (even if we don't collect your - personal data)? -

    -

    - Enabling these cookies is not strictly necessary for our website to work but it will provide you with a better - browsing experience. You can delete or block the cookies we set, but if you do that, some features of this - website may not work as intended. -

    -

    - Most browsers are initially set to accept cookies. If you prefer, you can set your browser to refuse cookies and - control and/or delete cookies as you wish – for details, see{' '} - - - https://aboutcookies.org - - - . You can delete all cookies that are already on your device and you can set most browsers to prevent them from - being placed. You should be aware that if you do this, you may have to manually adjust some preferences every - time you visit an Internet site and some services and functionalities may not work if you do not accept the - cookies they send. -

    -

    - Advertisers and business partners that you access on or through our website may also send you cookies. We do not - control any cookies outside of our website. -

    -

    - If you have any further questions regarding disabling cookies you should consult with your preferred - browser’s provider or manufacturer. -

    -

    - In order to implement your objection it may be necessary to install an opt-out cookie on your browser. This - cookie will only indicate that you have opted out. It is important to note, that for technical reasons, the - opt-out cookie will only affect the browser from which you actively object from. If you delete the cookies in - your browser or use a different end device or browser, you will need to opt out again. -

    -

    - To opt out of being tracked by Google Analytics across all websites, Google has developed Google Analytics - opt-out browser add-on. If you would like to opt out of Google Analytics, you have the option of downloading and - installing this browser add-on which can be found under the link:{' '} - - - https://tools.google.com/dlpage/gaoptout - - - . -

    -

    Revisions to this Cookie Policy

    -

    - On this website, you can always view the latest version of our Privacy Policy and our Cookie Policy. We may - modify this Cookie Policy from time to time. If we make changes to this Cookie Policy, we will provide notice of - such changes, such as by sending an email notification, providing notice through our website or updating the - ‘Last Updated’ date at the beginning of this Cookie Policy. The amended Cookie Policy will be - effective immediately after the date it is posted. By continuing to access or use our website after the - effective date, you confirm your acceptance of the revised Cookie Policy and all of the terms incorporated - therein by reference. We encourage you to review our Privacy Policy and our Cookie Policy whenever you access or - use our website to stay informed about our information practices and the choices available to you. -

    -

    - If you do not accept changes which are made to this Cookie Policy, or take any measures described above to - opt-out by removing or rejecting cookies, you may continue to use this website but accept that it may not - display and/or function as intended by us. Any social media channels connected to us and third party - applications will be subject to the privacy and cookie policies and practices of the relevant platform providers - which, unless otherwise indicated, are not affiliated or associated with us Your exercise of any rights to - opt-out may also impact how our information and content is displayed and/or accessible to you on this website - and on other websites. -

    -

    APPENDIX

    -

    Overview of cookies placed and the consequences if the cookies are not placed.

    -

    First-party cookies

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -

    #

    -
    -

    Name of cookie

    -
    -

    Domain

    -
    -

    Purpose(s) of cookie

    -
    -

    Storage period of cookie

    -
    -

    Consequences is cookie is not accepted

    -
    -

    1

    -
    -

    _BEAMER_FILTER_BY_URL_{'{productID}'}

    -
    -

    app.safe.global

    -
    -

    Stores whether to apply URL filtering on the feed.

    -
    -

    20 minutes

    -
    -

    User activity won't be tracked

    -
    -

    2

    -
    -

    _BEAMER_DATE_{'{productID}'}

    -
    -

    app.safe.global

    -
    -

    Stores the latest date in which the feed was opened.

    -
    -

    300 days

    -
    -

    User activity won't be tracked

    -
    -

    3

    -
    -

    _BEAMER_LAST_POST_SHOWN_{'{productID}'}

    -
    -

    app.safe.global

    -
    -

    Stores the ID of the last post shown as a teaser.

    -
    -

    Session

    -
    -

    User activity won't be tracked

    -
    -

    4

    -
    -

    _BEAMER_BOOSTED_ANNOUNCEMENT_DATE_{'{productID}'}

    -
    -

    app.safe.global

    -
    -

    Stores the latest date in which a boosted announcement was displayed.

    -
    -

    300 days

    -
    -

    User activity won't be tracked

    -
    -

    5

    -
    -

    _BEAMER_FIRST_VISIT_{'{productID}'}

    -
    -

    app.safe.global

    -
    -

    Stores the date of this user’s first visit to the site.

    -
    -

    300 days

    -
    -

    User activity won't be tracked

    -
    -

    6

    -
    -

    _BEAMER_USER_ID_{'{productID}'}

    -
    -

    app.safe.global

    -
    -

    Stores an internal ID for this user.

    -
    -

    300 days

    -
    -

    User activity won't be tracked

    -
    -

    Third-party cookies

    -

    The cookies from this table can be set by third-party wallets.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -

    #

    -
    -

    Name of cookie

    -
    -

    Domain

    -
    -

    Purpose(s) of cookie

    -
    -

    Storage period of cookie

    -
    -

    Consequences is cookie is not accepted

    -
    -

    1

    -
    -

    _ga

    -
    -

    safe.global

    -
    -

    Used to distinguish users

    -
    -

    2 years from set/update

    -
    -

    User activity won't be tracked

    -
    -

    2

    -
    -

    _ga

    -
    -

    getbeamer.com

    -
    -

    Used to distinguish users

    -
    -

    2 years from set/update

    -
    -

    User activity won't be tracked

    -
    -

    3

    -
    -

    _gid

    -
    -

    getbeamer.com

    -
    -

    Used to distinguish users

    -
    -

    24 hours

    -
    -

    User activity won't be tracked

    -
    -

    4

    -
    -

    _BEAMER_USER_ID_{'{productID}'}

    -
    -

    getbeamer.com

    -
    -

    Stores an internal ID for this user.

    -
    -

    300 days

    -
    -

    User activity won't be tracked

    -
    -

    5

    -
    -

    JSESSIONID

    -
    -

    app.getbeamer.com

    -
    -

    Stores an internal ID for this user.

    -
    -

    Session

    -
    -

    User activity won't be tracked

    -
    -
    - ) -} - -export default SafeCookiePolicy diff --git a/src/components/cookie-policy/styles.module.css b/src/components/cookie-policy/styles.module.css deleted file mode 100644 index fb38123c06..0000000000 --- a/src/components/cookie-policy/styles.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.table { - width: 100%; - border-spacing: 0; - border: 0; - border-collapse: collapse; -} - -.table td { - border: 1px solid; - padding: 0 8px; -} - -.table tr:first-child td { - font-weight: bold; -} diff --git a/src/components/dashboard/FeaturedApps/FeaturedApps.tsx b/src/components/dashboard/FeaturedApps/FeaturedApps.tsx index 6f9ebe139c..39b868b2f7 100644 --- a/src/components/dashboard/FeaturedApps/FeaturedApps.tsx +++ b/src/components/dashboard/FeaturedApps/FeaturedApps.tsx @@ -10,11 +10,7 @@ import { SafeAppsTag } from '@/config/constants' import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard' import { WalletConnectContext } from '@/services/walletconnect/WalletConnectContext' - -const isWalletConnectSafeApp = (app: SafeAppData): boolean => { - const WALLET_CONNECT = /wallet-connect/ - return WALLET_CONNECT.test(app.url) -} +import { isWalletConnectSafeApp } from '@/services/walletconnect/utils' const FeaturedAppCard = ({ app }: { app: SafeAppData }) => ( @@ -62,7 +58,7 @@ export const FeaturedApps = ({ stackedLayout }: { stackedLayout: boolean }): Rea > {featuredApps?.map((app) => ( - {isWalletConnectSafeApp(app) ? ( + {isWalletConnectSafeApp(app.url) ? ( diff --git a/src/components/imprint/index.tsx b/src/components/imprint/index.tsx deleted file mode 100644 index 6fb02ee89c..0000000000 --- a/src/components/imprint/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Typography } from '@mui/material' -import Link from 'next/link' -import MUILink from '@mui/material/Link' - -const SafeImprint = () => { - return ( -
    - - Imprint & Disclaimer - - - Information in accordance with section 5 of the Telemedia Act (TMG, Germany): - - - Core Contributors GmbH -
    - ℅ Full Node -
    - Skalitzer Str. 85-86 -
    - 10997 Berlin Germany -
    - - Managing directors: Richard Meißner, Tobias Schubotz -
    - Contact:{' '} - - info@cc0x.dev - -
    - District Court: Berlin Charlottenburg -
    - Register Number: HRB 240421 B -
    - - Disclaimer - - - Accountability for content - - - The contents of our pages have been created with the utmost care. However, we cannot guarantee the contents’ - accuracy, completeness or topicality. According to statutory provisions, we are furthermore responsible for our - own content on these web pages. In this context, please note that we are accordingly not obliged to monitor - merely the transmitted or saved information of third parties, or investigate circumstances pointing to illegal - activity. Our obligations to remove or block the use of information under generally applicable laws remain - unaffected by this as per §§ 8 to 10 of the Telemedia Act (TMG). - - - Accountability for links - - - Responsibility for the content of external links (to web pages of third parties) lies solely with the operators - of the linked pages. No violations were evident to us at the time of linking. Should any legal infringement - become known to us, we will remove the respective link immediately. - - - Copyright - - - This website and their contents are subject to copyright laws.{' '} - - - The code is open-source, released under GPL-3.0. - - - -
    - ) -} - -export default SafeImprint diff --git a/src/components/licenses/index.tsx b/src/components/licenses/index.tsx deleted file mode 100644 index fe6d496ebb..0000000000 --- a/src/components/licenses/index.tsx +++ /dev/null @@ -1,747 +0,0 @@ -import { Typography, Table, TableBody, TableRow, TableCell, TableHead, TableContainer, Box } from '@mui/material' -import ExternalLink from '@/components/common/ExternalLink' -import Paper from '@mui/material/Paper' - -const SafeLicenses = () => { - return ( - <> - - Licenses - - - Libraries we use - - - - This page contains a list of attribution notices for third party software that may be contained in portions of - the {'Safe{Wallet}'}. We thank the open source community for all of their contributions. - - - Android - - - - - - - Library - - - License - - - - - - AndroidX - - - https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/LICENSE.txt - - - - - Bivrost for Kotlin - - - https://github.com/gnosis/bivrost-kotlin/blob/master/LICENSE - - - - - Dagger - - - {' '} - https://github.com/google/dagger#license{' '} - - - - - FloatingActionButton - - - https://github.com/Clans/FloatingActionButton/blob/master/LICENSE - - - - - Material Progress Bar - - - https://github.com/DreaminginCodeZH/MaterialProgressBar/blob/master/LICENSE - - - - - Kethereum - - - https://github.com/walleth/kethereum/blob/master/LICENSE - - - - - Koptional - - - {' '} - https://github.com/gojuno/koptional#license{' '} - - - - - Moshi - - - {' '} - https://github.com/square/moshi#license{' '} - - - - - OkHttp - - - {' '} - https://github.com/square/okhttp#license{' '} - - - - - Okio - - - {' '} - https://github.com/square/okio#license{' '} - - - - - Phrase - - - {' '} - https://github.com/square/phrase/#license{' '} - - - - - Picasso - - - {' '} - https://github.com/square/picasso#license{' '} - - - - - ReTrofit - - - {' '} - https://github.com/square/reTrofit#license{' '} - - - - - RxAndroid - - - https://github.com/ReactiveX/RxAndroid#license - - - - - RxBinding - - - https://github.com/JakeWharton/RxBinding#license - - - - - RxJava - - - {' '} - https://github.com/ReactiveX/RxJava#license{' '} - - - - - RxKotlin - - - https://github.com/ReactiveX/RxKotlin/blob/2.x/LICENSE - - - - - SpongyCastle - - - https://github.com/rtyley/spongycastle/blob/spongy-master/LICENSE.html - - - - - Svalinn Android - - - https://github.com/gnosis/svalinn-kotlin/blob/master/LICENSE - - - - - Timber - - - https://github.com/JakeWharton/timber#license - - - - - Zxing - - - https://github.com/zxing/zxing/blob/master/LICENSE - - - - -
    -
    -
    - - - iOS - - - - - - - Library - - - License - - - - - - BigInt - - - https://github.com/attaswift/BigInt/blob/master/LICENSE.md - - - - - BlockiesSwift - - - https://github.com/gnosis/BlockiesSwift/blob/master/LICENSE - - - - - CryptoEthereumSwift - - - https://github.com/yuzushioh/CryptoEthereumSwift/blob/master/LICENSE - - - - - CryptoSwift - - - https://github.com/krzyzanowskim/CryptoSwift#license - - - - - DateTools - - - {' '} - https://github.com/gnosis/DateTools#license{' '} - - - - - EthereumKit - - - https://github.com/D-Technologies/EthereumKit#license - - - - - Keycard.swift - - - https://github.com/gnosis/Keycard.swift/blob/master/LICENSE - - - - - Kingfisher - - - https://github.com/onevcat/Kingfisher#license - - - - - SipHash - - - https://github.com/attaswift/SipHash/blob/master/LICENSE.md - - - - - Starscream - - - https://github.com/daltoniam/Starscream/blob/master/LICENSE - - - - - RsBarcodesSwift - - - https://github.com/yeahdongcn/RSBarcodes_Swift#license - - - - - libidn2 - - - https://github.com/gnosis/libidn2/blob/master/COPYING.LESSERv3 - - - - - libunisTring - - - https://github.com/gnosis/libunisTring/blob/master/COPYING.LIB - - - - -
    -
    -
    - - - Web - - - - - - Library - License - - - - - @date-io/date-fns - - - https://github.com/dmtrKovalenko/date-io/blob/master/LICENSE - - - - - @emotion/cache - - - https://github.com/emotion-js/emotion/blob/main/LICENSE - - - - - @emotion/react - - - https://github.com/emotion-js/emotion/blob/main/LICENSE - - - - - @emotion/server - - - https://github.com/emotion-js/emotion/blob/main/LICENSE - - - - - @emotion/styled - - - https://github.com/emotion-js/emotion/blob/main/LICENSE - - - - - @safe-global/safe-modules-deployments - - - https://github.com/safe-global/safe-modules-deployments/blob/main/LICENSE - - - - - @mui/icons-material - - - https://github.com/mui/material-ui/blob/master/LICENSE - - - - - @mui/material - - - https://github.com/mui/material-ui/blob/master/LICENSE - - - - - @mui/x-date-pickers - - - https://github.com/mui/mui-x#mit-vs-commercial-licenses - - - - - @reduxjs/toolkit - - - https://github.com/reduxjs/redux-toolkit/blob/master/LICENSE - - - - - @safe-global/safe-apps-sdk - - - https://github.com/safe-global/safe-apps-sdk/blob/main/LICENSE.md - - - - - @safe-global/safe-core-sdk - - - https://github.com/safe-global/safe-core-sdk/blob/main/LICENSE.md - - - - - @safe-global/safe-core-sdk-utils - - - https://github.com/safe-global/safe-core-sdk/blob/main/LICENSE.md - - - - - @safe-global/safe-deployments - - - https://github.com/safe-global/safe-deployments/blob/main/LICENSE - - - - - @safe-global/safe-ethers-lib - - - https://github.com/safe-global/safe-core-sdk/blob/main/LICENSE.md - - - - - @safe-global/safe-gateway-typescript-sdk - - - https://github.com/safe-global/safe-gateway-typescript-sdk/blob/main/LICENSE.md - - - - - @safe-global/safe-react-components - - - https://github.com/safe-global/safe-react-components/blob/main/LICENSE.md - - - - - @sentry/react - - - https://github.com/getsentry/sentry-javascript/blob/develop/LICENSE - - - - - @sentry/tracing - - - https://github.com/getsentry/sentry-javascript/blob/develop/LICENSE - - - - - @truffle/hdwallet-provider - - - https://github.com/trufflesuite/truffle/blob/develop/LICENSE - - - - - @web3-onboard/coinbase - - - https://github.com/blocknative/web3-onboard/blob/main/LICENSE - - - - - @web3-onboard/core - - - https://github.com/blocknative/web3-onboard/blob/main/LICENSE - - - - - @web3-onboard/injected-wallets - - - https://github.com/blocknative/web3-onboard/blob/main/LICENSE - - - - - @web3-onboard/keystone - - - https://github.com/blocknative/web3-onboard/blob/main/LICENSE - - - - - @web3-onboard/ledger - - - https://github.com/blocknative/web3-onboard/blob/main/LICENSE - - - - - @web3-onboard/trezor - - - https://github.com/blocknative/web3-onboard/blob/main/LICENSE - - - - - @web3-onboard/walletconnect - - - https://github.com/blocknative/web3-onboard/blob/main/LICENSE - - - - - classnames - - - https://github.com/JedWatson/classnames/blob/main/LICENSE - - - - - date-fns - - - https://github.com/date-fns/date-fns/blob/main/LICENSE.md - - - - - ethereum-blockies-base64 - - - https://github.com/MyCryptoHQ/ethereum-blockies-base64 - - - - - ethers - - - https://github.com/ethers-io/ethers.js/blob/main/LICENSE.md - - - - - exponential-backoff - - - https://github.com/coveo/exponential-backoff/blob/master/LICENSE - - - - - fuse.js - - - https://github.com/krisk/Fuse/blob/master/LICENSE - - - - - js-cookie - - - https://github.com/js-cookie/js-cookie/blob/main/LICENSE - - - - - lodash - - - https://github.com/lodash/lodash/blob/master/LICENSE - - - - - next - - - https://github.com/vercel/next.js/blob/canary/LICENSE - - - - - next-pwa - - - https://github.com/shadowwalker/next-pwa/blob/master/LICENSE - - - - - papaparse - - - https://github.com/mholt/PapaParse/blob/master/LICENSE - - - - - qrcode.react - - - https://github.com/zpao/qrcode.react/blob/main/LICENSE - - - - - react - - - https://github.com/facebook/react/blob/main/LICENSE - - - - - react-dom - - - https://github.com/facebook/react/blob/main/LICENSE - - - - - react-dropzone - - - https://github.com/react-dropzone/react-dropzone/blob/master/LICENSE - - - - - react-gtm-module - - - https://github.com/alinemorelli/react-gtm/blob/master/LICENSE - - - - - react-hook-form - - - https://github.com/react-hook-form/react-hook-form/blob/master/LICENSE - - - - - react-papaparse - - - https://github.com/Bunlong/react-papaparse/blob/master/LICENSE - - - - - react-qr-reader - - - https://github.com/JodusNodus/react-qr-reader/blob/master/LICENSE - - - - - react-redux - - - https://github.com/reduxjs/react-redux/blob/master/LICENSE - - - - - semver - - - https://github.com/npm/node-semver/blob/main/LICENSE - - - - -
    -
    -
    - - ) -} - -export default SafeLicenses diff --git a/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx b/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx index 81ee517a65..f791ff5ffd 100644 --- a/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx +++ b/src/components/new-safe/create/steps/OwnerPolicyStep/index.tsx @@ -170,10 +170,16 @@ const OwnerPolicyStep = ({ - - diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index 40225cdf2f..e9b8afc372 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -72,6 +72,7 @@ export const NetworkFee = ({ Your account is sponsored by {chain?.chainName + {data.owners.map((owner, index) => ( )} - + - - diff --git a/src/components/new-safe/create/steps/SetNameStep/index.tsx b/src/components/new-safe/create/steps/SetNameStep/index.tsx index e6efd1fcad..703cafc303 100644 --- a/src/components/new-safe/create/steps/SetNameStep/index.tsx +++ b/src/components/new-safe/create/steps/SetNameStep/index.tsx @@ -117,10 +117,10 @@ function SetNameStep({ - - diff --git a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx index f9b469476e..90300a14bc 100644 --- a/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx +++ b/src/components/new-safe/create/steps/StatusStep/StatusMessage.tsx @@ -64,7 +64,7 @@ const StatusMessage = ({ status, isError }: { status: SafeCreationStatus; isErro return ( <> - + {stepInfo.description} diff --git a/src/components/new-safe/create/steps/StatusStep/index.tsx b/src/components/new-safe/create/steps/StatusStep/index.tsx index 642b2ccd05..30c1cba83f 100644 --- a/src/components/new-safe/create/steps/StatusStep/index.tsx +++ b/src/components/new-safe/create/steps/StatusStep/index.tsx @@ -105,7 +105,7 @@ export const CreateSafeStatus = ({ data, setProgressColor, setStep }: StepRender - diff --git a/src/components/nfts/NftGrid/index.tsx b/src/components/nfts/NftGrid/index.tsx index 13e79cb71b..e35662b966 100644 --- a/src/components/nfts/NftGrid/index.tsx +++ b/src/components/nfts/NftGrid/index.tsx @@ -68,7 +68,15 @@ const headCells = [ const stopPropagation = (e: SyntheticEvent) => e.stopPropagation() const NftIndicator = ({ color }: { color: SvgIconProps['color'] }) => ( - + ) const activeNftIcon = @@ -191,7 +199,7 @@ const NftGrid = ({ const sx = item.imageUri ? { cursor: 'pointer' } : undefined return ( - + {/* Collection name */} @@ -233,7 +241,11 @@ const NftGrid = ({ {/* Checkbox */} - onCheckboxClick(e, item)} /> + onCheckboxClick(e, item)} + /> {/* Insert the children at the end of the table */} {index === filteredNfts.length - 1 && children} diff --git a/src/components/nfts/NftSendForm/index.tsx b/src/components/nfts/NftSendForm/index.tsx index edfb4f5735..9665dcd4e3 100644 --- a/src/components/nfts/NftSendForm/index.tsx +++ b/src/components/nfts/NftSendForm/index.tsx @@ -32,6 +32,7 @@ const NftSendForm = ({ selectedNfts }: NftSendFormProps): ReactElement => { {(isOk) => ( )} diff --git a/src/components/tx-flow/flows/AddOwner/ChooseOwner.tsx b/src/components/tx-flow/flows/AddOwner/ChooseOwner.tsx index 644a057c46..4950dd6870 100644 --- a/src/components/tx-flow/flows/AddOwner/ChooseOwner.tsx +++ b/src/components/tx-flow/flows/AddOwner/ChooseOwner.tsx @@ -153,7 +153,7 @@ export const ChooseOwner = ({ control={control} name="threshold" render={({ field }) => ( - + {safe.owners.map((_, idx) => ( {idx + 1} @@ -178,7 +178,7 @@ export const ChooseOwner = ({ - diff --git a/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx b/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx index 464d2bf570..0cf8a7bf43 100644 --- a/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx +++ b/src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx @@ -14,6 +14,7 @@ import { OwnerList } from '../../common/OwnerList' import MinusIcon from '@/public/images/common/minus.svg' import EthHashInfo from '@/components/common/EthHashInfo' import commonCss from '@/components/tx-flow/common/styles.module.css' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' export const ReviewOwner = ({ params }: { params: AddOwnerFlowProps | ReplaceOwnerFlowProps }) => { const dispatch = useAppDispatch() @@ -49,6 +50,7 @@ export const ReviewOwner = ({ params }: { params: AddOwnerFlowProps | ReplaceOwn trackEvent({ ...SETTINGS_EVENTS.SETUP.THRESHOLD, label: safe.threshold }) trackEvent({ ...SETTINGS_EVENTS.SETUP.OWNERS, label: safe.owners.length }) + trackEvent({ ...TX_EVENTS.CREATE, label: params.removedOwner ? TX_TYPES.owner_swap : TX_TYPES.owner_add }) } return ( diff --git a/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx b/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx index 6c1aa11d61..e9e7068a25 100644 --- a/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx +++ b/src/components/tx-flow/flows/ChangeThreshold/ReviewChangeThreshold.tsx @@ -8,6 +8,7 @@ import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' import { ChangeThresholdFlowFieldNames } from '@/components/tx-flow/flows/ChangeThreshold' import type { ChangeThresholdFlowProps } from '@/components/tx-flow/flows/ChangeThreshold' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' import commonCss from '@/components/tx-flow/common/styles.module.css' @@ -24,6 +25,7 @@ const ReviewChangeThreshold = ({ params }: { params: ChangeThresholdFlowProps }) const onChangeThreshold = () => { trackEvent({ ...SETTINGS_EVENTS.SETUP.OWNERS, label: safe.owners.length }) trackEvent({ ...SETTINGS_EVENTS.SETUP.THRESHOLD, label: newThreshold }) + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.owner_threshold_change }) } return ( diff --git a/src/components/tx-flow/flows/ConfirmBatch/index.tsx b/src/components/tx-flow/flows/ConfirmBatch/index.tsx index 27e53d211f..daf1ae5580 100644 --- a/src/components/tx-flow/flows/ConfirmBatch/index.tsx +++ b/src/components/tx-flow/flows/ConfirmBatch/index.tsx @@ -1,4 +1,4 @@ -import { type ReactElement, useContext, useEffect } from 'react' +import { type ReactElement, useContext, useEffect, useCallback } from 'react' import { type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender' @@ -9,6 +9,8 @@ import TxLayout from '../../common/TxLayout' import BatchIcon from '@/public/images/common/batch.svg' import { useDraftBatch } from '@/hooks/useDraftBatch' import BatchTxList from '@/components/batch/BatchSidebar/BatchTxList' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { trackEvent } from '@/services/analytics' type ConfirmBatchProps = { onSubmit: () => void @@ -32,8 +34,13 @@ const ConfirmBatch = ({ onSubmit }: ConfirmBatchProps): ReactElement => { createMultiSendCallOnlyTx(calls).then(setSafeTx).catch(setSafeTxError) }, [batchTxs, setSafeTx, setSafeTxError]) + const onTxSubmit = useCallback(() => { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.batch }) + onSubmit() + }, [onSubmit]) + return ( - + ) diff --git a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx index c1277142af..c95deb0b12 100644 --- a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx +++ b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx @@ -1,4 +1,4 @@ -import { Typography, Button, CardActions, Divider, Alert } from '@mui/material' +import { CircularProgress, Typography, Button, CardActions, Divider, Alert } from '@mui/material' import useAsync from '@/hooks/useAsync' import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' @@ -194,8 +194,14 @@ export const ReviewBatch = ({ params }: { params: ExecuteBatchFlowProps }) => { {(isOk) => ( - )} diff --git a/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx b/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx index 5ab7d1a2a2..15cb0b27e0 100644 --- a/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx +++ b/src/components/tx-flow/flows/NewSpendingLimit/ReviewSpendingLimit.tsx @@ -17,6 +17,7 @@ import type { SpendingLimitState } from '@/store/spendingLimitsSlice' import type { NewSpendingLimitFlowProps } from '.' import EthHashInfo from '@/components/common/EthHashInfo' import { SafeTxContext } from '../../SafeTxProvider' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' export const ReviewSpendingLimit = ({ params }: { params: NewSpendingLimitFlowProps }) => { const [existingSpendingLimit, setExistingSpendingLimit] = useState() @@ -53,6 +54,8 @@ export const ReviewSpendingLimit = ({ params }: { params: NewSpendingLimitFlowPr ...SETTINGS_EVENTS.SPENDING_LIMIT.RESET_PERIOD, label: resetTime, }) + + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.spending_limit_add }) } const existingAmount = existingSpendingLimit diff --git a/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx b/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx index c9024eb02f..baa97927fb 100644 --- a/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx +++ b/src/components/tx-flow/flows/NftTransfer/ReviewNftBatch.tsx @@ -1,4 +1,4 @@ -import { type ReactElement, useEffect, useContext } from 'react' +import { type ReactElement, useEffect, useContext, useCallback } from 'react' import { Grid, Typography } from '@mui/material' import SendToBlock from '@/components/tx-flow/flows/TokenTransfer/SendToBlock' import { createNftTransferParams } from '@/services/tx/tokenTransferParams' @@ -8,6 +8,8 @@ import { createMultiSendCallOnlyTx, createTx } from '@/services/tx/tx-sender' import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { SafeTxContext } from '../../SafeTxProvider' import { NftItems } from '@/components/tx-flow/flows/NftTransfer/SendNftBatch' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { trackEvent } from '@/services/analytics' type ReviewNftBatchProps = { params: NftTransferParams @@ -38,8 +40,13 @@ const ReviewNftBatch = ({ params, onSubmit, txNonce }: ReviewNftBatchProps): Rea promise.then(setSafeTx).catch(setSafeTxError) }, [safeAddress, params, setSafeTx, setSafeTxError]) + const onTxSubmit = useCallback(() => { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.transfer_nft }) + onSubmit() + }, [onSubmit]) + return ( - + diff --git a/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx b/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx index 3aeee4fab7..b058444f56 100644 --- a/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx +++ b/src/components/tx-flow/flows/NftTransfer/SendNftBatch.tsx @@ -37,7 +37,14 @@ const NftItem = ({ image, name, description }: { image: string; name: string; de - + {name} @@ -59,7 +66,15 @@ const NftItem = ({ image, name, description }: { image: string; name: string; de export const NftItems = ({ tokens }: { tokens: SafeCollectibleResponse[] }) => { return ( - + {tokens.map((token) => ( { )} - + Selected NFTs diff --git a/src/components/tx-flow/flows/RejectTx/RejectTx.tsx b/src/components/tx-flow/flows/RejectTx/RejectTx.tsx index 91db5ae55f..24b2f66251 100644 --- a/src/components/tx-flow/flows/RejectTx/RejectTx.tsx +++ b/src/components/tx-flow/flows/RejectTx/RejectTx.tsx @@ -4,11 +4,17 @@ import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { createRejectTx } from '@/services/tx/tx-sender' import { useContext, useEffect } from 'react' import { SafeTxContext } from '../../SafeTxProvider' +import { trackEvent } from '@/services/analytics' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' type RejectTxProps = { txNonce: number } +const onSubmit = () => { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.rejection }) +} + const RejectTx = ({ txNonce }: RejectTxProps): ReactElement => { const { setSafeTx, setSafeTxError, setNonce } = useContext(SafeTxContext) @@ -19,7 +25,7 @@ const RejectTx = ({ txNonce }: RejectTxProps): ReactElement => { }, [txNonce, setNonce, setSafeTx, setSafeTxError]) return ( - {}} isBatchable={false}> + To reject the transaction, a separate rejection transaction will be created to replace the original one. diff --git a/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx b/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx index e2155eb66e..6ac5920984 100644 --- a/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx +++ b/src/components/tx-flow/flows/RemoveGuard/ReviewRemoveGuard.tsx @@ -7,6 +7,12 @@ import { trackEvent, SETTINGS_EVENTS } from '@/services/analytics' import { createRemoveGuardTx } from '@/services/tx/tx-sender' import { type RemoveGuardFlowProps } from '.' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' + +const onFormSubmit = () => { + trackEvent(SETTINGS_EVENTS.MODULES.REMOVE_GUARD) + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.guard_remove }) +} export const ReviewRemoveGuard = ({ params }: { params: RemoveGuardFlowProps }) => { const { setSafeTx, safeTxError, setSafeTxError } = useContext(SafeTxContext) @@ -21,10 +27,6 @@ export const ReviewRemoveGuard = ({ params }: { params: RemoveGuardFlowProps }) } }, [safeTxError]) - const onFormSubmit = () => { - trackEvent(SETTINGS_EVENTS.MODULES.REMOVE_GUARD) - } - return ( ({ color: palette.primary.light })}>Transaction guard diff --git a/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx b/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx index 01a11b223d..d6fb40da21 100644 --- a/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx +++ b/src/components/tx-flow/flows/RemoveModule/ReviewRemoveModule.tsx @@ -7,6 +7,12 @@ import { createRemoveModuleTx } from '@/services/tx/tx-sender' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' import { type RemoveModuleFlowProps } from '.' import EthHashInfo from '@/components/common/EthHashInfo' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' + +const onFormSubmit = () => { + trackEvent(SETTINGS_EVENTS.MODULES.REMOVE_MODULE) + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.module_remove }) +} export const ReviewRemoveModule = ({ params }: { params: RemoveModuleFlowProps }) => { const { setSafeTx, safeTxError, setSafeTxError } = useContext(SafeTxContext) @@ -21,10 +27,6 @@ export const ReviewRemoveModule = ({ params }: { params: RemoveModuleFlowProps } } }, [safeTxError]) - const onFormSubmit = () => { - trackEvent(SETTINGS_EVENTS.MODULES.REMOVE_MODULE) - } - return ( diff --git a/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx b/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx index 676a2b1561..59c5498744 100644 --- a/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx +++ b/src/components/tx-flow/flows/RemoveOwner/ReviewRemoveOwner.tsx @@ -11,6 +11,7 @@ import MinusIcon from '@/public/images/common/minus.svg' import { SafeTxContext } from '../../SafeTxProvider' import type { RemoveOwnerFlowProps } from '.' import EthHashInfo from '@/components/common/EthHashInfo' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' import commonCss from '@/components/tx-flow/common/styles.module.css' @@ -29,6 +30,7 @@ export const ReviewRemoveOwner = ({ params }: { params: RemoveOwnerFlowProps }): const onFormSubmit = () => { trackEvent({ ...SETTINGS_EVENTS.SETUP.THRESHOLD, label: safe.threshold }) trackEvent({ ...SETTINGS_EVENTS.SETUP.OWNERS, label: safe.owners.length }) + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.owner_remove }) } return ( diff --git a/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx b/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx index 942fbdc0ae..71b125efef 100644 --- a/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx +++ b/src/components/tx-flow/flows/RemoveOwner/SetThreshold.tsx @@ -82,7 +82,7 @@ export const SetThreshold = ({ - diff --git a/src/components/tx-flow/flows/RemoveSpendingLimit/RemoveSpendingLimit.tsx b/src/components/tx-flow/flows/RemoveSpendingLimit/RemoveSpendingLimit.tsx index df046f6cc6..087cf68705 100644 --- a/src/components/tx-flow/flows/RemoveSpendingLimit/RemoveSpendingLimit.tsx +++ b/src/components/tx-flow/flows/RemoveSpendingLimit/RemoveSpendingLimit.tsx @@ -13,6 +13,12 @@ import SendAmountBlock from '@/components/tx-flow/flows/TokenTransfer/SendAmount import { safeFormatUnits } from '@/utils/formatters' import SpendingLimitLabel from '@/components/common/SpendingLimitLabel' import { createTx } from '@/services/tx/tx-sender' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' + +const onFormSubmit = () => { + trackEvent(SETTINGS_EVENTS.SPENDING_LIMIT.LIMIT_REMOVED) + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.spending_limit_remove }) +} export const RemoveSpendingLimit = ({ params }: { params: SpendingLimitState }) => { const { setSafeTx, setSafeTxError } = useContext(SafeTxContext) @@ -42,10 +48,6 @@ export const RemoveSpendingLimit = ({ params }: { params: SpendingLimitState }) createTx(txParams).then(setSafeTx).catch(setSafeTxError) }, [chainId, params.beneficiary, params.token, setSafeTx, setSafeTxError]) - const onFormSubmit = () => { - trackEvent(SETTINGS_EVENTS.SPENDING_LIMIT.LIMIT_REMOVED) - } - return ( {token && ( diff --git a/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx b/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx index 4580b6021f..6b4f57c3f8 100644 --- a/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx +++ b/src/components/tx-flow/flows/SafeAppsTx/ReviewSafeAppsTx.tsx @@ -17,13 +17,18 @@ import ApprovalEditor from '@/components/tx/ApprovalEditor' import { getInteractionTitle, isTxValid } from '@/components/safe-apps/utils' import ErrorMessage from '@/components/tx/ErrorMessage' import { asError } from '@/services/exceptions/utils' +import { trackEvent } from '@/services/analytics' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { isWalletConnectSafeApp } from '@/services/walletconnect/utils' type ReviewSafeAppsTxProps = { safeAppsTx: SafeAppsTxParams + onSubmit?: (txId: string, safeTxHash: string) => void } const ReviewSafeAppsTx = ({ safeAppsTx: { txs, requestId, params, appId, app }, + onSubmit, }: ReviewSafeAppsTxProps): ReactElement => { const { safe } = useSafeInfo() const onboard = useOnboard() @@ -54,11 +59,22 @@ const ReviewSafeAppsTx = ({ if (!safeTx || !onboard) return trackSafeAppTxCount(Number(appId)) + let safeTxHash = '' try { - await dispatchSafeAppsTx(safeTx, requestId, onboard, safe.chainId, txId) + safeTxHash = await dispatchSafeAppsTx(safeTx, requestId, onboard, safe.chainId, txId) } catch (error) { setSafeTxError(asError(error)) } + + // Track tx creation + if (safeTx.signatures.size === 0) { + trackEvent({ + ...TX_EVENTS.CREATE, + label: isWalletConnectSafeApp(app?.url || '') ? TX_TYPES.walletconnect : TX_TYPES.safeapps, + }) + } + + onSubmit?.(txId, safeTxHash) } const origin = useMemo(() => getTxOrigin(app), [app]) diff --git a/src/components/tx-flow/flows/SafeAppsTx/index.tsx b/src/components/tx-flow/flows/SafeAppsTx/index.tsx index a5f6bdf329..297d13f569 100644 --- a/src/components/tx-flow/flows/SafeAppsTx/index.tsx +++ b/src/components/tx-flow/flows/SafeAppsTx/index.tsx @@ -12,14 +12,20 @@ export type SafeAppsTxParams = { params?: SendTransactionRequestParams } -const SafeAppsTxFlow = ({ data }: { data: SafeAppsTxParams }) => { +const SafeAppsTxFlow = ({ + data, + onSubmit, +}: { + data: SafeAppsTxParams + onSubmit?: (txId: string, safeTxHash: string) => void +}) => { return ( } step={0} > - + ) } diff --git a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx index d331e10b96..dfd2cd79a9 100644 --- a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx +++ b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx @@ -38,6 +38,8 @@ import InfoBox from '@/components/safe-messages/InfoBox' import { DecodedMsg } from '@/components/safe-messages/DecodedMsg' import TxCard from '@/components/tx-flow/common/TxCard' import { dispatchPreparedSignature } from '@/services/safe-messages/safeMsgNotifications' +import { trackEvent } from '@/services/analytics' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' const createSkeletonMessage = (confirmationsRequired: number): SafeMessage => { return { @@ -169,19 +171,14 @@ const SignMessage = ({ message, safeAppId, requestId }: ProposeProps | ConfirmPr const { safe } = useSafeInfo() const isOwner = useIsSafeOwner() const wallet = useWallet() + useHighlightHiddenTab() const { decodedMessage, safeMessageMessage, safeMessageHash } = useDecodedSafeMessage(message, safe) const [safeMessage, setSafeMessage] = useSafeMessage(safeMessageHash) - - useHighlightHiddenTab() - - const decodedMessageAsString = - typeof decodedMessage === 'string' ? decodedMessage : JSON.stringify(decodedMessage, null, 2) - + const isPlainTextMessage = typeof decodedMessage === 'string' + const decodedMessageAsString = isPlainTextMessage ? decodedMessage : JSON.stringify(decodedMessage, null, 2) const hasSigned = !!safeMessage?.confirmations.some(({ owner }) => owner.value === wallet?.address) - const isFullySigned = !!safeMessage?.preparedSignature - const isDisabled = !isOwner || hasSigned const { onSign, submitError } = useSyncSafeMessageSigner( @@ -195,9 +192,15 @@ const SignMessage = ({ message, safeAppId, requestId }: ProposeProps | ConfirmPr const handleSign = async () => { const updatedMessage = await onSign() + if (updatedMessage) { setSafeMessage(updatedMessage) } + + // Track first signature as creation + if (updatedMessage?.confirmations.length === 1) { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.typed_message }) + } } const onContinue = async () => { diff --git a/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx b/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx index ab535b7ede..f667e65367 100644 --- a/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx +++ b/src/components/tx-flow/flows/SignMessageOnChain/ReviewSignMessageOnChain.tsx @@ -25,6 +25,8 @@ import useHighlightHiddenTab from '@/hooks/useHighlightHiddenTab' import { type SafeAppData } from '@safe-global/safe-gateway-typescript-sdk' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' import { asError } from '@/services/exceptions/utils' +import { trackEvent } from '@/services/analytics' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' export type SignMessageOnChainProps = { app?: SafeAppData @@ -98,6 +100,12 @@ const ReviewSignMessageOnChain = ({ message, method, requestId }: SignMessageOnC const handleSubmit = async () => { if (!safeTx || !onboard) return + + // Track the creation of a typed message + if (isTypedMessage && safeTx.signatures.size === 0) { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.typed_message }) + } + try { await dispatchSafeAppsTx(safeTx, requestId, onboard, safe.chainId) } catch (error) { diff --git a/src/components/tx-flow/flows/SuccessScreen/index.tsx b/src/components/tx-flow/flows/SuccessScreen/index.tsx index c7556fc201..6ee5b332c6 100644 --- a/src/components/tx-flow/flows/SuccessScreen/index.tsx +++ b/src/components/tx-flow/flows/SuccessScreen/index.tsx @@ -71,13 +71,13 @@ export const SuccessScreen = ({ txId }: { txId: string }) => {
    {txLink && ( - )} -
    diff --git a/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTx.tsx b/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTx.tsx index 98e2535f31..bbacba80e8 100644 --- a/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTx.tsx +++ b/src/components/tx-flow/flows/TokenTransfer/ReviewTokenTx.tsx @@ -1,9 +1,10 @@ -import { type ReactElement } from 'react' +import { useCallback, type ReactElement } from 'react' import { type TokenTransferParams, TokenTransferType } from '@/components/tx-flow/flows/TokenTransfer/index' import ReviewTokenTransfer from '@/components/tx-flow/flows/TokenTransfer/ReviewTokenTransfer' import ReviewSpendingLimitTx from '@/components/tx-flow/flows/TokenTransfer/ReviewSpendingLimitTx' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { trackEvent } from '@/services/analytics' -// TODO: Split this into separate flows const ReviewTokenTx = ({ params, onSubmit, @@ -15,10 +16,15 @@ const ReviewTokenTx = ({ }): ReactElement => { const isSpendingLimitTx = params.type === TokenTransferType.spendingLimit + const onTxSubmit = useCallback(() => { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.transfer_token }) + onSubmit() + }, [onSubmit]) + return isSpendingLimitTx ? ( - + ) : ( - + ) } diff --git a/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx b/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx index 62239c188f..8e3f7e7d7f 100644 --- a/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx +++ b/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx @@ -9,6 +9,12 @@ import { createUpdateSafeTxs } from '@/services/tx/safeUpdateParams' import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender' import { SafeTxContext } from '../../SafeTxProvider' import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' +import { TX_EVENTS, TX_TYPES } from '@/services/analytics/events/transactions' +import { trackEvent } from '@/services/analytics' + +const onSubmit = () => { + trackEvent({ ...TX_EVENTS.CREATE, label: TX_TYPES.safe_update }) +} export const UpdateSafeReview = () => { const { safe, safeLoaded } = useSafeInfo() @@ -25,7 +31,7 @@ export const UpdateSafeReview = () => { }, [chain, safe, safeLoaded, setNonce, setSafeTx, setSafeTxError]) return ( - null}> + Update now to take advantage of new features and the highest security standards available. diff --git a/src/components/tx/ExecuteCheckbox/index.tsx b/src/components/tx/ExecuteCheckbox/index.tsx index 43e1ad6720..7ec0bf2a56 100644 --- a/src/components/tx/ExecuteCheckbox/index.tsx +++ b/src/components/tx/ExecuteCheckbox/index.tsx @@ -12,7 +12,7 @@ const ExecuteCheckbox = ({ onChange }: { onChange: (checked: boolean) => void }) const handleChange = (_: ChangeEvent, value: string) => { const checked = value === 'true' - trackEvent({ ...MODALS_EVENTS.EXECUTE_TX, label: checked }) + trackEvent({ ...MODALS_EVENTS.TOGGLE_EXECUTE_TX, label: checked }) dispatch(setTransactionExecution(checked)) onChange(checked) } diff --git a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx index 72ae840fc6..5518a827a0 100644 --- a/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx +++ b/src/components/tx/SignOrExecuteForm/ExecuteForm.tsx @@ -1,5 +1,5 @@ import { type ReactElement, type SyntheticEvent, useContext, useState } from 'react' -import { Box, Button, CardActions, Divider } from '@mui/material' +import { CircularProgress, Box, Button, CardActions, Divider } from '@mui/material' import classNames from 'classnames' import ErrorMessage from '@/components/tx/ErrorMessage' @@ -96,8 +96,8 @@ const ExecuteForm = ({ } // On success - setTxFlow(, undefined, false) onSubmit(executedTxId) + setTxFlow(, undefined, false) } const cannotPropose = !isOwner && !onlyExecute @@ -156,8 +156,8 @@ const ExecuteForm = ({ {/* Submit button */} {(isOk) => ( - )} diff --git a/src/components/tx/SignOrExecuteForm/SignForm.tsx b/src/components/tx/SignOrExecuteForm/SignForm.tsx index 03cf9cbaff..e68a47c76f 100644 --- a/src/components/tx/SignOrExecuteForm/SignForm.tsx +++ b/src/components/tx/SignOrExecuteForm/SignForm.tsx @@ -1,5 +1,5 @@ import { type ReactElement, type SyntheticEvent, useContext, useState } from 'react' -import { Box, Button, CardActions, Divider } from '@mui/material' +import { CircularProgress, Box, Button, CardActions, Divider } from '@mui/material' import ErrorMessage from '@/components/tx/ErrorMessage' import { trackError, Errors } from '@/services/exceptions' @@ -63,9 +63,12 @@ const SignForm = ({ return } - // On success + // On successful sign + if (!isAddingToBatch) { + onSubmit(resultTxId) + } + setTxFlow(undefined) - onSubmit(resultTxId) } const onBatchClick = (e: SyntheticEvent) => { @@ -103,8 +106,14 @@ const SignForm = ({ {/* Submit button */} {(isOk) => ( - )} diff --git a/src/components/tx/SignOrExecuteForm/SubmitButton.tsx b/src/components/tx/SignOrExecuteForm/SubmitButton.tsx deleted file mode 100644 index 8412086eab..0000000000 --- a/src/components/tx/SignOrExecuteForm/SubmitButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import CheckWallet from '@/components/common/CheckWallet' -import { Button } from '@mui/material' -import { useContext } from 'react' -import { TxSecurityContext } from '../security/shared/TxSecurityContext' - -const SubmitButton = ({ - willExecute, - submitDisabled, - isEstimating, -}: { - willExecute: boolean - submitDisabled: boolean - isEstimating: boolean -}) => { - const { needsRiskConfirmation, isRiskConfirmed } = useContext(TxSecurityContext) - - const disableButton = submitDisabled || (needsRiskConfirmation && !isRiskConfirmed) - return ( - - {(isOk) => ( - - )} - - ) -} - -export default SubmitButton diff --git a/src/components/tx/SignOrExecuteForm/hooks.ts b/src/components/tx/SignOrExecuteForm/hooks.ts index ca40563468..acf79a1baa 100644 --- a/src/components/tx/SignOrExecuteForm/hooks.ts +++ b/src/components/tx/SignOrExecuteForm/hooks.ts @@ -15,7 +15,7 @@ import { import { useHasPendingTxs } from '@/hooks/usePendingTxs' import type { ConnectedWallet } from '@/services/onboard' import type { OnboardAPI } from '@web3-onboard/core' -import { getSafeTxGas, getRecommendedNonce } from '@/services/tx/tx-sender/recommendedNonce' +import { getSafeTxGas, getNonces } from '@/services/tx/tx-sender/recommendedNonce' import useAsync from '@/hooks/useAsync' import { useUpdateBatch } from '@/hooks/useDraftBatch' import { type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' @@ -163,9 +163,9 @@ export const useRecommendedNonce = (): number | undefined => { async () => { if (!safe.chainId || !safeAddress) return - const recommendedNonce = await getRecommendedNonce(safe.chainId, safeAddress) + const nonces = await getNonces(safe.chainId, safeAddress) - return recommendedNonce !== undefined ? Math.max(safe.nonce, recommendedNonce) : undefined + return nonces?.recommendedNonce }, // eslint-disable-next-line react-hooks/exhaustive-deps [safeAddress, safe.chainId, safe.txQueuedTag], // update when tx queue changes diff --git a/src/components/tx/security/tenderly/index.tsx b/src/components/tx/security/tenderly/index.tsx index 2af50abae5..dafe0dced5 100644 --- a/src/components/tx/security/tenderly/index.tsx +++ b/src/components/tx/security/tenderly/index.tsx @@ -18,6 +18,8 @@ import sharedCss from '@/components/tx/security/shared/styles.module.css' import { TxInfoContext } from '@/components/tx-flow/TxInfoProvider' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' import InfoIcon from '@/public/images/notifications/info.svg' +import Track from '@/components/common/Track' +import { MODALS_EVENTS } from '@/services/analytics' export type TxSimulationProps = { transactions?: SimulationTxParams['transactions'] @@ -113,15 +115,17 @@ const TxSimulationBlock = ({ transactions, disabled, gasLimit, executionOwner }:
    ) ) : ( - + + + )} diff --git a/src/components/walletconnect/WcProposalForm/ProposalVerification.tsx b/src/components/walletconnect/WcProposalForm/ProposalVerification.tsx index cbd0e66582..48cad9ae5b 100644 --- a/src/components/walletconnect/WcProposalForm/ProposalVerification.tsx +++ b/src/components/walletconnect/WcProposalForm/ProposalVerification.tsx @@ -29,7 +29,7 @@ const Validation: { }, INVALID: { color: 'error', - desc: 'has been flagged as a high risk by WalletConnect.', + desc: 'has a domain that does not match the sender of this request. Approving it may result in a loss of funds.', Icon: CloseIcon, }, } diff --git a/src/components/walletconnect/WcSessionList/WcNoSessions.tsx b/src/components/walletconnect/WcSessionList/WcNoSessions.tsx index 17d7960b07..e7183678a6 100644 --- a/src/components/walletconnect/WcSessionList/WcNoSessions.tsx +++ b/src/components/walletconnect/WcSessionList/WcNoSessions.tsx @@ -21,9 +21,9 @@ const WcSampleDapps = ({ onUnload }: { onUnload: () => void }) => { return ( {SAMPLE_DAPPS.map((item) => ( - + - {item.name} + {item.name} {item.name} diff --git a/src/components/welcome/WelcomeLogin/index.tsx b/src/components/welcome/WelcomeLogin/index.tsx index ad768bf84a..9fcc869f44 100644 --- a/src/components/welcome/WelcomeLogin/index.tsx +++ b/src/components/welcome/WelcomeLogin/index.tsx @@ -1,9 +1,9 @@ -import SocialSigner from '@/components/common/SocialSigner' import { AppRoutes } from '@/config/routes' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' -import { Paper, SvgIcon, Typography, Divider, Link, Box } from '@mui/material' +import { Paper, SvgIcon, Typography, Divider, Link, Box, Skeleton } from '@mui/material' import SafeLogo from '@/public/images/logo-text.svg' +import dynamic from 'next/dynamic' import css from './styles.module.css' import { useRouter } from 'next/router' import WalletLogin from './WalletLogin' @@ -11,6 +11,10 @@ import { LOAD_SAFE_EVENTS, CREATE_SAFE_EVENTS } from '@/services/analytics/event import Track from '@/components/common/Track' import { trackEvent } from '@/services/analytics' +const SocialSigner = dynamic(() => import('@/components/common/SocialSigner'), { + loading: () => , +}) + const WelcomeLogin = () => { const router = useRouter() const isSocialLoginEnabled = useHasFeature(FEATURES.SOCIAL_LOGIN) diff --git a/src/config/constants.ts b/src/config/constants.ts index 7edd0dd2e1..760a102c98 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -19,7 +19,6 @@ export const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN || '' export const BEAMER_ID = process.env.NEXT_PUBLIC_BEAMER_ID || '' // Wallets -export const WC_BRIDGE = process.env.NEXT_PUBLIC_WC_BRIDGE || 'https://bridge.walletconnect.org' export const WC_PROJECT_ID = process.env.NEXT_PUBLIC_WC_PROJECT_ID || '' export const TREZOR_APP_URL = 'app.safe.global' export const TREZOR_EMAIL = 'support@safe.global' diff --git a/src/hooks/useTxTracking.ts b/src/hooks/useTxTracking.ts index d62aa118ce..716752c1a0 100644 --- a/src/hooks/useTxTracking.ts +++ b/src/hooks/useTxTracking.ts @@ -1,10 +1,12 @@ import { trackEvent, WALLET_EVENTS } from '@/services/analytics' +import { TX_EVENTS } from '@/services/analytics/events/transactions' import { getTxDetails } from '@/services/tx/txDetails' import { TxEvent, txSubscribe } from '@/services/tx/txEvents' import { useEffect } from 'react' import useChainId from './useChainId' const events = { + [TxEvent.SIGNATURE_PROPOSED]: TX_EVENTS.CONFIRM, [TxEvent.SIGNED]: WALLET_EVENTS.OFFCHAIN_SIGNATURE, [TxEvent.PROCESSING]: WALLET_EVENTS.ONCHAIN_INTERACTION, [TxEvent.PROCESSING_MODULE]: WALLET_EVENTS.ONCHAIN_INTERACTION, diff --git a/src/hooks/wallets/consts.ts b/src/hooks/wallets/consts.ts index 6bb6a61f93..506fac0513 100644 --- a/src/hooks/wallets/consts.ts +++ b/src/hooks/wallets/consts.ts @@ -3,7 +3,6 @@ export const enum WALLET_KEYS { WALLETCONNECT_V2 = 'WALLETCONNECT_V2', SOCIAL = 'SOCIAL_LOGIN', COINBASE = 'COINBASE', - PAIRING = 'PAIRING', LEDGER = 'LEDGER', TREZOR = 'TREZOR', KEYSTONE = 'KEYSTONE', @@ -14,7 +13,6 @@ export const CGW_NAMES: { [key in WALLET_KEYS]: string | undefined } = { [WALLET_KEYS.INJECTED]: 'detectedwallet', [WALLET_KEYS.WALLETCONNECT_V2]: 'walletConnect_v2', [WALLET_KEYS.COINBASE]: 'coinbase', - [WALLET_KEYS.PAIRING]: 'safeMobile', [WALLET_KEYS.SOCIAL]: 'socialSigner', [WALLET_KEYS.LEDGER]: 'ledger', [WALLET_KEYS.TREZOR]: 'trezor', diff --git a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts index 2cdfd3f689..7c839a0cb8 100644 --- a/src/hooks/wallets/mpc/__tests__/useMPC.test.ts +++ b/src/hooks/wallets/mpc/__tests__/useMPC.test.ts @@ -1,8 +1,7 @@ import * as useOnboard from '@/hooks/wallets/useOnboard' import * as socialWalletOptions from '@/services/mpc/config' -import { renderHook, waitFor } from '@/tests/test-utils' -import { _getMPCCoreKitInstance, setMPCCoreKitInstance, useInitMPC } from '../useMPC' -import * as useChains from '@/hooks/useChains' +import { waitFor } from '@/tests/test-utils' +import { _getMPCCoreKitInstance, initMPC, setMPCCoreKitInstance } from '../useMPC' import { type ChainInfo, RPC_AUTHENTICATION } from '@safe-global/safe-gateway-typescript-sdk' import { hexZeroPad } from 'ethers/lib/utils' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' @@ -63,53 +62,55 @@ class EventEmittingMockProvider { } } -describe('useInitMPC', () => { +describe('initMPC', () => { + const mockOnboard = { + state: { + get: () => ({ + wallets: [], + walletModules: [], + }), + }, + } as unknown as OnboardAPI + + const mockChain = { + chainId: '5', + chainName: 'Goerli', + blockExplorerUriTemplate: { + address: 'https://goerli.someprovider.io/{address}', + txHash: 'https://goerli.someprovider.io/{txHash}', + api: 'https://goerli.someprovider.io/', + }, + nativeCurrency: { + decimals: 18, + logoUri: 'https://logo.goerli.com', + name: 'Goerli ETH', + symbol: 'ETH', + }, + rpcUri: { + authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, + value: 'https://goerli.somerpc.io', + }, + } as unknown as ChainInfo + beforeEach(() => { jest.resetAllMocks() jest.spyOn(socialWalletOptions, 'isSocialWalletOptions').mockReturnValue(true) }) + it('should set the coreKit if user is not logged in yet', async () => { - const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) - jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(() => Promise.resolve(undefined)) jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue(null) - jest.spyOn(useOnboard, 'default').mockReturnValue({ - state: { - get: () => ({ - wallets: [], - walletModules: [], - }), - }, - } as unknown as OnboardAPI) - jest.spyOn(useChains, 'useCurrentChain').mockReturnValue({ - chainId: '5', - chainName: 'Goerli', - blockExplorerUriTemplate: { - address: 'https://goerli.someprovider.io/{address}', - txHash: 'https://goerli.someprovider.io/{txHash}', - api: 'https://goerli.someprovider.io/', - }, - nativeCurrency: { - decimals: 18, - logoUri: 'https://logo.goerli.com', - name: 'Goerli ETH', - symbol: 'ETH', - }, - rpcUri: { - authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, - value: 'https://goerli.somerpc.io', - }, - } as unknown as ChainInfo) const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') mockWeb3AuthMpcCoreKit.mockImplementation(() => { return new MockMPCCoreKit(COREKIT_STATUS.INITIALIZED, null) }) - renderHook(() => useInitMPC()) + await initMPC(mockChain, mockOnboard) await waitFor(() => { expect(_getMPCCoreKitInstance()).toBeDefined() - expect(connectWalletSpy).not.toBeCalled() + expect(useOnboard.connectWallet).not.toBeCalled() }) }) @@ -117,33 +118,6 @@ describe('useInitMPC', () => { const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue(null) - jest.spyOn(useOnboard, 'default').mockReturnValue({ - state: { - get: () => ({ - wallets: [], - walletModules: [], - }), - }, - } as unknown as OnboardAPI) - jest.spyOn(useChains, 'useCurrentChain').mockReturnValue({ - chainId: '5', - chainName: 'Goerli', - blockExplorerUriTemplate: { - address: 'https://goerli.someprovider.io/{address}', - txHash: 'https://goerli.someprovider.io/{txHash}', - api: 'https://goerli.someprovider.io/', - }, - nativeCurrency: { - decimals: 18, - logoUri: 'https://logo.goerli.com', - name: 'Goerli ETH', - symbol: 'ETH', - }, - rpcUri: { - authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, - value: 'https://goerli.somerpc.io', - }, - } as unknown as ChainInfo) const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') const mockProvider = jest.fn() @@ -151,7 +125,7 @@ describe('useInitMPC', () => { return new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, mockProvider as unknown as MPCProvider) }) - renderHook(() => useInitMPC()) + await initMPC(mockChain, mockOnboard) await waitFor(() => { expect(connectWalletSpy).toBeCalled() @@ -160,41 +134,13 @@ describe('useInitMPC', () => { }) it('should copy event handlers and emit chainChanged if the current chain is updated', async () => { - const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve()) - jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy) + jest.spyOn(useOnboard, 'connectWallet').mockImplementation(() => Promise.resolve(undefined)) jest.spyOn(useOnboard, 'getConnectedWallet').mockReturnValue({ address: hexZeroPad('0x1', 20), label: ONBOARD_MPC_MODULE_LABEL, chainId: '1', provider: {} as unknown as EIP1193Provider, }) - jest.spyOn(useOnboard, 'default').mockReturnValue({ - state: { - get: () => ({ - wallets: [], - walletModules: [], - }), - }, - } as unknown as OnboardAPI) - jest.spyOn(useChains, 'useCurrentChain').mockReturnValue({ - chainId: '5', - chainName: 'Goerli', - blockExplorerUriTemplate: { - address: 'https://goerli.someprovider.io/{address}', - txHash: 'https://goerli.someprovider.io/{txHash}', - api: 'https://goerli.someprovider.io/', - }, - nativeCurrency: { - decimals: 18, - logoUri: 'https://logo.goerli.com', - name: 'Goerli ETH', - symbol: 'ETH', - }, - rpcUri: { - authentication: RPC_AUTHENTICATION.NO_AUTHENTICATION, - value: 'https://goerli.somerpc.io', - }, - } as unknown as ChainInfo) const mockWeb3AuthMpcCoreKit = jest.spyOn(require('@web3auth/mpc-core-kit'), 'Web3AuthMPCCoreKit') const mockChainChangedListener = jest.fn() @@ -215,12 +161,12 @@ describe('useInitMPC', () => { return new MockMPCCoreKit(COREKIT_STATUS.LOGGED_IN, mockProvider as unknown as MPCProvider) }) - renderHook(() => useInitMPC()) + await initMPC(mockChain, mockOnboard) await waitFor(() => { expect(mockChainChangedListener).toHaveBeenCalledWith('0x5') expect(_getMPCCoreKitInstance()).toBeDefined() - expect(connectWalletSpy).not.toBeCalled() + expect(useOnboard.connectWallet).not.toBeCalled() }) }) }) diff --git a/src/hooks/wallets/mpc/useMPC.ts b/src/hooks/wallets/mpc/useMPC.ts index b0b9d8544a..c73a8e2fee 100644 --- a/src/hooks/wallets/mpc/useMPC.ts +++ b/src/hooks/wallets/mpc/useMPC.ts @@ -1,88 +1,83 @@ -import { useEffect } from 'react' +import { IS_PRODUCTION } from '@/config/constants' +import { connectWallet, getConnectedWallet } from '@/hooks/wallets/useOnboard' import ExternalStore from '@/services/ExternalStore' -import { COREKIT_STATUS, Web3AuthMPCCoreKit, WEB3AUTH_NETWORK } from '@web3auth/mpc-core-kit' +import { SOCIAL_WALLET_OPTIONS } from '@/services/mpc/config' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' +import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { type OnboardAPI } from '@web3-onboard/core' import { CHAIN_NAMESPACES } from '@web3auth/base' - -import { useCurrentChain } from '@/hooks/useChains' +import type { Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' +import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' import { getRpcServiceUrl } from '../web3' -import useOnboard, { connectWallet, getConnectedWallet } from '@/hooks/wallets/useOnboard' -import { useInitSocialWallet } from './useSocialWallet' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { isSocialWalletOptions, SOCIAL_WALLET_OPTIONS } from '@/services/mpc/config' -import { IS_PRODUCTION } from '@/config/constants' const { getStore, setStore, useStore } = new ExternalStore() -export const useInitMPC = () => { - const chain = useCurrentChain() - const onboard = useOnboard() - useInitSocialWallet() +export const initMPC = async (chain: ChainInfo, onboard: OnboardAPI) => { + const chainConfig = { + chainId: `0x${Number(chain.chainId).toString(16)}`, + chainNamespace: CHAIN_NAMESPACES.EIP155, + rpcTarget: getRpcServiceUrl(chain.rpcUri), + displayName: chain.chainName, + blockExplorer: new URL(chain.blockExplorerUriTemplate.address).origin, + ticker: chain.nativeCurrency.symbol, + tickerName: chain.nativeCurrency.name, + } - useEffect(() => { - if (!chain || !onboard || !isSocialWalletOptions(SOCIAL_WALLET_OPTIONS)) { - return - } + const currentInstance = getStore() + let previousChainChangedListeners: Function[] = [] + if (currentInstance?.provider) { + // We are already connected. We copy onboards event listener for the chainChanged event to propagate a potentially new chainId + const oldProvider = currentInstance.provider + previousChainChangedListeners = oldProvider.listeners('chainChanged') + } - const chainConfig = { - chainId: `0x${Number(chain.chainId).toString(16)}`, - chainNamespace: CHAIN_NAMESPACES.EIP155, - rpcTarget: getRpcServiceUrl(chain.rpcUri), - displayName: chain.chainName, - blockExplorer: new URL(chain.blockExplorerUriTemplate.address).origin, - ticker: chain.nativeCurrency.symbol, - tickerName: chain.nativeCurrency.name, - } + const { Web3AuthMPCCoreKit, WEB3AUTH_NETWORK } = await import('@web3auth/mpc-core-kit') - const currentInstance = getStore() - let previousChainChangedListeners: Function[] = [] - if (currentInstance?.provider) { - // We are already connected. We copy onboards event listener for the chainChanged event to propagate a potentially new chainId - const oldProvider = currentInstance.provider - previousChainChangedListeners = oldProvider.listeners('chainChanged') - } + const web3AuthCoreKit = new Web3AuthMPCCoreKit({ + web3AuthClientId: SOCIAL_WALLET_OPTIONS.web3AuthClientId, + // Available networks are "sapphire_devnet", "sapphire_mainnet" + web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, + baseUrl: `${window.location.origin}/`, + uxMode: 'popup', + enableLogging: !IS_PRODUCTION, + //@ts-ignore + chainConfig, + manualSync: true, + hashedFactorNonce: 'safe-global-sfa-nonce', + }) - const web3AuthCoreKit = new Web3AuthMPCCoreKit({ - web3AuthClientId: SOCIAL_WALLET_OPTIONS.web3AuthClientId, - // Available networks are "sapphire_devnet", "sapphire_mainnet" - web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, - baseUrl: `${window.location.origin}/`, - uxMode: 'popup', - enableLogging: !IS_PRODUCTION, - chainConfig, - manualSync: true, - hashedFactorNonce: 'safe-global-sfa-nonce', - }) + return web3AuthCoreKit + .init() + .then(() => { + setStore(web3AuthCoreKit) + // If rehydration was successful, connect to onboard + if (web3AuthCoreKit.status !== COREKIT_STATUS.LOGGED_IN || !web3AuthCoreKit.provider) { + return web3AuthCoreKit + } - web3AuthCoreKit - .init() - .then(() => { - setStore(web3AuthCoreKit) - // If rehydration was successful, connect to onboard - if (web3AuthCoreKit.status !== COREKIT_STATUS.LOGGED_IN || !web3AuthCoreKit.provider) { - return - } - const connectedWallet = getConnectedWallet(onboard.state.get().wallets) - if (!connectedWallet) { - connectWallet(onboard, { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }).catch((reason) => console.error('Error connecting to MPC module:', reason)) - } else { - const newProvider = web3AuthCoreKit.provider + const connectedWallet = getConnectedWallet(onboard.state.get().wallets) + if (!connectedWallet) { + connectWallet(onboard, { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }).catch((reason) => console.error('Error connecting to MPC module:', reason)) + } else { + const newProvider = web3AuthCoreKit.provider - // To propagate the changedChain we disconnect and connect - if (previousChainChangedListeners.length > 0 && newProvider) { - previousChainChangedListeners.forEach((previousListener) => - newProvider.addListener('chainChanged', (...args: []) => previousListener(...args)), - ) - newProvider.emit('chainChanged', `0x${Number(chainConfig.chainId).toString(16)}`) - } + // To propagate the changedChain we disconnect and connect + if (previousChainChangedListeners.length > 0 && newProvider) { + previousChainChangedListeners.forEach((previousListener) => + newProvider.addListener('chainChanged', (...args: []) => previousListener(...args)), + ) + newProvider.emit('chainChanged', `0x${Number(chainConfig.chainId).toString(16)}`) } - }) - .catch((error) => console.error(error)) - }, [chain, onboard]) + } + + return web3AuthCoreKit + }) + .catch((error) => console.error(error)) } export const _getMPCCoreKitInstance = getStore diff --git a/src/hooks/wallets/mpc/useRehydrateSocialWallet.ts b/src/hooks/wallets/mpc/useRehydrateSocialWallet.ts new file mode 100644 index 0000000000..799d7f2036 --- /dev/null +++ b/src/hooks/wallets/mpc/useRehydrateSocialWallet.ts @@ -0,0 +1,68 @@ +import useAddressBook from '@/hooks/useAddressBook' +import useChainId from '@/hooks/useChainId' +import { useCurrentChain } from '@/hooks/useChains' +import useOnboard, { connectWallet } from '@/hooks/wallets/useOnboard' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' +import { useAppDispatch } from '@/store' +import { upsertAddressBookEntry } from '@/store/addressBookSlice' +import { type WalletState } from '@web3-onboard/core' +import { type UserInfo } from '@web3auth/mpc-core-kit' +import { useCallback, useEffect } from 'react' +import { checksumAddress } from '@/utils/addresses' + +const useRehydrateSocialWallet = () => { + const chain = useCurrentChain() + const onboard = useOnboard() + const currentChainId = useChainId() + const addressBook = useAddressBook() + const dispatch = useAppDispatch() + + const updateAddressBook = useCallback( + (userInfo: UserInfo | undefined, wallets: WalletState[] | undefined | void) => { + if (!userInfo || !wallets || !currentChainId || wallets.length === 0) return + + const address = wallets[0].accounts[0]?.address + if (address) { + const signerAddress = checksumAddress(address) + if (addressBook[signerAddress] === undefined) { + const email = userInfo.email + dispatch(upsertAddressBookEntry({ address: signerAddress, chainId: currentChainId, name: email })) + } + } + }, + [addressBook, currentChainId, dispatch], + ) + + useEffect(() => { + if (!chain || !onboard) return + + const rehydrate = async () => { + const { initMPC } = await import('./useMPC') + const { initSocialWallet } = await import('./useSocialWallet') + const mpcCoreKit = await initMPC(chain, onboard) + + if (!mpcCoreKit) return + + const socialWalletService = await initSocialWallet(mpcCoreKit) + + const onConnect = async () => { + const wallets = await connectWallet(onboard, { + autoSelect: { + label: ONBOARD_MPC_MODULE_LABEL, + disableModals: true, + }, + }).catch((reason) => console.error('Error connecting to MPC module:', reason)) + + // If the signer is not in the address book => add the user's email as name + const userInfo = socialWalletService?.getUserInfo() + updateAddressBook(userInfo, wallets) + } + + socialWalletService.setOnConnect(onConnect) + } + + void rehydrate() + }, [chain, onboard, updateAddressBook]) +} + +export default useRehydrateSocialWallet diff --git a/src/hooks/wallets/mpc/useSocialWallet.ts b/src/hooks/wallets/mpc/useSocialWallet.ts index d98998ffdd..8db21735d6 100644 --- a/src/hooks/wallets/mpc/useSocialWallet.ts +++ b/src/hooks/wallets/mpc/useSocialWallet.ts @@ -1,59 +1,15 @@ -import useAddressBook from '@/hooks/useAddressBook' -import useChainId from '@/hooks/useChainId' import ExternalStore from '@/services/ExternalStore' import type { ISocialWalletService } from '@/services/mpc/interfaces' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import SocialWalletService from '@/services/mpc/SocialWalletService' -import { useAppDispatch } from '@/store' -import { upsertAddressBookEntry } from '@/store/addressBookSlice' -import { checksumAddress } from '@/utils/addresses' -import { useCallback, useEffect } from 'react' -import useOnboard, { connectWallet } from '../useOnboard' -import useMpc from './useMPC' +import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' const { getStore, setStore, useStore } = new ExternalStore() -export const useInitSocialWallet = () => { - const mpcCoreKit = useMpc() - const onboard = useOnboard() - const addressBook = useAddressBook() - const currentChainId = useChainId() - const dispatch = useAppDispatch() - const socialWalletService = useStore() +export const initSocialWallet = async (mpcCoreKit: Web3AuthMPCCoreKit) => { + const SocialWalletService = (await import('@/services/mpc/SocialWalletService')).default + const socialWalletService = new SocialWalletService(mpcCoreKit) + setStore(socialWalletService) - const onConnect = useCallback(async () => { - if (!onboard || !socialWalletService) return - - const wallets = await connectWallet(onboard, { - autoSelect: { - label: ONBOARD_MPC_MODULE_LABEL, - disableModals: true, - }, - }).catch((reason) => console.error('Error connecting to MPC module:', reason)) - - // If the signer is not in the address book => add the user's email as name - const userInfo = socialWalletService.getUserInfo() - if (userInfo && wallets && currentChainId && wallets.length > 0) { - const address = wallets[0].accounts[0]?.address - if (address) { - const signerAddress = checksumAddress(address) - if (addressBook[signerAddress] === undefined) { - const email = userInfo.email - dispatch(upsertAddressBookEntry({ address: signerAddress, chainId: currentChainId, name: email })) - } - } - } - }, [addressBook, currentChainId, dispatch, onboard, socialWalletService]) - - useEffect(() => { - socialWalletService?.setOnConnect(onConnect) - }, [onConnect, socialWalletService]) - - useEffect(() => { - if (mpcCoreKit) { - setStore(new SocialWalletService(mpcCoreKit)) - } - }, [mpcCoreKit]) + return socialWalletService } export const getSocialWalletService = getStore diff --git a/src/hooks/wallets/useOnboard.ts b/src/hooks/wallets/useOnboard.ts index 4d8f2c1ab9..ef20d4f128 100644 --- a/src/hooks/wallets/useOnboard.ts +++ b/src/hooks/wallets/useOnboard.ts @@ -6,7 +6,6 @@ import useChains, { useCurrentChain } from '@/hooks/useChains' import ExternalStore from '@/services/ExternalStore' import { logError, Errors } from '@/services/exceptions' import { trackEvent, WALLET_EVENTS } from '@/services/analytics' -import { useInitPairing } from '@/services/pairing/hooks' import { useAppSelector } from '@/store' import { type EnvState, selectRpc } from '@/store/settingsSlice' import { E2E_WALLET_NAME } from '@/tests/e2e-wallet' @@ -93,21 +92,11 @@ const isMobile = () => /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) // Detect injected wallet const hasInjectedWallet = () => typeof window !== 'undefined' && !!window?.ethereum -// `connectWallet` is called when connecting/switching wallets and on pairing `connect` event (when prev. session connects) -// This re-entrant lock prevents multiple `connectWallet`/tracking calls that would otherwise occur for pairing module -let isConnecting = false - // Wrapper that tracks/sets the last used wallet export const connectWallet = async ( onboard: OnboardAPI, options?: Parameters[0], ): Promise => { - if (isConnecting) { - return - } - - isConnecting = true - // On mobile, automatically choose WalletConnect if there is no injected wallet if (!options && isMobile() && !hasInjectedWallet()) { options = { @@ -121,13 +110,9 @@ export const connectWallet = async ( wallets = await onboard.connectWallet(options) } catch (e) { logError(Errors._302, e) - - isConnecting = false return } - isConnecting = false - return wallets } @@ -153,8 +138,6 @@ export const useInitOnboard = () => { const onboard = useStore() const customRpc = useAppSelector(selectRpc) - useInitPairing() - useEffect(() => { if (configs.length > 0 && chain) { void initOnboard(configs, chain, customRpc) diff --git a/src/hooks/wallets/wallets.ts b/src/hooks/wallets/wallets.ts index 2de9efdd29..4195f57adf 100644 --- a/src/hooks/wallets/wallets.ts +++ b/src/hooks/wallets/wallets.ts @@ -9,7 +9,6 @@ import ledgerModule from '@web3-onboard/ledger/dist/index' import trezorModule from '@web3-onboard/trezor' import walletConnect from '@web3-onboard/walletconnect' -import pairingModule from '@/services/pairing/module' import e2eWalletModule from '@/tests/e2e-wallet' import { CGW_NAMES, WALLET_KEYS } from './consts' import MpcModule from '@/services/mpc/SocialLoginModule' @@ -42,7 +41,6 @@ const WALLET_MODULES: { [key in WALLET_KEYS]: (chain: ChainInfo) => WalletInit } [WALLET_KEYS.INJECTED]: () => injectedWalletModule(), [WALLET_KEYS.WALLETCONNECT_V2]: (chain) => walletConnectV2(chain), [WALLET_KEYS.COINBASE]: () => coinbaseModule({ darkMode: prefersDarkMode() }), - [WALLET_KEYS.PAIRING]: () => pairingModule(), [WALLET_KEYS.SOCIAL]: (chain) => MpcModule(chain), [WALLET_KEYS.LEDGER]: () => ledgerModule(), [WALLET_KEYS.TREZOR]: () => trezorModule({ appUrl: TREZOR_APP_URL, email: TREZOR_EMAIL }), diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index a849581113..0eb6c66d65 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,3 +1,4 @@ +import useRehydrateSocialWallet from '@/hooks/wallets/mpc/useRehydrateSocialWallet' import PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal' import Sentry from '@/services/sentry' // needs to be imported first import type { ReactNode } from 'react' @@ -38,12 +39,10 @@ import useSafeMessageNotifications from '@/hooks/messages/useSafeMessageNotifica import useSafeMessagePendingStatuses from '@/hooks/messages/useSafeMessagePendingStatuses' import useChangedValue from '@/hooks/useChangedValue' import { TxModalProvider } from '@/components/tx-flow' -import { useInitMPC } from '@/hooks/wallets/mpc/useMPC' import { WalletConnectProvider } from '@/services/walletconnect/WalletConnectContext' import useABTesting from '@/services/tracking/useAbTesting' import { AbTest } from '@/services/tracking/abTesting' import { useNotificationTracking } from '@/components/settings/PushNotifications/hooks/useNotificationTracking' -import MobilePairingModal from '@/services/pairing/QRModal' import { RecoveryProvider } from '@/components/recovery/RecoveryContext' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -66,7 +65,7 @@ const InitApp = (): null => { useTxTracking() useSafeMsgTracking() useBeamer() - useInitMPC() + useRehydrateSocialWallet() useABTesting(AbTest.HUMAN_DESCRIPTION) return null @@ -129,8 +128,6 @@ const WebCoreApp = ({ - - diff --git a/src/pages/cookie.tsx b/src/pages/cookie.tsx index 9fe14fc43e..624e833ef3 100644 --- a/src/pages/cookie.tsx +++ b/src/pages/cookie.tsx @@ -1,6 +1,594 @@ import type { NextPage } from 'next' import Head from 'next/head' -import SafeCookiePolicy from '@/components/cookie-policy' +import Link from 'next/link' +import MUILink from '@mui/material/Link' +import { AppRoutes } from '@/config/routes' +import { IS_OFFICIAL_HOST } from '@/config/constants' + +const SafeCookiePolicy = () => ( +
    + + +

    Cookie Policy

    +

    Last updated on March 2023

    +

    + As described in our{' '} + + Privacy Policy + + , for general web-browsing of this website, your personal data is not revealed to us, although certain statistical + information is available to us via our internet service provider as well as through the use of special tracking + technologies. Such information tells us about the pages you are clicking on or the hardware you are using, but not + your name, age, address or anything we can use to identify you personally. We exclusively process your personal + data in pseudonymised form. +

    +

    + This Cookie Policy applies to our website at{' '} + + https://app.safe.global + +  and sets out some further detail on how and why we use these technologies on our website.{' '} +

    +

    + In this policy, "we", "us" and "our" refers to Core Contributors GmbH a company + incorporated in Germany with its registered address at Skalitzer Str. 85-86, ℅ Full Node, 10997 Berlin, Germany. + The terms “you” and “your” includes our clients, business partners and users of this + website.{' '} +

    +

    + By using our website, you consent to storage and access to cookies and other technologies on your device, in + accordance with this Cookie Policy. +

    +

    What are cookies?

    +

    + Cookies are a feature of web browser software that allows web servers to recognize the computer or device used to + access a website. A cookie is a small text file that a website saves on your computer or mobile device when you + visit the site. It enables the website to remember your actions and preferences (such as login, language, font + size and other display preferences) over a period of time, so you don't have to keep re-entering them + whenever you come back to the site or browse from one page to another. +

    +

    What are the different types of cookies?

    +

    A cookie can be classified by its lifespan and the domain to which it belongs.

    +

    By lifespan, a cookie is either a:

    +
      +
    1. session cookie which is erased when the user closes the browser; or
    2. +
    3. + persistent cookie which is saved to the hard drive and remains on the user's computer/device for a + pre-defined period of time. As for the domain to which it belongs, cookies are either: +
    4. +
    +
      +
    1. + first-party cookies which are set by the web server of the visited page and share the same domain (i.e. set by + us); or +
    2. +
    3. third-party cookies stored by a different domain to the visited page's domain.
    4. +
    +

    What cookies do we use and why?

    +

    We list all the cookies we use on this website in the APPENDIX below.

    +

    + We do not use cookies set by ourselves via our web developers (first-party cookies). We only have those set by + others (third-party cookies). +

    +

    + Cookies are also sometimes classified by reference to their purpose. We use the following cookies for the + following purposes: +

    +
      +
    1. + Analytical/performance cookies: They allow us to recognize and count the number of visitors and to see how + visitors move around our website when they are using it, as well as dates and times they visit. This helps us to + improve the way our website works, for example, by ensuring that users are finding what they are looking for + easily. +
    2. +
    3. + Targeting cookies: These cookies record your visit to our website, the pages you have visited and the links you + have followed, as well as time spent on our website, and the websites visited just before and just after our + website. We will use this information to make our website and the advertising displayed on it more relevant to + your interests. We may also share this information with third parties for this purpose. +
    4. +
    +

    + In general, we use cookies and other technologies (such as web server logs) on our website to enhance your + experience and to collect information about how our website is used.{' '} +

    +

    + We will retain and evaluate information on your recent visits to our website and how you move around different + sections of our website for analytics purposes to understand how people use our website so that we can make it + more intuitive. The information also helps us to understand which parts of this website are most popular and + generally to assess user behavior and characteristics to measure interest in and use of the various areas of our + website. This then allows us to improve our website and the way we market our business. +

    +

    + This information may also be used to help us to improve, administer and diagnose problems with our server and + website. The information also helps us monitor traffic on our website so that we can manage our website's + capacity and efficiency. +

    +

    Other Technologies

    +

    + We may allow others to provide analytics services and serve advertisements on our behalf. In addition to the uses + of cookies described above, these entities may use other methods, such as the technologies described below, to + collect information about your use of our website and other websites and online services. +

    +

    + Pixels tags. Pixel tags (which are also called clear GIFs, web beacons, or pixels), are small pieces of code that + can be embedded on websites and emails. Pixels tags may be used to learn how you interact with our website pages + and emails, and this information helps us, and our partners provide you with a more tailored experience. +

    +

    + Device Identifiers. A device identifier is a unique label that can be used to identify a mobile device. Device + identifiers may be used to track, analyze and improve the performance of the website and ads delivered. +

    +

    What data is collected by cookies and other technologies on our website?

    +

    This information may include:

    +
      +
    1. + the IP and logical address of the server you are using (but the last digits are anonymized so we cannot identify + you). +
    2. +
    3. the top level domain name from which you access the internet (for example .ie, .com, etc)
    4. +
    5. the type of browser you are using,
    6. +
    7. the date and time you access our website
    8. +
    9. the internet address linking to our website.
    10. +
    +

    This website also uses cookies to:

    +
      +
    1. remember you and your actions while navigating between pages;
    2. +
    3. remember if you have agreed (or not) to our use of cookies on our website;
    4. +
    5. ensure the security of the website;
    6. +
    7. monitor and improve the performance of servers hosting the site;
    8. +
    9. distinguish users and sessions;
    10. +
    11. Improving the speed of the site when you access content repeatedly;
    12. +
    13. determine new sessions and visits;
    14. +
    15. show the traffic source or campaign that explains how you may have reached our website; and
    16. +
    17. allow us to store any customization preferences where our website allows this
    18. +
    +

    + We may also use other services, such as{' '} + + + Google Analytics + + +  (described below) or other third-party cookies, to assist with analyzing performance on our website. As part + of providing these services, these service providers may use cookies and the technologies described below to + collect and store information about your device, such as time of visit, pages visited, time spent on each page of + our website, links clicked and conversion information, IP address, browser, mobile network information, and type + of operating system used. +

    +

    Google Analytics Cookies

    +

    + This website uses{' '} + + + Google Analytics + + + , a web analytics service provided by Google, Inc. ("Google"). +

    +

    + We use Google Analytics to track your preferences and also to identify popular sections of our website. Use of + Google Analytics in this way, enables us to adapt the content of our website more specifically to your needs and + thereby improve what we can offer to you. +

    +

    + Google will use this information for the purpose of evaluating your use of our website, compiling reports on + website activity for website operators and providing other services relating to website activity and internet + usage. Google may also transfer this information to third parties where required to do so by law, or where such + third parties process the information on Google's behalf. Google will not associate your IP address with any + other data held by Google. +

    +

    In particular Google Analytics tells us

    +
      +
    1. your IP address (last 3 digits are masked);
    2. +
    3. the number of pages visited;
    4. +
    5. the time and duration of the visit;
    6. +
    7. your location;
    8. +
    9. the website you came from (if any);
    10. +
    11. the type of hardware you use (i.e. whether you are browsing from a desktop or a mobile device);
    12. +
    13. the software used (type of browser); and
    14. +
    15. your general interaction with our website.
    16. +
    +

    + As stated above, cookie-related information is not used to identify you personally, and what is compiled is only + aggregate data that tells us, for example, what countries we are most popular in, but not that you live in a + particular country or your precise location when you visited our website (this is because we have only half the + information- we know the country the person is browsing from, but not the name of person who is browsing). In such + an example Google will analyze the number of users for us, but the relevant cookies do not reveal their + identities. +

    +

    + By using this website, you consent to the processing of data about you by Google in the manner and for the + purposes set out above. Google Analytics, its purpose and function is further explained on the{' '} + + + Google Analytics website + + + . +

    +

    + For more information about Google Analytics cookies, please see Google's help pages and privacy policy:{' '} + + + Google's Privacy Policy + + +  and{' '} + + + Google Analytics Help pages + + + . For further information about the use of these cookies by Google{' '} + + + click here + + + . +

    +

    + What if you don’t agree with us monitoring your use of our website (even if we don't collect your + personal data)? +

    +

    + Enabling these cookies is not strictly necessary for our website to work but it will provide you with a better + browsing experience. You can delete or block the cookies we set, but if you do that, some features of this website + may not work as intended. +

    +

    + Most browsers are initially set to accept cookies. If you prefer, you can set your browser to refuse cookies and + control and/or delete cookies as you wish – for details, see{' '} + + + https://aboutcookies.org + + + . You can delete all cookies that are already on your device and you can set most browsers to prevent them from + being placed. You should be aware that if you do this, you may have to manually adjust some preferences every time + you visit an Internet site and some services and functionalities may not work if you do not accept the cookies + they send. +

    +

    + Advertisers and business partners that you access on or through our website may also send you cookies. We do not + control any cookies outside of our website. +

    +

    + If you have any further questions regarding disabling cookies you should consult with your preferred + browser’s provider or manufacturer. +

    +

    + In order to implement your objection it may be necessary to install an opt-out cookie on your browser. This cookie + will only indicate that you have opted out. It is important to note, that for technical reasons, the opt-out + cookie will only affect the browser from which you actively object from. If you delete the cookies in your browser + or use a different end device or browser, you will need to opt out again. +

    +

    + To opt out of being tracked by Google Analytics across all websites, Google has developed Google Analytics opt-out + browser add-on. If you would like to opt out of Google Analytics, you have the option of downloading and + installing this browser add-on which can be found under the link:{' '} + + + https://tools.google.com/dlpage/gaoptout + + + . +

    +

    Revisions to this Cookie Policy

    +

    + On this website, you can always view the latest version of our Privacy Policy and our Cookie Policy. We may modify + this Cookie Policy from time to time. If we make changes to this Cookie Policy, we will provide notice of such + changes, such as by sending an email notification, providing notice through our website or updating the + ‘Last Updated’ date at the beginning of this Cookie Policy. The amended Cookie Policy will be + effective immediately after the date it is posted. By continuing to access or use our website after the effective + date, you confirm your acceptance of the revised Cookie Policy and all of the terms incorporated therein by + reference. We encourage you to review our Privacy Policy and our Cookie Policy whenever you access or use our + website to stay informed about our information practices and the choices available to you. +

    +

    + If you do not accept changes which are made to this Cookie Policy, or take any measures described above to opt-out + by removing or rejecting cookies, you may continue to use this website but accept that it may not display and/or + function as intended by us. Any social media channels connected to us and third party applications will be subject + to the privacy and cookie policies and practices of the relevant platform providers which, unless otherwise + indicated, are not affiliated or associated with us Your exercise of any rights to opt-out may also impact how our + information and content is displayed and/or accessible to you on this website and on other websites. +

    +

    APPENDIX

    +

    Overview of cookies placed and the consequences if the cookies are not placed.

    +

    First-party cookies

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    #

    +
    +

    Name of cookie

    +
    +

    Domain

    +
    +

    Purpose(s) of cookie

    +
    +

    Storage period of cookie

    +
    +

    Consequences is cookie is not accepted

    +
    +

    1

    +
    +

    _BEAMER_FILTER_BY_URL_{'{productID}'}

    +
    +

    app.safe.global

    +
    +

    Stores whether to apply URL filtering on the feed.

    +
    +

    20 minutes

    +
    +

    User activity won't be tracked

    +
    +

    2

    +
    +

    _BEAMER_DATE_{'{productID}'}

    +
    +

    app.safe.global

    +
    +

    Stores the latest date in which the feed was opened.

    +
    +

    300 days

    +
    +

    User activity won't be tracked

    +
    +

    3

    +
    +

    _BEAMER_LAST_POST_SHOWN_{'{productID}'}

    +
    +

    app.safe.global

    +
    +

    Stores the ID of the last post shown as a teaser.

    +
    +

    Session

    +
    +

    User activity won't be tracked

    +
    +

    4

    +
    +

    _BEAMER_BOOSTED_ANNOUNCEMENT_DATE_{'{productID}'}

    +
    +

    app.safe.global

    +
    +

    Stores the latest date in which a boosted announcement was displayed.

    +
    +

    300 days

    +
    +

    User activity won't be tracked

    +
    +

    5

    +
    +

    _BEAMER_FIRST_VISIT_{'{productID}'}

    +
    +

    app.safe.global

    +
    +

    Stores the date of this user’s first visit to the site.

    +
    +

    300 days

    +
    +

    User activity won't be tracked

    +
    +

    6

    +
    +

    _BEAMER_USER_ID_{'{productID}'}

    +
    +

    app.safe.global

    +
    +

    Stores an internal ID for this user.

    +
    +

    300 days

    +
    +

    User activity won't be tracked

    +
    +

    Third-party cookies

    +

    The cookies from this table can be set by third-party wallets.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    #

    +
    +

    Name of cookie

    +
    +

    Domain

    +
    +

    Purpose(s) of cookie

    +
    +

    Storage period of cookie

    +
    +

    Consequences is cookie is not accepted

    +
    +

    1

    +
    +

    _ga

    +
    +

    safe.global

    +
    +

    Used to distinguish users

    +
    +

    2 years from set/update

    +
    +

    User activity won't be tracked

    +
    +

    2

    +
    +

    _ga

    +
    +

    getbeamer.com

    +
    +

    Used to distinguish users

    +
    +

    2 years from set/update

    +
    +

    User activity won't be tracked

    +
    +

    3

    +
    +

    _gid

    +
    +

    getbeamer.com

    +
    +

    Used to distinguish users

    +
    +

    24 hours

    +
    +

    User activity won't be tracked

    +
    +

    4

    +
    +

    _BEAMER_USER_ID_{'{productID}'}

    +
    +

    getbeamer.com

    +
    +

    Stores an internal ID for this user.

    +
    +

    300 days

    +
    +

    User activity won't be tracked

    +
    +

    5

    +
    +

    JSESSIONID

    +
    +

    app.getbeamer.com

    +
    +

    Stores an internal ID for this user.

    +
    +

    Session

    +
    +

    User activity won't be tracked

    +
    +
    +) const CookiePolicy: NextPage = () => { return ( @@ -9,9 +597,7 @@ const CookiePolicy: NextPage = () => { {'Safe{Wallet} – Cookie policy'} -
    - -
    +
    {IS_OFFICIAL_HOST && }
    ) } diff --git a/src/pages/imprint.tsx b/src/pages/imprint.tsx index 61182d1a74..c23ad614e7 100644 --- a/src/pages/imprint.tsx +++ b/src/pages/imprint.tsx @@ -1,7 +1,74 @@ import type { NextPage } from 'next' import Head from 'next/head' -import SafeImprint from '@/components/imprint' import { IS_OFFICIAL_HOST } from '@/config/constants' +import { Typography } from '@mui/material' +import Link from 'next/link' +import MUILink from '@mui/material/Link' + +const SafeImprint = () => ( +
    + + Imprint & Disclaimer + + + Information in accordance with section 5 of the Telemedia Act (TMG, Germany): + + + Core Contributors GmbH +
    + ℅ Full Node +
    + Skalitzer Str. 85-86 +
    + 10997 Berlin Germany +
    + + Managing directors: Richard Meißner, Tobias Schubotz +
    + Contact:{' '} + + info@cc0x.dev + +
    + District Court: Berlin Charlottenburg +
    + Register Number: HRB 240421 B +
    + + Disclaimer + + + Accountability for content + + + The contents of our pages have been created with the utmost care. However, we cannot guarantee the contents’ + accuracy, completeness or topicality. According to statutory provisions, we are furthermore responsible for our + own content on these web pages. In this context, please note that we are accordingly not obliged to monitor merely + the transmitted or saved information of third parties, or investigate circumstances pointing to illegal activity. + Our obligations to remove or block the use of information under generally applicable laws remain unaffected by + this as per §§ 8 to 10 of the Telemedia Act (TMG). + + + Accountability for links + + + Responsibility for the content of external links (to web pages of third parties) lies solely with the operators of + the linked pages. No violations were evident to us at the time of linking. Should any legal infringement become + known to us, we will remove the respective link immediately. + + + Copyright + + + This website and their contents are subject to copyright laws.{' '} + + + The code is open-source, released under GPL-3.0. + + + +
    +) const Imprint: NextPage = () => { return ( diff --git a/src/pages/licenses.tsx b/src/pages/licenses.tsx index 56c7e2f20a..cdf3713936 100644 --- a/src/pages/licenses.tsx +++ b/src/pages/licenses.tsx @@ -1,9 +1,751 @@ import type { NextPage } from 'next' import Head from 'next/head' -import SafeLicenses from '@/components/licenses' import { IS_OFFICIAL_HOST } from '@/config/constants' +import { Typography, Table, TableBody, TableRow, TableCell, TableHead, TableContainer, Box } from '@mui/material' +import ExternalLink from '@/components/common/ExternalLink' +import Paper from '@mui/material/Paper' -const Imprint: NextPage = () => { +const SafeLicenses = () => ( + <> + + Licenses + + + Libraries we use + + + + This page contains a list of attribution notices for third party software that may be contained in portions of + the {'Safe{Wallet}'}. We thank the open source community for all of their contributions. + + + Android + + + + + + + Library + + + License + + + + + + AndroidX + + + https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/LICENSE.txt + + + + + Bivrost for Kotlin + + + https://github.com/gnosis/bivrost-kotlin/blob/master/LICENSE + + + + + Dagger + + + {' '} + https://github.com/google/dagger#license{' '} + + + + + FloatingActionButton + + + https://github.com/Clans/FloatingActionButton/blob/master/LICENSE + + + + + Material Progress Bar + + + https://github.com/DreaminginCodeZH/MaterialProgressBar/blob/master/LICENSE + + + + + Kethereum + + + https://github.com/walleth/kethereum/blob/master/LICENSE + + + + + Koptional + + + {' '} + https://github.com/gojuno/koptional#license{' '} + + + + + Moshi + + + {' '} + https://github.com/square/moshi#license{' '} + + + + + OkHttp + + + {' '} + https://github.com/square/okhttp#license{' '} + + + + + Okio + + + {' '} + https://github.com/square/okio#license{' '} + + + + + Phrase + + + {' '} + https://github.com/square/phrase/#license{' '} + + + + + Picasso + + + {' '} + https://github.com/square/picasso#license{' '} + + + + + ReTrofit + + + {' '} + https://github.com/square/reTrofit#license{' '} + + + + + RxAndroid + + + https://github.com/ReactiveX/RxAndroid#license + + + + + RxBinding + + + https://github.com/JakeWharton/RxBinding#license + + + + + RxJava + + + {' '} + https://github.com/ReactiveX/RxJava#license{' '} + + + + + RxKotlin + + + https://github.com/ReactiveX/RxKotlin/blob/2.x/LICENSE + + + + + SpongyCastle + + + https://github.com/rtyley/spongycastle/blob/spongy-master/LICENSE.html + + + + + Svalinn Android + + + https://github.com/gnosis/svalinn-kotlin/blob/master/LICENSE + + + + + Timber + + + https://github.com/JakeWharton/timber#license + + + + + Zxing + + + https://github.com/zxing/zxing/blob/master/LICENSE + + + + +
    +
    +
    + + + iOS + + + + + + + Library + + + License + + + + + + BigInt + + + https://github.com/attaswift/BigInt/blob/master/LICENSE.md + + + + + BlockiesSwift + + + https://github.com/gnosis/BlockiesSwift/blob/master/LICENSE + + + + + CryptoEthereumSwift + + + https://github.com/yuzushioh/CryptoEthereumSwift/blob/master/LICENSE + + + + + CryptoSwift + + + https://github.com/krzyzanowskim/CryptoSwift#license + + + + + DateTools + + + {' '} + https://github.com/gnosis/DateTools#license{' '} + + + + + EthereumKit + + + https://github.com/D-Technologies/EthereumKit#license + + + + + Keycard.swift + + + https://github.com/gnosis/Keycard.swift/blob/master/LICENSE + + + + + Kingfisher + + + https://github.com/onevcat/Kingfisher#license + + + + + SipHash + + + https://github.com/attaswift/SipHash/blob/master/LICENSE.md + + + + + Starscream + + + https://github.com/daltoniam/Starscream/blob/master/LICENSE + + + + + RsBarcodesSwift + + + https://github.com/yeahdongcn/RSBarcodes_Swift#license + + + + + libidn2 + + + https://github.com/gnosis/libidn2/blob/master/COPYING.LESSERv3 + + + + + libunisTring + + + https://github.com/gnosis/libunisTring/blob/master/COPYING.LIB + + + + +
    +
    +
    + + + Web + + + + + + Library + License + + + + + @date-io/date-fns + + + https://github.com/dmtrKovalenko/date-io/blob/master/LICENSE + + + + + @emotion/cache + + + https://github.com/emotion-js/emotion/blob/main/LICENSE + + + + + @emotion/react + + + https://github.com/emotion-js/emotion/blob/main/LICENSE + + + + + @emotion/server + + + https://github.com/emotion-js/emotion/blob/main/LICENSE + + + + + @emotion/styled + + + https://github.com/emotion-js/emotion/blob/main/LICENSE + + + + + @safe-global/safe-modules-deployments + + + https://github.com/safe-global/safe-modules-deployments/blob/main/LICENSE + + + + + @mui/icons-material + + + https://github.com/mui/material-ui/blob/master/LICENSE + + + + + @mui/material + + + https://github.com/mui/material-ui/blob/master/LICENSE + + + + + @mui/x-date-pickers + + + https://github.com/mui/mui-x#mit-vs-commercial-licenses + + + + + @reduxjs/toolkit + + + https://github.com/reduxjs/redux-toolkit/blob/master/LICENSE + + + + + @safe-global/safe-apps-sdk + + + https://github.com/safe-global/safe-apps-sdk/blob/main/LICENSE.md + + + + + @safe-global/safe-core-sdk + + + https://github.com/safe-global/safe-core-sdk/blob/main/LICENSE.md + + + + + @safe-global/safe-core-sdk-utils + + + https://github.com/safe-global/safe-core-sdk/blob/main/LICENSE.md + + + + + @safe-global/safe-deployments + + + https://github.com/safe-global/safe-deployments/blob/main/LICENSE + + + + + @safe-global/safe-ethers-lib + + + https://github.com/safe-global/safe-core-sdk/blob/main/LICENSE.md + + + + + @safe-global/safe-gateway-typescript-sdk + + + https://github.com/safe-global/safe-gateway-typescript-sdk/blob/main/LICENSE.md + + + + + @safe-global/safe-react-components + + + https://github.com/safe-global/safe-react-components/blob/main/LICENSE.md + + + + + @sentry/react + + + https://github.com/getsentry/sentry-javascript/blob/develop/LICENSE + + + + + @sentry/tracing + + + https://github.com/getsentry/sentry-javascript/blob/develop/LICENSE + + + + + @truffle/hdwallet-provider + + + https://github.com/trufflesuite/truffle/blob/develop/LICENSE + + + + + @web3-onboard/coinbase + + + https://github.com/blocknative/web3-onboard/blob/main/LICENSE + + + + + @web3-onboard/core + + + https://github.com/blocknative/web3-onboard/blob/main/LICENSE + + + + + @web3-onboard/injected-wallets + + + https://github.com/blocknative/web3-onboard/blob/main/LICENSE + + + + + @web3-onboard/keystone + + + https://github.com/blocknative/web3-onboard/blob/main/LICENSE + + + + + @web3-onboard/ledger + + + https://github.com/blocknative/web3-onboard/blob/main/LICENSE + + + + + @web3-onboard/trezor + + + https://github.com/blocknative/web3-onboard/blob/main/LICENSE + + + + + @web3-onboard/walletconnect + + + https://github.com/blocknative/web3-onboard/blob/main/LICENSE + + + + + classnames + + + https://github.com/JedWatson/classnames/blob/main/LICENSE + + + + + date-fns + + + https://github.com/date-fns/date-fns/blob/main/LICENSE.md + + + + + ethereum-blockies-base64 + + + https://github.com/MyCryptoHQ/ethereum-blockies-base64 + + + + + ethers + + + https://github.com/ethers-io/ethers.js/blob/main/LICENSE.md + + + + + exponential-backoff + + + https://github.com/coveo/exponential-backoff/blob/master/LICENSE + + + + + fuse.js + + + https://github.com/krisk/Fuse/blob/master/LICENSE + + + + + js-cookie + + + https://github.com/js-cookie/js-cookie/blob/main/LICENSE + + + + + lodash + + + https://github.com/lodash/lodash/blob/master/LICENSE + + + + + next + + + https://github.com/vercel/next.js/blob/canary/LICENSE + + + + + next-pwa + + + https://github.com/shadowwalker/next-pwa/blob/master/LICENSE + + + + + papaparse + + + https://github.com/mholt/PapaParse/blob/master/LICENSE + + + + + qrcode.react + + + https://github.com/zpao/qrcode.react/blob/main/LICENSE + + + + + react + + + https://github.com/facebook/react/blob/main/LICENSE + + + + + react-dom + + + https://github.com/facebook/react/blob/main/LICENSE + + + + + react-dropzone + + + https://github.com/react-dropzone/react-dropzone/blob/master/LICENSE + + + + + react-gtm-module + + + https://github.com/alinemorelli/react-gtm/blob/master/LICENSE + + + + + react-hook-form + + + https://github.com/react-hook-form/react-hook-form/blob/master/LICENSE + + + + + react-papaparse + + + https://github.com/Bunlong/react-papaparse/blob/master/LICENSE + + + + + react-qr-reader + + + https://github.com/JodusNodus/react-qr-reader/blob/master/LICENSE + + + + + react-redux + + + https://github.com/reduxjs/react-redux/blob/master/LICENSE + + + + + semver + + + https://github.com/npm/node-semver/blob/main/LICENSE + + + + +
    +
    +
    + +) + +const Licenses: NextPage = () => { return ( <> @@ -15,4 +757,4 @@ const Imprint: NextPage = () => { ) } -export default Imprint +export default Licenses diff --git a/src/pages/privacy.tsx b/src/pages/privacy.tsx index 941876bbf8..e709b86f5d 100644 --- a/src/pages/privacy.tsx +++ b/src/pages/privacy.tsx @@ -1,8 +1,997 @@ +import type { MouseEventHandler, ReactNode } from 'react' import type { NextPage } from 'next' import Head from 'next/head' -import SafePrivacyPolicy from '@/components/privacy' import { IS_OFFICIAL_HOST } from '@/config/constants' +const SmoothScroll = ({ children }: { children: ReactNode }) => { + const onClick: MouseEventHandler = (e) => { + const anchor = (e.target as HTMLAnchorElement).getAttribute('href') + if (anchor?.startsWith('#')) { + e.preventDefault() + const element = document.querySelector(anchor) + if (element) { + element.scrollIntoView({ behavior: 'smooth' }) + } + } + } + + return
    {children}
    +} + +const SafePrivacyPolicy = () => ( + + + +

    Privacy Policy

    +

    Last updated in November 2023.

    +

    + Your privacy is important to us. It is our policy to respect your privacy and comply with any applicable law and + regulation regarding any personal information we may collect about you, including across our website,{' '} + https://app.safe.global + , and other sites we own and operate as well as mobile applications we offer. Wherever possible, we have + designed our website so that you may navigate and use our website without having to provide Personal Data. +

    +

    + This Privacy Policy describes how we, as a controller, collect, use and share your personal data. It applies to + personal data you voluntarily provide to us, or is automatically collected by us.{' '} +

    +

    + In this policy, "we", "us" and "our" refers to Core Contributors GmbH a company + incorporated in Germany with its registered address at Skalitzer Str. 85-86, ℅ Full Node, 10997 Berlin, + Germany. Any data protection related questions you might have about how we handle your personal data or if + you wish to exercise your data subject rights, please contact us by post or at privacy@cc0x.dev.{' '} +

    +

    + In this Policy, “personal data” means any information relating to you as an identified or identifiable + natural person (“Data Subject”); an identifiable natural person is one who can be identified, directly + or indirectly, in particular by reference to an identifier such as a name, an online identifier or to one or more + factors specific to your physical, physiological, genetic, mental, economic, cultural or social identity. +

    +

    + In this Policy, “processing” means any operation or set of operations which is performed on personal + data (as defined in this Privacy Policy) or on sets of personal data, whether or not by automated means, such as + collection, recording, organization, structuring, storage, adaptation or alteration, retrieval, consultation, use, + disclosure by transmission, dissemination or otherwise making available, alignment or combination, restriction, + erasure or destruction. +

    +

    1. Navigating this Policy

    +

    If you are viewing this policy online, you can click on the below links to jump to the relevant section:

    +
      +
    1. + Glossary +
    2. +
    3. + Your information and the Blockchain +
    4. +
    5. + How We Use Personal Data +
    6. +
    7. + Use of Third Party Applications +
    8. +
    9. + Sharing Your Personal Data +
    10. +
    11. + Transferring Your data outside of the EU +
    12. +
    13. + Existence of Automated Decision-making +
    14. +
    15. + Data Security +
    16. +
    17. + Your Rights as a Data Subject +
    18. +
    19. + Storing Personal Data +
    20. +
    21. + Children’s data +
    22. +
    23. + Changes to this Privacy Policy +
    24. +
    25. + Contacts us +
    26. +
    +

    2. Glossary

    +

    What do some of the capitalized terms mean in this policy?

    +
      +
    1. + “Blockchain” means a mathematically secured consensus ledger such as the Ethereum Virtual Machine, + an Ethereum Virtual Machine compatible validation mechanism, or other decentralized validation mechanisms. +
    2. +
    3. + “Transaction” means a change to the data set through a new entry in the continuous Blockchain. +
    4. +
    5. + “Smart Contract” is a piece of source code deployed as an application on the Blockchain which can be + executed, including self-execution of Transactions as well as execution triggered by 3rd parties. +
    6. +
    7. + “Token” is a digital asset transferred in a Transaction, including ETH, ERC20, ERC721 and ERC1155 + tokens. +
    8. +
    9. + “Wallet” is a cryptographic storage solution permitting you to store cryptographic assets by + correlation of a (i) Public Key and (ii) a Private Key or a Smart Contract to receive, manage and send Tokens. +
    10. +
    11. + “Recovery Phrase” is a series of secret words used to generate one or more Private Keys and derived + Public Keys. +
    12. +
    13. + “Public Key” is a unique sequence of numbers and letters within the Blockchain to distinguish the + network participants from each other. +
    14. +
    15. + “Private Key” is a unique sequence of numbers and/or letters required to initiate a Blockchain + Transaction and should only be known by the legal owner of the Wallet. +
    16. +
    17. + “Safe Account” is a modular, self-custodial (i.e. not supervised by us) smart contract-based + multi-signature Wallet. Safe Accounts are{' '} + + open-source + +  released under LGPL-3.0. +
    18. +
    19. + “{'Safe{Wallet}'}” refers to a web-based graphical user interface for Safe Accounts as well as a + mobile application on Android and iOS. +
    20. +
    21. + “Safe Account Transaction” is a Transaction of a Safe Account, authorized by a user, typically via + their Wallet.{' '} +
    22. +
    23. + “Profile” means the Public Key and user provided, human readable label stored locally on the + user's device. +
    24. +
    +

    3. Your information and the Blockchain

    +

    + Blockchains, also known as distributed ledger technology (or simply ‘DLT’), are made up of digitally + recorded data in a chain of packages called ‘blocks’. The manner in which these blocks are linked is + chronological, meaning that the data is very difficult to alter once recorded. Since the ledger may be distributed + all over the world (across several ‘nodes’ which usually replicate the ledger) this means there is no + single person making decisions or otherwise administering the system (such as an operator of a cloud computing + system), and that there is no centralized place where it is located either. +

    +

    + Accordingly, by design, records of a Blockchain cannot be changed or deleted and are said to be + ‘immutable’. This may affect your ability to exercise your rights such as your right to erasure + (‘right to be forgotten’), or your rights to object or restrict processing of your personal data. Data + on the Blockchain cannot be erased and cannot be changed. Although smart contracts may be used to revoke certain + access rights, and some content may be made invisible to others, it is not deleted. +

    +

    + In certain circumstances, in order to comply with our contractual obligations to you (such as delivery of Tokens) + it will be necessary to write certain personal data, such as your Wallet address, onto the Blockchain; this is + done through a smart contract and requires you to execute such transactions using your Wallet’s Private Key. +

    +

    + In most cases ultimate decisions to (i) transact on the Blockchain using your Wallet, as well as (ii) share the + Public Key relating to your Wallet with anyone (including us) rests with you. +

    +

    + IF YOU WANT TO ENSURE YOUR PRIVACY RIGHTS ARE NOT AFFECTED IN ANY WAY, YOU SHOULD NOT TRANSACT ON BLOCKCHAINS AS + CERTAIN RIGHTS MAY NOT BE FULLY AVAILABLE OR EXERCISABLE BY YOU OR US DUE TO THE TECHNOLOGICAL INFRASTRUCTURE OF + THE BLOCKCHAIN. IN PARTICULAR THE BLOCKCHAIN IS AVAILABLE TO THE PUBLIC AND ANY PERSONAL DATA SHARED ON THE + BLOCKCHAIN WILL BECOME PUBLICLY AVAILABLE +

    +

    4. How We Use Personal Data

    +

    4.1. When visiting our website and using {'Safe{Wallet}'}

    +

    + When visiting our website or using {'Safe{Wallet}'}, we may collect and process personal data. The data will be + stored in different instances +

    +
      +
    1. + We connect the Wallet to the web app to identify the user via their public Wallet address. For this purpose + we process: +
        +
      1. public Wallet address and
      2. +
      3. WalletConnect connection data
      4. +
      +
    2. +
    3. + When you create a new Safe Account we process the following data to compose a Transaction based on your entered + data to be approved by your Wallet: +
        +
      1. your public Wallet address,
      2. +
      3. account balance,
      4. +
      5. smart contract address of the Safe Account,
      6. +
      7. addresses of externally owned accounts and
      8. +
      9. user activity
      10. +
      +
    4. +
    +
      +
    1. + When you create a Profile for a new Safe Account we process the following data for the purpose of enabling you + to view your Safe Account after creation as well as enabling you to view all co-owned Safe Accounts: +
        +
      1. your public Wallet address and
      2. +
      3. account balance
      4. +
      +
    2. +
    +
      +
    1. + When you create a Profile for an existing Safe Account for the purpose of allowing you to view and use them in + the {'Safe{Wallet}'}, we process your +
        +
      1. public Wallet address,
      2. +
      3. Safe Account balance,
      4. +
      5. smart contract address of the Safe Account and
      6. +
      7. Safe Account owner's public Wallet addresses
      8. +
      +
    2. +
    +
      +
    1. + When you initiate a Safe Account Transaction we process the following data to compose the Transaction for + you based on your entered data:{' '} +
        +
      1. your public Wallet address and
      2. +
      3. smart contract address of the Safe Account
      4. +
      +
    2. +
    +
      +
    1. + When you sign a Safe Account Transaction we process the following data to enable you to sign the + Transaction using your Wallet: +
        +
      1. Safe Account balance,
      2. +
      3. smart contract address of Safe Account and
      4. +
      5. Safe Account owner's public Wallet addresses
      6. +
      +
    2. +
    +
      +
    1. + To enable you to execute The transaction on the Blockchain we process: +
        +
      1. your public Wallet address,
      2. +
      3. Safe Account balance,
      4. +
      5. smart contract address of the Safe Account,
      6. +
      7. Safe Account owner's public Wallet addresses and
      8. +
      9. Transactions signed by all Safe Account owners
      10. +
      +
    2. +
    +
      +
    1. + When we collect relevant data from the Blockchain to display context information in the + {`Safe{Wallet}`} + we process: +
        +
      1. your public Wallet address,
      2. +
      3. account balance,
      4. +
      5. account activity and
      6. +
      7. Safe Account owner's Public wallet addresses
      8. +
      +
    2. +
    +
      +
    1. + When we decode Transactions from the Blockchain for the purpose of providing Transaction information in a + conveniently readable format, we process: +
        +
      1. your public Wallet address
      2. +
      3. account balance and
      4. +
      5. account activity
      6. +
      +
    2. +
    +
      +
    1. + When we maintain a user profile to provide you with a good user experience through Profiles and an address + book we process: +
        +
      1. your public Wallet address,
      2. +
      3. label,
      4. +
      5. smart contract address of the Safe Account,
      6. +
      7. Safe Account owner's public wallet addresses,
      8. +
      9. last used Wallet (for automatic reconnect),
      10. +
      11. last used chain id,
      12. +
      13. selected currency,
      14. +
      15. theme and
      16. +
      17. address format
      18. +
      +
    2. +
    +

    The legal base for all these activities is the performance of the contract we have with you (GDPR Art.6.1b).

    +

    + THE DATA WILL BE STORED ON THE BLOCKCHAIN. GIVEN THE TECHNOLOGICAL DESIGN OF THE BLOCKCHAIN, AS EXPLAINED IN + SECTION 2, THIS DATA WILL BECOME PUBLIC AND IT WILL NOT LIKELY BE POSSIBLE TO DELETE OR CHANGE THE DATA AT ANY + GIVEN TIME. +

    +

    4.2. Tracking

    +

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

    +
      +
    1. IP address (will not be stored for EU users),
    2. +
    3. session tracking,
    4. +
    5. user behavior,
    6. +
    7. wallet type,
    8. +
    9. Safe Account address,
    10. +
    11. Signer wallet address,
    12. +
    13. device and browser user agent,
    14. +
    15. user consent,
    16. +
    17. operating system,
    18. +
    19. referrers,
    20. +
    21. user behavior: subpage, duration, and revisit, the date and time of access,
    22. +
    +

    + In the case you have given consent, we will additionally store an analytics cookie on your device to identify you + as a user across browsing sessions. The lawful basis for this processing is your consent (GDPR Art.6.1a) when + agreeing to accept cookies. +

    +

    + The collected data is solely used in the legitimate interest of improving our product and user experience. The + data is stored only temporarily and is deleted after 14 months. +

    +

    + We do not track any of the following: +

      +
    1. Wallet signatures
    2. +
    3. Granular transaction details
    4. +
    +

    +

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

    +
      +
    1. IP addresses,
    2. +
    3. meta and communication data,
    4. +
    5. website access and
    6. +
    7. log data
    8. +
    +

    + The lawful basis for this processing is our legitimate interest (GDPR Art.6.1f) in ensuring the correctness of the + service. +

    +

    4.2.3. Anonymized tracking

    +

    + We will anonymize the following personal data to gather anonymous user statistics on your browsing behavior on our + website: +

      +
    1. daily active users,
    2. +
    3. new users acquired from a specific campaign,
    4. +
    5. user journeys,
    6. +
    7. number of users per country,
    8. +
    9. difference in user behavior between mobile vs. web visitors.
    10. +
    +

    +

    + The lawful basis for this processing is our legitimate interest (GDPR Art.6.1f) in improving our product and user + experience. +

    +

    4.3. When Participating in User Experience Research (UXR)

    +

    + When you participate in our user experience research we may collect and process some personal data. This data may + include: +

    +
      +
    1. your name
    2. +
    3. your email
    4. +
    5. your phone type
    6. +
    7. your occupation
    8. +
    9. range of managed funds
    10. +
    +

    + In addition, we may take a recording of you while testing {'Safe{Wallet}'} for internal and external use. The + basis for this collection and processing is our legitimate business interest in monitoring and improving our + services. +

    +

    + The lawful basis for this processing is your consent as provided before participating in user experience research. +

    +

    4.4. Publishing the app

    +

    4.4.1 Publishing the app on Google Play Store.

    +

    We process the following information to enable you to download the app on smartphones running Android:

    +
      +
    1. google account and
    2. +
    3. e-mail address
    4. +
    +

    4.4.2 Publishing the app on Apple App Store

    +

    We process the following information to enable you to download the app on smartphones running iOS:

    +
      +
    1. apple account and
    2. +
    3. e-mail address
    4. +
    +

    + The lawful basis for these two processing activities is the performance of the contract we have with you (GDPR + Art.6.1b).{' '} +

    +

    4.5. Use of the app

    +

    4.5.1 We provide the app to you to enable you to use it. For this purpose we process your:

    +
      +
    1. mobile device information,
    2. +
    3. http request caches and
    4. +
    5. http request cookies
    6. +
    +

    + 4.5.2 In order to update you about changes in the app, we need to send you push notifications. For this purpose we + process your: +

    +
      +
    1. Transactions executed and failed,
    2. +
    3. assets sent,
    4. +
    5. assets received
    6. +
    +

    + 4.5.3 To provide support to you and notify you about outage resulting in unavailability of the service, we process + your: +

    +
      +
    1. pseudonymized user identifier
    2. +
    +

    + 4.5.4 In order to provide remote client configuration and control whether to inform about, recommend or force you + to update your app or enable/disable certain app features we process your: +

    +
      +
    1. User agent,
    2. +
    3. app information (version, build number etc.),
    4. +
    5. language,
    6. +
    7. Country,
    8. +
    9. Platform
    10. +
    11. operating system
    12. +
    13. Browser
    14. +
    15. Device category
    16. +
    17. User audience
    18. +
    19. User property
    20. +
    21. User in random percentage
    22. +
    23. Imported segment
    24. +
    25. date/time
    26. +
    27. first open
    28. +
    29. installation ID
    30. +
    +

    + For all these activities (4.5.1-4.54) we rely on the legal base of performance of a contract (GDPR Art.6.1b) with + you.{' '} +

    +

    4.5.5 Finally, to report errors and improve user experience we process your:

    +
      +
    1. User agent info (Browser, OS, device),
    2. +
    3. URL that you were on (Can contain Safe Account address) and
    4. +
    5. Error info: Time, stacktrace
    6. +
    +

    We rely on our legitimate interest (GDPR Art.6.1f) of ensuring product quality.

    +

    + 4.5.6 We process your personal data to allow you to authenticate using your gmail account or AppleID and to create + a signer wallet/owner account . For that purpose following personal data is processed: +

    +
      +
    1. Anonymised device information and identifiers, e.g. IP address, cookie IDs, device type
    2. +
    3. User account authentication information (e.g. username, password)
    4. +
    5. + Unique user identifier (e.g. a random string associated with authentication, at times can be email. If so, + sensitive strings are processed but hashed and not stored) +
    6. +
    7. Connection and usage Information (e.g. logins to the application)
    8. +
    +

    + For this processing, we rely on our legitimate interest (GDPR Art.6.1f) of facilitating the onboarding for users + and ameliorating the user experience with regards to our product. +

    +

    + 4.5.7 Providing on and off-ramp services to enable you to top up your Safe Account with e.g. bank transfer, debit + card, credit card. For this purpose MoonPay may process your: +

    +
      +
    1. full name
    2. +
    3. date of birth
    4. +
    5. nationality
    6. +
    7. gender
    8. +
    9. signature
    10. +
    11. utility bills
    12. +
    13. photographs
    14. +
    15. phone number
    16. +
    17. home address
    18. +
    19. email
    20. +
    21. + information about the transactions you make via MoonPay services (e.g. name of the recipient, your name, the + amount, and/or timestamp) +
    22. +
    23. geo location/tracking details
    24. +
    25. operating system
    26. +
    27. personal IP address
    28. +
    +

    + To conduct this activity we rely on our legitimate interest (GDPR Art.6.1f) of ameliorating the onboarding process + and the user experience through providing an easier option to customers to fund their account. +

    +

    4.6 Other uses of your Personal Data

    +

    + We may process any of your Personal Data where it is necessary to establish, exercise, or defend legal claims. The + legal basis for this is our legitimate interests, namely the protection and assertion of our legal rights, your + legal rights and the legal rights of others. +

    +

    + Further, we may process your Personal data where such processing is necessary in order for us to comply with a + legal obligation to which we are subject. The legal basis for this processing is our legitimate interests, namely + the protection and assertion of our legal rights. +

    +

    5. Use of Third Party Applications

    +

    5.1. Blockchain

    +

    + When using Safe Accounts your smart contract address, Safe Account Transactions, addresses of signer accounts and + ETH balances and token balances will be stored on the Blockchain. See section 2 of this Policy +

    +

    + THE INFORMATION WILL BE DISPLAYED PERMANENTLY AND PUBLIC, THIS IS PART OF THE NATURE OF THE BLOCKCHAIN. IF YOU ARE + NEW TO THIS FIELD, WE HIGHLY RECOMMEND INFORMING YOURSELF ABOUT THE BLOCKCHAIN TECHNOLOGY BEFORE USING OUR + SERVICES. +

    +

    5.2. Amazon Web Services

    +

    + We use{' '} + + Amazon Web Services (AWS) + +  to store log and database data as described in section 4.1. +

    +

    5.3. Datadog

    +

    + We use{' '} + + Datadog + +  to store log data as described in section 4.1. +

    +

    5.4. Mobile app stores

    +

    + {'Safe{Wallet}'} mobile apps are distributed via{' '} + + Apple AppStore + +  and{' '} + + Google Play Store + + . They most likely track user behavior when downloading apps from their stores as well as when using apps. We only + have very limited access to that data. We can view aggregated statistics on installs and uninstalls. Grouping by + device type, app version, language, carrier and country is possible. +

    +

    5.5. Fingerprint/Touch ID/ Face ID

    +

    + We enable the user to unlock the {'Safe{Wallet}'} mobile app via biometrics information (touch ID or face ID). + This is a feature of the operating system. We do not store any of this data. Instead, the API of the operating + system is used to validate the user input. If you have any further questions you should consult with your + preferred mobile device provider or manufacturer. +

    +

    5.6. Google Firebase

    +

    + We use the following{' '} + + Google Firebase + +  services: +

    +
      +
    • + Firebase Cloud Messaging: Provide updates to the user about changes in the mobile apps via push notifications. +
    • +
    • + Firebase remote config: Inform users about, recommend or force user to update their mobile app or + enabling/disabling certain app features. These settings are global for all users, no personalization is + happening. +
    • +
    • Firebase crash reporting: Report errors and crashes to improve product and user experience.
    • +
    +

    5.7. WalletConnect

    +

    + + WalletConnect + +  is used to connect wallets to dapps using end-to-end encryption by scanning a QR code. We do not store any + information collected by WalletConnect.{' '} +

    +

    5.8. Sentry

    +

    + We use{' '} + + Sentry + +  to collect error reports and crashes to improve product and user experience.{' '} +

    +

    5.9. Beamer

    +

    + We use{' '} + + Beamer + +  providing updates to the user about changes in the app. Beamer's purpose and function are further + explained under the following link{' '} + + https://www.getbeamer.com/showcase/notification-center + + . +

    +

    We do not store any information collected by Beamer.

    +

    5.10. Node providers

    +

    + We use{' '} + + Infura + +  and{' '} + + Nodereal + +  to query public blockchain data from our backend services. All Safe Accounts are monitored, no + personalization is happening and no user IP addresses are forwarded. Personal data processed are: +

    +
      +
    • Your smart contract address of the Safe;
    • +
    • Transaction id/hash
    • +
    • Transaction data
    • +
    +

    5.11. Tenderly

    +

    + We use{' '} + + Tenderly + +  to simulate blockchain transactions before they are executed. For that we send your smart contract address + of your Safe Account and transaction data to Tenderly. +

    +

    5.12. Internal communication

    +

    We use the following tools for internal communication.

    + +

    5.13. Web3Auth

    +

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

    +

    5.14. MoonPay

    +

    + We use{' '} + + MoonPay + {' '} + to offer on-ramp and off-ramp services. For that purpose personal data is required for KYC/AML or other financial + regulatory requirements. This data is encrypted by MoonPay. +

    +

    5.15. Spindl

    +

    + We use{' '} + + Spindl + + , a measurement and attribution solution for web3 that assists us in comprehending how users interact with + different decentralized applications and our app and to enhance your experience with {`Safe{Wallet}`}. For + enhanced privacy, data is stored for a period of 7 days after which it is securely deleted. +

    +

    6. Sharing Your Personal Data

    +

    + We may pass your information to our Business Partners, administration centers, third party service providers, + agents, subcontractors and other associated organizations for the purposes of completing tasks and providing our + services to you. +

    +

    + In addition, when we use any other third-party service providers, we will disclose only the personal information + that is necessary to deliver the service required and we will ensure that they keep your information secure and + not use it for their own direct marketing purposes. In addition, we may transfer your personal information to a + third party as part of a sale of some, or all, of our business and assets or as part of any business restructuring + or reorganization, or if we are under a duty to disclose or share your personal data in order to comply with any + legal obligation. However, we will take steps to ensure that your privacy rights continue to be protected. +

    +

    7. Transferring Your data outside of the EU

    +

    + Wherever possible we will choose service providers based in the EU. For those outside the EU, wherever possible we + will configure data to be inside the EU. We concluded the new version of the Standard Contractual Clauses with + these service providers (2021/914). +

    +

    Service providers in the US:

    +
      +
    • Amazon Web Service Inc.
    • +
    • Google LLC
    • +
    • Data Dog Inc.
    • +
    • Slack Technologies LLC
    • +
    • Joincube Inc. (Beamer)
    • +
    • Functional software Inc. (Sentry)
    • +
    • Notion Labs Inc.
    • +
    • ConsenSys Software Inc.
    • +
    +

    Service providers in other countries outside of the EU:

    +
      +
    • Tenderly d.o.o. is based in Serbia.
    • +
    • Node Real PTE Ltd. is based in Singapore.
    • +
    • Torus Labs PTE. Ltd. is based in Singapore.
    • +
    • Eighteenth September Limited (“MoonPay”) in the Seychelles.
    • +
    +

    + HOWEVER, WHEN INTERACTING WITH THE BLOCKCHAIN, AS EXPLAINED ABOVE IN THIS POLICY, THE BLOCKCHAIN IS A GLOBAL + DECENTRALIZED PUBLIC NETWORK AND ACCORDINGLY ANY PERSONAL DATA WRITTEN ONTO THE BLOCKCHAIN MAY BE TRANSFERRED AND + STORED ACROSS THE GLOBE. +

    +

    8. Existence of Automated Decision-making

    +

    We do not use automatic decision-making or profiling when processing Personal Data.

    +

    9. Data Security

    +

    + We have put in place appropriate security measures to prevent your personal data from being accidentally lost, + used or accessed in an unauthorized way, altered or disclosed. In addition, we limit access to your personal data + to those employees, agents, contractors and other third parties who have a business need to know. They will only + process your personal data on our instructions and they are subject to a duty of confidentiality. +

    +

    + We have put in place procedures to deal with any suspected personal data breach and will notify you and any + applicable regulator of a breach where we are legally required to do so. +

    +

    10. Your Rights as a Data Subject

    +

    + You have certain rights under applicable legislation, and in particular under Regulation EU 2016/679 (General Data + Protection Regulation or ‘GDPR’). We explain these below. You can find out more about the GDPR and + your rights by accessing the{' '} + + European Commission’s website + + . If you wish to exercise your data subject rights, please contact us by post or at privacy@cc0x.dev. +

    +
    Right Information and access
    +

    + You have a right to be informed about the processing of your personal data (and if you did not give it to us, + information as to the source) and this Privacy Policy intends to provide the information. Of course, if you have + any further questions you can contact us on the above details. +

    +
    Right to rectification
    +

    + You have the right to have any inaccurate personal information about you rectified and to have any incomplete + personal information about you completed. You may also request that we restrict the processing of that + information. The accuracy of your information is important to us. If you do not want us to use your Personal + Information in the manner set out in this Privacy Policy, or need to advise us of any changes to your personal + information, or would like any more information about the way in which we collect and use your Personal + Information, please contact us at the above details. +

    +
    Right to erasure (right to be ‘forgotten’)
    +

    + You have the general right to request the erasure of your personal information in the following circumstances: +

    +
      +
    • the personal information is no longer necessary for the purpose for which it was collected;
    • +
    • + you withdraw your consent to consent based processing and no other legal justification for processing applies; +
    • +
    • you object to processing for direct marketing purposes;
    • +
    • we unlawfully processed your personal information; and
    • +
    • erasure is required to comply with a legal obligation that applies to us.
    • +
    +

    + HOWEVER, WHEN INTERACTING WITH THE BLOCKCHAIN WE MAY NOT BE ABLE TO ENSURE THAT YOUR PERSONAL DATA IS DELETED. + THIS IS BECAUSE THE BLOCKCHAIN IS A PUBLIC DECENTRALIZED NETWORK AND BLOCKCHAIN TECHNOLOGY DOES NOT GENERALLY + ALLOW FOR DATA TO BE DELETED AND YOUR RIGHT TO ERASURE MAY NOT BE ABLE TO BE FULLY ENFORCED. IN THESE + CIRCUMSTANCES WE WILL ONLY BE ABLE TO ENSURE THAT ALL PERSONAL DATA THAT IS HELD BY US IS PERMANENTLY DELETED. +

    +

    We will proceed to comply with an erasure request without delay unless continued retention is necessary for:

    +
      +
    • Exercising the right of freedom of expression and information;
    • +
    • Complying with a legal obligation under EU or other applicable law;
    • +
    • The performance of a task carried out in the public interest;
    • +
    • + Archiving purposes in the public interest, scientific or historical research purposes, or statistical purposes, + under certain circumstances; and/or +
    • +
    • The establishment, exercise, or defense of legal claims.
    • +
    +
    Right to restrict processing and right to object to processing
    +

    You have a right to restrict processing of your personal information, such as where:

    +
      +
    1. you contest the accuracy of the personal information;
    2. +
    3. + where processing is unlawful you may request, instead of requesting erasure, that we restrict the use of the + unlawfully processed personal information; +
    4. +
    5. + we no longer need to process your personal information but need to retain your information for the + establishment, exercise, or defense of legal claims. +
    6. +
    +

    + You also have the right to object to processing of your personal information under certain circumstances, such as + where the processing is based on your consent and you withdraw that consent. This may impact the services we can + provide and we will explain this to you if you decide to exercise this right. +

    +

    + HOWEVER, WHEN INTERACTING WITH THE BLOCKCHAIN, AS IT IS A PUBLIC DECENTRALIZED NETWORK, WE WILL LIKELY NOT BE ABLE + TO PREVENT EXTERNAL PARTIES FROM PROCESSING ANY PERSONAL DATA WHICH HAS BEEN WRITTEN ONTO THE BLOCKCHAIN. IN THESE + CIRCUMSTANCES WE WILL USE OUR REASONABLE ENDEAVORS TO ENSURE THAT ALL PROCESSING OF PERSONAL DATA HELD BY US IS + RESTRICTED, NOTWITHSTANDING THIS, YOUR RIGHT TO RESTRICT TO PROCESSING MAY NOT BE ABLE TO BE FULLY ENFORCED. +

    +
    Right to data portability
    +

    + Where the legal basis for our processing is your consent or the processing is necessary for the performance of a + contract to which you are party or in order to take steps at your request prior to entering into a contract, you + have a right to receive the personal information you provided to us in a structured, commonly used and + machine-readable format, or ask us to send it to another person. +

    +
    Right to freedom from automated decision-making
    +

    + As explained above, we do not use automated decision-making, but where any automated decision-making takes place, + you have the right in this case to express your point of view and to contest the decision, as well as request that + decisions based on automated processing concerning you or significantly affecting you and based on your personal + data are made by natural persons, not only by computers. +

    +
    Right to object to direct marketing (‘opting out’)
    +

    + You have a choice about whether or not you wish to receive information from us. We will not contact you for + marketing purposes unless: +

    +
      +
    • + you have a business relationship with us, and we rely on our legitimate interests as the lawful basis for + processing (as described above) +
    • +
    • you have otherwise given your prior consent (such as when you download one of our guides)
    • +
    +

    + You can change your marketing preferences at any time by contacting us on the above details. On each and every + marketing communication, we will always provide the option for you to exercise your right to object to the + processing of your personal data for marketing purposes (known as ‘opting-out’) by clicking on the + ‘unsubscribe’ button on our marketing emails or choosing a similar opt-out option on any forms we use + to collect your data. You may also opt-out at any time by contacting us on the below details. +

    +

    + Please note that any administrative or service-related communications (to offer our services, or notify you of an + update to this Privacy Policy or applicable terms of business, etc.) will solely be directed at our clients or + business partners, and such communications generally do not offer an option to unsubscribe as they are necessary + to provide the services requested. Therefore, please be aware that your ability to opt-out from receiving + marketing and promotional materials does not change our right to contact you regarding your use of our website or + as part of a contractual relationship we may have with you. +

    +
    Right to request access
    +

    + You also have a right to access information we hold about you. We are happy to provide you with details of your + Personal Information that we hold or process. To protect your personal information, we follow set storage and + disclosure procedures, which mean that we will require proof of identity from you prior to disclosing such + information. You can exercise this right at any time by contacting us on the above details. +

    +
    Right to withdraw consent
    +

    + Where the legal basis for processing your personal information is your consent, you have the right to withdraw + that consent at any time by contacting us on the above details. +

    +
    Raising a complaint about how we have handled your personal data
    +

    + If you wish to raise a complaint on how we have handled your personal data, you can contact us as set out above + and we will then investigate the matter. +

    +
    Right to lodge a complaint with a relevant supervisory authority
    +

    + We encourage you to contact us at privacy@cc0de.dev if you have any privacy related concerns. Should you + disapprove of the response we have provided you, you have the right to lodge a complaint with our supervisory + authority, or with the data protection authority of the European member state you live or work in. The details of + the supervisory authority responsible for Berlin, Germany, are: +

    +

    Berliner Beauftragte für Datenschutz und Informationsfreiheit

    +

    + Alt-Moabit 59-61 +
    + 10555 Berlin +
    + Germany +
    + Phone: 030/138 89-0 +

    +

    + + https://www.datenschutz-berlin.de + +   +

    +

    + You also have the right to lodge a complaint with the supervisory authority in the country of your habitual + residence, place of work, or the place where you allege an infringement of one or more of our rights has taken + place, if that is based in the EEA. +

    +

    11. Storing Personal Data

    +

    + We retain your information only for as long as is necessary for the purposes for which we process the information + as set out in this policy. +

    +

    + However, we may retain your Personal Data for a longer period of time where such retention is necessary for + compliance with a legal obligation to which we are subject, or in order to protect your vital interests or the + vital interests of another natural person. +

    +

    12. Children’s data

    +

    + Our products and services are neither designed nor intended for use by children and persons under the age of 18. + If you suspect or discover that our products and services are being used by a child, please contact us immediately + at privacy@cc0x.dev +

    +

    13. Changes to this Privacy Policy

    +

    + We may modify this privacy policy at any time to comply with legal requirements as well as developments within our + organization. When we do, we will revise the date at the top of this page. Each visit or interaction with our + services will be subject to the new privacy policy. We encourage you to regularly review our privacy policy to + stay informed about our data protection policy. Unless, we implement profound changes that we proactively notify + you about, you acknowledge that it is your responsibility to review our privacy policy to be aware of + modifications. If you do not agree to the revised policy, you should discontinue your use of this website. +

    +

    14. Contact Us

    +
    Contact us by post or email at:
    +

    + Core Contributors GmbH +
    ℅ Full Node +
    + Skalitzer Str. 85-86 +
    + 10997 Berlin +
    + Germany +
    + privacy@cc0x.dev +

    +
    Contact our Data Protection Officer by post or email at:
    +

    + TechGDPR DPC GmbH +
    + Heinrich-Roller Str. 15 +
    + 10405 Berlin +
    + Germany +

    +

    + corecontributors.dpo@techgdpr.com +

    +
    +) + const PrivacyPolicy: NextPage = () => { return ( <> diff --git a/src/pages/settings/setup.tsx b/src/pages/settings/setup.tsx index a551c4a177..3229762803 100644 --- a/src/pages/settings/setup.tsx +++ b/src/pages/settings/setup.tsx @@ -24,7 +24,7 @@ const Setup: NextPage = () => {
    - + diff --git a/src/pages/terms.tsx b/src/pages/terms.tsx index 369d05aad3..7afbc146c9 100644 --- a/src/pages/terms.tsx +++ b/src/pages/terms.tsx @@ -1,9 +1,573 @@ import type { NextPage } from 'next' import Head from 'next/head' -import SafeTerms from '@/components/terms' +import { Typography } from '@mui/material' +import Link from 'next/link' +import MUILink from '@mui/material/Link' +import { AppRoutes } from '@/config/routes' +import { DISCORD_URL, HELP_CENTER_URL, TWITTER_URL } from '@/config/constants' import { IS_OFFICIAL_HOST } from '@/config/constants' -const Imprint: NextPage = () => { +const SafeTerms = () => ( +
    + + Terms and Conditions + +

    Last updated: May, 2023

    +

    1. What is the scope of the Terms?

    +
      +
    1. + These Terms and Conditions (“Terms”) become part of any contract (“Agreement”) between + you (“you”, “yours” or “User”) and Core Contributors GmbH (“CC”, + “we”, “our” or “us”) provided we made these Terms accessible to you prior to + entering into the Agreement and you consent to these Terms. We are a limited liability company registered with + the commercial register of Berlin Charlottenburg under company number HRB 240421 B, with its registered + office at the ℅ Full Node, Skalitzer Str. 85-86, 10997 Berlin, Germany. You can contact us by writing to + info@cc0x.dev. +
    2. +
    3. + The Agreement is concluded by using the Mobile App, Web App and/or Browser Extension subject to these + Terms. The use of our Services is only permitted to legal entities, partnerships and natural persons with + unlimited legal capacity. In particular, minors are prohibited from using our Services. +
    4. +
    5. + The application of your general terms and conditions is excluded. Your deviating, conflicting or supplementary + general terms and conditions shall only become part of the Agreement if and to the extent that CC has expressly + agreed to their application in writing. This consent requirement shall apply in any case, even if for example + CC, being aware of your general terms and conditions, accepts payments by the contractual partner without + reservations. +
    6. +
    7. + We reserve the right to change these Terms at any time and without giving reasons, while considering and + weighing your interests. The new Terms will be communicated to you in advance. They are considered as agreed + upon if you do not object to their validity within 14 days after receipt of the notification. We will separately + inform you about the essential changes, the possibility to object, the deadline and the consequences of + inactivity. If you object, the current version of the Terms remains applicable. Our right to terminate the + contract according to Clause 8 remains unaffected. +
    8. +
    +

    2. What do some of the capitalized terms mean in the Agreement?

    +
      +
    1. + “Blockchain” means a mathematically secured consensus ledger such as the Ethereum Virtual Machine, + an Ethereum Virtual Machine compatible validation mechanism, or other decentralized validation mechanisms. +
    2. +
    3. + “Transaction” means a change to the data set through a new entry in the continuous Blockchain. +
    4. +
    5. + “Smart Contract” means a piece of source code deployed as an application on the Blockchain which can + be executed, including self-execution of Transactions as well as execution triggered by 3rd parties. +
    6. +
    7. + “Token” means a digital asset transferred in a Transaction, including ETH, ERC20, ERC721 and ERC1155 + tokens. +
    8. +
    9. + “Wallet” means a cryptographic storage solution permitting you to store cryptographic assets by + correlation of a (i) Public Key and (ii) a Private Key, or a Smart Contract to receive, manage and send Tokens. +
    10. +
    11. + “Recovery Phrase” means a series of secret words used to generate one or more Private Keys and + derived Public Keys. +
    12. +
    13. + “Public Key” means a unique sequence of numbers and letters within the Blockchain to distinguish the + network participants from each other. +
    14. +
    15. + “Private Key” means a unique sequence of numbers and/or letters required to initiate a Blockchain + Transaction and should only be known by the legal owner of the Wallet. +
    16. +
    +

    3. What are the Services offered?

    +

    + Our services (“Services”) primarily consist of enabling users to create their Safe Accounts and + ongoing interaction with it on the Blockchain. +

    +
      +
    1. “Safe Account”
    2. +
    +

    + A Safe Account is a modular, self-custodial (i.e. not supervised by us) smart contract-based wallet not provided + by CC. Safe Accounts are{' '} + + + open-source + + +  released under LGPL-3.0. +

    +

    + Smart contract wallet means, unlike a standard private key Wallet, that access control for authorizing any + Transaction is defined in code. An example are multi-signature wallets which require that any Transaction must be + signed by a minimum number of signing wallets whereby the specifics of the requirements to authorize a Transaction + can be configured in code.{' '} +

    +

    + Owners need to connect a signing wallet with a Safe Account. Safe Accounts are compatible inter alia with standard + private key Wallets such as hardware wallets, browser extension wallets and mobile wallets that support + WalletConnect. +

    +
      +
    1. “Safe App”
    2. +
    +

    + You may access Safe Accounts using the {'Safe{Wallet}'} web app, mobile app for iOS and android, or the browser + extension (each a “Safe App”). The Safe App may be used to manage your personal digital assets on + Ethereum and other common EVM chains when you connect a Safe Account with third-party services (as defined + below). The Safe App provides certain features that may be amended from time to time.{' '} +

    +
      +
    1. “Third-Party Safe Apps”
    2. +
    +

    + The Safe App allows you to connect Safe Accounts to third-party decentralized applications + (“Third-Party Safe Apps”) and use third-party services such as from the decentralized + finance sector, DAO Tools or services related to NFTs (“Third-Party Services"). The + Third-Party Safe Apps are integrated in the user interface of the Safe App via inline framing. The provider + of the Third-Party Safe App and related Third-Party Service is responsible for the operation of the service + and the correctness, completeness and actuality of any information provided therein. We make a pre-selection of + Third-Party Safe Apps that we show in the Safe App. However, we only perform a rough triage in advance for + obvious problems and functionality in terms of loading time and resolution capability of the transactions. + Accordingly, in the event of any (technical) issues concerning the Third-Party Services, the user must only + contact the respective service provider directly. The terms of service, if any, shall be governed by the + applicable contractual provisions between the User and the respective provider of the Third-Party Service. + Accordingly, we are not liable in the event of a breach of contract, damage or loss related to the use of such + Third-Party Service. +

    +

    4. What do the Services not consist of?

    +

    Our Services do not consist of:

    +
      +
    1. + activity regulated by the Federal Financial Supervisory Authority (BaFin) or any other regulatory agency in any + jurisdiction; +
    2. +
    3. coverage underwritten by any regulatory agency’s compensation scheme;
    4. +
    5. + custody of your Recovery Phrase, Private Keys, Tokens or the ability to remove or freeze your Tokens, i.e. a + Safe Account is a self-custodial wallet; +
    6. +
    7. the storage or transmission of fiat currencies;
    8. +
    9. + back-up services to recover your Recovery Phrase or Private Keys, for whose safekeeping you are solely + responsible; CC has no means to recover your access to your Tokens, when you lose access to your Safe Account; +
    10. +
    11. + any form of legal, financial, investment, accounting, tax or other professional advice regarding Transactions + and their suitability to you;{' '} +
    12. +
    13. + the responsibility to monitor authorized Transactions or to check the correctness or completeness of + Transactions before you are authorizing them. +
    14. +
    +

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

    +
      +
    1. + We provide you the possibility to interact with your Safe Account through Third-Party Services. Any + activities you engage in with, or services you receive from a third party is between you and that third party + directly. The conditions of service provisions, if any, shall be governed by the applicable contractual + provisions between you and the respective provider of the Third-Party Service.{' '} +
    2. +
    3. + The Services rely in part on third-party and open-source software, including the Blockchain, and the continued + development and support by third parties. There is no assurance or guarantee that those third parties will + maintain their support of their software or that open-source software will continue to be maintained. This may + have a material adverse effect on the Services. +
    4. +
    5. This means specifically:
    6. +
    +
      +
    • + We do not have any oversight over your activities with Third-Party Services especially by using + Third-Party Safe Apps, and therefore we do not and cannot make any representation regarding their + appropriateness and suitability for you. +
    • +
    • + Third-Party Services are not hosted, owned, controlled or maintained by us. We also do not participate in + the Transaction and will not and cannot monitor, verify, censor or edit the functioning or content of any + Third-Party Service. +
    • +
    • + We have not conducted any security audit, bug bounty or formal verification (whether internal or external) of + the Third-Party Services. +
    • +
    • + We have no control over, do not recommend, endorse, or otherwise take a position on the integrity, functioning + of, content and your use of Third-Party Services, whose sole responsibility lies with the person from whom + such services or content originated. +
    • +
    • + When you access or use Third-Party Services you accept that there are risks in doing so and that you alone + assume any such risks when choosing to interact with them. We are not liable for any errors or omissions or for + any damages or loss you might suffer through interacting with those Third-Party Services, such as + Third-Party Safe Apps. +
    • +
    • + You know of the inherent risks of cryptographic and Blockchain-based systems and the high volatility of Token + markets. Transactions undertaken in the Blockchain are irrevocable and irreversible and there is no possibility + to refund Token that have been deployed. +
    • +
    • + You should read the license requirements, terms and conditions as well as privacy policy of each + Third-Party Service that you access or use. Certain Third-Party Services may involve complex + Transactions that entail a high degree of risk. +
    • +
    • + If you contribute integrations to Third-Party Services, you are responsible for all content you contribute, + in any manner, and you must have all rights necessary to do so, in the manner in which you contribute it. You + are responsible for all your activity in connection with any such Third-Party Service.{' '} +
    • +
    • + Your interactions with persons found on or through the Third-Party Service, including payment and delivery + of goods and services, financial transactions, and any other terms associated with such dealings, are solely + between you and such persons. You agree that we shall not be responsible or liable for any loss or damage of any + sort incurred as the result of any such dealings. +
    • +
    • + If there is a dispute between you and the Third-Party Service provider or/and other users of the + Third-Party Service, you agree that we are under no obligation to become involved. In the event that you + have a dispute with one or more other users, you release us, our officers, employees, agents, contractors and + successors from claims, demands, and damages of every kind or nature, known or unknown, suspected or + unsuspected, disclosed or undisclosed, arising out of or in any way related to such disputes and/or our + Services. +
    • +
    +

    6. What are the fees for the Services?

    +
      +
    1. + The use of the Safe App or Third-Party Safe Apps may cause fees, including network fees, as indicated in + the respective app. CC has no control over the fees charged by the Third-Party Services. CC may change its own + fees at any time. Price changes will be communicated to the User in due time before taking effect. +
    2. +
    3. + The User is only entitled to offset and/or assert rights of retention if his counterclaims are legally + established, undisputed or recognized by CC. +
    4. +
    +

    7. Are we responsible for the security of your Private Keys, Recovery Phrase or other credentials?

    +
      +
    1. + We shall not be responsible to secure your Private Keys, Recovery Phrase, credentials or other means of + authorization of your wallet(s). +
    2. +
    3. + You must own and control any wallet you use in connection with our Services. You are responsible for + implementing all appropriate measures for securing any wallet you use, including any Private Key(s), Recovery + Phrase, credentials or other means of authorization necessary to access such storage mechanism(s). +
    4. +
    5. + We exclude any and all liability for any security breaches or other acts or omissions, which result in your loss + of access or custody of any cryptographic assets stored thereon. +
    6. +
    +

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

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

    9. Can you terminate your Agreement with us?

    +

    You may terminate the Agreement at any time without notice.

    +

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

    +
      +
    1. + All intellectual property rights in Safe Accounts and the Services throughout the world belong to us as owner or + our licensors. Nothing in these Terms gives you any rights in respect of any intellectual property owned by us + or our licensors and you acknowledge that you do not acquire any ownership rights by downloading the Safe App or + any content from the Safe App. +
    2. +
    3. + If you are a consumer we grant you a simple, limited license, but do not sell, to you the Services you download + solely for your own personal, non-commercial use.{' '} +
    4. +
    +

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

    +
      +
    1. + Without limiting your mandatory warranties, we provide the Services to you “as is” and “as + available” in relation to merchantability, fitness for a particular purpose, availability, security, title + or non-infringement.{' '} +
    2. +
    3. + If you use the Safe App via web browser, the strict liability of CC for damages (sec. 536a German Civil Code) + for defects existing at the time of conclusion of the contract is precluded.{' '} +
    4. +
    5. The foregoing provisions will not limit CC’s liability as defined in Clause 13.
    6. +
    7. + We reserve the right to change the format and features of the Services by making any updates to Services + available for you to download or, where your device settings permit it, by automatic delivery of updates. +
    8. +
    9. + You are not obliged to download the updated Services, but we may cease to provide and/or update prior versions + of the Services and, depending on the nature of the update, in some circumstances you may not be able to + continue using the Services until you have downloaded the updated version. +
    10. +
    11. + We may cease to provide and/or update content to the Services, with or without notice to you, if it improves the + Services we provide to you, or we need to do so for security, legal or any other reasons. +
    12. +
    +

    12. What do you agree, warrant and represent?

    +

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

    +
      +
    1. + You are not a citizen, resident, or member of any jurisdiction or group that is subject to economic sanctions by + the European Union or the United States or any other relevant jurisdiction. +
    2. +
    3. + You do not appear on HMT Sanctions List, the U.S. Treasury Department’s Office of Foreign Asset + Control’s sanctions lists, the U.S. commerce department's consolidated screening list, the EU + consolidated list of persons, groups or entities subject to EU Financial Sanctions, nor do you act on behalf of + a person sanctioned thereunder. +
    4. +
    5. You have read and understood these Terms and agree to be bound by its terms.
    6. +
    7. + Your usage of our Services is legal under the laws of your jurisdiction or under the laws of any other + jurisdiction to which you may be subject. +
    8. +
    9. + You won’t use the Services or interact with the Services in a manner that violates any law or regulation, + including, without limitation, any applicable export control laws. +
    10. +
    11. + You understand the functionality, usage, storage, transmission mechanisms and intricacies associated with Tokens + as well as wallet (including Safe Account) and Blockchains. +
    12. +
    13. + You understand that Transactions on the Blockchain are irreversible and may not be erased and that your Safe + Account address and Transactions are displayed permanently and publicly. +
    14. +
    15. + You will comply with any applicable tax obligations in your jurisdiction arising from your use of the Services. +
    16. +
    17. + You will not misuse or gain unauthorized access to our Services by knowingly introducing viruses, cross-site + scripting, Trojan horses, worms, time-bombs, keystroke loggers, spyware, adware or any other harmful programs or + similar computer code designed to adversely affect our Services and that in the event you do so or otherwise + attack our Services, we reserve the right to report any such activity to the relevant law enforcement + authorities and we will cooperate with those authorities as required. +
    18. +
    19. + You won’t access without authority, interfere with, damage or disrupt any part of our Services, any + equipment or network on which our Services is stored, any software used in the provision of our Services or any + equipment or network or software owned or used by any third party. +
    20. +
    21. + You won’t use our Services for activities that are unlawful or fraudulent or have such purpose or effect + or otherwise support any activities that breach applicable local, national or international law or regulations. +
    22. +
    23. + You won’t use our Services to store, trade or transmit Tokens that are proceeds of criminal or fraudulent + activity. +
    24. +
    25. + You understand that the Services and the underlying Blockchain are in an early development stage and we + accordingly do not guarantee an error-free process and give no price or liquidity guarantee. +
    26. +
    27. You are using the Services at your own risk.
    28. +
    +

    13. What about our liability to you?

    +

    We are liable to you only as follows:

    +
      +
    1. We are liable for damages, in any case of negligence, resulting from injury to life, body or health.
    2. +
    3. + We are liable for damages – regardless of the legal grounds – in the event of intent and gross + negligence on our part, our legal representatives, our executive employees or other vicarious agents. +
    4. +
    5. + If we do not provide the Safe App or Services to you free of charge, we are liable in case of simple negligence + for damages resulting from the breach of an essential contractual duty (e.g. a duty, the performance of which + enables the proper execution of the contract in the first place and on the compliance of which the contractual + partner regularly relies and may rely), whereby in the latter case of breach of an essential contractual duty, + our liability shall be limited to compensation of the foreseeable, typically occurring damage. +
    6. +
    7. + The limitations of liability according to Clause 13.2 do not apply as far as we have assumed a guarantee or we + have fraudulently concealed a defect in the Services. These limitations of liability also do not apply to your + claims according to the Product Liability Act (”Produkthaftungsgesetz”) and any applicable data + privacy laws. +
    8. +
    9. + If you suffer damages from the loss of data, we are not liable for this, as far as the damages would have been + avoided by your regular and complete backup of all relevant data. +
    10. +
    11. + We take all possible measures to enable you to access our Services. In the event of disruptions to the technical + infrastructure, the internet connection or a relevant blockchain, we shall be exempt from our obligation to + perform. This also applies if we are prevented from performing due to force majeure or other circumstances, the + elimination of which is not possible or cannot be economically expected of CC. +
    12. +
    +

    14. What about viruses, bugs and security vulnerabilities?

    +
      +
    1. We endeavor to provide our Service free from material bugs, security vulnerabilities or viruses.
    2. +
    3. + You are responsible for configuring your information technology and computer programmes to access our Services + and to use your own virus protection software. +
    4. +
    5. If you become aware of any exploits, bugs or vulnerabilities, please inform bounty@safe.global.
    6. +
    7. + You must not misuse our Services by knowingly introducing material that is malicious or technologically harmful. + If you do, your right to use our Services will cease immediately. +
    8. +
    +

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

    +
      +
    1. + We may update and change our Services from time to time. We may suspend or withdraw or restrict the availability + of all or any part of our Services for business, operational or regulatory reasons or because of a Force Majeure + Event at no notice. +
    2. +
    3. + A “Force Majeure Event” shall mean any event, circumstance or cause beyond our reasonable control, + which prevents, hinders or delays the provision of our Services or makes their provision impossible or onerous, + including, without limitation: +
    4. +
    +
      +
    • acts of God, flood, storm, drought, earthquake or other natural disaster;
    • +
    • epidemic or pandemic (for the avoidance of doubt, including the 2020 Coronavirus Pandemic);
    • +
    • + terrorist attack, hacking or cyber threats, civil war, civil commotion or riots, war, threat of or preparation + for war, armed conflict, imposition of sanctions, embargo, or breaking off of diplomatic relations; +
    • +
    • + equipment or software malfunction or bugs including network splits or forks or unexpected changes in the + Blockchain, as well as hacks, phishing attacks, distributed denials of service or any other security attacks; +
    • +
    • nuclear, chemical or biological contamination;
    • +
    • + any law statutes, ordinances, rules, regulations, judgments, injunctions, orders and decrees or any action taken + by a government or public authority, including without limitation imposing a prohibition, or failing to grant a + necessary license or consent; +
    • +
    • collapse of buildings, breakdown of plant or machinery, fire, explosion or accident; and
    • +
    • strike, industrial action or lockout.
    • +
    +
      +
    1. + We shall not be liable or responsible to you, or be deemed to have defaulted under or breached this Agreement, + for any failure or delay in the provision of the Services or the performance of this Agreement, if and to the + extent such failure or delay is caused by or results from or is connected to acts beyond our reasonable control, + including the occurrence of a Force Majeure Event. +
    2. +
    +

    16. Who is responsible for your tax liabilities?

    +

    + You are solely responsible to determine if your use of the Services have tax implications, in particular income + tax and capital gains tax relating to the purchase or sale of Tokens, for you. By using the Services you agree not + to hold us liable for any tax liability associated with or arising from the operation of the Services or any other + action or transaction related thereto. +

    +

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

    +

    + Should individual provisions of these Terms be or become invalid or unenforceable in whole or in part, this shall + not affect the validity of the remaining provisions. The invalid or unenforceable provision shall be replaced by + the statutory provision. If there is no statutory provision or if the statutory provision would lead to an + unacceptable result, the parties shall enter negotiations to replace the invalid or unenforceable provision with a + valid provision that comes as close as possible to the economic purpose of the invalid or unenforceable provision. +

    +

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

    +

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

    +

    19. Do third parties have rights?

    +

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

    +

    20. Can this Agreement be assigned?

    +
      +
    1. + We are entitled to transfer our rights and obligations under the Agreement in whole or in part to third parties + with a notice period of four weeks. In this case, you have the right to terminate the Agreement without notice. +
    2. +
    3. + You shall not be entitled to assign this Agreement to any third party without our express prior written consent. +
    4. +
    +

    21. Which Clauses of this Agreement survive termination?

    +

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

    +

    22. Data Protection

    +

    + We inform you about our processing of personal data, including the disclosure to third parties and your rights as + an affected party, in the{' '} + + Privacy Policy + + . +

    +

    23. Which laws apply to the Agreement?

    +

    + The Agreement including these Terms shall be governed by German law. The application of the UN Convention on + Contracts for the International Sale of Goods is excluded. For consumers domiciled in another European country but + Germany, the mandatory provisions of the consumer protection laws of the member state in which the consumer is + domiciled shall also apply, provided that these are more advantageous for the consumer than the provisions of the + German law. +

    +

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

    +

    + If you want to learn more about Safe Accounts or the Service or have any problems using them or have any + complaints please get in touch via any of the following channels: +

    +
      +
    1. + Intercom:{' '} + + + {HELP_CENTER_URL} + + +
    2. +
    3. + Discord:{' '} + + + {DISCORD_URL} + + +
    4. +
    5. + Twitter:{' '} + + + {TWITTER_URL} + + +
    6. +
    +

    25. Where is the place of legal proceedings?

    +

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

    +

    26. Is this all?

    +

    + These Terms constitute the entire agreement between you and us in relation to the Agreement’s subject + matter. It replaces and extinguishes any and all prior agreements, draft agreements, arrangements, warranties, + statements, assurances, representations and undertakings of any nature made by, or on behalf of either of us, + whether oral or written, public or private, in relation to that subject matter. +

    +
    +) + +const Terms: NextPage = () => { return ( <> @@ -15,4 +579,4 @@ const Imprint: NextPage = () => { ) } -export default Imprint +export default Terms diff --git a/src/services/analytics/__tests__/gtm.test.ts b/src/services/analytics/__tests__/gtm.test.ts index 33eacac65c..5381ec244c 100644 --- a/src/services/analytics/__tests__/gtm.test.ts +++ b/src/services/analytics/__tests__/gtm.test.ts @@ -1,18 +1,131 @@ -import { normalizeAppName } from '../gtm' +import * as gtm from '../gtm' +import TagManager from '../TagManager' +import { EventType, DeviceType } from '../types' -const FAKE_SAFE_APP_NAME = 'Safe App' -const FAKE_DOMAIN = 'http://domain.crypto' +// Mock dependencies +jest.mock('../TagManager', () => ({ + initialize: jest.fn(), + dataLayer: jest.fn(), + enableCookies: jest.fn(), + disableCookies: jest.fn(), + setUserProperty: jest.fn(), +})) describe('gtm', () => { - describe('normalizeAppName', () => { - it('should return the app name if is not an URL', () => { - expect(normalizeAppName(FAKE_SAFE_APP_NAME)).toBe(FAKE_SAFE_APP_NAME) + // Reset mocks before each test + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('gtmTrack', () => { + it('should send correct data to the dataLayer', () => { + const mockEventData = { + event: EventType.CLICK, + category: 'testCategory', + action: 'testAction', + chainId: '1234', + label: 'testLabel', + } + + gtm.gtmTrack(mockEventData) + + expect(TagManager.dataLayer).toHaveBeenCalledWith( + expect.objectContaining({ + event: mockEventData.event, + eventCategory: mockEventData.category, + eventAction: mockEventData.action, + chainId: mockEventData.chainId, + eventLabel: mockEventData.label, + appVersion: expect.any(String), + deviceType: DeviceType.DESKTOP, + }), + ) + }) + + it('should set the chain ID correctly', () => { + const testChainId = '1234' + gtm.gtmSetChainId(testChainId) + + const mockEventData = { + event: EventType.CLICK, + category: 'testCategory', + action: 'testAction', + label: 'testLabel', + } + + gtm.gtmTrack(mockEventData) + + expect(TagManager.dataLayer).toHaveBeenCalledWith( + expect.objectContaining({ + event: mockEventData.event, + eventCategory: mockEventData.category, + eventAction: mockEventData.action, + chainId: testChainId, + eventLabel: mockEventData.label, + appVersion: expect.any(String), + deviceType: DeviceType.DESKTOP, + }), + ) }) + }) + + describe('gtmTrackSafeApp', () => { + it('should send correct data to the dataLayer for a Safe App event', () => { + Object.defineProperty(window, 'location', { + writable: true, + value: { + pathname: '/apps', + }, + }) + + const mockEventData = { + event: EventType.SAFE_APP, + category: 'testCategory', + action: 'testAction', + label: 'testLabel', + chainId: '1234', + } + + const mockAppName = 'Test App' + const mockSdkEventData = { + method: 'testMethod', + ethMethod: 'testEthMethod', + version: '1.0.0', + } + + gtm.gtmTrackSafeApp(mockEventData, mockAppName, mockSdkEventData) + + expect(TagManager.dataLayer).toHaveBeenCalledWith( + expect.objectContaining({ + appVersion: expect.any(String), + chainId: expect.any(String), + deviceType: DeviceType.DESKTOP, + event: EventType.SAFE_APP, + eventAction: 'testAction', + eventCategory: 'testCategory', + eventLabel: 'testLabel', + safeAddress: '', + safeAppEthMethod: '', + safeAppMethod: '', + safeAppName: 'Test App', + safeAppSDKVersion: '', + }), + ) + }) + + describe('normalizeAppName', () => { + const FAKE_SAFE_APP_NAME = 'Safe App' + const FAKE_DOMAIN = 'http://domain.crypto' + + it('should return the app name if is not an URL', () => { + expect(gtm.normalizeAppName(FAKE_SAFE_APP_NAME)).toBe(FAKE_SAFE_APP_NAME) + }) - it('should strip the querystring or hash when is an URL', () => { - expect(normalizeAppName(FAKE_DOMAIN)).toBe(FAKE_DOMAIN) - expect(normalizeAppName(`${FAKE_DOMAIN}?q1=query1&q2=query2`)).toBe(FAKE_DOMAIN) - expect(normalizeAppName(`${FAKE_DOMAIN}#hash`)).toBe(FAKE_DOMAIN) + it('should strip the querystring or hash when is an URL', () => { + expect(gtm.normalizeAppName(FAKE_DOMAIN)).toBe(FAKE_DOMAIN) + expect(gtm.normalizeAppName(`${FAKE_DOMAIN}?q1=query1&q2=query2`)).toBe(FAKE_DOMAIN) + expect(gtm.normalizeAppName(`${FAKE_DOMAIN}#hash`)).toBe(FAKE_DOMAIN) + }) }) }) }) diff --git a/src/services/analytics/events/modals.ts b/src/services/analytics/events/modals.ts index d13418b99c..6d81dd8139 100644 --- a/src/services/analytics/events/modals.ts +++ b/src/services/analytics/events/modals.ts @@ -31,8 +31,8 @@ export const MODALS_EVENTS = { action: 'Estimation', category: MODALS_CATEGORY, }, - EXECUTE_TX: { - action: 'Execute transaction', + TOGGLE_EXECUTE_TX: { + action: 'Toggle execute transaction', category: MODALS_CATEGORY, }, USE_SPENDING_LIMIT: { @@ -44,18 +44,10 @@ export const MODALS_EVENTS = { action: 'Simulate transaction', category: MODALS_CATEGORY, }, - REJECT_TX: { - action: 'Reject transaction', - category: MODALS_CATEGORY, - }, EDIT_APPROVALS: { action: 'Edit approval', category: MODALS_CATEGORY, }, - PROPOSE_TX: { - action: 'Propose transaction', - category: MODALS_CATEGORY, - }, ACCEPT_RISK: { action: 'Accept transaction risk', category: MODALS_CATEGORY, diff --git a/src/services/analytics/events/overview.ts b/src/services/analytics/events/overview.ts index 369dad15eb..9c8dc47202 100644 --- a/src/services/analytics/events/overview.ts +++ b/src/services/analytics/events/overview.ts @@ -96,4 +96,9 @@ export const OVERVIEW_EVENTS = { action: 'Click on SEP5 allocation button', category: OVERVIEW_CATEGORY, }, + SAFE_VIEWED: { + event: EventType.META, + action: 'Safe viewed', + category: OVERVIEW_CATEGORY, + }, } diff --git a/src/services/analytics/events/transactions.ts b/src/services/analytics/events/transactions.ts new file mode 100644 index 0000000000..4f3cf0e048 --- /dev/null +++ b/src/services/analytics/events/transactions.ts @@ -0,0 +1,45 @@ +import { EventType } from '../types' + +export enum TX_TYPES { + // Owner txs + owner_add = 'owner_add', + owner_remove = 'owner_remove', + owner_swap = 'owner_swap', + owner_threshold_change = 'owner_threshold_change', + + // Module txs + guard_remove = 'guard_remove', + module_remove = 'module_remove', + spending_limit_remove = 'spending_limit_remove', + spending_limit_add = 'spending_limit_add', + + // Safe txs + safe_update = 'safe_update', + + // Transfers + transfer_token = 'transfer_token', + transfer_nft = 'transfer_nft', + + // Other + batch = 'batch', + rejection = 'rejection', + typed_message = 'typed_message', + safeapps = 'safeapps', + walletconnect = 'walletconnect', +} + +const TX_CATEGORY = 'transactions' + +export const TX_EVENTS = { + CREATE: { + event: EventType.META, + action: 'Create transaction', + category: TX_CATEGORY, + // label: TX_TYPES, + }, + CONFIRM: { + event: EventType.META, + action: 'Confirm transaction', + category: TX_CATEGORY, + }, +} diff --git a/src/services/analytics/gtm.ts b/src/services/analytics/gtm.ts index 6f13652e7f..2a87cbc003 100644 --- a/src/services/analytics/gtm.ts +++ b/src/services/analytics/gtm.ts @@ -22,6 +22,7 @@ import { SAFE_APPS_SDK_CATEGORY } from './events' import { getAbTest } from '../tracking/abTesting' import type { AbTest } from '../tracking/abTesting' import { AppRoutes } from '@/config/routes' +import packageJson from '../../../package.json' type GTMEnvironment = 'LIVE' | 'LATEST' | 'DEVELOPMENT' type GTMEnvironmentArgs = Required> @@ -42,6 +43,7 @@ const GTM_ENV_AUTH: Record = { } const commonEventParams = { + appVersion: packageJson.version, chainId: '', deviceType: DeviceType.DESKTOP, safeAddress: '', diff --git a/src/services/analytics/spindl.ts b/src/services/analytics/spindl.ts new file mode 100644 index 0000000000..1f5e45d00f --- /dev/null +++ b/src/services/analytics/spindl.ts @@ -0,0 +1,15 @@ +import spindl from '@spindl-xyz/attribution-lite' +import { IS_PRODUCTION } from '@/config/constants' + +export const spindlInit = () => { + const SPINDL_SDK_KEY = process.env.NEXT_PUBLIC_SPINDL_SDK_KEY + + spindl.configure({ + sdkKey: SPINDL_SDK_KEY || '', + debugMode: !IS_PRODUCTION, + }) + + spindl.enableAutoPageViews() +} + +export default spindl diff --git a/src/services/analytics/types.ts b/src/services/analytics/types.ts index 47fb97dbb6..07c3c4a59b 100644 --- a/src/services/analytics/types.ts +++ b/src/services/analytics/types.ts @@ -32,4 +32,5 @@ export enum DeviceType { export enum AnalyticsUserProperties { WALLET_LABEL = 'walletLabel', + WALLET_ADDRESS = 'walletAddress', } diff --git a/src/services/analytics/useGtm.ts b/src/services/analytics/useGtm.ts index f2fc484de3..8c461b293a 100644 --- a/src/services/analytics/useGtm.ts +++ b/src/services/analytics/useGtm.ts @@ -1,7 +1,5 @@ /** - * This hook is used to initialize GTM and for anonymized page view tracking. - * It won't initialize GTM if a consent wasn't given for analytics cookies. - * The hook needs to be called when the app starts. + * Track analytics events using Google Tag Manager */ import { useEffect, useState } from 'react' import { useTheme } from '@mui/material/styles' @@ -14,7 +12,9 @@ import { gtmSetDeviceType, gtmSetSafeAddress, gtmSetUserProperty, + gtmTrack, } from '@/services/analytics/gtm' +import spindl, { spindlInit } from './spindl' import { useAppSelector } from '@/store' import { CookieType, selectCookies } from '@/store/cookiesSlice' import useChainId from '@/hooks/useChainId' @@ -25,6 +25,7 @@ import { useMediaQuery } from '@mui/material' import { AnalyticsUserProperties, DeviceType } from './types' import useSafeAddress from '@/hooks/useSafeAddress' import useWallet from '@/hooks/wallets/useWallet' +import { OVERVIEW_EVENTS } from './events' const useGtm = () => { const chainId = useChainId() @@ -37,11 +38,12 @@ const useGtm = () => { const isTablet = useMediaQuery(theme.breakpoints.down('md')) const deviceType = isMobile ? DeviceType.MOBILE : isTablet ? DeviceType.TABLET : DeviceType.DESKTOP const safeAddress = useSafeAddress() - const walletLabel = useWallet()?.label + const wallet = useWallet() - // Initialize GTM + // Initialize GTM and Spindl useEffect(() => { gtmInit() + spindlInit() }, []) // Enable GA cookies if consent was given @@ -72,6 +74,10 @@ const useGtm = () => { // Set safe address for all GTM events useEffect(() => { gtmSetSafeAddress(safeAddress) + + if (safeAddress) { + gtmTrack(OVERVIEW_EVENTS.SAFE_VIEWED) + } }, [safeAddress]) // Track page views – anonymized by default. @@ -83,10 +89,17 @@ const useGtm = () => { }, [router.pathname]) useEffect(() => { - if (walletLabel) { - gtmSetUserProperty(AnalyticsUserProperties.WALLET_LABEL, walletLabel) + if (wallet?.label) { + gtmSetUserProperty(AnalyticsUserProperties.WALLET_LABEL, wallet.label) + } + }, [wallet?.label]) + + useEffect(() => { + if (wallet?.address) { + gtmSetUserProperty(AnalyticsUserProperties.WALLET_ADDRESS, wallet.address) + spindl.attribute(wallet.address) } - }, [walletLabel]) + }, [wallet?.address]) // Track meta events on app load useMetaEvents() diff --git a/src/services/exceptions/ErrorCodes.ts b/src/services/exceptions/ErrorCodes.ts index 7ce2b41436..d5fdab60ca 100644 --- a/src/services/exceptions/ErrorCodes.ts +++ b/src/services/exceptions/ErrorCodes.ts @@ -15,7 +15,6 @@ enum ErrorCodes { _106 = '106: Failed to get connected wallet', _302 = '302: Error connecting to the wallet', - _303 = '303: Error creating pairing session', _304 = '304: Error enabling MFA', _305 = '305: Error exporting account key', _306 = '306: Error logging in', diff --git a/src/services/mpc/SocialLoginModule.ts b/src/services/mpc/SocialLoginModule.ts index 6e34ce89d8..84052dfbed 100644 --- a/src/services/mpc/SocialLoginModule.ts +++ b/src/services/mpc/SocialLoginModule.ts @@ -1,14 +1,9 @@ -import { _getMPCCoreKitInstance } from '@/hooks/wallets/mpc/useMPC' -import { getSocialWalletService } from '@/hooks/wallets/mpc/useSocialWallet' import { getWeb3ReadOnly } from '@/hooks/wallets/web3' -import * as PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal' import { FEATURES, hasFeature } from '@/utils/chains' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import { type WalletInit, ProviderRpcError } from '@web3-onboard/common' import { type EIP1193Provider } from '@web3-onboard/core' -import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' - -const getMPCProvider = () => _getMPCCoreKitInstance()?.provider +import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' const assertDefined = (mpcProvider: T | undefined) => { if (!mpcProvider) { @@ -23,9 +18,9 @@ export const isSocialLoginWallet = (walletLabel: string | undefined) => { return walletLabel === ONBOARD_MPC_MODULE_LABEL } -const getConnectedAccounts = async () => { +const getConnectedAccounts = async (provider: typeof Web3AuthMPCCoreKit.prototype.provider | undefined) => { try { - const web3 = assertDefined(getMPCProvider()) + const web3 = assertDefined(provider) return web3.request({ method: 'eth_accounts' }) } catch (e) { throw new ProviderRpcError({ @@ -50,6 +45,13 @@ function MpcModule(chain: ChainInfo): WalletInit { label: ONBOARD_MPC_MODULE_LABEL, getIcon: async () => (await import('./icon')).default, getInterface: async () => { + const { _getMPCCoreKitInstance } = await import('@/hooks/wallets/mpc/useMPC') + const { getSocialWalletService } = await import('@/hooks/wallets/mpc/useSocialWallet') + const { COREKIT_STATUS } = await import('@web3auth/mpc-core-kit') + const { open } = await import('./PasswordRecoveryModal') + + const getMPCProvider = () => _getMPCCoreKitInstance()?.provider + const provider: EIP1193Provider = { on: (event, listener) => { const web3 = assertDefined(getMPCProvider()) @@ -85,11 +87,11 @@ function MpcModule(chain: ChainInfo): WalletInit { const status = await socialWalletService.loginAndCreate() if (status === COREKIT_STATUS.REQUIRED_SHARE) { - PasswordRecoveryModal.open(() => { - getConnectedAccounts().then(resolve).catch(reject) + open(() => { + getConnectedAccounts(getMPCProvider()).then(resolve).catch(reject) }) } else { - getConnectedAccounts().then(resolve).catch(reject) + getConnectedAccounts(getMPCProvider()).then(resolve).catch(reject) } } return diff --git a/src/services/pairing/QRModal.tsx b/src/services/pairing/QRModal.tsx deleted file mode 100644 index 47b2e18b14..0000000000 --- a/src/services/pairing/QRModal.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Dialog, DialogContent, DialogTitle, IconButton } from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' -import PairingQRCode from '@/components/common/PairingDetails/PairingQRCode' -import PairingDescription from '@/components/common/PairingDetails/PairingDescription' -import { PAIRING_MODULE_LABEL } from '@/services/pairing/module' -import PairingDeprecationWarning from '@/components/common/PairingDetails/PairingDeprecationWarning' -import ExternalStore from '@/services/ExternalStore' - -const { useStore: useCloseCallback, setStore: setCloseCallback } = new ExternalStore<() => void>() - -export const open = (cb: () => void) => { - setCloseCallback(() => cb) -} - -export const close = () => { - setCloseCallback(undefined) -} - -const QRModal = () => { - const closeCallback = useCloseCallback() - const open = !!closeCallback - - const handleClose = () => { - closeCallback?.() - setCloseCallback(undefined) - close() - } - - if (!open) return null - - return ( - - - {PAIRING_MODULE_LABEL} - - - - - - - -
    - -
    -
    - ) -} - -export default QRModal diff --git a/src/services/pairing/__tests__/utils.test.ts b/src/services/pairing/__tests__/utils.test.ts deleted file mode 100644 index 7551a55ca8..0000000000 --- a/src/services/pairing/__tests__/utils.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { addDays } from 'date-fns' -// @ts-expect-error - with native WalletConnect v2, the type is no longer present -import type { IWalletConnectSession } from '@walletconnect/types' - -import { formatPairingUri, isPairingSupported, hasValidPairingSession, _isPairingSessionExpired } from '../utils' - -describe('Pairing utils', () => { - describe('formatPairingUri', () => { - it('should return a prefixed URI', () => { - const uri = 'wc:1-2@1?bridge=https://test.com/&key=1234' - - const result = formatPairingUri(uri) - expect(result).toBe('safe-wc:1-2@1?bridge=https://test.com/&key=1234') - }) - - it('should return undefined if no URI exists', () => { - const result = formatPairingUri('') - expect(result).toBeUndefined() - }) - - it("should return undefined if the URI doesn't end with a key", () => { - const uri = 'wc:1-2@1?bridge=https://test.com/&key=' - - const result = formatPairingUri(uri) - expect(result).toBeUndefined() - }) - }) - - describe('isPairingSupported', () => { - it('should return true if the wallet is enabled', () => { - const disabledWallets = ['walletConnect'] - const result = isPairingSupported(disabledWallets) - expect(result).toBe(true) - }) - - it('should return false if the wallet is disabled', () => { - const disabledWallets1: string[] = [] - const result1 = isPairingSupported(disabledWallets1) - expect(result1).toBe(true) - - const disabledWallets2 = ['safeMobile'] - const result2 = isPairingSupported(disabledWallets2) - expect(result2).toBe(false) - }) - }) - - describe('isPairingSessionExpired', () => { - it('should return true if the session is older than 24h', () => { - const session: Pick = { - handshakeId: 1000000000000123, - } - - expect(_isPairingSessionExpired(session as IWalletConnectSession)).toBe(true) - }) - - it('should return false if the session is within the last 24h', () => { - const session: Pick = { - handshakeId: +`${Date.now()}123`, - } - - expect(_isPairingSessionExpired(session as IWalletConnectSession)).toBe(false) - }) - }) - - describe('hasValidPairingSession', () => { - beforeEach(() => { - window.localStorage.clear() - }) - - it('should return false if there is no cached session', () => { - expect(hasValidPairingSession()).toBe(false) - }) - - it('should return true if the cached session date is within the last 24h', () => { - const session: Pick = { - handshakeId: 1000000000000123, - } - - window.localStorage.setItem('SAFE_v2__pairingConnector', JSON.stringify(session)) - - jest.spyOn(Date, 'now').mockImplementation(() => +session.handshakeId.toString().slice(0, -3) + 1) - - expect(hasValidPairingSession()).toBe(true) - }) - - it('should return false and clear the cache if the cached session date is older than 24h', () => { - const session: Pick = { - handshakeId: 1000000000000123, - } - - window.localStorage.setItem('SAFE_v2__pairingConnector', JSON.stringify(session)) - - const sessionTimestamp = session.handshakeId.toString().slice(0, -3) - const expirationDate = addDays(new Date(+sessionTimestamp), 1) - - jest.spyOn(Date, 'now').mockImplementation(() => expirationDate.getTime() + 1) - - expect(hasValidPairingSession()).toBe(false) - - expect(window.localStorage.getItem('SAFE_v2__pairingConnector')).toBeNull() - }) - }) -}) diff --git a/src/services/pairing/connector.ts b/src/services/pairing/connector.ts deleted file mode 100644 index c60436f48a..0000000000 --- a/src/services/pairing/connector.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type WalletConnect from '@walletconnect/client' -import bowser from 'bowser' - -import packageJson from '../../../package.json' -import { IS_PRODUCTION } from '@/config/constants' -import ExternalStore from '@/services/ExternalStore' -import PairingIcon from '@/public/images/safe-logo-green.png' - -export const PAIRING_MODULE_STORAGE_ID = 'pairingConnector' - -export const getClientMeta = () => { - const host = location.origin - - const APP_META = { - name: `Safe{Wallet} web v${packageJson.version}`, - url: host, - icons: [`${host}${PairingIcon.src}`], - } - - if (typeof window === 'undefined') { - return { - description: APP_META.name, - ...APP_META, - } - } - - const parsed = bowser.getParser(window.navigator.userAgent) - const os = parsed.getOS() - const browser = parsed.getBrowser() - - return { - description: `${browser.name} ${browser.version} (${os.name});${APP_META.name}`, - ...APP_META, - } -} - -export const { - getStore: getPairingConnector, - setStore: setPairingConnector, - useStore: usePairingConnector, -} = new ExternalStore() - -export enum WalletConnectEvents { - CONNECT = 'connect', - DISPLAY_URI = 'display_uri', - DISCONNECT = 'disconnect', - CALL_REQUEST = 'call_request', - SESSION_REQUEST = 'session_request', - SESSION_UPDATE = 'session_update', - WC_SESSION_REQUEST = 'wc_sessionRequest', - WC_SESSION_UPDATE = 'wc_sessionUpdate', -} - -if (!IS_PRODUCTION) { - Object.values(WalletConnectEvents).forEach((event) => { - getPairingConnector()?.on(event, (...args) => console.info('[Pairing]', event, ...args)) - }) -} diff --git a/src/services/pairing/hooks.ts b/src/services/pairing/hooks.ts deleted file mode 100644 index 07292eb0f8..0000000000 --- a/src/services/pairing/hooks.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' - -import { useCurrentChain } from '@/hooks/useChains' -import useOnboard, { connectWallet, getConnectedWallet } from '@/hooks/wallets/useOnboard' -import { logError, Errors } from '@/services/exceptions' -import { - getClientMeta, - PAIRING_MODULE_STORAGE_ID, - setPairingConnector, - usePairingConnector, - WalletConnectEvents, -} from '@/services/pairing/connector' -import { PAIRING_MODULE_LABEL } from '@/services/pairing/module' -import { formatPairingUri, isPairingSupported, killPairingSession } from '@/services/pairing/utils' -import WalletConnect from '@walletconnect/client' -import { WC_BRIDGE } from '@/config/constants' -import local from '@/services/local-storage/local' - -/** - * `useInitPairing` is responsible for WC session management, creating a session when: - * - * - no wallet is connected to onboard, deemed "initializing" pairing (disconnecting wallets via the UI) - * - on WC 'disconnect' event (disconnecting via the app) - */ - -// First session will be created by onboard's state subscription, only -// when there is no connected wallet -let hasInitialized = false - -// WC has no flag to determine if a session is currently being created -let isConnecting = false - -export const useInitPairing = () => { - const onboard = useOnboard() - const chain = useCurrentChain() - const connector = usePairingConnector() - - const canConnect = !connector?.connected && !isConnecting - const isSupported = isPairingSupported(chain?.disabledWallets) - - useEffect(() => { - const _pairingConnector = new WalletConnect({ - bridge: WC_BRIDGE, - storageId: local.getPrefixedKey(PAIRING_MODULE_STORAGE_ID), - clientMeta: getClientMeta(), - }) - - setPairingConnector(_pairingConnector) - }, []) - - const createSession = useCallback(() => { - if (!canConnect || !chain || !isSupported || !onboard) { - return - } - - isConnecting = true - connector - ?.createSession({ chainId: +chain.chainId }) - .then(() => { - isConnecting = false - }) - .catch((e) => logError(Errors._303, e)) - }, [canConnect, chain, isSupported, onboard, connector]) - - useEffect(() => { - if (!onboard || !isSupported) { - return - } - - // Upon successful WC connection, connect it to onboard - connector?.on(WalletConnectEvents.CONNECT, () => { - connectWallet(onboard, { - autoSelect: { - label: PAIRING_MODULE_LABEL, - disableModals: true, - }, - }) - }) - - connector?.on(WalletConnectEvents.DISCONNECT, () => { - createSession() - }) - - // Create new session when no wallet is connected to onboard - const subscription = onboard.state.select('wallets').subscribe((wallets) => { - if (!getConnectedWallet(wallets) && !hasInitialized) { - createSession() - hasInitialized = true - } - }) - - return () => { - subscription.unsubscribe() - } - }, [onboard, createSession, isSupported, connector]) - - /** - * It's not possible to update the `chainId` of the current WC session - * We therefore kill the current session when switching chain to trigger - * a new `createSession` above - */ - useEffect(() => { - // We need to wait for chains to have been fetched before killing the session - if (!chain) { - return - } - - const isConnected = +chain.chainId === connector?.chainId - const shouldKillSession = !isSupported || (!isConnected && hasInitialized && canConnect) - - if (!shouldKillSession || !connector) { - return - } - - killPairingSession(connector) - }, [chain, isSupported, canConnect, connector]) -} - -/** - * `usePairingUri` is responsible for returning to pairing URI - * @returns uri - "safe-" prefixed WC connection URI - */ -const usePairingUri = () => { - const connector = usePairingConnector() - const [uri, setUri] = useState(connector ? formatPairingUri(connector.uri) : undefined) - - useEffect(() => { - connector?.on(WalletConnectEvents.DISPLAY_URI, (_, { params }) => { - setUri(formatPairingUri(params[0])) - }) - - // Prevent the `connector` from setting state when not mounted. - // Note: `off` clears _all_ listeners associated with that event - return () => { - connector?.off(WalletConnectEvents.DISPLAY_URI) - } - }, [connector]) - - return uri -} - -export default usePairingUri diff --git a/src/services/pairing/icon.ts b/src/services/pairing/icon.ts deleted file mode 100644 index b7ddc80874..0000000000 --- a/src/services/pairing/icon.ts +++ /dev/null @@ -1,8 +0,0 @@ -const pairingIcon = ` - - - - -` - -export default pairingIcon diff --git a/src/services/pairing/module.ts b/src/services/pairing/module.ts deleted file mode 100644 index add033b65c..0000000000 --- a/src/services/pairing/module.ts +++ /dev/null @@ -1,257 +0,0 @@ -import type { Chain, ProviderAccounts, WalletInit, EIP1193Provider } from '@web3-onboard/common' -// @ts-expect-error - with native WalletConnect v2, the type is no longer present -import type { ITxData } from '@walletconnect/types' - -import { getPairingConnector, PAIRING_MODULE_STORAGE_ID } from '@/services/pairing/connector' -import local from '@/services/local-storage/local' -import { killPairingSession } from '@/services/pairing/utils' -import * as QRModal from '@/services/pairing/QRModal' - -enum ProviderEvents { - ACCOUNTS_CHANGED = 'accountsChanged', - CHAIN_CHANGED = 'chainChanged', - DISCONNECT = 'disconnect', - CONNECT = 'connect', - WC_SESSION_UPDATE = 'session_update', -} - -enum ProviderMethods { - PERSONAL_SIGN = 'personal_sign', - ETH_CHAIN_ID = 'eth_chainId', - ETH_REQUEST_ACCOUNTS = 'eth_requestAccounts', - ETH_SELECT_ACCOUNTS = 'eth_selectAccounts', - ETH_SEND_TRANSACTION = 'eth_sendTransaction', - ETH_SIGN_TRANSACTION = 'eth_signTransaction', - ETH_SIGN = 'eth_sign', - ETH_SIGN_TYPED_DATA = 'eth_signTypedData', - ETH_SIGN_TYPED_DATA_V3 = 'eth_signTypedData_v3', - ETH_SIGN_TYPED_DATA_V4 = 'eth_signTypedData_v4', - ETH_ACCOUNTS = 'eth_accounts', - WALLET_SWITCH_ETHEREUM_CHAIN = 'wallet_switchEthereumChain', -} - -export const PAIRING_MODULE_LABEL = 'Safe{Wallet}' - -// Modified version of: https://github.com/blocknative/web3-onboard/blob/v2-web3-onboard-develop/packages/walletconnect/src/index.ts -const pairingModule = (): WalletInit => { - return () => { - return { - label: PAIRING_MODULE_LABEL, - getIcon: async () => (await import('./icon')).default, - getInterface: async ({ chains, EventEmitter }) => { - const { StaticJsonRpcProvider } = await import('@ethersproject/providers') - - const { ProviderRpcError, ProviderRpcErrorCode } = await import('@web3-onboard/common') - - const { default: WalletConnect } = await import('@walletconnect/client') - - const { Subject, fromEvent } = await import('rxjs') - const { takeUntil, take } = await import('rxjs/operators') - - const emitter = new EventEmitter() - - class EthProvider { - public request: EIP1193Provider['request'] - public connector: InstanceType - public chains: Chain[] - public disconnect: EIP1193Provider['disconnect'] - // @ts-expect-error - 'emit' does not exist on `typeof EventEmitter` - public emit: EventEmitter['emit'] - // @ts-expect-error - 'on' does not exist on `typeof EventEmitter` - public on: EventEmitter['on'] - // @ts-expect-error - 'removeListener' does not exist on `typeof EventEmitter` - public removeListener: EventEmitter['removeListener'] - - private disconnected$: InstanceType - private providers: Record> - - constructor({ connector, chains }: { connector: InstanceType; chains: Chain[] }) { - this.emit = emitter.emit.bind(emitter) - this.on = emitter.on.bind(emitter) - this.removeListener = emitter.removeListener.bind(emitter) - - this.connector = connector - this.chains = chains - this.disconnected$ = new Subject() - this.providers = {} - - // @ts-expect-error - `payload` type (`ISessionStatus`) is not correctly `pipe`ed - fromEvent(this.connector, ProviderEvents.WC_SESSION_UPDATE, (error, payload) => { - if (error) { - throw error - } - - return payload - }) - .pipe(takeUntil(this.disconnected$)) - .subscribe({ - next: ({ params }) => { - const [{ accounts, chainId }] = params - - this.emit(ProviderEvents.ACCOUNTS_CHANGED, accounts) - this.emit(ProviderEvents.CHAIN_CHANGED, `0x${chainId.toString(16)}`) - }, - error: console.warn, - }) - - // @ts-expect-error - `this.connector` does not satisfy the event target type - fromEvent(this.connector, ProviderEvents.DISCONNECT, (error, payload) => { - if (error) { - throw error - } - - return payload - }) - .pipe(takeUntil(this.disconnected$)) - .subscribe({ - next: () => { - this.emit(ProviderEvents.ACCOUNTS_CHANGED, []) - - this.disconnected$.next(true) - - local.removeItem(PAIRING_MODULE_STORAGE_ID) - }, - error: console.warn, - }) - - fromEvent(window, 'unload').subscribe(() => { - this.disconnect?.() - }) - - this.disconnect = () => killPairingSession(this.connector) - - this.request = async ({ method, params }) => { - switch (method) { - case ProviderMethods.ETH_CHAIN_ID: { - return `0x${this.connector.chainId.toString(16)}` - } - - case ProviderMethods.ETH_REQUEST_ACCOUNTS: { - return new Promise((resolve, reject) => { - if (!this.connector.connected) { - this.connector.createSession().then(() => { - QRModal.open(() => - reject( - new ProviderRpcError({ - code: 4001, - message: 'User rejected the request.', - }), - ), - ) - }) - } else { - const { accounts, chainId } = this.connector.session - - this.emit(ProviderEvents.CHAIN_CHANGED, `0x${chainId.toString(16)}`) - - return resolve(accounts) - } - - // @ts-ignore - fromEvent(this.connector, ProviderEvents.CONNECT, (error, payload) => { - if (error) { - throw error - } - - return payload - }) - .pipe(take(1)) - .subscribe({ - next: ({ params }) => { - const [{ accounts, chainId }] = params - - this.emit(ProviderEvents.ACCOUNTS_CHANGED, accounts) - this.emit(ProviderEvents.CHAIN_CHANGED, `0x${chainId.toString(16)}`) - - QRModal.close() - - resolve(accounts) - }, - error: reject, - }) - }) - } - - case ProviderMethods.ETH_SEND_TRANSACTION: { - const txData = params![0] as ITxData - - return this.connector.sendTransaction({ - ...txData, - // Mobile app expects `value` - value: txData.value || '0x0', - }) - } - - case ProviderMethods.ETH_SIGN_TRANSACTION: { - return this.connector.signTransaction(params![0] as ITxData) - } - - // Mobile app only supports `eth_sign` but emits `personal_sign` event - case ProviderMethods.PERSONAL_SIGN: { - const [safeTxHash, sender] = params as [string, string] - return this.connector.signMessage([sender, safeTxHash]) // `eth_sign` - } - - case ProviderMethods.ETH_ACCOUNTS: { - return this.connector.sendCustomRequest({ - id: 1337, - jsonrpc: '2.0', - method, - params, - }) - } - - // Not supported by mobile app - case ProviderMethods.ETH_SIGN: - case ProviderMethods.ETH_SIGN_TYPED_DATA: - case ProviderMethods.ETH_SIGN_TYPED_DATA_V3: - case ProviderMethods.ETH_SIGN_TYPED_DATA_V4: - // Not supported by WC - case ProviderMethods.ETH_SELECT_ACCOUNTS: { - throw new ProviderRpcError({ - code: ProviderRpcErrorCode.UNSUPPORTED_METHOD, - message: `Safe{Wallet} mobile does not support the requested method: ${method}`, - }) - } - - // Switch wallet chain - case ProviderMethods.WALLET_SWITCH_ETHEREUM_CHAIN: { - this.connector.updateSession({ - chainId: parseInt((params as [{ chainId: string }])[0].chainId), - accounts: this.connector.accounts, - }) - return - } - - default: { - const chainId = await this.request({ method: ProviderMethods.ETH_CHAIN_ID }) - - if (!this.providers[chainId]) { - const currentChain = chains.find(({ id }) => id === chainId) - - if (!currentChain) { - throw new ProviderRpcError({ - code: ProviderRpcErrorCode.CHAIN_NOT_ADDED, - message: `The Provider does not have an RPC to request the method: ${method}`, - }) - } - - this.providers[chainId] = new StaticJsonRpcProvider(currentChain.rpcUrl) - } - - return this.providers[chainId].send(method, params!) - } - } - } - } - } - - return { - provider: new EthProvider({ chains, connector: getPairingConnector()! }), - } - }, - } - } -} - -export default pairingModule diff --git a/src/services/pairing/utils.ts b/src/services/pairing/utils.ts deleted file mode 100644 index 4423a2cfa5..0000000000 --- a/src/services/pairing/utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { addDays, isAfter } from 'date-fns' -// @ts-expect-error - with native WalletConnect v2, the type is no longer present -import type { IWalletConnectSession } from '@walletconnect/types' -import type WalletConnect from '@walletconnect/client' - -import { CGW_NAMES, WALLET_KEYS } from '@/hooks/wallets/consts' -import local from '@/services/local-storage/local' -import { PAIRING_MODULE_STORAGE_ID } from '@/services/pairing/connector' - -export const formatPairingUri = (wcUri: string) => { - const PAIRING_MODULE_URI_PREFIX = 'safe-' - - // A disconnected session returns URI with an empty `key` - if (!wcUri || !/key=.+/.test(wcUri)) { - return - } - - return `${PAIRING_MODULE_URI_PREFIX}${wcUri}` -} - -export const killPairingSession = (connector: InstanceType) => { - const TEMP_PEER_ID = '_tempPeerId' - - // WalletConnect throws if no `peerId` is set when attempting to `killSession` - // We therefore manually set it in order to `killSession` without throwing - if (!connector.peerId) { - connector.peerId = TEMP_PEER_ID - } - - return connector.killSession() -} - -export const isPairingSupported = (disabledWallets?: string[]) => { - return disabledWallets && !disabledWallets.includes(CGW_NAMES[WALLET_KEYS.PAIRING] as string) -} - -export const _isPairingSessionExpired = (session: IWalletConnectSession): boolean => { - // WC appends 3 digits to the timestamp. NOTE: This may change in WC v2 - const sessionTimestamp = session.handshakeId.toString().slice(0, -3) - // The session is valid for 24h (mobile clears it on their end) - const expirationDate = addDays(new Date(+sessionTimestamp), 1) - - return isAfter(Date.now(), expirationDate) -} - -export const hasValidPairingSession = (): boolean => { - const cachedSession = local.getItem(PAIRING_MODULE_STORAGE_ID) - - if (!cachedSession) { - return false - } - - const isExpired = _isPairingSessionExpired(cachedSession) - - if (isExpired) { - local.removeItem(PAIRING_MODULE_STORAGE_ID) - } - - return !isExpired -} diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx index 4d4ea0ff51..57bc87b834 100644 --- a/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx +++ b/src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx @@ -271,6 +271,7 @@ describe('useSafeWalletProvider', () => { ], params: { safeTxGas: 0 }, }, + onSubmit: expect.any(Function), }) expect(resp).toBeInstanceOf(Promise) diff --git a/src/services/safe-wallet-provider/useSafeWalletProvider.tsx b/src/services/safe-wallet-provider/useSafeWalletProvider.tsx index ae04d26073..289f75a62e 100644 --- a/src/services/safe-wallet-provider/useSafeWalletProvider.tsx +++ b/src/services/safe-wallet-provider/useSafeWalletProvider.tsx @@ -131,23 +131,12 @@ export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK | code: RpcErrorCode.USER_REJECTED, message: 'User rejected transaction', }) - unsubscribe() } - const unsubscribeSignaturePrepared = txSubscribe( - TxEvent.SAFE_APPS_REQUEST, - async ({ safeAppRequestId, safeTxHash, txId }) => { - if (safeAppRequestId === id) { - const txHash = txId ? pendingTxs.current[txId] : undefined - resolve({ safeTxHash, txHash }) - unsubscribe() - } - }, - ) - - const unsubscribe = () => { + const onSubmit = (txId: string, safeTxHash: string) => { + const txHash = pendingTxs.current[txId] onClose = () => {} - unsubscribeSignaturePrepared() + resolve({ safeTxHash, txHash }) } setTxFlow( @@ -159,6 +148,7 @@ export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK | txs: transactions, params: params.params, }} + onSubmit={onSubmit} />, onClose, ) diff --git a/src/services/tx/tx-sender/dispatch.ts b/src/services/tx/tx-sender/dispatch.ts index 0aaddfb471..c6a49e320e 100644 --- a/src/services/tx/tx-sender/dispatch.ts +++ b/src/services/tx/tx-sender/dispatch.ts @@ -319,10 +319,11 @@ export const dispatchSafeAppsTx = async ( onboard: OnboardAPI, chainId: SafeInfo['chainId'], txId?: string, -) => { +): Promise => { const sdk = await getSafeSDKWithSigner(onboard, chainId) const safeTxHash = await sdk.getTransactionHash(safeTx) txDispatch(TxEvent.SAFE_APPS_REQUEST, { safeAppRequestId, safeTxHash, txId }) + return safeTxHash } export const dispatchTxRelay = async ( diff --git a/src/services/tx/tx-sender/recommendedNonce.ts b/src/services/tx/tx-sender/recommendedNonce.ts index 9874a5f5b8..164d7179b2 100644 --- a/src/services/tx/tx-sender/recommendedNonce.ts +++ b/src/services/tx/tx-sender/recommendedNonce.ts @@ -1,9 +1,12 @@ -import type { SafeTransactionEstimation } from '@safe-global/safe-gateway-typescript-sdk' -import { Operation, postSafeGasEstimation } from '@safe-global/safe-gateway-typescript-sdk' +import { + Operation, + postSafeGasEstimation, + getNonces as fetchNonces, + type SafeTransactionEstimation, +} from '@safe-global/safe-gateway-typescript-sdk' import type { MetaTransactionData, SafeTransactionDataPartial } from '@safe-global/safe-core-sdk-types' import { isLegacyVersion } from '@/hooks/coreSDK/safeCoreSDK' import { Errors, logError } from '@/services/exceptions' -import { EMPTY_DATA } from '@safe-global/safe-core-sdk/dist/src/utils/constants' const fetchRecommendedParams = async ( chainId: string, @@ -37,11 +40,9 @@ export const getSafeTxGas = async ( } } -export const getRecommendedNonce = async (chainId: string, safeAddress: string): Promise => { - const blankTxParams = { data: EMPTY_DATA, to: safeAddress, value: '0' } +export const getNonces = async (chainId: string, safeAddress: string) => { try { - const estimation = await fetchRecommendedParams(chainId, safeAddress, blankTxParams) - return Number(estimation.recommendedNonce) + return fetchNonces(chainId, safeAddress) } catch (e) { logError(Errors._616, e) } diff --git a/src/services/walletconnect/WalletConnectWallet.ts b/src/services/walletconnect/WalletConnectWallet.ts index 88b28f45fc..b2f8b3079b 100644 --- a/src/services/walletconnect/WalletConnectWallet.ts +++ b/src/services/walletconnect/WalletConnectWallet.ts @@ -7,7 +7,7 @@ import type { SessionTypes } from '@walletconnect/types' import { type JsonRpcResponse } from '@walletconnect/jsonrpc-utils' import uniq from 'lodash/uniq' -import { IS_PRODUCTION, WC_PROJECT_ID } from '@/config/constants' +import { IS_PRODUCTION, LS_NAMESPACE, WC_PROJECT_ID } from '@/config/constants' import { EIP155, SAFE_COMPATIBLE_METHODS, SAFE_WALLET_METADATA } from './constants' import { invariant } from '@/utils/helpers' import { getEip155ChainId, stripEip155Prefix } from './utils' @@ -36,6 +36,7 @@ class WalletConnectWallet { const core = new Core({ projectId: WC_PROJECT_ID, logger: IS_PRODUCTION ? undefined : 'debug', + customStoragePrefix: LS_NAMESPACE, }) const web3wallet = await Web3Wallet.init({ diff --git a/src/services/walletconnect/constants.ts b/src/services/walletconnect/constants.ts index 2206a0069c..50656e9072 100644 --- a/src/services/walletconnect/constants.ts +++ b/src/services/walletconnect/constants.ts @@ -26,9 +26,9 @@ export const SAFE_COMPATIBLE_METHODS = [ export const SAFE_WALLET_METADATA = { name: 'Safe{Wallet}', - description: 'The most trusted platform to manage digital assets on Ethereum', url: 'https://app.safe.global', - icons: ['https://app.safe.global/favicons/mstile-150x150.png', 'https://app.safe.global/favicons/logo_120x120.png'], + description: 'Smart contract wallet for Ethereum', + icons: ['https://app.safe.global/images/logo-round.svg'], } export const EIP155 = 'eip155' as const diff --git a/src/services/walletconnect/utils.ts b/src/services/walletconnect/utils.ts index 047e1003ad..bd3f903646 100644 --- a/src/services/walletconnect/utils.ts +++ b/src/services/walletconnect/utils.ts @@ -62,3 +62,8 @@ export const getPeerName = (peer: SessionTypes.Struct['peer'] | ProposalTypes.St export const splitError = (message: string): string[] => { return message.split(/: (.+)/).slice(0, 2) } + +export const isWalletConnectSafeApp = (url: string): boolean => { + const WALLET_CONNECT = /wallet-connect/ + return WALLET_CONNECT.test(url) +} diff --git a/src/styles/globals.css b/src/styles/globals.css index f57bf7c902..d544bc6d04 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -35,7 +35,6 @@ button { :root { --header-height: 52px; --footer-height: 67px; - --import-export-widget-height: 166px; } input::-webkit-outer-spin-button, diff --git a/yarn.lock b/yarn.lock index 23f0036f7e..948706dc2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1114,13 +1114,6 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - "@cypress/request@2.88.12": version "2.88.12" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.12.tgz#ba4911431738494a85e93fb04498cb38bc55d590" @@ -1948,31 +1941,6 @@ dependencies: "@ethersproject/logger" "^5.7.0" -"@ethersproject/providers@5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.5.0.tgz#bc2876a8fe5e0053ed9828b1f3767ae46e43758b" - integrity sha512-xqMbDnS/FPy+J/9mBLKddzyLLAQFjrVff5g00efqxPzcAwXiR+SiCGVy6eJ5iAIirBOATjx7QLhDNPGV+AEQsw== - dependencies: - "@ethersproject/abstract-provider" "^5.5.0" - "@ethersproject/abstract-signer" "^5.5.0" - "@ethersproject/address" "^5.5.0" - "@ethersproject/basex" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/constants" "^5.5.0" - "@ethersproject/hash" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/networks" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/random" "^5.5.0" - "@ethersproject/rlp" "^5.5.0" - "@ethersproject/sha2" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - "@ethersproject/transactions" "^5.5.0" - "@ethersproject/web" "^5.5.0" - bech32 "1.1.4" - ws "7.4.6" - "@ethersproject/providers@5.5.2": version "5.5.2" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.5.2.tgz#131ccf52dc17afd0ab69ed444b8c0e3a27297d99" @@ -2812,6 +2780,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -3029,7 +3002,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.1.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== @@ -3052,14 +3025,6 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.9": version "0.3.19" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" @@ -3727,6 +3692,98 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.0.tgz#ee0e4b4564f101a5c4ee398cd4d73c0bd92b289c" integrity sha512-bv2sdS6LKqVVMLI5+zqnNrNU/CA+6z6CmwFXm/MzmOPBRSO5reEJN7z0Gbzvs0/bv/MZZXNklubpwy3v2+azsw== +"@parcel/watcher-android-arm64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.3.0.tgz#d82e74bb564ebd4d8a88791d273a3d2bd61e27ab" + integrity sha512-f4o9eA3dgk0XRT3XhB0UWpWpLnKgrh1IwNJKJ7UJek7eTYccQ8LR7XUWFKqw6aEq5KUNlCcGvSzKqSX/vtWVVA== + +"@parcel/watcher-darwin-arm64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.3.0.tgz#c9cd03f8f233d512fcfc873d5b4e23f1569a82ad" + integrity sha512-mKY+oijI4ahBMc/GygVGvEdOq0L4DxhYgwQqYAz/7yPzuGi79oXrZG52WdpGA1wLBPrYb0T8uBaGFo7I6rvSKw== + +"@parcel/watcher-darwin-x64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.3.0.tgz#83c902994a2a49b9e1ab5050dba24876fdc2c219" + integrity sha512-20oBj8LcEOnLE3mgpy6zuOq8AplPu9NcSSSfyVKgfOhNAc4eF4ob3ldj0xWjGGbOF7Dcy1Tvm6ytvgdjlfUeow== + +"@parcel/watcher-freebsd-x64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.3.0.tgz#7a0f4593a887e2752b706aff2dae509aef430cf6" + integrity sha512-7LftKlaHunueAEiojhCn+Ef2CTXWsLgTl4hq0pkhkTBFI3ssj2bJXmH2L67mKpiAD5dz66JYk4zS66qzdnIOgw== + +"@parcel/watcher-linux-arm-glibc@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.3.0.tgz#3fc90c3ebe67de3648ed2f138068722f9b1d47da" + integrity sha512-1apPw5cD2xBv1XIHPUlq0cO6iAaEUQ3BcY0ysSyD9Kuyw4MoWm1DV+W9mneWI+1g6OeP6dhikiFE6BlU+AToTQ== + +"@parcel/watcher-linux-arm64-glibc@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.3.0.tgz#f7bbbf2497d85fd11e4c9e9c26ace8f10ea9bcbc" + integrity sha512-mQ0gBSQEiq1k/MMkgcSB0Ic47UORZBmWoAWlMrTW6nbAGoLZP+h7AtUM7H3oDu34TBFFvjy4JCGP43JlylkTQA== + +"@parcel/watcher-linux-arm64-musl@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.3.0.tgz#de131a9fcbe1fa0854e9cbf4c55bed3b35bcff43" + integrity sha512-LXZAExpepJew0Gp8ZkJ+xDZaTQjLHv48h0p0Vw2VMFQ8A+RKrAvpFuPVCVwKJCr5SE+zvaG+Etg56qXvTDIedw== + +"@parcel/watcher-linux-x64-glibc@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.3.0.tgz#193dd1c798003cdb5a1e59470ff26300f418a943" + integrity sha512-P7Wo91lKSeSgMTtG7CnBS6WrA5otr1K7shhSjKHNePVmfBHDoAOHYRXgUmhiNfbcGk0uMCHVcdbfxtuiZCHVow== + +"@parcel/watcher-linux-x64-musl@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.3.0.tgz#6dbdb86d96e955ab0fe4a4b60734ec0025a689dd" + integrity sha512-+kiRE1JIq8QdxzwoYY+wzBs9YbJ34guBweTK8nlzLKimn5EQ2b2FSC+tAOpq302BuIMjyuUGvBiUhEcLIGMQ5g== + +"@parcel/watcher-wasm@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-wasm/-/watcher-wasm-2.3.0.tgz#73b66c6fbd2a3326ae86a1ec77eab7139d0dd725" + integrity sha512-ejBAX8H0ZGsD8lSICDNyMbSEtPMWgDL0WFCt/0z7hyf5v8Imz4rAM8xY379mBsECkq/Wdqa5WEDLqtjZ+6NxfA== + dependencies: + is-glob "^4.0.3" + micromatch "^4.0.5" + napi-wasm "^1.1.0" + +"@parcel/watcher-win32-arm64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.3.0.tgz#59da26a431da946e6c74fa6b0f30b120ea6650b6" + integrity sha512-35gXCnaz1AqIXpG42evcoP2+sNL62gZTMZne3IackM+6QlfMcJLy3DrjuL6Iks7Czpd3j4xRBzez3ADCj1l7Aw== + +"@parcel/watcher-win32-ia32@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.3.0.tgz#3ee6a18b08929cd3b788e8cc9547fd9a540c013a" + integrity sha512-FJS/IBQHhRpZ6PiCjFt1UAcPr0YmCLHRbTc00IBTrelEjlmmgIVLeOx4MSXzx2HFEy5Jo5YdhGpxCuqCyDJ5ow== + +"@parcel/watcher-win32-x64@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.3.0.tgz#14e7246289861acc589fd608de39fe5d8b4bb0a7" + integrity sha512-dLx+0XRdMnVI62kU3wbXvbIRhLck4aE28bIGKbRGS7BJNt54IIj9+c/Dkqb+7DJEbHUZAX1bwaoM8PqVlHJmCA== + +"@parcel/watcher@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.3.0.tgz#803517abbc3981a1a1221791d9f59dc0590d50f9" + integrity sha512-pW7QaFiL11O0BphO+bq3MgqeX/INAk9jgBldVDYjlQPO4VddoZnF22TcF9onMhnLVHuNqBJeRf+Fj7eezi/+rQ== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.3.0" + "@parcel/watcher-darwin-arm64" "2.3.0" + "@parcel/watcher-darwin-x64" "2.3.0" + "@parcel/watcher-freebsd-x64" "2.3.0" + "@parcel/watcher-linux-arm-glibc" "2.3.0" + "@parcel/watcher-linux-arm64-glibc" "2.3.0" + "@parcel/watcher-linux-arm64-musl" "2.3.0" + "@parcel/watcher-linux-x64-glibc" "2.3.0" + "@parcel/watcher-linux-x64-musl" "2.3.0" + "@parcel/watcher-win32-arm64" "2.3.0" + "@parcel/watcher-win32-ia32" "2.3.0" + "@parcel/watcher-win32-x64" "2.3.0" + "@polka/url@^1.0.0-next.20": version "1.0.0-next.23" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.23.tgz#498e41218ab3b6a1419c735e5c6ae2c5ed609b6c" @@ -3907,7 +3964,12 @@ "@safe-global/safe-core-sdk-utils" "^1.7.4" ethers "5.7.2" -"@safe-global/safe-gateway-typescript-sdk@^3.12.0", "@safe-global/safe-gateway-typescript-sdk@^3.5.3": +"@safe-global/safe-gateway-typescript-sdk@^3.13.2": + version "3.13.2" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.13.2.tgz#f03884c7eb766f5508085d95ab96063a28e20920" + integrity sha512-kGlJecJHBzGrGTq/yhLANh56t+Zur6Ubpt+/w03ARX1poDb4TM8vKU3iV8tuYpk359PPWp+Qvjnqb9oW2YQcYw== + +"@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.12.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.12.0.tgz#aa767a32f4d10f4ec9a47ad7e32d547d3b51e94c" integrity sha512-hExCo62lScVC9/ztVqYEYL2pFxcqLTvB8fj0WtdP5FWrvbtEgD0pbVolchzD5bf85pbzvEwdAxSVS7EdCZxTNw== @@ -4118,6 +4180,11 @@ rpc-websockets "^7.5.1" superstruct "^0.14.2" +"@spindl-xyz/attribution-lite@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@spindl-xyz/attribution-lite/-/attribution-lite-1.4.0.tgz#eca26c1fdd4c556ba00bee8c6c799908eceb84e9" + integrity sha512-FUOSfMoN+FlLcKZT0rIhcl9QiOo3RN5YDURoXE0IWPjL061hn2BDQyBUuvfxLrG8Tho1eh8K0LkES7viphdTmQ== + "@stablelib/aead@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/aead/-/aead-1.0.1.tgz#c4b1106df9c23d1b867eb9b276d8f42d5fc4c0c3" @@ -5055,26 +5122,6 @@ mkdirp "^1.0.4" path-browserify "^1.0.1" -"@tsconfig/node10@^1.0.7": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" - integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" - integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== - "@typechain/ethers-v5@^10.2.0": version "10.2.1" resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-10.2.1.tgz#50241e6957683281ecfa03fb5a6724d8a3ce2391" @@ -5641,27 +5688,6 @@ events "^3.3.0" isomorphic-unfetch "^3.1.0" -"@walletconnect/browser-utils@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@walletconnect/browser-utils/-/browser-utils-1.8.0.tgz#33c10e777aa6be86c713095b5206d63d32df0951" - integrity sha512-Wcqqx+wjxIo9fv6eBUFHPsW1y/bGWWRboni5dfD8PtOmrihrEpOCmvRJe4rfl7xgJW8Ea9UqKEaq0bIRLHlK4A== - dependencies: - "@walletconnect/safe-json" "1.0.0" - "@walletconnect/types" "^1.8.0" - "@walletconnect/window-getters" "1.0.0" - "@walletconnect/window-metadata" "1.0.0" - detect-browser "5.2.0" - -"@walletconnect/client@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@walletconnect/client/-/client-1.8.0.tgz#6f46b5499c7c861c651ff1ebe5da5b66225ca696" - integrity sha512-svyBQ14NHx6Cs2j4TpkQaBI/2AF4+LXz64FojTjMtV4VMMhl81jSO1vNeg+yYhQzvjcGH/GpSwixjyCW0xFBOQ== - dependencies: - "@walletconnect/core" "^1.8.0" - "@walletconnect/iso-crypto" "^1.8.0" - "@walletconnect/types" "^1.8.0" - "@walletconnect/utils" "^1.8.0" - "@walletconnect/core@2.10.2", "@walletconnect/core@^2.10.1": version "2.10.2" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.10.2.tgz#a1bf6e3e87b33f9df795ce0970d8ddd400fdc8a3" @@ -5684,35 +5710,27 @@ lodash.isequal "4.5.0" uint8arrays "^3.1.0" -"@walletconnect/core@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.8.0.tgz#6b2748b90c999d9d6a70e52e26a8d5e8bfeaa81e" - integrity sha512-aFTHvEEbXcZ8XdWBw6rpQDte41Rxwnuk3SgTD8/iKGSRTni50gI9S3YEzMj05jozSiOBxQci4pJDMVhIUMtarw== - dependencies: - "@walletconnect/socket-transport" "^1.8.0" - "@walletconnect/types" "^1.8.0" - "@walletconnect/utils" "^1.8.0" - -"@walletconnect/crypto@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@walletconnect/crypto/-/crypto-1.0.3.tgz#7b8dd4d7e2884fe3543c7c07aea425eef5ef9dd4" - integrity sha512-+2jdORD7XQs76I2Odgr3wwrtyuLUXD/kprNVsjWRhhhdO9Mt6WqVzOPu0/t7OHSmgal8k7SoBQzUc5hu/8zL/g== - dependencies: - "@walletconnect/encoding" "^1.0.2" - "@walletconnect/environment" "^1.0.1" - "@walletconnect/randombytes" "^1.0.3" - aes-js "^3.1.2" - hash.js "^1.1.7" - tslib "1.14.1" - -"@walletconnect/encoding@^1.0.1", "@walletconnect/encoding@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@walletconnect/encoding/-/encoding-1.0.2.tgz#cb3942ad038d6a6bf01158f66773062dd25724da" - integrity sha512-CrwSBrjqJ7rpGQcTL3kU+Ief+Bcuu9PH6JLOb+wM6NITX1GTxR/MfNwnQfhLKK6xpRAyj2/nM04OOH6wS8Imag== +"@walletconnect/core@2.10.5": + version "2.10.5" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.10.5.tgz#d61706c6459d9baaef05e83907df8cab82a03251" + integrity sha512-QnGHkA05KzJrtqExPqXm/TsstM1uTDI8tQT0x86/DuR6LdiYEntzSpVjnv7kKK6Mo9UxlXfud431dNRfOW5uJg== dependencies: - is-typedarray "1.0.0" - tslib "1.14.1" - typedarray-to-buffer "3.1.5" + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-provider" "1.0.13" + "@walletconnect/jsonrpc-types" "1.0.3" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.14" + "@walletconnect/keyvaluestorage" "^1.1.1" + "@walletconnect/logger" "^2.0.1" + "@walletconnect/relay-api" "^1.0.9" + "@walletconnect/relay-auth" "^1.0.4" + "@walletconnect/safe-json" "^1.0.2" + "@walletconnect/time" "^1.0.2" + "@walletconnect/types" "2.10.5" + "@walletconnect/utils" "2.10.5" + events "^3.3.0" + lodash.isequal "4.5.0" + uint8arrays "^3.1.0" "@walletconnect/environment@^1.0.1": version "1.0.1" @@ -5721,19 +5739,20 @@ dependencies: tslib "1.14.1" -"@walletconnect/ethereum-provider@^2.10.1": - version "2.10.2" - resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.10.2.tgz#d5aca538fbcbbf7dd771bceb2430de30f06411de" - integrity sha512-QMYFZ6+rVq2CJLdIPdKK0j1Qm66UA27oQU5V2SrL8EVwl7wFfm0Bq7fnL+qAWeDpn612dNeNErpk/ROa1zWlWg== +"@walletconnect/ethereum-provider@^2.10.2": + version "2.10.5" + resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.10.5.tgz#a14ede16a751f115f8a0d050736155c8654835d5" + integrity sha512-Pihi2M03cRkWEiGetRUiO2A506YTj/Bbbxp+Ct7t5N5SccoeuhrzsEt30pA7I0XAiOnAeKp79OKmXHRhXfRmhg== dependencies: "@walletconnect/jsonrpc-http-connection" "^1.0.7" "@walletconnect/jsonrpc-provider" "^1.0.13" "@walletconnect/jsonrpc-types" "^1.0.3" "@walletconnect/jsonrpc-utils" "^1.0.8" - "@walletconnect/sign-client" "2.10.2" - "@walletconnect/types" "2.10.2" - "@walletconnect/universal-provider" "2.10.2" - "@walletconnect/utils" "2.10.2" + "@walletconnect/modal" "^2.4.3" + "@walletconnect/sign-client" "2.10.5" + "@walletconnect/types" "2.10.5" + "@walletconnect/universal-provider" "2.10.5" + "@walletconnect/utils" "2.10.5" events "^3.3.0" "@walletconnect/events@^1.0.1": @@ -5753,15 +5772,6 @@ "@walletconnect/time" "^1.0.2" tslib "1.14.1" -"@walletconnect/iso-crypto@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@walletconnect/iso-crypto/-/iso-crypto-1.8.0.tgz#44ddf337c4f02837c062dbe33fa7ab36789df451" - integrity sha512-pWy19KCyitpfXb70hA73r9FcvklS+FvO9QUIttp3c2mfW8frxgYeRXfxLRCIQTkaYueRKvdqPjbyhPLam508XQ== - dependencies: - "@walletconnect/crypto" "^1.0.2" - "@walletconnect/types" "^1.8.0" - "@walletconnect/utils" "^1.8.0" - "@walletconnect/jsonrpc-http-connection@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.7.tgz#a6973569b8854c22da707a759d241e4f5c2d5a98" @@ -5789,7 +5799,7 @@ keyvaluestorage-interface "^1.0.0" tslib "1.14.1" -"@walletconnect/jsonrpc-utils@1.0.8", "@walletconnect/jsonrpc-utils@^1.0.3", "@walletconnect/jsonrpc-utils@^1.0.6", "@walletconnect/jsonrpc-utils@^1.0.7", "@walletconnect/jsonrpc-utils@^1.0.8": +"@walletconnect/jsonrpc-utils@1.0.8", "@walletconnect/jsonrpc-utils@^1.0.6", "@walletconnect/jsonrpc-utils@^1.0.7", "@walletconnect/jsonrpc-utils@^1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz#82d0cc6a5d6ff0ecc277cb35f71402c91ad48d72" integrity sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw== @@ -5809,6 +5819,16 @@ tslib "1.14.1" ws "^7.5.1" +"@walletconnect/jsonrpc-ws-connection@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.14.tgz#eec700e74766c7887de2bd76c91a0206628732aa" + integrity sha512-Jsl6fC55AYcbkNVkwNM6Jo+ufsuCQRqViOQ8ZBPH9pRREHH9welbBiszuTLqEJiQcO/6XfFDl6bzCJIkrEi8XA== + dependencies: + "@walletconnect/jsonrpc-utils" "^1.0.6" + "@walletconnect/safe-json" "^1.0.2" + events "^3.3.0" + ws "^7.5.1" + "@walletconnect/keyvaluestorage@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.0.2.tgz#92f5ca0f54c1a88a093778842ce0c874d86369c8" @@ -5817,6 +5837,15 @@ safe-json-utils "^1.1.1" tslib "1.14.1" +"@walletconnect/keyvaluestorage@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz#dd2caddabfbaf80f6b8993a0704d8b83115a1842" + integrity sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA== + dependencies: + "@walletconnect/safe-json" "^1.0.1" + idb-keyval "^6.2.1" + unstorage "^1.9.0" + "@walletconnect/logger@2.0.1", "@walletconnect/logger@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/logger/-/logger-2.0.1.tgz#7f489b96e9a1ff6bf3e58f0fbd6d69718bf844a8" @@ -5825,11 +5854,6 @@ pino "7.11.0" tslib "1.14.1" -"@walletconnect/mobile-registry@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@walletconnect/mobile-registry/-/mobile-registry-1.4.0.tgz#502cf8ab87330841d794819081e748ebdef7aee5" - integrity sha512-ZtKRio4uCZ1JUF7LIdecmZt7FOLnX72RPSY7aUVu7mj7CSfxDwUn6gBuK6WGtH+NZCldBqDl5DenI5fFSvkKYw== - "@walletconnect/modal-core@2.6.2": version "2.6.2" resolved "https://registry.yarnpkg.com/@walletconnect/modal-core/-/modal-core-2.6.2.tgz#d73e45d96668764e0c8668ea07a45bb8b81119e9" @@ -5847,7 +5871,7 @@ motion "10.16.2" qrcode "1.5.3" -"@walletconnect/modal@2.6.2": +"@walletconnect/modal@2.6.2", "@walletconnect/modal@^2.4.3": version "2.6.2" resolved "https://registry.yarnpkg.com/@walletconnect/modal/-/modal-2.6.2.tgz#4b534a836f5039eeb3268b80be7217a94dd12651" integrity sha512-eFopgKi8AjKf/0U4SemvcYw9zlLpx9njVN8sf6DAkowC2Md0gPU/UNEbH1Wwj407pEKnEds98pKWib1NN1ACoA== @@ -5855,28 +5879,6 @@ "@walletconnect/modal-core" "2.6.2" "@walletconnect/modal-ui" "2.6.2" -"@walletconnect/qrcode-modal@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@walletconnect/qrcode-modal/-/qrcode-modal-1.8.0.tgz#ddd6f5c9b7ee52c16adf9aacec2a3eac4994caea" - integrity sha512-BueaFefaAi8mawE45eUtztg3ZFbsAH4DDXh1UNwdUlsvFMjqcYzLUG0xZvDd6z2eOpbgDg2N3bl6gF0KONj1dg== - dependencies: - "@walletconnect/browser-utils" "^1.8.0" - "@walletconnect/mobile-registry" "^1.4.0" - "@walletconnect/types" "^1.8.0" - copy-to-clipboard "^3.3.1" - preact "10.4.1" - qrcode "1.4.4" - -"@walletconnect/randombytes@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@walletconnect/randombytes/-/randombytes-1.0.3.tgz#e795e4918367fd1e6a2215e075e64ab93e23985b" - integrity sha512-35lpzxcHFbTN3ABefC9W+uBpNZl1GC4Wpx0ed30gibfO/y9oLdy1NznbV96HARQKSBV9J9M/rrtIvf6a23jfYw== - dependencies: - "@walletconnect/encoding" "^1.0.2" - "@walletconnect/environment" "^1.0.1" - randombytes "^2.1.0" - tslib "1.14.1" - "@walletconnect/relay-api@^1.0.9": version "1.0.9" resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.9.tgz#f8c2c3993dddaa9f33ed42197fc9bfebd790ecaf" @@ -5897,11 +5899,6 @@ tslib "1.14.1" uint8arrays "^3.0.0" -"@walletconnect/safe-json@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@walletconnect/safe-json/-/safe-json-1.0.0.tgz#12eeb11d43795199c045fafde97e3c91646683b2" - integrity sha512-QJzp/S/86sUAgWY6eh5MKYmSfZaRpIlmCJdi5uG4DJlKkZrHEF7ye7gA+VtbVzvTtpM/gRwO2plQuiooIeXjfg== - "@walletconnect/safe-json@^1.0.1", "@walletconnect/safe-json@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/safe-json/-/safe-json-1.0.2.tgz#7237e5ca48046e4476154e503c6d3c914126fa77" @@ -5924,14 +5921,20 @@ "@walletconnect/utils" "2.10.2" events "^3.3.0" -"@walletconnect/socket-transport@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@walletconnect/socket-transport/-/socket-transport-1.8.0.tgz#9a1128a249628a0be11a0979b522fe82b44afa1b" - integrity sha512-5DyIyWrzHXTcVp0Vd93zJ5XMW61iDM6bcWT4p8DTRfFsOtW46JquruMhxOLeCOieM4D73kcr3U7WtyR4JUsGuQ== +"@walletconnect/sign-client@2.10.5": + version "2.10.5" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.10.5.tgz#f97f0fed544179a3303941f3bebd13ede3a736ed" + integrity sha512-HEYsoeGC6fGplQy0NIZSRNHgOwZwQ892UWG1Ahkcasf2R35QaBgnTVQkSCisl1PAAOKXZG7yB1YDoAAZBF+g5Q== dependencies: - "@walletconnect/types" "^1.8.0" - "@walletconnect/utils" "^1.8.0" - ws "7.5.3" + "@walletconnect/core" "2.10.5" + "@walletconnect/events" "^1.0.1" + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/logger" "^2.0.1" + "@walletconnect/time" "^1.0.2" + "@walletconnect/types" "2.10.5" + "@walletconnect/utils" "2.10.5" + events "^3.3.0" "@walletconnect/time@^1.0.2": version "1.0.2" @@ -5952,24 +5955,36 @@ "@walletconnect/logger" "^2.0.1" events "^3.3.0" -"@walletconnect/types@^1.8.0", "@walletconnect/types@~1.8.0": +"@walletconnect/types@2.10.5": + version "2.10.5" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.10.5.tgz#bf4e692cf736b6e71423f96a106d7a96089245de" + integrity sha512-N8xaN7/Kob93rKxKDaT6oy6symgIkAwyLqq0/dLJEhXfv7S/gyNvDka4SosjVVTc4oTvE1+OmxNIR8pB1DuwJw== + dependencies: + "@walletconnect/events" "^1.0.1" + "@walletconnect/heartbeat" "1.2.1" + "@walletconnect/jsonrpc-types" "1.0.3" + "@walletconnect/keyvaluestorage" "^1.1.1" + "@walletconnect/logger" "^2.0.1" + events "^3.3.0" + +"@walletconnect/types@~1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.8.0.tgz#3f5e85b2d6b149337f727ab8a71b8471d8d9a195" integrity sha512-Cn+3I0V0vT9ghMuzh1KzZvCkiAxTq+1TR2eSqw5E5AVWfmCtECFkVZBP6uUJZ8YjwLqXheI+rnjqPy7sVM4Fyg== -"@walletconnect/universal-provider@2.10.2": - version "2.10.2" - resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.10.2.tgz#85c8da39f65da8fe33f65f62689e703607b5ddc5" - integrity sha512-wFgI0LbQ3D56sgaUMsgOHCM5m8WLxiC71BGuCKQfApgsbNMVKugYVy2zWHyUyi8sqTQHI+uSaVpDev4UHq9LEw== +"@walletconnect/universal-provider@2.10.5": + version "2.10.5" + resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.10.5.tgz#b08965a9306a30775796cc0cc384f2dfc9fde512" + integrity sha512-sQOvjrGF6za7+6zv7KI9eQz2gzRbS19j7U1z+JwIWdn4VBJmriaTjVHDz/R1liwKcS4sUiUthDC6WmQvjukjZQ== dependencies: "@walletconnect/jsonrpc-http-connection" "^1.0.7" "@walletconnect/jsonrpc-provider" "1.0.13" "@walletconnect/jsonrpc-types" "^1.0.2" "@walletconnect/jsonrpc-utils" "^1.0.7" "@walletconnect/logger" "^2.0.1" - "@walletconnect/sign-client" "2.10.2" - "@walletconnect/types" "2.10.2" - "@walletconnect/utils" "2.10.2" + "@walletconnect/sign-client" "2.10.5" + "@walletconnect/types" "2.10.5" + "@walletconnect/utils" "2.10.5" events "^3.3.0" "@walletconnect/utils@2.10.2", "@walletconnect/utils@^2.10.1", "@walletconnect/utils@^2.10.2": @@ -5992,18 +6007,25 @@ query-string "7.1.3" uint8arrays "^3.1.0" -"@walletconnect/utils@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.8.0.tgz#2591a197c1fa7429941fe428876088fda6632060" - integrity sha512-zExzp8Mj1YiAIBfKNm5u622oNw44WOESzo6hj+Q3apSMIb0Jph9X3GDIdbZmvVZsNPxWDL7uodKgZcCInZv2vA== - dependencies: - "@walletconnect/browser-utils" "^1.8.0" - "@walletconnect/encoding" "^1.0.1" - "@walletconnect/jsonrpc-utils" "^1.0.3" - "@walletconnect/types" "^1.8.0" - bn.js "4.11.8" - js-sha3 "0.8.0" - query-string "6.13.5" +"@walletconnect/utils@2.10.5": + version "2.10.5" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.10.5.tgz#8c8ef90c9e2b59886aae002ab8cbbdb35da1df9a" + integrity sha512-3yeclD9/AlPEIHBqBVzrHUO/KRAEIXVK0ViIQ5oUH+zT3TpdsDGDiW1Z0TsAQ1EiYoiiz8dOQzd80a3eZVwnrg== + dependencies: + "@stablelib/chacha20poly1305" "1.0.1" + "@stablelib/hkdf" "1.0.1" + "@stablelib/random" "^1.0.2" + "@stablelib/sha256" "1.0.1" + "@stablelib/x25519" "^1.0.3" + "@walletconnect/relay-api" "^1.0.9" + "@walletconnect/safe-json" "^1.0.2" + "@walletconnect/time" "^1.0.2" + "@walletconnect/types" "2.10.5" + "@walletconnect/window-getters" "^1.0.1" + "@walletconnect/window-metadata" "^1.0.1" + detect-browser "5.3.0" + query-string "7.1.3" + uint8arrays "^3.1.0" "@walletconnect/web3wallet@^1.9.2": version "1.9.2" @@ -6019,25 +6041,13 @@ "@walletconnect/types" "2.10.2" "@walletconnect/utils" "2.10.2" -"@walletconnect/window-getters@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@walletconnect/window-getters/-/window-getters-1.0.0.tgz#1053224f77e725dfd611c83931b5f6c98c32bfc8" - integrity sha512-xB0SQsLaleIYIkSsl43vm8EwETpBzJ2gnzk7e0wMF3ktqiTGS6TFHxcprMl5R44KKh4tCcHCJwolMCaDSwtAaA== - -"@walletconnect/window-getters@^1.0.0", "@walletconnect/window-getters@^1.0.1": +"@walletconnect/window-getters@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-getters/-/window-getters-1.0.1.tgz#f36d1c72558a7f6b87ecc4451fc8bd44f63cbbdc" integrity sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q== dependencies: tslib "1.14.1" -"@walletconnect/window-metadata@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@walletconnect/window-metadata/-/window-metadata-1.0.0.tgz#93b1cc685e6b9b202f29c26be550fde97800c4e5" - integrity sha512-9eFvmJxIKCC3YWOL97SgRkKhlyGXkrHwamfechmqszbypFspaSk+t2jQXAEU7YClHF6Qjw5eYOmy1//zFi9/GA== - dependencies: - "@walletconnect/window-getters" "^1.0.0" - "@walletconnect/window-metadata@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz#2124f75447b7e989e4e4e1581d55d25bc75f7be5" @@ -6144,16 +6154,13 @@ ethereumjs-util "^7.1.3" hdkey "^2.0.1" -"@web3-onboard/walletconnect@^2.4.7": - version "2.4.7" - resolved "https://registry.yarnpkg.com/@web3-onboard/walletconnect/-/walletconnect-2.4.7.tgz#e7b0c6494b5414fbf6450689941a01cb1eb7898c" - integrity sha512-Fh0tej56icsjTu0t0njoebXa/yZ7KGIQxfaTD31s/LsbUXMiyMFwqvyYQEaD1dvKlPMWLj/4AgRQlYYHTqjLLg== +"@web3-onboard/walletconnect@^2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@web3-onboard/walletconnect/-/walletconnect-2.5.0.tgz#3370079a1366c196d5d089d754e54bb6f2c04a01" + integrity sha512-cD5OZYPCg1tvG/mevsEnBauhPK/TJQALru7S6fCB4IpML8tcIf3bk0S+0caZdOlqK+UfOT2P/iiQrIh8OKZ01g== dependencies: - "@ethersproject/providers" "5.5.0" - "@walletconnect/client" "^1.8.0" - "@walletconnect/ethereum-provider" "^2.10.1" + "@walletconnect/ethereum-provider" "^2.10.2" "@walletconnect/modal" "2.6.2" - "@walletconnect/qrcode-modal" "^1.8.0" "@web3-onboard/common" "^2.3.3" joi "17.9.1" rxjs "^7.5.2" @@ -6456,7 +6463,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1: +acorn-walk@^8.0.0, acorn-walk@^8.0.2: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -6466,21 +6473,21 @@ acorn@7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== -acorn@^8.0.4, acorn@^8.1.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.0.4, acorn@^8.1.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: version "8.10.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.10.0: + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== + aes-js@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== -aes-js@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a" - integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ== - agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -6540,17 +6547,12 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: dependencies: type-fest "^0.21.3" -ansi-regex@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" - integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^3.2.0, ansi-styles@^3.2.1: +ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== @@ -6569,7 +6571,7 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -anymatch@^3.0.3, anymatch@~3.1.2: +anymatch@^3.0.3, anymatch@^3.1.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -6582,11 +6584,6 @@ arch@^2.2.0: resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -7121,11 +7118,6 @@ bn.js@4.11.6: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" integrity sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA== -bn.js@4.11.8: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== - bn.js@^4.11.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.12.0: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" @@ -7293,30 +7285,12 @@ btoa@^1.2.1: resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== -buffer-alloc-unsafe@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" - integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== - -buffer-alloc@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" - integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== - dependencies: - buffer-alloc-unsafe "^1.1.0" - buffer-fill "^1.0.0" - buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== -buffer-fill@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" - integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== - -buffer-from@^1.0.0, buffer-from@^1.1.1: +buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== @@ -7344,7 +7318,7 @@ buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -buffer@^5.0.5, buffer@^5.1.0, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0: +buffer@^5.0.5, buffer@^5.1.0, buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -7498,7 +7472,7 @@ checkpoint-store@^1.1.0: dependencies: functional-red-black-tree "^1.0.1" -"chokidar@>=3.0.0 <4.0.0": +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -7547,6 +7521,13 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +citty@^0.1.3, citty@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.5.tgz#fe37ceae5dc764af75eb2fece99d2bf527ea4e50" + integrity sha512-AS7n5NSc0OQVMV9v6wt3ByujNIrne0/cTjiC2MYqhvao57VNfiuVksTSr2p17nVOhEr2KtqiAkGwHcgMC/qUuQ== + dependencies: + consola "^3.2.3" + cjs-module-lexer@^1.0.0: version "1.2.3" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" @@ -7614,14 +7595,14 @@ client-only@0.0.1: resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== +clipboardy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-3.0.0.tgz#f3876247404d334c9ed01b6f269c11d09a5e3092" + integrity sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg== dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" + arch "^2.2.0" + execa "^5.1.1" + is-wsl "^2.2.0" cliui@^6.0.0: version "6.0.0" @@ -7663,6 +7644,11 @@ clsx@^2.0.0: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -7759,6 +7745,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +consola@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" + integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ== + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -7790,6 +7781,11 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-es@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-1.0.0.tgz#4759684af168dfc54365b2c2dda0a8d7ee1e4865" + integrity sha512-mWYvfOLrfEc996hlKcdABeIiPHUPC6DM2QYZdGGOvhOTbA3tjm2eBwqlJpoFdjC89NI4Qt6h0Pu06Mp+1Pj5OQ== + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -7807,13 +7803,6 @@ copy-anything@^2.0.1: dependencies: is-what "^3.14.1" -copy-to-clipboard@^3.3.1: - version "3.3.3" - resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" - integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== - dependencies: - toggle-selection "^1.0.6" - core-js-compat@^3.31.0, core-js-compat@^3.32.2: version "3.33.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.33.0.tgz#24aa230b228406450b2277b7c8bfebae932df966" @@ -7898,11 +7887,6 @@ create-jest@^29.7.0: jest-util "^29.7.0" prompts "^2.0.1" -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -8238,6 +8222,11 @@ define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, de has-property-descriptors "^1.0.0" object-keys "^1.1.1" +defu@^6.1.2, defu@^6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.3.tgz#6d7f56bc61668e844f9f593ace66fd67ef1205fd" + integrity sha512-Vy2wmG3NTkmHNg/kzpuvHhkqeIx3ODWqasgCRbKtbXEN0G+HpEEv9BtJLp7ZG1CZloFaC41Ah3ZFbq7aqCqMeQ== + del@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" @@ -8261,6 +8250,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -8271,21 +8265,26 @@ dequal@^2.0.3: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +destr@^2.0.1, destr@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.2.tgz#8d3c0ee4ec0a76df54bc8b819bca215592a8c218" + integrity sha512-65AlobnZMiCET00KaFFjUefxDX0khFA/E4myqZ7a6Sq1yZtR8+FVIvilVX66vF2uobSumxooYZChiRPCKNqhmg== + destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== -detect-browser@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.2.0.tgz#c9cd5afa96a6a19fda0bbe9e9be48a6b6e1e9c97" - integrity sha512-tr7XntDAu50BVENgQfajMLzacmSe34D+qZc4zjnniz0ZVuw/TZcLcyxHQjYpJTM36sGEkZZlYLnIM1hH7alTMA== - detect-browser@5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.3.0.tgz#9705ef2bddf46072d0f7265a1fe300e36fe7ceca" integrity sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -8296,11 +8295,6 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - dijkstrajs@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" @@ -8487,11 +8481,6 @@ emittery@^0.13.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -9602,7 +9591,7 @@ execa@4.1.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" -execa@^5.0.0: +execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -9891,13 +9880,6 @@ find-root@^1.1.0: resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -10124,6 +10106,11 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-port-please@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/get-port-please/-/get-port-please-3.1.1.tgz#2556623cddb4801d823c0a6a15eec038abb483be" + integrity sha512-3UBAyM3u4ZBVYDsxOQfJDxEa6XTbpBDrOjp4mf7ExFRt5BKs/QywQQiJsh2B+hxcZLSapWqCRvElUe8DnKcFHA== + get-stream@^5.0.0, get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -10340,6 +10327,20 @@ gzip-size@^6.0.0: dependencies: duplexer "^0.1.2" +h3@^1.8.1, h3@^1.8.2: + version "1.9.0" + resolved "https://registry.yarnpkg.com/h3/-/h3-1.9.0.tgz#c5f512a93026df9837db6f30c9ef51135dd46752" + integrity sha512-+F3ZqrNV/CFXXfZ2lXBINHi+rM4Xw3CDC5z2CDK3NMPocjonKipGLLDSkrqY9DOrioZNPTIdDMWfQKm//3X2DA== + dependencies: + cookie-es "^1.0.0" + defu "^6.1.3" + destr "^2.0.2" + iron-webcrypto "^1.0.0" + radix3 "^1.1.0" + ufo "^1.3.2" + uncrypto "^0.1.3" + unenv "^1.7.4" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -10503,6 +10504,11 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" +http-shutdown@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/http-shutdown/-/http-shutdown-1.2.2.tgz#41bc78fc767637c4c95179bc492f312c0ae64c5f" + integrity sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw== + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -10708,6 +10714,21 @@ invariant@2, invariant@^2.2.2: dependencies: loose-envify "^1.0.0" +ioredis@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.2.tgz#9139f596f62fc9c72d873353ac5395bcf05709f7" + integrity sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ip@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" @@ -10718,6 +10739,11 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +iron-webcrypto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-1.0.0.tgz#e3b689c0c61b434a0a4cb82d0aeabbc8b672a867" + integrity sha512-anOK1Mktt8U1Xi7fCM3RELTuYbnFikQY5VtrDj7kPgpejV7d43tWKhzgioO0zpkazLEL/j/iayRqnJhrGfqUsg== + is-arguments@^1.0.4, is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" @@ -10795,6 +10821,11 @@ is-date-object@^1.0.1, is-date-object@^1.0.5: dependencies: has-tostringtag "^1.0.0" +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -10812,11 +10843,6 @@ is-fn@^1.0.0: resolved "https://registry.yarnpkg.com/is-fn/-/is-fn-1.0.0.tgz#9543d5de7bcf5b08a22ec8a20bae6e286d510d8c" integrity sha512-XoFPJQmsAShb3jEQRfzf2rqXavq7fIqF/jOekp308JlThqrODnMpweVSGilKTCXELfLhltGP2AGgbQGVP8F1dg== -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -10984,7 +11010,7 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.3, is-typed- dependencies: which-typed-array "^1.1.11" -is-typedarray@1.0.0, is-typedarray@^1.0.0, is-typedarray@~1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== @@ -11019,12 +11045,19 @@ is-what@^3.14.1: resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== -isarray@^2.0.1, isarray@^2.0.5: +isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== @@ -11544,6 +11577,11 @@ jest@^29.6.2: import-local "^3.0.2" jest-cli "^29.7.0" +jiti@^1.20.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + joi@17.9.1: version "17.9.1" resolved "https://registry.yarnpkg.com/joi/-/joi-17.9.1.tgz#74899b9fa3646904afa984a11df648eca66c9018" @@ -11722,6 +11760,11 @@ json5@^2.1.3, json5@^2.2.0, json5@^2.2.2, json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonc-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -11954,6 +11997,29 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +listhen@^1.5.5: + version "1.5.5" + resolved "https://registry.yarnpkg.com/listhen/-/listhen-1.5.5.tgz#58915512af70f770aa3e9fb19367adf479bb58c4" + integrity sha512-LXe8Xlyh3gnxdv4tSjTjscD1vpr/2PRpzq8YIaMJgyKzRG8wdISlWVWnGThJfHnlJ6hmLt2wq1yeeix0TEbuoA== + dependencies: + "@parcel/watcher" "^2.3.0" + "@parcel/watcher-wasm" "2.3.0" + citty "^0.1.4" + clipboardy "^3.0.0" + consola "^3.2.3" + defu "^6.1.2" + get-port-please "^3.1.1" + h3 "^1.8.1" + http-shutdown "^1.2.2" + jiti "^1.20.0" + mlly "^1.4.2" + node-forge "^1.3.1" + pathe "^1.1.1" + std-env "^3.4.3" + ufo "^1.3.0" + untun "^0.1.2" + uqr "^0.1.2" + listr2@^3.8.3: version "3.14.0" resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" @@ -11998,14 +12064,6 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -12035,6 +12093,11 @@ lodash.defaults@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.isequal@4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -12125,6 +12188,11 @@ lowercase-keys@^3.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== +lru-cache@^10.0.2: + version "10.1.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" + integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -12183,11 +12251,6 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - makeerror@1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" @@ -12279,7 +12342,7 @@ micro-ftch@^0.3.1: resolved "https://registry.yarnpkg.com/micro-ftch/-/micro-ftch-0.3.1.tgz#6cb83388de4c1f279a034fb0cf96dfc050853c5f" integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg== -micromatch@^4.0.4: +micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -12304,6 +12367,11 @@ mime@1.6.0, mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -12399,6 +12467,16 @@ mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mlly@^1.2.0, mlly@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.2.tgz#7cf406aa319ff6563d25da6b36610a93f2a8007e" + integrity sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg== + dependencies: + acorn "^8.10.0" + pathe "^1.1.1" + pkg-types "^1.0.3" + ufo "^1.3.0" + mock-fs@^4.1.0: version "4.14.0" resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" @@ -12416,7 +12494,7 @@ motion@10.16.2: "@motionone/utils" "^10.15.1" "@motionone/vue" "^10.16.2" -mri@^1.1.0: +mri@^1.1.0, mri@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== @@ -12519,6 +12597,11 @@ nanoid@^4.0.0: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e" integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw== +napi-wasm@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/napi-wasm/-/napi-wasm-1.1.0.tgz#bbe617823765ae9c1bc12ff5942370eae7b2ba4e" + integrity sha512-lHwIAJbmLSjF9VDRm9GoVOy9AGp3aIvkjv+Kvz9h16QR3uSVYH78PNQUnT2U4X53mhlnV2M7wrhibQ3GHicDmg== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -12596,6 +12679,11 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.0.0.tgz#8136add2f510997b3b94814f4af1cce0b0e3962e" integrity sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA== +node-fetch-native@^1.4.0, node-fetch-native@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.4.1.tgz#5a336e55b4e1b1e72b9927da09fecd2b374c9be5" + integrity sha512-NsXBU0UgBxo2rQLOeWNZqS3fvflWePMECr8CoSWoSTqCqGbVVsvl9vZu1HfQicYN0g5piV9Gh8RTEvo/uP752w== + node-fetch@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -12610,6 +12698,11 @@ node-fetch@^2.0.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, nod dependencies: whatwg-url "^5.0.0" +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + node-gyp-build@^4.2.0, node-gyp-build@^4.2.2, node-gyp-build@^4.3.0, node-gyp-build@^4.5.0: version "4.6.1" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e" @@ -12762,6 +12855,15 @@ oboe@2.1.5: dependencies: http-https "^1.0.0" +ofetch@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.3.3.tgz#588cb806a28e5c66c2c47dd8994f9059a036d8c0" + integrity sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg== + dependencies: + destr "^2.0.1" + node-fetch-native "^1.4.0" + ufo "^1.3.0" + on-exit-leak-free@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz#b39c9e3bf7690d890f4861558b0d7b90a442d209" @@ -12820,7 +12922,7 @@ p-cancelable@^3.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== -p-limit@^2.0.0, p-limit@^2.2.0: +p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== @@ -12834,13 +12936,6 @@ p-limit@^3.0.2, p-limit@^3.1.0: dependencies: yocto-queue "^0.1.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -12921,11 +13016,6 @@ path-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -12961,6 +13051,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathe@^1.1.0, pathe@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" + integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== + pbkdf2@^3.0.17: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" @@ -13066,10 +13161,14 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -pngjs@^3.3.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" - integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== +pkg-types@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868" + integrity sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A== + dependencies: + jsonc-parser "^3.2.0" + mlly "^1.2.0" + pathe "^1.1.0" pngjs@^5.0.0: version "5.0.0" @@ -13132,11 +13231,6 @@ postcss@8.4.31, postcss@^8.0.0, postcss@^8.4.21: picocolors "^1.0.0" source-map-js "^1.0.2" -preact@10.4.1: - version "10.4.1" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.4.1.tgz#9b3ba020547673a231c6cf16f0fbaef0e8863431" - integrity sha512-WKrRpCSwL2t3tpOOGhf2WfTpcmbpxaWtDbdJdKdjd0aEiTkvOmS4NBkG6kzlaAHI9AkQ3iVqbFWM3Ei7mZ4o1Q== - preact@^10.5.9: version "10.18.1" resolved "https://registry.yarnpkg.com/preact/-/preact-10.18.1.tgz#3b84bb305f0b05f4ad5784b981d15fcec4e105da" @@ -13327,19 +13421,6 @@ qrcode.react@^3.1.0: resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8" integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q== -qrcode@1.4.4: - version "1.4.4" - resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83" - integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q== - dependencies: - buffer "^5.4.3" - buffer-alloc "^1.2.0" - buffer-from "^1.1.1" - dijkstrajs "^1.0.1" - isarray "^2.0.1" - pngjs "^3.3.0" - yargs "^13.2.4" - qrcode@1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.3.tgz#03afa80912c0dccf12bc93f615a535aad1066170" @@ -13376,15 +13457,6 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== -query-string@6.13.5: - version "6.13.5" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.5.tgz#99e95e2fb7021db90a6f373f990c0c814b3812d8" - integrity sha512-svk3xg9qHR39P3JlHuD7g3nRnyay5mHbrPctEBDUxUkHRifPHXJDhBUycdCC0NBjXoDf44Gb+IsOZL1Uwn8M/Q== - dependencies: - decode-uri-component "^0.2.0" - split-on-first "^1.0.0" - strict-uri-encode "^2.0.0" - query-string@7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" @@ -13424,6 +13496,11 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +radix3@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.1.0.tgz#9745df67a49c522e94a33d0a93cf743f104b6e0d" + integrity sha512-pNsHDxbGORSvuSScqNJ+3Km6QAVqk8CfsCBIEoDgpqLrkD2f3QM4I7d1ozJJ172OmIcoUcerZaNWqtLkRXTV3A== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -13677,6 +13754,18 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + reduce-flatten@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" @@ -14561,11 +14650,21 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +std-env@^3.4.3: + version "3.5.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.5.0.tgz#83010c9e29bd99bf6f605df87c19012d82d63b97" + integrity sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA== + stop-iteration-iterator@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" @@ -14614,15 +14713,6 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -14702,13 +14792,6 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -15051,11 +15134,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toggle-selection@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" - integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== - toidentifier@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" @@ -15143,25 +15221,6 @@ ts-morph@^13.0.1: "@ts-morph/common" "~0.12.3" code-block-writer "^11.0.0" -ts-node@^10.8.2: - version "10.9.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" - integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - ts-prune@^0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/ts-prune/-/ts-prune-0.10.3.tgz#b6c71a525543b38dcf947a7d3adfb7f9e8b91f38" @@ -15332,7 +15391,7 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typedarray-to-buffer@3.1.5, typedarray-to-buffer@^3.1.5: +typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== @@ -15410,6 +15469,11 @@ ua-parser-js@^1.0.35: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.36.tgz#a9ab6b9bd3a8efb90bb0816674b412717b7c428c" integrity sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw== +ufo@^1.3.0, ufo@^1.3.1, ufo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.2.tgz#c7d719d0628a1c80c006d2240e0d169f6e3c0496" + integrity sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA== + uint8arrays@^3.0.0, uint8arrays@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0" @@ -15432,11 +15496,27 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +uncrypto@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" + integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== + undici-types@~5.25.1: version "5.25.3" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== +unenv@^1.7.4: + version "1.8.0" + resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.8.0.tgz#0f860d5278405700bd95d47b23bc01f3a735d68c" + integrity sha512-uIGbdCWZfhRRmyKj1UioCepQ0jpq638j/Cf0xFTn4zD1nGJ2lSdzYHLzfdXN791oo/0juUiSWW1fBklXMTsuqg== + dependencies: + consola "^3.2.3" + defu "^6.1.3" + mime "^3.0.0" + node-fetch-native "^1.4.1" + pathe "^1.1.1" + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" @@ -15497,11 +15577,37 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +unstorage@^1.9.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.10.1.tgz#bf8cc00a406e40a6293e893da9807057d95875b0" + integrity sha512-rWQvLRfZNBpF+x8D3/gda5nUCQL2PgXy2jNG4U7/Rc9BGEv9+CAJd0YyGCROUBKs9v49Hg8huw3aih5Bf5TAVw== + dependencies: + anymatch "^3.1.3" + chokidar "^3.5.3" + destr "^2.0.2" + h3 "^1.8.2" + ioredis "^5.3.2" + listhen "^1.5.5" + lru-cache "^10.0.2" + mri "^1.2.0" + node-fetch-native "^1.4.1" + ofetch "^1.3.3" + ufo "^1.3.1" + untildify@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== +untun@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/untun/-/untun-0.1.2.tgz#fa42a62ae24c1c5c6f3209692a2b0e1f573f1353" + integrity sha512-wLAMWvxfqyTiBODA1lg3IXHQtjggYLeTK7RnSfqtOXixWJ3bAa2kK/HHmOOg19upteqO3muLvN6O/icbyQY33Q== + dependencies: + citty "^0.1.3" + consola "^3.2.3" + pathe "^1.1.1" + upath@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" @@ -15515,6 +15621,11 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" +uqr@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/uqr/-/uqr-0.1.2.tgz#5c6cd5dcff9581f9bb35b982cb89e2c483a41d7d" + integrity sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA== + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -15602,11 +15713,6 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - v8-to-istanbul@^9.0.1: version "9.1.3" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz#ea456604101cd18005ac2cae3cdd1aa058a6306b" @@ -16493,15 +16599,6 @@ workbox-window@7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.0.0" -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -16538,11 +16635,6 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== -ws@7.5.3: - version "7.5.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" - integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== - ws@7.5.9, ws@^7.2.0, ws@^7.3.1, ws@^7.4.5, ws@^7.5.1: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" @@ -16666,14 +16758,6 @@ yaml@^1.10.0, yaml@^1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" @@ -16687,22 +16771,6 @@ yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^13.2.4: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - yargs@^15.3.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" @@ -16741,11 +16809,6 @@ yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"