├── .commitlintrc.json ├── .env ├── .eslintignore ├── .eslintrc.cjs ├── .firebaserc ├── .gitattributes ├── .github └── workflows │ ├── e2e.yml │ ├── feature.yml │ ├── lint-pr-title.yml │ ├── production.yml │ ├── pull_request_template.md │ ├── release.yml │ ├── secrets_scanner.yaml │ └── version.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .releaserc ├── DEPLOYMENT.md ├── Dockerfile ├── LICENSE ├── README.md ├── app.vue ├── assets ├── .DS_Store └── css │ ├── _mixins.scss │ ├── style.scss │ └── tailwind.css ├── components ├── .DS_Store ├── BridgeFromEthereumButton.vue ├── ConnectWalletBlock.vue ├── EcosystemBlock.vue ├── address │ ├── AddressAvatar.vue │ ├── AddressCard.vue │ └── AddressCardLoader.vue ├── animations │ ├── EcosystemLogotypes.vue │ └── TransactionProgress.vue ├── common │ ├── Alert.vue │ ├── Badge.vue │ ├── CardWithLineButtons.vue │ ├── Checkbox.vue │ ├── CheckboxWithText.vue │ ├── CircleLoader.vue │ ├── ContentBlock.vue │ ├── ContentLoader.vue │ ├── EmptyBlock.vue │ ├── ErrorBlock.vue │ ├── HeightTransition.vue │ ├── ImageLoader.vue │ ├── LineButtonsGroup.vue │ ├── Modal.vue │ ├── QrAddressInput.vue │ ├── QrCodeStyled.vue │ ├── QrInput.vue │ ├── QrUploadIconButton.vue │ ├── SmallInput.vue │ ├── Spinner.vue │ ├── Timer.vue │ ├── TotalBalance.vue │ ├── button │ │ ├── Button.vue │ │ ├── ButtonBack.vue │ │ ├── ButtonGroup.vue │ │ ├── Dropdown.vue │ │ ├── Label.vue │ │ ├── Line.vue │ │ ├── LineBodyInfo.vue │ │ ├── LineWithImg.vue │ │ ├── TopInfo.vue │ │ ├── TopLink.vue │ │ └── UnderlineText.vue │ └── input │ │ ├── ErrorMessage.vue │ │ ├── InputLine.vue │ │ ├── Search.vue │ │ ├── TransactionAddress.vue │ │ └── TransactionAmount.vue ├── destination │ ├── DestinationItem.vue │ ├── DestinationLabel.vue │ └── IconContainer.vue ├── footer │ └── Footer.vue ├── header │ ├── AccountDropdown.vue │ ├── AccountDropdownButton.vue │ ├── Header.vue │ ├── HelpModal.vue │ ├── MobileAccountNavigation.vue │ ├── MobileMainNavigation.vue │ ├── MobileNavigation.vue │ └── NetworkDropdown.vue ├── icons │ ├── Discord.vue │ ├── Era.vue │ ├── Ethereum.vue │ ├── GitHub.vue │ ├── Twitter.vue │ └── zkSync.vue ├── loaders │ └── Connecting.vue ├── modal │ ├── ConnectingWalletError.vue │ ├── LegalNotice.vue │ ├── NetworkChangedWarning.vue │ └── transaction │ │ ├── DepositUnavailable.vue │ │ └── WithdrawalUnavailable.vue ├── network │ ├── DeprecationAlert.vue │ └── NetworkSelectModal.vue ├── page │ ├── BackButton.vue │ └── Title.vue ├── token │ ├── TokenBalance.vue │ ├── TokenBalanceLoader.vue │ ├── TokenImage.vue │ ├── TokenLine.vue │ └── TokenSelectModal.vue ├── transaction │ ├── CustomBridge.vue │ ├── EthereumTransactionFooter.vue │ ├── FeeDetails.vue │ ├── TransactionFooter.vue │ ├── TransactionHashButton.vue │ ├── TransactionProgress.vue │ ├── TransferLineItem.vue │ ├── TransferWithdrawalLineItem.vue │ ├── WithdrawalsAvailableForClaimAlert.vue │ ├── buttonUnderline │ │ ├── ConfirmTransaction.vue │ │ └── ContinueInWallet.vue │ ├── lineItem │ │ ├── TokenAmount.vue │ │ ├── TotalPrice.vue │ │ └── TransactionLineItem.vue │ └── summary │ │ ├── AddressEntry.vue │ │ └── TokenEntry.vue └── typography │ └── CategoryLabel.vue ├── composables ├── transaction │ └── useAllowance.ts ├── useColorMode.ts ├── useCopy.ts ├── useEnsName.ts ├── useInterval.ts ├── useIsBeforeDate.ts ├── useObservable.ts ├── usePortalRuntimeConfig.ts ├── usePromise.ts ├── useScreening.ts ├── useSentryLogger.ts ├── useSingleLoading.ts ├── useStaticConfig.ts ├── useTimedCache.ts └── zksync │ ├── deposit │ ├── useEcosystemBanner.ts │ ├── useFee.ts │ └── useTransaction.ts │ ├── useFee.ts │ ├── usePaginatedRequest.ts │ ├── useTransaction.ts │ └── useWithdrawalFinalization.ts ├── data ├── customBridgeTokens.ts ├── meta.ts ├── networks.ts ├── wagmi.ts └── wallets.ts ├── error.vue ├── firebase.json ├── hyperchains ├── .gitignore ├── README.md ├── config.json └── example.config.json ├── layouts └── default.vue ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── pages ├── assets.vue ├── balances.vue ├── bridge │ ├── index.vue │ └── withdraw.vue ├── index.vue ├── on-ramp │ └── index.vue ├── receive-methods.vue ├── receive.vue ├── send-methods.vue ├── send.vue ├── transaction │ └── [hash].vue └── transfers.vue ├── plugins ├── alwaysRun.client.ts ├── qr.client.ts ├── redirects.client.ts ├── sentry.client.ts └── tooltip.client.ts ├── public ├── config.js ├── icon.png ├── img │ ├── banxa.svg │ ├── binance.svg │ ├── ecosystem │ │ ├── 1inch.svg │ │ ├── clave.svg │ │ ├── gravity.svg │ │ ├── layerswap.svg │ │ ├── lido.svg │ │ ├── mase.svg │ │ ├── pudgy_penguin.svg │ │ ├── quarkid.svg │ │ ├── ramp.svg │ │ ├── rocket_pool.svg │ │ └── uniswap.svg │ ├── era.svg │ ├── eth.svg │ ├── ethereum.svg │ ├── ezkalibur.svg │ ├── faucet.svg │ ├── izumi.svg │ ├── maverick-protocol.svg │ ├── moonpay.svg │ ├── multichain.svg │ ├── mute.svg │ ├── orbiter.svg │ ├── ramp.svg │ ├── rhino.svg │ ├── spacefi.svg │ ├── symbiosis.svg │ ├── syncswap.svg │ ├── txsync.png │ ├── velocore.svg │ ├── vesync.svg │ ├── zigzag.svg │ └── zksync-lite.svg ├── logo.svg └── preview.png ├── scripts ├── create-release-assets.sh ├── hyperchains │ ├── common.ts │ ├── configure.ts │ ├── create.ts │ ├── empty-check.ts │ └── utils.ts ├── tsconfig.json └── updateBridgeMetaTags.ts ├── store ├── contacts.ts ├── destinations.ts ├── ens.ts ├── ethereumBalance.ts ├── network.ts ├── on-ramp │ ├── on-ramp.ts │ ├── order-processing.ts │ ├── quotes.ts │ └── routes.ts ├── onboard.ts ├── preferences.ts └── zksync │ ├── ethereumBalance.ts │ ├── provider.ts │ ├── tokens.ts │ ├── transactionStatus.ts │ ├── transfersHistory.ts │ ├── wallet.ts │ └── withdrawals.ts ├── tailwind.config.js ├── tests └── e2e │ ├── README.md │ ├── cucumber.mjs │ ├── features │ ├── actions │ │ └── mainPage │ │ │ ├── actions-menu.feature │ │ │ └── faucet │ │ │ └── actions-faucet.feature │ ├── artifacts │ │ ├── assetsPage │ │ │ ├── artifacts-emptyWallet.feature │ │ │ └── artifacts-richWallet.feature │ │ ├── bridgePage │ │ │ └── artifacts.feature │ │ ├── contactsPage │ │ │ └── artifacts.feature │ │ ├── depositPage │ │ │ └── artifacts-deposits.feature │ │ ├── mainPage │ │ │ ├── artifacts-404.feature │ │ │ ├── artifacts-header.feature │ │ │ ├── artifacts-menuitems.feature │ │ │ └── artifacts-upperNavigarionMenu.feature │ │ ├── swapPage │ │ │ └── artifacts-swap.feature │ │ ├── transactionsPage │ │ │ └── artifacts-transactions.feature │ │ ├── transferPage │ │ │ └── atrifacts-transfers.feature │ │ ├── whereToSendPage │ │ │ └── artifacts.feature │ │ └── withdrawPage │ │ │ └── artifacts-withdraw.feature │ ├── contacts │ │ └── сontacts.feature │ ├── navigation │ │ └── navigation.feature │ ├── redirection │ │ ├── depositPage │ │ │ └── redirection.feature │ │ ├── loginPage │ │ │ └── redirection.feature │ │ └── mainPage │ │ │ └── redirection.feature │ └── transactions │ │ ├── deposit │ │ ├── bridge-deposit-with-blockchain.feature │ │ ├── deposit-empty-wallet.feature │ │ ├── deposit-no-blockchain.feature │ │ ├── deposit-with-blockchain.feature │ │ └── deposit-with-revoke.feature │ │ ├── transfer.feature │ │ └── withdraw.feature │ ├── package.json │ ├── src │ ├── data │ │ └── data.ts │ ├── helpers │ │ └── helper.ts │ ├── pages │ │ ├── base.page.ts │ │ ├── contacts.page.ts │ │ ├── login.page.ts │ │ ├── main.page.ts │ │ ├── metamask.page.ts │ │ └── revoke.page.ts │ ├── steps │ │ └── portal.steps.ts │ └── support │ │ ├── common-hooks.ts │ │ ├── config.ts │ │ ├── custom-world.ts │ │ └── reporters │ │ └── allure-reporter.js │ ├── tsconfig.json │ └── utils │ ├── metamaskDownloader.mjs │ └── metamaskId.json ├── tsconfig.json ├── types ├── dompurify.d.ts └── index.d.ts ├── utils ├── analytics.ts ├── constants.ts ├── doc-links.ts ├── formatters.ts ├── helpers.ts ├── logger.ts ├── mappers.ts ├── sentry-logger.ts └── transitions.ts └── views ├── on-ramp ├── ActiveTransactionsAlert.vue ├── CompletedView.vue ├── FormView.vue ├── LoadingTransition.vue ├── MiddlePanel.vue ├── ProcessStatusIcon.vue ├── ProcessingView.vue ├── QuoteFilter.vue ├── QuotePreview.vue ├── QuotesList.vue ├── SelectTokenModal.vue └── TransactionsView.vue └── transactions ├── Deposit.vue ├── DepositSubmitted.vue ├── Receive.vue ├── Transfer.vue ├── TransferSubmitted.vue └── WithdrawalSubmitted.vue /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ] 5 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | WALLET_CONNECT_PROJECT_ID=373a8744ceaac00934aec708a3fceea6 2 | ANKR_TOKEN= 3 | SENTRY_DSN= 4 | SENTRY_ENV=localhost # "localhost" | "production" | "development" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .idea 4 | .nuxt 5 | .nitro 6 | .cache 7 | .output 8 | dist 9 | tests 10 | tailwind.config.js -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ["@nuxtjs/eslint-config-typescript", "@vue/eslint-config-prettier"], 4 | rules: { 5 | semi: ["error", "always"], // Require semicolons 6 | quotes: ["error", "double"], // Require double quotes 7 | 8 | "import/order": [ 9 | "error", 10 | { 11 | groups: [["builtin", "external"], ["internal"], ["sibling", "parent"], "index", "object", "type"], 12 | "newlines-between": "always", 13 | alphabetize: { 14 | order: "asc", 15 | caseInsensitive: true, 16 | }, 17 | }, 18 | ], 19 | 20 | "vue/multi-word-component-names": "off", // Allow multi-word component names 21 | "vue/require-default-prop": "off", // Allow props without default values 22 | "vue/no-multiple-template-root": "off", // Allow multiple root elements in templates 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "zksync-dapp-wallet-v2" 4 | }, 5 | "targets": { 6 | "zksync-dapp-wallet-v2": { 7 | "hosting": { 8 | "web": [ 9 | "zksync-dapp-wallet-v2" 10 | ] 11 | } 12 | }, 13 | "staging-zksync-dapp-wallet-v2": { 14 | "hosting": { 15 | "web": [ 16 | "staging-zksync-dapp-wallet-v2" 17 | ] 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/feature.yml: -------------------------------------------------------------------------------- 1 | name: Deploy To Feature Branch 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | outputs: 10 | dappUrl: ${{ steps.deploy.outputs.details_url }} 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '20' 21 | cache: 'npm' 22 | 23 | - name: Install dependencies 24 | run: | 25 | npm ci --force 26 | 27 | - name: Lint 28 | run: | 29 | npm run lint 30 | 31 | - name: Setup .env 32 | run: | 33 | echo "WALLET_CONNECT_PROJECT_ID=${{ secrets.WALLET_CONNECT_PROJECT_ID }}" > .env 34 | echo "ANKR_TOKEN=${{ secrets.ANKR_TOKEN }}" >> .env 35 | echo "SCREENING_API_URL=${{ secrets.SCREENING_API_URL }}" >> .env 36 | echo "DATAPLANE_URL=${{ secrets.DATAPLANE_URL }}" >> .env 37 | echo "RUDDER_KEY=${{ secrets.RUDDER_KEY }}" >> .env 38 | echo "ONRAMP_STAGING=true" >> .env 39 | 40 | - name: Build 41 | run: | 42 | npm run generate 43 | 44 | - name: Deploy 45 | uses: matter-labs/action-hosting-deploy@main 46 | id: deploy 47 | with: 48 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 49 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_STAGING_ZKSYNC_DAPP_WALLET_V2 }}" 50 | projectId: staging-zksync-dapp-wallet-v2 51 | expires: 7d 52 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | label: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v4 16 | with: 17 | subjectPattern: ^(?![A-Z]).+$ 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/production.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Package to Production 2 | "on": 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | type: string 7 | description: "A release version to deploy, e.g. v1.0.0" 8 | required: true 9 | default: "v1.0.0" 10 | 11 | jobs: 12 | build_and_deploy: 13 | name: Build and Deploy 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | fetch-tags: true 21 | ref: refs/tags/${{ github.event.inputs.version }} 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: '20' 27 | cache: 'npm' 28 | 29 | - name: Install dependencies 30 | run: | 31 | npm ci --force 32 | 33 | - name: Setup .env 34 | run: | 35 | echo "WALLET_CONNECT_PROJECT_ID=${{ secrets.WALLET_CONNECT_PROJECT_ID }}" > .env 36 | echo "ANKR_TOKEN=${{ secrets.ANKR_TOKEN }}" >> .env 37 | echo "SCREENING_API_URL=${{ secrets.SCREENING_API_URL }}" >> .env 38 | echo "DATAPLANE_URL=${{ secrets.DATAPLANE_URL }}" >> .env 39 | echo "RUDDER_KEY=${{ secrets.RUDDER_KEY }}" >> .env 40 | 41 | - name: Build 42 | run: | 43 | npm run generate 44 | 45 | - name: Deploy to Production 46 | uses: matter-labs/action-hosting-deploy@main 47 | with: 48 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 49 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_ZKSYNC_DAPP_WALLET_V2 }}" 50 | projectId: zksync-dapp-wallet-v2 51 | channelID: live 52 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # What :computer: 2 | 3 | - First thing updated with this PR 4 | - Second thing updated with this PR 5 | - Third thing updated with this PR 6 | 7 | # Why :hand: 8 | 9 | - Reason why the first thing was added to PR 10 | - Reason why the second thing was added to PR 11 | - Reason why the third thing was added to PR 12 | 13 | # Evidence :camera: 14 | 15 | Include screenshots, screen recordings, or `console` output here demonstrating that your changes work as intended. 16 | 17 | # Testing User Flows 18 | 19 | Ensure the following user flows are tested and verified: 20 | 21 | - [ ] Users can bridge funds from L1 - L2, or L2 - L1 successfully. 22 | - [ ] Users can view correct asset balances. 23 | - [ ] Users can view an accurate history of transfers. 24 | - [ ] Users can send assets to another address. 25 | -------------------------------------------------------------------------------- /.github/workflows/secrets_scanner.yaml: -------------------------------------------------------------------------------- 1 | name: Leaked Secrets Scan 2 | on: [pull_request] 3 | jobs: 4 | TruffleHog: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout code 8 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 9 | with: 10 | fetch-depth: 0 11 | - name: TruffleHog OSS 12 | uses: trufflesecurity/trufflehog@0c66d30c1f4075cee1aada2e1ab46dabb1b0071a 13 | with: 14 | path: ./ 15 | base: ${{ github.event.repository.default_branch }} 16 | head: HEAD 17 | extra_args: --debug --only-verified 18 | -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Package to Preview 2 | "on": 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | type: string 7 | description: "A release version to deploy, e.g. v1.0.0" 8 | required: true 9 | default: "v1.0.0" 10 | 11 | jobs: 12 | deploy: 13 | name: Deploy 14 | runs-on: ubuntu-latest 15 | outputs: 16 | dappUrl: ${{ steps.deploy.outputs.details_url }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Download Dist package 24 | uses: dsaltares/fetch-gh-release-asset@master 25 | with: 26 | version: "tags/${{ github.event.inputs.version }}" 27 | file: "dist.zip" 28 | target: "dist.zip" 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Unzip Dist package 32 | run: | 33 | unzip dist.zip 34 | 35 | - name: Deploy 36 | id: deploy 37 | uses: matter-labs/action-hosting-deploy@main 38 | with: 39 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 40 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_ZKSYNC_DAPP_WALLET_V2 }}" 41 | expires: 7d 42 | projectId: zksync-dapp-wallet-v2 43 | channelID: ${{ github.event.inputs.version }} 44 | 45 | e2e: 46 | needs: deploy 47 | uses: ./.github/workflows/e2e.yml 48 | name: E2E 49 | secrets: inherit 50 | with: 51 | targetUrl: ${{ needs.deploy.outputs.dappUrl }} 52 | network: Sepolia 53 | publish_to_allure: true 54 | environmentTags: "" 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .idea 4 | .nuxt 5 | .nitro 6 | .cache 7 | .output 8 | dist 9 | dist-node-memory 10 | dist-node-docker 11 | .DS_Store 12 | tests/e2e/src/support/extension 13 | tests/e2e/reports 14 | tests/e2e/artifacts 15 | allure-results 16 | .env.local 17 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "+([0-9])?(.{+([0-9]),x}).x", 4 | "main", 5 | "next", 6 | "next-major", 7 | { 8 | "name": "beta", 9 | "prerelease": true 10 | }, 11 | { 12 | "name": "alpha", 13 | "prerelease": true 14 | } 15 | ], 16 | "plugins": [ 17 | "@semantic-release/commit-analyzer", 18 | "@semantic-release/release-notes-generator", 19 | "@semantic-release/changelog", 20 | [ 21 | "@semantic-release/npm", 22 | { 23 | "npmPublish": false 24 | } 25 | ], 26 | [ 27 | "@semantic-release/exec", 28 | { 29 | "prepareCmd": "VITE_VERSION=${nextRelease.version} sh scripts/create-release-assets.sh", 30 | "publishCmd": "zip -r dist.zip dist && zip -r dist-node-memory.zip dist-node-memory && zip -r dist-node-docker.zip dist-node-docker", 31 | "successCmd": "echo '::set-output name=releaseVersion::${nextRelease.version}'" 32 | } 33 | ], 34 | [ 35 | "@semantic-release/github", 36 | { 37 | "assets": [ 38 | { 39 | "path": "dist.zip", 40 | "label": "Dist package" 41 | }, 42 | { 43 | "path": "dist-node-memory.zip", 44 | "label": "In-memory node dist package" 45 | }, 46 | { 47 | "path": "dist-node-docker.zip", 48 | "label": "Dockerized node dist package" 49 | } 50 | ] 51 | } 52 | ] 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.17.1-alpine AS base-stage 2 | 3 | WORKDIR /usr/src/app 4 | COPY package*.json ./ 5 | 6 | FROM base-stage AS build-stage 7 | RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/* 8 | RUN npm cache clean --force && npm install 9 | COPY . . 10 | RUN npm run generate 11 | 12 | FROM base-stage AS production-stage 13 | COPY --from=build-stage /usr/src/app/.output/public ./dist 14 | RUN npm i -g http-server 15 | 16 | ARG PORT=3000 17 | ENV PORT=${PORT} 18 | 19 | WORKDIR /usr/src/app/dist 20 | 21 | CMD http-server -p $PORT -c-1 --proxy="http://127.0.0.1:$PORT/index.html?" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jack Hamer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matter-labs/dapp-portal/892e2bf1cb7bedc3ce598f932b609c454b135381/assets/.DS_Store -------------------------------------------------------------------------------- /assets/css/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin interactiveStyles { 2 | &:enabled, 3 | &[interactive="true"]:not([aria-disabled="true"]), 4 | &:is(a, label):not([aria-disabled="true"]) { 5 | @content; 6 | } 7 | } -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | html.dark { 2 | @apply bg-black; 3 | } 4 | html, 5 | body { 6 | @apply min-h-full text-neutral-950 w-full bg-neutral-50 font-sans antialiased dark:bg-black dark:text-white; 7 | -webkit-text-size-adjust: 100%; 8 | 9 | .tippy-box { 10 | @apply rounded-lg text-sm; 11 | &[data-theme~="light"] { 12 | @apply bg-white bg-opacity-70 shadow backdrop-blur; 13 | 14 | &[data-placement^="top"] > .tippy-arrow:before { 15 | @apply -bottom-[8px] border-t-white/70; 16 | } 17 | &[data-placement^="bottom"] > .tippy-arrow:before { 18 | @apply -top-[8px] border-b-white/70; 19 | } 20 | } 21 | } 22 | } 23 | button, a { -webkit-tap-highlight-color: transparent; } -------------------------------------------------------------------------------- /assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matter-labs/dapp-portal/892e2bf1cb7bedc3ce598f932b609c454b135381/components/.DS_Store -------------------------------------------------------------------------------- /components/ConnectWalletBlock.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /components/EcosystemBlock.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 41 | -------------------------------------------------------------------------------- /components/address/AddressAvatar.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 38 | -------------------------------------------------------------------------------- /components/address/AddressCard.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 38 | 39 | 46 | -------------------------------------------------------------------------------- /components/address/AddressCardLoader.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /components/animations/EcosystemLogotypes.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 63 | -------------------------------------------------------------------------------- /components/common/Badge.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/common/CardWithLineButtons.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /components/common/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 44 | -------------------------------------------------------------------------------- /components/common/CheckboxWithText.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | 38 | 54 | -------------------------------------------------------------------------------- /components/common/ContentBlock.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 33 | -------------------------------------------------------------------------------- /components/common/ContentLoader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /components/common/EmptyBlock.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /components/common/ErrorBlock.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | 26 | 39 | -------------------------------------------------------------------------------- /components/common/HeightTransition.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | 34 | -------------------------------------------------------------------------------- /components/common/ImageLoader.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 36 | 37 | 56 | -------------------------------------------------------------------------------- /components/common/LineButtonsGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 | 37 | -------------------------------------------------------------------------------- /components/common/QrAddressInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 45 | -------------------------------------------------------------------------------- /components/common/QrCodeStyled.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 33 | -------------------------------------------------------------------------------- /components/common/QrUploadIconButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /components/common/Spinner.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | 27 | 42 | -------------------------------------------------------------------------------- /components/common/Timer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 77 | -------------------------------------------------------------------------------- /components/common/TotalBalance.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 44 | 45 | 55 | -------------------------------------------------------------------------------- /components/common/button/ButtonBack.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /components/common/button/ButtonGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/common/button/Dropdown.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 29 | 30 | 49 | 50 | 61 | -------------------------------------------------------------------------------- /components/common/button/Label.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 | 45 | -------------------------------------------------------------------------------- /components/common/button/Line.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 | 80 | -------------------------------------------------------------------------------- /components/common/button/LineBodyInfo.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /components/common/button/LineWithImg.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 31 | 32 | 53 | -------------------------------------------------------------------------------- /components/common/button/TopInfo.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /components/common/button/TopLink.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /components/common/button/UnderlineText.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /components/common/input/ErrorMessage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /components/common/input/InputLine.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | 27 | 37 | -------------------------------------------------------------------------------- /components/destination/DestinationItem.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 43 | 44 | 55 | -------------------------------------------------------------------------------- /components/destination/DestinationLabel.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | 32 | -------------------------------------------------------------------------------- /components/destination/IconContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /components/footer/Footer.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 45 | 46 | 59 | -------------------------------------------------------------------------------- /components/header/AccountDropdownButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | -------------------------------------------------------------------------------- /components/header/HelpModal.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 56 | -------------------------------------------------------------------------------- /components/header/MobileNavigation.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 63 | 64 | 82 | -------------------------------------------------------------------------------- /components/icons/Discord.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /components/icons/Era.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /components/icons/Ethereum.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /components/icons/GitHub.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /components/icons/Twitter.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /components/loaders/Connecting.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 37 | 38 | 43 | -------------------------------------------------------------------------------- /components/modal/ConnectingWalletError.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /components/modal/LegalNotice.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | 38 | 49 | -------------------------------------------------------------------------------- /components/modal/transaction/DepositUnavailable.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /components/modal/transaction/WithdrawalUnavailable.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /components/network/DeprecationAlert.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /components/page/BackButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /components/page/Title.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 33 | -------------------------------------------------------------------------------- /components/token/TokenBalanceLoader.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 37 | 38 | 47 | -------------------------------------------------------------------------------- /components/token/TokenImage.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 44 | 45 | 58 | -------------------------------------------------------------------------------- /components/token/TokenLine.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /components/transaction/CustomBridge.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 65 | -------------------------------------------------------------------------------- /components/transaction/WithdrawalsAvailableForClaimAlert.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /components/transaction/buttonUnderline/ConfirmTransaction.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /components/transaction/buttonUnderline/ContinueInWallet.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /components/transaction/lineItem/TokenAmount.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 44 | -------------------------------------------------------------------------------- /components/transaction/lineItem/TotalPrice.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | -------------------------------------------------------------------------------- /components/transaction/summary/AddressEntry.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 55 | 56 | 79 | -------------------------------------------------------------------------------- /components/transaction/summary/TokenEntry.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 56 | -------------------------------------------------------------------------------- /components/typography/CategoryLabel.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | 33 | 55 | -------------------------------------------------------------------------------- /composables/useColorMode.ts: -------------------------------------------------------------------------------- 1 | import { useColorMode } from "@vueuse/core"; 2 | 3 | const { store } = useColorMode({ 4 | initialValue: "dark", 5 | listenToStorageChanges: false, 6 | }); 7 | 8 | export default () => { 9 | const selectedColorMode = computed(() => (store.value === "auto" ? "dark" : store.value)); 10 | 11 | const switchColorMode = (colorMode?: "dark" | "light") => { 12 | if (colorMode) { 13 | return (store.value = colorMode); 14 | } 15 | if (selectedColorMode.value === "dark") { 16 | store.value = "light"; 17 | } else { 18 | store.value = "dark"; 19 | } 20 | }; 21 | 22 | return { 23 | selectedColorMode, 24 | switchColorMode, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /composables/useCopy.ts: -------------------------------------------------------------------------------- 1 | import { useClipboard, useThrottleFn } from "@vueuse/core"; 2 | 3 | export default (text: Ref, copiedDuring = 1000) => { 4 | const { copy: clipboardCopy, copied: isCopied } = useClipboard({ 5 | source: text, 6 | copiedDuring, 7 | }); 8 | const tooltipShownViaLegacyCopy = ref(false); 9 | 10 | const showLegacyCopyTooltip = useThrottleFn(() => { 11 | tooltipShownViaLegacyCopy.value = true; 12 | setTimeout(() => { 13 | tooltipShownViaLegacyCopy.value = false; 14 | }, copiedDuring); 15 | }, copiedDuring); 16 | 17 | const copied = computed(() => isCopied.value || tooltipShownViaLegacyCopy.value); 18 | 19 | async function copy() { 20 | try { 21 | await clipboardCopy(); 22 | } catch (error) { 23 | legacyCopy(); 24 | showLegacyCopyTooltip(); 25 | } 26 | } 27 | function legacyCopy() { 28 | const ta = document.createElement("textarea"); 29 | ta.value = text.value; 30 | ta.style.position = "absolute"; 31 | ta.style.opacity = "0"; 32 | document.body.appendChild(ta); 33 | ta.select(); 34 | document.execCommand("copy"); 35 | ta.remove(); 36 | } 37 | 38 | return { 39 | copied, 40 | copy, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /composables/useEnsName.ts: -------------------------------------------------------------------------------- 1 | import { useMemoize } from "@vueuse/core"; 2 | import { getEnsAddress } from "@wagmi/core"; 3 | 4 | import { wagmiConfig } from "@/data/wagmi"; 5 | 6 | export default (ensName: Ref) => { 7 | const fetchEnsAddress = useMemoize((name: string) => getEnsAddress(wagmiConfig, { name, chainId: 1 })); 8 | 9 | const nameToAddress = ref<{ [name: string]: string }>({}); 10 | const { 11 | inProgress, 12 | error, 13 | execute: parseEns, 14 | } = usePromise( 15 | async () => { 16 | const name = ensName.value; 17 | const result = await fetchEnsAddress(name); 18 | if (result) { 19 | nameToAddress.value[name] = result; 20 | } 21 | }, 22 | { cache: false } 23 | ); 24 | 25 | const isValidEnsFormat = computed(() => ensName.value.endsWith(".eth")); 26 | watch( 27 | ensName, 28 | async () => { 29 | if (isValidEnsFormat.value) { 30 | await parseEns(); 31 | } 32 | }, 33 | { immediate: true } 34 | ); 35 | 36 | return { 37 | address: computed(() => nameToAddress.value[ensName.value]), 38 | isValidEnsFormat, 39 | inProgress, 40 | error, 41 | parseEns, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /composables/useInterval.ts: -------------------------------------------------------------------------------- 1 | export default (fn: () => ResultType, delay: number) => { 2 | let interval: ReturnType | undefined; 3 | const currentDelay = ref(delay); 4 | 5 | function stop() { 6 | clearInterval(interval); 7 | interval = undefined; 8 | } 9 | 10 | function start() { 11 | stop(); 12 | interval = setInterval(fn, currentDelay.value); 13 | } 14 | start(); 15 | 16 | function reset() { 17 | stop(); 18 | start(); 19 | } 20 | 21 | return { 22 | stop, 23 | reset, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /composables/useIsBeforeDate.ts: -------------------------------------------------------------------------------- 1 | export default (dateRef: Ref) => { 2 | const isBefore = ref(false); 3 | let intervalId: ReturnType | undefined; 4 | 5 | const updateIsBefore = () => { 6 | if (!dateRef.value) { 7 | isBefore.value = false; 8 | reset(); 9 | return; 10 | } 11 | isBefore.value = new Date() < new Date(dateRef.value); 12 | }; 13 | 14 | const startChecking = () => { 15 | // Initial check 16 | updateIsBefore(); 17 | 18 | // Watch for changes on the date 19 | watch(dateRef, updateIsBefore); 20 | 21 | // Periodically check every second 22 | intervalId = setInterval(updateIsBefore, 1000); 23 | }; 24 | 25 | const reset = () => { 26 | if (intervalId) { 27 | clearInterval(intervalId); 28 | intervalId = undefined; 29 | } 30 | }; 31 | 32 | // Start checking when the composable is used 33 | startChecking(); 34 | 35 | // Stop checking when the component unmounts 36 | onUnmounted(reset); 37 | 38 | return { 39 | isBefore, 40 | reset, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /composables/useObservable.ts: -------------------------------------------------------------------------------- 1 | type CallbackFunction = (param: T) => void; 2 | class Observable { 3 | private subscribers: CallbackFunction[] = []; 4 | 5 | public subscribe(callback: CallbackFunction) { 6 | this.subscribers.push(callback); 7 | const unsubscribe = () => { 8 | this.unsubscribe(callback); 9 | }; 10 | return unsubscribe; 11 | } 12 | 13 | public unsubscribe(callback: CallbackFunction) { 14 | const index = this.subscribers.indexOf(callback); 15 | if (index !== -1) { 16 | this.subscribers.splice(index, 1); 17 | } 18 | } 19 | 20 | public notify(param: T) { 21 | this.subscribers.forEach((callback) => { 22 | callback(param); 23 | }); 24 | } 25 | } 26 | 27 | export default () => { 28 | const observable = new Observable(); 29 | 30 | return { 31 | subscribe: observable.subscribe.bind(observable), 32 | notify: observable.notify.bind(observable), 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /composables/usePortalRuntimeConfig.ts: -------------------------------------------------------------------------------- 1 | export const usePortalRuntimeConfig = () => { 2 | const runtimeConfig = window && window["##runtimeConfig"]; 3 | 4 | // Important: before adding new env variables, make sure to list them as public in `nuxt.config.ts` 5 | return { 6 | nodeType: runtimeConfig?.nodeType || (process.env.NODE_TYPE as undefined | "memory" | "dockerized" | "hyperchain"), 7 | walletConnectProjectId: runtimeConfig?.walletConnectProjectId || process.env.WALLET_CONNECT_PROJECT_ID, 8 | ankrToken: runtimeConfig?.ankrToken || process.env.ANKR_TOKEN, 9 | sentryDSN: runtimeConfig?.sentryDSN || process.env.SENTRY_DSN, 10 | sentryENV: runtimeConfig?.sentryENV || process.env.SENTRY_ENV, 11 | screeningApiUrl: runtimeConfig?.screeningApiUrl || process.env.SCREENING_API_URL, 12 | analytics: { 13 | rudder: runtimeConfig?.analytics?.rudder 14 | ? { 15 | key: (runtimeConfig.analytics.rudder.key || process.env.RUDDER_KEY)!, 16 | dataplaneUrl: (runtimeConfig.analytics.rudder.dataplaneUrl || process.env.DATAPLANE_URL)!, 17 | } 18 | : undefined, 19 | }, 20 | hyperchainsConfig: runtimeConfig?.hyperchainsConfig, 21 | gitCommitHash: runtimeConfig?.gitCommitHash || process.env.GIT_COMMIT_HASH, 22 | gitRepoUrl: runtimeConfig?.gitRepoUrl || process.env.GIT_REPO_URL, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /composables/useScreening.ts: -------------------------------------------------------------------------------- 1 | import { useMemoize } from "@vueuse/core"; 2 | import { $fetch } from "ofetch"; 3 | 4 | /* Returns void if address screening was successful */ 5 | /* Fails if address screening was unsuccessful */ 6 | const validateAddress = useMemoize(async (address: string) => { 7 | const portalRuntimeConfig = usePortalRuntimeConfig(); 8 | if (!portalRuntimeConfig.screeningApiUrl) return; 9 | 10 | const url = new URL(portalRuntimeConfig.screeningApiUrl); 11 | url.searchParams.append("address", address); 12 | const response = await $fetch(url.toString()).catch(() => ({ result: true })); 13 | if (!response.result) { 14 | throw new Error("We were unable to process this transaction..."); 15 | } 16 | }); 17 | 18 | export default () => { 19 | return { 20 | validateAddress, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /composables/useSentryLogger.ts: -------------------------------------------------------------------------------- 1 | import { storeToRefs } from "pinia"; 2 | 3 | import { useOnboardStore } from "@/store/onboard"; 4 | import { sentryCaptureException } from "@/utils/sentry-logger"; 5 | 6 | import type { SentryCaptureExceptionParams } from "@/utils/sentry-logger"; 7 | 8 | export const useSentryLogger = () => { 9 | const onboardStore = useOnboardStore(); 10 | const { account } = storeToRefs(onboardStore); 11 | 12 | const captureException = (params: Omit) => { 13 | sentryCaptureException({ ...params, accountAddress: account?.value?.address || "" }); 14 | }; 15 | 16 | return { captureException }; 17 | }; 18 | -------------------------------------------------------------------------------- /composables/useSingleLoading.ts: -------------------------------------------------------------------------------- 1 | export default (loading: Ref) => { 2 | const loaded = ref(false); 3 | 4 | watch( 5 | loading, 6 | (value, oldValue) => { 7 | if (value === false && oldValue === true) { 8 | loaded.value = true; 9 | } 10 | }, 11 | { immediate: true } 12 | ); 13 | 14 | const reset = () => { 15 | loaded.value = false; 16 | }; 17 | 18 | return { 19 | loading: computed(() => { 20 | if (loaded.value) { 21 | return false; 22 | } 23 | return loading.value; 24 | }), 25 | reset, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /composables/useStaticConfig.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matter-labs/dapp-portal/892e2bf1cb7bedc3ce598f932b609c454b135381/composables/useStaticConfig.ts -------------------------------------------------------------------------------- /composables/useTimedCache.ts: -------------------------------------------------------------------------------- 1 | export default function useTimedCache( 2 | fn: (...args: ParamsType) => Promise, 3 | cacheTime: number 4 | ) { 5 | let cache: { 6 | params: ParamsType; 7 | promise: Promise; 8 | timestamp: number; 9 | }; 10 | 11 | return (...args: ParamsType): Promise => { 12 | const now = Date.now(); 13 | if (cache && now - cache.timestamp < cacheTime && JSON.stringify(cache.params) === JSON.stringify(args)) { 14 | return cache.promise; 15 | } else { 16 | const promise = fn(...args); 17 | cache = { 18 | params: args, 19 | promise, 20 | timestamp: now, 21 | }; 22 | return promise; 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /composables/zksync/deposit/useEcosystemBanner.ts: -------------------------------------------------------------------------------- 1 | import { useSessionStorage } from "@vueuse/core"; 2 | 3 | export default () => { 4 | const recentlyBridged = useSessionStorage("portal-recently-bridged", false); 5 | const ecosystemBannerClosed = useSessionStorage("portal-ecosystem-banner-closed", false); 6 | const ecosystemBannerVisible = computed(() => !ecosystemBannerClosed.value && recentlyBridged.value); 7 | 8 | return { 9 | recentlyBridged, 10 | ecosystemBannerClosed, 11 | ecosystemBannerVisible, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /composables/zksync/usePaginatedRequest.ts: -------------------------------------------------------------------------------- 1 | import type { Api } from "@/types"; 2 | 3 | export default (resource: () => URL) => { 4 | const meta = ref["meta"] | undefined>(); 5 | const links = ref["links"] | undefined>(); 6 | 7 | const loadNext = async () => { 8 | const url = links.value?.next ? new URL(links.value.next, resource().origin) : resource(); 9 | const response = await $fetch>(url.toString()); 10 | meta.value = response.meta; 11 | links.value = response.links; 12 | return response; 13 | }; 14 | 15 | return { 16 | meta, 17 | canLoadMore: computed(() => !!links.value?.next), 18 | loadNext, 19 | reset: () => { 20 | meta.value = undefined; 21 | links.value = undefined; 22 | }, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /data/customBridgeTokens.ts: -------------------------------------------------------------------------------- 1 | export type CustomBridgeToken = { 2 | chainId: number; 3 | l1Address: string; 4 | l2Address: string; 5 | symbol: string; 6 | bridgedSymbol: string; 7 | name?: string; 8 | bridgingDisabled?: true; 9 | learnMoreUrl?: string; 10 | bridges: { 11 | label: string; 12 | iconUrl: string; 13 | depositUrl?: string; 14 | withdrawUrl?: string; 15 | }[]; 16 | }; 17 | 18 | export const customBridgeTokens: CustomBridgeToken[] = [ 19 | { 20 | chainId: 1, 21 | l1Address: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", 22 | l2Address: "0x703b52F2b28fEbcB60E1372858AF5b18849FE867", 23 | bridges: [ 24 | { 25 | label: "txSync Bridge", 26 | iconUrl: "/img/txsync.png", 27 | depositUrl: "https://portal.txsync.io/bridge/?token=0x703b52F2b28fEbcB60E1372858AF5b18849FE867", 28 | withdrawUrl: "https://portal.txsync.io/bridge/withdraw/?token=0x703b52F2b28fEbcB60E1372858AF5b18849FE867", 29 | }, 30 | ], 31 | symbol: "wstETH", 32 | bridgedSymbol: "wstETH", 33 | name: "Wrapped liquid staked Ether 2.0", 34 | bridgingDisabled: true, 35 | }, 36 | { 37 | chainId: 1, 38 | l1Address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 39 | l2Address: "0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4", 40 | learnMoreUrl: "https://www.circle.com/blog/native-usdc-now-available-on-zksync", 41 | bridges: [], 42 | symbol: "USDC", 43 | bridgedSymbol: "USDC.e", 44 | name: "USD Coin", 45 | }, 46 | ]; 47 | -------------------------------------------------------------------------------- /data/meta.ts: -------------------------------------------------------------------------------- 1 | export const portal = { 2 | title: "ZKsync Portal | View balances, transfer and bridge tokens", 3 | description: 4 | "ZKsync Portal allows you to view your balances, transfer tokens and bridge assets between ZKsync and Ethereum", 5 | previewImg: { 6 | src: "https://portal.zksync.io/preview.png", 7 | alt: "ZKsync Portal", 8 | }, 9 | }; 10 | export const bridge = { 11 | title: "ZKsync Bridge | Transfer funds between ZKsync and Ethereum", 12 | description: 13 | "With the ZKsync Bridge you can easily deposit tokens to ZKsync. Enjoy faster, cheaper and more efficient transactions with the future proof zkEVM scaling Ethereum's security and values.", 14 | previewImg: { 15 | src: "https://portal.zksync.io/preview.png", 16 | alt: "ZKsync Bridge", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /data/wallets.ts: -------------------------------------------------------------------------------- 1 | export const confirmedSupportedWallets = ["BitKeep", "BlockWallet", "MathWallet", "imToken"]; 2 | export const disabledWallets = []; 3 | -------------------------------------------------------------------------------- /error.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | 40 | 58 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": [ 3 | { 4 | "target": "web", 5 | "public": "dist", 6 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**", "**/*.map"], 7 | "rewrites": [ 8 | { 9 | "source": "**", 10 | "destination": "/index.html" 11 | } 12 | ], 13 | "headers": [ 14 | { 15 | "source": "**", 16 | "headers": [ 17 | { 18 | "key": "Cache-Control", 19 | "value": "no-cache, no-store, must-revalidate" 20 | }, 21 | { 22 | "key": "Referrer-Policy", 23 | "value": "no-referrer, strict-origin-when-cross-origin" 24 | }, 25 | { 26 | "key": "X-Content-Type-Options", 27 | "value": "nosniff" 28 | }, 29 | { 30 | "key": "X-Frame-Options", 31 | "value": "DENY" 32 | }, 33 | { 34 | "key": "X-XSS-Protection", 35 | "value": "1; mode=block" 36 | } 37 | ] 38 | } 39 | ], 40 | "redirects": [ 41 | { 42 | "source": "https://sepolia.staging-portal.zksync.dev/*", 43 | "destination": "https://staging-portal.zksync.dev/?network=sepolia", 44 | "type": 301 45 | }, 46 | { 47 | "source": "https://sepolia.portal.zksync.io/*", 48 | "destination": "https://portal.zksync.io/?network=sepolia", 49 | "type": 301 50 | }, 51 | { 52 | "source": "https://goerli.staging-portal.zksync.dev/*", 53 | "destination": "https://staging-portal.zksync.dev/?network=sepolia", 54 | "type": 301 55 | }, 56 | { 57 | "source": "https://goerli.portal.zksync.io/*", 58 | "destination": "https://portal.zksync.io/?network=sepolia", 59 | "type": 301 60 | } 61 | ] 62 | } 63 | ], 64 | "emulators": { 65 | "hosting": { 66 | "host": "localhost", 67 | "port": "3000" 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /hyperchains/.gitignore: -------------------------------------------------------------------------------- 1 | *.env 2 | *.json 3 | !config.json 4 | !example.config.json -------------------------------------------------------------------------------- /hyperchains/config.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hyperchains/example.config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "network": { 4 | "id": 1234, 5 | "key": "my-hyperchain", 6 | "name": "My new hyperchain", 7 | "rpcUrl": "http://127.0.0.1:3050", 8 | "l1Network": { 9 | "id": 9, 10 | "name": "Localhost", 11 | "network": "localhost", 12 | "nativeCurrency": { "name": "Ether", "symbol": "ETH", "decimals": 18 }, 13 | "rpcUrls": { 14 | "default": { "http": ["http://localhost:8545"] }, 15 | "public": { "http": ["http://localhost:8545"] } 16 | } 17 | } 18 | }, 19 | "tokens": [ 20 | { 21 | "address": "0x000000000000000000000000000000000000800A", 22 | "l1Address": "0x0000000000000000000000000000000000000000", 23 | "symbol": "ETH", 24 | "decimals": 18, 25 | "iconUrl": "/img/eth.svg" 26 | } 27 | ] 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 19 | 20 | 32 | -------------------------------------------------------------------------------- /pages/bridge/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /pages/bridge/withdraw.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /pages/receive.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /pages/send-methods.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /pages/send.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /pages/transaction/[hash].vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 51 | -------------------------------------------------------------------------------- /plugins/alwaysRun.client.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtPlugin(() => { 2 | useColorMode(); 3 | 4 | const router = useRouter(); 5 | router.onError((error, to) => { 6 | // Happens when new version is deployed and user has active session on the old version 7 | if (error?.message?.includes("Failed to fetch dynamically imported module")) { 8 | const win: Window = window; // ts error hack: https://github.com/microsoft/TypeScript/issues/48949 9 | win.location = to.fullPath; 10 | } 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /plugins/qr.client.ts: -------------------------------------------------------------------------------- 1 | import VueQrcode from "@chenfengyuan/vue-qrcode"; 2 | 3 | export default defineNuxtPlugin((nuxtApp) => { 4 | nuxtApp.vueApp.component("QrCode", VueQrcode); 5 | }); 6 | -------------------------------------------------------------------------------- /plugins/redirects.client.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtPlugin(() => { 2 | const currentUrl = new URL(window.location.href); 3 | const redirectNetworkKeys = ["goerli", "sepolia"]; 4 | const deprecatedNetworkKeys = ["goerli"]; 5 | for (const network of redirectNetworkKeys) { 6 | if (currentUrl.origin === `https://${network}.portal.zksync.io`) { 7 | const newUrl = new URL(currentUrl.href); 8 | newUrl.hostname = "portal.zksync.io"; 9 | if (deprecatedNetworkKeys.includes(network)) { 10 | newUrl.searchParams.set( 11 | "network", 12 | redirectNetworkKeys.filter((key) => !deprecatedNetworkKeys.includes(key))[0] 13 | ); 14 | } else { 15 | newUrl.searchParams.set("network", network); 16 | } 17 | navigateTo(newUrl.href, { 18 | external: true, 19 | }); 20 | break; 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /plugins/sentry.client.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/vue"; 2 | 3 | export default defineNuxtPlugin((nuxtApp) => { 4 | const { vueApp } = nuxtApp; 5 | 6 | const config = useRuntimeConfig(); 7 | 8 | const sentryDSN = (config.public.sentryDSN as string) || ""; 9 | 10 | if (!sentryDSN) { 11 | return; 12 | } 13 | 14 | const sentryENV = (config.public.sentryENV as string) || "localhost"; 15 | 16 | Sentry.init({ 17 | app: [vueApp], 18 | dsn: sentryDSN, 19 | environment: sentryENV, 20 | integrations: [ 21 | Sentry.browserTracingIntegration(), 22 | Sentry.replayIntegration({ 23 | maskAllText: false, 24 | blockAllMedia: false, 25 | maskAllInputs: false, 26 | }), 27 | Sentry.captureConsoleIntegration({ 28 | handled: true, 29 | }), 30 | ], 31 | tracesSampleRate: 1.0, 32 | replaysSessionSampleRate: config.public.sentryENV === "production" ? 0.1 : 1.0, 33 | replaysOnErrorSampleRate: 1.0, 34 | }); 35 | 36 | vueApp.mixin( 37 | Sentry.createTracingMixins({ trackComponents: true, timeout: 2000, hooks: ["activate", "mount", "update"] }) 38 | ); 39 | Sentry.attachErrorHandler(vueApp, { 40 | attachProps: true, 41 | attachErrorHandler: true, 42 | }); 43 | 44 | return { 45 | provide: { 46 | sentrySetContext: Sentry.setContext, 47 | sentrySetUser: Sentry.setUser, 48 | sentrySetTag: Sentry.setTag, 49 | sentryAddBreadcrumb: Sentry.addBreadcrumb, 50 | sentryCaptureException: Sentry.captureException, 51 | }, 52 | }; 53 | }); 54 | -------------------------------------------------------------------------------- /plugins/tooltip.client.ts: -------------------------------------------------------------------------------- 1 | import { plugin as VueTippy } from "vue-tippy"; 2 | import "tippy.js/dist/tippy.css"; 3 | import "tippy.js/themes/light.css"; 4 | 5 | export default defineNuxtPlugin((nuxtApp) => { 6 | nuxtApp.vueApp.use(VueTippy, { 7 | directive: "tooltip", 8 | defaultProps: { 9 | placement: "top", 10 | theme: "light", 11 | }, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /public/config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matter-labs/dapp-portal/892e2bf1cb7bedc3ce598f932b609c454b135381/public/config.js -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matter-labs/dapp-portal/892e2bf1cb7bedc3ce598f932b609c454b135381/public/icon.png -------------------------------------------------------------------------------- /public/img/banxa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /public/img/binance.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/img/ecosystem/clave.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/img/ecosystem/layerswap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 14 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /public/img/ecosystem/mase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/img/era.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/img/eth.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/ethereum.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/img/faucet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/img/izumi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/img/maverick-protocol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/img/moonpay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /public/img/multichain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/img/mute.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/img/ramp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 15 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /public/img/spacefi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/img/syncswap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /public/img/txsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matter-labs/dapp-portal/892e2bf1cb7bedc3ce598f932b609c454b135381/public/img/txsync.png -------------------------------------------------------------------------------- /public/img/vesync.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/img/zksync-lite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matter-labs/dapp-portal/892e2bf1cb7bedc3ce598f932b609c454b135381/public/preview.png -------------------------------------------------------------------------------- /scripts/create-release-assets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Ensure the script stops if any command fails 4 | set -e 5 | 6 | # Run the final npm command 7 | npm run generate 8 | 9 | # Run the first npm command and move folder 10 | cp -r .output/public/ ./dist-node-memory/ 11 | 12 | # Run the second npm command and move folder 13 | cp -r .output/public/ ./dist-node-docker/ -------------------------------------------------------------------------------- /scripts/hyperchains/common.ts: -------------------------------------------------------------------------------- 1 | import { mainnet, sepolia } from "viem/chains"; 2 | 3 | import type { ZkSyncNetwork } from "../../data/networks"; 4 | import type { Token } from "../../types"; 5 | 6 | export type Network = Omit; 7 | export type Config = { network: Network; tokens: Token[] }[]; 8 | 9 | export const PUBLIC_L1_CHAINS = [mainnet, sepolia]; 10 | -------------------------------------------------------------------------------- /scripts/hyperchains/empty-check.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { readFileSync } from "fs"; 3 | import { join as pathJoin } from "path"; 4 | 5 | const configPath = pathJoin(__dirname, "../../hyperchains/config.json"); 6 | const chains = JSON.parse(readFileSync(configPath).toString()); 7 | if (!chains.length) { 8 | console.error("No networks found in hyperchains config file"); 9 | console.error("refer to the instructions in /hyperchains/README.md"); 10 | process.exit(1); 11 | } 12 | -------------------------------------------------------------------------------- /scripts/hyperchains/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { prompt } from "enquirer"; 3 | import { readFileSync, writeFileSync } from "fs"; 4 | import { join as pathJoin } from "path"; 5 | 6 | import type { Network, Config } from "./common"; 7 | import type { Token } from "../../types"; 8 | 9 | export const configPath = pathJoin(__dirname, "../../hyperchains/config.json"); 10 | const getConfig = (): Config => { 11 | return JSON.parse(readFileSync(configPath).toString()); 12 | }; 13 | const saveConfig = (config: Config) => { 14 | return writeFileSync(configPath, JSON.stringify(config, null, 2)); 15 | }; 16 | 17 | export const promptNetworkReplacement = async (network: Network) => { 18 | const config = getConfig(); 19 | 20 | if (config.find((e) => e.network.key === network.key)) { 21 | const { sameNetworkAction }: { sameNetworkAction: "replace" | "add-as-copy" } = await prompt([ 22 | { 23 | message: "Network with the same key found in the config, how do you want to proceed?", 24 | name: "sameNetworkAction", 25 | type: "select", 26 | choices: [ 27 | { message: "Replace", name: "replace" }, 28 | { message: `Add as "${network.key}-copy"`, name: "add-as-copy" }, 29 | ], 30 | }, 31 | ]); 32 | 33 | if (sameNetworkAction === "add-as-copy") { 34 | network.key = `${network.key}-copy`; 35 | } else if (sameNetworkAction === "replace") { 36 | config.splice( 37 | config.findIndex((e) => e.network.key === network.key), 38 | 1 39 | ); 40 | saveConfig(config); 41 | } 42 | } 43 | }; 44 | 45 | export const generateNetworkConfig = (network: Network, tokens: Token[]) => { 46 | const config = getConfig(); 47 | 48 | config.unshift({ network, tokens }); 49 | saveConfig(config); 50 | }; 51 | 52 | export const logUserInfo = () => { 53 | console.log("\nConfig has been generated successfully!"); 54 | console.log("You can find more info in /hyperchains/README.md\n"); 55 | 56 | console.log("You can start Portal with your new config by running"); 57 | console.log("npm run dev:node:hyperchain"); 58 | }; 59 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /scripts/updateBridgeMetaTags.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable quotes */ 3 | /* 4 | Problem: Since the build was done in SPA mode, the meta tags are same for all pages (taken from nuxt.config.ts) 5 | Solution: This script is used to replace meta tags for Bridge pages after the build was done 6 | */ 7 | import { load } from "cheerio"; 8 | import { readFile, writeFile } from "fs"; 9 | 10 | import { bridge as bridgeMeta } from "../data/meta"; 11 | 12 | const filePaths = ["./dist/index.html", "./dist/bridge/index.html", "./dist/bridge/withdraw/index.html"]; 13 | 14 | filePaths.forEach((filePath) => { 15 | readFile(filePath, "utf8", function (err, data) { 16 | if (err) { 17 | return console.log(err); 18 | } 19 | const $ = load(data); 20 | 21 | $("title").text(bridgeMeta.title); 22 | $('meta[name="apple-mobile-web-app-title"]').attr("content", bridgeMeta.title); 23 | $('meta[property="og:title"]').attr("content", bridgeMeta.title); 24 | $('meta[property="og:site_name"]').attr("content", bridgeMeta.title); 25 | $('meta[name="description"]').attr("content", bridgeMeta.description); 26 | $('meta[property="og:description"]').attr("content", bridgeMeta.description); 27 | $('meta[property="og:image:alt"]').attr("content", bridgeMeta.previewImg.alt); 28 | $('meta[property="og:image"]').attr("content", bridgeMeta.previewImg.src); 29 | 30 | writeFile(filePath, $.html(), "utf8", function (err) { 31 | if (err) return console.log(err); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /store/contacts.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from "@vueuse/core"; 2 | 3 | export type Contact = { 4 | name: string; 5 | address: string; 6 | }; 7 | 8 | export const useContactsStore = defineStore("contacts", () => { 9 | const { account } = storeToRefs(useOnboardStore()); 10 | const storageContacts = useStorage<{ [userAddress: string]: Contact[] }>("contacts", {}); 11 | 12 | const userContacts = computed({ 13 | get: () => { 14 | if (account.value.address && Array.isArray(storageContacts.value[account.value.address])) { 15 | return [...storageContacts.value[account.value.address]].sort((a, b) => a.name.localeCompare(b.name)); 16 | } 17 | return []; 18 | }, 19 | set: (contacts: Contact[]) => { 20 | if (!account.value.address) { 21 | throw new Error("Account is not available"); 22 | } 23 | storageContacts.value[account.value.address] = contacts; 24 | }, 25 | }); 26 | 27 | const userContactsByFirstCharacter = computed(() => { 28 | const contacts: { [firstCharacter: string]: Contact[] } = {}; 29 | for (const contact of userContacts.value) { 30 | const firstCharacter = contact.name.slice(0, 1).toUpperCase(); 31 | if (!contacts[firstCharacter]) { 32 | contacts[firstCharacter] = []; 33 | } 34 | contacts[firstCharacter].push(contact); 35 | } 36 | return contacts; 37 | }); 38 | 39 | const saveContact = (contact: Contact) => { 40 | if (contact.address === account.value.address) { 41 | throw new Error("Can't add own account to contacts"); 42 | } 43 | removeContact(contact.address); 44 | userContacts.value = [...userContacts.value, contact]; 45 | }; 46 | const removeContact = (contactAddress: string) => { 47 | const contactIndex = userContacts.value.map((e) => e.address).indexOf(contactAddress); 48 | if (contactIndex !== -1) { 49 | const newContacts = [...userContacts.value]; 50 | newContacts.splice(contactIndex, 1); 51 | userContacts.value = newContacts; 52 | } 53 | }; 54 | 55 | return { 56 | userContacts, 57 | userContactsByFirstCharacter, 58 | saveContact, 59 | removeContact, 60 | }; 61 | }); 62 | -------------------------------------------------------------------------------- /store/destinations.ts: -------------------------------------------------------------------------------- 1 | export type TransactionDestination = { 2 | key?: string; 3 | label: string; 4 | iconUrl: string; 5 | }; 6 | 7 | export const useDestinationsStore = defineStore("destinations", () => { 8 | const { l1Network } = storeToRefs(useNetworkStore()); 9 | const { eraNetwork } = storeToRefs(useZkSyncProviderStore()); 10 | 11 | const destinations = computed(() => ({ 12 | era: { 13 | key: "era", 14 | label: eraNetwork.value.name, 15 | iconUrl: "/img/era.svg", 16 | }, 17 | ethereum: { 18 | key: "ethereum", 19 | label: l1Network.value ? l1Network.value.name : "", 20 | iconUrl: "/img/ethereum.svg", 21 | }, 22 | layerswap: { 23 | key: "layerswap", 24 | label: "Layerswap", 25 | iconUrl: "/img/layerswap.svg", 26 | }, 27 | ramp: { 28 | key: "ramp", 29 | label: "Ramp", 30 | iconUrl: "/img/ramp.svg", 31 | }, 32 | moonpay: { 33 | key: "moonpay", 34 | label: "Moonpay", 35 | iconUrl: "/img/moonpay.svg", 36 | }, 37 | binance: { 38 | key: "binance", 39 | label: "Binance", 40 | iconUrl: "/img/binance.svg", 41 | }, 42 | zigzag: { 43 | key: "zigzag", 44 | label: "ZigZag", 45 | iconUrl: "/img/zigzag.svg", 46 | }, 47 | rhino: { 48 | key: "rhino", 49 | label: "rhino.fi", 50 | iconUrl: "/img/rhino.svg", 51 | }, 52 | })); 53 | 54 | return { 55 | destinations, 56 | }; 57 | }); 58 | -------------------------------------------------------------------------------- /store/ens.ts: -------------------------------------------------------------------------------- 1 | import { getEnsAvatar, getEnsName } from "@wagmi/core"; 2 | 3 | import { wagmiConfig } from "@/data/wagmi"; 4 | 5 | export const useEnsStore = defineStore("ens", () => { 6 | const onboardStore = useOnboardStore(); 7 | const { account } = storeToRefs(onboardStore); 8 | 9 | const ensName = ref(null); 10 | const ensAvatar = ref(null); 11 | 12 | const fetchEnsData = async () => { 13 | ensName.value = null; 14 | ensAvatar.value = null; 15 | 16 | if (!account.value.address) { 17 | return; 18 | } 19 | 20 | const initialAddress = account.value.address; 21 | const name = await getEnsName(wagmiConfig, { address: account.value.address, chainId: 1 }); 22 | if (account.value.address === initialAddress) { 23 | ensName.value = name; 24 | } else { 25 | return; 26 | } 27 | if (name) { 28 | const avatar = await getEnsAvatar(wagmiConfig, { name, chainId: 1 }).catch(() => null); 29 | if (account.value.address === initialAddress) { 30 | ensAvatar.value = avatar; 31 | } 32 | } 33 | }; 34 | 35 | fetchEnsData(); 36 | 37 | onboardStore.subscribeOnAccountChange(() => { 38 | fetchEnsData(); 39 | }); 40 | 41 | return { 42 | name: computed(() => ensName.value), 43 | avatar: computed(() => ensAvatar.value), 44 | }; 45 | }); 46 | -------------------------------------------------------------------------------- /store/on-ramp/quotes.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from "@vueuse/core"; 2 | import { defineStore } from "pinia"; 3 | import { ref } from "vue"; 4 | import { fetchQuotes as fetchSDKQuotes } from "zksync-easy-onramp"; 5 | 6 | import type { FetchQuoteParams, PaymentMethod, ProviderQuoteOption } from "zksync-easy-onramp"; 7 | 8 | export const useQuotesStore = defineStore("quotes", () => { 9 | const inProgress = ref(false); 10 | const error = ref(null); 11 | const quotes = ref(null); 12 | // const quoteFilter = ref([]); 13 | const quoteFilter = useStorage("quoteFilter", []); 14 | 15 | async function fetchQuotes(params: FetchQuoteParams) { 16 | inProgress.value = true; 17 | quotes.value = null; 18 | try { 19 | const response = await fetchSDKQuotes(params); 20 | if (!response.quotes) { 21 | throw new Error("Failed to fetch quotes"); 22 | } 23 | quotes.value = response.quotes; 24 | error.value = null; 25 | } catch (err: unknown) { 26 | if (err instanceof Error) { 27 | error.value = err; 28 | } else { 29 | error.value = new Error(String(err)); 30 | } 31 | quotes.value = []; 32 | } finally { 33 | inProgress.value = false; 34 | } 35 | } 36 | 37 | const reset = () => { 38 | quotes.value = null; 39 | }; 40 | 41 | return { 42 | inProgress, 43 | fetchQuotes, 44 | error, 45 | quotes, 46 | reset, 47 | quoteFilter, 48 | }; 49 | }); 50 | -------------------------------------------------------------------------------- /store/on-ramp/routes.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from "@vueuse/core"; 2 | import { defineStore } from "pinia"; 3 | import { computed } from "vue"; 4 | 5 | import type { Route } from "zksync-easy-onramp"; 6 | 7 | export const useRoutesStore = defineStore("routes", () => { 8 | const _routes = useStorage>("onramp-routes", {}); 9 | 10 | const routes = computed(() => { 11 | return Object.values(_routes.value); 12 | }); 13 | 14 | const updateRoute = (route: Route) => { 15 | if (route.id) { 16 | _routes.value[route.id] = route; 17 | } 18 | }; 19 | 20 | const removeRoute = (routeId: string) => { 21 | if (routeId) { 22 | delete _routes.value[routeId]; 23 | } 24 | }; 25 | 26 | return { 27 | routes, 28 | updateRoute, 29 | removeRoute, 30 | }; 31 | }); 32 | -------------------------------------------------------------------------------- /store/preferences.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from "@vueuse/core"; 2 | import { getAddress, isAddress } from "ethers"; 3 | 4 | export const usePreferencesStore = defineStore("preferences", () => { 5 | const { account, isConnected } = storeToRefs(useOnboardStore()); 6 | 7 | const previousTransactionAddress = useStorage<{ [userAddress: string]: string }>("last-transaction-address", {}); 8 | 9 | return { 10 | previousTransactionAddress: computed({ 11 | get: () => { 12 | if (!isConnected.value) { 13 | return undefined; 14 | } 15 | const lastAddress = previousTransactionAddress.value[account.value.address!]; 16 | if (isAddress(lastAddress)) { 17 | return getAddress(lastAddress) as string; 18 | } 19 | return undefined; 20 | }, 21 | set: (address?: string) => { 22 | if (!isConnected.value || !address) { 23 | return; 24 | } 25 | address = getAddress(address); 26 | if (address === account.value.address) { 27 | return; 28 | } 29 | previousTransactionAddress.value[account.value.address!] = address; 30 | }, 31 | }), 32 | }; 33 | }); 34 | -------------------------------------------------------------------------------- /store/zksync/provider.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from "zksync-ethers"; 2 | 3 | export const useZkSyncProviderStore = defineStore("zkSyncProvider", () => { 4 | const { selectedNetwork } = storeToRefs(useNetworkStore()); 5 | let provider: Provider | undefined; 6 | 7 | const requestProvider = () => { 8 | if (!provider) { 9 | provider = new Provider(selectedNetwork.value.rpcUrl); 10 | } 11 | return provider; 12 | }; 13 | 14 | return { 15 | eraNetwork: selectedNetwork, 16 | 17 | requestProvider, 18 | 19 | blockExplorerUrl: computed(() => selectedNetwork.value.blockExplorerUrl), 20 | }; 21 | }); 22 | -------------------------------------------------------------------------------- /tests/e2e/cucumber.mjs: -------------------------------------------------------------------------------- 1 | const getWorldParams = () => { 2 | const params = { 3 | foo: "bar", 4 | }; 5 | 6 | return params; 7 | }; 8 | 9 | export default { 10 | requireModule: ["ts-node/register"], 11 | paths: ["features/**/*.feature"], 12 | require: ["src/**/*.ts"], 13 | format: [ 14 | // "json:reports/cucumber-report.json", 15 | // "html:reports/report.html", 16 | "summary", 17 | "progress-bar", 18 | "@cucumber/pretty-formatter", 19 | "./src/support/reporters/allure-reporter.js", 20 | ], 21 | formatOptions: { snippetInterface: "async-await" }, 22 | worldParameters: getWorldParams(), 23 | publishQuiet: true, 24 | retry: 1, 25 | // retryTagFilter: "@flaky", 26 | }; 27 | -------------------------------------------------------------------------------- /tests/e2e/features/actions/mainPage/actions-menu.feature: -------------------------------------------------------------------------------- 1 | @action @smoke @regression 2 | Feature: Menu 3 | 4 | Background: 5 | Given Connect Metamask extension with login action 6 | 7 | @id1484 8 | Scenario: Check Logout 9 | Given I am on the Main page 10 | When I click by "partial class" with "account-name" value 11 | When I click by the "Logout" text element on the Menu 12 | Then Element with "text" "Connect your Ethereum wallet to ZKsync Portal" should be "visible" 13 | Then Element with "testId" "network-switcher" should be "visible" 14 | Then Element with "title" "ZKsync Portal GitHub page" should be "visible" 15 | 16 | -------------------------------------------------------------------------------- /tests/e2e/features/actions/mainPage/faucet/actions-faucet.feature: -------------------------------------------------------------------------------- 1 | @action @smoke @regression @faucet @emptyWallet 2 | Feature: Faucet 3 | 4 | Background: 5 | Given Connect Metamask extension with login action 6 | 7 | @id1550 @mainnet 8 | Scenario: Check Faucet NOT available on Mainnet 9 | Given A wallet should be "empty" 10 | When I go to page "/?network=era-mainnet" 11 | Then Element with "text" "Not enough tokens?" should be "invisible" 12 | Then Element with "text" "Use official ZKsync Era faucet" should be "invisible" 13 | Then Element with "text" " Get free test tokens " should be "invisible" 14 | 15 | @id1635 16 | Scenario: Check Out of funds Faucet page 17 | Given A wallet should be "empty" 18 | When I go to page "/transaction/zksync/era/faucet/?network=era-goerli" 19 | Then Faucet tokens animation should be visible 20 | Then Element with "text" "Out of funds" should be "visible" 21 | Then Element with "text" "out of funds currently" should be "visible" 22 | Then Element with "text" ". If you want to test your app, consider testing it locally with access to rich wallets " should be "visible" 23 | Then Element with "text" "following the instructions from our docs" should be "visible" 24 | Then Element with "partial text" "You can also request Goerli ETH from one of the following third party faucets:" should be "visible" 25 | Then Element with "partial text" "You can also request Goerli ETH from one of the following third party faucets:" should be "visible" 26 | Then Element with "text" "Chainstack faucet" should be "visible" 27 | Then Element with "text" "Chainstack faucet" should be "clickable" 28 | Then Element with "text" "QuickNode faucet" should be "visible" 29 | Then Element with "text" "QuickNode faucet" should be "clickable" 30 | Then Element with "text" "PoW faucet" should be "visible" 31 | Then Element with "text" "PoW faucet" should be "clickable" 32 | 33 | -------------------------------------------------------------------------------- /tests/e2e/features/artifacts/assetsPage/artifacts-richWallet.feature: -------------------------------------------------------------------------------- 1 | @artifacts @regression @assetsPage @richWallet 2 | Feature: Artifacts - UI 3 | 4 | Background: 5 | Given Connect Metamask extension with login action 6 | 7 | @id1342 8 | Scenario: Check artifacts for rich wallet on Assets page 9 | When I am on the Main page 10 | Then A wallet should be "fullfilled" 11 | Then Element with "text" "ETH" should be "visible" 12 | Then Element with "partial src" "eth.svg" should be "visible" 13 | Then Element with "text" "0x000000...00A" should be "visible" 14 | Then Element with "text" "Ether" should be "visible" 15 | Then Element with "class" "token-balance-amount" should be "visible" 16 | Then Element with "class" "token-balance-price" should be "visible" 17 | Then Element with "class" "line-button-with-img-icon" should be "visible" 18 | Then Element with "text" "View all" should be "visible" 19 | -------------------------------------------------------------------------------- /tests/e2e/features/artifacts/mainPage/artifacts-404.feature: -------------------------------------------------------------------------------- 1 | @artifacts @regression @mainPage @various 2 | Feature: Artifacts - Incorrect Page - 404 3 | 4 | Background: 5 | Given Connect Metamask extension with login action 6 | 7 | @id1542 8 | Scenario: Check artifacts on the 404 page 9 | Given I am on the Main page 10 | When I go to page "/a" 11 | Then Element with "text" "404" should be "visible" 12 | Then Element with "text" "Page not found: /a" should be "visible" 13 | Then Element with "text" "Back to Portal" should be "visible" 14 | Then Element with "text" "Back to Portal" should be "clickable" -------------------------------------------------------------------------------- /tests/e2e/features/artifacts/transactionsPage/artifacts-transactions.feature: -------------------------------------------------------------------------------- 1 | @artifacts @regression @transactionsItems 2 | Feature: Artifacts - UI - Transactions - Items 3 | 4 | Background: 5 | Given Connect Metamask extension with login action 6 | 7 | @id1488 8 | Scenario: Check artifacts for the Transaction items 9 | Given I go to page "/transactions/?network=era-goerli" 10 | Then Element with "xpath" "//a[text()='Send']" should be "visible" 11 | Then Element with "xpath" "//a[text()='Send']" should be "clickable" 12 | Then Element with "xpath" "//a[text()='Swap']" should be "visible" 13 | Then Element with "xpath" "//a[text()='Swap']" should be "clickable" 14 | Then Element with "href" "/transaction/zksync/era" should be "visible" 15 | Then Element with "href" "/transaction/zksync/era" should be "clickable" 16 | Then Element with "href" "/transaction/zksync/era/swap" should be "visible" 17 | Then Element with "href" "/transaction/zksync/era/swap" should be "clickable" 18 | Then Element with "href and text" "'/transactions/all' and 'View all'" should be "visible" 19 | Then Element with "text" "View all" should be "clickable" 20 | Then The list has the one of the expected type of transactions -------------------------------------------------------------------------------- /tests/e2e/features/redirection/depositPage/redirection.feature: -------------------------------------------------------------------------------- 1 | @deposit @regression @redirection @resetAllowance 2 | Feature: Deposit 3 | 4 | Background: 5 | Given Connect Metamask extension with login action 6 | 7 | 8 | @deposit @id1493 @id1495 @id1496 9 | Scenario: Redirection - approving allowance modal 10 | Given I reset allowance 11 | Given I go to the main page 12 | Given I go to "Deposit" transaction section 13 | When I click by "text" with "Your account" value 14 | Then I confirm the network switching 15 | When I choose "DAI" as token and insert "0.001" as amount 16 | When I click by text " Continue " 17 | # modal card first link 18 | Then Element with "text" " Learn more " should be "clickable" 19 | When I click by "text" with " Learn more " value 20 | Then New page has "https://cryptotesters.com/blog/token-allowances" address 21 | # id1495 modal card approving allovance links 22 | When I "continue" transaction after clicking "Approve allowance" button 23 | Then Modal card element with the "//*[text()='Approving allowance']" xpath should be "visible" 24 | Then Element with "class" "line-button-with-img-icon" should be "clickable" 25 | When I click by "class" with "line-button-with-img-icon" value 26 | Then New page has "https://goerli.etherscan.io/tx" partial address 27 | #Then Element with "partial text" "Track status" should be "clickable" 28 | #When I click by "partial text" with "Track status" value 29 | #Then New page has "https://goerli.etherscan.io/tx" partial address 30 | # id1496 modal card approved allovance links 31 | #Then Modal card element with the "//*[text()='Allowance approved']" xpath should be "visible" 32 | #Then Element with "class" "line-button-with-img-icon" should be "clickable" 33 | #When I click by "class" with "line-button-with-img-icon" value 34 | #Then New page has "https://goerli.etherscan.io/tx" partial address 35 | -------------------------------------------------------------------------------- /tests/e2e/features/redirection/loginPage/redirection.feature: -------------------------------------------------------------------------------- 1 | @redirection @regression @loginPage @authorized @smoke 2 | 3 | Feature: External Redirection on the Login Page 4 | 5 | @id1541 6 | Scenario Outline: Check redirection for the "View on Explorer" links (ZKsync Era∎) 7 | When I click by "" with "" value 8 | Then New page has "" address 9 | 10 | Examples: 11 | | Selector type | Selector value | url | 12 | | title | ZKsync Portal GitHub page | https://github.com/matter-labs/dapp-portal | 13 | | id | zk-sync-white-total | https://zksync.io/ | 14 | 15 | @id1586 16 | Scenario Outline: Check redirection for the Header links 17 | When I click by "" with "" value 18 | Then New page has "" address 19 | 20 | Examples: 21 | | Selector type | Selector value | url | 22 | | id | zk-sync-white-total | https://zksync.io/ | 23 | | aria-label | Blog | https://zksync.mirror.xyz/ | 24 | | aria-label | Discord Community | https://join.zksync.dev/ | 25 | | aria-label | Telegram Support | https://t.me/zksync | 26 | | aria-label | Twitter Community | https://twitter.com/i/flow/login?redirect_after_login=%2Fzksync | 27 | | aria-label | Email | https://zksync.io/contact | 28 | -------------------------------------------------------------------------------- /tests/e2e/features/transactions/deposit/deposit-empty-wallet.feature: -------------------------------------------------------------------------------- 1 | @deposit @regression @transactions @emptyWallet 2 | Feature: Deposit 3 | 4 | Background: 5 | Given Connect Metamask extension with login action 6 | 7 | 8 | @id1294 @emptyWallet @mainnet 9 | Scenario: Deposit - Receive - [Transaction] insufficient funds 0 balance 10 | Given I go to page "/transaction/zksync/era/deposit/?network=era-mainnet" 11 | When I click by "text" with "Your account" value 12 | When I choose "ETH" as token and insert "0" as amount 13 | When Element with "text" " Insufficient " should be "visible" 14 | Then Message "ETH" should be visible 15 | Then Message " balance on Ethereum Mainnet to cover the fee. We recommend having at least " should be visible 16 | Then Message " on Mainnet for deposit. " should be visible 17 | When I confirm the network switching 18 | Then Element with "text" " Continue " should be "disabled" 19 | 20 | @id1294 @emptyWallet @testnet 21 | Scenario: Deposit - Receive - [Transaction] insufficient funds 0 balance 22 | Given I am on the Main page 23 | When I go to "Deposit" transaction section 24 | When I click by "text" with "Your account" value 25 | When I choose "ETH" as token and insert "0" as amount 26 | When Element with "text" " Insufficient " should be "visible" 27 | Then Message "ETH" should be visible 28 | Then Message " balance on Ethereum Goerli Testnet to cover the fee. We recommend having at least " should be visible 29 | Then Message " on Goerli Testnet for deposit. " should be visible 30 | When I confirm the network switching 31 | Then Element with "text" " Continue " should be "disabled" 32 | 33 | -------------------------------------------------------------------------------- /tests/e2e/features/transactions/transfer.feature: -------------------------------------------------------------------------------- 1 | @transfer @regression @transactions 2 | Feature: Transfer 3 | 4 | Background: 5 | Given Connect Metamask extension with login action 6 | 7 | @id1321 8 | Scenario: Make a transfer in ETH 9 | Given I am on the Main page 10 | When I go to page "/transaction/zksync/era/send?network=era-goerli&address=0x9CC8DC9c4d73fC5647A4eE78A2e8EC49D447AeB8" 11 | When I choose "ETH" as token and insert "0.0000000001" as amount 12 | When I "confirm" transaction after clicking "Send to ZKsync Era Testnet" button 13 | Then Message "Transaction submitted" should be visible 14 | Then Message "Transaction completed" should be visible 15 | Then Element with "text" "Send" should be "visible" 16 | Then Arrow element for "Transfer" external link should be "visible" 17 | Then Arrow element for "Transfer" external link should be "clickable" 18 | 19 | @id1276 20 | Scenario: Reject a transfer transaction 21 | Given I am on the Main page 22 | When I go to page "/transaction/zksync/era/send?network=era-goerli&address=0x9CC8DC9c4d73fC5647A4eE78A2e8EC49D447AeB8" 23 | When I choose "ETH" as token and insert "0.0000000001" as amount 24 | When I "reject" transaction after clicking "Send to ZKsync Era Testnet" button 25 | Then Element with "text" "Confirm transaction" should be "visible" 26 | 27 | -------------------------------------------------------------------------------- /tests/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zksync-portal test solution", 3 | "description": "zksync-portal test solution is based on BDD/Playwright and focused on the e2e scenarios", 4 | "private": true, 5 | "scripts": { 6 | "metamask:install": "METAMASK_VERSION=10.14.1 node utils/metamaskDownloader.mjs", 7 | "postinstall": "npm run metamask:install", 8 | "test:e2e": "cucumber-js --tags \"@artifacts and not @emptyWallet\"" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/e2e/src/data/data.ts: -------------------------------------------------------------------------------- 1 | export enum Extension { 2 | specifiedExtensionUrl = "chrome-extension://", 3 | allExtensionsUrl = "chrome://extensions/", 4 | metamaskHomeHtml = "/home.html", 5 | metamaskInitialize = "#initialize/welcome", 6 | metamaskAdvSettings = "#settings/advanced", 7 | metamaskNetworkSettings = "#settings/networks", 8 | } 9 | 10 | export enum NetworkSwitcher { 11 | zkSyncEraGoerli = "/?network=era-goerli", 12 | zkSyncEraMainnet = "/?network=era-mainnet", 13 | } 14 | 15 | export enum Routes { 16 | withdraw = "/transaction/zksync/era/withdraw", 17 | deposit = "/transaction/zksync/era/deposit", 18 | txBlockExplorer = "https://goerli.explorer.zksync.io/tx", 19 | } 20 | -------------------------------------------------------------------------------- /tests/e2e/src/pages/contacts.page.ts: -------------------------------------------------------------------------------- 1 | import type { ICustomWorld } from "../support/custom-world"; 2 | import { BasePage } from "./base.page"; 3 | 4 | export class ContactsPage extends BasePage { 5 | constructor(world: ICustomWorld) { 6 | super(world); 7 | } 8 | 9 | get sendBtnModal() { 10 | return "//*[@class='modal-card']//a[contains(@href, '/transaction/send?address=')]"; 11 | } 12 | 13 | get contactsPageContent() { 14 | return "//*[@class='app-layout-main']"; 15 | } 16 | 17 | get editBtnModal() { 18 | return "//*[@class='buttons-line-group']//button[1]"; 19 | } 20 | 21 | get savedContact() { 22 | return "your-account"; 23 | } 24 | 25 | get modalCard() { 26 | return "//*[@class='modal-card']"; 27 | } 28 | 29 | get addContactButton() { 30 | return "//*[@class='line-button-with-img-body']"; 31 | } 32 | 33 | get headerTextModal() { 34 | return `${this.modalCard}//div[text()='Add contact']`; 35 | } 36 | 37 | async contactNameModal(contactName: string) { 38 | return `${this.modalCard}//div[text()='${contactName}']`; 39 | } 40 | 41 | async addressModal(address: string) { 42 | return `${this.modalCard}//div[text()='${address}']`; 43 | } 44 | 45 | async contactItem(contactName: string) { 46 | return `${this.contactsPageContent}//div[text()='${contactName}']`; 47 | } 48 | 49 | async pressSendBtnModal() { 50 | await this.click(this.sendBtnModal); 51 | } 52 | 53 | async pressEditBtnModal() { 54 | await this.click(this.editBtnModal); 55 | } 56 | 57 | async pressRemoveBtnModal(removeButtonText: string) { 58 | await this.clickByText(removeButtonText); 59 | } 60 | 61 | async getContactItem(contactName: string) { 62 | const result = await this.contactItem(contactName); 63 | const selector = await this.world.page?.locator(result); 64 | return selector; 65 | } 66 | 67 | async clickAddButton() { 68 | await this.click(this.addContactButton); 69 | } 70 | 71 | async clickOnSavedContact() { 72 | await this.click(this.savedContact, true); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/e2e/src/support/config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import * as dotenv from "dotenv"; 3 | 4 | import type { LaunchOptions } from "@playwright/test"; 5 | import { NetworkSwitcher } from "../data/data"; 6 | 7 | dotenv.config({ path: path.resolve(__dirname, ".env.local") }); 8 | 9 | const browserOptions: LaunchOptions = { 10 | slowMo: 10, 11 | devtools: true, 12 | headless: false, 13 | args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--disable-web-security"], 14 | }; 15 | 16 | export const wallet = { 17 | _1_public_key: process.env.E2E_WALLET_1_MAIN_PUB_KEY || "undefined", 18 | _2_public_key: process.env.E2E_WALLET_2_SECOND_PUB_KEY || "undefined", 19 | _0_public_key: process.env.E2E_WALLET_0_EMPTY_PUB_KEY || "undefined", 20 | secret: process.env.E2E_WALLET_SECRET_PK || "undefined", // key for wallets to decrypt 21 | salt: process.env.E2E_WALLET_SALT_IV || "undefined", 22 | _1: process.env.E2E_WALLET_1_MAIN || "undefined", 23 | _2: process.env.E2E_WALLET_2_SECOND || "undefined", 24 | _0: process.env.E2E_WALLET_0_EMPTY || "undefined", 25 | password: process.env.E2E_WALLET_PASSWORD_MM || "undefined", // password MM 26 | }; 27 | 28 | export const config = { 29 | browser: process.env.BROWSER || "chromium", 30 | browserOptions, 31 | BASE_URL: process.env.TARGET_ENV || "http://localhost:3000", 32 | METAMASK_VERSION: process.env.METAMASK_VERSION || "10.14.1", 33 | IMG_THRESHOLD: { threshold: 0.4 }, 34 | mainWindowSize: { width: 1280, height: 720 }, 35 | popUpWindowSize: { width: 355, height: 500 }, 36 | DAPP_NETWORK: NetworkSwitcher.zkSyncEraGoerli, 37 | headless: false, 38 | slowMo: 10, 39 | defaultTimeout: { timeout: 6 * 1000 }, 40 | minimalTimeout: { timeout: 1 * 1000 }, 41 | increasedTimeout: { timeout: 15 * 1000 }, 42 | extraTimeout: { timeout: 30 * 1000 }, 43 | stepTimeout: { timeout: 60 * 1000 }, 44 | stepExtraTimeout: { timeout: 180 * 1000 }, 45 | feeLimitations: true, 46 | feeBoundaryLevel: 0.2, // in ETH 47 | networkL1: "goerli", 48 | networkL2: "https://testnet.era.zksync.dev", 49 | thresholdBalance: 0.6, 50 | preThresholdBalance: 0.9, 51 | matterMostURL: "most.matter-labs.io", 52 | }; 53 | -------------------------------------------------------------------------------- /tests/e2e/src/support/custom-world.ts: -------------------------------------------------------------------------------- 1 | import { setWorldConstructor, World } from "@cucumber/cucumber"; 2 | 3 | import type { IWorldOptions } from "@cucumber/cucumber"; 4 | import type * as messages from "@cucumber/messages"; 5 | import type { BrowserContext, Page, PlaywrightTestOptions } from "@playwright/test"; 6 | import type { AxiosInstance } from "axios"; 7 | 8 | export interface ICustomWorld extends World { 9 | debug: boolean; 10 | feature?: messages.Pickle; 11 | context?: BrowserContext; 12 | persistentContext?: BrowserContext; 13 | page?: Page; 14 | 15 | testName?: string; 16 | startTime?: Date; 17 | 18 | server?: AxiosInstance; 19 | 20 | playwrightOptions?: PlaywrightTestOptions; 21 | } 22 | 23 | export class CustomWorld extends World implements ICustomWorld { 24 | constructor(options: IWorldOptions) { 25 | super(options); 26 | } 27 | 28 | debug = false; 29 | } 30 | 31 | setWorldConstructor(CustomWorld); 32 | -------------------------------------------------------------------------------- /tests/e2e/src/support/reporters/allure-reporter.js: -------------------------------------------------------------------------------- 1 | const { CucumberJSAllureFormatter } = require("allure-cucumberjs8"); 2 | const { AllureRuntime } = require("allure-cucumberjs8"); 3 | function Reporter(options) { 4 | return new CucumberJSAllureFormatter(options, new AllureRuntime({ resultsDir: "./allure-results" }), {}); 5 | } 6 | Reporter.prototype = Object.create(CucumberJSAllureFormatter.prototype); 7 | Reporter.prototype.constructor = Reporter; 8 | 9 | exports.default = Reporter; 10 | -------------------------------------------------------------------------------- /tests/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "esModuleInterop": true 6 | }, 7 | "ts-node": { 8 | "compilerOptions": { 9 | "module": "CommonJS" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/e2e/utils/metamaskDownloader.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import AdmZip from "adm-zip"; 3 | import fetch from "node-fetch"; 4 | 5 | const metamaskVersion = process.env.METAMASK_VERSION; 6 | const metamaskZipSource = 7 | "https://github.com/MetaMask/metamask-extension/releases/download/v" + 8 | metamaskVersion + 9 | "/metamask-chrome-" + 10 | metamaskVersion + 11 | ".zip"; 12 | const targetDirectory = "src/support/extension/"; 13 | const fileName = "extension.zip"; 14 | const metamaskZipTarget = targetDirectory + fileName; 15 | 16 | async function downloadExtension() { 17 | // check and create directory 18 | if (!fs.existsSync(targetDirectory)) { 19 | fs.mkdirSync(targetDirectory, { recursive: true }); 20 | } 21 | 22 | if (!metamaskZipSource) return Promise.reject(new Error("Incorrect source url " + metamaskZipSource)); 23 | if (!targetDirectory) return Promise.reject(new Error("Incorrect target directory " + targetDirectory)); 24 | 25 | return new Promise(function (resolve, reject) { 26 | // download file 27 | fetch(metamaskZipSource).then(function (res) { 28 | const fileStream = fs.createWriteStream(metamaskZipTarget); 29 | res.body.on("error", reject); 30 | fileStream.on("finish", resolve); 31 | res.body.pipe(fileStream); 32 | }); 33 | }); 34 | } 35 | 36 | async function extractExtension(filepath) { 37 | try { 38 | const zip = new AdmZip(filepath); 39 | const outputDir = targetDirectory; 40 | await zip.extractAllTo(outputDir); 41 | console.log(`Extracted to "${outputDir}" successfully`); 42 | await fs.unlink(metamaskZipTarget, () => { 43 | console.log(`Successfully deleted ${fileName} with downloaded Metamask extension`); 44 | }); 45 | } catch (e) { 46 | console.log(`Something went wrong. ${e}`); 47 | } 48 | } 49 | 50 | await downloadExtension(); 51 | await extractExtension(metamaskZipTarget); 52 | -------------------------------------------------------------------------------- /tests/e2e/utils/metamaskId.json: -------------------------------------------------------------------------------- 1 | { 2 | "zksyncGoerliNetwork": { 3 | "networkName": "ZKsync Era Testnet", 4 | "newRpcUrl": "https://testnet.era.zksync.dev", 5 | "chainId": "280", 6 | "currencySymbol": "ETH", 7 | "blockExplorerUrl": "https://goerli.explorer.zksync.io" 8 | }, 9 | "goerliNetwork": { 10 | "networkName": "Goerli test network", 11 | "newRpcUrl": "https://goerli.infura.io/v3/", 12 | "chainId": "5", 13 | "currencySymbol": "GoerliETH", 14 | "blockExplorerUrl": "https://goerli.etherscan.io" 15 | }, 16 | "ethereumMainnetNetwork": { 17 | "networkName": "Ethereum Mainnet", 18 | "newRpcUrl": "https://mainnet.infura.io/v3/", 19 | "chainId": "1", 20 | "currencySymbol": "ETH", 21 | "blockExplorerUrl": "https://etherscan.io" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /types/dompurify.d.ts: -------------------------------------------------------------------------------- 1 | declare module "dompurify"; 2 | -------------------------------------------------------------------------------- /utils/analytics.ts: -------------------------------------------------------------------------------- 1 | const portalRuntimeConfig = usePortalRuntimeConfig(); 2 | let analyticsLoaded = false; 3 | 4 | async function loadRudder() { 5 | if (!window.rudderanalytics) { 6 | await new Promise((resolve) => setTimeout(resolve, 250)); 7 | throw new Error("Rudder not loaded"); 8 | } 9 | window.rudderanalytics.load( 10 | portalRuntimeConfig.analytics.rudder!.key, 11 | portalRuntimeConfig.analytics.rudder!.dataplaneUrl 12 | ); 13 | } 14 | 15 | export async function initAnalytics(): Promise { 16 | if (analyticsLoaded) return true; 17 | 18 | const useRudder = Boolean(portalRuntimeConfig.analytics.rudder); 19 | if (!useRudder || analyticsLoaded) { 20 | return false; 21 | } 22 | 23 | await loadRudder(); 24 | analyticsLoaded = true; 25 | return true; 26 | } 27 | 28 | export async function trackPage(): Promise { 29 | if (await initAnalytics()) { 30 | window.rudderanalytics?.page(); 31 | } 32 | } 33 | 34 | export async function trackEvent(eventName: string, params?: object): Promise { 35 | if (await initAnalytics()) { 36 | window.rudderanalytics?.track(eventName, params); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const L2_BASE_TOKEN_ADDRESS = "0x000000000000000000000000000000000000800A"; 2 | -------------------------------------------------------------------------------- /utils/doc-links.ts: -------------------------------------------------------------------------------- 1 | export const TOKEN_ALLOWANCE = "https://cryptotesters.com/blog/token-allowances"; 2 | 3 | export const ZKSYNC_WITHDRAWAL_DELAY = "https://docs.zksync.io/build/support/withdrawal-delay.html#withdrawal-delay"; 4 | -------------------------------------------------------------------------------- /utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { ZkSyncNetwork } from "@/data/networks"; 2 | import type { TokenAmount } from "@/types"; 3 | 4 | export function isOnlyZeroes(value: string) { 5 | return value.replace(/0/g, "").replace(/\./g, "").length === 0; 6 | } 7 | 8 | export function calculateFee(gasLimit: bigint, gasPrice: bigint) { 9 | return gasLimit * gasPrice; 10 | } 11 | 12 | export const getNetworkUrl = (network: ZkSyncNetwork, routePath: string) => { 13 | const url = new URL(routePath, window.location.origin); 14 | url.searchParams.set("network", network.key); 15 | return url.toString(); 16 | }; 17 | 18 | export const isMobile = () => { 19 | return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent); 20 | }; 21 | 22 | export const calculateTotalTokensPrice = (tokens: TokenAmount[]) => { 23 | return tokens.reduce((acc, { amount, decimals, price }) => { 24 | if (typeof price !== "number") return acc; 25 | return acc + parseFloat(parseTokenAmount(amount, decimals)) * price; 26 | }, 0); 27 | }; 28 | 29 | // Changes URL without changing actual router view 30 | export const silentRouterChange = (location: string, mode: "push" | "replace" = "push") => { 31 | window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", location); 32 | }; 33 | 34 | interface RetryOptions { 35 | retries?: number; 36 | delay?: number; 37 | } 38 | const DEFAULT_RETRY_OPTIONS: RetryOptions = { 39 | retries: 2, 40 | delay: 0, 41 | }; 42 | export async function retry(func: () => Promise, options: RetryOptions = {}): Promise { 43 | const { retries, delay } = Object.assign({}, DEFAULT_RETRY_OPTIONS, options); 44 | try { 45 | return await func(); 46 | } catch (error) { 47 | if (retries && retries > 0) { 48 | if (delay) { 49 | await new Promise((resolve) => setTimeout(resolve, delay)); 50 | } 51 | return retry(func, { retries: retries - 1, delay }); 52 | } else { 53 | throw error; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /utils/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | export default { 4 | log(...data: any[]): void { 5 | console.log(...data); 6 | }, 7 | 8 | error(e: any, ...data: any[]): void { 9 | if (process.env.NODE_ENV !== "test") { 10 | console.error(e, ...data); 11 | } 12 | }, 13 | 14 | warn(message: string, ...data: any[]): void { 15 | if (process.env.NODE_ENV !== "test") { 16 | console.warn(message, ...data); 17 | } 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /utils/sentry-logger.ts: -------------------------------------------------------------------------------- 1 | import { useNuxtApp } from "#app"; 2 | 3 | type SentryCaptureExceptionParams = { 4 | error: Error | string; 5 | parentFunctionName: string; 6 | parentFunctionParams: unknown[]; 7 | accountAddress?: string; 8 | filePath?: string; 9 | }; 10 | 11 | const sentryCaptureException = ({ 12 | error, 13 | parentFunctionName, 14 | parentFunctionParams, 15 | accountAddress = "", 16 | filePath = "", 17 | }: SentryCaptureExceptionParams) => { 18 | const config = useRuntimeConfig(); 19 | if (!config.public.sentryDSN) { 20 | return; 21 | } 22 | 23 | const { $sentryCaptureException, $sentrySetContext } = useNuxtApp(); 24 | 25 | const newError = typeof error === "string" ? new Error(error) : error; 26 | 27 | $sentrySetContext(newError.name, { 28 | error, 29 | parentFunctionName, 30 | parentFunctionParams, 31 | accountAddress, 32 | filePath, 33 | }); 34 | 35 | $sentryCaptureException(newError, { 36 | tags: { 37 | parentFunctionName, 38 | accountAddress, 39 | }, 40 | extra: { 41 | parentFunctionParams, 42 | filePath, 43 | }, 44 | }); 45 | }; 46 | 47 | export { sentryCaptureException }; 48 | export type { SentryCaptureExceptionParams }; 49 | -------------------------------------------------------------------------------- /utils/transitions.ts: -------------------------------------------------------------------------------- 1 | export const TransitionAlertScaleInOutTransition = { 2 | enterActiveClass: "transition ease duration-200", 3 | enterFromClass: "opacity-0 scale-95", 4 | enterToClass: "opacity-100 scale-100", 5 | leaveActiveClass: "transition ease duration-50", 6 | leaveFromClass: "opacity-100 scale-100", 7 | leaveToClass: "opacity-0 scale-95", 8 | }; 9 | 10 | export const TransitionOpacity = (durationIn = 150, durationOut = 150) => ({ 11 | enterActiveClass: `transition duration-[${durationIn}ms]`, 12 | enterFromClass: "opacity-0", 13 | enterToClass: "opacity-100", 14 | leaveActiveClass: `transition duration-[${durationOut}ms]`, 15 | leaveFromClass: "opacity-100", 16 | leaveToClass: "opacity-0", 17 | }); 18 | 19 | export const TransitionPrimaryButtonText = { 20 | enterActiveClass: "transition transform ease-in duration-150", 21 | enterFromClass: "-translate-y-3 opacity-0", 22 | enterToClass: "translate-y-0", 23 | leaveActiveClass: "transition transform ease-in duration-100", 24 | leaveFromClass: "translate-y-0 opacity-100", 25 | leaveToClass: "translate-y-3 opacity-0", 26 | }; 27 | 28 | export const TransitionSlideOutToRight = { 29 | enterActiveClass: "transition ease duration-200", 30 | enterFromClass: "opacity-0 -translate-x-2", 31 | enterToClass: "opacity-100 translate-x-0", 32 | leaveActiveClass: "transition ease duration-200", 33 | leaveFromClass: "opacity-100 translate-x-0", 34 | leaveToClass: "opacity-0 translate-x-2", 35 | }; 36 | export const TransitionSlideOutToLeft = { 37 | enterActiveClass: "transition ease duration-200", 38 | enterFromClass: "opacity-0 translate-x-2", 39 | enterToClass: "opacity-100 translate-x-0", 40 | leaveActiveClass: "transition ease duration-200", 41 | leaveFromClass: "opacity-100 translate-x-0", 42 | leaveToClass: "opacity-0 -translate-x-2", 43 | }; 44 | -------------------------------------------------------------------------------- /views/on-ramp/ActiveTransactionsAlert.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /views/on-ramp/LoadingTransition.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /views/on-ramp/ProcessStatusIcon.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | -------------------------------------------------------------------------------- /views/on-ramp/QuoteFilter.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 50 | 51 | 60 | -------------------------------------------------------------------------------- /views/on-ramp/QuotesList.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 41 | 42 | 50 | -------------------------------------------------------------------------------- /views/transactions/DepositSubmitted.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 57 | -------------------------------------------------------------------------------- /views/transactions/Receive.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 51 | -------------------------------------------------------------------------------- /views/transactions/TransferSubmitted.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 63 | --------------------------------------------------------------------------------