diff --git a/.drawbridgerc b/.drawbridgerc index 41b68fab2eb..fd75fe68c07 100644 --- a/.drawbridgerc +++ b/.drawbridgerc @@ -2,7 +2,7 @@ "environments": { "develop": { "gitUrl": "git@github.com:MyCryptoHQ/MyCrypto.git", - "distFolder": "dist/prod", + "distFolder": "dist/web", "buildCommand": "rm -rf node_modules && yarn && yarn build" }, "beta": { diff --git a/.env.example b/.env.example new file mode 100644 index 00000000000..6f367f553af --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +E2E_BASE_URL="https://localhost:3000" \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000000..4a588020a33 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,141 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "extends": ["eslint:recommended", "eslint-config-prettier", "plugin:eslint-plugin-import/errors"], + "plugins": ["eslint-plugin-react", "eslint-plugin-import"], + "ignorePatterns": ["**/static/matomo.js", "hardhat.config.ts", "jest_config", "node-scripts"], + "rules": { + "eqeqeq": ["error", "always", { "null": "ignore" }], + "import/first": "error", + "import/newline-after-import": "error", + "import/no-deprecated": "error", + "import/no-duplicates": "error", + "import/no-mutable-exports": "error", + "import/no-namespace": "error", + "import/no-self-import": "error", + "import/no-unresolved": "off", + "import/no-useless-path-segments": "error", + "import/order": [ + "error", + { + "newlines-between": "always", + "groups": [["builtin", "external"], "internal", ["parent", "sibling", "index"]], + "pathGroups": [ + { + "pattern": "@{assets,components,config,database,fixtures,features,helpers,hooks,routing,sass,services,store,theme,translations,types,utils,vendor,workers}{**,**/**}", + "group": "internal" + }, + { + "pattern": "react", + "group": "external", + "position": "before" + } + ], + "pathGroupsExcludedImportTypes": [], + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ], + "sort-imports": [ + "error", + { + "ignoreCase": true, + "ignoreDeclarationSort": true, + "allowSeparatedGroups": true + } + ] + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "extends": [ + "plugin:@typescript-eslint/recommended", + "plugin:eslint-plugin-react/recommended", + "plugin:eslint-plugin-react-hooks/recommended", + "plugin:eslint-plugin-import/typescript", + "eslint-config-prettier/@typescript-eslint" + ], + "plugins": ["@typescript-eslint"], + "rules": { + "@typescript-eslint/consistent-type-assertions": [ + "error", + { + "assertionStyle": "as" + } + ], + "@typescript-eslint/consistent-type-definitions": ["error", "interface"], + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/method-signature-style": ["error", "method"], + "@typescript-eslint/no-inferrable-types": [ + "error", + { + "ignoreParameters": true + } + ], + "@typescript-eslint/no-empty-function": [ + "error", + { + "allow": ["private-constructors"] + } + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "ignoreRestSiblings": true, + "argsIgnorePattern": "^_" + } + ], + "@typescript-eslint/prefer-nullish-coalescing": "error", + "react/display-name": "off", + "react/no-unescaped-entities": "off", + "react/prop-types": "off", + "react/react-in-jsx-scope": "off", + "react-hooks/exhaustive-deps": "off", + // Configure Jest to recognise react-saga-test-plan assertions + // https://github.com/jfairbank/redux-saga-test-plan/issues/329 + "jest/expect-expect": [ + "error", + { + "assertFunctionNames": ["expect", "expectSaga", "testSaga", "expectToThrow"] + } + ] + }, + "settings": { + "react": { + "version": "detect" + } + }, + "parserOptions": { + "project": "./tsconfig.json", + "ecmaFeatures": { + "jsx": true + } + } + }, + { + "files": ["*.test.{ts,tsx}", "*.spec.{ts,tsx}", "jest_config/**/*"], + "env": { + "node": true, + "jest": true + }, + "extends": ["plugin:eslint-plugin-jest/recommended", "plugin:eslint-plugin-jest/style"], + "plugins": ["eslint-plugin-jest"] + }, + { + "files": ["__tests__/**/*.js"], + "extends": ["plugin:testcafe/recommended"], + "plugins": ["eslint-plugin-testcafe"] + } + ], + "parserOptions": { + "ecmaVersion": 2020 + }, + "env": { + "browser": true, + "node": true + } +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ceeddd3d690..798b80de1ac 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: https://donate.unlock-protocol.com/?r=mycryptohq/mycrypto \ No newline at end of file +custom: https://donate.unlock-protocol.com/?r=mycryptohq/mycrypto diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 148a9e0ea98..7f2d33bf986 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,24 +1,20 @@ -*Github issues are primarily for reporting bugs or getting assistance with development-related items. If you have a question or need support with our product, please email support@mycrypto.com for faster attention.* +_Github issues are primarily for reporting bugs or getting assistance with development-related items. If you have a question or need support with our product, please email support@mycrypto.com for faster attention._ -*If you are reporting an issue, please use the template below so we can reproduce, determine the cause of the issue, and fix it! If you are not reporting an issue, delete this entire thing and type away. Thanks!* +_If you are reporting an issue, please use the template below so we can reproduce, determine the cause of the issue, and fix it! If you are not reporting an issue, delete this entire thing and type away. Thanks!_ --- - ### Description of Issue ... - ### What exactly were you doing when you encountered this issue? ...Give us some steps to reproduce... - ### How were you accessing your wallet? -...Ledger, Trezor, MetaMask, Private Key, Keystore File, Parity Signer, Mnemonic Phrase?... - +...Ledger, Trezor, MetaMask, Private Key, Keystore File, Mnemonic Phrase?... ### Description of Your Machine @@ -29,10 +25,6 @@ - MyCrypto.com vs Desktop Application? - MyCrypto Version Number: - ### Console Logs / Screenshots ... - - - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 658647d2eba..e10955d54a3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -35,3 +35,4 @@ - [ ] This is related to a maximum of one Clubhouse story or GitHub issue - [ ] Types are safe (avoid TypeScript/TSLint features like any and disable, instead use more specific types) - [ ] If code is copied from existing directories, there is an explanation of why this is necesary in the description/changes, and all copying is done in separate commits to make them easy to filter out +- [ ] If your code adds new text to the UI, the added text is [translatable](https://github.com/MyCryptoHQ/MyCrypto/wiki/Contributing---Translatable-strings) diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000000..c9a67d54b45 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,20 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 60 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 7 + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: [] + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 30 diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml new file mode 100644 index 00000000000..270a0964e04 --- /dev/null +++ b/.github/workflows/beta.yml @@ -0,0 +1,67 @@ +name: Beta + +env: + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET_BUILDS_APP }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_BETA }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_BETA }} + AWS_REGION: 'us-east-1' + +on: + push: + branches: + - master + +jobs: + beta-build: + environment: PRODUCTION + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Retrieve node version from .nvmrc + run: echo "##[set-output name=NVMRC;]$(cat package.json | grep '\"node\":' | sed 's/^ *//;s/ *$//;s/\"node\":\ \"//;s/\",//')" + id: nvm + + - name: Setup node ${{ steps.nvm.outputs.NVMRC }} + uses: actions/setup-node@v1 + with: + node-version: '${{ steps.nvm.outputs.NVMRC }}' + + - name: Install Dependencies + run: yarn install --frozen-lockfile + + - name: Set outputs + id: vars + run: echo "::set-output name=sha::$(git rev-parse HEAD)" + + - name: yarn build + run: yarn build + env: + COMMIT_HASH: ${{ steps.vars.outputs.sha }} + + - name: Zip standalone build folder + run: zip -r dist/web/standalone_mycrypto.zip dist/web/ + + - name: 'Save standalone_mycrypto.zip to artifacts' + uses: actions/upload-artifact@v1 + with: + name: 'standalone_mycrypto' + path: 'dist/web/standalone_mycrypto.zip' + + - name: S3 Sync - SHA Push + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'push' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --exclude '.git/*' + env: + DEST_DIR: ${GITHUB_SHA} + SOURCE_DIR: 'dist/web' + + - name: S3 Sync - SHA Pull Request + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'pull_request' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --exclude '.git/*' + env: + DEST_DIR: ${{ github.event.pull_request.head.sha }} + SOURCE_DIR: 'dist/web' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a7695d55b5..ad49c20322e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,281 +9,81 @@ on: - master jobs: -## WINDOWS-PREBUILD START ## - pre-build-and-build-windows: - name: Build el:win + validate-and-test: + name: Validate and Test runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-latest] + os: [ubuntu-latest] + # macOS-latest + env: + ELECTRON_CACHE: $HOME/.cache/electron + ELECTRON_BUILDER_CACHE: $HOME/.cache/electron-builder steps: - - name: Retrieve project from the git branch + - name: Retrieves project from the git branch uses: actions/checkout@v1 - - name: Print current engines' versions - run: > - echo "node: $(node -v)"; - echo "npm: $(npm -v)"; - echo "yarn: $(yarn -v)"; - echo "python: $(python -V)"; - - - name: Retrieve engines' versions from package.json to env variables - id: engines - run: > - $hashtable = Get-Content -Raw -Path ./package.json | ConvertFrom-Json -AsHashtable; - echo $hashtable.engines; - $nodeVersion = $hashtable.engines.node; - $npmVersion = $hashtable.engines.npm; - $yarnVersion = $hashtable.engines.yarn; - Write-Output "##[set-output name=NODE_VERSION;]$($nodeVersion)"; - Write-Output "##[set-output name=NPM_VERSION;]$($npmVersion)"; - Write-Output "##[set-output name=YARN_VERSION;]$($yarnVersion)"; + - name: Read .nvmrc + run: echo "##[set-output name=NVMRC;]$(cat package.json | grep '\"node\":' | sed 's/^ *//;s/ *$//;s/\"node\":\ \"//;s/\",//' )" + id: nvm - - name: Setup node ${{ steps.engines.outputs.NODE_VERSION }} - uses: actions/setup-node@master + - name: Setup node ${{ steps.nvm.outputs.NVMRC }} + uses: actions/setup-node@v1 with: - node-version: ${{ steps.engines.outputs.NODE_VERSION }} + node-version: '${{ steps.nvm.outputs.NVMRC }}' - - run: node -v - - name: install node tools - run: npm install --global --production windows-build-tools@4.0.0 - - name: install node-gyp - run: npm install --global node-gyp@latest - - name: Set node config to use python2.7 - run: npm config set python python2.7 - - name: Set node config to set msvs_version to 2015 - run: npm config set msvs_version 2015 - - name: Work around for Windows Server 2019 - run: set path=%path%;'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin' + - run: node --version; npm --version; yarn --version - name: Cache node modules id: cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: node_modules ## Check cache based on yarn.lock hashfile key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.OS }}-build-${{ env.cache-name }}- - ${{ runner.OS }}-build- - ${{ runner.OS }}- - name: Install Dependencies ## If no cache is found, install dependencies if: steps.cache.outputs.cache-hit != 'true' - run: yarn install + run: yarn install --frozen-lockfile - - name: yarn tslint - run: yarn tslint + - name: yarn validate + run: yarn validate - - name: yarn tscheck - run: yarn tscheck + - name: yarn test:coverage + run: yarn test:coverage --maxWorkers=2 - - name: yarn test:coverage -- --maxWorkers=2 - run: yarn test:coverage -- --maxWorkers=2 + - name: CodeCov + uses: codecov/codecov-action@v1 -## NOT WORKING -# - name: yarn report-coverage -# run: yarn report-coverage + standalone-build: + name: Standalone Build + runs-on: ubuntu-latest - - name: yarn build:electron:windows - run: yarn build:electron:windows - - - name: ls dist/electron-builds - run: ls dist/electron-builds - - - name: Read MyCrypto version from package.json - id: version - run: $json = Get-Content package.json | ConvertFrom-Json; Write-Output "##[set-output name=VERSION;]$($json.version)" - - - name: Save windows_${{ steps.version.outputs.VERSION }}_MyCrypto.exe to artifacts - uses: actions/upload-artifact@v1 - with: - name: "windows_${{ steps.version.outputs.VERSION }}_MyCrypto.exe" - path: "dist/electron-builds/MyCrypto Setup ${{ steps.version.outputs.VERSION }}.exe" -# ## WINDOWS-PREBUILD END ## - - pre-build: - name: Pre-build - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ - macOS-latest - # ubuntu-latest - ] - env: - ELECTRON_CACHE: $HOME/.cache/electron - ELECTRON_BUILDER_CACHE: $HOME/.cache/electron-builder steps: - - name: Retrieves project from the git branch - uses: actions/checkout@v1 - -# - name: Slack Notification -# if: matrix.os == 'ubuntu-latest' -# uses: rtCamp/action-slack-notify@master -# env: -# # SLACK_CHANNEL: general -# SLACK_COLOR: '#3278BD' -# # SLACK_ICON: https://github.com/rtCamp.png?size=48 -# SLACK_MESSAGE: 'Post Content :rocket:' -# SLACK_TITLE: Post Title -# SLACK_USERNAME: rtCamp -# SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - - - name: Read .nvmrc - run: echo "##[set-output name=NVMRC;]$(cat package.json | grep '\"node\":' | sed 's/^ *//;s/ *$//;s/\"node\":\ \"//;s/\",//' )" - id: nvm - - - name: Setup node ${{ steps.nvm.outputs.NVMRC }} - uses: actions/setup-node@v1 - with: - node-version: "${{ steps.nvm.outputs.NVMRC }}" - - - run: node --version; npm --version; yarn --version - - - name: Cache node modules - id: cache - uses: actions/cache@v1 - with: - path: node_modules - ## Check cache based on yarn.lock hashfile - key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.OS }}-build-${{ env.cache-name }}- - ${{ runner.OS }}-build- - ${{ runner.OS }}- - - - name: Install Dependencies - ## If no cache is found, install dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: yarn install - - - name: yarn tslint - run: yarn tslint - - - name: yarn tscheck - run: yarn tscheck - - - name: yarn test:coverage -- --maxWorkers=2 - run: yarn test:coverage -- --maxWorkers=2 - -## NOT WORKING -# - name: yarn report-coverage -# run: yarn report-coverage - - electron-osx: - name: Build el:osx - ## Executes only if pre-build is true - needs: [pre-build] - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [macOS-latest] - env: - ELECTRON_CACHE: $HOME/.cache/electron - ELECTRON_BUILDER_CACHE: $HOME/.cache/electron-builder - - steps: - - name: Retrieves project from the git branch - uses: actions/checkout@v1 - - - name: Retrieve node version from .nvmrc - run: echo "##[set-output name=NVMRC;]$(cat package.json | grep '\"node\":' | sed 's/^ *//;s/ *$//;s/\"node\":\ \"//;s/\",//')" - id: nvm - - - name: Setup node ${{ steps.nvm.outputs.NVMRC }} - uses: actions/setup-node@v1 - with: - node-version: "${{ steps.nvm.outputs.NVMRC }}" - - - name: Restoring node modules - uses: actions/cache@v1 - with: - path: node_modules - key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }} - - - name: yarn build:electron:osx - run: yarn build:electron:osx - - - name: ls -la dist/electron-builds - run: ls -la dist/electron-builds - - - name: Read app version - id: version - run: echo "##[set-output name=VERSION;]$(cat package.json | grep -m1 version | sed 's/...version....//' | sed 's/.\{2\}$//')" - - - name: "Save standalone_${{ steps.version.outputs.VERSION }}_MyCrypto.zip to artifacts" - uses: actions/upload-artifact@v1 - with: - name: "standalone_${{ steps.version.outputs.VERSION }}_MyCrypto.zip" - path: "dist/electron-builds/MyCrypto-${{ steps.version.outputs.VERSION }}-mac.zip" - - name: "Save mac_${{ steps.version.outputs.VERSION }}_MyCrypto.dmg to artifacts" - uses: actions/upload-artifact@v1 - with: - name: "mac_${{ steps.version.outputs.VERSION }}_MyCrypto.dmg" - path: "dist/electron-builds/MyCrypto-${{ steps.version.outputs.VERSION }}.dmg" - - electron-linux: - name: Build el:linux - # Executes only if pre-build is true - needs: [pre-build] - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ - ## Uncomment to run linux build on macOS - macOS-latest - # ubuntu-latest - ] - env: - ELECTRON_CACHE: $HOME/.cache/electron - ELECTRON_BUILDER_CACHE: $HOME/.cache/electron-builder - - steps: - - name: Retrieves project from the git branch - uses: actions/checkout@v1 - - - name: Retrieve node version from .nvmrc - run: echo "##[set-output name=NVMRC;]$(cat package.json | grep '\"node\":' | sed 's/^ *//;s/ *$//;s/\"node\":\ \"//;s/\",//')" - id: nvm - - - name: Setup node ${{ steps.nvm.outputs.NVMRC }} - uses: actions/setup-node@v1 - with: - node-version: "${{ steps.nvm.outputs.NVMRC }}" - - - name: Cache node modules - uses: actions/cache@v1 - with: - path: node_modules - ## Check cache based on yarn.lock hashfile - key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.OS }}-build-${{ env.cache-name }}- - ${{ runner.OS }}-build- - ${{ runner.OS }}- + - name: Retrieves project from the git branch + uses: actions/checkout@v1 - - name: yarn build:electron:linux - run: yarn build:electron:linux + - name: Retrieve node version from .nvmrc + run: echo "##[set-output name=NVMRC;]$(cat package.json | grep '\"node\":' | sed 's/^ *//;s/ *$//;s/\"node\":\ \"//;s/\",//')" + id: nvm - - name: ls -la dist/electron-builds - run: ls -la dist/electron-builds + - name: Setup node ${{ steps.nvm.outputs.NVMRC }} + uses: actions/setup-node@v1 + with: + node-version: '${{ steps.nvm.outputs.NVMRC }}' - - name: Read app version - id: version - run: echo "##[set-output name=VERSION;]$(cat package.json | grep -m1 version | sed 's/...version....//' | sed 's/.\{2\}$//')" + - name: Install Dependencies + run: yarn install --frozen-lockfile - - name: "Save linux-i386_${{ steps.version.outputs.VERSION }}_MyCrypto.AppImage to artifacts" - uses: actions/upload-artifact@v1 - with: - name: "linux-i386_${{ steps.version.outputs.VERSION }}_MyCrypto.AppImage" - path: "dist/electron-builds/MyCrypto-${{ steps.version.outputs.VERSION }}-i386.AppImage" + - name: yarn build + run: yarn build - - name: "Save linus-x86-64_${{ steps.version.outputs.VERSION }}-MyCrypto.AppImage to artifacts" - uses: actions/upload-artifact@v1 - with: - name: "linus-x86-64_${{ steps.version.outputs.VERSION }}-MyCrypto.AppImage" - path: "dist/electron-builds/MyCrypto-${{ steps.version.outputs.VERSION }}.AppImage" + - name: Zip standalone build folder + run: zip -r dist/web/standalone_mycrypto.zip dist/web/ -## TODO: Add "build electron:windows from Mac or Linux" + - name: 'Save standalone_mycrypto.zip to artifacts' + uses: actions/upload-artifact@v1 + with: + name: 'standalone_mycrypto' + path: 'dist/web/standalone_mycrypto.zip' diff --git a/.github/workflows/mycryptobuilds.yml b/.github/workflows/mycryptobuilds.yml new file mode 100644 index 00000000000..75c24eb3ca0 --- /dev/null +++ b/.github/workflows/mycryptobuilds.yml @@ -0,0 +1,223 @@ +name: MyCryptoBuilds + +env: + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET_MYCRYPTOBUILDS }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_MYCRYPTOBUILDS }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_MYCRYPTOBUILDS }} + AWS_REGION: 'us-east-2' + +on: + pull_request: + branches: + - '*' + push: + branches: + - master + repository_dispatch: + types: [ok-to-test-command] + +jobs: + myc-build: + environment: STAGING + runs-on: ubuntu-latest + + steps: + # Fetch the PR number to use it with AWS + - name: Set PR_NUMBER + run: | + PR_NUMBER=$(echo $GITHUB_REF | awk 'BEGIN { FS = "/" } ; { print $3 }') + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + + # Define url as env var to be used by TestCafe. + - name: Set E2E_BASE_URL + if: github.ref != 'refs/heads/master' && github.event_name == 'pull_request' + run: | + E2E_BASE_URL=$( echo https://mycryptobuilds.com/pr/${PR_NUMBER}/# ) + echo $E2E_BASE_URL + echo "E2E_BASE_URL=$E2E_BASE_URL" >> $GITHUB_ENV + + # Define url as env var to be used by TestCafe. + - name: Set E2E_BASE_URL + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + run: | + E2E_BASE_URL=$( echo https://mycryptobuilds.com/master/# ) + echo $E2E_BASE_URL + echo "E2E_BASE_URL=$E2E_BASE_URL" >> $GITHUB_ENV + + # Define url as env var to be used by TestCafe. + - name: Set E2E_BASE_URL + if: github.event_name == 'repository_dispatch' + run: | + E2E_BASE_URL=$( echo https://mycryptobuilds.com/pr/${{ github.event.client_payload.pull_request.number }}/# ) + echo $E2E_BASE_URL + echo "E2E_BASE_URL=$E2E_BASE_URL" >> $GITHUB_ENV + + - uses: actions/checkout@v2 + if: github.event_name != 'repository_dispatch' + + - uses: actions/checkout@v2 + if: github.event_name == 'repository_dispatch' + with: + ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' + + - name: Retrieve node version from .nvmrc + run: echo "##[set-output name=NVMRC;]$(cat package.json | grep '\"node\":' | sed 's/^ *//;s/ *$//;s/\"node\":\ \"//;s/\",//')" + id: nvm + + - name: Setup node ${{ steps.nvm.outputs.NVMRC }} + uses: actions/setup-node@v1 + with: + node-version: '${{ steps.nvm.outputs.NVMRC }}' + + - name: Cache node modules + id: cache + uses: actions/cache@v2 + with: + path: node_modules + ## Check cache based on yarn.lock hashfile + key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }} + + - name: Cache Hardhat network + uses: actions/cache@v2 + with: + path: .cache/hardhat + key: ${{ runner.OS }}-build-${{ hashFiles('**/hardhat.config.ts') }} + + - name: Install Dependencies + ## If no cache is found, install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + + - name: Set outputs + id: vars + run: echo "::set-output name=sha::$(git rev-parse HEAD)" + + - name: yarn build:staging + run: yarn build:staging + if: github.event_name == 'pull_request' + env: + COMMIT_HASH: ${{ github.event.pull_request.head.sha }} + + - name: yarn build:staging + run: yarn build:staging + if: github.event_name == 'repository_dispatch' + env: + COMMIT_HASH: ${{ github.event.client_payload.pull_request.head.sha }} + + - name: yarn build:staging + run: yarn build:staging + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + env: + COMMIT_HASH: ${{ steps.vars.outputs.sha }} + + - name: S3 Sync - Downloadable Branch - push + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'push' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --delete --exclude '.git/*' + env: + DEST_DIR: ${GITHUB_REF##*/} + SOURCE_DIR: 'dist/web' + + - name: S3 Sync - Downloadable Branch - repository_dispatch + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'repository_dispatch' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --delete --exclude '.git/*' + env: + DEST_DIR: ${{ github.event.client_payload.pull_request.head.ref }} + SOURCE_DIR: 'dist/web' + + - name: S3 Sync - Downloadable Branch - pull_request + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'pull_request' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --delete --exclude '.git/*' + env: + DEST_DIR: ${{ github.head_ref }} + SOURCE_DIR: 'dist/web' + #### + - name: S3 Sync - SHA Push + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'push' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --exclude '.git/*' + env: + DEST_DIR: ${GITHUB_SHA} + SOURCE_DIR: 'dist/web' + + - name: S3 Sync - SHA Pull Request + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'pull_request' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --exclude '.git/*' + env: + DEST_DIR: ${{ github.event.pull_request.head.sha }} + SOURCE_DIR: 'dist/web' + + - name: S3 Sync - SHA Repository Dispatch + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'repository_dispatch' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --exclude '.git/*' + env: + DEST_DIR: ${{ github.event.client_payload.pull_request.head.sha }} + SOURCE_DIR: 'dist/web' + #### + - name: S3 Sync - Downloadable - PR Number + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'pull_request' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --delete --exclude '.git/*' + env: + DEST_DIR: 'pr/${PR_NUMBER}' + SOURCE_DIR: 'dist/web' + + - name: S3 Sync - Downloadable - PR Number - Repository Dispatch + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'repository_dispatch' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --delete --exclude '.git/*' + env: + DEST_DIR: 'pr/${{ github.event.client_payload.pull_request.number }}' + SOURCE_DIR: 'dist/web' + + - name: Mycrypto-bot trigger + if: github.event_name == 'pull_request' + run: aws sns publish --topic-arn ${{ secrets.SNS_TOPIC_MYCRYPTO_BOT }} --region us-east-2 --message $(echo $GITHUB_REF | awk 'BEGIN { FS = "/" } ; { print $3 }') + + # Launch E2E tests. When in pull request it runs against the PR's mycryptobuilds url. + - name: E2E - run + run: yarn test:e2e:ci + env: + INFURA_API_KEY: ${{ secrets.INFURA_API_KEY }} + + - name: Update PR status + uses: actions/github-script@v1 + id: update-check-run + if: github.event_name == 'repository_dispatch' + env: + number: ${{ github.event.client_payload.pull_request.number }} + job: ${{ github.job }} + # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run + conclusion: ${{ job.status }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: pull } = await github.pulls.get({ + ...context.repo, + pull_number: process.env.number + }); + const ref = pull.head.sha; + const { data: checks } = await github.checks.listForRef({ + ...context.repo, + ref + }); + const check = checks.check_runs.filter(c => c.name === process.env.job); + const { data: result } = await github.checks.update({ + ...context.repo, + check_run_id: check[0].id, + status: 'completed', + conclusion: process.env.conclusion + }); + return result; diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml new file mode 100644 index 00000000000..0befe337125 --- /dev/null +++ b/.github/workflows/ok-to-test.yml @@ -0,0 +1,34 @@ +# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event +name: Ok to test + +on: + issue_comment: + types: [created] + +jobs: + ok-to-test: + runs-on: ubuntu-latest + steps: + # Generate a GitHub App installation access token from an App ID and private key + # To create a new GitHub App: + # https://developer.github.com/apps/building-github-apps/creating-a-github-app/ + # See app.yml for an example app manifest + - name: Generate token + id: generate_token + uses: MyCryptoHQ/github-app-token@v1 + with: + app_id: ${{ secrets.WORKFLOW_APP_ID }} + private_key: ${{ secrets.WORKFLOW_PRIVATE_KEY }} + + - name: Slash Command Dispatch + uses: peter-evans/slash-command-dispatch@v1 + env: + TOKEN: ${{ steps.generate_token.outputs.token }} + with: + token: ${{ env.TOKEN }} # GitHub App installation access token + # token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work + reaction-token: ${{ secrets.GITHUB_TOKEN }} + issue-type: pull-request + commands: ok-to-test + named-args: true + permission: write diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml new file mode 100644 index 00000000000..3945a528221 --- /dev/null +++ b/.github/workflows/storybook.yml @@ -0,0 +1,155 @@ +name: Storybook + +env: + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET_MYCRYPTOBUILDS }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_MYCRYPTOBUILDS }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_MYCRYPTOBUILDS }} + AWS_REGION: 'us-east-2' + +on: + pull_request: + branches: + - '*' + push: + branches: + - master + repository_dispatch: + types: [ok-to-test-command] + +jobs: + storybook-build: + runs-on: ubuntu-latest + + steps: + # Fetch the PR number to use it with AWS + - name: Set PR_NUMBER + run: | + PR_NUMBER=$(echo $GITHUB_REF | awk 'BEGIN { FS = "/" } ; { print $3 }') + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + + - uses: actions/checkout@v2 + if: github.event_name != 'repository_dispatch' + + - uses: actions/checkout@v2 + if: github.event_name == 'repository_dispatch' + with: + ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' + + - name: Retrieve node version from .nvmrc + run: echo "##[set-output name=NVMRC;]$(cat package.json | grep '\"node\":' | sed 's/^ *//;s/ *$//;s/\"node\":\ \"//;s/\",//')" + id: nvm + + - name: Setup node ${{ steps.nvm.outputs.NVMRC }} + uses: actions/setup-node@v1 + with: + node-version: '${{ steps.nvm.outputs.NVMRC }}' + + - name: Cache node modules + id: cache + uses: actions/cache@v2 + with: + path: node_modules + ## Check cache based on yarn.lock hashfile + key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }} + + - name: Install Dependencies + ## If no cache is found, install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile + + - name: yarn build:storybook + if: github.ref == 'refs/heads/master' || github.event_name == 'pull_request' + run: yarn build:storybook + + - name: S3 Sync - Storybook SHA + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'pull_request' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --exclude '.git/*' + env: + DEST_DIR: 'storybook/${GITHUB_SHA}' + SOURCE_DIR: 'storybook-static' + + - name: S3 Sync - Storybook SHA - repository_dispatch + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'repository_dispatch' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --exclude '.git/*' + env: + DEST_DIR: 'storybook/${{ github.event.client_payload.pull_request.head.sha }}' + SOURCE_DIR: 'storybook-static' + + - name: S3 Sync - Storybook Branch - push + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --delete --exclude '.git/*' + env: + DEST_DIR: 'storybook/${GITHUB_REF##*/}' + SOURCE_DIR: 'storybook-static' + + - name: S3 Sync - Storybook Branch - pull_request + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'pull_request' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --delete --exclude '.git/*' + env: + DEST_DIR: 'storybook/${{ github.head_ref }}' + SOURCE_DIR: 'storybook-static' + + - name: S3 Sync - Storybook Branch - repository_dispatch + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'repository_dispatch' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --delete --exclude '.git/*' + env: + DEST_DIR: 'storybook/${{ github.event.client_payload.pull_request.head.ref }}' + SOURCE_DIR: 'storybook-static' + + - name: S3 Sync - Storybook - PR Number - pull_request + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'pull_request' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --delete --exclude '.git/*' + env: + DEST_DIR: 'storybook/pr/${PR_NUMBER}' + SOURCE_DIR: 'storybook-static' + + - name: S3 Sync - Storybook - PR Number - repository_dispatch + uses: jakejarvis/s3-sync-action@v0.5.1 + if: github.event_name == 'repository_dispatch' + with: + args: --acl public-read --cache-control max-age=0 --follow-symlinks --delete --exclude '.git/*' + env: + DEST_DIR: 'storybook/pr/${{ github.event.client_payload.pull_request.number }}' + SOURCE_DIR: 'storybook-static' + + - name: Update PR status + uses: actions/github-script@v1 + id: update-check-run + if: github.event_name == 'repository_dispatch' + env: + number: ${{ github.event.client_payload.pull_request.number }} + job: ${{ github.job }} + # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run + conclusion: ${{ job.status }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: pull } = await github.pulls.get({ + ...context.repo, + pull_number: process.env.number + }); + const ref = pull.head.sha; + const { data: checks } = await github.checks.listForRef({ + ...context.repo, + ref + }); + const check = checks.check_runs.filter(c => c.name === process.env.job); + const { data: result } = await github.checks.update({ + ...context.repo, + check_run_id: check[0].id, + status: 'completed', + conclusion: process.env.conclusion + }); + return result; diff --git a/.gitignore b/.gitignore index 0aaa39fefc1..d312342cac5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ npm-debug.log* # Build dist +storybook-static # Runtime data pids @@ -61,4 +62,11 @@ package-lock.json .wwp-cache # MacOSX -.DS_Store \ No newline at end of file +.DS_Store + +# Ignore js transpiled scripts +node-scripts/**/*.js +/node-scripts/translations/__mocks__/sample.json + +.env +.cache diff --git a/.modernizrrc.js b/.modernizrrc.js index 65eea9c035d..1a638954ddf 100644 --- a/.modernizrrc.js +++ b/.modernizrrc.js @@ -1,10 +1,6 @@ module.exports = { - "enableJSClass": false, - "enableClasses": false, - "minify": true, - "feature-detects": [ - "css/flexbox", - "css/flexwrap", - "storage/localstorage" - ] + enableJSClass: false, + enableClasses: false, + minify: true, + 'feature-detects': ['css/flexbox', 'css/flexwrap', 'storage/localstorage'] }; diff --git a/.prettierignore b/.prettierignore index 2a03f7056bd..6dfbbe07d0a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,76 @@ -common/translations -package.json +.idea +# Logs +logs +*.log +npm-debug.log* + +# Build +dist +storybook-static + +# Runtime data +pids +*.pid +*.seed +# Python venv +venv + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# VScode workspace settings +.vscode/ +static/dll.vendor.js +dll +.cache-loader + +# SSL cert stuff +webpack_config/server.key +webpack_config/server.crt +webpack_config/server.csr + + +v8-compile-cache-0/ +package-lock.json + +# Favicon cache +.wwp-cache + +# MacOSX +.DS_Store + +# Ignore js transpiled scripts +node-scripts/**/*.js +/node-scripts/translations/__mocks__/sample.json + +.env +.cache + +# Prettier specific __snapshots__ +static/matomo.js diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 00000000000..165bde91790 --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,34 @@ +const merge = require('webpack-merge'); +const path = require('path'); +const defaultConfig = require('../webpack_config/config'); +const commonConfig = require('../webpack_config/common'); + +module.exports = { + stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: ['@storybook/addon-links', '@storybook/addon-essentials'], + reactOptions: { + fastRefresh: true + }, + webpackFinal: async (config) => { + return merge.smart(config, { + resolve: commonConfig.resolve, + module: { + rules: [ + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + include: [ + path.join(defaultConfig.path.src, 'vendor'), + path.join(defaultConfig.path.root, 'node_modules/typeface-lato') + ] + }, + + { + test: /\.scss$/, + use: ['style-loader', 'css-loader', 'sass-loader'] + } + ] + } + }); + } +}; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 00000000000..777537264f6 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,7 @@ + diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 00000000000..aea8d25d824 --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,45 @@ +import 'font-awesome/scss/font-awesome.scss'; +import 'sass/styles.scss'; + +import { Provider } from 'react-redux'; +import { MemoryRouter, Route, Switch } from 'react-router-dom'; +import { configureStore } from '@reduxjs/toolkit'; +import { ThemeProvider } from 'styled-components'; + +import { rootReducer } from '@store'; +import { theme } from '@theme'; +import { FeatureFlagProvider } from '@services'; +import { mockAppState } from '../jest_config/test-utils'; + +const store = configureStore({ + reducer: rootReducer, + preloadedState: mockAppState() +}); + +export const parameters = { + actions: { argTypesRegex: '^on[A-Z].*' }, + options: { + storySort: { + method: '', + order: ['Atoms', 'Molecules', 'Organisms', 'Features'], + locales: '' + } + } +}; + +export const decorators = [ + (story) => ( + + + {story()} + + + ), + (story) => ( + + + {story()} + + + ) +]; diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 00000000000..6d8cca69e43 --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,9 @@ +{ + "processors": [ + "stylelint-processor-styled-components" + ], + "extends": [ + "stylelint-config-recommended", + "stylelint-config-styled-components" + ] +} diff --git a/.testcaferc.json b/.testcaferc.json new file mode 100644 index 00000000000..58b05c9c1e5 --- /dev/null +++ b/.testcaferc.json @@ -0,0 +1,15 @@ +{ + "browsers": ["chrome"], + "src": ["./__tests__/*.test.js"], + "proxyByPass": ["localhost:3000"], + "clientScripts": [ + { + "module": "@testing-library/dom/dist/@testing-library/dom.umd.js" + } + ], + "developmentMode": true, + "color": true, + "quarantineMode": { + "successThreshold": 1 + } +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3f5484dec22..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: node_js -matrix: - include: - - os: osx - osx_image: xcode9.3 - env: - - ELECTRON_CACHE=$HOME/.cache/electron - - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder - fast_finish: true -install: - - yarn --silent -before_script: - - chmod +x ./travis-scripts/test-osx.sh -script: - - './travis-scripts/test-osx.sh' -notifications: - email: - on_success: never - on_failure: never - slack: - secure: lVqvDVG3rTCjlpMtnW5Ca4lLLzO2ZvTB6feiQTpcIIu+GINr2q9KcfWUxLt/E5wU371Gua6+mb5SRUCiD/eO+4Dc9Cz+mZLm5wfDjC9Zk6tI1T7AvNfwH/M7bf1PIAPPghXkaVuNN9pcJ7FAd6/sun08syUYEEb94xFePaU2f5uKBV4TqbCIJvGrK3yhsGf8b+GDz0o1+YoO/UQ6WyCisQdgIk2Op09vFAmspZnGXp5tlhhXz1iTHzp54VUAa27NmsoGyEoY55OKSDA6vQkpLi+BLVXo1GyJA/2K+jJIIunyKyxgWwXJTaM4gNBaorfoiGFaUiK078wRkAOeCXJmrQfrXzz9bgFIQV//vxW3j+xU0wa+zRORSFnDfwzSFnliJ168UBlD0eKHT+Z+XADI4wREZtQj5K8IW/Ggu8PI0NCuWPAyfaF2sJUeQjVvVvc9hNRu+sAE7HD36e2/e4/DS25cBKPKIdwokCk+CxuxRAGcWgkWRk3vxsNcOwVrye7IdZd2297Q1xaN4ZKVNg9Fc2u/NLGc2G+xXFYG6OlfBXNn4+uD85xQp4iJ7S8cPUKGNUTAY7JQ9gN2lfEswm9XXv80zkl7IMj1yFgPcl44eb9liG4GdcAHGdDe3YKz101GdQIyJdviHxt29kHXIrMOy5jEK/p+vNlx0VgIeQ6/+JY= diff --git a/.unlock-protocol.config.json b/.unlock-protocol.config.json index 5f2943c5793..3e865db1eea 100644 --- a/.unlock-protocol.config.json +++ b/.unlock-protocol.config.json @@ -2,12 +2,12 @@ "persistentCheckout": true, "icon": "https://assets.unlock-protocol.com/tmp/my-crypto.png", "callToAction": { - "default": "Support the MyCrypto team and unlock your membership today!\n\nYou can make a donation by purchasing a key using your Ethereum wallet. The key is a non fungible token which represents your membership. ", - "confirmed": "Thank you for choosing to be a MyCrypto supporter! ❤️\n\n\n\nAt this time, your support is a simple donation that helps us continue working to bring you products.\n\n\n\nIn the future we'll be further developing this functionality with the Unlock Protocol team to reward supporters with things like custom themes, swag, and more.\n\n\n\n-Taylor & Team MyCrypto" + "default": "Support the MyCrypto team and unlock your membership today!\n\nYou can make a donation by purchasing a key using your Ethereum wallet. The key is a non fungible token which represents your membership. ", + "confirmed": "Thank you for choosing to be a MyCrypto supporter! ❤️\n\n\n\nAt this time, your support is a simple donation that helps us continue working to bring you products.\n\n\n\nIn the future we'll be further developing this functionality with the Unlock Protocol team to reward supporters with things like custom themes, swag, and more.\n\n\n\n-Taylor & Team MyCrypto" }, "locks": { - "0x14e81196e60b128527db03d40bdba00710777805": { - "name": "MyCrypto Members" - } + "0x14e81196e60b128527db03d40bdba00710777805": { + "name": "MyCrypto Members" + } } -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000000..400effacc61 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "editor.formatOnSave": true, + "editor.tabSize": 2, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "prettier.requireConfig": true, + "prettier.useEditorConfig": false +} diff --git a/README.md b/README.md index 96812a0639d..96feed45d7f 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,32 @@ -# MyCrypto Beta Web App +# MyCrypto Web App -[![Build Status](https://travis-ci.org/MyCryptoHQ/MyCrypto.svg?branch=master)](https://travis-ci.org/MyCryptoHQ/MyCrypto) -[![Coverage Status](https://coveralls.io/repos/github/MyCryptoHQ/MyCrypto/badge.svg?branch=master)](https://coveralls.io/github/MyCryptoHQ/MyCrypto?branch=develop) +[![Build](https://github.com/MyCryptoHQ/MyCrypto/workflows/GitHub%20CI/badge.svg?branch=master)](https://github.com/MyCryptoHQ/MyCrypto/actions) +[![codecov](https://codecov.io/gh/MyCryptoHQ/MyCrypto/branch/master/graph/badge.svg)](https://codecov.io/gh/MyCryptoHQ/MyCrypto) + +This repo stores the MyCrypto codebase. The code is served at [app.mycrypto.com](https://app.mycrypto.com) -This repo stores both the beta and production versions of the MyCrypto codebase. -#### The default `master` branch represents the beta code hosted on [beta.mycrypto.com](https://beta.mycrypto.com) -#### The `legacy` branch represents the production code hosted on [mycrypto.com](https://mycrypto.com) #### Documentation can be found in [our wiki](https://github.com/MyCryptoHQ/MyCrypto/wiki) +- [Contributor Guidelines](https://github.com/MyCryptoHQ/MyCrypto/wiki/Contributor-Guidelines) A must-read if you'd like to contribute to MyCrypto - [Getting your token added to MyCrypto](https://github.com/MyCryptoHQ/MyCrypto/wiki/Contributing-%E2%80%90-Adding-Tokens) - [Adding your Network or Node](https://github.com/MyCryptoHQ/MyCrypto/wiki/Contributing-%E2%80%90-Network-or-Node) - [Adding your Web3 Wallet & Logo](https://github.com/MyCryptoHQ/MyCrypto/wiki/Contributing-%E2%80%90-Web3-Wallet) - [MyCryptoBuilds](https://github.com/MyCryptoHQ/MyCrypto/wiki/MyCryptoBuilds) - access a build by commithash, PR number, or branch name without building it yourself! +- [Enabling or disabling experimental features](https://github.com/MyCryptoHQ/MyCrypto/wiki/Feature-flags) ## Development / Build Requirements -* Node 12.14.1\* -* Yarn >= 1.19.1\*\* -* Python 2.7.X\*\*\* +- Node 12.14.1\* +- Yarn >= 1.19.1\*\* + +On systems where a pre-built version of node-hid is not available (e.g. 32-bit versions of Windows and Linux), you additionally need Python (3+) and node-gyp installed. \*Higher versions should work fine, but may cause inconsistencies.
-**npm is NOT supported for package management. MyCrypto uses yarn.lock to ensure sub-dependency versions are pinned, so yarn is required to install node_modules +\*\*npm is NOT supported for package management. MyCrypto uses yarn.lock to ensure sub-dependency versions are pinned, so yarn is required to install node_modules
-\***Python 3 is **not** supported, since our dependencies use `node-gyp`.
-\***For users trying to build with WSL, you'll need to have install libpng via `sudo apt-get install libpng16-dev`. +\*\*\*For users trying to build with WSL, you'll need to have install libpng via `sudo apt-get install libpng16-dev`. ## Running the App @@ -39,19 +40,89 @@ Then, you can run various commands depending on what you want to do: # run app in dev mode in browser, rebuild on file changes yarn start ``` + A development server will be available on https://localhost:3000 If you're using Chrome, you will get a `net::ERR_CERT_AUTHORITY_INVALID` warning. To disable it you can your settings in chrome: chrome://flags/#allow-insecure-localhost +#### Staging ```bash -# run app in dev mode in electron, rebuild on file changes -yarn dev:electron +# builds app for staging +yarn build:staging ``` -#### Staging +#### Production + +```bash +# builds app for production +yarn build +``` + +## Testing + +### Unit Tests ```bash -# builds production app version used on mycryptobuilds.com -yarn build:downloadable +# runs all unit tests using jest +yarn test ``` + +### End-to-end Tests + +```bash +# runs all e2e tests using testcafe +yarn test:e2e +``` + +To debug E2E tests in the browser + +```bash +yarn test:e2e:dev +``` + +You can also run a single file + +```bash +yarn test:e2e:dev __tests__/dashboard.test.js +``` + +## Environments + +In development we use a `.env` file to define required values. The list is defined in `.env.example`. These values are made available to the React code through the `dotenv` webpack plugin. +In production, the values are set through Github secrets and made available to the React app through the `webpack.EnvironmentPlugin`. +NB. Since TestCafe has access to the node, the values are available at `process.env` and do not need to be cloned. + +## Product Analytics + +We use product analytics to help us understand how to improve our features. + +The collection of usage data requires the use of unique identifiers which are stored in the browsers local storage. The id is random and can be removed or reset in the Settings panel. This ensures that our product team can identify places to improve the product, while at the same time protecting the users anonymity. + +We strive to provide access to the Ethereum blockchain in the most secure way possible, we refuse any dynamic script injections and only rely on HTTPS calls to the API. The calls are formatted using the excellant [@blockstock/stats](https://github.com/MyCryptoHQ/stats) library. + +Finally, since User privacy is part of our core values, we also take special care to ensure that the data we send to thrid-party servers is purged from any information which may be de-anonymised such as: + +- Every analytics call is sent to a MYC hosted reverse-proxy, strip of the users ip address and browser fingerprint before being forwarded to the data-warehouse (in this case ) + +- never sending an ETH address or Transaction hash to our analytics warehouse. + +## Dependency verification. + +The integrity hash is generated by base64 encoding the sha512 binary digest: + +``` + openssl dgst -binary -sha512 .tgz | openssl base64 +``` + +The best way to verify the integrity hash of a `yarn.lock` is to compare the integrity hashes. This may be done manually. + +1. Download tgz from yarnpkg host +2. Generate integrity hash for it (sha512 base64) +3. Unpackage tgz +4. Download files from Github at relevant tag +5. Diff the files from Github and the files from the unpackages tgz + + ``` + $ diff -r node_modules/ tmp/ + ``` diff --git a/SECURITY.md b/SECURITY.md index 7e3a967c821..667ab837c65 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,176 +1,3 @@ # Security Policy -## Reporting a Vulnerability - - -MyCrypto is a cryptocurrency interface that allows people to interact with their cryptocurrency assets in a way that is more user-friendly than other solutions. We do not hold custody of any customer's assets nor do we collect personally-identifiable information about any of our customers, so our security policy is centered on how well our software allows people to safely and privately interact with their own assets. - -MyCrypto looks forward to working with the security community to find security vulnerabilities in order to keep our businesses and customers safe. We are particularly interested in vulnerabilities found in the application layer, but any vulnerability across our stack that could lead to compromise of user funds is within scope (except as listed in **"Out of Scope"**). - -## Vulnerability Response Process - -### Preliminary Statement - -1. Please refrain from committing the following acts, as they will not be recognized as an exploit on our platform: - - - Denial of Service attacks to any servers running MyCrypto software, 3rd party software we rely on (such as our Geth and Parity nodes), or APIs we rely on. - - - Social engineering manipulation of MyCrypto staff members or contractors. - - - Attempts to gain physical access to hardware that MyCrypto uses. - -2. Other instances running our software (private or public forks, domains that are not listed in **“In Scope”** but are running our software) are not in scope - only the code under the `mycryptohq` namespace on GitHub.com is (with a select few other projects under the MyCrypto banner but a different namespace) - see the **"In Scope"** section for more information. - -3. The disclosure process will be run under our [HackerOne program](https://hackerone.com/mycrypto "https://hackerone.com/mycrypto"). - - - Anyone who submits a matter that is eligible for a vulnerability disclosure to [security@mycrypto.com](mailto:security@mycrypto.com) (or the other emails listed below in **"Avenues to Contact"**) will be directed to open a ticket with our [HackerOne program](https://hackerone.com/mycrypto "https://hackerone.com/mycrypto") or a ticket will be opened on their behalf. - -4. Any ticket that we have agreed to award swag for will not be rewarded if the vulnerability / exploit is made public before: - - - It has been made known to us through the [HackerOne program](https://hackerone.com/mycrypto "https://hackerone.com/mycrypto"). - - - You have given us enough time from responsible disclosure (90 days) to patch. - -5. A researcher will open one vulnerability per report with enough details for us to start an investigation. - - -### Avenues to Contact - -- HackerOne: [https://hackerone.com/mycrypto](https://hackerone.com/mycrypto "https://hackerone.com/mycrypto") - -- Email: [security@mycrypto.com](mailto:security@mycrypto.com) - PGP fingerprint: [BE7B 7A02 069B A692 E759 A7FC 5245 6166 91A3 EE98](https://keybase.io/mycrypto/pgp_keys.asc?fingerprint=be7b7a02069ba692e759a7fc5245616691a3ee98) - -- Email: [harry@mycrypto.com](mailto:harry@mycrypto.com) - PGP fingerprint: [5E1F 6F7F 1BA7 C31B 0455 B504 1DF9 D1E1 E9B5 C0D2](https://pgp.mit.edu/pks/lookup?op=get&search=0x1DF9D1E1E9B5C0D2) - -- Email: [mia@mycrypto.com](mailto:mia@mycrypto.com) - PGP fingerprint: [74F5 3783 F6C4 C6E5 A467 D63C 2792 C9AB 5B72 F196](https://keybase.io/miagx/pgp_keys.asc?fingerprint=74f53783f6c4c6e5a467d63c2792c9ab5b72f196) -- Email: [taylor@mycrypto.com](mailto:taylor@mycrypto.com) - PGP fingerprint: FF65 5D72 A0DB 81E9 4A8C 12FB E8C6 DD68 17C6 2CBA -- Email: [tayvano@gmail.com](mailto:tayvano@gmail.com) - PGP fingerprint: 1B7A 2D12 DE76 F0F0 A215 3B62 697F 4D4B 81B1 5C84 - -### Response Team - -- Harry Denley ([@409h](https://keybase.io/409h "https://keybase.io/409h")) - -- Blurpesec ([@blurpesec](https://keybase.io/blurpesec "https://keybase.io/blurpesec")) - -- Taylor Monahan ([@tayvano](https://keybase.io/tayvano "https://keybase.io/tayvano")) - -- Mia Alexander ([@miagx](https://keybase.io/miagx)) - -### SLA - -MyCrypto will make a best effort to meet the following SLAs for hackers participating in our program: - -- Time to first response (from report submit) - 3 business days -- Time to triage (from report submit) - 9 business days - -We’ll try to keep you informed about our progress throughout the process. - -### Incident Response - -1. A ticket is opened on our [HackerOne program](https://hackerone.com/mycrypto "https://hackerone.com/mycrypto") with details about the vulnerability (outlining the impacts) and enough information to replicate it. - - - If there is not enough information to replicate the issue, we will ask for more - - - We will contact you if your issue: - - - Has already been noticed internally and is included in a scheduled release (a public acknowledgment will still be made to reporters who have opened a ticket about it before release) - - - Is either out-of-scope or a non-issue - the process will halt here until the issue becomes in-scope/a valid vulnerability. - -2. We will verify and internally escalate the issue with the appropriate team (dev, infra, ...) - -3. We will discuss internally and establish the severity of the vulnerability - any dispute about the classification of the vulnerability can be discussed but the MyCrypto team will ultimately define it. - - - **HIGH** - User funds are at risk of being taken maliciously or user secrets are at risk of being exposed - anything that directly affects user funds/secrets with ease. - - - **MEDIUM** - Exploits in the software that give the user bad data, which could lead users to take actions that expose themselves to risk. - - - **LOW** - Issues that are low impact to users or are extremely hard to do (e.g., needing very specific hardware) - -4. We will respond to you within three days, acknowledging that we have verified the issue, escalated it internally as appropriate, and classified it. - -5. We will develop a patch and communicate with the researcher to validate. - - - The vulnerability is still to be treated as private - - - The researcher is given a private branch to verify the patch (if code related - this could be a staging URL for the patch) or asked to verify externally (if infrastructure related) - -6. A vulnerability announcement is drafted (using an internal template that includes the severity, details of the patch, and acknowledgment to the researcher(s)) - - - A release date is internally discussed and set - -7. At release date, we coordinate with the developers (if code-related vulnerability) to include the vulnerability announcement in the release notes (published on our repository at GitHub) - - -Please also refer to [HackerOne Disclosure Guidelines](https://www.hackerone.com/disclosure-guidelines). - -### Post-Release Disclosure Process - -1. The team has 90 days to go through the life cycle of a valid vulnerability report and issue a patch to production. - -2. The published vulnerability announcement will acknowledge the reporter(s) - - - The reporter will be named by default, but can be kept anonymous upon request - - - The published announcement will detail: - - - The project affected (repository name / infrastructure descriptor) - - - The versions of software known to be affected - - - The versions of software known not to be affected (e.g., exploit was unintentionally introduced in v1.5 - anything prior to this version is unaffected) - - - The versions of software that were not checked (e.g., versions that are now considered EOL by the MyCrypto team) - - - Any mitigating factors (e.g., the vulnerability is only present in non-default configurations on a specific environment) - -3. If we have decided to reward a user with swag, we will notify them and the reward will be processed through the HackerOne swag system. - - -### Swag Distribution - -MyCrypto has not set standards for swag rewards. Swag rewards, when applicable, will be considered on a case-by-case basis and are at the sole discretion of MyCrypto. - -If your report has been considered for a swag reward, this will be communicated to you on HackerOne and handled via the HackerOne Swag Reward feature. - -### In Scope - -MyCrypto manages many applications under different domains. - -- buy.mycrypto.com - -- support.mycrypto.com - -- overflow.mycrypto.com - -- about.mycrypto.com - -- www.mycrypto.com - -- beta.mycrypto.com - -- download.mycrypto.com - -- etherscamdb.info - -- cryptoscamdb.org - -- ambo.io -- ambo.herokuapp.com -- Ambo IOS App (production link found on https://ambo.io/downloads/) - -A description of each domain can be found on our knowledge base: [https://support.mycrypto.com/general-knowledge/about-mycrypto/official-domain-names-used-by-mycrypto](https://support.mycrypto.com/general-knowledge/about-mycrypto/official-domain-names-used-by-mycrypto) - -Infrastructure and code used on these domains are in scope (except the aspects stated in **“Out of Scope”**) - -### Out of Scope - -When reporting vulnerabilities, please consider (1) attack scenario / exploitability, and (2) security impact of the bug. The following issues are considered out of scope: - -- Attacks requiring MITM or physical access to a user's device. -- Missing best practices in SSL/TLS configuration. -- Any activity that could lead to the disruption of our service (DoS). - - - -Thank you for helping keep MyCrypto and our users safe! +Please see [https://security.mycrypto.com](https://security.mycrypto.com) diff --git a/STRUCTURE.md b/STRUCTURE.md index 962075cc0b9..1a0ad60ab4d 100644 --- a/STRUCTURE.md +++ b/STRUCTURE.md @@ -27,23 +27,23 @@ ## Providers -* `Providers` are special React components that are used to provide functionality potentially many layers deep, while avoiding prop-drilling. -* Each Provider file exports a constant (e.g. `const ExampleContext = React.createContext()`), as well as a class. -* The class contains a state variable that includes all variables and functions to be accessed down the hierarchy. _Functions are placed in state._ -* In the render method of the class, `this.props.children` is returned inside of (e.g. `). -* When `Service` modules need to interact with UI across features, they should use a `Provider`, and communicate changes through state (e.g. ExampleProvider should use an ExampleContext). -* Providers should be placed in the `Layout` feature, wrapping all other components. +- `Providers` are special React components that are used to provide functionality potentially many layers deep, while avoiding prop-drilling. +- Each Provider file exports a constant (e.g. `const ExampleContext = createContext()`), as well as a class. +- The class contains a state variable that includes all variables and functions to be accessed down the hierarchy. _Functions are placed in state._ +- In the render method of the class, `this.props.children` is returned inside of (e.g. `). +- When `Service` modules need to interact with UI across features, they should use a `Provider`, and communicate changes through state (e.g. ExampleProvider should use an ExampleContext). +- Providers should be placed in the `Layout` feature, wrapping all other components. ## Services -* `Service` modules are business-layer-logic classes that encapsulate interactions with a particular entity. -* The API service module is used to interact with external servers to gather information. -* The Storage and Cache service modules are used to interact with local storage, to aid in persisting data. -* Services should export an abstract class, containing the functionality, as a named export. -* Services should default-export a singleton class which prevents other classes from being instantiated. +- `Service` modules are business-layer-logic classes that encapsulate interactions with a particular entity. +- The API service module is used to interact with external servers to gather information. +- The Storage and Cache service modules are used to interact with local storage, to aid in persisting data. +- Services should export an abstract class, containing the functionality, as a named export. +- Services should default-export a singleton class which prevents other classes from being instantiated. ## Indexing -* Every level of directory should contain an `index.ts` file which re-exports everything in the directory, recursively. -* Externally interfacing with a directory should always be done with the most senior index file (e.g. `import { Foo } from 'v2/components';` vs. `import Foo from 'v2/components/Foo/Foo';`) -* Internally interfacing with a directory should always be done with relative pathing `../../`. +- Every level of directory should contain an `index.ts` file which re-exports everything in the directory, recursively. +- Externally interfacing with a directory should always be done with the most senior index file (e.g. `import { Foo } from '@components';` vs. `import Foo from '@components/Foo/Foo';`) +- Internally interfacing with a directory should always be done with relative pathing `../../`. diff --git a/TRANSLATIONS.md b/TRANSLATIONS.md new file mode 100644 index 00000000000..bc307d20337 --- /dev/null +++ b/TRANSLATIONS.md @@ -0,0 +1,55 @@ +## Translations + +In order to use built in translation extractor you can run: +``` +yarn translations:extract +``` + +The script will auto extract all translation keys wrapped inside `translate`, `translateRaw` and `translateMarker` functions. +It will merge new keys, without modifying old ones and sort them alphabetically. +It will update all *.json files inside `src/translations/lang/*.json`, meaning all languages will be updated with new keys. + +### Add new translation key +To add new translation key, just wrap the key inside `translate`, `translateRaw` or `translateMarker` functions. +And run `yarn translations:extract`, to update the *.json files. Find the key in *.json file, +and insert the value(Do this at least for English language(`en.json`)). + +#### Naming keys(IMPORTANT!): +##### Always name the key with uppercase letters(otherwise they will be ignored) +- Correct: `translateRaw('SUCCESS_MESSAGE')` +- Incorrect: `translateRaw('successMessage')` +##### Don't use only number key for translation keys(the will be ignored) +- Correct: `translateRaw('SUCCESS_0')` OR `translateRaw('0_SUCCESS')` +- Incorrect: `translateRaw('0')` OR `translateRaw('123456789')` +##### Don't use variables as translation keys +- Correct, use map function: +```typescript +const mapTranslationKey = (message: string) => { + switch(message) { + case 'success': + return translateMarker('SUCCESS_MESSAGE'); + case 'error': + default: + return translateMarker('ERROR_MESSAGE'); + } +}; + +translateRaw(mapTranslationKey(message)); +``` +- Incorrect: `translateRaw(message)`, where message is a variable + +### Merging translations +If all translation keys were added via this script, just remove all merging conflicts tag `<<<<<<<`, +`=======` and `>>>>>>>` so that the `.json` file is valid. And rerun `yarn translations:extract`. + +### Adding new language + +To add a new translation create a file `.json` and use the following boilerplate: +```json +{ + "code": "", + "data": { + + } +} +``` diff --git a/__tests__/addaccount-page.po.js b/__tests__/addaccount-page.po.js new file mode 100644 index 00000000000..2e18f489fdd --- /dev/null +++ b/__tests__/addaccount-page.po.js @@ -0,0 +1,59 @@ +import { getByText } from '@testing-library/testcafe'; +import { Selector, t } from 'testcafe'; + +import BasePage from './base-page.po'; +import { setupEthereumMock } from './ethereum-mock'; +import { + FIXTURE_ETHEREUM, + FIXTURE_HARDHAT_PRIVATE_KEY, + FIXTURE_VIEW_ONLY_ADDRESS, + PAGES +} from './fixtures'; +import { getTransValueByKey } from './translation-utils'; + +export default class AddAccountPage extends BasePage { + async navigateToPage() { + this.navigateTo(PAGES.ADD_ACCOUNT); + } + + async waitPageLoaded(timeToWait) { + await this.waitForPage(PAGES.ADD_ACCOUNT, timeToWait); + } + + async addViewOnly() { + await this.waitPageLoaded(); + await t.click(getByText(getTransValueByKey('VIEW_ADDR'))); + this.waitForPage(PAGES.ADD_ACCOUNT_VIEWONLY); + + await t + .typeText(Selector('div[data-testid="selector"]').find('input'), FIXTURE_ETHEREUM, { + paste: true + }) + .click(Selector('div[data-testid="selector"]').find('span').withText(FIXTURE_ETHEREUM)) + .click(Selector('button').withText(getTransValueByKey('ACTION_6'))) + .typeText(Selector(`div[data-testid="selector"]`).find('input'), FIXTURE_VIEW_ONLY_ADDRESS, { + paste: true + }) + // Lose focus + .click(getByText(getTransValueByKey('INPUT_PUBLIC_ADDRESS_LABEL'))) + .click(Selector('button').withText(getTransValueByKey('ACTION_6'))); + } + + async addWeb3() { + await this.waitPageLoaded(); + await setupEthereumMock(FIXTURE_HARDHAT_PRIVATE_KEY, 1); + await t.click(getByText(getTransValueByKey('X_WEB3_DEFAULT'))); + this.waitForPage(PAGES.ADD_ACCOUNT_WEB3); + + await t.click(getByText(getTransValueByKey('ADD_WEB3_DEFAULT'))); + } + + async selectEthereumNetwork() { + await t + .typeText(Selector('div[data-testid="selector"]').find('input'), FIXTURE_ETHEREUM, { + paste: true + }) + .click(Selector('div[data-testid="selector"]').find('span').withText(FIXTURE_ETHEREUM)) + .click(Selector('button').withText(getTransValueByKey('ACTION_6'))); + } +} diff --git a/__tests__/addaccount.test.js b/__tests__/addaccount.test.js new file mode 100644 index 00000000000..30ebc406384 --- /dev/null +++ b/__tests__/addaccount.test.js @@ -0,0 +1,44 @@ +import { getByText } from '@testing-library/testcafe'; + +import AddAccountPage from './addaccount-page.po'; +import DashboardPage from './dashboard-page.po'; +import { + FIXTURE_VIEW_ONLY_ADDRESS, + FIXTURE_VIEW_ONLY_TOKENS, + FIXTURE_WEB3_ADDRESS, + PAGES +} from './fixtures'; +import { findByTKey } from './translation-utils'; + +const addAccountPage = new AddAccountPage(); +const dashboardPage = new DashboardPage(); + +fixture('Add Accounts').page(PAGES.ADD_ACCOUNT); + +test('Should show wallet add UI', async (t) => { + await addAccountPage.navigateToPage(); + await addAccountPage.waitPageLoaded(); + + const title = getByText(findByTKey('DECRYPT_ACCESS')); + await t.expect(title.exists).ok(); +}); + +// Add Account - View Only +test('Should be able to add a view only address', async () => { + await addAccountPage.addViewOnly(); + await dashboardPage.waitPageLoaded(); + + await dashboardPage.expectAddressToBePresent(FIXTURE_VIEW_ONLY_ADDRESS); + await dashboardPage.expectAccountTableToMatchCount(1); + + FIXTURE_VIEW_ONLY_TOKENS.forEach((t) => dashboardPage.expectBalanceInBalanceList(t)); +}); + +// Add Account - Web3 +test('Should be able to add a web3 address', async () => { + await addAccountPage.addWeb3(); + await dashboardPage.waitPageLoaded(); + + await dashboardPage.expectAddressToBePresent(FIXTURE_WEB3_ADDRESS); + await dashboardPage.expectAccountTableToMatchCount(1); +}); diff --git a/__tests__/base-page.po.js b/__tests__/base-page.po.js new file mode 100644 index 00000000000..627fedf492a --- /dev/null +++ b/__tests__/base-page.po.js @@ -0,0 +1,17 @@ +import { ClientFunction, t } from 'testcafe'; + +import { FIXTURES_CONST } from './fixtures'; + +export default class BasePage { + getCurrentLocation = ClientFunction(() => window.location.href); + + async navigateTo(url) { + await t.navigateTo(url); + } + + async waitForPage(url, timeout) { + await t + .expect(this.getCurrentLocation()) + .eql(url, { timeout: timeout || FIXTURES_CONST.TIMEOUT }); + } +} diff --git a/__tests__/clientScripts.js b/__tests__/clientScripts.js new file mode 100644 index 00000000000..86a6c5d17ab --- /dev/null +++ b/__tests__/clientScripts.js @@ -0,0 +1,12 @@ +import { FIXTURE_MYC_STORAGE_KEY } from './fixtures'; + +// Client Script to set LS before loading the page +// Double stringified as that makes it a valid JS string +const injectLS = (ls) => ` + localStorage.setItem( + '${FIXTURE_MYC_STORAGE_KEY}', + ${JSON.stringify(JSON.stringify(ls))} + ); +`; + +export { injectLS }; diff --git a/__tests__/contracts-page.po.js b/__tests__/contracts-page.po.js new file mode 100644 index 00000000000..c59782d0866 --- /dev/null +++ b/__tests__/contracts-page.po.js @@ -0,0 +1,55 @@ +import { getByText, queryAllByText } from '@testing-library/testcafe'; +import { Selector, t } from 'testcafe'; + +import BasePage from './base-page.po'; +import { setupEthereumMock } from './ethereum-mock'; +import { + FIXTURE_CONTRACT_ALLOWANCE, + FIXTURE_CONTRACT_BYTECODE, + FIXTURE_CONTRACT_SPENDER, + FIXTURE_DAI_ADDRESS, + FIXTURE_ERC20_ABI_JSON, + FIXTURE_HARDHAT_PRIVATE_KEY, + PAGES +} from './fixtures'; +import { findByTKey, getTransValueByKey } from './translation-utils'; + +export default class ContractsPage extends BasePage { + async waitDeployLoaded(timeout) { + await this.waitForPage(PAGES.DEPLOY_CONTRACTS, timeout); + } + + async waitInteractLoaded(timeout) { + await this.waitForPage(PAGES.INTERACT_WITH_CONTRACTS, timeout); + } + + async fillDeployForm() { + await t.typeText(Selector('textarea[name="byteCode"]'), FIXTURE_CONTRACT_BYTECODE, { + paste: true + }); + } + + async fillInteractForm() { + await t + .typeText(Selector('div[data-testid="selector"]').nth(1).find('input'), FIXTURE_DAI_ADDRESS, { + paste: true + }) + .click(getByText(getTransValueByKey('CONTRACT_TITLE'))) + .typeText(Selector('textarea[name="abi"]'), FIXTURE_ERC20_ABI_JSON, { + paste: true + }) + .click(queryAllByText(findByTKey('INTERACT_WITH_CONTRACT')).nth(1)) + .click(Selector('div[data-testid="selector"]').nth(2)) + .click(getByText('approve')) + .typeText(Selector('input').nth(6), FIXTURE_CONTRACT_SPENDER, { + paste: true + }) + .typeText(Selector('input').nth(7), FIXTURE_CONTRACT_ALLOWANCE, { + paste: true + }); + } + + async setupMock() { + await setupEthereumMock(FIXTURE_HARDHAT_PRIVATE_KEY, 1); + } +} diff --git a/__tests__/contracts.test.js b/__tests__/contracts.test.js new file mode 100644 index 00000000000..0fec9ac6329 --- /dev/null +++ b/__tests__/contracts.test.js @@ -0,0 +1,75 @@ +import { queryAllByTestId, queryAllByText, queryByText } from '@testing-library/testcafe'; + +import { injectLS } from './clientScripts'; +import ContractsPage from './contracts-page.po'; +import { FIXTURE_HARDHAT, FIXTURES_CONST, PAGES } from './fixtures'; +import { resetFork } from './hardhat-utils'; +import { findByTKey } from './translation-utils'; + +const contractsPage = new ContractsPage(); + +fixture('Interact with Contracts') + .clientScripts({ content: injectLS(FIXTURE_HARDHAT) }) + .page(PAGES.INTERACT_WITH_CONTRACTS); + +test('Can interact with contract', async (t) => { + await resetFork(); + await contractsPage.waitInteractLoaded(); + await contractsPage.setupMock(); + + await contractsPage.fillInteractForm(); + + await t.wait(FIXTURES_CONST.TIMEOUT); + + const button = await queryAllByText(findByTKey('ACTION_17')).nth(1); + await t.click(button); + + const signBtn = await queryByText(findByTKey('CONFIRM_AND_SEND')).with({ + timeout: FIXTURES_CONST.HARDHAT_TIMEOUT + }); + await t.expect(signBtn.exists).ok(); + await t.click(signBtn); + + const viewDetailsButton = await queryByText(findByTKey('VIEW_TRANSACTION_DETAILS')).with({ + timeout: FIXTURES_CONST.TIMEOUT + }); + await t.expect(viewDetailsButton.exists).ok(); + await t.click(viewDetailsButton); + + await t + .expect(queryAllByTestId('SUCCESS').with({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }).exists) + .ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); +}); + +fixture('Deploy Contracts') + .clientScripts({ content: injectLS(FIXTURE_HARDHAT) }) + .page(PAGES.DEPLOY_CONTRACTS); + +test('Can deploy contract', async (t) => { + await resetFork(); + await contractsPage.waitDeployLoaded(); + await contractsPage.setupMock(); + + await contractsPage.fillDeployForm(); + + await t.wait(FIXTURES_CONST.TIMEOUT); + + const button = await queryAllByText(findByTKey('NAV_DEPLOYCONTRACT')).nth(1); + await t.click(button); + + const signBtn = await queryByText(findByTKey('CONFIRM_AND_SEND')).with({ + timeout: FIXTURES_CONST.HARDHAT_TIMEOUT + }); + await t.expect(signBtn.exists).ok(); + await t.click(signBtn); + + const viewDetailsButton = await queryByText(findByTKey('VIEW_TRANSACTION_DETAILS')).with({ + timeout: FIXTURES_CONST.TIMEOUT + }); + await t.expect(viewDetailsButton.exists).ok(); + await t.click(viewDetailsButton); + + await t + .expect(queryAllByTestId('SUCCESS').with({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }).exists) + .ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); +}); diff --git a/__tests__/dashboard-page.po.js b/__tests__/dashboard-page.po.js new file mode 100644 index 00000000000..a82ca6e209d --- /dev/null +++ b/__tests__/dashboard-page.po.js @@ -0,0 +1,29 @@ +import { getAllByText } from '@testing-library/testcafe'; +import { Selector, t } from 'testcafe'; + +import BasePage from './base-page.po'; +import { FIXTURES_CONST, PAGES } from './fixtures'; + +export default class DashboardPage extends BasePage { + async navigateToPage() { + this.navigateTo(PAGES.DASHBOARD); + } + + async waitPageLoaded(timeout) { + await this.waitForPage(PAGES.DASHBOARD, timeout); + } + + async expectAddressToBePresent(address) { + await t.expect(getAllByText(address.substring(0, 6), { exact: false }).exists).ok(); + } + + async expectAccountTableToMatchCount(count) { + await t + .expect(Selector('section[data-testid="account-list"] > div > table tbody tr').count) + .eql(count); + } + + async expectBalanceInBalanceList(token) { + await t.expect(Selector('div').withText(token).exists).ok({ timeout: FIXTURES_CONST.TIMEOUT }); + } +} diff --git a/__tests__/dashboard.test.js b/__tests__/dashboard.test.js new file mode 100644 index 00000000000..3d40db2439a --- /dev/null +++ b/__tests__/dashboard.test.js @@ -0,0 +1,37 @@ +import { getByText } from '@testing-library/testcafe'; + +import AddAccountPage from './addaccount-page.po'; +import { injectLS } from './clientScripts'; +import DashboardPage from './dashboard-page.po'; +import { FIXTURE_LOCALSTORAGE_WITH_ONE_ACC, PAGES } from './fixtures'; +import { getTransValueByKey } from './translation-utils'; + +const addAccountPage = new AddAccountPage(); +const dashboardPage = new DashboardPage(); + +/** + * Test that the app redirects when no accounts in database. + */ +fixture('Dashboard').page(PAGES.DASHBOARD); + +test('Should redirect to add-account', async (t) => { + await addAccountPage.waitPageLoaded(); + + const title = getByText(getTransValueByKey('DECRYPT_ACCESS')); + await t.expect(title.exists).ok(); +}); + +/** + * Test that the app can load with an existing database. + */ +fixture('Dashboard') + .clientScripts({ content: injectLS(FIXTURE_LOCALSTORAGE_WITH_ONE_ACC) }) + .page(PAGES.DASHBOARD); + +test('Should load dashboard', async () => { + await dashboardPage.waitPageLoaded(); + + const address = Object.values(FIXTURE_LOCALSTORAGE_WITH_ONE_ACC.accounts)[0].address; + await dashboardPage.expectAddressToBePresent(address); + await dashboardPage.expectAccountTableToMatchCount(1); +}); diff --git a/__tests__/demo.test.js b/__tests__/demo.test.js new file mode 100644 index 00000000000..46129d43466 --- /dev/null +++ b/__tests__/demo.test.js @@ -0,0 +1,20 @@ +import { getByText } from '@testing-library/testcafe'; + +import DashboardPage from './dashboard-page.po'; +import { PAGES } from './fixtures'; +import { findByTKey } from './translation-utils'; + +const dashboardPage = new DashboardPage(); + +fixture('Demo Mode').page(PAGES.ADD_ACCOUNT); + +test('Can prompt to use demo mode', async (t) => { + const button = getByText(findByTKey('DEMO_BUTTON_TEXT')); + await t.expect(button.exists).ok(); +}); + +test('Can redirect to dashboard when activated', async (t) => { + await t.click(getByText(findByTKey('DEMO_BUTTON_TEXT'))); + await dashboardPage.waitPageLoaded(); + await dashboardPage.expectAccountTableToMatchCount(4); +}); diff --git a/__tests__/ethereum-mock.js b/__tests__/ethereum-mock.js new file mode 100644 index 00000000000..ea50adcecc6 --- /dev/null +++ b/__tests__/ethereum-mock.js @@ -0,0 +1,20 @@ +import { ClientFunction, t } from 'testcafe'; + +import { FIXTURES_CONST } from './fixtures'; +import { waitForProperty } from './helpers'; + +const NETWORKS = { + 1: 'http://127.0.0.1:8546/' +}; + +const _setupEthereumMock = ClientFunction((privateKey, chainId, rpcUrl) => { + window.ethereum.initialize(privateKey, chainId, rpcUrl); +}); + +export const setupEthereumMock = async (privateKey, chainId) => { + await t + .expect(waitForProperty((w) => w.ethereum.initialize)) + .ok({ timeout: FIXTURES_CONST.TIMEOUT }); + const rpcUrl = NETWORKS[chainId]; + await _setupEthereumMock(privateKey, chainId, rpcUrl); +}; diff --git a/__tests__/export.test.js b/__tests__/export.test.js new file mode 100644 index 00000000000..119dee948b6 --- /dev/null +++ b/__tests__/export.test.js @@ -0,0 +1,86 @@ +import { getByTestId, getByText } from '@testing-library/testcafe'; +import { existsSync, readFileSync, unlinkSync } from 'fs'; +import { homedir } from 'os'; +import { join } from 'path'; +import { lensPath, omit, pipe, set } from 'ramda'; +import { ClientFunction } from 'testcafe'; +import timeLimit from 'time-limit-promise'; + +import { injectLS } from './clientScripts'; +import { FIXTURE_LOCALSTORAGE_WITH_ONE_ACC, FIXTURES_CONST, PAGES } from './fixtures'; +import SettingsPage from './settings-page.po'; +import { getTransValueByKey } from './translation-utils'; + +function getFilePath(fileName) { + return join(homedir(), 'Downloads', fileName); +} + +async function waitForFile(path) { + while (!existsSync(path)) await setTimeout(() => {}, FIXTURES_CONST.TIMEOUT); +} + +async function waitForFileWithTimeout(path, ms) { + await timeLimit(waitForFile(path), ms, { + rejectWith: new Error('Timed out when waiting for a file') + }); +} + +const settingsPage = new SettingsPage(); + +fixture('Settings/Export') + .clientScripts({ content: injectLS(FIXTURE_LOCALSTORAGE_WITH_ONE_ACC) }) + .page(PAGES.SETTINGS); + +test('Can export AppState to file', async (t) => { + await settingsPage.waitPageLoaded(); + + // Navigate to Export page + const getCurrentLocationUrl = ClientFunction(() => window.location.href); + await t + .click(getByText(getTransValueByKey('SETTINGS_EXPORT_LABEL'))) + .expect(getCurrentLocationUrl()) + .eql(PAGES.SETTINGS_EXPORT, { + timeout: FIXTURES_CONST.TIMEOUT + }); + + // Assert the json is displayed in a code block + await t.expect(getByTestId('export-json-display').exists).ok(); + + // Download file + const downloadBtn = getByTestId('export-json-link'); // Button is inside link, so to acess attr we use data-testid + await t.expect(downloadBtn.exists).ok(); + await t.click(downloadBtn); + + // Check for file download every x seconds and assert it actual contents match fixture + const fileName = await downloadBtn.getAttribute('download'); + const filePath = getFilePath(fileName); + await waitForFileWithTimeout(filePath, FIXTURES_CONST.TIMEOUT); + + // mtime and balances are dynamic values. + // remove them before we assert equality. + const removeKeysFromAccountAsset = omit(['mtime', 'balance']); + const accountAssetsLens = lensPath([ + 'accounts', + '1782c060-8bc0-55d6-8078-ff255b4aae90', // First account in account object. + 'assets', + 0 // First asset in asset list. + ]); + + // analyticsUserID is a dynamic value. + // remove it before we assert equality. + const removeKeysFromAccountSettings = omit(['analyticsUserID']); + const accountSettingsLens = lensPath(['settings']); + + const omitDynamicValues = pipe( + omit(['mtime', 'promoPoaps', 'notifications', 'connections', 'claims']), + set(accountAssetsLens, removeKeysFromAccountAsset), + set(accountSettingsLens, removeKeysFromAccountSettings) + ); + + const actual = omitDynamicValues(JSON.parse(readFileSync(filePath))); + const expected = omitDynamicValues(FIXTURE_LOCALSTORAGE_WITH_ONE_ACC); + await t.expect(actual).eql(expected); + + // Clean up + unlinkSync(filePath); +}); diff --git a/__tests__/featureflag-utils.js b/__tests__/featureflag-utils.js new file mode 100644 index 00000000000..f69cbb64896 --- /dev/null +++ b/__tests__/featureflag-utils.js @@ -0,0 +1,21 @@ +import { ClientFunction, t } from 'testcafe'; + +import { FIXTURES_CONST } from './fixtures'; +import { waitForProperty } from './helpers'; + +// Accesses functions available on window object that change state in FeatureFlagProvider +const setFeatureFlag = async (key, value) => { + await t.expect(waitForProperty('setFeatureFlag', FIXTURES_CONST.TIMEOUT)).ok(); + await ClientFunction((key, value) => { + window.setFeatureFlag(key, value); + })(key, value); +}; + +const resetFeatureFlags = async () => { + await t.expect(waitForProperty('resetFeatureFlags', FIXTURES_CONST.TIMEOUT)).ok(); + await ClientFunction(() => { + window.resetFeatureFlags(); + }); +}; + +export { setFeatureFlag, resetFeatureFlags }; diff --git a/__tests__/fixtures.js b/__tests__/fixtures.js new file mode 100644 index 00000000000..8f4948cfc64 --- /dev/null +++ b/__tests__/fixtures.js @@ -0,0 +1,481 @@ +require('dotenv').config(); + +const ENV = { + // Defined in github/workflow to run against mycryptobuilds + E2E_BASE_URL: process.env.E2E_BASE_URL +}; + +const ENV_KEYS = Object.keys(ENV); +if (ENV_KEYS.some((k) => !ENV[k])) { + const envKeysUnset = ENV_KEYS.filter((k) => !ENV[k]); + throw Error(`Required ENV variables to be set: '${envKeysUnset.join("','")}'`); +} + +const FIXTURES_CONST = { + TIMEOUT: 5000, + HARDHAT_TIMEOUT: 60000, + BASE_URL: ENV.E2E_BASE_URL +}; + +const PAGES = { + DASHBOARD: `${FIXTURES_CONST.BASE_URL}/dashboard`, + SETTINGS: `${FIXTURES_CONST.BASE_URL}/settings`, + SETTINGS_EXPORT: `${FIXTURES_CONST.BASE_URL}/settings/export`, + SETTINGS_IMPORT: `${FIXTURES_CONST.BASE_URL}/settings/import`, + ADD_ACCOUNT_MNEMONIC: `${FIXTURES_CONST.BASE_URL}/add-account/mnemonic_phrase`, + ADD_ACCOUNT_VIEWONLY: `${FIXTURES_CONST.BASE_URL}/add-account/view_only`, + ADD_ACCOUNT_PRIVATE_KEY: `${FIXTURES_CONST.BASE_URL}/add-account/private_key`, + ADD_ACCOUNT_KEYSTORE: `${FIXTURES_CONST.BASE_URL}/add-account/keystore_file`, + ADD_ACCOUNT_WEB3: `${FIXTURES_CONST.BASE_URL}/add-account/web3`, + SEND: `${FIXTURES_CONST.BASE_URL}/send`, + ADD_ACCOUNT: `${FIXTURES_CONST.BASE_URL}/add-account`, + TX_STATUS: `${FIXTURES_CONST.BASE_URL}/tx-status`, + SWAP: `${FIXTURES_CONST.BASE_URL}/swap`, + BUY_MEMBERSHIP: `${FIXTURES_CONST.BASE_URL}/membership/buy`, + INTERACT_WITH_CONTRACTS: `${FIXTURES_CONST.BASE_URL}/interact-with-contracts`, + DEPLOY_CONTRACTS: `${FIXTURES_CONST.BASE_URL}/deploy-contracts`, + MIGRATE: `${FIXTURES_CONST.BASE_URL}/migrate` +}; + +const FIXTURE_ETHEREUM = 'Ethereum'; + +// MyCrypto Dev Testing Account with multiple tokens - should probably be swapped out for a dedicated E2E testing account +const FIXTURE_VIEW_ONLY_ADDRESS = '0x82D69476357A03415E92B5780C89e5E9e972Ce75'; + +const FIXTURE_VIEW_ONLY_TOKENS = ['ANT', 'BAT', 'DAI']; + +const FIXTURE_SEND_CONTACT = 'Web3 Account 1'; + +const FIXTURE_SEND_ADDRESS = '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520'; + +const FIXTURE_SEND_AMOUNT = '0.001'; + +const FIXTURE_WEB3_ADDRESS = '0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf'; + +const FIXTURE_MYC_STORAGE_KEY = 'MYC_Storage'; + +const FIXTURE_HARDHAT_PRIVATE_KEY = + '0xeaf2c50dfd10524651e7e459c1286f0c2404eb0f34ffd2a1eb14373db49fceb6'; + +const FIXTURE_LOCALSTORAGE_WITH_ONE_ACC = { + version: 'v2.0.0', + accounts: { + '1782c060-8bc0-55d6-8078-ff255b4aae90': { + address: '0x32F08711dC8ca3EB239e01f427AE3713DB1f6Be3', + networkId: 'Goerli', + wallet: 'WEB3', + path: { + name: 'Default (ETH)', + path: "m/44'/60'/0'/0/" + }, + index: 0, + assets: [ + { + ticker: 'GoerliETH', + name: 'GoerliETH', + decimal: 18, + networkId: 'Goerli', + type: 'base', + isCustom: false, + uuid: 'ac3478d6-9a3c-51fa-a2e6-0f5c3696165a', + balance: '10119688100000000000', + mtime: 1614873218615 + } + ], + transactions: [], + favorite: false, + mtime: 0, + uuid: '1782c060-8bc0-55d6-8078-ff255b4aae90', + label: 'Web3 Account 1' + } + }, + addressBook: { + 'a1acf1f2-0380-5bd6-90c3-2b4a0974a6fe': { + label: 'MyCrypto Team Tip Jar', + address: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520', + notes: "This is MyCrypto's Donate address. Feel free to delete it!", + network: 'Ethereum', + uuid: 'a1acf1f2-0380-5bd6-90c3-2b4a0974a6fe' + }, + 'b6260cfc-c6ff-b385-af20-1cc95e308b33': { + label: 'Web3 Account 1', + address: '0x32F08711dC8ca3EB239e01f427AE3713DB1f6Be3', + notes: '', + network: 'Goerli', + uuid: 'b6260cfc-c6ff-b385-af20-1cc95e308b33' + } + }, + assets: {}, + rates: {}, + trackedAssets: {}, + contracts: {}, + networks: {}, + notifications: { + '1c44bb44-02d4-0db1-b7dd-f3d001ef877b': { + uuid: '1c44bb44-02d4-0db1-b7dd-f3d001ef877b', + template: 'wallet-added', + templateData: { address: '0x88F7B1E26c3A52CA3cD8aF4ba1b448391eb31d88' }, + dateDisplayed: '2020-06-04T09:58:09.485Z', + dismissed: false + }, + '4cce94f3-f9b7-ff5f-2ad4-74d881376e7c': { + uuid: '4cce94f3-f9b7-ff5f-2ad4-74d881376e7c', + template: 'onboarding-responsible', + templateData: { firstDashboardVisitDate: '2020-06-04T09:58:09.575Z' }, + dateDisplayed: '2020-06-04T09:58:09.575Z', + dismissed: false + } + }, + settings: { + canTrackProductAnalytics: true, + fiatCurrency: 'USD', + darkMode: false, + dashboardAccounts: ['1782c060-8bc0-55d6-8078-ff255b4aae90'], + excludedAssets: [], + language: 'en', + isDemoMode: false + }, + networkNodes: {}, + userActions: {}, + mtime: 1607526708529 +}; + +const FIXTURE_HARDHAT = { + version: 'v2.0.0', + accounts: { + '4a236c90-b722-5e26-8718-659db6b5d302': { + address: '0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf', + networkId: 'Ethereum', + wallet: 'WEB3', + path: { + name: 'Default (ETH)', + path: "m/44'/60'/0'/0/" + }, + index: 0, + assets: [ + { + ticker: 'ETH', + name: 'Ether (Ethereum)', + decimal: 18, + support: {}, + social: {}, + networkId: 'Ethereum', + type: 'base', + mappings: { + coinCapId: 'ethereum', + coinGeckoId: 'ethereum', + cryptoCompareId: 'ETH', + cryptoCurrencyIconName: 'eth', + dexAgId: 'ETH' + }, + isCustom: false, + uuid: '356a192b-7913-504c-9457-4d18c28d46e6', + balance: '9998866308480000000000', + mtime: 1621347441875 + }, + { + ticker: 'DAI', + name: 'DAI Stablecoin', + decimal: 18, + support: {}, + social: {}, + networkId: 'Ethereum', + type: 'erc20', + isCustom: false, + contractAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + uuid: 'e1f698bf-cb85-5405-b563-14774af14bf1', + balance: '9998866308480000000000', + mtime: 1621347441875 + }, + { + ticker: 'LEND', + name: 'EthLend', + decimal: 18, + support: {}, + social: {}, + networkId: 'Ethereum', + type: 'erc20', + isCustom: false, + contractAddress: '0x80fB784B7eD66730e8b1DBd9820aFD29931aab03', + uuid: '1c77b322-a88c-57cc-b956-78c2bc17c360', + balance: '9998866308480000000000', + mtime: 1621347441875 + } + ], + transactions: [], + favorite: false, + mtime: 0, + uuid: '4a236c90-b722-5e26-8718-659db6b5d302', + label: 'Web3 Account 1' + } + }, + addressBook: { + 'a1acf1f2-0380-5bd6-90c3-2b4a0974a6fe': { + label: 'MyCrypto Team Tip Jar', + address: '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520', + notes: "This is MyCrypto's Donate address. Feel free to delete it!", + network: 'Ethereum', + uuid: 'a1acf1f2-0380-5bd6-90c3-2b4a0974a6fe' + }, + 'b6260cfc-c6ff-b385-af20-1cc95e308b33': { + label: 'Web3 Account 1', + address: '0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf', + notes: '', + network: 'Ethereum', + uuid: '4a236c90-b722-5e26-8718-659db6b5d302' + } + }, + assets: {}, + rates: {}, + trackedAssets: {}, + contracts: {}, + networks: { + Ethereum: { + id: 'Ethereum', + selectedNode: 'ethereum_Hardhat', + nodes: [ + { + url: 'http://127.0.0.1:8546/', + service: 'Hardhat', + name: 'ethereum_Hardhat', + isCustom: true, + type: 'myccustom' + } + ] + } + }, + notifications: { + '1c44bb44-02d4-0db1-b7dd-f3d001ef877b': { + uuid: '1c44bb44-02d4-0db1-b7dd-f3d001ef877b', + template: 'wallet-added', + templateData: { address: '0x88F7B1E26c3A52CA3cD8aF4ba1b448391eb31d88' }, + dateDisplayed: '2020-06-04T09:58:09.485Z', + dismissed: false + }, + '4cce94f3-f9b7-ff5f-2ad4-74d881376e7c': { + uuid: '4cce94f3-f9b7-ff5f-2ad4-74d881376e7c', + template: 'onboarding-responsible', + templateData: { firstDashboardVisitDate: '2020-06-04T09:58:09.575Z' }, + dateDisplayed: '2020-06-04T09:58:09.575Z', + dismissed: false + } + }, + settings: { + canTrackProductAnalytics: true, + fiatCurrency: 'USD', + darkMode: false, + dashboardAccounts: ['4a236c90-b722-5e26-8718-659db6b5d302'], + excludedAssets: [], + language: 'en', + isDemoMode: false + }, + userActions: {} +}; + +const FIXTURE_CONTRACT_BYTECODE = `0x608060405234801561001057600080fd5b5061017c806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063ef5fb05b14610030575b600080fd5b61003861004e565b60405161004591906100c4565b60405180910390f35b60606040518060400160405280600c81526020017f48656c6c6f20576f726c64210000000000000000000000000000000000000000815250905090565b6000610096826100e6565b6100a081856100f1565b93506100b0818560208601610102565b6100b981610135565b840191505092915050565b600060208201905081810360008301526100de818461008b565b905092915050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610120578082015181840152602081019050610105565b8381111561012f576000848401525b50505050565b6000601f19601f830116905091905056fea26469706673582212201a6960a39a385911e5d30972e41b9ee5841bb76084125663c0df508f87da619e64736f6c63430008030033`; + +const FIXTURE_ERC20_ABI = [ + { + name: 'decimals', + type: 'function', + + constant: true, + payable: false, + inputs: [], + + outputs: [ + { + name: '', + type: 'uint8' + } + ] + }, + { + name: 'symbol', + type: 'function', + constant: true, + payable: false, + + inputs: [], + + outputs: [ + { + name: '', + type: 'string' + } + ] + }, + + { + name: 'balanceOf', + type: 'function', + constant: true, + payable: false, + + inputs: [ + { + name: '_owner', + type: 'address' + } + ], + + outputs: [ + { + name: 'balance', + type: 'uint256' + } + ] + }, + + { + name: 'transfer', + type: 'function', + constant: false, + payable: false, + + inputs: [ + { + name: '_to', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + + outputs: [ + { + name: 'success', + type: 'bool' + } + ] + }, + { + name: 'transferFrom', + type: 'function', + constant: false, + payable: false, + + inputs: [ + { + name: '_from', + type: 'address' + }, + { + name: '_to', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + + outputs: [ + { + name: 'success', + type: 'bool' + } + ] + }, + { + name: 'Transfer', + type: 'event', + anonymous: false, + inputs: [ + { + indexed: true, + name: '_from', + type: 'address' + }, + { + indexed: true, + name: '_to', + type: 'address' + }, + { + indexed: false, + name: '_value', + type: 'uint256' + } + ] + }, + { + constant: false, + inputs: [ + { + name: '_spender', + type: 'address' + }, + { + name: '_value', + type: 'uint256' + } + ], + name: 'approve', + outputs: [ + { + name: '', + type: 'bool' + } + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function' + }, + + { + name: 'allowance', + type: 'function', + constant: true, + payable: false, + stateMutability: 'view', + + inputs: [ + { + name: '_owner', + type: 'address' + }, + { + name: '_spender', + type: 'address' + } + ], + + outputs: [ + { + name: 'allowance', + type: 'uint256' + } + ] + } +]; + +const FIXTURE_ERC20_ABI_JSON = JSON.stringify(FIXTURE_ERC20_ABI); + +const FIXTURE_DAI_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; +const FIXTURE_CONTRACT_SPENDER = '0x7a250d5630b4cf539739df2c5dacb4c659f2488d'; +const FIXTURE_CONTRACT_ALLOWANCE = '1000000000000000000'; + +export { + ENV, + FIXTURES_CONST, + PAGES, + FIXTURE_SEND_CONTACT, + FIXTURE_SEND_ADDRESS, + FIXTURE_SEND_AMOUNT, + FIXTURE_LOCALSTORAGE_WITH_ONE_ACC, + FIXTURE_MYC_STORAGE_KEY, + FIXTURE_ETHEREUM, + FIXTURE_VIEW_ONLY_ADDRESS, + FIXTURE_VIEW_ONLY_TOKENS, + FIXTURE_WEB3_ADDRESS, + FIXTURE_HARDHAT_PRIVATE_KEY, + FIXTURE_HARDHAT, + FIXTURE_CONTRACT_BYTECODE, + FIXTURE_ERC20_ABI_JSON, + FIXTURE_DAI_ADDRESS, + FIXTURE_CONTRACT_SPENDER, + FIXTURE_CONTRACT_ALLOWANCE +}; diff --git a/__tests__/fixtures/persisted.json b/__tests__/fixtures/persisted.json new file mode 100644 index 00000000000..6d6b8ea32f2 --- /dev/null +++ b/__tests__/fixtures/persisted.json @@ -0,0 +1,2136 @@ +{ + "version": "v2.0.0", + "mtime": 1606324295478, + "accounts": { + "ef94a882-3af7-5887-89f0-be9833c6b071": { + "address": "0x71810d732c6f0a799000f583e77fc301d4d8c1c8", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "ticker": "ETH", + "name": "Ether (Ethereum)", + "networkId": "Ethereum", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "balance": { + "_hex": "0x00" + }, + "mtime": 1606324232650 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "ef94a882-3af7-5887-89f0-be9833c6b071", + "label": "MetaMask Account 10" + }, + "5038d1b0-9afb-597f-8a0f-0f879390873f": { + "address": "0x9c9c3d1576de533427b4adcb5e6bfda935a86c78", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "ticker": "REP", + "name": "Augur", + "decimal": 18, + "contractAddress": "0x1985365e9f78359a9B6AD760e32412f4a445E862", + "website": "https://augur.net", + "support": {}, + "social": { + "reddit": "https://www.reddit.com/r/Augur", + "slack": "https://invite.augur.net", + "twitter": "https://twitter.com/AugurProject" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinCapId": "augur", + "coinGeckoId": "augur", + "cryptoCompareId": "REP", + "cryptoCurrencyIconName": "rep", + "dexAgId": "REP" + }, + "isCustom": false, + "uuid": "d017a1e8-bdd3-5c32-8866-da258f75b0e9", + "balance": { + "_hex": "0x0de0b6b3a7640000" + }, + "mtime": 1606324232650 + }, + { + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "ticker": "ETH", + "name": "Ether (Ethereum)", + "networkId": "Ethereum", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "balance": { + "_hex": "0x00" + }, + "mtime": 1606324232650 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "5038d1b0-9afb-597f-8a0f-0f879390873f", + "label": "MetaMask Account 11" + }, + "5034876a-d91c-5b4b-9851-cce700717e4b": { + "address": "0x48b599afcc9e2b6d7b549bf0845ce687700b954e", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "ticker": "ETH", + "name": "Ether (Ethereum)", + "networkId": "Ethereum", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "balance": { + "_hex": "0x00" + }, + "mtime": 1606324232650 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "5034876a-d91c-5b4b-9851-cce700717e4b", + "label": "MetaMask Account 12" + }, + "7b9d75ac-ddfd-51bb-a9b8-30e3266fa075": { + "address": "0x82d69476357a03415e92b5780c89e5e9e972ce75", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "ticker": "uniDAI", + "name": "Uniswap: Dai Stablecoin", + "decimal": 18, + "contractAddress": "0x2a1530C4C41db0B0b2bB646CB5Eb1A67b7158667", + "website": "https://www.uniswap.io", + "support": { + "email": "contact@uniswap.io" + }, + "social": { + "blog": "https://medium.com/uniswap", + "github": "https://github.com/Uniswap", + "reddit": "https://www.reddit.com/r/UniSwap/", + "twitter": "https://twitter.com/UniswapExchange" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": {}, + "isCustom": false, + "uuid": "2b7a4d65-9c40-5c21-96eb-f7d380a4dc87", + "balance": { + "_hex": "0x902358479aa7a3" + }, + "mtime": 1606324232650 + }, + { + "ticker": "AMB", + "name": "Amber Token", + "decimal": 18, + "contractAddress": "0x4DC3643DbC642b72C158E7F3d2ff232df61cb6CE", + "website": "https://ambrosus.com/index.html", + "support": {}, + "social": { + "blog": "https://blog.ambrosus.com/", + "facebook": "https://www.facebook.com/ambrosusAMB", + "github": "https://github.com/ambrosus", + "linkedin": "https://www.linkedin.com/company/ambrosus", + "reddit": "https://www.reddit.com/r/ambrosus/", + "telegram": "https://t.me/AmbrosusAMB", + "twitter": "https://twitter.com/AmbrosusAMB", + "youtube": "https://youtu.be/OkdCV6zw3lI" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "cryptoCompareId": "AMB", + "cryptoCurrencyIconName": "amb" + }, + "isCustom": false, + "uuid": "4e7e2121-1f46-528b-bfe2-1fb1e2b512fb", + "balance": { + "_hex": "0x016345785d8a0000" + }, + "mtime": 1606324232650 + }, + { + "ticker": "ABT", + "name": "ArcBlock Token", + "decimal": 18, + "contractAddress": "0xB98d4C97425d9908E66E53A6fDf673ACcA0BE986", + "website": "https://www.arcblock.io", + "support": { + "email": "contact@arcblock.io" + }, + "social": {}, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinCapId": "arcblock", + "coinGeckoId": "arcblock", + "cryptoCompareId": "ABT", + "cryptoCurrencyIconName": "abt", + "dexAgId": "ABT" + }, + "isCustom": false, + "uuid": "4f387b74-88e2-5a56-9664-0224ee546c6a", + "balance": { + "_hex": "0x0d810265adbf539d" + }, + "mtime": 1606324232650 + }, + { + "ticker": "ANT (old)", + "name": "Aragon", + "decimal": 18, + "contractAddress": "0x960b236A07cf122663c4303350609A66A7B288C0", + "website": "https://aragon.org", + "support": { + "email": "support@aragon.org" + }, + "social": { + "blog": "https://aragon.org/blog", + "chat": "https://aragon.chat/", + "forum": "https://forum.aragon.org/", + "github": "https://github.com/aragon/", + "linkedin": "https://www.linkedin.com/company/aragonproject/", + "reddit": "https://www.reddit.com/r/aragonproject/", + "twitter": "https://twitter.com/AragonProject", + "youtube": "https://www.youtube.com/channel/UCODiU_-FWhr4SVOoBlm-qaQ" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinCapId": "aragon", + "coinGeckoId": "aragon", + "cryptoCompareId": "ANT", + "cryptoCurrencyIconName": "ant", + "dexAgId": "ANT" + }, + "isCustom": false, + "uuid": "7f4d2f19-9a76-5930-b63b-ac9b0afe6201", + "balance": { + "_hex": "0x4bfb1b7e01438000" + }, + "mtime": 1606324232650 + }, + { + "ticker": "cDAI", + "name": "Compound Dai", + "decimal": 8, + "contractAddress": "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", + "support": {}, + "social": {}, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinGeckoId": "cdai", + "cryptoCompareId": "CDAI", + "dexAgId": "CDAI" + }, + "isCustom": false, + "uuid": "a9cc6884-14bd-53b6-abcd-f9b56b60463d", + "balance": { + "_hex": "0x068b7f8a9b" + }, + "mtime": 1606324232650 + }, + { + "ticker": "LINK (Chainlink)", + "name": "LINK Chainlink", + "decimal": 18, + "contractAddress": "0x514910771AF9Ca656af840dff83E8264EcF986CA", + "website": "https://link.smartcontract.com", + "support": { + "email": "support@smartcontract.com" + }, + "social": { + "slack": "https://chainlinknetwork.slack.com" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinCapId": "chainlink", + "coinGeckoId": "chainlink", + "cryptoCompareId": "LINK" + }, + "isCustom": false, + "uuid": "c0ae47d5-f020-5ea8-9886-522d3af70845", + "balance": { + "_hex": "0x3590c346e35b" + }, + "mtime": 1606324232650 + }, + { + "ticker": "unisETH", + "name": "Uniswap: Synth sETH", + "decimal": 18, + "contractAddress": "0xe9Cf7887b93150D4F2Da7dFc6D502B216438F244", + "website": "https://www.uniswap.io", + "support": { + "email": "contact@uniswap.io" + }, + "social": { + "blog": "https://medium.com/uniswap", + "github": "https://github.com/Uniswap", + "reddit": "https://www.reddit.com/r/UniSwap/", + "twitter": "https://twitter.com/UniswapExchange" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": {}, + "isCustom": false, + "uuid": "ca27272a-891e-577d-ae75-f8efe4d55231", + "balance": { + "_hex": "0x58c83319ffacc3" + }, + "mtime": 1606324232650 + }, + { + "ticker": "DAI", + "name": "Dai Stablecoin v2.0", + "decimal": 18, + "contractAddress": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "website": "https://makerdao.com", + "support": { + "email": "support@makerdao.com", + "url": "https://chat.makerdao.com" + }, + "social": { + "chat": "https://chat.makerdao.com", + "github": "https://github.com/makerdao", + "reddit": "https://www.reddit.com/r/MakerDAO", + "twitter": "https://twitter.com/MakerDAO" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinCapId": "dai", + "coinGeckoId": "dai", + "cryptoCompareId": "DAI", + "cryptoCurrencyIconName": "dai", + "dexAgId": "DAI" + }, + "isCustom": false, + "uuid": "e1f698bf-cb85-5405-b563-14774af14bf1", + "balance": { + "_hex": "0x025bfb3601c32f1d" + }, + "mtime": 1606324232650 + }, + { + "ticker": "BAT", + "name": "Basic Attention Token", + "decimal": 18, + "contractAddress": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + "website": "https://basicattentiontoken.org", + "support": {}, + "social": { + "reddit": "https://www.reddit.com/r/BATProject", + "twitter": "https://twitter.com/attentiontoken" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinCapId": "basic-attention-token", + "coinGeckoId": "basic-attention-token", + "cryptoCompareId": "BAT", + "cryptoCurrencyIconName": "bat", + "dexAgId": "BAT" + }, + "isCustom": false, + "uuid": "e34bb9ff-47be-503b-956b-30d4b3f3507b", + "balance": { + "_hex": "0x571f0b44ebf42873" + }, + "mtime": 1606324232650 + }, + { + "ticker": "KICK", + "name": "KickToken", + "decimal": 8, + "contractAddress": "0xC12D1c73eE7DC3615BA4e37E4ABFdbDDFA38907E", + "website": "https://www.kickico.com", + "support": { + "email": "support@kickico.com" + }, + "social": { + "blog": "https://medium.com/@kickecosystem", + "chat": "https://t.me/kickico", + "facebook": "https://www.facebook.com/kickecosystem", + "github": "https://github.com/kickecosystem/", + "linkedin": "https://www.linkedin.com/company/kickecosystem/", + "reddit": "https://www.reddit.com/r/KICKICO_Platform/", + "telegram": "https://t.me/kickico", + "twitter": "https://twitter.com/KickEcosystem" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "cryptoCompareId": "KICK" + }, + "isCustom": false, + "uuid": "f383cc01-4942-5d0e-8442-3dbbc8cc8836", + "balance": { + "_hex": "0x50d809593800" + }, + "mtime": 1606324232650 + }, + { + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "ticker": "ETH", + "name": "Ether (Ethereum)", + "networkId": "Ethereum", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "balance": { + "_hex": "0x03f0a45449eeb6" + }, + "mtime": 1606324232650 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "7b9d75ac-ddfd-51bb-a9b8-30e3266fa075", + "label": "Test" + }, + "faceb7fa-2551-561d-8374-3cfcb7fd9cf8": { + "address": "0xfeac75a09662396283f4bb50f0a9249576a81866", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "ticker": "sETH", + "name": "Synth sETH", + "decimal": 18, + "contractAddress": "0x5e74C9036fb86BD7eCdcb084a0673EFc32eA31cb", + "support": {}, + "social": {}, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinGeckoId": "seth", + "dexAgId": "SETH" + }, + "isCustom": false, + "uuid": "19c48978-796d-55c6-a0e2-9999d1592728", + "balance": { + "_hex": "0x24cb4cc5cdb644" + }, + "mtime": 1606324232650 + }, + { + "ticker": "ABT", + "name": "ArcBlock Token", + "decimal": 18, + "contractAddress": "0xB98d4C97425d9908E66E53A6fDf673ACcA0BE986", + "website": "https://www.arcblock.io", + "support": { + "email": "contact@arcblock.io" + }, + "social": {}, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinCapId": "arcblock", + "coinGeckoId": "arcblock", + "cryptoCompareId": "ABT", + "cryptoCurrencyIconName": "abt", + "dexAgId": "ABT" + }, + "isCustom": false, + "uuid": "4f387b74-88e2-5a56-9664-0224ee546c6a", + "balance": { + "_hex": "0x027dc459dc5275a28e" + }, + "mtime": 1606324232650 + }, + { + "ticker": "sETH", + "name": "Synth sETH", + "decimal": 18, + "contractAddress": "0xD81AdA188331e627567BBEF80F91217cd3109592", + "support": {}, + "social": {}, + "networkId": "Ethereum", + "type": "erc20", + "mappings": {}, + "isCustom": false, + "uuid": "6c7a1a4b-8987-57bc-a7ce-e763a6541839", + "balance": { + "_hex": "0x24cb4cc5cdb644" + }, + "mtime": 1606324232650 + }, + { + "ticker": "ANT (old)", + "name": "Aragon", + "decimal": 18, + "contractAddress": "0x960b236A07cf122663c4303350609A66A7B288C0", + "website": "https://aragon.org", + "support": { + "email": "support@aragon.org" + }, + "social": { + "blog": "https://aragon.org/blog", + "chat": "https://aragon.chat/", + "forum": "https://forum.aragon.org/", + "github": "https://github.com/aragon/", + "linkedin": "https://www.linkedin.com/company/aragonproject/", + "reddit": "https://www.reddit.com/r/aragonproject/", + "twitter": "https://twitter.com/AragonProject", + "youtube": "https://www.youtube.com/channel/UCODiU_-FWhr4SVOoBlm-qaQ" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinCapId": "aragon", + "coinGeckoId": "aragon", + "cryptoCompareId": "ANT", + "cryptoCurrencyIconName": "ant", + "dexAgId": "ANT" + }, + "isCustom": false, + "uuid": "7f4d2f19-9a76-5930-b63b-ac9b0afe6201", + "balance": { + "_hex": "0x0bc18e1999fb5e8e" + }, + "mtime": 1606324232650 + }, + { + "ticker": "cDAI", + "name": "Compound Dai", + "decimal": 8, + "contractAddress": "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643", + "support": {}, + "social": {}, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinGeckoId": "cdai", + "cryptoCompareId": "CDAI", + "dexAgId": "CDAI" + }, + "isCustom": false, + "uuid": "a9cc6884-14bd-53b6-abcd-f9b56b60463d", + "balance": { + "_hex": "0x01742f03e4" + }, + "mtime": 1606324232650 + }, + { + "ticker": "BAT", + "name": "Basic Attention Token", + "decimal": 18, + "contractAddress": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + "website": "https://basicattentiontoken.org", + "support": {}, + "social": { + "reddit": "https://www.reddit.com/r/BATProject", + "twitter": "https://twitter.com/attentiontoken" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "coinCapId": "basic-attention-token", + "coinGeckoId": "basic-attention-token", + "cryptoCompareId": "BAT", + "cryptoCurrencyIconName": "bat", + "dexAgId": "BAT" + }, + "isCustom": false, + "uuid": "e34bb9ff-47be-503b-956b-30d4b3f3507b", + "balance": { + "_hex": "0x27f7fa156dd364d2" + }, + "mtime": 1606324232650 + }, + { + "ticker": "KICK", + "name": "KickToken", + "decimal": 8, + "contractAddress": "0xC12D1c73eE7DC3615BA4e37E4ABFdbDDFA38907E", + "website": "https://www.kickico.com", + "support": { + "email": "support@kickico.com" + }, + "social": { + "blog": "https://medium.com/@kickecosystem", + "chat": "https://t.me/kickico", + "facebook": "https://www.facebook.com/kickecosystem", + "github": "https://github.com/kickecosystem/", + "linkedin": "https://www.linkedin.com/company/kickecosystem/", + "reddit": "https://www.reddit.com/r/KICKICO_Platform/", + "telegram": "https://t.me/kickico", + "twitter": "https://twitter.com/KickEcosystem" + }, + "networkId": "Ethereum", + "type": "erc20", + "mappings": { + "cryptoCompareId": "KICK" + }, + "isCustom": false, + "uuid": "f383cc01-4942-5d0e-8442-3dbbc8cc8836", + "balance": { + "_hex": "0x50d809593800" + }, + "mtime": 1606324232650 + }, + { + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "ticker": "ETH", + "name": "Ether (Ethereum)", + "networkId": "Ethereum", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "balance": { + "_hex": "0x00" + }, + "mtime": 1606324232650 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "faceb7fa-2551-561d-8374-3cfcb7fd9cf8", + "label": "MetaMask Account 14" + }, + "4a236c90-b722-5e26-8718-659db6b5d302": { + "address": "0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "ticker": "ETH", + "name": "Ether (Ethereum)", + "networkId": "Ethereum", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "balance": { + "_hex": "0x00" + }, + "mtime": 1606324232650 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "4a236c90-b722-5e26-8718-659db6b5d302", + "label": "MetaMask Account 16" + }, + "f1c35f92-e0ba-55e0-a3ce-085c8c767b02": { + "address": "0x71810d732c6f0a799000f583e77fc301d4d8c1c8", + "networkId": "Ropsten", + "wallet": "METAMASK", + "assets": [ + { + "uuid": "77de68da-ecd8-53ba-bbb5-8edb1c8e14d7", + "ticker": "RopstenETH", + "name": "Ether (Ropsten)", + "networkId": "Ropsten", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": {}, + "isCustom": false, + "balance": { + "_hex": "0x00" + }, + "mtime": 1606324232650 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "f1c35f92-e0ba-55e0-a3ce-085c8c767b02", + "label": "ouuiiii\\\"test\"\\eeer" + }, + "f25322d8-327c-5efa-896a-82f89c86a2f2": { + "address": "0x9c9c3d1576de533427b4adcb5e6bfda935a86c78", + "networkId": "Ropsten", + "wallet": "METAMASK", + "assets": [ + { + "uuid": "77de68da-ecd8-53ba-bbb5-8edb1c8e14d7", + "ticker": "RopstenETH", + "name": "Ether (Ropsten)", + "networkId": "Ropsten", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": {}, + "isCustom": false, + "balance": { + "_hex": "0x00" + }, + "mtime": 1606324232650 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "f25322d8-327c-5efa-896a-82f89c86a2f2", + "label": "MetaMask Account 3" + }, + "84963ccf-7ad8-53b4-8195-2720861007e6": { + "address": "0x48b599afcc9e2b6d7b549bf0845ce687700b954e", + "networkId": "Ropsten", + "wallet": "METAMASK", + "assets": [ + { + "uuid": "77de68da-ecd8-53ba-bbb5-8edb1c8e14d7", + "ticker": "RopstenETH", + "name": "Ether (Ropsten)", + "networkId": "Ropsten", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": {}, + "isCustom": false, + "balance": { + "_hex": "0x00" + }, + "mtime": 1606324232650 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "84963ccf-7ad8-53b4-8195-2720861007e6", + "label": "MetaMask Account 4" + }, + "54a618a2-6da5-57fa-893f-f10a114e27ae": { + "address": "0x82d69476357a03415e92b5780c89e5e9e972ce75", + "networkId": "Ropsten", + "wallet": "METAMASK", + "assets": [ + { + "uuid": "77de68da-ecd8-53ba-bbb5-8edb1c8e14d7", + "ticker": "RopstenETH", + "name": "Ether (Ropsten)", + "networkId": "Ropsten", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": {}, + "isCustom": false, + "balance": { + "_hex": "0x60b89bc0a8c18097" + }, + "mtime": 1606324232650 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "54a618a2-6da5-57fa-893f-f10a114e27ae", + "label": "MetaMask Account 5" + }, + "e4f87fd1-2104-5b81-af05-669ca4d7d99b": { + "address": "0xfeac75a09662396283f4bb50f0a9249576a81866", + "networkId": "Ropsten", + "wallet": "METAMASK", + "assets": [ + { + "uuid": "77de68da-ecd8-53ba-bbb5-8edb1c8e14d7", + "ticker": "RopstenETH", + "name": "Ether (Ropsten)", + "networkId": "Ropsten", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": {}, + "isCustom": false, + "balance": { + "_hex": "0x4563451c7ecac000" + }, + "mtime": 1606324232650 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "e4f87fd1-2104-5b81-af05-669ca4d7d99b", + "label": "MetaMask Account 6" + }, + "f7ebd825-d1b7-5255-ac95-5fd737ef8477": { + "address": "0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf", + "networkId": "Ropsten", + "wallet": "METAMASK", + "assets": [ + { + "uuid": "77de68da-ecd8-53ba-bbb5-8edb1c8e14d7", + "ticker": "RopstenETH", + "name": "Ether (Ropsten)", + "networkId": "Ropsten", + "type": "base", + "decimal": 18, + "support": {}, + "social": {}, + "mappings": {}, + "isCustom": false, + "balance": { + "_hex": "0x00" + }, + "mtime": 1606324232651 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "f7ebd825-d1b7-5255-ac95-5fd737ef8477", + "label": "MetaMask Account 8" + } + }, + "addressBook": { + "8be109ad-6816-45d5-ad8f-bd50ea08f4b0": { + "label": "Test", + "address": "0x82d69476357a03415e92b5780c89e5e9e972ce75", + "notes": "", + "network": "Ethereum", + "uuid": "8be109ad-6816-45d5-ad8f-bd50ea08f4b0" + }, + "6748daca-1715-450a-b042-d750a465e568": { + "label": "ouuiiii\\\"test\"\\eeer", + "address": "0x71810d732c6f0a799000f583e77fc301d4d8c1c8", + "notes": "", + "network": "Ropsten", + "uuid": "6748daca-1715-450a-b042-d750a465e568" + }, + "a1acf1f2-0380-5bd6-90c3-2b4a0974a6fe": { + "label": "MyCrypto Team Tip Jar", + "address": "0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520", + "notes": "This is MyCrypto's Donate address. Feel free to delete it!", + "network": "Ethereum", + "uuid": "a1acf1f2-0380-5bd6-90c3-2b4a0974a6fe" + }, + "9ae4141f-3098-4930-838b-36fcc6709873": { + "label": "MetaMask Account 3", + "address": "0x9c9c3d1576de533427b4adcb5e6bfda935a86c78", + "notes": "", + "network": "Ropsten", + "uuid": "9ae4141f-3098-4930-838b-36fcc6709873" + }, + "b94e1ee6-2243-44b9-83ae-7a1468bc366b": { + "label": "MetaMask Account 4", + "address": "0x48b599afcc9e2b6d7b549bf0845ce687700b954e", + "notes": "", + "network": "Ropsten", + "uuid": "b94e1ee6-2243-44b9-83ae-7a1468bc366b" + }, + "041d0e8c-1bcb-4f4f-9877-7db3304a0b44": { + "label": "MetaMask Account 5", + "address": "0x82d69476357a03415e92b5780c89e5e9e972ce75", + "notes": "", + "network": "Ropsten", + "uuid": "041d0e8c-1bcb-4f4f-9877-7db3304a0b44" + }, + "208a5a47-93e7-4c05-9c96-74084415f2aa": { + "label": "MetaMask Account 6", + "address": "0xfeac75a09662396283f4bb50f0a9249576a81866", + "notes": "", + "network": "Ropsten", + "uuid": "208a5a47-93e7-4c05-9c96-74084415f2aa" + }, + "020ce739-6d54-48a0-a371-3dcb13868a0f": { + "label": "MetaMask Account 8", + "address": "0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf", + "notes": "", + "network": "Ropsten", + "uuid": "020ce739-6d54-48a0-a371-3dcb13868a0f" + }, + "5d6aa5bb-5c13-4916-a619-cd5af1c58a47": { + "label": "MetaMask Account 10", + "address": "0x71810d732c6f0a799000f583e77fc301d4d8c1c8", + "notes": "", + "network": "Ethereum", + "uuid": "5d6aa5bb-5c13-4916-a619-cd5af1c58a47" + }, + "69c38114-7439-4071-8b85-56a0aae9ed92": { + "label": "MetaMask Account 11", + "address": "0x9c9c3d1576de533427b4adcb5e6bfda935a86c78", + "notes": "", + "network": "Ethereum", + "uuid": "69c38114-7439-4071-8b85-56a0aae9ed92" + }, + "7d3a7d76-f0be-4994-bb1f-dfa2af401c2f": { + "label": "MetaMask Account 12", + "address": "0x48b599afcc9e2b6d7b549bf0845ce687700b954e", + "notes": "", + "network": "Ethereum", + "uuid": "7d3a7d76-f0be-4994-bb1f-dfa2af401c2f" + }, + "2ca1ba00-ed0f-4660-bd26-68858b628297": { + "label": "MetaMask Account 14", + "address": "0xfeac75a09662396283f4bb50f0a9249576a81866", + "notes": "", + "network": "Ethereum", + "uuid": "2ca1ba00-ed0f-4660-bd26-68858b628297" + }, + "1c5c19c2-a2bd-47b1-9daf-0d8112c1e615": { + "label": "MetaMask Account 16", + "address": "0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf", + "notes": "", + "network": "Ethereum", + "uuid": "1c5c19c2-a2bd-47b1-9daf-0d8112c1e615" + } + }, + "assets": { + "3efdf01a-63d5-5a79-ab4e-deb84bd63c83": { + "uuid": "3efdf01a-63d5-5a79-ab4e-deb84bd63c83", + "name": "300", + "decimal": 18, + "ticker": "300", + "networkId": "Ethereum", + "contractAddress": "0xaec98a708810414878c3bcdf46aad31ded4a4557", + "type": "erc20", + "isCustom": true + } + }, + "contracts": { + "a65bd393-4f60-5f69-ae79-2bfe17e8fd3f": { + "uuid": "a65bd393-4f60-5f69-ae79-2bfe17e8fd3f", + "name": "Super duper custom contract", + "address": "0x126d852A3150C53B851eB340ea54Dc379d62f27c", + "abi": "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"router_\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"factory_\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"trader_\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"quantity\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"payout\",\"type\":\"uint256\"}],\"name\":\"FlashClosed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"quantity\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"premium\",\"type\":\"uint256\"}],\"name\":\"FlashOpened\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"optionAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"quantityOptions\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amountBMax\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amountBMin\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"}],\"name\":\"addShortLiquidityWithUnderlying\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IOption\",\"name\":\"optionToken\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amountRedeems\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"minPayout\",\"type\":\"uint256\"}],\"name\":\"closeFlashLong\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"optionAddress\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"otherToken\",\"type\":\"address\"}],\"name\":\"deployUniswapMarket\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"factory\",\"outputs\":[{\"internalType\":\"contract IUniswapV2Factory\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"pairAddress\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optionAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"flashLoanQuantity\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"minPayout\",\"type\":\"uint256\"},{\"internalType\":\"address[]\",\"name\":\"path\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"flashCloseLongOptionsThenSwap\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"pairAddress\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optionAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"flashLoanQuantity\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxPremium\",\"type\":\"uint256\"},{\"internalType\":\"address[]\",\"name\":\"path\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"flashMintShortOptionsThenSwap\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IOption\",\"name\":\"optionToken\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"quantity\",\"type\":\"uint256\"}],\"name\":\"getClosePremium\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getName\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IOption\",\"name\":\"optionToken\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"quantity\",\"type\":\"uint256\"}],\"name\":\"getOpenPremium\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token0\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token1\",\"type\":\"address\"}],\"name\":\"getUniswapMarketForTokens\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getVersion\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IOption\",\"name\":\"optionToken\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amountIn\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amountOutMin\",\"type\":\"uint256\"},{\"internalType\":\"address[]\",\"name\":\"path\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"}],\"name\":\"mintShortOptionsThenSwapToTokens\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IOption\",\"name\":\"optionToken\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amountOptions\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maxPremium\",\"type\":\"uint256\"}],\"name\":\"openFlashLong\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"optionAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"liquidity\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amountAMin\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amountBMin\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"}],\"name\":\"removeShortLiquidityThenCloseOptions\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"router\",\"outputs\":[{\"internalType\":\"contract IUniswapV2Router02\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"trader\",\"outputs\":[{\"internalType\":\"contract ITrader\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount0\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"amount1\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"uniswapV2Call\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + "networkId": "Ethereum", + "isCustom": true + } + }, + "networks": {}, + "notifications": { + "7ed39d53-d5d5-4688-98e1-21bd9acf812b": { + "uuid": "7ed39d53-d5d5-4688-98e1-21bd9acf812b", + "template": "onboarding-responsible", + "templateData": { + "firstDashboardVisitDate": "2020-11-25T16:40:30.950Z" + }, + "dateDisplayed": "2020-11-25T16:40:30.950Z", + "dismissed": false + }, + "122d4054-f650-4683-95e4-b4d0accc6be5": { + "uuid": "122d4054-f650-4683-95e4-b4d0accc6be5", + "template": "wallets-added", + "templateData": { + "accounts": [ + { + "address": "0x71810d732c6f0a799000f583e77fc301d4d8c1c8", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "ticker": "ETH", + "name": "Ether (Ethereum)", + "decimal": 18, + "support": {}, + "social": {}, + "networkId": "Ethereum", + "type": "base", + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "balance": { + "_hex": "0x00" + }, + "mtime": 1606322464395 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "ef94a882-3af7-5887-89f0-be9833c6b071", + "network": { + "id": "Ethereum", + "name": "Ethereum", + "chainId": 1, + "isCustom": false, + "color": "#007896", + "gasPriceSettings": { + "min": 1, + "max": 60, + "initial": 20 + }, + "shouldEstimateGasPrice": true, + "dPaths": { + "TREZOR": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "TREZOR_NEW": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S_NEW": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "default": { + "label": "Default (ETH)", + "value": "m/44'/60'/0'/0" + } + }, + "tokenExplorer": { + "name": "https://ethplorer.io" + }, + "contracts": [ + "7f4bee9a-b3a7-5f5d-9fdd-70a07fc30ccf", + "a22967bf-7809-5318-976c-3cfd6076b44e", + "d40ae695-e12f-5d20-8eb7-feba7f9fb771", + "4c9ed17d-9862-5c1d-8e45-d9ea7a155eff", + "83c3a8c1-6797-5268-8059-d31f2489bad7", + "d70c4394-f31a-5bde-b86d-0f804ecb10ab", + "570e4c2e-05ee-553e-b9ca-3502ccd029ab", + "138e4404-35cf-5469-8415-dcc0a6e52baa", + "c2ac20d6-5844-5748-9b12-960e6b6d1812", + "027ea87c-6029-5648-97e2-fa5207e644c9", + "13f61e61-fc49-5ae7-a933-d146d5b86127", + "b6902782-3d18-5179-8a60-9a3cf4bb7667", + "f93d9c0d-be4e-589b-a1e9-63b62b25bebc", + "bfe2570d-f158-5bbd-986c-9beffc84781b", + "da868a97-24fd-5225-b06a-e03a332ad719", + "6c40b877-2c6c-5b7b-a522-a5812fcad293", + "d606d804-6280-5795-b79b-1ac0c879a765", + "29e8e081-2b69-5508-a63c-84abff5277fb", + "566f9dba-0e28-5dfe-bf78-d0aea8e7b2ff", + "2f8533df-9b1e-5cf1-a802-b3a87cad7185", + "4fa8789e-c769-5c20-bf02-ce5669186fa7", + "4701d934-99dc-5c7e-9575-efed8f694ea6", + "f51049a5-f560-5d86-8937-4d80ddf71f46", + "f9b60875-3992-537c-8836-d0b3b3212cd7", + "08925f01-5d2a-533f-a03c-63ae46b7dc61", + "c815ccd6-ffae-57e6-baad-7a5dc48a5be1", + "8156f598-11dd-5a7a-8a3a-805c3acc356b", + "624d4cb6-b9ba-5b46-b40b-02bf4e435b08", + "eacdc803-011b-5397-a0f6-b400dc9f61ba", + "6e7abb55-78c1-56c9-b513-18cb253726d9", + "86e5fb90-a15f-5173-b774-6454e3c0a014", + "738d757b-a438-5f21-b116-c18c4decc5e5", + "635c7c2c-545b-5844-8826-27b5d69f5b29", + "13bde552-ab9e-5422-b21f-09f3fbad0f2b", + "7a7bf9a0-723e-5289-96da-8108134dd780" + ], + "assets": [], + "baseAsset": "356a192b-7913-504c-9457-4d18c28d46e6", + "baseUnit": "ETH", + "nodes": [ + { + "name": "eth_mycrypto", + "type": "rpc", + "service": "MyCrypto", + "url": "https://api.mycryptoapi.com/eth" + }, + { + "name": "eth_ethscan", + "type": "etherscan", + "service": "Etherscan", + "url": "https://api.etherscan.io/api" + }, + { + "name": "eth_infura", + "type": "infura", + "service": "Infura", + "url": "https://mainnet.infura.io/v3/f3b4711ae677488bb3c56de93c6cab1a" + }, + { + "name": "eth_pocket", + "type": "pocket", + "service": "Pocket", + "url": "https://eth-mainnet.gateway.pokt.network/v1/lb/5f6a53d75053d3232349e41e" + }, + { + "name": "web3", + "isCustom": false, + "type": "web3", + "url": "", + "service": "MetaMask / Web3", + "hidden": true + } + ], + "autoNode": "eth_mycrypto", + "selectedNode": "web3", + "blockExplorer": { + "name": "Etherscan", + "origin": "https://etherscan.io" + } + }, + "label": "MetaMask Account 10" + }, + { + "address": "0x9c9c3d1576de533427b4adcb5e6bfda935a86c78", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "ticker": "ETH", + "name": "Ether (Ethereum)", + "decimal": 18, + "support": {}, + "social": {}, + "networkId": "Ethereum", + "type": "base", + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "balance": { + "_hex": "0x00" + }, + "mtime": 1606322464395 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "5038d1b0-9afb-597f-8a0f-0f879390873f", + "network": { + "id": "Ethereum", + "name": "Ethereum", + "chainId": 1, + "isCustom": false, + "color": "#007896", + "gasPriceSettings": { + "min": 1, + "max": 60, + "initial": 20 + }, + "shouldEstimateGasPrice": true, + "dPaths": { + "TREZOR": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "TREZOR_NEW": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S_NEW": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "default": { + "label": "Default (ETH)", + "value": "m/44'/60'/0'/0" + } + }, + "tokenExplorer": { + "name": "https://ethplorer.io" + }, + "contracts": [ + "7f4bee9a-b3a7-5f5d-9fdd-70a07fc30ccf", + "a22967bf-7809-5318-976c-3cfd6076b44e", + "d40ae695-e12f-5d20-8eb7-feba7f9fb771", + "4c9ed17d-9862-5c1d-8e45-d9ea7a155eff", + "83c3a8c1-6797-5268-8059-d31f2489bad7", + "d70c4394-f31a-5bde-b86d-0f804ecb10ab", + "570e4c2e-05ee-553e-b9ca-3502ccd029ab", + "138e4404-35cf-5469-8415-dcc0a6e52baa", + "c2ac20d6-5844-5748-9b12-960e6b6d1812", + "027ea87c-6029-5648-97e2-fa5207e644c9", + "13f61e61-fc49-5ae7-a933-d146d5b86127", + "b6902782-3d18-5179-8a60-9a3cf4bb7667", + "f93d9c0d-be4e-589b-a1e9-63b62b25bebc", + "bfe2570d-f158-5bbd-986c-9beffc84781b", + "da868a97-24fd-5225-b06a-e03a332ad719", + "6c40b877-2c6c-5b7b-a522-a5812fcad293", + "d606d804-6280-5795-b79b-1ac0c879a765", + "29e8e081-2b69-5508-a63c-84abff5277fb", + "566f9dba-0e28-5dfe-bf78-d0aea8e7b2ff", + "2f8533df-9b1e-5cf1-a802-b3a87cad7185", + "4fa8789e-c769-5c20-bf02-ce5669186fa7", + "4701d934-99dc-5c7e-9575-efed8f694ea6", + "f51049a5-f560-5d86-8937-4d80ddf71f46", + "f9b60875-3992-537c-8836-d0b3b3212cd7", + "08925f01-5d2a-533f-a03c-63ae46b7dc61", + "c815ccd6-ffae-57e6-baad-7a5dc48a5be1", + "8156f598-11dd-5a7a-8a3a-805c3acc356b", + "624d4cb6-b9ba-5b46-b40b-02bf4e435b08", + "eacdc803-011b-5397-a0f6-b400dc9f61ba", + "6e7abb55-78c1-56c9-b513-18cb253726d9", + "86e5fb90-a15f-5173-b774-6454e3c0a014", + "738d757b-a438-5f21-b116-c18c4decc5e5", + "635c7c2c-545b-5844-8826-27b5d69f5b29", + "13bde552-ab9e-5422-b21f-09f3fbad0f2b", + "7a7bf9a0-723e-5289-96da-8108134dd780" + ], + "assets": [], + "baseAsset": "356a192b-7913-504c-9457-4d18c28d46e6", + "baseUnit": "ETH", + "nodes": [ + { + "name": "eth_mycrypto", + "type": "rpc", + "service": "MyCrypto", + "url": "https://api.mycryptoapi.com/eth" + }, + { + "name": "eth_ethscan", + "type": "etherscan", + "service": "Etherscan", + "url": "https://api.etherscan.io/api" + }, + { + "name": "eth_infura", + "type": "infura", + "service": "Infura", + "url": "https://mainnet.infura.io/v3/f3b4711ae677488bb3c56de93c6cab1a" + }, + { + "name": "eth_pocket", + "type": "pocket", + "service": "Pocket", + "url": "https://eth-mainnet.gateway.pokt.network/v1/lb/5f6a53d75053d3232349e41e" + }, + { + "name": "web3", + "isCustom": false, + "type": "web3", + "url": "", + "service": "MetaMask / Web3", + "hidden": true + } + ], + "autoNode": "eth_mycrypto", + "selectedNode": "web3", + "blockExplorer": { + "name": "Etherscan", + "origin": "https://etherscan.io" + } + }, + "label": "MetaMask Account 11" + }, + { + "address": "0x48b599afcc9e2b6d7b549bf0845ce687700b954e", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "ticker": "ETH", + "name": "Ether (Ethereum)", + "decimal": 18, + "support": {}, + "social": {}, + "networkId": "Ethereum", + "type": "base", + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "balance": { + "_hex": "0x00" + }, + "mtime": 1606322464395 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "5034876a-d91c-5b4b-9851-cce700717e4b", + "network": { + "id": "Ethereum", + "name": "Ethereum", + "chainId": 1, + "isCustom": false, + "color": "#007896", + "gasPriceSettings": { + "min": 1, + "max": 60, + "initial": 20 + }, + "shouldEstimateGasPrice": true, + "dPaths": { + "TREZOR": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "TREZOR_NEW": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S_NEW": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "default": { + "label": "Default (ETH)", + "value": "m/44'/60'/0'/0" + } + }, + "tokenExplorer": { + "name": "https://ethplorer.io" + }, + "contracts": [ + "7f4bee9a-b3a7-5f5d-9fdd-70a07fc30ccf", + "a22967bf-7809-5318-976c-3cfd6076b44e", + "d40ae695-e12f-5d20-8eb7-feba7f9fb771", + "4c9ed17d-9862-5c1d-8e45-d9ea7a155eff", + "83c3a8c1-6797-5268-8059-d31f2489bad7", + "d70c4394-f31a-5bde-b86d-0f804ecb10ab", + "570e4c2e-05ee-553e-b9ca-3502ccd029ab", + "138e4404-35cf-5469-8415-dcc0a6e52baa", + "c2ac20d6-5844-5748-9b12-960e6b6d1812", + "027ea87c-6029-5648-97e2-fa5207e644c9", + "13f61e61-fc49-5ae7-a933-d146d5b86127", + "b6902782-3d18-5179-8a60-9a3cf4bb7667", + "f93d9c0d-be4e-589b-a1e9-63b62b25bebc", + "bfe2570d-f158-5bbd-986c-9beffc84781b", + "da868a97-24fd-5225-b06a-e03a332ad719", + "6c40b877-2c6c-5b7b-a522-a5812fcad293", + "d606d804-6280-5795-b79b-1ac0c879a765", + "29e8e081-2b69-5508-a63c-84abff5277fb", + "566f9dba-0e28-5dfe-bf78-d0aea8e7b2ff", + "2f8533df-9b1e-5cf1-a802-b3a87cad7185", + "4fa8789e-c769-5c20-bf02-ce5669186fa7", + "4701d934-99dc-5c7e-9575-efed8f694ea6", + "f51049a5-f560-5d86-8937-4d80ddf71f46", + "f9b60875-3992-537c-8836-d0b3b3212cd7", + "08925f01-5d2a-533f-a03c-63ae46b7dc61", + "c815ccd6-ffae-57e6-baad-7a5dc48a5be1", + "8156f598-11dd-5a7a-8a3a-805c3acc356b", + "624d4cb6-b9ba-5b46-b40b-02bf4e435b08", + "eacdc803-011b-5397-a0f6-b400dc9f61ba", + "6e7abb55-78c1-56c9-b513-18cb253726d9", + "86e5fb90-a15f-5173-b774-6454e3c0a014", + "738d757b-a438-5f21-b116-c18c4decc5e5", + "635c7c2c-545b-5844-8826-27b5d69f5b29", + "13bde552-ab9e-5422-b21f-09f3fbad0f2b", + "7a7bf9a0-723e-5289-96da-8108134dd780" + ], + "assets": [], + "baseAsset": "356a192b-7913-504c-9457-4d18c28d46e6", + "baseUnit": "ETH", + "nodes": [ + { + "name": "eth_mycrypto", + "type": "rpc", + "service": "MyCrypto", + "url": "https://api.mycryptoapi.com/eth" + }, + { + "name": "eth_ethscan", + "type": "etherscan", + "service": "Etherscan", + "url": "https://api.etherscan.io/api" + }, + { + "name": "eth_infura", + "type": "infura", + "service": "Infura", + "url": "https://mainnet.infura.io/v3/f3b4711ae677488bb3c56de93c6cab1a" + }, + { + "name": "eth_pocket", + "type": "pocket", + "service": "Pocket", + "url": "https://eth-mainnet.gateway.pokt.network/v1/lb/5f6a53d75053d3232349e41e" + }, + { + "name": "web3", + "isCustom": false, + "type": "web3", + "url": "", + "service": "MetaMask / Web3", + "hidden": true + } + ], + "autoNode": "eth_mycrypto", + "selectedNode": "web3", + "blockExplorer": { + "name": "Etherscan", + "origin": "https://etherscan.io" + } + }, + "label": "MetaMask Account 12" + }, + { + "address": "0x82d69476357a03415e92b5780c89e5e9e972ce75", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "ticker": "ETH", + "name": "Ether (Ethereum)", + "decimal": 18, + "support": {}, + "social": {}, + "networkId": "Ethereum", + "type": "base", + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "balance": { + "_hex": "0x00" + }, + "mtime": 1606322464395 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "7b9d75ac-ddfd-51bb-a9b8-30e3266fa075", + "network": { + "id": "Ethereum", + "name": "Ethereum", + "chainId": 1, + "isCustom": false, + "color": "#007896", + "gasPriceSettings": { + "min": 1, + "max": 60, + "initial": 20 + }, + "shouldEstimateGasPrice": true, + "dPaths": { + "TREZOR": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "TREZOR_NEW": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S_NEW": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "default": { + "label": "Default (ETH)", + "value": "m/44'/60'/0'/0" + } + }, + "tokenExplorer": { + "name": "https://ethplorer.io" + }, + "contracts": [ + "7f4bee9a-b3a7-5f5d-9fdd-70a07fc30ccf", + "a22967bf-7809-5318-976c-3cfd6076b44e", + "d40ae695-e12f-5d20-8eb7-feba7f9fb771", + "4c9ed17d-9862-5c1d-8e45-d9ea7a155eff", + "83c3a8c1-6797-5268-8059-d31f2489bad7", + "d70c4394-f31a-5bde-b86d-0f804ecb10ab", + "570e4c2e-05ee-553e-b9ca-3502ccd029ab", + "138e4404-35cf-5469-8415-dcc0a6e52baa", + "c2ac20d6-5844-5748-9b12-960e6b6d1812", + "027ea87c-6029-5648-97e2-fa5207e644c9", + "13f61e61-fc49-5ae7-a933-d146d5b86127", + "b6902782-3d18-5179-8a60-9a3cf4bb7667", + "f93d9c0d-be4e-589b-a1e9-63b62b25bebc", + "bfe2570d-f158-5bbd-986c-9beffc84781b", + "da868a97-24fd-5225-b06a-e03a332ad719", + "6c40b877-2c6c-5b7b-a522-a5812fcad293", + "d606d804-6280-5795-b79b-1ac0c879a765", + "29e8e081-2b69-5508-a63c-84abff5277fb", + "566f9dba-0e28-5dfe-bf78-d0aea8e7b2ff", + "2f8533df-9b1e-5cf1-a802-b3a87cad7185", + "4fa8789e-c769-5c20-bf02-ce5669186fa7", + "4701d934-99dc-5c7e-9575-efed8f694ea6", + "f51049a5-f560-5d86-8937-4d80ddf71f46", + "f9b60875-3992-537c-8836-d0b3b3212cd7", + "08925f01-5d2a-533f-a03c-63ae46b7dc61", + "c815ccd6-ffae-57e6-baad-7a5dc48a5be1", + "8156f598-11dd-5a7a-8a3a-805c3acc356b", + "624d4cb6-b9ba-5b46-b40b-02bf4e435b08", + "eacdc803-011b-5397-a0f6-b400dc9f61ba", + "6e7abb55-78c1-56c9-b513-18cb253726d9", + "86e5fb90-a15f-5173-b774-6454e3c0a014", + "738d757b-a438-5f21-b116-c18c4decc5e5", + "635c7c2c-545b-5844-8826-27b5d69f5b29", + "13bde552-ab9e-5422-b21f-09f3fbad0f2b", + "7a7bf9a0-723e-5289-96da-8108134dd780" + ], + "assets": [], + "baseAsset": "356a192b-7913-504c-9457-4d18c28d46e6", + "baseUnit": "ETH", + "nodes": [ + { + "name": "eth_mycrypto", + "type": "rpc", + "service": "MyCrypto", + "url": "https://api.mycryptoapi.com/eth" + }, + { + "name": "eth_ethscan", + "type": "etherscan", + "service": "Etherscan", + "url": "https://api.etherscan.io/api" + }, + { + "name": "eth_infura", + "type": "infura", + "service": "Infura", + "url": "https://mainnet.infura.io/v3/f3b4711ae677488bb3c56de93c6cab1a" + }, + { + "name": "eth_pocket", + "type": "pocket", + "service": "Pocket", + "url": "https://eth-mainnet.gateway.pokt.network/v1/lb/5f6a53d75053d3232349e41e" + }, + { + "name": "web3", + "isCustom": false, + "type": "web3", + "url": "", + "service": "MetaMask / Web3", + "hidden": true + } + ], + "autoNode": "eth_mycrypto", + "selectedNode": "web3", + "blockExplorer": { + "name": "Etherscan", + "origin": "https://etherscan.io" + } + }, + "label": "MetaMask Account 13" + }, + { + "address": "0xfeac75a09662396283f4bb50f0a9249576a81866", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "ticker": "ETH", + "name": "Ether (Ethereum)", + "decimal": 18, + "support": {}, + "social": {}, + "networkId": "Ethereum", + "type": "base", + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "balance": { + "_hex": "0x00" + }, + "mtime": 1606322464395 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "faceb7fa-2551-561d-8374-3cfcb7fd9cf8", + "network": { + "id": "Ethereum", + "name": "Ethereum", + "chainId": 1, + "isCustom": false, + "color": "#007896", + "gasPriceSettings": { + "min": 1, + "max": 60, + "initial": 20 + }, + "shouldEstimateGasPrice": true, + "dPaths": { + "TREZOR": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "TREZOR_NEW": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S_NEW": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "default": { + "label": "Default (ETH)", + "value": "m/44'/60'/0'/0" + } + }, + "tokenExplorer": { + "name": "https://ethplorer.io" + }, + "contracts": [ + "7f4bee9a-b3a7-5f5d-9fdd-70a07fc30ccf", + "a22967bf-7809-5318-976c-3cfd6076b44e", + "d40ae695-e12f-5d20-8eb7-feba7f9fb771", + "4c9ed17d-9862-5c1d-8e45-d9ea7a155eff", + "83c3a8c1-6797-5268-8059-d31f2489bad7", + "d70c4394-f31a-5bde-b86d-0f804ecb10ab", + "570e4c2e-05ee-553e-b9ca-3502ccd029ab", + "138e4404-35cf-5469-8415-dcc0a6e52baa", + "c2ac20d6-5844-5748-9b12-960e6b6d1812", + "027ea87c-6029-5648-97e2-fa5207e644c9", + "13f61e61-fc49-5ae7-a933-d146d5b86127", + "b6902782-3d18-5179-8a60-9a3cf4bb7667", + "f93d9c0d-be4e-589b-a1e9-63b62b25bebc", + "bfe2570d-f158-5bbd-986c-9beffc84781b", + "da868a97-24fd-5225-b06a-e03a332ad719", + "6c40b877-2c6c-5b7b-a522-a5812fcad293", + "d606d804-6280-5795-b79b-1ac0c879a765", + "29e8e081-2b69-5508-a63c-84abff5277fb", + "566f9dba-0e28-5dfe-bf78-d0aea8e7b2ff", + "2f8533df-9b1e-5cf1-a802-b3a87cad7185", + "4fa8789e-c769-5c20-bf02-ce5669186fa7", + "4701d934-99dc-5c7e-9575-efed8f694ea6", + "f51049a5-f560-5d86-8937-4d80ddf71f46", + "f9b60875-3992-537c-8836-d0b3b3212cd7", + "08925f01-5d2a-533f-a03c-63ae46b7dc61", + "c815ccd6-ffae-57e6-baad-7a5dc48a5be1", + "8156f598-11dd-5a7a-8a3a-805c3acc356b", + "624d4cb6-b9ba-5b46-b40b-02bf4e435b08", + "eacdc803-011b-5397-a0f6-b400dc9f61ba", + "6e7abb55-78c1-56c9-b513-18cb253726d9", + "86e5fb90-a15f-5173-b774-6454e3c0a014", + "738d757b-a438-5f21-b116-c18c4decc5e5", + "635c7c2c-545b-5844-8826-27b5d69f5b29", + "13bde552-ab9e-5422-b21f-09f3fbad0f2b", + "7a7bf9a0-723e-5289-96da-8108134dd780" + ], + "assets": [], + "baseAsset": "356a192b-7913-504c-9457-4d18c28d46e6", + "baseUnit": "ETH", + "nodes": [ + { + "name": "eth_mycrypto", + "type": "rpc", + "service": "MyCrypto", + "url": "https://api.mycryptoapi.com/eth" + }, + { + "name": "eth_ethscan", + "type": "etherscan", + "service": "Etherscan", + "url": "https://api.etherscan.io/api" + }, + { + "name": "eth_infura", + "type": "infura", + "service": "Infura", + "url": "https://mainnet.infura.io/v3/f3b4711ae677488bb3c56de93c6cab1a" + }, + { + "name": "eth_pocket", + "type": "pocket", + "service": "Pocket", + "url": "https://eth-mainnet.gateway.pokt.network/v1/lb/5f6a53d75053d3232349e41e" + }, + { + "name": "web3", + "isCustom": false, + "type": "web3", + "url": "", + "service": "MetaMask / Web3", + "hidden": true + } + ], + "autoNode": "eth_mycrypto", + "selectedNode": "web3", + "blockExplorer": { + "name": "Etherscan", + "origin": "https://etherscan.io" + } + }, + "label": "MetaMask Account 14" + }, + { + "address": "0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf", + "networkId": "Ethereum", + "wallet": "METAMASK", + "assets": [ + { + "ticker": "ETH", + "name": "Ether (Ethereum)", + "decimal": 18, + "support": {}, + "social": {}, + "networkId": "Ethereum", + "type": "base", + "mappings": { + "coinCapId": "ethereum", + "coinGeckoId": "ethereum", + "cryptoCompareId": "ETH", + "cryptoCurrencyIconName": "eth", + "dexAgId": "ETH" + }, + "isCustom": false, + "uuid": "356a192b-7913-504c-9457-4d18c28d46e6", + "balance": { + "_hex": "0x00" + }, + "mtime": 1606322464395 + } + ], + "transactions": [], + "favorite": false, + "mtime": 0, + "uuid": "4a236c90-b722-5e26-8718-659db6b5d302", + "network": { + "id": "Ethereum", + "name": "Ethereum", + "chainId": 1, + "isCustom": false, + "color": "#007896", + "gasPriceSettings": { + "min": 1, + "max": 60, + "initial": 20 + }, + "shouldEstimateGasPrice": true, + "dPaths": { + "TREZOR": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "TREZOR_NEW": { + "label": "TREZOR (ETH)", + "value": "m/44'/60'/0'/0" + }, + "LEDGER_NANO_S_NEW": { + "label": "Ledger (ETH)", + "value": "m/44'/60'/0'" + }, + "default": { + "label": "Default (ETH)", + "value": "m/44'/60'/0'/0" + } + }, + "tokenExplorer": { + "name": "https://ethplorer.io" + }, + "contracts": [ + "7f4bee9a-b3a7-5f5d-9fdd-70a07fc30ccf", + "a22967bf-7809-5318-976c-3cfd6076b44e", + "d40ae695-e12f-5d20-8eb7-feba7f9fb771", + "4c9ed17d-9862-5c1d-8e45-d9ea7a155eff", + "83c3a8c1-6797-5268-8059-d31f2489bad7", + "d70c4394-f31a-5bde-b86d-0f804ecb10ab", + "570e4c2e-05ee-553e-b9ca-3502ccd029ab", + "138e4404-35cf-5469-8415-dcc0a6e52baa", + "c2ac20d6-5844-5748-9b12-960e6b6d1812", + "027ea87c-6029-5648-97e2-fa5207e644c9", + "13f61e61-fc49-5ae7-a933-d146d5b86127", + "b6902782-3d18-5179-8a60-9a3cf4bb7667", + "f93d9c0d-be4e-589b-a1e9-63b62b25bebc", + "bfe2570d-f158-5bbd-986c-9beffc84781b", + "da868a97-24fd-5225-b06a-e03a332ad719", + "6c40b877-2c6c-5b7b-a522-a5812fcad293", + "d606d804-6280-5795-b79b-1ac0c879a765", + "29e8e081-2b69-5508-a63c-84abff5277fb", + "566f9dba-0e28-5dfe-bf78-d0aea8e7b2ff", + "2f8533df-9b1e-5cf1-a802-b3a87cad7185", + "4fa8789e-c769-5c20-bf02-ce5669186fa7", + "4701d934-99dc-5c7e-9575-efed8f694ea6", + "f51049a5-f560-5d86-8937-4d80ddf71f46", + "f9b60875-3992-537c-8836-d0b3b3212cd7", + "08925f01-5d2a-533f-a03c-63ae46b7dc61", + "c815ccd6-ffae-57e6-baad-7a5dc48a5be1", + "8156f598-11dd-5a7a-8a3a-805c3acc356b", + "624d4cb6-b9ba-5b46-b40b-02bf4e435b08", + "eacdc803-011b-5397-a0f6-b400dc9f61ba", + "6e7abb55-78c1-56c9-b513-18cb253726d9", + "86e5fb90-a15f-5173-b774-6454e3c0a014", + "738d757b-a438-5f21-b116-c18c4decc5e5", + "635c7c2c-545b-5844-8826-27b5d69f5b29", + "13bde552-ab9e-5422-b21f-09f3fbad0f2b", + "7a7bf9a0-723e-5289-96da-8108134dd780" + ], + "assets": [], + "baseAsset": "356a192b-7913-504c-9457-4d18c28d46e6", + "baseUnit": "ETH", + "nodes": [ + { + "name": "eth_mycrypto", + "type": "rpc", + "service": "MyCrypto", + "url": "https://api.mycryptoapi.com/eth" + }, + { + "name": "eth_ethscan", + "type": "etherscan", + "service": "Etherscan", + "url": "https://api.etherscan.io/api" + }, + { + "name": "eth_infura", + "type": "infura", + "service": "Infura", + "url": "https://mainnet.infura.io/v3/f3b4711ae677488bb3c56de93c6cab1a" + }, + { + "name": "eth_pocket", + "type": "pocket", + "service": "Pocket", + "url": "https://eth-mainnet.gateway.pokt.network/v1/lb/5f6a53d75053d3232349e41e" + }, + { + "name": "web3", + "isCustom": false, + "type": "web3", + "url": "", + "service": "MetaMask / Web3", + "hidden": true + } + ], + "autoNode": "eth_mycrypto", + "selectedNode": "web3", + "blockExplorer": { + "name": "Etherscan", + "origin": "https://etherscan.io" + } + }, + "label": "MetaMask Account 16" + } + ] + }, + "dateDisplayed": "2020-11-25T16:41:04.592Z", + "dismissed": true, + "dateDismissed": "2020-11-25T16:46:00.343Z", + "viewed": true + }, + "947d052d-0c76-4be8-9b03-76895ca48d89": { + "uuid": "947d052d-0c76-4be8-9b03-76895ca48d89", + "template": "save-settings", + "dateDisplayed": "2020-11-25T16:41:04.592Z", + "dismissed": false, + "viewed": true + } + }, + "settings": { + "fiatCurrency": "RUB", + "darkMode": false, + "dashboardAccounts": [ + "f1c35f92-e0ba-55e0-a3ce-085c8c767b02", + "f25322d8-327c-5efa-896a-82f89c86a2f2", + "84963ccf-7ad8-53b4-8195-2720861007e6", + "54a618a2-6da5-57fa-893f-f10a114e27ae", + "e4f87fd1-2104-5b81-af05-669ca4d7d99b", + "33369d60-dc7d-5bd8-b304-97e90eba8a2e", + "f7ebd825-d1b7-5255-ac95-5fd737ef8477", + "b3ee0bdc-b8b6-50fd-9b8a-204e825d03b8", + "ef94a882-3af7-5887-89f0-be9833c6b071", + "5038d1b0-9afb-597f-8a0f-0f879390873f", + "5034876a-d91c-5b4b-9851-cce700717e4b", + "7b9d75ac-ddfd-51bb-a9b8-30e3266fa075", + "faceb7fa-2551-561d-8374-3cfcb7fd9cf8", + "4a236c90-b722-5e26-8718-659db6b5d302" + ], + "excludedAssets": [], + "rates": { + "e1f698bf-cb85-5405-b563-14774af14bf1": { + "usd": 0.996893, + "eur": 0.837184, + "gbp": 0.745264, + "rub": 75.57, + "inr": 73.57, + "cny": 6.56, + "try": 7.93 + }, + "c737fec0-a9e9-5841-b4c8-64d48731eb1c": { + "usd": 3.98, + "eur": 3.34, + "gbp": 2.97, + "rub": 301.38, + "inr": 293.58, + "cny": 26.17, + "try": 31.64 + }, + "356a192b-7913-504c-9457-4d18c28d46e6": { + "usd": 588.43, + "eur": 494.13, + "gbp": 439.85, + "rub": 44592, + "inr": 43412, + "cny": 3871.27, + "try": 4674.13 + }, + "d017a1e8-bdd3-5c32-8866-da258f75b0e9": { + "usd": 17.35, + "eur": 14.57, + "gbp": 12.97, + "rub": 1314.7, + "inr": 1279.92, + "cny": 114.14, + "try": 137.81 + }, + "2ed3ebcd-2d4f-52f7-b12b-54360ed49b55": { + "usd": 0.998981, + "eur": 0.838889, + "gbp": 0.746742, + "rub": 75.7, + "inr": 73.7, + "cny": 6.57, + "try": 7.94 + }, + "0e99ca80-7edf-5b74-a2f5-03d37eed30bf": { + "usd": 0.184194, + "eur": 0.154694, + "gbp": 0.137713, + "rub": 13.95, + "inr": 13.59, + "cny": 1.21, + "try": 1.46 + }, + "a9cc6884-14bd-53b6-abcd-f9b56b60463d": { + "usd": 0.02082965, + "eur": 0.01749159, + "gbp": 0.01557024, + "rub": 1.58, + "inr": 1.54, + "cny": 0.137038, + "try": 0.165458 + }, + "e34bb9ff-47be-503b-956b-30d4b3f3507b": { + "usd": 0.274964, + "eur": 0.2309, + "gbp": 0.205537, + "rub": 20.84, + "inr": 20.29, + "cny": 1.81, + "try": 2.18 + }, + "7f4d2f19-9a76-5930-b63b-ac9b0afe6201": { + "usd": 3.92, + "eur": 3.29, + "gbp": 2.93, + "rub": 296.97, + "inr": 289.11, + "cny": 25.78, + "try": 31.13 + }, + "19c48978-796d-55c6-a0e2-9999d1592728": { + "usd": 580.4, + "eur": 487.45, + "gbp": 433.94, + "rub": 43972, + "inr": 42834, + "cny": 3818.48, + "try": 4615.94 + }, + "c0ae47d5-f020-5ea8-9886-522d3af70845": { + "usd": 15.13, + "eur": 12.7, + "gbp": 11.31, + "rub": 1146.43, + "inr": 1116.11, + "cny": 99.53, + "try": 120.17 + }, + "4f387b74-88e2-5a56-9664-0224ee546c6a": { + "usd": 0.089783, + "eur": 0.075395, + "gbp": 0.067113, + "rub": 6.8, + "inr": 6.62, + "cny": 0.590681, + "try": 0.713181 + } + }, + "language": "en", + "node": "web3" + }, + "networkNodes": { + "Ethereum": { + "selectedNode": "ethereum_Emmanuel_Macron", + "nodes": [ + { + "url": "https://mainnet.infura.io/v3/ba946b0ddc10409dab63cbfc0f68dd1a", + "service": "Emmanuel Macron", + "name": "ethereum_Emmanuel_Macron", + "isCustom": true, + "type": "myccustom" + } + ] + }, + "Ropsten": { + "selectedNode": "web3" + } + }, + "userActions": { + "2ddea243-9534-4c9e-b5bc-17386a410d82": { + "name": "twitter", + "state": "completed", + "uuid": "2ddea243-9534-4c9e-b5bc-17386a410d82" + }, + "d69a3098-3862-44b6-9b8a-623dceb1b506": { + "name": "migrate_rep", + "state": "hidden", + "uuid": "d69a3098-3862-44b6-9b8a-623dceb1b506" + }, + "d449e9f7-8743-45e6-9884-4fa6160256df": { + "name": "update_label", + "state": "completed", + "uuid": "d449e9f7-8743-45e6-9884-4fa6160256df" + }, + "c20d39d3-1e2e-4db5-8690-b2b57d33031e": { + "name": "buy_hw", + "state": "new", + "uuid": "c20d39d3-1e2e-4db5-8690-b2b57d33031e" + }, + "d8a4346f-da5a-43f1-94df-8aae916b8ea8": { + "name": "add_account", + "state": "new", + "uuid": "d8a4346f-da5a-43f1-94df-8aae916b8ea8" + }, + "78a147c5-d467-4a12-9808-c75ce6bc04a2": { + "name": "myc_membership", + "state": "new", + "uuid": "78a147c5-d467-4a12-9808-c75ce6bc04a2" + }, + "e05108d8-bd88-455b-b78a-4631faf73bed": { + "name": "feedback", + "state": "new", + "uuid": "e05108d8-bd88-455b-b78a-4631faf73bed" + }, + "932e31e4-ced0-4ccf-8561-e7d682bb5f5a": { + "name": "newsletter", + "state": "new", + "uuid": "932e31e4-ced0-4ccf-8561-e7d682bb5f5a" + }, + "7b60e7df-1020-41b8-82a8-c4041f2696e6": { + "name": "telegram", + "state": "new", + "uuid": "7b60e7df-1020-41b8-82a8-c4041f2696e6" + }, + "6e12e325-06c4-44e0-8077-4b3f0ef90d17": { + "name": "backup", + "state": "new", + "uuid": "6e12e325-06c4-44e0-8077-4b3f0ef90d17" + }, + "e56064e7-919d-4275-845d-c28394a1f905": { + "name": "swap", + "state": "new", + "uuid": "e56064e7-919d-4275-845d-c28394a1f905" + }, + "32d292bc-8669-4aa2-837b-f0441fe91c40": { + "name": "migrate_ant", + "state": "new", + "uuid": "32d292bc-8669-4aa2-837b-f0441fe91c40" + }, + "c7e4b0a6-13a7-4d13-b841-ae83e1b5083c": { + "name": "claim_uni", + "state": "new", + "uuid": "c7e4b0a6-13a7-4d13-b841-ae83e1b5083c" + } + } +} diff --git a/__tests__/fixtures/testKeystoreFile.json b/__tests__/fixtures/testKeystoreFile.json new file mode 100644 index 00000000000..c19573680c4 --- /dev/null +++ b/__tests__/fixtures/testKeystoreFile.json @@ -0,0 +1,19 @@ +{ + "version": 3, + "id": "1f6c4b83-f83d-4530-bc4e-6f9f3c08dae8", + "address": "ff06ad5d076fa274b49c297f3fe9e29b5ba9aadc", + "crypto": { + "ciphertext": "2e35fca95fc4b6f20bf89138c5fa3c259cbae83cd0c0f5ba7311dc69eeb9f46a", + "cipherparams": { "iv": "a74dabab32dd1a3b5147e3700c102984" }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "e29569708685966b9766a6c0dd03dd639bdaaa9335ff0ca084a4ca0750589b1d", + "n": 8192, + "r": 8, + "p": 1 + }, + "mac": "4647a96672ca5c3ac1120e708394fbb8742ca315b2b5d19e2f4d60359a40c267" + } +} diff --git a/__tests__/hardhat-utils.js b/__tests__/hardhat-utils.js new file mode 100644 index 00000000000..d55a8bb938a --- /dev/null +++ b/__tests__/hardhat-utils.js @@ -0,0 +1,74 @@ +import { Contract } from '@ethersproject/contracts'; +import { JsonRpcProvider } from '@ethersproject/providers'; + +import { FIXTURE_WEB3_ADDRESS } from './fixtures'; + +const provider = new JsonRpcProvider('http://127.0.0.1:8546/'); + +export const resetFork = async (stickyBlockNum = true) => { + await provider.send('hardhat_reset', [ + { + forking: { + jsonRpcUrl: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, + blockNumber: stickyBlockNum ? 13373860 : undefined + } + } + ]); +}; + +const setupERC20 = async (account, contract, amount) => { + await provider.send('hardhat_impersonateAccount', [account]); + + const signer = await provider.getSigner(account); + + const abi = [ + // Read-Only Functions + 'function balanceOf(address owner) view returns (uint256)', + 'function decimals() view returns (uint8)', + 'function symbol() view returns (string)', + + // Authenticated Functions + 'function transfer(address to, uint amount) returns (boolean)', + + // Events + 'event Transfer(address indexed from, address indexed to, uint amount)' + ]; + + // send ERC20 + const erc20 = new Contract(contract, abi, signer); + const tx = await erc20.populateTransaction.transfer(FIXTURE_WEB3_ADDRESS, amount); + + await sendTx({ ...tx, from: account }); + + await provider.send('hardhat_stopImpersonatingAccount', [account]); +}; + +// Transfers DAI to the test address +export const setupDAI = async () => + setupERC20( + '0x28c6c06298d514db089934071355e5743bf21d60', + '0x6b175474e89094c44da98b954eedeac495271d0f', + '100000000000000000000' + ); + +// Transfers LEND to the test address +export const setupLEND = async () => + setupERC20( + '0xb4Ad52fBBBb46871C043Cc8B4c074ad8B2a0c83b', + '0x80fB784B7eD66730e8b1DBd9820aFD29931aab03', + '100000000000000000000' + ); + +export const sendTx = async (tx) => { + const signer = await provider.getSigner(tx.from); + + const sent = await signer.sendTransaction({ + ...tx, + maxFeePerGas: '0xe8990a4600', + maxPriorityFeePerGas: '0xe8990a4600' + }); + + await sent.wait(); + + return sent.hash; +}; diff --git a/__tests__/helpers.js b/__tests__/helpers.js new file mode 100644 index 00000000000..cd2740e9aa4 --- /dev/null +++ b/__tests__/helpers.js @@ -0,0 +1,34 @@ +import { ClientFunction } from 'testcafe'; + +/** + * Testcafe is run in node, while ClientFunctions are run in the DOM. + * Custom window properties are not immediately available in ClientFunction. + * https://stackoverflow.com/a/56704217 + */ +export const waitForProperty = ClientFunction((predicate, timeout) => { + return new Promise((resolve, reject) => { + let intervalId = null; + let timeoutId = null; + + const checkCondition = function () { + if (typeof predicate === 'string') return window[predicate]; + else { + return predicate(window); + } + }; + + timeoutId = setTimeout(function () { + clearInterval(intervalId); + clearInterval(timeoutId); + if (checkCondition()) resolve(true); + else reject(false); + }, timeout || 3000); + + intervalId = setInterval(function () { + if (checkCondition()) { + clearInterval(intervalId); + resolve(true); + } + }, 1000); + }); +}); diff --git a/__tests__/import.test.js b/__tests__/import.test.js new file mode 100644 index 00000000000..feee7ef5440 --- /dev/null +++ b/__tests__/import.test.js @@ -0,0 +1,38 @@ +import { getByTestId, getByText } from '@testing-library/testcafe'; +import { ClientFunction } from 'testcafe'; + +import { FIXTURES_CONST, PAGES } from './fixtures'; +import importable from './fixtures/persisted.json'; +import SettingsPage from './settings-page.po'; +import { getTransValueByKey } from './translation-utils'; + +const settingsPage = new SettingsPage(); + +fixture('Settings/Import').page(PAGES.SETTINGS); + +test('Can import AppState from file', async (t) => { + await settingsPage.waitPageLoaded(); + + const getCurrentLocationUrl = ClientFunction(() => window.location.href); + await t + .click(getByText(getTransValueByKey('SETTINGS_IMPORT'))) + .expect(getCurrentLocationUrl()) + .eql(PAGES.SETTINGS_IMPORT, { + timeout: FIXTURES_CONST.TIMEOUT + }); + + const btn = getByText(getTransValueByKey('SETTINGS_IMPORT_COMPLETE')); + await t + .setFilesToUpload(getByTestId('upload-input'), ['./fixtures/persisted.json']) + .expect(btn.exists) + .ok() + .click(btn) + .expect(getCurrentLocationUrl()) + .eql(PAGES.SETTINGS, { timeout: FIXTURES_CONST.TIMEOUT }) + // The file sets the Ethereum selectedNode to a custom node named "Emmanuel Macron" + .expect(getByText(/Emmanuel Macron/).exists) + .ok(); + + await settingsPage.expectAccountTableToMatchCount(Object.keys(importable.accounts).length); + await settingsPage.expectContactTableToMatchCount(Object.keys(importable.addressBook).length); +}); diff --git a/__tests__/membership-page.po.js b/__tests__/membership-page.po.js new file mode 100644 index 00000000000..b92553faed9 --- /dev/null +++ b/__tests__/membership-page.po.js @@ -0,0 +1,26 @@ +import { getAllByTestId, queryAllByText } from '@testing-library/testcafe'; +import { t } from 'testcafe'; + +import BasePage from './base-page.po'; +import { setupEthereumMock } from './ethereum-mock'; +import { FIXTURE_HARDHAT_PRIVATE_KEY, PAGES } from './fixtures'; +import { findByTKey } from './translation-utils'; + +export default class MembershipPage extends BasePage { + async navigateToPage() { + this.navigateTo(PAGES.BUY_MEMBERSHIP); + } + + async waitPageLoaded(timeout) { + await this.waitForPage(PAGES.BUY_MEMBERSHIP, timeout); + } + + async useETH() { + await t.click(getAllByTestId('selector').nth(0)); + await t.click(queryAllByText(findByTKey('MEMBERSHIP_LIFETIME_EMOJI')).nth(0)); + } + + async setupMock() { + await setupEthereumMock(FIXTURE_HARDHAT_PRIVATE_KEY, 1); + } +} diff --git a/__tests__/membership.test.js b/__tests__/membership.test.js new file mode 100644 index 00000000000..918638d4da1 --- /dev/null +++ b/__tests__/membership.test.js @@ -0,0 +1,91 @@ +import { + getByText, + queryAllByTestId, + queryAllByText, + queryByText +} from '@testing-library/testcafe'; + +import { injectLS } from './clientScripts'; +import DashboardPage from './dashboard-page.po'; +import { FIXTURE_HARDHAT, FIXTURES_CONST, PAGES } from './fixtures'; +import { resetFork, setupDAI } from './hardhat-utils'; +import MembershipPage from './membership-page.po'; +import { findByTKey } from './translation-utils'; + +const membershipPage = new MembershipPage(); +const dashboard = new DashboardPage(); + +fixture('Membership') + .clientScripts({ content: injectLS(FIXTURE_HARDHAT) }) + .page(PAGES.BUY_MEMBERSHIP); + +test('can buy with ETH', async (t) => { + await membershipPage.waitPageLoaded(); + await membershipPage.setupMock(); + await resetFork(); + + await membershipPage.useETH(); + + await t.wait(FIXTURES_CONST.TIMEOUT); + + const button = await getByText(findByTKey('BUY_MEMBERSHIP')); + await t.click(button); + + const send = await queryByText(findByTKey('CONFIRM_AND_SEND')).with({ + timeout: FIXTURES_CONST.HARDHAT_TIMEOUT + }); + await t.expect(send.exists).ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); + await t.click(send); + + const viewDetailsButton = await queryByText(findByTKey('VIEW_TRANSACTION_DETAILS')).with({ + timeout: FIXTURES_CONST.TIMEOUT + }); + await t.expect(viewDetailsButton.exists).ok(); + await t.click(viewDetailsButton); + + await t + .expect(queryAllByTestId('SUCCESS').with({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }).exists) + .ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); +}); + +test('can buy with DAI', async (t) => { + await resetFork(); + await setupDAI(); + await membershipPage.waitPageLoaded(); + await membershipPage.setupMock(); + + await t.wait(FIXTURES_CONST.TIMEOUT); + + const button = await getByText(findByTKey('BUY_MEMBERSHIP')); + await t.click(button); + + const approve = await queryAllByText(findByTKey('APPROVE_MEMBERSHIP')) + .with({ + timeout: FIXTURES_CONST.HARDHAT_TIMEOUT + }) + .nth(1); + await t.expect(approve.exists).ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); + await t.click(approve); + + await t.wait(FIXTURES_CONST.TIMEOUT); + + const send = await queryAllByText(findByTKey('CONFIRM_TRANSACTION')) + .with({ + timeout: FIXTURES_CONST.HARDHAT_TIMEOUT + }) + .nth(1); + await t.expect(send.exists).ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); + await t.click(send); + + await t + .expect(queryAllByTestId('SUCCESS').with({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }).count) + .eql(2, { timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); + + const home = await queryByText(findByTKey('NAVIGATION_HOME')); + await t.click(home); + + await dashboard.waitPageLoaded(); + const request = await queryByText(findByTKey('MANAGE_MEMBERSHIP'), { exact: false }); + + await t.expect(request.exists).ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); +}); diff --git a/__tests__/migration-page.po.js b/__tests__/migration-page.po.js new file mode 100644 index 00000000000..899984f874e --- /dev/null +++ b/__tests__/migration-page.po.js @@ -0,0 +1,17 @@ +import BasePage from './base-page.po'; +import { setupEthereumMock } from './ethereum-mock'; +import { FIXTURE_HARDHAT_PRIVATE_KEY, PAGES } from './fixtures'; + +export default class MigrationPage extends BasePage { + async navigateToPage() { + this.navigateTo(PAGES.MIGRATE); + } + + async waitPageLoaded(timeout) { + await this.waitForPage(PAGES.MIGRATE, timeout); + } + + async setupMock() { + await setupEthereumMock(FIXTURE_HARDHAT_PRIVATE_KEY, 1); + } +} diff --git a/__tests__/migration.test.js b/__tests__/migration.test.js new file mode 100644 index 00000000000..58dc7f6c625 --- /dev/null +++ b/__tests__/migration.test.js @@ -0,0 +1,47 @@ +import { getByTestId, queryAllByTestId, queryAllByText } from '@testing-library/testcafe'; + +import { injectLS } from './clientScripts'; +import { FIXTURE_HARDHAT, FIXTURES_CONST, PAGES } from './fixtures'; +import { resetFork, setupLEND } from './hardhat-utils'; +import MigrationPage from './migration-page.po'; +import { findByTKey } from './translation-utils'; + +const migrationPage = new MigrationPage(); + +fixture('Migration') + .clientScripts({ content: injectLS(FIXTURE_HARDHAT) }) + .page(PAGES.MIGRATE); + +test('can do a LEND migration', async (t) => { + await resetFork(); + await setupLEND(); + await migrationPage.waitPageLoaded(); + await migrationPage.setupMock(); + + await t.wait(FIXTURES_CONST.TIMEOUT); + + const button = await getByTestId('confirm-migrate'); + await t.click(button); + + const approve = await queryAllByText(findByTKey('APPROVE_AAVE_TOKEN_MIGRATION')) + .with({ + timeout: FIXTURES_CONST.HARDHAT_TIMEOUT + }) + .nth(1); + await t.expect(approve.exists).ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); + await t.click(approve); + + await t.wait(FIXTURES_CONST.TIMEOUT); + + const send = await queryAllByText(findByTKey('CONFIRM_TRANSACTION')) + .with({ + timeout: FIXTURES_CONST.HARDHAT_TIMEOUT + }) + .nth(1); + await t.expect(send.exists).ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); + await t.click(send); + + await t + .expect(queryAllByTestId('SUCCESS').with({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }).count) + .eql(2, { timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); +}); diff --git a/__tests__/selectors.js b/__tests__/selectors.js new file mode 100644 index 00000000000..f62d4382078 --- /dev/null +++ b/__tests__/selectors.js @@ -0,0 +1,7 @@ +import { Selector } from 'testcafe'; + +import { getTransValueByKey } from './translation-utils'; + +export const selectMnemonicInput = Selector( + `input[placeholder="${getTransValueByKey('MNEMONIC_ENTER_PHRASE')}"]` +).parent(); diff --git a/__tests__/send-assets-page.po.js b/__tests__/send-assets-page.po.js new file mode 100644 index 00000000000..06eff3d8131 --- /dev/null +++ b/__tests__/send-assets-page.po.js @@ -0,0 +1,27 @@ +import { getAllByText, getByText } from '@testing-library/testcafe'; +import { Selector, t } from 'testcafe'; + +import BasePage from './base-page.po'; +import { FIXTURE_SEND_AMOUNT, FIXTURE_SEND_CONTACT, PAGES } from './fixtures'; +import { getTransValueByKey } from './translation-utils'; + +export default class SendAssetsPage extends BasePage { + async navigateToPage(queryString) { + this.navigateTo(`${PAGES.SEND}/${queryString}`); + } + + async waitPageLoaded(queryString) { + await this.waitForPage(`${PAGES.SEND}/${queryString}`); + } + + async fillForm() { + await t + .click(getByText(getTransValueByKey('ACCOUNT_LOOKUP_SELECTION_PLACEHOLDER'))) + .click(getAllByText(FIXTURE_SEND_CONTACT).nth(1)) + .click(Selector('input[name="amount"]').parent()) + .typeText(Selector('input[name="amount"]').parent(), FIXTURE_SEND_AMOUNT); + } + async submitForm() { + await t.click(getByText(getTransValueByKey('ACTION_6'))); + } +} diff --git a/__tests__/send.test.js b/__tests__/send.test.js new file mode 100644 index 00000000000..26c15243d15 --- /dev/null +++ b/__tests__/send.test.js @@ -0,0 +1,101 @@ +import { getAllByText, getByText, queryByText } from '@testing-library/testcafe'; +import { Selector } from 'testcafe'; + +import { injectLS } from './clientScripts'; +import { setupEthereumMock } from './ethereum-mock'; +import { setFeatureFlag } from './featureflag-utils'; +import { + FIXTURE_HARDHAT, + FIXTURE_HARDHAT_PRIVATE_KEY, + FIXTURE_SEND_AMOUNT, + FIXTURE_SEND_CONTACT, + FIXTURES_CONST, + PAGES +} from './fixtures'; +import SendAssetsPage from './send-assets-page.po'; +import { findByTKey } from './translation-utils'; + +const sendAssetsPage = new SendAssetsPage(); + +fixture('Send') + .clientScripts({ content: injectLS(FIXTURE_HARDHAT) }) + .page(PAGES.SEND); + +test('Complete SendFlow', async (t) => { + await t.click(getByText(findByTKey('SEND_ASSETS'))); + + await sendAssetsPage.waitForPage(PAGES.SEND); + + /* Should have and support PTX */ + await setFeatureFlag('PROTECT_TX', true); + + const ptxBtn = getByText(findByTKey('PROTECTED_TX_GET_TX_PROTECTION')); + await t.expect(ptxBtn.exists).ok(); + + await t.click(ptxBtn); + + const ptxNextButton = getByText(findByTKey('PROTECTED_TX_PROTECT_MY_TX')); + await t.expect(ptxNextButton.exists).ok(); + + // Information Missing since form isn't filled + const missingInfo = getByText(findByTKey('MISSING_INFORMATION')); + await t.expect(missingInfo.exists).ok(); + + // Close the PTX modal + await t.click(Selector('.close-icon')); + + await setupEthereumMock(FIXTURE_HARDHAT_PRIVATE_KEY, 1); + + /* Can complete form and send tx */ + await sendAssetsPage.fillForm(); + await t.wait(FIXTURES_CONST.TIMEOUT); + await sendAssetsPage.submitForm(); + await t.wait(FIXTURES_CONST.TIMEOUT); + + // Has continued to next step with sign button + const signBtn = queryByText(findByTKey('CONFIRM_AND_SEND')).with({ + timeout: FIXTURES_CONST.HARDHAT_TIMEOUT + }); + await t.expect(signBtn.exists).ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); + + // Expect to reach confirm tx + await t.expect(getByText(findByTKey('CONFIRM_TX_MODAL_TITLE')).exists).ok(); + await t.expect(getAllByText(FIXTURE_SEND_AMOUNT, { exact: false }).exists).ok(); + await t.expect(getAllByText(FIXTURE_SEND_CONTACT).exists).ok(); + + // Send TX + await t.click(getByText(findByTKey('CONFIRM_AND_SEND'))); + + // Expect to reach Tx Receipt + const viewDetailsButton = await queryByText(findByTKey('VIEW_TRANSACTION_DETAILS')).with({ + timeout: FIXTURES_CONST.TIMEOUT + }); + await t.expect(viewDetailsButton.exists).ok(); + await t.click(viewDetailsButton); + + await t.expect(getAllByText(FIXTURE_SEND_AMOUNT, { exact: false }).exists).ok(); + await t.expect(getAllByText(FIXTURE_SEND_CONTACT).exists).ok(); +}); + +test('Valid transaction query params are correctly parsed and loaded into send flow', async (t) => { + const validQueryParams = + '?queryType=speedup&gasLimit=0xcb56&chainId=1&nonce=0xD8&gasPrice=0x059682f000&from=0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf&to=0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf&value=0x0&data=0xa9059cbb0000000000000000000000005dd6e754d37bababeb95f34639568812900fec7900000000000000000000000000000000000000000000000000038D7EA4C68000'; + + await sendAssetsPage.navigateToPage(validQueryParams); + await sendAssetsPage.waitPageLoaded(validQueryParams); + + // Expect to reach confirm tx and correctly interpret amount field for erc20 tx + await t.expect(getByText(findByTKey('CONFIRM_TX_MODAL_TITLE')).exists).ok(); + await t.expect(getAllByText(FIXTURE_SEND_AMOUNT, { exact: false }).exists).ok(); +}); + +test('Shows warning for invalid query params', async (t) => { + const invalidQueryParams = '?queryType=speedup&chainId=1'; + + await sendAssetsPage.navigateToPage(invalidQueryParams); + await sendAssetsPage.waitPageLoaded(invalidQueryParams); + + // Should show invalid speedup query params message + const invalidSpeedUpQueryParamsMessage = Selector('.alert-info'); + await t.expect(invalidSpeedUpQueryParamsMessage.exists).ok(); +}); diff --git a/__tests__/settings-page.po.js b/__tests__/settings-page.po.js new file mode 100644 index 00000000000..b1b43aef6da --- /dev/null +++ b/__tests__/settings-page.po.js @@ -0,0 +1,22 @@ +import { Selector, t } from 'testcafe'; + +import BasePage from './base-page.po'; +import { PAGES } from './fixtures'; + +export default class SettingsPage extends BasePage { + async navigateToPage() { + this.navigateTo(PAGES.SETTINGS); + } + + async waitPageLoaded() { + await this.waitForPage(PAGES.SETTINGS); + } + + async expectAccountTableToMatchCount(count) { + await t.expect(Selector('section:first-of-type > div > table tbody tr').count).eql(count); + } + + async expectContactTableToMatchCount(count) { + await t.expect(Selector('section:nth-of-type(2) > div > table tbody tr').count).eql(count); + } +} diff --git a/__tests__/storage.test.js b/__tests__/storage.test.js new file mode 100644 index 00000000000..90eae143a47 --- /dev/null +++ b/__tests__/storage.test.js @@ -0,0 +1,30 @@ +import { getByText } from '@testing-library/testcafe'; + +import AddAccountPage from './addaccount-page.po'; +import { injectLS } from './clientScripts'; +import { FIXTURE_LOCALSTORAGE_WITH_ONE_ACC, PAGES } from './fixtures'; +import SettingsPage from './settings-page.po'; +import { getTransValueByKey } from './translation-utils'; + +const addAccountPage = new AddAccountPage(); +const settingsPage = new SettingsPage(); + +fixture('Storage').page(PAGES.DASHBOARD); + +test('Should apply empty storage', async (t) => { + // localStorage is set after page load, so first navigation should happen + await addAccountPage.navigateTo(PAGES.DASHBOARD); + await addAccountPage.waitPageLoaded(); + + const title = getByText(getTransValueByKey('DECRYPT_ACCESS')); + await t.expect(title.exists).ok(); +}); + +test.clientScripts({ content: injectLS(FIXTURE_LOCALSTORAGE_WITH_ONE_ACC) })( + 'Should apply storage with one account', + async () => { + await settingsPage.navigateToPage(); + await settingsPage.waitPageLoaded(); + await settingsPage.expectAccountTableToMatchCount(1); + } +); diff --git a/__tests__/swap-page.po.js b/__tests__/swap-page.po.js new file mode 100644 index 00000000000..ede59d70e01 --- /dev/null +++ b/__tests__/swap-page.po.js @@ -0,0 +1,30 @@ +import { getByTestId } from '@testing-library/testcafe'; +import { Selector, t } from 'testcafe'; + +import BasePage from './base-page.po'; +import { setupEthereumMock } from './ethereum-mock'; +import { FIXTURE_HARDHAT_PRIVATE_KEY, FIXTURE_SEND_AMOUNT, PAGES } from './fixtures'; + +export default class SwapPage extends BasePage { + async navigateToPage() { + this.navigateTo(PAGES.SWAP); + } + + async waitPageLoaded(timeout) { + await this.waitForPage(PAGES.SWAP, timeout); + } + + async fillForm() { + await t.typeText(Selector('input[name="swap-from"]').parent(), FIXTURE_SEND_AMOUNT); + } + + async fillFormERC20() { + await t.click(getByTestId('asset-selector-option-ETH')); + await t.click(getByTestId('asset-selector-option-DAI')); + await t.typeText(Selector('input[name="swap-from"]').parent(), FIXTURE_SEND_AMOUNT); + } + + async setupMock() { + await setupEthereumMock(FIXTURE_HARDHAT_PRIVATE_KEY, 1); + } +} diff --git a/__tests__/swap.test.js b/__tests__/swap.test.js new file mode 100644 index 00000000000..de841274ad4 --- /dev/null +++ b/__tests__/swap.test.js @@ -0,0 +1,89 @@ +import { + getByTestId, + queryAllByTestId, + queryAllByText, + queryByText +} from '@testing-library/testcafe'; + +import { injectLS } from './clientScripts'; +import DashboardPage from './dashboard-page.po'; +import { FIXTURE_HARDHAT, FIXTURES_CONST, PAGES } from './fixtures'; +import { resetFork, setupDAI } from './hardhat-utils'; +import SwapPage from './swap-page.po'; +import { findByTKey } from './translation-utils'; + +const swapPage = new SwapPage(); +const dashboardPage = new DashboardPage(); + +fixture('Swap') + .clientScripts({ content: injectLS(FIXTURE_HARDHAT) }) + .page(PAGES.SWAP); + +test('can do an ETH swap', async (t) => { + await swapPage.waitPageLoaded(); + await swapPage.setupMock(); + await resetFork(false); + + await swapPage.fillForm(); + await t.wait(FIXTURES_CONST.TIMEOUT); + + const button = await getByTestId('confirm-swap'); + await t.click(button); + + const send = await queryByText(findByTKey('CONFIRM_AND_SEND')).with({ + timeout: FIXTURES_CONST.HARDHAT_TIMEOUT + }); + await t.expect(send.exists).ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); + await t.click(send); + + const viewDetailsButton = await queryByText(findByTKey('VIEW_TRANSACTION_DETAILS')).with({ + timeout: FIXTURES_CONST.TIMEOUT + }); + await t.expect(viewDetailsButton.exists).ok(); + await t.click(viewDetailsButton); + + await t + .expect(queryAllByTestId('SUCCESS').with({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }).exists) + .ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); +}); + +test('can do an ERC20 swap', async (t) => { + await resetFork(false); + await setupDAI(); + await swapPage.waitPageLoaded(); + await swapPage.setupMock(); + + await swapPage.fillFormERC20(); + await t.wait(FIXTURES_CONST.TIMEOUT); + + const button = await getByTestId('confirm-swap'); + await t.click(button); + + const approve = await queryAllByText(findByTKey('APPROVE_SWAP')) + .with({ + timeout: FIXTURES_CONST.HARDHAT_TIMEOUT + }) + .nth(1); + await t.expect(approve.exists).ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); + await t.click(approve); + + await t.wait(FIXTURES_CONST.TIMEOUT); + + const send = await queryByText(findByTKey('CONFIRM_TRANSACTION')).with({ + timeout: FIXTURES_CONST.HARDHAT_TIMEOUT + }); + await t.expect(send.exists).ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); + await t.click(send); + + await t + .expect(queryAllByTestId('SUCCESS').with({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }).count) + .eql(2, { timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); + + const home = await queryByText(findByTKey('NAVIGATION_HOME')); + await t.click(home); + + await dashboardPage.waitPageLoaded(); + const balance = await queryAllByText('1INCH', { exact: false }); + + await t.expect(balance.exists).ok({ timeout: FIXTURES_CONST.HARDHAT_TIMEOUT }); +}); diff --git a/__tests__/translation-utils.js b/__tests__/translation-utils.js new file mode 100644 index 00000000000..1c97d36ca2c --- /dev/null +++ b/__tests__/translation-utils.js @@ -0,0 +1,6 @@ +const enLang = require('../src/translations/lang/en.json'); + +const getTransValueByKey = (translateKey) => enLang.data[translateKey]; +const findByTKey = (k) => new RegExp(getTransValueByKey(k, 'i')); + +export { getTransValueByKey, findByTKey }; diff --git a/__tests__/txstatus-page.po.js b/__tests__/txstatus-page.po.js new file mode 100644 index 00000000000..5a5ae58b6d2 --- /dev/null +++ b/__tests__/txstatus-page.po.js @@ -0,0 +1,23 @@ +import { Selector, t } from 'testcafe'; + +import BasePage from './base-page.po'; +import { FIXTURE_ETHEREUM, PAGES } from './fixtures'; +import { getTransValueByKey } from './translation-utils'; + +export default class TxStatusPage extends BasePage { + async navigateToPage() { + this.navigateTo(PAGES.TX_STATUS); + } + + async waitPageLoaded(timeout) { + await this.waitForPage(PAGES.TX_STATUS, timeout); + } + + async fillForm(hash) { + await t + .click(Selector('div[data-testid="selector"]')) + .click(Selector('div[data-testid="selector"]').find('span').withText(FIXTURE_ETHEREUM)) + .typeText(Selector(`input[name="txhash"]`), hash, { paste: true }) + .click(Selector('button').withText(getTransValueByKey('FETCH'))); + } +} diff --git a/__tests__/txstatus.test.js b/__tests__/txstatus.test.js new file mode 100644 index 00000000000..dfebfbc0443 --- /dev/null +++ b/__tests__/txstatus.test.js @@ -0,0 +1,44 @@ +import { getAllByTestId, getAllByText, getByText } from '@testing-library/testcafe'; +import { Selector } from 'testcafe'; + +import { injectLS } from './clientScripts'; +import { resetFeatureFlags, setFeatureFlag } from './featureflag-utils'; +import { + FIXTURE_HARDHAT, + FIXTURE_SEND_AMOUNT, + FIXTURE_WEB3_ADDRESS, + FIXTURES_CONST, + PAGES +} from './fixtures'; +import { sendTx } from './hardhat-utils'; +import { getTransValueByKey } from './translation-utils'; +import TxStatusPage from './txstatus-page.po'; + +const txStatusPage = new TxStatusPage(); + +fixture('TX Status') + .clientScripts({ content: injectLS(FIXTURE_HARDHAT) }) + .page(PAGES.TX_STATUS); + +test('Can fetch status of a successful TX', async (t) => { + const hash = await sendTx({ + from: FIXTURE_WEB3_ADDRESS, + to: FIXTURE_WEB3_ADDRESS, + value: '0x38d7ea4c68000' + }); + await setFeatureFlag('TX_STATUS', true); + await txStatusPage.waitPageLoaded(); + + const title = getByText(getTransValueByKey('TX_STATUS')); + await t.expect(title.exists).ok(); + + await txStatusPage.fillForm(hash); + + await t.wait(FIXTURES_CONST.TIMEOUT); + + await t.expect(Selector('button').withText(getTransValueByKey('FETCH')).exists).eql(false); + await t.expect(getAllByText(FIXTURE_SEND_AMOUNT, { exact: false }).exists).ok(); + await t.expect(getAllByTestId('SUCCESS').exists).ok(); + + await resetFeatureFlags(); +}); diff --git a/__tests__/website.test.js b/__tests__/website.test.js new file mode 100644 index 00000000000..f8a46bc9667 --- /dev/null +++ b/__tests__/website.test.js @@ -0,0 +1,28 @@ +import { ClientFunction, Selector } from 'testcafe'; + +import AddAccountPage from './addaccount-page.po'; +import { PAGES } from './fixtures'; + +const addAccount = new AddAccountPage(); +fixture('Dashboard').page(PAGES.DASHBOARD); + +const getInfo = ClientFunction(() => ({ + domain: document.domain, + hostname: document.location.hostname +})); + +test('It contains a favicon tag', async (t) => { + await addAccount.waitPageLoaded(); + + const favicon = Selector('head link[rel=icon]'); + const hasHref = await favicon.hasAttribute('href'); + + await t.expect(favicon.exists).ok(); + await t.expect(hasHref).ok(); +}); + +// Ensure we have the same domain with the landing page. +test('It set the correct document.domain per environment', async (t) => { + const info = await getInfo(); + await t.expect(info.domain).eql(info.hostname); +}); diff --git a/babel.config.js b/babel.config.js index ab8c44ed904..580494e5c79 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,22 +1,36 @@ module.exports = { - 'presets': [ - '@babel/preset-react', + presets: [ + [ + '@babel/preset-react', + { + runtime: 'automatic' + } + ], '@babel/preset-typescript', [ '@babel/preset-env', { - // The following plugin fixes false warnings - // "export '...' was not found in '...' " - // https://github.com/webpack-contrib/imports-loader/issues/68 - 'modules': ['commonjs'], + // Enable lodash tree shaking + // https://www.azavea.com/blog/2019/03/07/lessons-on-tree-shaking-lodash/ + modules: false, + targets: { node: 12 }, + // babel defaults to core-js@2. + // https://github.com/babel/babel/issues/9713#issuecomment-474828830 + useBuiltIns: 'usage', + corejs: 3, + loose: true } ] ], - 'plugins': [ + plugins: [ 'babel-plugin-styled-components', '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-proposal-private-methods', '@babel/plugin-syntax-dynamic-import', - 'react-hot-loader/babel', - ] + // Fix warnings with TS "export '...' was not found in '...' " + // https: //github.com/webpack-contrib/imports-loader/issues/68#issuecomment-528788909 + '@babel/plugin-transform-modules-commonjs', + process.env.NODE_ENV === 'development' && 'react-refresh/babel' + ].filter(Boolean) }; diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000000..3bcdd981d65 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,11 @@ +coverage: + round: down + precision: 2 + status: + patch: + default: + threshold: 10% + target: auto + base: auto +ignore: + - '**/analytics.middleware.ts' diff --git a/common/AppProviders.tsx b/common/AppProviders.tsx deleted file mode 100644 index 7ec94e298d5..00000000000 --- a/common/AppProviders.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; - -import { NotificationsProvider, ToastProvider, ErrorProvider } from 'v2/features'; -import { - AccountProvider, - AddressBookProvider, - AssetProvider, - NetworkProvider, - SettingsProvider, - ContractProvider, - DataProvider -} from 'v2/services/Store'; -import { DevToolsProvider, RatesProvider, StoreProvider } from 'v2/services'; - -function AppProviders({ children }: { children: JSX.Element[] | JSX.Element | null }) { - return ( - - - - - - - - - - - - {/* StoreProvider relies on the others Providers */} - - {/* RatesProvider relies on the Store */} - {children} - - - - - - - - - - - - - ); -} - -export default AppProviders; diff --git a/common/AppRoutes.tsx b/common/AppRoutes.tsx deleted file mode 100644 index 166b7c6048a..00000000000 --- a/common/AppRoutes.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { Route, Switch, withRouter } from 'react-router-dom'; - -import { Layout } from 'v2/features/Layout'; -import { Home, PageNotFound, ScreenLockProvider, DrawerProvider } from 'v2/features'; -import { ScrollToTop } from 'v2/utils'; -import { ROUTE_PATHS } from 'v2/config/routePaths'; -import { - APP_ROUTES, - PageVisitsAnalytics, - LegacyRoutesHandler, - DefaultHomeHandler, - PrivateRoute -} from 'v2/routing'; - -const LayoutWithLocation = withRouter(({ location, children }) => { - const homeLayout = { - centered: false, - fluid: true, - fullW: true, - bgColor: '#fff' - }; - const isHomeRoute = - location.pathname === ROUTE_PATHS.ROOT.path || location.pathname === ROUTE_PATHS.HOME.path; - return {children}; -}); - -export const AppRoutes = () => { - return ( - <> - - - - - - - {/* To avoid fiddling with layout we provide a complete route to home */} - - - - - {APP_ROUTES.filter(route => !route.seperateLayout).map((config, idx) => ( - - ))} - - - - - - - - - - - ); -}; diff --git a/common/Root.tsx b/common/Root.tsx deleted file mode 100644 index ee42d07954e..00000000000 --- a/common/Root.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { Component } from 'react'; -import { setConfig } from 'react-hot-loader'; -import { hot } from 'react-hot-loader/root'; -import styled, { ThemeProvider } from 'styled-components'; -import { HashRouter, BrowserRouter } from 'react-router-dom'; -// v2 -import { BREAK_POINTS, GAU_THEME } from 'v2/theme'; -import { IS_DEV, IS_ELECTRON, IS_DOWNLOADABLE, IS_PROD } from 'v2/utils'; -import { NewAppReleaseModal } from 'v2/components'; -import { DevToolsManager } from 'v2/features'; -import AppProviders from './AppProviders'; -import { AppRoutes } from './AppRoutes'; - -const AppProvidersInnerContainer = styled.div` - display: flex; -`; -const AppRouterContainer = styled.div` - flex: 1; - overflow: auto; - max-height: 100vh; -`; -const DevToolsManagerContainer = styled.div` - overflow: auto; - max-height: 100vh; - - @media (max-width: ${BREAK_POINTS.SCREEN_SM}) { - position: absolute; - z-index: 100; - width: 100vw; - } -`; - -class RootClass extends Component { - public componentDidMount() { - this.addBodyClasses(); - } - - public render() { - const Router: any = IS_DOWNLOADABLE && IS_PROD ? HashRouter : BrowserRouter; - - return ( - - - - - {/* DevToolsManager */} - - {IS_DEV ? : <>} - - - {/* Router */} - - -
- {IS_ELECTRON ? : <>} - - - - - - ); - } - - private addBodyClasses() { - const classes: string[] = []; - - if (IS_ELECTRON) { - classes.push('is-electron'); - - if (navigator.appVersion.includes('Win')) { - classes.push('is-windows'); - } else if (navigator.appVersion.includes('Mac')) { - classes.push('is-osx'); - } else { - classes.push('is-linux'); - } - } - - document.body.className += ` ${classes.join(' ')}`; - } -} - -// Silence RHL 'reconciliation failed' errors -// https://github.com/gatsbyjs/gatsby/issues/7209#issuecomment-415807021 -setConfig({ logLevel: 'no-errors-please' }); -export default hot(RootClass); diff --git a/common/assets/images/arrow-right.svg b/common/assets/images/arrow-right.svg deleted file mode 100644 index 7912ffa1111..00000000000 --- a/common/assets/images/arrow-right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/common/assets/images/banners/Ledger_Desktop.png b/common/assets/images/banners/Ledger_Desktop.png deleted file mode 100644 index 1ee864ba99d..00000000000 Binary files a/common/assets/images/banners/Ledger_Desktop.png and /dev/null differ diff --git a/common/assets/images/banners/Ledger_Mobile.png b/common/assets/images/banners/Ledger_Mobile.png deleted file mode 100644 index 236ae88da70..00000000000 Binary files a/common/assets/images/banners/Ledger_Mobile.png and /dev/null differ diff --git a/common/assets/images/banners/Simplex_Desktop.png b/common/assets/images/banners/Simplex_Desktop.png deleted file mode 100644 index 3f7d8ce2074..00000000000 Binary files a/common/assets/images/banners/Simplex_Desktop.png and /dev/null differ diff --git a/common/assets/images/banners/Simplex_Mobile.png b/common/assets/images/banners/Simplex_Mobile.png deleted file mode 100644 index 4543e67010e..00000000000 Binary files a/common/assets/images/banners/Simplex_Mobile.png and /dev/null differ diff --git a/common/assets/images/banners/Trezor_Desktop.png b/common/assets/images/banners/Trezor_Desktop.png deleted file mode 100644 index 4987c6ba6f0..00000000000 Binary files a/common/assets/images/banners/Trezor_Desktop.png and /dev/null differ diff --git a/common/assets/images/banners/Trezor_Mobile.png b/common/assets/images/banners/Trezor_Mobile.png deleted file mode 100644 index 8aa00944423..00000000000 Binary files a/common/assets/images/banners/Trezor_Mobile.png and /dev/null differ diff --git a/common/assets/images/chevron-right.svg b/common/assets/images/chevron-right.svg deleted file mode 100644 index d2ed6658186..00000000000 --- a/common/assets/images/chevron-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/common/assets/images/close.svg b/common/assets/images/close.svg deleted file mode 100644 index 2805991d14d..00000000000 --- a/common/assets/images/close.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/common/assets/images/control-crypto-mobile.svg b/common/assets/images/control-crypto-mobile.svg deleted file mode 100644 index 19388d0370f..00000000000 --- a/common/assets/images/control-crypto-mobile.svg +++ /dev/null @@ -1,529 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Manage your information and your - cryptocurrency easily and without fear. - - - diff --git a/common/assets/images/control-crypto.svg b/common/assets/images/control-crypto.svg deleted file mode 100644 index 57adb79a5cd..00000000000 --- a/common/assets/images/control-crypto.svg +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/credits/credits-coingecko.png b/common/assets/images/credits/credits-coingecko.png deleted file mode 100644 index d1c8dcb6b76..00000000000 Binary files a/common/assets/images/credits/credits-coingecko.png and /dev/null differ diff --git a/common/assets/images/ether.png b/common/assets/images/ether.png deleted file mode 100644 index e2b7336252e..00000000000 Binary files a/common/assets/images/ether.png and /dev/null differ diff --git a/common/assets/images/icn-back.svg b/common/assets/images/icn-back.svg deleted file mode 100644 index eca2910c902..00000000000 --- a/common/assets/images/icn-back.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/common/assets/images/icn-create-pw.svg b/common/assets/images/icn-create-pw.svg deleted file mode 100644 index 4496abcd055..00000000000 --- a/common/assets/images/icn-create-pw.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - 4FBCFCD9-6341-4ABD-AD1C-C7692052E1E0 - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/common/assets/images/icn-dashboard.svg b/common/assets/images/icn-dashboard.svg deleted file mode 100644 index 85aa7d790f8..00000000000 --- a/common/assets/images/icn-dashboard.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/common/assets/images/icn-existing-wallet.svg b/common/assets/images/icn-existing-wallet.svg deleted file mode 100644 index 02054f6a9cb..00000000000 --- a/common/assets/images/icn-existing-wallet.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/common/assets/images/icn-external-link.svg b/common/assets/images/icn-external-link.svg deleted file mode 100644 index 241e9d970ce..00000000000 --- a/common/assets/images/icn-external-link.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/common/assets/images/icn-fee.svg b/common/assets/images/icn-fee.svg deleted file mode 100644 index 3f78a730603..00000000000 --- a/common/assets/images/icn-fee.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/icn-forgot-password.svg b/common/assets/images/icn-forgot-password.svg deleted file mode 100644 index 07602d24c3f..00000000000 --- a/common/assets/images/icn-forgot-password.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - 79B14051-D0B7-4E9F-BC9E-B7AD99A31F70 - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/common/assets/images/icn-hardware-wallet.svg b/common/assets/images/icn-hardware-wallet.svg deleted file mode 100644 index d5ce01228e2..00000000000 --- a/common/assets/images/icn-hardware-wallet.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/icn-important-update.svg b/common/assets/images/icn-important-update.svg deleted file mode 100644 index 0edb0e29151..00000000000 --- a/common/assets/images/icn-important-update.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/common/assets/images/icn-keystore.svg b/common/assets/images/icn-keystore.svg deleted file mode 100644 index 76a82365853..00000000000 --- a/common/assets/images/icn-keystore.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - 0EAEF11C-05BE-4E50-AF1D-32B5CD0EE171 - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/common/assets/images/icn-lock-safety.svg b/common/assets/images/icn-lock-safety.svg deleted file mode 100644 index 20d048995cc..00000000000 --- a/common/assets/images/icn-lock-safety.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - F0739086-BD8C-4061-9ABD-93792C7F5EB2 - Created with sketchtool. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/common/assets/images/icn-new-window.svg b/common/assets/images/icn-new-window.svg deleted file mode 100644 index 2fd7463c864..00000000000 --- a/common/assets/images/icn-new-window.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/common/assets/images/icn-opensource.svg b/common/assets/images/icn-opensource.svg deleted file mode 100644 index 307c3be2e41..00000000000 --- a/common/assets/images/icn-opensource.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/icn-oval.svg b/common/assets/images/icn-oval.svg deleted file mode 100644 index 74b302c2323..00000000000 --- a/common/assets/images/icn-oval.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/common/assets/images/icn-printer.svg b/common/assets/images/icn-printer.svg deleted file mode 100644 index 1fd310f9d41..00000000000 --- a/common/assets/images/icn-printer.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/common/assets/images/icn-privatekey-new.svg b/common/assets/images/icn-privatekey-new.svg deleted file mode 100644 index a2bd66818ec..00000000000 --- a/common/assets/images/icn-privatekey-new.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/common/assets/images/icn-protect.svg b/common/assets/images/icn-protect.svg deleted file mode 100644 index afc489324d3..00000000000 --- a/common/assets/images/icn-protect.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/icn-purple-swoosh.svg b/common/assets/images/icn-purple-swoosh.svg deleted file mode 100644 index c2c650a4527..00000000000 --- a/common/assets/images/icn-purple-swoosh.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/common/assets/images/icn-safet-mini-new.svg b/common/assets/images/icn-safet-mini-new.svg deleted file mode 100644 index ec762d3b104..00000000000 --- a/common/assets/images/icn-safet-mini-new.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/common/assets/images/icn-sent.svg b/common/assets/images/icn-sent.svg deleted file mode 100644 index d28f83d2a54..00000000000 --- a/common/assets/images/icn-sent.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/icn-settings.svg b/common/assets/images/icn-settings.svg deleted file mode 100644 index 9392e8e1569..00000000000 --- a/common/assets/images/icn-settings.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/common/assets/images/icn-sparkles-1.svg b/common/assets/images/icn-sparkles-1.svg deleted file mode 100644 index 743fca922c7..00000000000 --- a/common/assets/images/icn-sparkles-1.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/common/assets/images/icn-sparkles-2.svg b/common/assets/images/icn-sparkles-2.svg deleted file mode 100644 index 0fe88788d21..00000000000 --- a/common/assets/images/icn-sparkles-2.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/common/assets/images/icn-sparkles-3.svg b/common/assets/images/icn-sparkles-3.svg deleted file mode 100644 index a6136cbb5ce..00000000000 --- a/common/assets/images/icn-sparkles-3.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/common/assets/images/icn-sparkles-4.svg b/common/assets/images/icn-sparkles-4.svg deleted file mode 100644 index a530bf75490..00000000000 --- a/common/assets/images/icn-sparkles-4.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/common/assets/images/icn-unlock.svg b/common/assets/images/icn-unlock.svg deleted file mode 100644 index 83affbf590c..00000000000 --- a/common/assets/images/icn-unlock.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/common/assets/images/icn-update.svg b/common/assets/images/icn-update.svg deleted file mode 100644 index 58a71fc456b..00000000000 --- a/common/assets/images/icn-update.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/common/assets/images/icn-vault2.svg b/common/assets/images/icn-vault2.svg deleted file mode 100644 index abe923f6744..00000000000 --- a/common/assets/images/icn-vault2.svg +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/icn-wallet.svg b/common/assets/images/icn-wallet.svg deleted file mode 100644 index a1676e99dcb..00000000000 --- a/common/assets/images/icn-wallet.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/icn-website.svg b/common/assets/images/icn-website.svg deleted file mode 100644 index c576317e797..00000000000 --- a/common/assets/images/icn-website.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/illo-bank.svg b/common/assets/images/illo-bank.svg deleted file mode 100644 index ca694e8d360..00000000000 --- a/common/assets/images/illo-bank.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/illo-with-mycrypto.svg b/common/assets/images/illo-with-mycrypto.svg deleted file mode 100644 index 86969399545..00000000000 --- a/common/assets/images/illo-with-mycrypto.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/logo-mycrypto.svg b/common/assets/images/logo-mycrypto.svg deleted file mode 100755 index a20d08bb560..00000000000 --- a/common/assets/images/logo-mycrypto.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - Artboard - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/manage-your-wallets-mobile.svg b/common/assets/images/manage-your-wallets-mobile.svg deleted file mode 100644 index 8f93304548a..00000000000 --- a/common/assets/images/manage-your-wallets-mobile.svg +++ /dev/null @@ -1,528 +0,0 @@ - - - - - - image/svg+xml - - 0EFC2C9C-4B99-4A38-BD8F-E4DD90E57515 - - - - - 0EFC2C9C-4B99-4A38-BD8F-E4DD90E57515 - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MyCrypto allows you to create, import, and - manage all the wallets you'll ever need. - - - - View Your Dashboard - - - diff --git a/common/assets/images/manage-your-wallets.svg b/common/assets/images/manage-your-wallets.svg deleted file mode 100644 index 56c6b29e796..00000000000 --- a/common/assets/images/manage-your-wallets.svg +++ /dev/null @@ -1,744 +0,0 @@ - - - - - - image/svg+xml - - BF3F2E58-79B5-47F9-987C-A6C2C981FAE0 - - - - - BF3F2E58-79B5-47F9-987C-A6C2C981FAE0 - Created with sketchtool. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/returning.svg b/common/assets/images/returning.svg deleted file mode 100644 index 566281297ab..00000000000 --- a/common/assets/images/returning.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/common/assets/images/swap copy.svg b/common/assets/images/swap copy.svg deleted file mode 100644 index be9e67d0b33..00000000000 --- a/common/assets/images/swap copy.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/title-illustration.svg b/common/assets/images/title-illustration.svg deleted file mode 100644 index 09525174e02..00000000000 --- a/common/assets/images/title-illustration.svg +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/transactions/inbound.svg b/common/assets/images/transactions/inbound.svg deleted file mode 100644 index ad7e0a243f5..00000000000 --- a/common/assets/images/transactions/inbound.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/common/assets/images/transactions/outbound.svg b/common/assets/images/transactions/outbound.svg deleted file mode 100644 index 90ac5ac9194..00000000000 --- a/common/assets/images/transactions/outbound.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/common/assets/images/transactions/transfer.svg b/common/assets/images/transactions/transfer.svg deleted file mode 100644 index cfe2fa3243e..00000000000 --- a/common/assets/images/transactions/transfer.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/common/assets/images/view-dashboard-mobile.svg b/common/assets/images/view-dashboard-mobile.svg deleted file mode 100644 index 5f0fa34dadc..00000000000 --- a/common/assets/images/view-dashboard-mobile.svg +++ /dev/null @@ -1,304 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Oct 29 - - - - - - - - - - - - - - - - See a detailed breakdown of all your - wallets, tokens, and transactions. - - - diff --git a/common/assets/images/view-dashboard.svg b/common/assets/images/view-dashboard.svg deleted file mode 100644 index 42f2367fa9c..00000000000 --- a/common/assets/images/view-dashboard.svg +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/wallets/cipher.svg b/common/assets/images/wallets/cipher.svg deleted file mode 100644 index 7a3bcd38abe..00000000000 --- a/common/assets/images/wallets/cipher.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - Group 3 - Created with Sketch. - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/common/assets/images/wallets/ledger.svg b/common/assets/images/wallets/ledger.svg deleted file mode 100644 index 7fe44bd7711..00000000000 --- a/common/assets/images/wallets/ledger.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - ledger - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/common/assets/images/wallets/parity-signer.svg b/common/assets/images/wallets/parity-signer.svg deleted file mode 100644 index 63065b758bd..00000000000 --- a/common/assets/images/wallets/parity-signer.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/common/assets/images/wallets/safe-t.png b/common/assets/images/wallets/safe-t.png deleted file mode 100644 index 9f11125fbc5..00000000000 Binary files a/common/assets/images/wallets/safe-t.png and /dev/null differ diff --git a/common/assets/images/wallets/safe-t.svg b/common/assets/images/wallets/safe-t.svg deleted file mode 100644 index 103d46512de..00000000000 --- a/common/assets/images/wallets/safe-t.svg +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - image/svg+xml - - trezor - - - - - - trezor - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/assets/images/wallets/trezor.svg b/common/assets/images/wallets/trezor.svg deleted file mode 100644 index 6f0e66d5ea0..00000000000 --- a/common/assets/images/wallets/trezor.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - trezor - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/common/assets/images/wallets/trust-2.svg b/common/assets/images/wallets/trust-2.svg deleted file mode 100644 index 2379517087b..00000000000 --- a/common/assets/images/wallets/trust-2.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/common/assets/images/wallets/trust.svg b/common/assets/images/wallets/trust.svg deleted file mode 100644 index dd4eb931752..00000000000 --- a/common/assets/images/wallets/trust.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - trust - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/common/index.tsx b/common/index.tsx deleted file mode 100644 index 48bce5fb2fc..00000000000 --- a/common/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// Application styles must come first in order, to allow for overrides -import 'font-awesome/scss/font-awesome.scss'; -import 'sass/styles.scss'; -import '@babel/polyfill'; - -import 'whatwg-fetch'; // @TODO: Investigate utility of dependency -import 'what-input'; // @TODO: Investigate utility of dependency; Used in /common/sass/styles.scss for `data-whatintent` - -import React from 'react'; -import { render } from 'react-dom'; - -import { consoleAdvertisement } from 'v2/utils'; - -import Root from './Root'; - -render(, document.getElementById('app')); - -if (process.env.NODE_ENV === 'production') { - consoleAdvertisement(); -} - -const noOp = (event: DragEvent) => { - event.preventDefault(); - return false; -}; - -// disables drag-and-drop due to potential security issues by Cure53 recommendation -document.addEventListener('dragover', noOp, false); -document.addEventListener('drop', noOp, false); diff --git a/common/sass/styles.scss b/common/sass/styles.scss deleted file mode 100644 index 155605f0161..00000000000 --- a/common/sass/styles.scss +++ /dev/null @@ -1,51 +0,0 @@ -// Where shared common styles live - -// --- BOOTSTRAP --- -@import './variables'; -@import './mixins'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/normalize'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/print'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/scaffolding'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/type'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/code'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/grid'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/tables'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/dropdowns'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/input-groups'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/wells'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/close'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/utilities'; -@import '~bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities'; - -// --- RC SLIDER --- -@import '~rc-slider/assets/index.css'; - -// --- React Select --- -@import '~react-select/dist/react-select.css'; - -// --- CUSTOM --- -@import './styles/badbrowser'; -@import './styles/noscript'; -@import './styles/overrides'; -@import './styles/scaffolding'; -@import './styles/tab'; -@import './styles/flexbox'; -@import './styles/helpers'; -@import './styles/code'; -@import './styles/buttons'; -@import './styles/forms'; -@import './styles/alerts'; -@import './styles/themes'; -@import './fonts'; -@import './styles/react-datetime'; - -/* suppress focus ring on form controls for mouse users */ -// 'what-input' package / https://github.com/ten1seven/what-input#basic-styling -[data-whatintent='mouse'] *:focus { - outline: none; -} - -// This is fine because the outline effect is reproduced with border and box-shadow styles on input elements -input { - outline: none; -} diff --git a/common/sass/styles/forms.scss b/common/sass/styles/forms.scss deleted file mode 100644 index ac8e6b53e76..00000000000 --- a/common/sass/styles/forms.scss +++ /dev/null @@ -1,132 +0,0 @@ -// A fork of bootstrap's forms to have some custom behavior, and to work with -// css variables -@import 'common/sass/variables'; -@import 'common/sass/mixins'; - -// Normalize elements for various browsers -fieldset { - padding: 0; - margin: 0; - border: 0; - min-width: 0; -} - -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: $line-height-computed; - font-size: ($font-size-base * 1.5); - line-height: inherit; - color: $legend-color; - border: 0; - border-bottom: 1px solid $legend-border-color; -} - -input[type="search"] { - box-sizing: border-box; - -webkit-appearance: none; -} - -input[type="file"] { - display: block; -} - -input[type="range"] { - display: block; - width: 100%; -} - -select[multiple], -select[size] { - height: auto; -} - -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - @include tab-focus; -} - -// Labels -label { - display: inline-block; - max-width: 100%; - margin-bottom: $space-xs; - font-weight: bold; - - &.is-required:after { - content: '*'; - padding-left: 2px; - color: color(brand-warning); - } -} - -// Checkbox / radio -.radio, -.checkbox { - position: relative; - display: block; - margin: 15px 0; - font-weight: 500; - - label { - min-height: $line-height-computed; - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - cursor: pointer; - } -} - -.radio input[type="radio"], -.radio-inline input[type="radio"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - position: absolute; - margin-left: -20px; - margin-top: 4px \9; -} - -.radio + .radio, -.checkbox + .checkbox { - margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing -} - -input[type='radio'], -input[type='checkbox'] { - margin: 3px 0 0; - line-height: normal; -} - -// Misc -input[readonly] { - background-color: color(control-disabled-bg); - color: color(control-disabled-color); - border-color: color(control-disabled-border); - cursor: text !important; -} - -.form-group { - display: block; - margin-bottom: $form-group-margin-bottom; -} - -.help-block { - display: block; - margin-top: $space-xs; - margin-bottom: $space-sm; - opacity: 0.8; - - &.is-valid { - color: color(brand-success); - } - - &.is-invalid { - color: color(brand-danger); - } - - &.is-semivalid { - color: color(brand-warning); - } -} diff --git a/common/sass/styles/helpers.scss b/common/sass/styles/helpers.scss deleted file mode 100644 index dadbf9a9e03..00000000000 --- a/common/sass/styles/helpers.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'common/sass/mixins'; - -.mono { - @include mono; -} - -.no-scroll { - height: 100%; - overflow: hidden; -} diff --git a/common/sass/styles/noscript.scss b/common/sass/styles/noscript.scss deleted file mode 100644 index 155e0472334..00000000000 --- a/common/sass/styles/noscript.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import "common/sass/variables"; -@import "common/sass/mixins"; - -.NoScript { - @include cover-message; -} diff --git a/common/sass/styles/overrides/dropdowns.scss b/common/sass/styles/overrides/dropdowns.scss deleted file mode 100644 index 0fe5bd6cf17..00000000000 --- a/common/sass/styles/overrides/dropdowns.scss +++ /dev/null @@ -1,47 +0,0 @@ -// Button overrides -@import 'common/sass/variables'; - -.dropdown { - .caret { - margin-left: $space-sm; - } -} - -// If it's a span, we probably want it inline-block, not inline -span.dropdown { - display: inline-block; -} - -.dropdown-menu { - padding: 0; - border: none; - background: color(control-bg); - box-shadow: $dropdown-shadow; - - > li { - margin: 0; - - &:first-child { - padding-top: 4px; - } - - &:last-child { - padding-bottom: 4px; - } - - > a { - color: color(control-color); - font-weight: 300; - transition: none; - - &.active { - color: color(link-color); - } - - &:hover { - background-color: color(brand-primary); - color: color(text-color-inverted); - } - } - } -} diff --git a/common/sass/styles/overrides/forms.scss b/common/sass/styles/overrides/forms.scss deleted file mode 100644 index 23b2a4e962f..00000000000 --- a/common/sass/styles/overrides/forms.scss +++ /dev/null @@ -1,49 +0,0 @@ -// Form overrides -@import 'common/sass/variables'; -@import 'common/sass/mixins'; - -label { - margin-bottom: $space-xs; - - &.is-required:after { - content: '*'; - padding-left: 2px; - color: color(brand-warning); - } -} - -input[type='radio'], -input[type='checkbox'] { - margin: 3px 0 0; - line-height: normal; -} - -input[readonly] { - background-color: color(control-disabled-bg); - cursor: text !important; -} - -.form-group { - display: block; - margin-bottom: $form-group-margin-bottom; -} - -.radio, -.checkbox { - margin: 15px 0; - font-weight: 500; -} - -.help-block { - &.is-valid { - color: color(brand-success); - } - - &.is-invalid { - color: color(brand-danger); - } - - &.is-semivalid { - color: color(brand-warning); - } -} diff --git a/common/sass/styles/overrides/grid.scss b/common/sass/styles/overrides/grid.scss deleted file mode 100644 index 15ad0fa67ea..00000000000 --- a/common/sass/styles/overrides/grid.scss +++ /dev/null @@ -1,12 +0,0 @@ -// Bootstrap grid overrides -@import "common/sass/variables"; - -.container { - width: 100%; - max-width: $cont-width; - padding: 0 $grid-gutter-width * 2.5; - - @media screen and (max-width: $screen-lg) { - padding: 0 3%; - } -} diff --git a/common/sass/styles/overrides/react-select.scss b/common/sass/styles/overrides/react-select.scss deleted file mode 100644 index 949b6538729..00000000000 --- a/common/sass/styles/overrides/react-select.scss +++ /dev/null @@ -1,124 +0,0 @@ -@import 'common/sass/variables'; - -.Select { - border-radius: 2px; - background-color: color(control-bg); - border: 1px solid color(control-border); - border-radius: 2px; - font-weight: 400; - font-size: 1rem; - box-shadow: inset 0 1px 0 0 color(control-inset-shadow); - transition: border-color 120ms, box-shadow 120ms; - height: $input-height-base; - - &-control { - min-width: 7.5rem; - height: inherit; - border: none !important; - box-shadow: none !important; - background-color: transparent !important; - border: none !important; - &:hover { - box-shadow: none; - } - } - &-placeholder { - line-height: $line-height-base; - color: color(control-placeholder); - } - &-input { - position: absolute; - > input { - line-height: $line-height-base; - padding: 0; - color: color(control-color); - } - } - &-placeholder, - &-input { - padding-left: 0; - padding-right: 0; - padding: 0rem 1rem; - top: 50%; - transform: translateY(-50%); - display: block; - height: auto; - } - &-option { - padding: 0.75rem 1rem; - background: color(control-bg); - color: shade-dark(0.8); - - &.is-selected { - background: shade-dark(0.03); - color: color(text-color); - } - - &.is-focused { - background: color(brand-primary); - color: color(text-color-inverted); - } - } - &-menu-outer { - z-index: 3; - width: calc(100% + 2px); - left: -1px; - background-color: color(control-bg); - border: 2px solid color(brand-primary); - border-top: none; - border-radius: $input-border-radius; - border-top-left-radius: 0; - border-top-right-radius: 0; - border-color: inherit; - } - &-menu { - max-height: 10.0625rem; - } - // Selects should never have invalid input - // &.invalid { - // border-color: color(brand-danger); - // box-shadow: inset 0px 0px 0px 1px color(brand-danger); - // } - &.is-focused { - border-color: color(brand-primary); - box-shadow: inset 0px 0px 0px 1px color(brand-primary); - &.valid { - border-color: color(brand-success); - box-shadow: inset 0px 0px 0px 1px color(brand-success); - } - } - &-value { - line-height: $line-height-base !important; - padding-left: 0 !important; - padding: 0.75rem 1rem !important; - height: inherit !important; - - &-label { - position: relative; - top: 50%; - transform: translateY(-50%); - display: block; - } - } - &-multi-value-wrapper { - display: block; - position: relative; - height: inherit; - } - - // This shitty selector is the text color of the select, no joke. - &.has-value.Select--single > .Select-control .Select-value, - &.has-value.is-pseudo-focused.Select--single > .Select-control .Select-value { - .Select-value-label { - color: color(control-color); - } - } - - // Identicons need to fit into the select - .Identicon { - display: inline-block; - width: 22px !important; - height: 22px !important; - top: -1px; - } -} diff --git a/common/sass/variables/colors.scss b/common/sass/variables/colors.scss deleted file mode 100644 index ca61d4861fc..00000000000 --- a/common/sass/variables/colors.scss +++ /dev/null @@ -1,191 +0,0 @@ -$brand-primary: #007896; -$brand-info: #163151; -$brand-success: #5dba5a; -$brand-warning: #ff9800; -$brand-danger: #db5846; - -$greyish-brown: #424242; -$dark-slate-blue: #163150; -$bright-sky-blue: #1eb8e7; -$silver: #f7f7f7; - -$dark-rgb: 0, 0, 0; -$light-rgb: 255, 255, 255; - -$gray-base: #000; -$gray-darker: #1c1c1c; -$gray-dark: #333; -$gray: #737373; -$gray-light: #9a9a9a; -$gray-lighter: #ececec; -$gray-lightest: #fafafa; - -$theme-light: ( - dark-rgb: $dark-rgb, - light-rgb: $light-rgb, - gray-base: $gray-base, - gray-darker: $gray-darker, - gray-dark: $gray-dark, - gray: $gray, - gray-light: $gray-light, - gray-lighter: $gray-lighter, - gray-lightest: $gray-lightest, - brand-primary: $brand-primary, - brand-primary-dark: darken($brand-primary, 6%), - brand-primary-darker: darken($brand-primary, 12%), - brand-primary-light: lighten($brand-primary, 6%), - brand-primary-lighter: lighten($brand-primary, 12%), - brand-success: $brand-success, - brand-success-dark: darken($brand-success, 6%), - brand-success-darker: darken($brand-success, 12%), - brand-success-light: lighten($brand-success, 6%), - brand-success-lighter: lighten($brand-success, 12%), - brand-info: $brand-info, - brand-info-dark: darken($brand-info, 6%), - brand-info-darker: darken($brand-info, 12%), - brand-info-light: lighten($brand-info, 6%), - brand-info-lighter: lighten($brand-info, 12%), - brand-warning: $brand-warning, - brand-warning-dark: darken($brand-warning, 6%), - brand-warning-darker: darken($brand-warning, 12%), - brand-warning-light: lighten($brand-warning, 6%), - brand-warning-lighter: lighten($brand-warning, 12%), - brand-danger: $brand-danger, - brand-danger-dark: darken($brand-danger, 6%), - brand-danger-darker: darken($brand-danger, 12%), - brand-danger-light: lighten($brand-danger, 6%), - brand-danger-lighter: lighten($brand-danger, 12%), - white-clear: rgba(#fff, 0.8), - white-clearer: rgba(#fff, 0.7), - white-clearest: rgba(#fff, 0.6), - body-bg: $gray-lightest, - pane-bg: #fff, - footer-bg: $brand-info, - footer-border: $brand-primary, - text-color: $gray-dark, - text-color-inverted: #fff, - link-color: $brand-primary, - link-hover-color: darken($brand-primary, 10%), - tab-color: $brand-info, - tab-border: #ececec, - tab-active-color: $brand-primary, - tab-active-border: $brand-primary, - control-bg: #fff, - control-color: $gray-dark, - control-border: #e5ecf3, - control-disabled-bg: $gray-lightest, - control-disabled-color: $gray-dark, - control-disabled-border: #e6e6e6, - control-placeholder: rgba($gray-dark, 0.3), - control-inset-shadow: rgba($gray-base, 0.03), - control-addon-bg: $gray-lighter, - loader-light: #fff, - loader-dark: $brand-info, - code-bg: #eff5fe, - code-border: #e5ecf3, - tooltip-bg: $gray-darker, - tooltip-color: #fff, - component-active-color: #fff, - component-active-bg: $brand-primary -); - -$gray-base: #b8bfd0; -$gray-darker: #8e97a9; -$gray-dark: #4d5463; -$gray: #393f4c; -$gray-light: #333842; -$gray-lighter: #282c34; -$gray-lightest: #21252b; - -$theme-dark: ( - dark-rgb: $light-rgb, - light-rgb: $dark-rgb, - gray-base: $gray-base, - gray-darker: $gray-darker, - gray-dark: $gray-dark, - gray: $gray, - gray-light: $gray-light, - gray-lighter: $gray-lighter, - gray-lightest: $gray-lightest, - brand-primary: lighten($brand-primary, 6%), - brand-primary-dark: $brand-primary, - brand-primary-darker: darken($brand-primary, 6%), - brand-primary-light: lighten($brand-primary, 12%), - brand-primary-lighter: lighten($brand-primary, 16%), - brand-success: lighten($brand-success, 6%), - brand-success-dark: $brand-success, - brand-success-darker: darken($brand-success, 6%), - brand-success-light: lighten($brand-success, 12%), - brand-success-lighter: lighten($brand-success, 16%), - brand-info: lighten($brand-info, 6%), - brand-info-dark: $brand-info, - brand-info-darker: darken($brand-info, 6%), - brand-info-light: lighten($brand-info, 12%), - brand-info-lighter: lighten($brand-info, 16%), - brand-warning: lighten($brand-warning, 6%), - brand-warning-dark: $brand-warning, - brand-warning-darker: darken($brand-warning, 6%), - brand-warning-light: lighten($brand-warning, 12%), - brand-warning-lighter: lighten($brand-warning, 16%), - brand-danger: saturate(lighten($brand-danger, 6%), 7%), - brand-danger-dark: saturate($brand-danger, 7%), - brand-danger-darker: saturate(darken($brand-danger, 6%), 7%), - brand-danger-light: saturate(lighten($brand-danger, 12%), 7%), - brand-danger-lighter: saturate(lighten($brand-danger, 16%), 7%), - white-clear: rgba($gray-lightest, 0.9), - white-clearer: rgba($gray-lightest, 0.8), - white-clearest: rgba($gray-lightest, 0.7), - body-bg: $gray-lightest, - pane-bg: $gray-lighter, - footer-bg: $gray-light, - footer-border: $gray-light, - text-color: #fff, - text-color-inverted: #fff, - link-color: lighten($brand-primary, 10%), - link-hover-color: lighten($brand-primary, 20%), - tab-color: $gray-base, - tab-border: $gray-base, - tab-active-color: #fff, - tab-active-border: #fff, - control-bg: $gray, - control-color: #fff, - control-border: $gray-dark, - control-disabled-bg: rgba($gray-light, 0.5), - control-disabled-color: #aaa, - control-disabled-border: rgba($gray-light, 0.5), - control-placeholder: rgba(#fff, 0.3), - control-inset-shadow: rgba(#fff, 0.03), - control-addon-bg: $gray-dark, - loader-light: #fff, - loader-dark: #fff, - code-bg: $gray, - code-border: $gray-dark, - tooltip-bg: #1c1c1c, - tooltip-color: #fff, - component-active-color: $gray-light, - component-active-bg: $brand-primary -); - -$themes: ( - ('light', $theme-light), - ('dark', $theme-dark) -); - -$color-var-prefix: '--color'; -@function color($name) { - // First make sure it's a legit color, and error if not - @if (map-get($theme-light, $name) == null) { - @error 'Color `#{$name}` is not a valid color'; - @return false; - } - - @return var(#{$color-var-prefix}-#{$name}); -} - -@function shade-dark($opacity) { - @return rgba(var(#{$color-var-prefix}-dark-rgb), $opacity); -} - -@function shade-light($opacity) { - @return rgba(var(#{$color-var-prefix}-light-rgb), $opacity); -} diff --git a/common/sass/variables/electron.scss b/common/sass/variables/electron.scss deleted file mode 100644 index 41bce6efd0d..00000000000 --- a/common/sass/variables/electron.scss +++ /dev/null @@ -1,2 +0,0 @@ -$electron-sidebar-width: 170px; -$electron-osx-control-spacing: 20px; diff --git a/common/sass/variables/grid.scss b/common/sass/variables/grid.scss deleted file mode 100644 index 3ef2f965f03..00000000000 --- a/common/sass/variables/grid.scss +++ /dev/null @@ -1,40 +0,0 @@ -// In electron, we have a sidebar besides the content, so the media queries -// need to shrink to reflect that -$screen-xs: 512px; -$screen-sm: 820px; -$screen-md: 1060px; -$screen-lg: 1280px; -$screen-xl: 1440px; -$screen-xxl: 1600px; -@if ($is-electron) { - $screen-xs: $screen-xs + $electron-sidebar-width; - $screen-sm: $screen-sm + $electron-sidebar-width; - $screen-md: $screen-md + $electron-sidebar-width; - $screen-lg: $screen-lg + $electron-sidebar-width; - $screen-xl: $screen-xl + $electron-sidebar-width; -} - -$screen-xs-min: $screen-xs; -$screen-sm-min: $screen-sm; -$screen-md-min: $screen-md; -$screen-lg-min: $screen-lg; -$screen-xl-min: $screen-xl; -$screen-xs-max: ($screen-sm-min - 1); -$screen-sm-max: ($screen-md-min - 1); -$screen-md-max: ($screen-lg-min - 1); -$screen-lg-max: ($screen-xl-min - 1); - -$grid-columns: 12; -$grid-gutter-width: 2rem; -$grid-float-breakpoint: $screen-sm-min; -$grid-float-breakpoint-max: ($grid-float-breakpoint - 1); - -$cont-padding: 1%; -$cont-padding-lg: 3%; -$cont-width: 113rem; -$container-tablet: (48rem + $grid-gutter-width); -$container-sm: $container-tablet; -$container-desktop: (62rem + $grid-gutter-width); -$container-md: $container-desktop; -$container-large-desktop: (76rem + $grid-gutter-width); -$container-lg: $container-large-desktop; diff --git a/common/sass/variables/tooltips.scss b/common/sass/variables/tooltips.scss deleted file mode 100644 index 34dc9785d62..00000000000 --- a/common/sass/variables/tooltips.scss +++ /dev/null @@ -1,3 +0,0 @@ -$tooltip-start-distance: 5px; -$tooltip-hover-distance: 9px; -$tooltip-arrow-size: 5px; diff --git a/common/v2/components/Account.tsx b/common/v2/components/Account.tsx deleted file mode 100644 index f124b631c64..00000000000 --- a/common/v2/components/Account.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { ReactNode } from 'react'; - -import styled from 'styled-components'; -import { Avatar, Identicon, Tooltip, scale } from '@mycrypto/ui'; -import Typography from './Typography'; -import EthAddress from './EthAddress'; - -const Flex = styled.div` - align-items: center; - display: flex; -`; - -const Content = styled.div` - padding-left: 1em; -`; - -const Title = styled(Typography)` - display: inline; - font-size: ${scale(0.5)}; -`; - -Title.defaultProps = { as: 'div' }; - -const MissingTitle = styled(Title)` - color: ${props => props.theme.textLight}; - font-style: italic; -`; - -MissingTitle.defaultProps = { children: 'No Label' }; - -const Address = styled(EthAddress)` - font-size: ${scale(0.25)}; -`; - -const SAvatar = styled(Avatar)` - &&& img { - height: 45px; - width: 44px; - } -`; - -const SIdenticon = styled(Identicon)` - &&& img { - height: 45px; - width: 44px; - } -`; - -interface TooltipType { - image?: string; - content: ReactNode | string; -} - -interface Props { - address: string; - className?: string; - title?: string; - isCopyable?: boolean; - tooltip?: TooltipType; - onSubmit?(title?: string): void; - truncate?(text: string): string; -} - -export default function Account({ - title, - address, - isCopyable = true, - truncate, - tooltip, - className -}: Props) { - const TitleComponent = title ? Title : MissingTitle; - const ImageComponent = () => - tooltip && tooltip.image ? : ; - - const renderAddressContent = () => ( - - - - <> - {title} - -
-
-
-
-
- ); - - return tooltip ? ( - {tooltip.content}}> - {renderAddressContent()} - - ) : ( - renderAddressContent() - ); -} diff --git a/common/v2/components/AccountDropdown.tsx b/common/v2/components/AccountDropdown.tsx deleted file mode 100644 index bd2a759ce6d..00000000000 --- a/common/v2/components/AccountDropdown.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useContext } from 'react'; -import { translateRaw } from 'v2/translations'; -import { formatEther } from 'ethers/utils'; - -import { AccountSummary, AccountOption, Dropdown } from 'v2/components'; -import { StoreAccount, Asset } from 'v2/types'; -import { AddressBookContext, getAccountBalance, getBaseAsset } from 'v2/services/Store'; -import { useEffectOnce } from 'v2/vendor'; - -// Option item displayed in Dropdown menu. Props are passed by react-select Select. -// To know: Select needs to receive a class in order to attach refs https://github.com/JedWatson/react-select/issues/2459 -// Since Account summary is using Address which still has the 'copy', we must handle hover ourself. - -interface IAccountDropdownProps { - accounts: StoreAccount[]; - name: string; - value: StoreAccount; - asset?: Asset; - onSelect(option: StoreAccount): void; -} - -function AccountDropdown({ accounts, name, value, onSelect, asset }: IAccountDropdownProps) { - const { getAccountLabel } = useContext(AddressBookContext); - const relevantAccounts: StoreAccount[] = accounts.map(account => ({ - ...account, - label: getAccountLabel(account), - balance: formatEther(asset ? getAccountBalance(account, asset) : getAccountBalance(account)), - assetSymbol: asset ? asset.ticker : getBaseAsset(account)!.ticker - })); - - useEffectOnce(() => { - // preselect first value from options - if ((!value || !value.address) && relevantAccounts.length > 0) { - onSelect(relevantAccounts[0]); - } - }); - - return ( - onSelect(option)} - optionComponent={AccountOption} - value={value && value.address ? value : undefined} // Allow the value to be undefined at the start in order to display the placeholder - valueComponent={({ value: { label, address, balance, assetSymbol } }) => ( - - )} - /> - ); -} - -export default AccountDropdown; diff --git a/common/v2/components/AccountList.tsx b/common/v2/components/AccountList.tsx deleted file mode 100644 index e7429d38818..00000000000 --- a/common/v2/components/AccountList.tsx +++ /dev/null @@ -1,332 +0,0 @@ -import React, { useContext, useState } from 'react'; -import styled, { css } from 'styled-components'; -import { Button, Identicon } from '@mycrypto/ui'; - -import { translateRaw } from 'v2/translations'; -import { ROUTE_PATHS, Fiats, WALLETS_CONFIG } from 'v2/config'; -import { - EthAddress, - CollapsibleTable, - Network, - RowDeleteOverlay, - RouterLink, - EditableText -} from 'v2/components'; -import { truncate } from 'v2/utils'; -import { BREAK_POINTS, COLORS, SPACING, breakpointToNumber } from 'v2/theme'; -import { ExtendedAccount, StoreAccount, ExtendedAddressBook } from 'v2/types'; -import { - AccountContext, - getLabelByAccount, - StoreContext, - SettingsContext, - AddressBookContext -} from 'v2/services/Store'; -import { DashboardPanel } from './DashboardPanel'; -import { RatesContext } from 'v2/services'; -import { default as Currency } from './Currency'; -import { TUuid } from 'v2/types/uuid'; - -const Label = styled.span` - display: flex; - align-items: center; - @media (min-width: ${BREAK_POINTS.SCREEN_SM}) { - font-weight: bold; - } -`; - -const LabelWithWallet = styled.span` - display: flex; - flex-direction: column; - @media (min-width: ${BREAK_POINTS.SCREEN_SM}) { - font-weight: bold; - } -`; - -const WalletTypeLabel = styled.div` - display: inline-block; - text-align: center; - background: ${COLORS.GREY}; - border-radius: 600px; - color: ${COLORS.WHITE}; - font-size: 0.6em; - padding: 3px 6px; -`; - -const SIdenticon = styled(Identicon)` - img { - height: 2em; - } - margin-right: 0.8em; - @media (min-width: ${BREAK_POINTS.SCREEN_SM}) { - margin-right: 1em; - } -`; - -// On mobile screen the CollapisableTable becomes a Stacked card. -// We provide better styles for desktop screens -const CurrencyContainer = styled(Currency)` - @media (min-width: ${BREAK_POINTS.SCREEN_SM}) { - float: right; - } -`; - -const HeaderAlignment = styled.div` - ${(props: { align?: string }) => css` - @media (min-width: ${BREAK_POINTS.SCREEN_SM}) { - text-align: ${props.align || 'inherit'}; - } - `}; -`; - -interface IFavoriteProps { - favorited: boolean; -} - -const FavoriteButton = styled(Button)` - span { - span { - svg { - path { - fill: ${(props: IFavoriteProps) => (props.favorited ? COLORS.GOLD : COLORS.WHITE)}; - stroke: ${(props: IFavoriteProps) => (props.favorited ? COLORS.GOLD : COLORS.GREY)}; - } - } - } - } - align-self: flex-start; - margin-left: 1em; -`; - -const DeleteButton = styled(Button)` - align-self: flex-end; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.7em; - width: 100%; -`; - -const TableContainer = styled.div` - display: block; - overflow: auto; - flex: 1; -`; - -const AccountListFooterWrapper = styled.div` - & * { - color: ${COLORS.BLUE_BRIGHT}; - } - & img { - height: 1.1em; - margin-right: 0.5em; - } -`; - -const AddAccountButton = styled(Button)` - color: ${COLORS.BLUE_BRIGHT}; - padding: ${SPACING.BASE}; - opacity: 1; - &:hover { - transition: 200ms ease all; - transform: scale(1.02); - opacity: 0.7; - } -`; - -interface AccountListProps { - accounts: StoreAccount[]; - className?: string; - currentsOnly?: boolean; - deletable?: boolean; - favoritable?: boolean; - copyable?: boolean; - dashboard?: boolean; -} - -export const screenIsMobileSized = (breakpoint: number): boolean => - window.matchMedia(`(max-width: ${breakpoint}px)`).matches; - -export default function AccountList(props: AccountListProps) { - const { - accounts: displayAccounts, - className, - deletable, - favoritable, - copyable, - dashboard - } = props; - const { deleteAccountFromCache } = useContext(StoreContext); - const { updateAccount } = useContext(AccountContext); - const [deletingIndex, setDeletingIndex] = useState(); - const overlayRows = [deletingIndex]; - - // Verify if AccountList is used in Dashboard to display Settings button - const headingRight = dashboard ? translateRaw('SETTINGS_HEADING') : undefined; - const actionLink = dashboard ? ROUTE_PATHS.SETTINGS.path : undefined; - - const Footer = () => { - return ( - - - {`+ ${translateRaw( - 'ACCOUNT_LIST_TABLE_ADD_ACCOUNT' - )}`} - - - ); - }; - - return ( - } - > - - - - - ); -} - -function buildAccountTable( - accounts: StoreAccount[], - deleteAccount: (a: ExtendedAccount) => void, - updateAccount: (u: TUuid, a: ExtendedAccount) => void, - deletable?: boolean, - favoritable?: boolean, - copyable?: boolean, - overlayRows?: number[], - setDeletingIndex?: any -) { - const { totalFiat } = useContext(StoreContext); - const { getAssetRate } = useContext(RatesContext); - const { settings } = useContext(SettingsContext); - const { addressBook, updateAddressBooks, createAddressBooks } = useContext(AddressBookContext); - const columns = [ - translateRaw('ACCOUNT_LIST_LABEL'), - translateRaw('ACCOUNT_LIST_ADDRESS'), - translateRaw('ACCOUNT_LIST_NETWORK'), - - {translateRaw('ACCOUNT_LIST_VALUE')} - , - - {translateRaw('ACCOUNT_LIST_DELETE')} - - ]; - - return { - head: deletable ? columns : columns.slice(0, columns.length - 1), - overlay: - overlayRows && overlayRows[0] !== undefined ? ( - { - deleteAccount(accounts[overlayRows[0]]); - setDeletingIndex(undefined); - }} - cancelAction={() => setDeletingIndex(undefined)} - /> - ) : ( - <> - ), - overlayRows, - body: accounts.map((account, index) => { - const addressCard: ExtendedAddressBook | undefined = getLabelByAccount(account, addressBook); - const total = totalFiat([account])(getAssetRate); - const label = addressCard ? addressCard.label : 'Unknown Account'; - const bodyContent = [ - , - , - - {account.networkId} - , - - ]; - return deletable - ? [ - ...bodyContent, - setDeletingIndex(index)} icon="exit" /> - ] - : favoritable - ? [ - - updateAccount(account.uuid, { - ...account, - favorite: !account.favorite - }) - } - />, - ...bodyContent - ] - : bodyContent; - }), - config: { - primaryColumn: translateRaw('ACCOUNT_LIST_LABEL'), - sortableColumn: translateRaw('ACCOUNT_LIST_LABEL'), - sortFunction: (a: any, b: any) => { - const aLabel = a.props.label; - const bLabel = b.props.label; - return aLabel === bLabel ? true : aLabel.localeCompare(bLabel); - } - } - }; -} diff --git a/common/v2/components/AccountOption.tsx b/common/v2/components/AccountOption.tsx deleted file mode 100644 index ab4bdf7590d..00000000000 --- a/common/v2/components/AccountOption.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { OptionComponentProps } from 'react-select'; - -import { AccountSummary, Divider } from 'v2/components'; - -class AccountOption extends React.PureComponent { - public render() { - const { option, onSelect } = this.props; - return ( - <> - onSelect!(option, null)} // Since it's a custom Dropdown we know onSelect is defined - /> - - - ); - } -} - -/* ToDo: React Select doesn't seem to like these memoized components as optionComponents, figure out a solution to this. */ -/*const MemoizedAccountOption = React.memo(AccountOption); -export default MemoizedAccountOption;*/ - -export default AccountOption; diff --git a/common/v2/components/AccountSummary.tsx b/common/v2/components/AccountSummary.tsx deleted file mode 100644 index 5c45179cece..00000000000 --- a/common/v2/components/AccountSummary.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -import { truncate } from 'v2/utils'; -import { TSymbol } from 'v2/types'; -import Currency from './Currency'; -import Account from './Account'; - -interface Props { - address: string; - balance: string; - assetSymbol?: string; - label?: string; - selectable?: boolean; - onClick?(): void; -} - -interface SProps { - selectable: boolean; -} - -const SCurrency = styled(Currency)` - margin-left: 58px; -`; -// To change the hover behavior on Address, we need it to be a SC. -// https://www.styled-components.com/docs/advanced#caveat -const SAddress = styled(Account)``; - -const SAccountWrapper = styled('div')` - display: flex; - padding: 16px 15px 16px 19px; - flex-direction: column; - & > div { - line-height: 1.2; - color: var(--greyish-brown); - } - & > ${SAddress} { - pointer-events: none; - } - &:hover { - background-color: ${(p: SProps) => (p.selectable ? 'var(--color-gray-lighter)' : 'inherit')}; - } -`; - -// Display an address with it's balance -function AccountSummary({ - address, - balance, - assetSymbol, - label, - selectable = true, - onClick -}: Props) { - return ( - - - - - ); -} - -export default AccountSummary; diff --git a/common/v2/components/AddressField.tsx b/common/v2/components/AddressField.tsx deleted file mode 100644 index 8b1f84c163d..00000000000 --- a/common/v2/components/AddressField.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React from 'react'; -import { Identicon } from '@mycrypto/ui'; -import { FieldProps, Field } from 'formik'; -import styled from 'styled-components'; - -import { DomainStatus } from 'v2/components'; -import { Network, InlineMessageType } from 'v2/types'; -import { monospace } from 'v2/theme'; -import { ResolutionError } from '@unstoppabledomains/resolution'; -import InputField from './InputField'; -/* - Eth address field to be used within a Formik Form - - the 'fieldname' must exist wihtin the Formik default fields - - validation of the field is handled here. -*/ - -interface ErrorObject { - type: InlineMessageType; - message: string | JSX.Element; -} - -interface Props { - error?: string | ErrorObject; - className?: string; - fieldName: string; - placeholder?: string; - network?: Network; - isLoading: boolean; - isError: boolean; - resolutionError?: ResolutionError; - handleDomainResolve?(name: string): Promise; - onBlur?(ev: React.FocusEvent): void; - onChange?(event: any): void; -} - -const Wrapper = styled.div` - width: 100%; -`; - -const InputWrapper = styled.div` - display: flex; - - > div:nth-child(2) { - flex: 1; - } -`; - -const IdenticonWrapper = styled.div` - width: 45px; - margin-right: 0.5em; -`; - -const EmptyIdenticon = styled.span` - display: block; - width: 45px; - height: 45px; - background-color: #f7f7f7; - border-radius: 50%; -`; - -interface InputProps { - value: string; -} - -const SInput = styled(InputField)` - ${props => props.value && `font-family: ${monospace};`} - font-size: 1rem !important; // to override Typography from mycrypto/ui -`; - -function ETHAddressField({ - className, - fieldName, - error, - network, - placeholder = 'ETH Address or blockchain domain', - isLoading, - isError, - resolutionError, - handleDomainResolve, - onBlur, - onChange -}: Props) { - const errorMessage = typeof error === 'object' ? error.message : error; - const errorType = typeof error === 'object' ? error.type : undefined; - - // By destructuring 'field' in the rendered component we are mapping - // the Inputs 'value' and 'onChange' props to Formiks handlers. - return ( - <> - ( - - - - {field.value.value ? : } - - { - form.setFieldValue(fieldName, { - display: e.currentTarget.value, - value: e.currentTarget.value - }); - if (onChange) { - onChange(e); - } - }} - onBlur={async e => { - if (!network || !network.chainId) { - return; - } - const action = handleDomainResolve - ? (domainName: string) => handleDomainResolve(domainName) - : (address: string) => - form.setFieldValue(fieldName, { display: address, value: address }); - await action(e.currentTarget.value); - form.setFieldTouched(fieldName); - if (onBlur) { - onBlur(e); - } - }} - /> - - - - )} - /> - - ); -} - -export default ETHAddressField; diff --git a/common/v2/components/Amount.tsx b/common/v2/components/Amount.tsx deleted file mode 100644 index ef35a6b8fb1..00000000000 --- a/common/v2/components/Amount.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import styled, { css } from 'styled-components'; - -import { COLORS, BREAK_POINTS } from 'v2/theme'; -import { default as Typography } from './Typography'; - -const SAmount = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-end; - font-size: 16px; - @media(min-width: ${BREAK_POINTS.SCREEN_XS} { - font-size: 18px; - }) -`; - -const Asset = styled(Typography)<{ discrete?: boolean }>` - ${props => - props.discrete - ? css` - color: ${COLORS.BLUE_GREY}; - font-size: 0.9em; - @media(min-width: ${BREAK_POINTS.SCREEN_XS} { - font-size: 1em; - }) - ` - : ``}; -`; - -const Fiat = styled(Typography)` - font-size: 0.9em; - color: ${COLORS.BLUE_GREY}; - @media (min-width: ${BREAK_POINTS.SCREEN_XS}) { - font-size: 1em; - } -`; - -interface Props { - assetValue: string; - fiatValue?: string; - baseAssetValue?: string; - bold?: boolean; -} - -// @TODO: -// - use Currency component for Fiat -// - accept BN instead of string for asset and fiat and define default decimals -export default function Amount({ assetValue, fiatValue, baseAssetValue, bold = false }: Props) { - return ( - - - {assetValue} - - {baseAssetValue && ( - - {baseAssetValue} - - )} - {fiatValue && {fiatValue}} - - ); -} diff --git a/common/v2/components/AmountInput.tsx b/common/v2/components/AmountInput.tsx deleted file mode 100644 index ce19059c544..00000000000 --- a/common/v2/components/AmountInput.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { Input } from '@mycrypto/ui'; - -import { Asset } from 'v2/types'; -import { TSymbol } from 'v2/types/symbols'; - -import AssetIcon from './AssetIcon'; -import Typography from './Typography'; -interface Props { - asset: Asset; - value: string; - placeholder?: string; - onChange(event: any): void; - onBlur?(event: any): void; -} - -const SInput = styled(Input)` - font-size: 1rem !important; // to override Typography from mycrypto/ui -`; - -const SAssetIcon = styled(AssetIcon)` - margin-right: 16px; -`; - -function AmountInput({ asset, value, onChange, onBlur, placeholder, ...props }: Props) { - return ( - ( -
- {asset.ticker && } - {asset.ticker} -
- )} - /> - ); -} - -export default AmountInput; diff --git a/common/v2/components/AssetDropdown.tsx b/common/v2/components/AssetDropdown.tsx deleted file mode 100644 index 1ff33847b1a..00000000000 --- a/common/v2/components/AssetDropdown.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; -import { OptionComponentProps } from 'react-select'; -import styled from 'styled-components'; -import * as R from 'ramda'; - -import { translateRaw } from 'v2/translations'; -import { Asset, TSymbol } from 'v2/types'; -import { AssetDropdownItem, Divider, Dropdown } from 'v2/components'; -import { useEffectOnce } from 'v2/vendor'; - -const Label = styled.div` - font-size: 18px; - width: 100%; - line-height: 1; - text-align: left; - font-weight: normal; - margin-bottom: 9px; - color: ${props => props.theme.text}; -`; - -// Fixes weird placement issues for react-select -const DropdownContainer = styled('div')` - width: ${(props: { fluid: boolean }) => (props.fluid ? '100%' : 'default')}; - - .has-value > .Select-control > .Select-multi-value-wrapper > .Select-input:only-child { - transform: translateY(0%); - padding: 16px 15px 16px 15px; - position: inherit; - } -`; -// Class component to avoid 'Function components cannot be given refs' error -class AssetOption extends React.PureComponent { - public render() { - const { option, onSelect } = this.props; - const { ticker, symbol, name } = option; - const ref = ticker ? ticker : symbol; - return ( - <> - onSelect && onSelect(option, null)} - selectable={true} - /> - - - ); - } -} -// Class component to avoid 'Function components cannot be given refs' error -// tslint:disable:max-classes-per-file -class AssetOptionShort extends React.PureComponent { - public render() { - const { option, onSelect } = this.props; - const { ticker, symbol } = option; - const ref = ticker ? ticker : symbol; - return ( - <> - onSelect && onSelect(option, null)} - selectable={true} - /> - - - ); - } -} - -function AssetDropdown({ - assets, - selectedAsset, - onSelect, - showOnlyTicker = false, - searchable = false, - disabled = false, - fluid = false, - label -}: Props) { - useEffectOnce(() => { - // Preselect first value when not provided - if (R.isEmpty(selectedAsset) && onSelect && !R.isEmpty(assets)) { - onSelect(assets[0]); - } - }); - - return ( - - {label && } - onSelect && onSelect(option)} - optionComponent={showOnlyTicker ? AssetOptionShort : AssetOption} - value={!R.isEmpty(selectedAsset) && selectedAsset} - valueComponent={({ value: option }) => { - const { ticker, symbol, name } = option; - const ref = ticker ? ticker : symbol; - return ; - }} - /> - - ); -} - -export interface Props { - assets: T[]; - selectedAsset?: T; - showOnlyTicker?: boolean; - disabled?: boolean; - fluid?: boolean; - searchable?: boolean; - label?: string; - onSelect?(option: T): void; -} - -export default AssetDropdown; diff --git a/common/v2/components/AssetDropdownItem.tsx b/common/v2/components/AssetDropdownItem.tsx deleted file mode 100644 index 2ffea13a14d..00000000000 --- a/common/v2/components/AssetDropdownItem.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -import { TSymbol } from 'v2/types'; -import { AssetIcon, Typography } from 'v2/components'; - -const SContainer = styled('div')` - display: flex; - flex-direction: row; - align-items: center; - padding: 14px 15px 14px 15px; - - &:hover { - background-color: ${(p: { selectable: boolean }) => - p.selectable ? 'var(--color-gray-lighter)' : 'inherit'}; - } -`; - -function AssetDropdownItem({ symbol, name, onClick, selectable = false }: Props) { - return ( - - - - {name &&   -  } - - - ); -} - -export interface Props { - symbol: TSymbol; - name?: string; - selectable?: boolean; - onClick?(): void; -} - -export default AssetDropdownItem; diff --git a/common/v2/components/AssetIcon.tsx b/common/v2/components/AssetIcon.tsx deleted file mode 100644 index 2f3d56c74f2..00000000000 --- a/common/v2/components/AssetIcon.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import manifest from 'cryptocurrency-icons/manifest.json'; - -import { IAsset, TSymbol } from 'v2/types'; -// Relies on https://github.com/atomiclabs/cryptocurrency-icons using fixed version number through CDN -// @TODO: We should be using our own sprite served over a trusted CDN -const baseURL = 'https://cdn.mycryptoapi.com/v1/icons'; - -function buildUrl(symbol: TSymbol) { - return `${baseURL}/${symbol.toLowerCase()}.svg`; -} - -function getIconUrl(symbol: TSymbol) { - const curr = manifest.find((c: IAsset) => c.symbol === symbol); - return curr ? buildUrl(symbol) : buildUrl('generic' as TSymbol); -} - -const SImg = styled('img')` - height: ${(p: { size: string }) => p.size}; - width: ${(p: { size: string }) => p.size}; -`; - -interface Props { - symbol: TSymbol; - size?: string; - className?: string; -} - -function AssetIcon({ symbol, size = '32px', className }: Props) { - const iconUrl = getIconUrl(symbol); - - return ; -} - -export default AssetIcon; diff --git a/common/v2/components/Banner.tsx b/common/v2/components/Banner.tsx deleted file mode 100644 index 3c67553ad4b..00000000000 --- a/common/v2/components/Banner.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -import { BannerType } from 'v2/types'; -import { BREAK_POINTS, COLORS } from 'v2/theme'; -import { default as Typography } from './Typography'; -import announcementSVG from 'assets/images/icn-announcement.svg'; -import errorSVG from 'assets/images/icn-toast-error.svg'; - -interface Props { - value: string | React.ReactElement; - type: BannerType; -} - -interface Config { - bgColor: string; - color: string; - icon: string; -} - -const Container = styled.div` - background-color: ${(p: { config: Config }) => p.config.bgColor || 'transparent'}; - color: ${p => p.config.color || 'inherit'}; - padding: 15px 10px; - padding-right: 30px; - display: flex; - align-items: flex-start; - justify-content: center; - & > img { - vertical-align: top; - margin-right: 15px; - margin-top: 5px; - } - - // In mobile we let the icon stay top-aligned with the text - // as screen grows we remove the arbitray value used to align the icon. - @media (min-width: ${BREAK_POINTS.SCREEN_SM}) { - align-items: center; - & > img { - margin-top: 0; - } - } -`; - -const Icon = styled.img` - max-width: 24px; -`; - -interface BannerTypographyProps { - color: string; -} - -const STypography = styled(Typography)` - color: ${props => props.color}; - - a { - color: ${props => props.color}; - text-decoration: underline; - font-weight: normal; - } - - a:hover { - color: ${props => props.color}; - font-weight: bold; - } -`; - -const bannerConfig = (type: BannerType): Config => { - switch (type) { - default: - case BannerType.ANNOUNCEMENT: - return { - color: COLORS.WHITE, - bgColor: COLORS.BLUE_BRIGHT, - icon: announcementSVG - }; - case BannerType.ERROR: - return { - color: COLORS.WHITE, - bgColor: COLORS.ERROR_RED, - icon: errorSVG - }; - } -}; - -export const Banner = ({ value, type, ...props }: Props) => { - const config = bannerConfig(type); - return ( - - - - - ); -}; diff --git a/common/v2/components/BannerAd/BannerAd.tsx b/common/v2/components/BannerAd/BannerAd.tsx deleted file mode 100644 index ab9156f32ee..00000000000 --- a/common/v2/components/BannerAd/BannerAd.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import styled from 'styled-components'; - -import { ads } from './constants'; -import { BREAK_POINTS } from 'v2/theme'; -import { ANALYTICS_CATEGORIES, AnalyticsService } from 'v2/services'; - -const { SCREEN_SM } = BREAK_POINTS; - -const AdWrapper = styled.div` - margin-bottom: 14px; - cursor: pointer; -`; - -const BannerImageDesktop = styled.img` - width: 100%; - height: auto; - - @media (max-width: ${SCREEN_SM}) { - display: none; - } -`; - -const BannerImageMobile = styled.img` - width: 100%; - height: auto; - - @media (min-width: ${SCREEN_SM}) { - display: none; - } -`; - -const onAdClick = (ad: string) => { - AnalyticsService.instance.track(ANALYTICS_CATEGORIES.AD, `${ad} ad clicked`); -}; - -export default function BannerAd() { - const randomIndex = Math.floor(Math.random() * ads.length); - const ad = ads[randomIndex]; - const isExternalLink = ad.url.startsWith('http'); - - return ( - onAdClick(ad.name)}> - {isExternalLink ? ( - - - - - ) : ( - - - - - )} - - ); -} diff --git a/common/v2/components/BannerAd/constants.ts b/common/v2/components/BannerAd/constants.ts deleted file mode 100644 index ed1aa22c30b..00000000000 --- a/common/v2/components/BannerAd/constants.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ROUTE_PATHS, EXT_URLS } from 'v2/config'; - -import simplexDesktop from 'assets/images/banners/Simplex_Desktop.png'; -import simplexMobile from 'assets/images/banners/Simplex_Mobile.png'; -import ledgerDesktop from 'assets/images/banners/Ledger_Desktop.png'; -import ledgerMobile from 'assets/images/banners/Ledger_Mobile.png'; -import trezorDesktop from 'assets/images/banners/Trezor_Desktop.png'; -import trezorMobile from 'assets/images/banners/Trezor_Mobile.png'; - -export const ads = [ - { - name: 'Simplex', - srcDesktop: simplexDesktop, - srcMobile: simplexMobile, - url: ROUTE_PATHS.BUY.path - }, - { - name: 'Trezor', - srcDesktop: trezorDesktop, - srcMobile: trezorMobile, - url: EXT_URLS.TREZOR_REFERRAL.path - }, - { - name: 'Ledger', - srcDesktop: ledgerDesktop, - srcMobile: ledgerMobile, - url: EXT_URLS.LEDGER_REFERRAL.path - } -]; diff --git a/common/v2/components/Button.tsx b/common/v2/components/Button.tsx deleted file mode 100644 index 61f1be5d753..00000000000 --- a/common/v2/components/Button.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import styled, { css } from 'styled-components'; -import { Button } from '@mycrypto/ui'; - -interface ButtonProps { - fullwidth?: boolean; - disabled?: boolean; - color?: string; -} - -const StyledButton = styled(Button)` - ${props => - props.fullwidth === true && - css` - width: 100%; - margin-top: 1rem; - `} - :disabled { - opacity: 0.4; - } - color: ${props => (props.color ? props.color : 'inherit')}; -`; - -export default StyledButton; diff --git a/common/v2/components/Checkbox.tsx b/common/v2/components/Checkbox.tsx deleted file mode 100644 index 7928e0de505..00000000000 --- a/common/v2/components/Checkbox.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { Typography } from '@mycrypto/ui'; - -interface CheckboxProps { - name: string; - label: string; - checked: boolean; - icon?: any; - className?: string; - onChange(): void; -} - -const SContainer = styled('div')` - display: flex; - flex-direction: row; - align-items: center; - height: 31px; - margin-bottom: 15px; - font-size: 1em; -`; - -// Styling Checkbox Starts -// Adapted from: https://appitventures.com/blog/styling-checkbox-css-tips/ -const checkboxSize = '20px'; -const borderRadius = '2px'; -const SLabel = styled('label')` - display: block; - position: relative; - cursor: pointer; - font-size: 18px; - line-height: ${checkboxSize}; - height: ${checkboxSize}; - width: ${checkboxSize}; - clear: both; - margin: 0; - - & > input { - position: absolute; - opacity: 0; - cursor: pointer; - } - - & > input:checked ~ span { - background-color: transparent; - border-radius: ${borderRadius}; - transform: rotate(0deg) scale(1); - opacity: 1; - border: ${props => `1px solid ${props.theme.GAU.COLORS.borderColor}`}; - } - - & > span { - position: absolute; - height: ${checkboxSize}; - width: ${checkboxSize}; - background-color: transparent; - border-radius: ${borderRadius}; - border: ${props => `1px solid ${props.theme.GAU.COLORS.borderColor}`}; - transition: all 0.2s ease-out; - } - - // Create the pseudo content element and place it in the center - // Hide it by setting the scale(0) - // This will be the origin of the transition. - & > span::after { - position: absolute; - content: ''; - left: 50%; - top: 50%; - margin-top: -1px; - margin-left: -1px; - height: 0px; - width: 0px; - border: ${props => `1px solid ${props.theme.GAU.COLORS.brightSkyBlue}`}; - opacity: 1; - transform: rotate(0deg) scale(0); - transition: all 0.2s ease-out; - } - - // When the checkbox is selected we transform to show half the borders - // of a rotated rectangle which provides the swoosh effect. - // 'top' & 'left' are used to position the swoosh with the container. - // 'width' & 'height' serve as the size. - & > input:checked ~ span::after { - opacity: 1; - left: 35%; - top: 15%; - width: 7px; - height: 12px; - border: ${props => `solid ${props.theme.GAU.COLORS.brightSkyBlue}`}; - border-radius: 1px; - border-width: 0 2px 2px 0; - background-color: transparent; - transform: rotate(45deg) scale(1); - } -`; - -const SIconContainer = styled('div')` - display: flex; - align-items: center; - margin-right: 15px; - & img { - width: 30px; - height: 30px; - } -`; - -const SLabelContainer = styled('div')` - padding: 7px 0; -`; - -export default function Checkbox({ - className, - name, - label, - checked, - onChange, - icon -}: CheckboxProps) { - return ( - - - - - -
- {icon && {icon()}} - - {label} - -
-
- ); -} diff --git a/common/v2/components/CollapsibleTable.tsx b/common/v2/components/CollapsibleTable.tsx deleted file mode 100644 index 7446a24a34b..00000000000 --- a/common/v2/components/CollapsibleTable.tsx +++ /dev/null @@ -1,231 +0,0 @@ -/* - Imported and modified from @mycrypto/ui to style headers - In order to style the headers particularyly the Icon caret -*/ - -import React, { Component, DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react'; -import throttle from 'lodash.throttle'; -import styled, { StyledComponentClass } from 'styled-components'; - -import { Theme, scale, Icon, StackedCardData, Typography } from '@mycrypto/ui'; - -import { Table, TableData, TableConfig } from './Table'; -import { StackedCard } from './StackedCard'; - -export enum CollapsibleTableModes { - Mobile, - Desktop -} - -export interface CollapsibleTableConfig extends TableConfig { - primaryColumn: string; - iconColumns?: string[]; -} - -export interface CollapsibleTableData extends TableData { - config: CollapsibleTableConfig; -} - -interface CollapsedGroups { - [title: string]: boolean; -} - -type StyledHTMLElement = StyledComponentClass< - DetailedHTMLProps, HTMLElement>, - Theme ->; - -interface Flippable { - isFlipped?: boolean; -} - -interface Props extends CollapsibleTableData { - breakpoint: number; -} - -interface State { - mode: CollapsibleTableModes; - collapsedGroups: CollapsedGroups; -} - -export const transformRowToCards = ( - row: ReactNode[], - head: (string | JSX.Element)[], - primaryColumnIndex: number, - iconColumns: string[] = [] -): StackedCardData => - row.reduce( - (prev: StackedCardData, next, index) => { - const label = head[index]; - - if (index === primaryColumnIndex) { - prev.heading = next; - } else if (iconColumns.includes(label as string)) { - prev.icons!.push(next); - } else { - prev.entries.push([label, next]); - } - - return prev; - }, - { - heading: '', - entries: [], - icons: [] - } - ); - -export const transformTableToCards = ( - { head, body, groups = [], config }: CollapsibleTableData, - collapsedGroups: CollapsedGroups = {} -): (StackedCardData | string)[] => { - const { primaryColumn, iconColumns } = config; - const primaryColumnIndex = head.indexOf(primaryColumn); - const cards: (StackedCardData | string)[] = body.map(row => - transformRowToCards(row, head, primaryColumnIndex, iconColumns) - ); - - groups.forEach(({ title, entries }) => - cards.push( - title, - ...(collapsedGroups[title] - ? [] - : entries.map(row => transformRowToCards(row, head, primaryColumnIndex, iconColumns))) - ) - ); - - return cards; -}; - -export const screenIsMobileSized = (breakpoint: number): boolean => - window.matchMedia(`(max-width: ${breakpoint}px)`).matches; - -const GroupHeading = styled(Typography)` - display: flex; - align-items: center; - margin: 0; - padding: 0.9375rem; - border-bottom: 0.0625rem solid #dde3ee; - background: ${props => props.theme.tableHeadBackground}; - text-transform: uppercase; - font-size: ${scale(2)}; - cursor: pointer; -` as StyledHTMLElement; - -GroupHeading.defaultProps = { - as: 'section', - role: 'button' -}; - -const GroupHeadingCaret = styled(Icon)` - margin-left: 0.5em; - ${props => - props.isFlipped && - ` - svg { - transform: rotateX(180deg) - } - `}; -`; - -const StackedCardContainer = styled.div` - position: relative; -`; -const StackedCardOverlay = styled.div` - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; -`; - -export class CollapsibleTable extends Component { - public static defaultProps = { - head: [], - body: [], - groups: [], - config: {}, - breakpoint: 450 - }; - - public constructor(props: Props) { - super(props); - - this.state = { - mode: screenIsMobileSized(props.breakpoint) - ? CollapsibleTableModes.Mobile - : CollapsibleTableModes.Desktop, - collapsedGroups: {} - }; - - this.checkWindowSize = throttle(this.checkWindowSize, 200); - } - - public componentDidMount() { - window.addEventListener('resize', this.checkWindowSize); - } - - public componentWillUnmount() { - window.removeEventListener('resize', this.checkWindowSize); - } - - public render() { - const { mode, collapsedGroups } = this.state; - const { overlay, overlayRows } = this.props; - - return mode === CollapsibleTableModes.Mobile ? ( - transformTableToCards(this.props, collapsedGroups).map((cardData, index) => - typeof cardData === 'string' ? ( - // The element being iterated on is a group heading. - - {cardData} - - - ) : ( - // The element being iterated on is table data. - - - {overlay && overlayRows!.includes(index) && ( - {overlay} - )} - - ) - ) - ) : ( - - ); - } - - private readonly checkWindowSize = () => { - const { breakpoint } = this.props; - const { mode } = this.state; - const wasMobile = mode === CollapsibleTableModes.Mobile; - const isMobile = screenIsMobileSized(breakpoint); - - if (wasMobile && !isMobile) { - // Mobile-to-Desktop - this.setState({ - mode: CollapsibleTableModes.Desktop, - collapsedGroups: {} - }); - } - - if (!wasMobile && isMobile) { - // Desktop-to-Mobile - this.setState({ - mode: CollapsibleTableModes.Mobile, - collapsedGroups: {} - }); - } - }; - - private readonly toggleCollapseGroup = (title: string) => - this.setState(prevState => ({ - collapsedGroups: { - ...prevState.collapsedGroups, - [title]: !prevState.collapsedGroups[title] - } - })); -} - -export default CollapsibleTable; diff --git a/common/v2/components/ContentPanel.tsx b/common/v2/components/ContentPanel.tsx deleted file mode 100644 index c34b7023886..00000000000 --- a/common/v2/components/ContentPanel.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from 'react'; -import { Button, Panel, Typography } from '@mycrypto/ui'; -import styled from 'styled-components'; - -import { BREAK_POINTS } from 'v2/theme'; -import Stepper from './Stepper'; -import backArrowIcon from 'common/assets/images/icn-back-arrow.svg'; - -interface ContentPanelProps { - width?: string; - mobileMaxWidth?: string; -} - -const ContentPanelWrapper = styled.div` - width: ${(props: ContentPanelProps) => props.width}; - max-width: ${(props: ContentPanelProps) => props.width}; - @media (max-width: ${BREAK_POINTS.SCREEN_SM}) { - width: ${(props: ContentPanelProps) => props.mobileMaxWidth}; - padding-left: 0px; - margin-bottom: 1em; - } -`; - -const BackButton = styled(Button)` - color: #007a99; - font-weight: bold; - display: flex; - align-items: center; - font-size: 20px; - img { - margin-right: 13px; - } -`; - -const ContentPanelHeading = styled.p` - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 15px; - color: #303030; - font-family: Lato; - font-size: 32px; - font-weight: bold; -`; - -const ContentPanelHeadingIcon = styled.img` - width: 45px; - height: 45px; -`; - -const ContentPanelDescription = styled(Typography)` - margin: 0; - margin-bottom: 15px; -`; - -interface ContentPanelTopProps { - stepperOnly: boolean; -} - -const ContentPanelTop = styled.div` - display: flex; - align-items: center; - justify-content: ${(props: ContentPanelTopProps) => - props.stepperOnly ? 'flex-end' : 'space-between'}; - margin: 0 30px 10px 30px; - padding: 0; -`; - -interface Props { - children: any; - className?: string; - heading?: string; - icon?: string; - description?: string; - stepper?: { - current: number; - total: number; - }; - width?: string; - mobileMaxWidth?: string; - backBtnText?: string; - onBack?(): void | null; -} - -export default function ContentPanel({ - onBack, - backBtnText, - stepper, - heading, - icon, - description, - children, - className = '', - width = '650px', - mobileMaxWidth = '100%', - ...rest -}: Props) { - return ( - - {(onBack || stepper) && ( - - {onBack && ( - - Back arrow{' '} - {backBtnText ? `Back : ${backBtnText}` : `Back`} - - )} - {stepper && } - - )} - - {heading && ( - - {heading} - {icon && } - - )} - {description && {description}} - {children} - - - ); -} diff --git a/common/v2/components/CopyableCodeBlock.tsx b/common/v2/components/CopyableCodeBlock.tsx deleted file mode 100644 index f22a5caa91d..00000000000 --- a/common/v2/components/CopyableCodeBlock.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { Copyable } from '@mycrypto/ui'; - -interface Props { - children?: React.ReactNode; -} - -const truncate = (_: string) => { - return ''; -}; - -const CopyableCodeBlockWrapper = styled('div')` - font-weight: 400; - font-size: 1rem; - margin: 0; - margin-bottom: 1rem; - font-size: 1rem; - box-shadow: inset 0 1px 0 0 rgba(63, 63, 68, 0.05); - border: 1px solid #e5ecf3; - display: flex; -`; - -const CodeDisplay = styled('pre')` - font-weight: 400; - font-size: 1rem; - padding: 0.75rem 1rem; - margin: 0; - font-size: 1rem; - border: none; - overflow: hidden; - white-space: normal; - background: none; - & > code { - display: block; - border: none; - text-align: left; - max-height: 320px; - font-size: 14px; - } -`; - -const CodeCopyButton = styled('div')` - display: block; - align-content: center; - justify-items: center; - position: relative; - line-height: 0px; - right: 0px; - max-height: 320px; - padding: 9px 6px 9px 6px; - font-size: 14px; - white-space: pre; -`; - -const CopyableCodeBlock = ({ children }: Props) => ( - - - {children} - - - - - -); - -export default CopyableCodeBlock; diff --git a/common/v2/components/Currency.tsx b/common/v2/components/Currency.tsx deleted file mode 100644 index fedc97ed8c6..00000000000 --- a/common/v2/components/Currency.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -import { TSymbol } from 'v2/types'; -import { default as Typography } from './Typography'; -import AssetIcon from './AssetIcon'; - -const SContainer = styled('div')` - display: inline-flex; - align-contents: center; -`; - -const SAssetIconContainer = styled('span')` - padding-right: 5px; -`; - -interface Props { - amount: string; - symbol: TSymbol; - decimals?: number; - icon?: boolean; - prefix?: boolean; - bold?: boolean; - fontSize?: string; -} - -function Currency({ - amount, - symbol, - decimals = 5, - icon = false, - prefix = false, - bold = false, - fontSize, - ...props -}: Props) { - const format = (value: string, decimalPlaces: number) => { - const v = parseFloat(value); - return Number(v).toLocaleString(undefined, { - minimumFractionDigits: decimalPlaces, - maximumFractionDigits: decimalPlaces - }); - // const multiplier = Math.pow(10, decimalPlaces); - // return Math.round(v * multiplier + Number.EPSILON) / multiplier; - }; - - return ( - - {icon && ( - - - - )} - - {prefix && `${symbol}`} - {format(amount, decimals)} - {!prefix && ` ${symbol}`} - - - ); -} - -export default Currency; diff --git a/common/v2/components/DashboardPanel.tsx b/common/v2/components/DashboardPanel.tsx deleted file mode 100644 index 4b4cd15fbbd..00000000000 --- a/common/v2/components/DashboardPanel.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { Heading } from '@mycrypto/ui'; - -import { COLORS, FONT_SIZE, SPACING } from 'v2/theme'; - -import settingsIcon from 'common/assets/images/icn-settings.svg'; - -import { Panel } from './Panel'; -import RouterLink from './RouterLink'; -import Typography from './Typography'; - -const SRouterLink = styled(RouterLink)` - display: flex; - flex-direction: row; - align-items: center; - & span, - p, - div { - color: ${COLORS.BLUE_BRIGHT}; - } - & img { - margin-right: 0.5em; - } -`; - -const Content = styled.div` - padding: ${SPACING.BASE}; -`; - -const DPanel = styled(Panel)` - display: flex; - flex-direction: column; - flex: 1; - margin: 0 0 ${SPACING.BASE} 0; - padding: 0; -`; - -const DHeadingWrapper = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - padding: ${SPACING.BASE}; -`; - -const DFooterWrapper = styled.div` - display: flex; - align-items: center; - justify-content: center; - background: ${COLORS.BLUE_GREY_LIGHTEST}; -`; - -const DHeading = styled(Heading)` - && { - margin: 0; - font-size: ${FONT_SIZE.XL}; - font-weight: bold; - color: ${COLORS.BLUE_DARK_SLATE}; - } -`; - -interface Props { - heading: any; - children: any; - headingRight?: string | JSX.Element; - footer?: JSX.Element; - actionLink?: string; - className?: string; - padChildren?: boolean; -} - -export const DashboardPanel = ({ - heading, - headingRight, - actionLink, - footer, - className = '', - children, - padChildren, - ...rest -}: Props) => { - return ( - - - {heading} - {headingRight && - (actionLink ? ( - - {'settings'} - {headingRight} - - ) : ( - headingRight - ))} - - {padChildren ? {children} : children} - {footer && {footer}} - - ); -}; diff --git a/common/v2/components/DisclaimerModal.scss b/common/v2/components/DisclaimerModal.scss deleted file mode 100644 index 31ed6a68e8e..00000000000 --- a/common/v2/components/DisclaimerModal.scss +++ /dev/null @@ -1,15 +0,0 @@ -@import 'common/sass/variables'; -@import 'common/sass/mixins'; - -.Disclaimer { - color: color(text-color); - - &-button { - @include reset-button; - color: #FFF; - - &:hover { - opacity: 0.8; - } - } -} diff --git a/common/v2/components/DisclaimerModal.tsx b/common/v2/components/DisclaimerModal.tsx deleted file mode 100644 index 4e93b134999..00000000000 --- a/common/v2/components/DisclaimerModal.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; - -import { HELP_ARTICLE } from 'v2/config'; -import translate from 'v2/translations'; - -import { LegacyModal, IButton, HelpLink } from 'v2/components'; -import './DisclaimerModal.scss'; - -interface Props { - isOpen: boolean; - handleClose(): void; -} - -const DisclaimerModal: React.SFC = ({ isOpen, handleClose }) => { - const buttons: IButton[] = [ - { text: translate('ACTION_10'), type: 'default', onClick: handleClose } - ]; - return ( - -

- Be safe & secure: - - We highly recommend that you read our guide on How to Prevent Loss & Theft for some - recommendations on how to be proactive about your security. - -

-

- Always backup your keys: - MyCrypto.com & MyCrypto CX are not "web wallets". You do not create an account or give us - your funds to hold onto. No data leaves your computer / your browser. We make it easy for - you to create, save, and access your information and interact with the blockchain. -

-

- We are not responsible for any loss: - Ethereum, MyCrypto.com & MyCrypto CX, and some of the underlying Javascript libraries we use - are under active development. While we have thoroughly tested & tens of thousands of wallets - have been successfully created by people all over the globe, there is always the possibility - something unexpected happens that causes your funds to be lost. Please do not invest more - than you are willing to lose, and please be careful. -

-

- Default Network Endorsements: - MyCrypto, by default, supports all Ethereum-based blockchains ("networks") through the - adding of custom nodes. To save time and provide a convenience factor to users, if an - Ethereum-based network has enough perceived interest or users, MyCrypto will accept GitHub - pull requests to add that network as one of the default supported networks. This will bypass - the user's need to add that blockchain as a custom node in the MyCrypto network selector.{' '} -
-
- If MyCrypto chooses to implement support for a specific network, it should be noted that - MyCrypto does not assume the responsibility of managing that network.
-
A network being added to MyCrypto's default supported networks list does not mean that - MyCrypto has a partnership with said network, and is not an endorsement of said network. -

-

- Translations of MyCrypto: - The community has done an amazing job translating MyCrypto into a variety of languages. - However, MyCrypto can only verify the validity and accuracy of the information provided in - English and, because of this, the English version of our website is the official text. -

-

- MIT License -
- Copyright (c) 2015-2017 MyEtherWallet LLC -
- Copyright (c) 2019 MyCrypto, Inc. -

-

- Permission is hereby granted, free of charge, to any person obtaining a copy of this - software and associated documentation files (the "Software"), to deal in the Software - without restriction, including without limitation the rights to use, copy, modify, merge, - publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons - to whom the Software is furnished to do so, subject to the following conditions: -

-

- The above copyright notice and this permission notice shall be included in all copies or - substantial portions of the Software. -

- - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR - PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE - FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR - OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. - -
- ); -}; - -export default DisclaimerModal; diff --git a/common/v2/components/Divider.tsx b/common/v2/components/Divider.tsx deleted file mode 100644 index 185da2cee8e..00000000000 --- a/common/v2/components/Divider.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -interface Props { - height?: string; - color?: string; - padding?: string; -} - -const SDivider = styled('div')` - height: ${(p: Props) => (p.height ? p.height : '1px')}; - background-color: ${(p: Props) => (p.color ? p.color : 'var(--color-gray-lighter)')}; - width: 100%; - background-clip: content-box; - padding-left: ${(p: Props) => (p.padding ? p.padding : 0)}; - padding-right: ${(p: Props) => (p.padding ? p.padding : 0)}; -`; -function Divider({ color, height, padding, ...props }: Props) { - return ; -} - -export default Divider; diff --git a/common/v2/components/DomainStatus.tsx b/common/v2/components/DomainStatus.tsx deleted file mode 100644 index 9f51dc123f8..00000000000 --- a/common/v2/components/DomainStatus.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import translate, { translateRaw } from 'v2/translations'; -import { Spinner } from './Spinner'; -import { ResolutionError } from '@unstoppabledomains/resolution'; - -export interface DomainStatusProps { - isLoading: boolean; - isError: boolean; - rawAddress: string; - domain: string; - resolutionError?: ResolutionError; -} - -export const DomainStatus: React.FC = (props: DomainStatusProps) => { - const parseError = (resolutionError?: ResolutionError) => { - if (!resolutionError) - return
{`Could not resolve the domain ${props.domain}`}
; - return
{`${resolutionError.message}`}
; - }; - - const spinner = () => ( -
- {translate('LOADING_ADDRESS')}{' '} -
- ); - - if (props.isLoading) return spinner(); - if (props.domain === '') return
{''}
; - if (props.isError || props.resolutionError) { - return parseError(props.resolutionError); - } - return ( -
{`${translateRaw('SEND_ASSETS_DID_RESOLVE')}: ${ - props.rawAddress - }`}
- ); -}; diff --git a/common/v2/components/Dropdown.tsx b/common/v2/components/Dropdown.tsx deleted file mode 100644 index ee873c2bbf1..00000000000 --- a/common/v2/components/Dropdown.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import Select, { OptionComponentProps, ArrowRendererProps } from 'react-select'; -import styled from 'styled-components'; -import { Icon } from '@mycrypto/ui'; - -import { COLORS, FONT_SIZE } from 'v2/theme'; - -// Give a height to the input when value is defined -// Overide custom styles common/sass/styles/overrides/react-select.scss -interface SProps { - value?: string; - disabled?: boolean; -} - -const SSelect = styled(Select)` - height: ${(props: SProps) => (props.value ? 'auto' : '54px')}; - background-color: ${(props: SProps) => (props.disabled ? COLORS.GREY_LIGHTEST : 'default')}; - ${props => props.disabled && '.Select-arrow {display: none};'} font-size: ${FONT_SIZE.BASE}; - - /* Set max-height to prevent the dropdown form overflowing the footer. */ - .Select-menu { - max-height: 20em !important; - } -`; - -interface Props { - options: T[]; - value: T; - disabled?: boolean; - placeholder?: string; - searchable?: boolean; - name?: string; - optionComponent?: - | React.ComponentClass> - | React.StatelessComponent>; - valueComponent?: React.ComponentClass | React.StatelessComponent; - onChange?(option: T): void; -} - -const Chevron = styled(Icon)` - font-size: 0.75rem; -`; - -const DropdownIndicator = (props: ArrowRendererProps) => ( - -); - -export default function Dropdown({ - onChange, - options, - optionComponent, - value, - valueComponent, - placeholder, - disabled, - searchable, - name // field name for hidden input. Important for Formik -}: Props) { - return ( - - ); -} diff --git a/common/v2/components/EditableText.tsx b/common/v2/components/EditableText.tsx deleted file mode 100644 index 648b3881940..00000000000 --- a/common/v2/components/EditableText.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import Typography from './Typography'; -import styled from 'styled-components'; - -import editIcon from 'common/assets/images/icn-edit.svg'; -import { COLORS, SPACING } from 'v2/theme'; - -const Wrapper = styled.div` - display: flex; - height: 100%; - align-items: center; -`; - -const EditIcon = styled.img` - cursor: pointer; - padding: ${SPACING.XS} ${SPACING.SM}; - opacity: 1; - &:hover { - transition: 200ms ease all; - transform: scale(1.02); - opacity: 0.7; - } -`; - -const SInputField = styled.input` - border-radius: 6px; - border: 2px solid ${COLORS.GREY_ATHENS}; - font-weight: 400; - width: auto; - padding: ${SPACING.XS}; -`; - -const STypography = styled(Typography)` - border-color: transparent; - line-height: 1.4; - &:hover { - border-bottom: 1px ${COLORS.BLUE_GREY} dashed; - cursor: pointer; - } -`; - -export interface Props { - value: string; - className?: string; - bold?: boolean; - truncate?: boolean; - saveValue(value: string): void; -} - -function EditableText({ saveValue, value, className, bold, truncate }: Props) { - const [editMode, setEditMode] = useState(false); - const [editValue, setEditValue] = useState(''); - - const handleKeyDown = ({ key }: { key: string }) => { - if (key === 'Escape') { - cancel(); - } else if (key === 'Enter') { - save(); - } - }; - - useEffect(() => { - setEditValue(value); - }, [value]); - - const edit = () => { - setEditMode(true); - }; - - const cancel = () => { - setEditMode(false); - setEditValue(value); - }; - - const save = () => { - saveValue(editValue); - setEditMode(false); - }; - - return ( - - {editMode ? ( - setEditValue(e.currentTarget.value)} - onBlur={save} - onKeyDown={handleKeyDown} - /> - ) : ( - <> - - {value} - - - - )} - - ); -} - -export default EditableText; diff --git a/common/v2/components/EthAddress.tsx b/common/v2/components/EthAddress.tsx deleted file mode 100644 index 55e7e06eeff..00000000000 --- a/common/v2/components/EthAddress.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { Copyable } from '@mycrypto/ui'; - -import { COLORS, monospace } from 'v2/theme'; - -// Override styles of @mycrypto/ui in order to vertically align text and icon. -// Ensure the icon uses the discrete color. -const Overrides = styled.div` - * { - font-family: ${monospace}; - } - - & button { - vertical-align: middle; - font-size: 1em; - line-height: 24px; - } - & button span { - display: inline-flex; - height: 100%; - align-items: center; - vertical-align: middle; - margin-top: -0.1ch; - line-height: 24px; - } - & button svg { - height: 0.8em; - color: ${COLORS.BLUE_GREY}; - } -`; -interface Props { - address: string; - isCopyable?: boolean; - truncate: any; -} - -function EthAddress({ address, isCopyable = true, truncate }: Props) { - return ( - - - - ); -} - -export default EthAddress; diff --git a/common/v2/components/FlippablePanel.scss b/common/v2/components/FlippablePanel.scss deleted file mode 100644 index 106ab7b1c79..00000000000 --- a/common/v2/components/FlippablePanel.scss +++ /dev/null @@ -1,3 +0,0 @@ -.FlippablePanel { - // -} diff --git a/common/v2/components/GasSelector.tsx b/common/v2/components/GasSelector.tsx deleted file mode 100644 index 437e54de627..00000000000 --- a/common/v2/components/GasSelector.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import styled from 'styled-components'; -import BN from 'bn.js'; -import { addHexPrefix } from 'ethereumjs-util'; - -import { InputField, Typography, Checkbox } from 'v2/components'; -import { translateRaw } from 'v2/translations'; -import { - fetchGasPriceEstimates, - inputGasPriceToHex, - hexWeiToString, - getNonce, - getGasEstimate, - hexToNumber -} from 'v2/services'; -import { StoreAccount } from 'v2/types'; -import { COLORS, monospace } from 'v2/theme'; - -const { GREY_LIGHTER } = COLORS; - -const Wrapper = styled.div` - display: flex; - width: 100%; - margin-top: 16px; - flex-direction: column; -`; - -const CustomLabel = styled(Typography)` - font-size: 1em; -`; - -const FieldWrapper = styled.div` - input { - font-family: ${monospace}; - :disabled { - background-color: ${GREY_LIGHTER}; - } - } -`; - -interface Props { - gasPrice: string; - gasLimit: string; - nonce: string; - account: StoreAccount; - estimateGasCallProps: object; - setGasPrice(gasPrice: string): void; - setGasLimit(gasLimit: string): void; - setNonce(nonce: string): void; -} - -export default function GasSelector(props: Props) { - const { - gasPrice, - gasLimit, - nonce, - setGasPrice, - setGasLimit, - setNonce, - estimateGasCallProps, - account - } = props; - - const [isAutoGasSet, setIsAutoGasSet] = useState(true); - - useEffect(() => { - if (isAutoGasSet) { - estimateGas(); - } - }, [estimateGasCallProps, isAutoGasSet]); - - const toggleIsAutoGasSet = () => { - setIsAutoGasSet(!isAutoGasSet); - }; - - const handleGasPriceChange = (e: React.KeyboardEvent) => { - setGasPrice((e.target as HTMLTextAreaElement).value); - }; - - const handleGasLimitChange = (e: React.KeyboardEvent) => { - setGasLimit((e.target as HTMLTextAreaElement).value); - }; - - const handleNonceChange = (e: React.KeyboardEvent) => { - setNonce((e.target as HTMLTextAreaElement).value); - }; - - const estimateGas = async () => { - if (!account) { - return; - } - - try { - const { network } = account; - const { fast } = await fetchGasPriceEstimates(network); - setGasPrice(fast.toString()); - const fetchedGasPrice = hexWeiToString(inputGasPriceToHex(fast.toString())); - const fetchedNonce = await getNonce(network, account); - setNonce(fetchedNonce.toString()); - - const txConfig: any = Object.assign({}, estimateGasCallProps, { - gasPrice: addHexPrefix(new BN(fetchedGasPrice).toString(16)), - nonce: fetchedNonce, - chainId: network.chainId - }); - const fetchedGasLimit = await getGasEstimate(network, txConfig); - setGasLimit(hexToNumber(fetchedGasLimit).toString()); - } catch (e) { - console.debug(e); - } - }; - - return ( - - - - {translateRaw('OFFLINE_STEP2_LABEL_3')}} - value={gasPrice} - onChange={handleGasPriceChange} - /> - - - {translateRaw('OFFLINE_STEP2_LABEL_4')}} - value={gasLimit} - onChange={handleGasLimitChange} - disabled={isAutoGasSet} - /> - - - {translateRaw('OFFLINE_STEP2_LABEL_5')}} - value={nonce} - onChange={handleNonceChange} - /> - - - ); -} diff --git a/common/v2/components/GeneralStepper/GeneralStepper.tsx b/common/v2/components/GeneralStepper/GeneralStepper.tsx deleted file mode 100644 index 6b7f3d0d190..00000000000 --- a/common/v2/components/GeneralStepper/GeneralStepper.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { useState } from 'react'; -import { useHistory } from 'react-router-dom'; - -import { ContentPanel } from 'v2/components'; -import { translateRaw } from 'v2/translations'; -import { ROUTE_PATHS } from 'v2/config'; - -import { IStepperPath } from './types'; - -export interface StepperProps { - steps: IStepperPath[]; - defaultBackPath?: string; // Path that component reverts to when user clicks "back" from first step in flow. - defaultBackPathLabel?: string; - completeBtnText?: string; // Text for btn to navigate out of flow in last step. -} - -export function GeneralStepper({ - steps, - defaultBackPath, - defaultBackPathLabel, - completeBtnText -}: StepperProps) { - const history = useHistory(); - const [step, setStep] = useState(0); - - const goToPrevStep = () => { - return setStep(Math.max(0, step - 1)); - }; - - const goToFirstStep = () => { - return setStep(0); - }; - - const goBack = () => - step === 0 ? history.push(defaultBackPath || ROUTE_PATHS.DASHBOARD.path) : goToPrevStep(); - - const getStep = (stepIndex: number) => { - const path = steps; - const { label, component, actions, props } = steps[stepIndex]; // tslint:disable-line - return { currentPath: path, label, Step: component, stepAction: actions, props }; - }; - - const { currentPath, label, Step, stepAction } = getStep(step); - - // Creates the label for the btn that sends user to the previous step. - // If there is no previous step, you'll be going to either the default location (dashboard), or a user-specified location. - // If there is a previous step, you'll go to that step and it's title is used as the back btn label. - const getBackBtnLabel = () => - Math.max(-1, step - 1) === -1 - ? defaultBackPathLabel || translateRaw('DASHBOARD') - : getStep(Math.max(0, step - 1)).label; - - const goToNextStep = () => setStep(Math.min(step + 1, currentPath.length - 1)); - - const stepObject = steps[step]; - const stepProps = stepObject.props; - const stepActions = stepObject.actions; - - return ( - - - stepAction ? stepAction(payload, goToNextStep, goToPrevStep) : goToNextStep() - } - completeButtonText={completeBtnText || translateRaw('SEND_ASSETS_SEND_ANOTHER')} - resetFlow={goToFirstStep} - {...stepProps} - {...stepActions} - /> - - ); -} - -export default GeneralStepper; diff --git a/common/v2/components/GeneralStepper/__tests__/GeneralStepper.test.tsx b/common/v2/components/GeneralStepper/__tests__/GeneralStepper.test.tsx deleted file mode 100644 index a64da92dcf1..00000000000 --- a/common/v2/components/GeneralStepper/__tests__/GeneralStepper.test.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react'; -import { MemoryRouter, Switch, Route } from 'react-router-dom'; -import { simpleRender, fireEvent } from 'test-utils'; - -import GeneralStepper, { StepperProps } from '../GeneralStepper'; - -const ExampleButtonComponent = ({ onComplete, onCompleteText }: any) => ( - <> - - -); - -const ExampleButtonFinalComponent = ({ - onComplete, - onCompleteText, - resetFlow, - completeButtonText -}: any) => ( - <> - - - -); - -const functionTest = (_: string, cb: any) => { - cb(); -}; -const testSteps = [ - { - label: 'Test Component 1', - component: ExampleButtonComponent, - props: { onCompleteText: 'Finished this1' }, - actions: (payload: any, cb: any) => functionTest(payload, cb) - }, - { - label: 'Test Component 2', - component: ExampleButtonFinalComponent, - props: { onCompleteText: 'Finished this2' }, - actions: (payload: any, cb: any) => functionTest(payload, cb) - } -]; - -const defaultBackPath = '/dashboard'; -const defaultBackPathLabel = 'Dashboard'; -const completeBtnText = 'Finished'; - -const defaultProps: StepperProps = { - steps: testSteps, - defaultBackPath, - defaultBackPathLabel, - completeBtnText -}; - -describe('GeneralStepper', () => { - let location: any; - const StepperComponent = (props: StepperProps, path?: any) => ( - - - { - location = renderProps.location; - return ; - }} - /> - - - ); - const renderComponent = (props: StepperProps) => simpleRender(); - - test('it renders step 1 correctly first', async () => { - const { getByText } = renderComponent(defaultProps); - const text = getByText('Test Component 1'); // The header for step 1 - expect(text).toBeInTheDocument(); - }); - test('it renders step 1 back button correctly', async () => { - const { getByText } = renderComponent(defaultProps); - const text = getByText(`Back : ${defaultBackPathLabel}`); // The back button from step 1 - expect(text).toBeInTheDocument(); - }); - test('it correctly reroutes on step 1 back button click', async () => { - const { getByText } = renderComponent(defaultProps); - const text = getByText(`Back : ${defaultBackPathLabel}`); // The back button from step 1 - fireEvent.click(text); - expect(location.pathname).toEqual(defaultBackPath); - }); - test('it renders step 2 when goToNext is clicked in step 1', async () => { - const { getByText } = renderComponent(defaultProps); - const text = getByText('Click Me'); - fireEvent.click(text); // Go to step 2 - const newText = getByText('Test Component 2'); // The header for step 2 - expect(newText).toBeInTheDocument(); - }); - test('it renders step 1 when back button is clicked from step 2', async () => { - const { getByText } = renderComponent(defaultProps); - const text = getByText('Click Me'); - fireEvent.click(text); // Go to step 2 - const goBackButton = getByText(`Back : Test Component 1`); // The back button when step 2 is rendered - fireEvent.click(goBackButton); - const stepOneText = getByText('Test Component 1'); // The header for step 1 - expect(stepOneText).toBeInTheDocument(); - }); - test('it resets flow when resetFlow button is clicked from final component', async () => { - const { getByText } = renderComponent(defaultProps); - const text = getByText('Click Me'); - fireEvent.click(text); // Go to step 2 - - const resetFlowButton = getByText(completeBtnText); // The back button when step 2 is rendered - fireEvent.click(resetFlowButton); - const stepOneText = getByText('Test Component 1'); // The header for step 1 - expect(stepOneText).toBeInTheDocument(); - }); -}); diff --git a/common/v2/components/GeneralStepper/index.ts b/common/v2/components/GeneralStepper/index.ts deleted file mode 100644 index 036fa201940..00000000000 --- a/common/v2/components/GeneralStepper/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './GeneralStepper'; diff --git a/common/v2/components/GeneralStepper/types.ts b/common/v2/components/GeneralStepper/types.ts deleted file mode 100644 index 77006d39577..00000000000 --- a/common/v2/components/GeneralStepper/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type TStepAction = (payload: any, after: () => void) => void; - -export interface IStepperComponentProps { - completeButtonText: string; - resetFlow(): void; - onComplete(payload?: any): void; -} - -export interface IStepperPath { - label: string; - component: any; - props?: any; - actions?(payload?: any, cb?: any, cbTwo?: any): any; // cb = goToNextStep; cbTwo = goToPrevStep -} diff --git a/common/v2/components/HelpLink.tsx b/common/v2/components/HelpLink.tsx deleted file mode 100644 index 76a9aaf7140..00000000000 --- a/common/v2/components/HelpLink.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -import { HELP_ARTICLE, knowledgeBaseURL } from 'v2/config'; -import NewTabLink, { AAttributes } from './NewTabLink'; - -interface Props { - article?: HELP_ARTICLE; - children?: string | React.ReactElement; -} - -const HelpLink: React.SFC = ({ article, children, ...rest }) => ( - - {children} - -); - -export default HelpLink; diff --git a/common/v2/components/IconArrow.tsx b/common/v2/components/IconArrow.tsx deleted file mode 100644 index 57a7c21a272..00000000000 --- a/common/v2/components/IconArrow.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -import chevronIcon from 'assets/images/chevron-right.svg'; - -const ArrowContainer = styled.img<{ isFlipped?: boolean }>` - margin-left: 0.5em; - ${props => - props.isFlipped && - ` - transform: rotateX(180deg); - `}; -`; - -interface ArrowProps { - isFlipped?: boolean; -} - -export const IconArrow = ({ isFlipped }: ArrowProps) => { - return ; -}; - -export default IconArrow; diff --git a/common/v2/components/IconCaret.tsx b/common/v2/components/IconCaret.tsx deleted file mode 100644 index feb78601ef2..00000000000 --- a/common/v2/components/IconCaret.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import styled from 'styled-components'; -import { Icon } from '@mycrypto/ui'; - -const IconCaret = styled(Icon)` - margin: 0; - margin-left: 20px; - margin-bottom: -1em; - font-size: 0.75em; -`; - -export default IconCaret; diff --git a/common/v2/components/InlineMessage/InlineMessage.tsx b/common/v2/components/InlineMessage/InlineMessage.tsx deleted file mode 100644 index 6a9ea57ebcd..00000000000 --- a/common/v2/components/InlineMessage/InlineMessage.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { COLORS, SPACING, FONT_SIZE } from 'v2/theme'; -import { InlineMessageType } from 'v2/types/inlineMessages'; -import Typography from '../Typography'; -import infoSVG from 'assets/images/icn-info.svg'; -import warningSVG from 'assets/images/icn-warning.svg'; -import arrowSVG from 'assets/images/icn-arrow-purple.svg'; - -interface Props { - type?: InlineMessageType; - value?: any; - children?: any; - className?: string; -} - -interface Config { - color: string; - icon: string; -} - -export const Wrapper = styled.div` - font-size: ${FONT_SIZE.BASE}; - width: 100%; - text-align: justify; - white-space: pre-line; -`; - -interface BannerTypographyProps { - color: string; -} - -const STypography = styled(Typography)` - color: ${props => props.color}; - - a { - color: ${props => props.color}; - text-decoration: underline; - font-weight: normal; - } - - a:hover { - color: ${props => props.color}; - font-weight: bold; - } -`; - -const Icon = styled.img` - color: ${props => props.color}; - max-width: 24px; - margin-right: ${SPACING.XS}; -`; - -const messageConfig = (type: InlineMessageType): Config => { - switch (type) { - default: - case InlineMessageType.ERROR: - return { - color: COLORS.PASTEL_RED, - icon: warningSVG - }; - case InlineMessageType.INFO_CIRCLE: - return { - color: COLORS.ORANGE, - icon: infoSVG - }; - case InlineMessageType.INFO_ARROW: - return { - color: COLORS.PURPLE, - icon: arrowSVG - }; - } -}; - -export default function InlineMessage({ - type = InlineMessageType.ERROR, - value, - children, - className -}: Props) { - const config = messageConfig(type); - return ( - - - - - ); -} diff --git a/common/v2/components/InputField.tsx b/common/v2/components/InputField.tsx deleted file mode 100644 index c504d438f57..00000000000 --- a/common/v2/components/InputField.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import React, { Component } from 'react'; -import styled from 'styled-components'; -import { Icon } from '@mycrypto/ui'; - -import { COLORS } from 'v2/theme'; -import { InlineMessage, Spinner } from 'v2/components'; -import { InlineMessageType } from 'v2/types'; - -const MainWrapper = styled.div` - margin-bottom: ${props => props.marginBottom}; - width: 100%; -`; - -interface WrapperProps { - marginBottom?: string; -} - -const Label = styled.p` - font-size: 18px; - width: 100%; - line-height: 1; - text-align: left; - font-weight: normal; - margin-bottom: 9px; - color: ${props => props.theme.text}; -`; - -interface CustomInputProps { - inputError?: string | JSX.Element; - inputErrorBorder?: boolean; - showEye?: boolean; - customIcon?: React.ReactType; - height?: string; - maxHeight?: string; - resizable?: boolean; -} - -const CustomInput = styled.input` - width: 100%; - background: ${props => props.theme.controlBackground}; - border: 0.125em solid ${props => props.theme.controlBorder}; - border-radius: 0.125em; - padding: ${props => (props.showEye || props.customIcon ? '12px 36px 12px 12px' : '12px 12px')}; - display: flex; - :focus-within { - outline: none; - box-shadow: ${props => props.theme.outline}; - } - ::placeholder { - color: ${COLORS.GREY_LIGHT}; - opacity: 1; - } - border-color: ${props => (props.inputError && props.inputErrorBorder ? COLORS.PASTEL_RED : '')}; - ${props => props.height && `height: ${props.height}`}; -`; - -const CustomTextArea = styled.textarea` - width: 100%; - background: ${props => props.theme.controlBackground}; - border: 0.125em solid ${props => props.theme.controlBorder}; - border-radius: 0.125em; - padding: ${props => (props.showEye ? '12px 36px 12px 12px' : '12px 12px')} - display: flex; - :focus-within { - outline: none; - box-shadow: ${props => props.theme.outline}; - } - ::placeholder { - color: ${COLORS.GREY_LIGHT}; - opacity: 1; - } - border-color: ${props => (props.inputError ? COLORS.PASTEL_RED : '')}; - resize: ${props => (props.resizable ? 'default' : 'none')}; - ${props => props.height && `height: ${props.height}`}; - ${props => props.maxHeight && `max-height: ${props.maxHeight}`}; -`; - -const InputWrapper = styled.div` - position: relative; - width: 100%; - - input { - :disabled { - background-color: ${COLORS.GREY_LIGHTER}; - } - } -`; - -interface CustomIconProps { - showPassword?: boolean; -} - -const EyeIcon = styled(Icon)` - svg { - margin-top: 6px; - width: 23px; - height: 23px; - color: ${(props: CustomIconProps) => (props.showPassword ? COLORS.BLUE_BRIGHT : '')}; - cursor: pointer; - user-select: none; - } -`; - -const DefaultIconWrapper = styled.div` - display: flex; - height: 100%; - align-items: center; - position: absolute; - right: 10px; - top: 0; -`; - -const CustomIconWrapper = styled.div` - display: flex; - height: 100%; - align-items: center; - position: absolute; - right: 10px; - top: 0; -`; - -const CustomIcon = styled.span` - display: flex; - border-left: 1px solid ${COLORS.GREY_GEYSER}; - img { - margin-top: 2px; - margin-bottom: 2px; - margin-left: 8px; - color: ${COLORS.BLUE_BRIGHT}}; - cursor: pointer; - user-select: none; - } -`; - -interface Props { - name?: string; - type?: string; - label?: string | JSX.Element; - value: string | undefined; - inputError?: string | JSX.Element | undefined; - inputErrorType?: InlineMessageType; - inputErrorBorder?: boolean; - showEye?: boolean; - customIcon?: React.ElementType; - textarea?: boolean; - placeholder?: string; - height?: string; - maxHeight?: string; - marginBottom?: string; - resizableTextArea?: boolean; - disabled?: boolean; - isLoading?: boolean; - onChange?(event: any): void; - onBlur?(event: any): void; - validate?(): Promise | void | undefined; - onFocus?(event: any): void; -} - -export class InputField extends Component { - public state = { showPassword: false }; - private validatorTimeout: any = null; - - public handleEyeClick = () => { - this.setState({ showPassword: !this.state.showPassword }); - }; - - public render() { - const { - name, - value, - label, - onChange, - onBlur, - onFocus, - inputError, - inputErrorType, - inputErrorBorder = false, - type, - showEye, - customIcon, - textarea, - placeholder, - height, - resizableTextArea, - disabled, - isLoading, - maxHeight, - marginBottom = '15px' - } = this.props; - - const IconComponent = customIcon as React.ElementType; - - return ( - - {label && } - - {textarea ? ( - - ) : ( - - )} - - {customIcon && ( - - - - - - )} - - {showEye && ( - - - - )} - - {isLoading && ( - - - - )} - - - {inputError && {inputError}} - - ); - } - - public handleKeyUp = () => { - const { validate } = this.props; - if (!validate) { - return; - } - - clearTimeout(this.validatorTimeout); - - // Call validation function 500ms after the user stops typing - this.validatorTimeout = setTimeout(() => { - validate(); - }, 500); - }; -} - -export default InputField; diff --git a/common/v2/components/LegacyModal/Modal.tsx b/common/v2/components/LegacyModal/Modal.tsx deleted file mode 100644 index 85b532ff5bc..00000000000 --- a/common/v2/components/LegacyModal/Modal.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { PureComponent } from 'react'; -import { createPortal } from 'react-dom'; -import { TransitionGroup, CSSTransition } from 'react-transition-group'; - -import ModalBody from './ModalBody'; -import { IButton } from './types'; -import './index.scss'; - -interface Props { - isOpen?: boolean; - title?: React.ReactNode; - disableButtons?: boolean; - hideButtons?: boolean; - children: React.ReactNode; - buttons?: IButton[]; - maxWidth?: number; - handleClose(): void; -} -interface ModalStyle { - width?: string; - maxWidth?: string; -} - -const Fade = ({ ...props }: any) => ( - -); - -export class Modal extends PureComponent { - public modalParent: HTMLElement; - public modalBody: ModalBody; - - public componentDidUpdate(prevProps: Props) { - if (prevProps.isOpen !== this.props.isOpen) { - document.body.classList.toggle('no-scroll', !!this.props.isOpen); - } - } - - public componentDidMount() { - const modalEl = document.getElementById('ModalContainer'); - if (modalEl) { - this.modalParent = document.createElement('div'); - modalEl.appendChild(this.modalParent); - } - } - - public componentWillUnmount() { - document.body.classList.remove('no-scroll'); - const modalEl = document.getElementById('ModalContainer'); - if (modalEl) { - modalEl.removeChild(this.modalParent); - } - } - - public render() { - const { - isOpen, - title, - children, - buttons, - disableButtons, - hideButtons, - handleClose, - maxWidth - } = this.props; - const hasButtons = buttons && buttons.length; - const modalStyle: ModalStyle = {}; - - if (maxWidth) { - modalStyle.width = '100%'; - modalStyle.maxWidth = `${maxWidth}px`; - } - - const modalBodyProps = { - title, - children, - modalStyle, - hasButtons, - buttons, - disableButtons, - hideButtons, - handleClose - }; - - const modal = ( - - {isOpen && ( - // Trap focus in modal by focusing the first element after the animation is complete - this.modalBody.firstTabStop.focus()}> -
-
- (this.modalBody = div as ModalBody)} /> -
- - )} - - ); - - if (this.modalParent) { - return createPortal(modal, this.modalParent); - } else { - return modal; - } - } -} - -export default Modal; diff --git a/common/v2/components/LegacyModal/ModalBody.tsx b/common/v2/components/LegacyModal/ModalBody.tsx deleted file mode 100644 index 3df17e5cf95..00000000000 --- a/common/v2/components/LegacyModal/ModalBody.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { CSSProperties } from 'react'; - -import closeIcon from 'assets/images/close.svg'; -import { IButton } from 'v2/components'; - -interface Props { - title?: React.ReactNode; - children: React.ReactNode; - modalStyle?: CSSProperties; - hasButtons?: number; - buttons?: IButton[]; - disableButtons?: boolean; - hideButtons?: boolean; - handleClose(): void; -} - -export default class ModalBody extends React.Component { - public firstTabStop: HTMLElement; - private modal: HTMLElement; - private modalContent: HTMLElement; - private focusedElementBeforeModal: HTMLElement; - private lastTabStop: HTMLElement; - - public componentDidMount() { - this.focusedElementBeforeModal = document.activeElement as HTMLElement; - // Find all focusable children - const focusableElementsString = - 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]'; - const focusableElements = Array.prototype.slice.call( - this.modal.querySelectorAll(focusableElementsString) - ); - - // Convert NodeList to Array - this.firstTabStop = focusableElements[0]; - this.lastTabStop = focusableElements[focusableElements.length - 1]; - - this.modal.addEventListener('keydown', this.keyDownListener); - } - - public componentWillUnmount() { - document.removeEventListener('keydown', this.keyDownListener); - } - - public scrollContentToTop = () => { - this.modalContent.scrollTop = 0; - }; - - public render() { - const { title, children, modalStyle, hasButtons, hideButtons, handleClose } = this.props; - return ( -
{ - this.modal = div as HTMLElement; - }} - > - {title && ( -
-

{title}

-
- -
- )} - -
(this.modalContent = div as HTMLElement)}> - {children} -
-
- {hasButtons && !hideButtons &&
{this.renderButtons()}
} -
- ); - } - - private renderButtons = () => { - const { disableButtons, buttons } = this.props; - if (!buttons || !buttons.length) { - return; - } - - return buttons.map((btn, idx: number) => { - let btnClass = 'Modal-footer-btn btn'; - - if (btn.type) { - btnClass += ` btn-${btn.type}`; - } - - return ( - - ); - }); - }; - - private keyDownListener = (e: KeyboardEvent) => { - // Check for TAB key press - if (e.keyCode === 9) { - // SHIFT + TAB - if (e.shiftKey) { - if (document.activeElement === this.firstTabStop) { - e.preventDefault(); - this.lastTabStop.focus(); - } - - // TAB - } else { - if (document.activeElement === this.lastTabStop) { - e.preventDefault(); - this.firstTabStop.focus(); - } - } - } - - // Check for ESC key press - if (e.keyCode === 27) { - this.focusedElementBeforeModal.focus(); - this.props.handleClose(); - } - }; -} diff --git a/common/v2/components/LegacyModal/index.scss b/common/v2/components/LegacyModal/index.scss deleted file mode 100644 index dbabfd3e94b..00000000000 --- a/common/v2/components/LegacyModal/index.scss +++ /dev/null @@ -1,154 +0,0 @@ -@import 'common/sass/variables'; -@import 'common/sass/mixins'; - -$m-background: color(pane-bg); -$m-window-padding-w: 20px; -$m-window-padding-h: 30px; -$m-window-padding-w-mobile: 10px; -$m-window-padding-h-mobile: 10px; -$m-header-height: 62px; -$m-header-padding: 1rem 2rem 0.5rem 2rem; -$m-content-padding: 1.5rem 2rem; -$m-footer-padding: 0.5rem 2rem 1rem 2rem; -$m-close-size: 26px; -$m-anim-speed: 400ms; - -.Modal { - position: fixed; - top: $m-window-padding-h; - left: 50%; - width: initial; - max-width: 95%; - max-width: calc(100% - #{$m-window-padding-w * 2}); - max-height: 95%; - max-height: calc(100% - #{$m-window-padding-h * 2}); - background: $m-background; - border-radius: 2px; - transform: translateX(-50%); - z-index: $zindex-modal; - overflow: hidden; - display: flex; - flex-direction: column; - animation: modal-open $m-anim-speed ease 1; - text-align: left; - box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14), - 0px 6px 30px 5px rgba(0, 0, 0, 0.12); - - &-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(#000, 0.54); - z-index: $zindex-modal-background; - display: block; - } - - &-fade { - background: linear-gradient(to bottom, transparent, $m-background); - position: fixed; - height: 25px; - width: calc(100% - 3rem); - bottom: 4.5rem; - left: 50%; - transform: translateX(-50%); - - &.has-no-footer { - bottom: 0; - } - } - - &-header { - display: flex; - flex-wrap: nowrap; - padding: $m-header-padding; - align-items: center; - - &-title { - font-size: 1.625rem; - font-weight: 400; - margin: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - letter-spacing: 0; - } - - &-close { - @include reset-button; - height: $m-close-size; - width: $m-close-size; - opacity: 0.3; - transition: opacity 120ms; - - @include theme(dark) { - filter: invert(1); - } - - &:hover { - opacity: 0.87; - } - - &-icon { - width: 100%; - } - } - } - - &-content { - display: flex; - flex: 1; - flex-direction: column; - padding: $m-content-padding; - overflow: auto; - > .Spinner { - margin: 2.5rem auto; - } - } - - &-footer { - padding: $m-footer-padding; - background: $m-background; - - // Selector needs a little extra oomph to override bootstrap - &-btn.btn { - float: right; - margin: 0 0 0 8px; - min-width: 100px; - } - } - - // Mobile styles - @media (max-width: $screen-sm) { - top: 100px; - width: calc(100% - #{$m-window-padding-w-mobile}) !important; - max-width: calc(100% - #{$m-window-padding-w-mobile * 2}); - max-height: calc(100% - #{$m-window-padding-h-mobile * 2}); - } -} - -.animate-modal { - &-enter, - &-exit { - position: relative; - z-index: 3; - transition: opacity 300ms; - } - - &-enter { - opacity: 0; - - &-active { - opacity: 1; - } - } - - &-exit { - opacity: 1; - - &-active { - opacity: 0; - } - } -} diff --git a/common/v2/components/LegacyModal/index.ts b/common/v2/components/LegacyModal/index.ts deleted file mode 100644 index e2fd0b8c835..00000000000 --- a/common/v2/components/LegacyModal/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as LegacyModal } from './Modal'; -export * from './types'; diff --git a/common/v2/components/LegacyModal/types.ts b/common/v2/components/LegacyModal/types.ts deleted file mode 100644 index 2c201d81e09..00000000000 --- a/common/v2/components/LegacyModal/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface IButton { - text: string | React.ReactElement; - type?: 'default' | 'primary' | 'success' | 'info' | 'warning' | 'danger' | 'link'; - disabled?: boolean; - onClick?(): void; -} diff --git a/common/v2/components/Link.tsx b/common/v2/components/Link.tsx deleted file mode 100644 index 5365bb27c2a..00000000000 --- a/common/v2/components/Link.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import styled, { css } from 'styled-components'; - -interface LinkProps { - fullwidth?: boolean; -} - -const StyledLink = styled.a` - ${props => - props.fullwidth === true && - css` - width: 100%; - `} -`; - -export default StyledLink; diff --git a/common/v2/components/LinkOut.tsx b/common/v2/components/LinkOut.tsx deleted file mode 100644 index 004724459f5..00000000000 --- a/common/v2/components/LinkOut.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { Typography } from 'v2/components'; -import { COLORS } from 'v2/theme'; - -import LinkOutIcon from 'assets/images/link-out.svg'; - -const { BLUE_BRIGHT } = COLORS; - -const LinkOutWrapper = styled.a` - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-end; - p { - margin: 0; - } -`; - -const OutIcon = styled.img` - color: ${BLUE_BRIGHT}; - width: 18px; - height: 18px; - margin-left: 10px; -`; - -interface Props { - text: string; - link: string; - truncate?(text: string): string; -} - -const LinkOut = ({ text, link, truncate }: Props) => { - const content = truncate ? truncate(text) : text; - return ( - - {content} - - - ); -}; - -export default LinkOut; diff --git a/common/v2/components/Modal/Modal.tsx b/common/v2/components/Modal/Modal.tsx deleted file mode 100644 index abc6148887d..00000000000 --- a/common/v2/components/Modal/Modal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -interface ModalWrapperProps { - backgroundOpacity?: string; -} - -// prettier-ignore -const ModalWrapper = styled.section` - position: fixed; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, ${props => props.backgroundOpacity || '0.66'}); - z-index: 9999; - top: 0px; -`; - -interface Props { - children: any; - backgroundOpacity?: string; -} - -export default function Modal({ backgroundOpacity, children }: Props) { - return {children}; -} diff --git a/common/v2/components/Modal/index.ts b/common/v2/components/Modal/index.ts deleted file mode 100644 index 0690fecf679..00000000000 --- a/common/v2/components/Modal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Modal'; diff --git a/common/v2/components/Network.tsx b/common/v2/components/Network.tsx deleted file mode 100644 index c88dbd9b6c0..00000000000 --- a/common/v2/components/Network.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { ReactNode } from 'react'; -import styled from 'styled-components'; -import { size } from 'polished'; - -import { scale } from '@mycrypto/ui'; -import { default as Typography } from './Typography'; - -const Container = styled.div` - display: flex; - flex-direction: row; - align-items: center; -`; - -const Color = styled.div` - background: ${props => props.color}; - - border-radius: 50%; - display: inline-block; - margin-right: ${scale(-1)}; - ${size(scale(-1))}; -`; - -export function Network({ children, color }: { children: ReactNode; color: string }) { - return ( - - - {children} - - ); -} - -export default Network; diff --git a/common/v2/components/NetworkSelectDropdown.tsx b/common/v2/components/NetworkSelectDropdown.tsx deleted file mode 100644 index 29c33ddc9b3..00000000000 --- a/common/v2/components/NetworkSelectDropdown.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useContext, useEffect } from 'react'; - -import { OptionComponentProps } from 'react-select'; -import styled from 'styled-components'; -import translate from 'v2/translations'; -import { NetworkContext, isWalletFormatSupportedOnNetwork } from 'v2/services/Store'; -import { NetworkId, WalletId } from 'v2/types'; -import { DEFAULT_NETWORK } from 'v2/config'; -import { Typography, Dropdown } from 'v2/components'; - -interface Props { - network: string | undefined; - accountType?: WalletId; - onChange(network: NetworkId): void; -} - -const DropdownContainer = styled('div')` - .is-open > .Select-control > .Select-multi-value-wrapper > .Select-input:only-child { - transform: translateY(0%); - padding: 16px 15px 16px 15px; - position: inherit; - } -`; - -const SContainer = styled('div')` - display: flex; - flex-direction: row; - padding: 16px 15px 16px 15px; - - &:hover { - background-color: var(--color-gray-lighter); - } -`; - -class NetworkOption extends React.PureComponent { - public render() { - const { option, onSelect } = this.props; - return ( - onSelect && onSelect(option, null)}> - - - ); - } -} - -function NetworkSelectDropdown({ network, accountType, onChange }: Props) { - const { networks } = useContext(NetworkContext); - - // set default network if none selected - useEffect(() => { - if (!network) { - onChange(DEFAULT_NETWORK); - } - }, []); - - // @ADD_ACCOUNT_TODO: The difference in accountType is likely causing - // the absence of list. - const validNetworks = networks - // @ts-ignore CHANGE IN WALLETYPE OBJECT CAUSING accountType to error -> TODO: FIX accountType - .filter(options => isWalletFormatSupportedOnNetwork(options, accountType)) - .map(n => ({ label: n.name, value: n })); - - return ( -
- - - onChange(option.value.id)} - optionComponent={NetworkOption} - valueComponent={({ value: option }) => } - /> - -
- ); -} - -export default NetworkSelectDropdown; diff --git a/common/v2/components/NewAppReleaseModal.tsx b/common/v2/components/NewAppReleaseModal.tsx deleted file mode 100644 index 1ae6092b1d8..00000000000 --- a/common/v2/components/NewAppReleaseModal.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import React from 'react'; -import { Panel, Button, Icon, Typography } from '@mycrypto/ui'; -import styled from 'styled-components'; -import semver from 'semver'; - -import Modal from './Modal'; -import { BREAK_POINTS, COLORS } from 'v2/theme'; -import { GITHUB_RELEASE_NOTES_URL, OS, VERSION as currentVersion } from 'v2/config'; -import { getFeaturedOS } from 'v2/utils'; -import { AnalyticsService, ANALYTICS_CATEGORIES, GithubService } from 'v2/services/ApiService'; -import translate from 'v2/translations'; - -// Legacy -import closeIcon from 'common/assets/images/icn-close.svg'; -import updateIcon from 'common/assets/images/icn-update.svg'; -import updateImportantIcon from 'common/assets/images/icn-important-update.svg'; - -const { SCREEN_SM } = BREAK_POINTS; -const { PASTEL_RED } = COLORS; - -const MainPanel = styled(Panel)` - background-color: white; - padding: 50px 65px; - max-width: 770px; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - position: relative; - max-height: 90%; - overflow: auto; - - @media (max-width: ${SCREEN_SM}) { - padding: 40px 20px; - width: 90%; - } -`; - -const CloseButton = styled(Button)` - position: absolute; - right: 17px; - top: 9px; - - img { - width: 14px; - height: 14px; - } -`; - -const UpdateImg = styled.img` - width: 60px; - height: 60px; - margin-bottom: 15px; -`; - -const Header = styled(Typography)` - font-size: 32px; - font-weight: bold; - line-height: normal; - margin-top: 0; - margin-bottom: 15px; - color: ${props => props.theme.headline}; -`; - -interface DescriptionProps { - warning?: boolean; - noMargin?: boolean; -} - -// prettier-ignore -const Description = styled(Typography)` - margin-top: 5px; - font-weight: normal; - padding: ${props => (props.warning ? '0 30px 20px 30px' : '0 30px')}; - color: ${props => (props.warning ? PASTEL_RED : props.theme.text)}; - ${props => props.noMargin && 'margin: 0;'}; -`; - -interface ActionsWrapperProps { - marginTop?: string; -} - -// prettier-ignore -const ActionsWrapper = styled.div` - margin-top: ${props => (props.marginTop ? props.marginTop : '54px')}; - display: flex; - flex-direction: row; - justify-content: center; - - @media (max-width: ${SCREEN_SM}) { - flex-direction: column; - } -`; - -const ActionButton = styled(Button)` - font-size: 18px; - font-weight: normal; - margin-bottom: 15px; -`; - -const SecondaryActionButton = styled(ActionButton)` - margin-right: 50px; - - @media (max-width: ${SCREEN_SM}) { - margin-right: 0px; - } -`; - -const ReleaseLink = styled(Typography)` - word-break: break-all; -`; - -const WarningIcon = styled(Icon)` - margin-right: 5px; - vertical-align: middle; - - svg { - color: ${PASTEL_RED}; - } -`; - -const OSNames: { [key: string]: string } = { - [OS.WINDOWS]: 'Windows', - [OS.MAC]: 'macOS', - [OS.LINUX64]: 'Linux' -}; - -const featuredOS = getFeaturedOS(); - -interface State { - isOpen: boolean; - OSName: string; - newVersion?: string; - newVersionUrl?: string; - isCritical?: boolean; -} - -export default class NewAppReleaseModal extends React.PureComponent<{}, State> { - public state: State = { - isOpen: false, - OSName: OSNames[featuredOS] - }; - - public async componentDidMount() { - try { - const releasesInfo = await GithubService.instance.getReleasesInfo(); - const { version: newVersion, name, releaseUrls } = releasesInfo; - - const isCritical = name.toLowerCase().includes('[critical]'); - const newVersionUrl = releaseUrls[featuredOS]; - if (semver.lt(currentVersion, newVersion)) { - this.setState({ - isOpen: true, - newVersion, - newVersionUrl, - isCritical - }); - } - } catch (err) { - console.error('Failed to fetch latest release from GitHub:', err); - } - } - - public render() { - const { isOpen, isCritical } = this.state; - - return ( - isOpen && ( - - {isCritical ? this.getCriticalModal() : this.getNonCriticalModal()} - - ) - ); - } - - private getNonCriticalModal = () => { - const { OSName, newVersion } = this.state; - - return ( - <> - - Close - - -
{translate('APP_UPDATE_TITLE')}
- {translate('APP_UPDATE_BODY')} - - - {translate('APP_UPDATE_CANCEL')} - - - {translate('APP_UPDATE_CONFIRM', { $osName: OSName, $appVersion: `v${newVersion}` })} - - - - ); - }; - - private getCriticalModal = () => { - const { OSName, newVersion } = this.state; - - return ( - <> - -
{translate('APP_UPDATE_TITLE_CRITICAL')}
- - {translate('APP_UPDATE_WARNING')} - - {translate('APP_UPDATE_BODY_CRITICAL')} - - - {GITHUB_RELEASE_NOTES_URL}. - - - - - {translate('APP_UPDATE_CONFIRM', { $osName: OSName, $appVersion: `v${newVersion}` })} - - - - ); - }; - - private onClose = () => { - const { OSName, newVersion } = this.state; - this.setState({ isOpen: false }); - AnalyticsService.instance.track( - ANALYTICS_CATEGORIES.UPDATE_DESKTOP, - 'Not Right Now button clicked', - { current_version: currentVersion, new_version: newVersion, os: OSName } - ); - }; - - private downloadRelease = () => { - const { OSName, newVersion, newVersionUrl } = this.state; - window.open(newVersionUrl, '_self'); - AnalyticsService.instance.track( - ANALYTICS_CATEGORIES.UPDATE_DESKTOP, - 'Get New Version button clicked', - { current_version: currentVersion, new_version: newVersion, os: OSName } - ); - }; -} diff --git a/common/v2/components/NewTabLink.tsx b/common/v2/components/NewTabLink.tsx deleted file mode 100644 index 2b4db986739..00000000000 --- a/common/v2/components/NewTabLink.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; - -export interface AAttributes { - charset?: string; - className?: string; - coords?: string; - download?: string; - hreflang?: string; - media?: string; - name?: string; - rel?: - | 'alternate' - | 'author' - | 'bookmark' - | 'external' - | 'help' - | 'license' - | 'next' - | 'nofollow' - | 'noreferrer' - | 'noopener' - | 'prev' - | 'search' - | 'tag'; - rev?: string; - shape?: 'default' | 'rect' | 'circle' | 'poly'; - target?: '_blank' | '_parent' | '_self' | '_top'; - type?: string; - onClick?(ev: React.MouseEvent): void; -} - -interface NewTabLinkProps extends AAttributes { - href: string | undefined; - content?: - | React.ReactElement - | string - | number - | (string | number | React.ReactElement)[]; - children?: - | React.ReactElement - | string - | number - | (string | number | React.ReactElement)[]; -} - -export class NewTabLink extends React.Component { - public render() { - const { content, children, ...rest } = this.props; - return ( - - {content || children} - - ); - } -} - -export default NewTabLink; diff --git a/common/v2/components/Overlay.scss b/common/v2/components/Overlay.scss deleted file mode 100644 index b9c51820944..00000000000 --- a/common/v2/components/Overlay.scss +++ /dev/null @@ -1,13 +0,0 @@ -.Overlay { - position: fixed; - top: 81px; - right: 0; - z-index: 2; - width: 100vw; - height: 100vh; - background-color: rgba(0, 0, 0, 0.15); - - @media (min-width: 1000px) { - top: 123px; - } -} diff --git a/common/v2/components/Overlay.tsx b/common/v2/components/Overlay.tsx deleted file mode 100644 index d2d4b00f1e0..00000000000 --- a/common/v2/components/Overlay.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import './Overlay.scss'; - -interface Props { - onClick(): void; -} - -export default function Overlay({ onClick }: Props) { - return
; -} diff --git a/common/v2/components/PaperWallet.tsx b/common/v2/components/PaperWallet.tsx deleted file mode 100644 index f38ed60f2d5..00000000000 --- a/common/v2/components/PaperWallet.tsx +++ /dev/null @@ -1,337 +0,0 @@ -import React from 'react'; -import html2canvas from 'html2canvas'; -import jsPDF from 'jspdf'; -import styled from 'styled-components'; -import { Identicon } from '@mycrypto/ui'; - -import { QRCode } from 'v2/components'; -import { KNOWLEDGE_BASE_URL } from 'v2/config'; - -import walletIcon from 'common/assets/images/icn-hardware-wallet.svg'; -import myCryptoIcon from 'common/assets/images/logo-mycrypto-transparent.png'; - -interface PaperWalletWrapperProps { - isHidden?: boolean; -} - -// size of paper wallet is 2 times of design size -const paperWalletWidth: number = 1458; -const paperWalletHeight: number = 612; - -const PaperWalletWrapper = styled.div` - ${props => props.isHidden && `position: fixed; top: -${paperWalletHeight}px;`} - width: ${paperWalletWidth}px; - height: ${paperWalletHeight}px; - color: black; -`; - -interface PartProps { - hasRightBorder?: boolean; - hasLeftBorder?: boolean; - hasTopBorder?: boolean; -} - -const borderStyle = '6px dashed rgba(227, 237, 255, 0.3);'; -const Part = styled.div` - width: 486px; - height: 304px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - ${props => props.hasRightBorder && `border-right: ${borderStyle}`} - ${props => props.hasLeftBorder && `border-left: ${borderStyle}`} - ${props => props.hasTopBorder && `border-top: ${borderStyle}`} -`; - -interface PartWrapperProps { - rotateChildren?: boolean; -} -const PartWrapper = styled.div` - display: flex; - ${Part} { - ${props => props.rotateChildren && 'transform: rotate(180deg);'} - } -`; - -const WalletImage = styled.img` - width: 120px; - height: 120px; -`; - -const Resources = styled.p` - text-align: center; - font-size: 28px; - font-weight: normal; -`; - -const MyCryptoImage = styled.img` - width: 360px; - height: auto; -`; - -const LogoText = styled.p` - text-align: right; - font-size: 30px; - font-weight: normal; - margin-top: -14px; -`; - -const Notes = styled.p` - text-align: left; - font-size: 26px; - font-weight: normal; - margin: 0; -`; - -const InnerPartWrapper = styled.div` - width: 100%; - height: 100%; - padding: 30px; -`; - -const HorizontalLine = styled.hr` - border: 1px solid #b5bfc7; - width: 100%; - margin: 0 0 30px 0; -`; - -const IdenticonHeader = styled.div` - display: flex; - align-items: flex-start; -`; - -const IdenticonIcon = styled(Identicon)` - margin-right: 20px; - - img { - width: 68px; - height: 68px; - max-width: none; - } -`; - -const TextHeader = styled.div` - font-size: 26px; - font-weight: normal; - line-height: 26px; -`; - -interface TextSubHeaderProps { - noTopMargin?: boolean; -} -const TextSubHeader = styled.p` - font-size: 14px; - font-weight: normal; - margin: ${props => (props.noTopMargin ? '0' : '14px')} 0 0 0; -`; - -interface QRAddressWrapperProps { - isInversed: boolean; -} -const QRAddressWrapper = styled.div` - width: 244px; - height: 426px; - border: 6px solid ${props => (props.isInversed ? '#ef4747' : '#a7e07b')}; - display: flex; - flex-direction: column; - align-items: center; - transform: rotate(${props => props.isInversed && '-'}90deg) translate - ${props => (props.isInversed ? 'X' : 'Y')} (-100%); - transform-origin: 0 0; -`; - -const QRCodeWrapper = styled.div` - width: 200px; - height: 200px; -`; - -const QRCodeTitle = styled.div` - font-size: 36px; - text-align: center; - font-weight: bold; -`; - -const QRCodeSubTitle = styled.div` - min-height: 50px; - font-size: 14px; - text-align: center; - font-weight: normal; -`; - -const Path = styled.div` - min-height: 30px; - font-size: 14px; - text-align: center; - font-weight: normal; - margin-top: 6px; -`; - -const Address = styled.div` - font-size: 18px; - text-align: center; - font-weight: normal; - margin: 0px 32px 20px 32px; - line-height: 24px; - word-break: break-all; -`; - -interface MnemonicProps { - breakWords?: boolean; -} -const Mnemonic = styled.div` - font-size: 14px; - text-align: center; - font-weight: normal; - margin: 0px 16px 10px 16px; - line-height: 17px; - ${props => props.breakWords && 'word-break: break-all;'} -`; - -interface Props { - address: string; - privateKey?: string; - mnemonic?: string; - path?: string; - isHidden?: boolean; -} - -export default class PaperWallet extends React.Component { - private container: HTMLElement | null; - - public render() { - const { isHidden, address } = this.props; - - return ( - (this.container = el)}> - - - - - - - Your Identicon - - Look for this icon when sending funds to this wallet. - - - - - To deposit funds to this wallet, send ETH or tokens to the public address. - - - To check the balance of this wallet, go to www.mycrypto.com or the MyCrypto Desktop - app and unlock your wallet. - - - DO NOT SHARE YOUR PRIVATE KEY with anyone. You are responsible for keeping your - funds safe. MyCrypto cannot recover your funds for you. - - - - - - Notes: - {[...Array(7)].map((x: any, index: any) => ( - - ))} - - - {this.getQRAddressWrapper(true)} - - - -
- - Paper Wallet -
-
- - - For Resources and Help, Visit: {KNOWLEDGE_BASE_URL} - - {this.getQRAddressWrapper(false)} -
-
- ); - } - - public toPNG = async (scale: number = 1) => { - if (!this.container) { - return ''; - } - const canvas = await html2canvas(this.container, { scale }); - return canvas.toDataURL('image/png'); - }; - - public toPDF = async () => { - const { address } = this.props; - const png = await this.toPNG(3); - const pdf = new jsPDF('l', 'px'); - const pdfWidth = pdf.internal.pageSize.getWidth(); - const pdfHeight = pdf.internal.pageSize.getHeight(); - - // ratio setting size of paper wallet in PDF - const pdfImageSizeRation = 2.6; - const printedWidth = paperWalletWidth / pdfImageSizeRation; - const printedHeight = paperWalletHeight / pdfImageSizeRation; - - pdf.addImage( - png, - 'PNG', - (pdfWidth - printedWidth) / 2, - (pdfHeight - printedHeight) / 2, - printedWidth, - printedHeight, - undefined, - 'FAST' - ); - - pdf.save(`paper-wallet-${address.substr(0, 8)}`); - - //return pdf.output('datauristring'); - }; - - private getQRAddressWrapper = (isPrivate: boolean) => { - const { mnemonic, privateKey, path, address } = this.props; - let dataText: string = ''; - let data: string = ''; - if (isPrivate) { - if (mnemonic) { - data = mnemonic; - dataText = 'Your Mnemonic Phrase'; - } - if (privateKey) { - data = privateKey; - dataText = 'Your Private Key'; - } - } else { - data = address; - } - const subTitle = isPrivate - ? 'Keep this safe. Do not share.' - : 'Share this with your friends when they need to send you crypto!'; - - return ( - - - {isPrivate ? 'PRIVATE' : 'PUBLIC'} - {subTitle} - - - - {isPrivate && path && `Derived Path: ${path}`} - {isPrivate ? ( - - {dataText}: -
- {data} -
- ) : ( -
{data}
- )} -
-
- ); - }; -} diff --git a/common/v2/components/ParityQrSigner.scss b/common/v2/components/ParityQrSigner.scss deleted file mode 100644 index c1d34691cfe..00000000000 --- a/common/v2/components/ParityQrSigner.scss +++ /dev/null @@ -1,41 +0,0 @@ -@import 'common/sass/variables'; - -.ParityQrSigner { - display: flex; - z-index: 0; - align-items: center; - justify-content: center; - width: 230px; - height: 230px; - text-align: center; - margin: 0 auto 1.5em; - border-radius: 4px; - overflow: hidden; - - & img { - border: solid white 15px; - } - - &.is-disabled { - background: color(gray-lighter); - } - - &-error { - padding: $space; - font-size: $font-size-small; - color: color(gray-light); - - &-icon { - display: block; - font-size: 60px; - margin-bottom: $space-md; - opacity: 0.8; - } - } -} - -.Panel-description{ - &-parity { - height: 25px; - } -} \ No newline at end of file diff --git a/common/v2/components/ParityQrSigner.tsx b/common/v2/components/ParityQrSigner.tsx deleted file mode 100644 index 4c92a1b6430..00000000000 --- a/common/v2/components/ParityQrSigner.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from 'react'; -import classnames from 'classnames'; -import QrSigner from '@parity/qr-signer'; - -import translate from 'v2/translations'; -import { Spinner } from 'v2/components'; -import './ParityQrSigner.scss'; - -interface State { - webcamError: null | React.ReactElement; - isLoading: boolean; -} - -interface ScanProps { - scan: true; - onScan(data: string): void; -} - -interface ShowProps { - scan: false; - account: string; - data?: string; - rlp?: string; -} - -interface SharedProps { - size?: number; -} - -type Props = (ScanProps | ShowProps) & SharedProps; - -export default class ParityQrSigner extends React.PureComponent { - public state: State = { - webcamError: null, - isLoading: true - }; - - public componentDidMount() { - this.checkForWebcam(); - if (navigator.mediaDevices) { - navigator.mediaDevices.addEventListener('devicechange', this.checkForWebcam); - } - } - - public componentWillUnmount() { - if (navigator.mediaDevices && navigator.mediaDevices.ondevicechange) { - navigator.mediaDevices.removeEventListener('devicechange', this.checkForWebcam); - } - } - - public checkForWebcam = async () => { - if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { - try { - await navigator.mediaDevices.getUserMedia({ video: true }); - this.setState({ - webcamError: null, - isLoading: false - }); - } catch (e) { - const err = e as DOMException; - let errorMessage; - switch (err.name) { - case 'NotAllowedError': - case 'SecurityError': - errorMessage = translate('ADD_PARITY_ERROR_DISABLED'); - break; - case 'NotFoundError': - case 'OverconstrainedError': - errorMessage = translate('ADD_PARITY_ERROR_NO_CAM'); - break; - default: - errorMessage = translate('ADD_PARITY_ERROR_UNKNOWN'); - } - this.setState({ - webcamError: errorMessage, - isLoading: false - }); - } - } - }; - - public render() { - const { webcamError, isLoading } = this.state; - const size = this.props.size || 300; - - return ( -
- {isLoading ? ( -
- -
- ) : webcamError ? ( -
- - {webcamError} -
- ) : ( - - )} -
- ); - } -} diff --git a/common/v2/components/PrintPaperWalletButton.tsx b/common/v2/components/PrintPaperWalletButton.tsx deleted file mode 100644 index ef56999a0f7..00000000000 --- a/common/v2/components/PrintPaperWalletButton.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { Component } from 'react'; -import { Button } from '@mycrypto/ui'; -import styled from 'styled-components'; -import { addHexPrefix, toChecksumAddress } from 'ethereumjs-util'; - -import PaperWallet from './PaperWallet'; -import { Spinner } from 'v2/components'; - -// Legacy -import printerIcon from 'common/assets/images/icn-printer.svg'; - -const PrinterImage = styled.img` - width: 24px; - height: 24px; - margin-right: 10px; - pointer-events: none; - display: inline; -`; - -const StyledButton = styled(Button)` - font-size: 18px; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 16px; - - &:focus, - &:hover { - img { - filter: brightness(0) invert(1); - } - } -`; - -const ButtonWrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 100%; - height: 66px; -`; - -interface Props { - address: string; - printText: React.ReactElement; - privateKey?: string; - mnemonic?: string; - path?: string; - onPrintWalletClick?(): void; -} - -interface State { - paperWalletPdf: string; - loading: boolean; -} - -export default class PrintPaperWalletButton extends Component { - public state: State = { - paperWalletPdf: '', - loading: false - }; - - private paperWallet: PaperWallet | null; - - public render() { - const { address, path, printText, mnemonic, privateKey } = this.props; - const prefixedAddress = toChecksumAddress(addHexPrefix(address)); - - return ( - <> - - {this.state.loading ? ( - - ) : ( - - - {printText} - - )} - - (this.paperWallet = c)} - isHidden={true} - /> - - ); - } - - private handlePrintClick = async () => { - const { onPrintWalletClick } = this.props; - if (!this.paperWallet) { - return; - } - - this.setState({ loading: true }); - await this.paperWallet.toPDF(); - this.setState({ loading: false }); - - if (onPrintWalletClick) { - onPrintWalletClick(); - } - }; -} diff --git a/common/v2/components/QRCode.tsx b/common/v2/components/QRCode.tsx deleted file mode 100644 index e9d9d189417..00000000000 --- a/common/v2/components/QRCode.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import QRCodeLib from 'qrcode'; - -// FIXME should store limited amount if history -// data -> qr cache -const cache: { [key: string]: string } = {}; - -interface Props { - data: string; -} - -interface State { - qr?: string; -} - -export default class QRCode extends React.PureComponent { - public state: State = {}; - public UNSAFE_componentWillMount() { - // Start generating QR codes immediately - this.generateQrCode(this.props.data); - } - - public UNSAFE_componentWillReceiveProps(nextProps: Props) { - // Regenerate QR codes if props change - if (nextProps.data !== this.props.data) { - this.generateQrCode(nextProps.data); - } - } - - public render() { - const { qr } = this.state; - if (!qr) { - return null; - } - return ( - QR Code - ); - } - - private generateQrCode(value: string) { - if (cache[value]) { - this.setState({ qr: cache[value] }); - return; - } - QRCodeLib.toDataURL( - value, - { - color: { - dark: '#000', - light: '#fff' - }, - margin: 0, - errorCorrectionLevel: 'H' - }, - (err, qr) => { - if (err) { - return; - } - cache[value] = qr; - this.setState({ qr }); - } - ); - } -} diff --git a/common/v2/components/Query/GasQuery.tsx b/common/v2/components/Query/GasQuery.tsx deleted file mode 100644 index 985b253e8d0..00000000000 --- a/common/v2/components/Query/GasQuery.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { Query } from './Query'; - -interface Props { - withQuery({ gasLimit }: { gasLimit: string | null }): React.ReactElement; -} - -export const GasQuery: React.SFC = ({ withQuery }) => ( - withQuery({ gasLimit: limit || gaslimit })} - /> -); diff --git a/common/v2/components/Query/Query.tsx b/common/v2/components/Query/Query.tsx deleted file mode 100644 index 2d7bbfc5951..00000000000 --- a/common/v2/components/Query/Query.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; -import queryString from 'query-string'; -import { getParam } from 'v2/utils'; - -const parse = (location: RouteComponentProps['location']): Query => { - const searchStr = location.search; - const query = queryString.parse(searchStr); - return query; -}; - -interface IQueryResults { - [key: string]: string | null; -} - -export type Param = - | 'to' - | 'data' - | 'readOnly' - | 'tokenSymbol' - | 'value' - | 'gaslimit' - | 'limit' - | 'windowSize' - | 'windowStart' - | 'scheduleTimestamp' - | 'timeBounty' - | 'network'; - -interface Props extends RouteComponentProps<{}> { - params: Param[]; - withQuery(query: IQueryResults): React.ReactElement | null; -} - -interface Query { - [key: string]: string; -} - -export const Query = withRouter( - class extends React.Component { - public render() { - const { withQuery, params, location } = this.props; - const query = parse(location); - const res = params.reduce((obj, param) => ({ ...obj, [param]: getParam(query, param) }), {}); - - return withQuery(res); - } - } -); diff --git a/common/v2/components/Query/WhenQueryExists.tsx b/common/v2/components/Query/WhenQueryExists.tsx deleted file mode 100644 index 780441d8ed9..00000000000 --- a/common/v2/components/Query/WhenQueryExists.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; - -import { Query, Param } from './Query'; - -interface Props { - whenQueryExists: React.ReactElement | null; -} - -const params: Param[] = ['to', 'data', 'tokenSymbol', 'value', 'gaslimit', 'limit', 'readOnly']; - -export const WhenQueryExists: React.SFC = ({ whenQueryExists }) => ( - (Object.values(queries).some(v => !!v) ? whenQueryExists : null)} - /> -); diff --git a/common/v2/components/Query/index.ts b/common/v2/components/Query/index.ts deleted file mode 100644 index eca48c8621d..00000000000 --- a/common/v2/components/Query/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './GasQuery'; -export * from './Query'; -export * from './WhenQueryExists'; diff --git a/common/v2/components/ResponsiveLayout.tsx b/common/v2/components/ResponsiveLayout.tsx deleted file mode 100644 index 92c09c7def8..00000000000 --- a/common/v2/components/ResponsiveLayout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import Responsive from 'react-responsive'; - -import { BREAK_POINTS } from 'v2/theme'; - -export const Desktop = (props: any) => ; -export const Mobile = (props: any) => ; diff --git a/common/v2/components/RouterLink.tsx b/common/v2/components/RouterLink.tsx deleted file mode 100644 index 511f5dcd33f..00000000000 --- a/common/v2/components/RouterLink.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Link } from 'react-router-dom'; -import styled, { css } from 'styled-components'; - -interface RouterLinkProps { - fullwidth?: boolean; -} - -const StyledRouterLink = styled(Link)` - ${props => - props.fullwidth === true && - css` - width: 100%; - `} -`; - -export default StyledRouterLink; diff --git a/common/v2/components/RowDeleteOverlay.tsx b/common/v2/components/RowDeleteOverlay.tsx deleted file mode 100644 index fe6132824c5..00000000000 --- a/common/v2/components/RowDeleteOverlay.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -import { COLORS, BREAK_POINTS } from 'v2/theme'; -import { Button } from 'v2/components'; - -/* - Passed to CollapisableTable and Table by AccountList and AddressBook - It handles its own display to adapt to overlay on table row or over - StackCard. -*/ - -const TableOverlay = styled.div` - height: 100%; - max-height: 69px; - background-color: ${COLORS.GREY_DARKER}; - color: ${COLORS.WHITE}; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 1em; - @media (max-width: ${BREAK_POINTS.SCREEN_SM}) { - align-items: space-between; - flex-direction: column; - justify-content: start; - max-height: 100%; - } -`; - -const OverlayText = styled('span')` - color: ${COLORS.WHITE}; - flex-grow: 1; - overflow-wrap: break-word; - max-width: 70%; - @media (max-width: ${BREAK_POINTS.SCREEN_SM}) { - max-width: 100%; - } -`; - -const OverlayButtons = styled.div` - padding: 8px 0px; - & > * { - margin-left: 2ch; - font-size: 0.9em; - } - @media (max-width: ${BREAK_POINTS.SCREEN_SM}) { - align-self: flex-end; - } -`; - -const RowDeleteOverlay = (props: any) => ( - - {props.prompt} - - - - - -); - -export default RowDeleteOverlay; diff --git a/common/v2/components/SignTransactionWallets/Hardware.scss b/common/v2/components/SignTransactionWallets/Hardware.scss deleted file mode 100644 index efb7f35922d..00000000000 --- a/common/v2/components/SignTransactionWallets/Hardware.scss +++ /dev/null @@ -1,50 +0,0 @@ -.SignTransactionHardware-title { - font-family: Lato; - font-size: 32px; - font-weight: bold; - text-align: center; - margin-top: 40px; -} - -.SignTransactionHardware-instructions { - padding-top: 2em; - font-size: 18px; - line-height: 1.5; - text-align: center; -} - -.SignTransactionHardware-without-pass { - margin-top: 5em; -} - -.SignTransactionHardware-img { - display: flex; - justify-content: center; - margin: 3em; -} - -.SignTransactionLegder-description { - font-size: 18px; - line-height: 1.5; - text-align: center; - margin-bottom: 1em; -} - -.SignTransactionHardware-footer { - width: 100%; -} - -.SignTransactionHardware-button { - width: 100%; -} - -.SignTransactionHardware-help { - margin-top: 1em; - text-align: center; - font-size: 16px; -} - -.SignTransactionHardware-referal { - text-align: center; - font-size: 16px; -} diff --git a/common/v2/components/SignTransactionWallets/Hardware.tsx b/common/v2/components/SignTransactionWallets/Hardware.tsx deleted file mode 100644 index 21f85ffa5f7..00000000000 --- a/common/v2/components/SignTransactionWallets/Hardware.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -import { ExtendedAccount as IExtendedAccount, ITxReceipt, ITxObject, ISignedTx } from 'v2/types'; -import { WALLETS_CONFIG } from 'v2/config'; -import { makeTransaction } from 'v2/services/EthService'; -import { WalletFactory, HardwareWallet } from 'v2/services/WalletService'; -import { InlineMessage } from 'v2/components'; -import translate, { translateRaw } from 'v2/translations'; - -import './Hardware.scss'; - -export interface IDestructuredDPath { - dpath: string; - index: number; -} - -export const splitDPath = (fullDPath: string): IDestructuredDPath => { - /* - m/44'/60'/0'/0 => { dpath: "m/44'/60'/0'", index: "0" } - */ - const dPathArray = fullDPath.split('/'); - const index = dPathArray.pop() as string; - return { - dpath: dPathArray.join('/'), - index: parseInt(index, 10) - }; -}; - -export interface IProps { - walletIcon: any; - signerDescription: string; - senderAccount: IExtendedAccount; - rawTransaction: ITxObject; - onSuccess(receipt: ITxReceipt | ISignedTx): void; -} - -export default function HardwareSignTransaction({ - walletIcon, - signerDescription, - senderAccount, - rawTransaction, - onSuccess -}: IProps) { - const [isRequestingWalletUnlock, setIsRequestingWalletUnlock] = useState(false); - const [isWalletUnlocked, setIsWalletUnlocked] = useState(false); - const [isRequestingTxSignature, setIsRequestingTxSignature] = useState(false); - const [isTxSignatureRequestDenied, setIsTxSignatureRequestDenied] = useState(false); - const [wallet, setWallet] = useState(); - const SigningWalletService = WalletFactory(senderAccount.wallet); - - useEffect(() => { - // Unlock Wallet - const WalletLoginRequest = setInterval(() => { - if (!isWalletUnlocked && !isRequestingWalletUnlock) { - setIsRequestingWalletUnlock(true); - const dpathObject = splitDPath(senderAccount.dPath); - const walletObject = SigningWalletService.init( - senderAccount.address, - dpathObject.dpath, - dpathObject.index - ); - try { - SigningWalletService.getChainCode(dpathObject.dpath) - .then((_: any) => { - // User has connected device. - setIsRequestingWalletUnlock(false); - setIsWalletUnlocked(true); - setWallet(walletObject); - }) - .catch((_: any) => { - // User hasn't connected device or there was an error. Try again - setIsRequestingWalletUnlock(false); - }); - } catch (error) { - setIsRequestingWalletUnlock(false); - } - } - }, 3000); - return () => clearInterval(WalletLoginRequest); - }); - - useEffect(() => { - // Wallet has been unlocked. Attempting to sign tx now. - if (wallet && 'signRawTransaction' in wallet && !isRequestingTxSignature) { - setIsRequestingTxSignature(true); - const madeTx = makeTransaction(rawTransaction); - wallet - .signRawTransaction(madeTx) - .then((data: any) => { - // User approves tx. - setIsTxSignatureRequestDenied(false); - onSuccess(data); - }) - .catch((_: any) => { - // User denies tx, or tx times out. - setIsTxSignatureRequestDenied(true); - setIsRequestingTxSignature(false); - }); - } - }, [wallet, isRequestingTxSignature]); - return ( - <> -
- {translate('SIGN_TX_TITLE', { - $walletName: WALLETS_CONFIG[senderAccount.wallet].name || 'Hardware Wallet' - })} -
-
{signerDescription}
-
-
- -
-
- {translateRaw('SIGN_TX_EXPLANATION')} - {isTxSignatureRequestDenied && ( - - )} -
-
-
- {translate(senderAccount.wallet + '_HELP')} -
-
- {translate(senderAccount.wallet + '_REFERRAL')} -
-
-
- - ); -} diff --git a/common/v2/components/SignTransactionWallets/Keystore.scss b/common/v2/components/SignTransactionWallets/Keystore.scss deleted file mode 100644 index 248d0aae653..00000000000 --- a/common/v2/components/SignTransactionWallets/Keystore.scss +++ /dev/null @@ -1,63 +0,0 @@ -.SignTransactionKeystore-title { - height: 78px; - font-family: Lato; - font-size: 32px; - font-weight: bold; - text-align: center; - margin-top: 40px; -} - -.SignTransactionKeystore-instructions { - padding-top: 2em; - font-size: 18px; - line-height: 1.5; - text-align: center; -} - -.SignTransactionKeystore-error { - color: red; - text-align: center; -} - -.SignTransactionKeystore-without-pass { - margin-top: 5em; -} - -.SignTransactionKeystore-img { - display: flex; - justify-content: center; - margin: 1em; -} - -.SignTransactionKeystore-description { - font-size: 18px; - text-align: center; - margin-bottom: 1em; - display: flex; - justify-content: center; -} - -.SignTransactionKeystore-footer { - width: 100%; -} - -.SignTransactionKeystore-spinner { - display: flex; - justify-content: center; - width: 100%; -} - -.SignTransactionKeystore-button { - width: 100%; -} - -.SignTransactionKeystore-help { - margin-top: 1em; - text-align: center; - font-size: 16px; -} - -.SignTransactionKeystore-referal { - text-align: center; - font-size: 16px; -} diff --git a/common/v2/components/SignTransactionWallets/Keystore.tsx b/common/v2/components/SignTransactionWallets/Keystore.tsx deleted file mode 100644 index a1f4a101bad..00000000000 --- a/common/v2/components/SignTransactionWallets/Keystore.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import React, { Component } from 'react'; -import { ethers, utils } from 'ethers'; - -import { Spinner, Input } from 'v2/components'; -import translate, { translateRaw } from 'v2/translations'; -import { ISignComponentProps } from 'v2/types'; -import { isKeystorePassRequired } from 'v2/services/WalletService'; -import { WALLETS_CONFIG } from 'v2/config'; - -import './Keystore.scss'; -import PrivateKeyicon from 'common/assets/images/icn-privatekey-new.svg'; - -export interface KeystoreValueState { - file: string; - password: string; - filename: string | undefined; - valid: boolean | undefined; - walletState: WalletSigningState; - hasCorrectPassword: boolean | undefined; - isSigning: boolean; -} - -enum WalletSigningState { - READY, //when signerWallet is ready to sendTransaction - NOT_READY, //use when signerWallet rejects transaction - UNKNOWN //used upon component initialization when wallet status is not determined -} - -function isPassRequired(file: string): boolean { - let passReq = false; - try { - passReq = isKeystorePassRequired(file); - } catch (e) { - // TODO: communicate invalid file to user - } - return passReq; -} - -function isValidFile(rawFile: File): boolean { - const fileType = rawFile.type; - return fileType === '' || fileType === 'application/json'; -} - -export default class SignTransactionKeystore extends Component< - ISignComponentProps, - KeystoreValueState -> { - public state: KeystoreValueState = { - file: '', - password: '', - filename: undefined, - valid: undefined, - walletState: WalletSigningState.UNKNOWN, - hasCorrectPassword: undefined, - isSigning: false - }; - - public render() { - // const { isWalletPending } = this.props; - const isWalletPending = false; - const { file, password, filename, walletState, hasCorrectPassword, isSigning } = this.state; - const passReq = file ? isPassRequired(file) : true; - const unlockDisabled = !file || (passReq && !password); - - return ( - <> -
- {translate('SIGN_TX_TITLE', { $walletName: WALLETS_CONFIG.KEYSTORE_FILE.name })} -
-
-
-
-
- -
- - {walletState === WalletSigningState.NOT_READY ? ( -
- {translateRaw('SIGN_TX_KEYSTORE_WRONG_FILE')} -
- ) : null} - {hasCorrectPassword === false ? ( -
- {translateRaw('SIGN_TX_KEYSTORE_WRONG_PASSWORD')} -
- ) : null} - - - - - 0 : false} - className={`${file.length && isWalletPending ? 'hidden' : ''}`} - disabled={!file} - value={password} - onChange={this.onPasswordChange} - onKeyDown={this.onKeyDown} - placeholder={translateRaw('INPUT_PASSWORD_LABEL')} - type="password" - /> -
-
- {translateRaw('SIGN_TX_EXPLANATION')} -
-
- {isSigning ? ( - - ) : ( - - )} -
- - {WALLETS_CONFIG.KEYSTORE_FILE.helpLink && ( -
- {translate('SIGN_TX_HELP_LINK', { $helpLink: WALLETS_CONFIG.KEYSTORE_FILE.helpLink })} -
- )} -
- - ); - } - - private onKeyDown = (e: any) => { - if (e.keyCode === 13) { - this.unlock(e); - } - }; - - private unlock = async (e: React.SyntheticEvent) => { - e.preventDefault(); - e.stopPropagation(); - this.getPublicKey(); - }; - - private async getPublicKey() { - this.setState({ isSigning: true }); - await ethers.Wallet.fromEncryptedJson(this.state.file, this.state.password) - .then(wallet => { - const checkSumAddress = utils.getAddress(wallet.address); - this.checkPublicKeyMatchesCache(checkSumAddress); - }) - .catch(e => { - if (e) { - this.setState({ hasCorrectPassword: false, isSigning: false }); - } - }); - } - - private checkPublicKeyMatchesCache(walletAddres: string) { - const { senderAccount } = this.props; - const localCacheAddress = utils.getAddress(senderAccount.address); - const keystoreFileAddress = walletAddres; - - if (localCacheAddress === keystoreFileAddress) { - this.setState({ walletState: WalletSigningState.READY }); - this.signTransaction(); - } else { - this.setState({ walletState: WalletSigningState.NOT_READY }); - throw new Error('Incorrect Keystore File'); - } - } - - private async signTransaction() { - const { rawTransaction } = this.props; - const signerWallet = await ethers.Wallet.fromEncryptedJson( - this.state.file, - this.state.password - ); - const rawSignedTransaction: any = await signerWallet.sign(rawTransaction); - this.props.onSuccess(rawSignedTransaction); - } - private onPasswordChange = (e: any) => { - this.setState({ - password: e.target.value, - valid: this.state.file.length && e.target.value.length - }); - }; - - private handleFileSelection = (e: any) => { - const fileReader = new FileReader(); - const target = e.target; - const inputFile = target.files[0]; - const fileName = inputFile.name; - - fileReader.onload = () => { - const keystore = fileReader.result; - const passReq = isPassRequired(keystore as any); - - this.setState({ - file: keystore, - valid: (keystore as any).length && !passReq, - password: '', - filename: fileName - } as any); - }; - - if (isValidFile(inputFile)) { - fileReader.readAsText(inputFile, 'utf-8'); - } - }; -} diff --git a/common/v2/components/SignTransactionWallets/Ledger.tsx b/common/v2/components/SignTransactionWallets/Ledger.tsx deleted file mode 100644 index a31b0407a82..00000000000 --- a/common/v2/components/SignTransactionWallets/Ledger.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -import { ISignComponentProps } from 'v2/types'; -import { translateRaw } from 'v2/translations'; -import ledgerIcon from 'common/assets/images/icn-ledger-nano-large.svg'; -import HardwareSignTransaction from './Hardware'; - -export default function SignTransactionLedger({ - senderAccount, - rawTransaction, - onSuccess -}: ISignComponentProps) { - return ( - - ); -} diff --git a/common/v2/components/SignTransactionWallets/Mnemonic.tsx b/common/v2/components/SignTransactionWallets/Mnemonic.tsx deleted file mode 100644 index 7226f4884f1..00000000000 --- a/common/v2/components/SignTransactionWallets/Mnemonic.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { Component } from 'react'; -import { ethers, utils } from 'ethers'; -import { isValidMnemonic } from 'ethers/utils/hdnode'; - -import { ISignComponentProps } from 'v2/types'; -import translate, { translateRaw } from 'v2/translations'; -import { Spinner } from 'v2/components/Spinner'; -import { Input } from 'v2/components'; -import { WALLETS_CONFIG } from 'v2/config'; - -import PrivateKeyicon from 'common/assets/images/icn-privatekey-new.svg'; - -interface MnemonicValueState { - seed: string; - phrase: string; - pass: string; - walletState: WalletSigningState; - isSigning: boolean; -} - -enum WalletSigningState { - READY, //when signerWallet is ready to sendTransaction - NOT_READY, //use when signerWallet rejects transaction - UNKNOWN //used upon component initialization when wallet status is not determined -} - -export default class SignTransactionMnemonic extends Component< - ISignComponentProps, - MnemonicValueState -> { - public state: MnemonicValueState = { - seed: '', - phrase: '', - pass: '', - walletState: WalletSigningState.UNKNOWN, - isSigning: false - }; - - public render() { - const isWalletPending = false; - const { phrase, pass, walletState, isSigning } = this.state; - - const unlockDisabled = !phrase; - - return ( - <> -
- {translate('SIGN_TX_TITLE', { $walletName: WALLETS_CONFIG.MNEMONIC_PHRASE.name })} -
-
-
-
-
- -
- - {walletState === WalletSigningState.NOT_READY ? ( -
- The input Mnemonic Phrase's Public Address does not match Account's sender - Address, please try again. -
- ) : null} - - 0 ? isValidMnemonic(phrase) : false} - type="text" - onChange={this.handleMnemonicEnter} - placeholder="please enter mnemonic phrase" - value={phrase} - /> - - - 0 : false} - className={`${phrase.length && isWalletPending ? 'hidden' : ''}`} - disabled={!phrase} - value={pass} - onChange={this.onPasswordChange} - onKeyDown={this.onKeyDown} - placeholder={translateRaw('INPUT_PASSWORD_LABEL')} - type="password" - /> -
-
- {translateRaw('SIGN_TX_EXPLANATION')} -
-
- {isSigning ? ( - - ) : ( - - )} -
- - {WALLETS_CONFIG.MNEMONIC_PHRASE.helpLink && ( -
- {translate('SIGN_TX_HELP_LINK', { - $helpLink: WALLETS_CONFIG.MNEMONIC_PHRASE.helpLink - })} -
- )} -
- - ); - } - - private onKeyDown = (e: any) => { - if (e.keyCode === 13) { - this.unlock(e); - } - }; - - private unlock = async (e: React.SyntheticEvent) => { - e.preventDefault(); - e.stopPropagation(); - this.getPublicKey(); - }; - - private async getPublicKey() { - this.setState({ isSigning: true }); - try { - const { senderAccount } = this.props; - const wallet = await ethers.Wallet.fromMnemonic(this.state.phrase, senderAccount.dPath); - const checkSumAddress = utils.getAddress(wallet.address); - this.checkPublicKeyMatchesCache(checkSumAddress); - } catch (err) { - console.error(err); - } - } - - private checkPublicKeyMatchesCache(walletAddres: string) { - const { senderAccount } = this.props; - const localCacheAddress = utils.getAddress(senderAccount.address); - const mnemonicAddress = walletAddres; - - if (localCacheAddress === mnemonicAddress) { - this.setState({ walletState: WalletSigningState.READY }); - this.signTransaction(); - } else { - this.setState({ walletState: WalletSigningState.NOT_READY }); - throw new Error('Incorrect Mnemonic Phrase or DPath'); - } - } - - private async signTransaction() { - const { rawTransaction, senderAccount } = this.props; - - const signerWallet = await ethers.Wallet.fromMnemonic(this.state.phrase, senderAccount.dPath); - const rawSignedTransaction: any = await signerWallet.sign(rawTransaction); - this.props.onSuccess(rawSignedTransaction); - } - private onPasswordChange = (e: any) => { - this.setState({ - pass: e.target.value - }); - }; - - private handleMnemonicEnter = (e: any) => { - this.setState({ - phrase: e.target.value - }); - }; -} diff --git a/common/v2/components/SignTransactionWallets/Parity.tsx b/common/v2/components/SignTransactionWallets/Parity.tsx deleted file mode 100644 index e705e0cf261..00000000000 --- a/common/v2/components/SignTransactionWallets/Parity.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -import { ISignComponentProps } from 'v2/types'; - -export default function SignTransactionParity({ }: ISignComponentProps) { - return
Sign Transaction with Parity
; -} diff --git a/common/v2/components/SignTransactionWallets/PrivateKey.scss b/common/v2/components/SignTransactionWallets/PrivateKey.scss deleted file mode 100644 index 351e75c3036..00000000000 --- a/common/v2/components/SignTransactionWallets/PrivateKey.scss +++ /dev/null @@ -1,39 +0,0 @@ -.SignTransactionPrivateKey-title { - height: 78px; - font-family: Lato; - font-size: 32px; - font-weight: bold; - text-align: center; - margin-top: 40px; -} - -.SignTransactionPrivateKey-without-pass { - margin-top: 5em; -} - -.SignTransactionPrivateKey-img { - display: flex; - justify-content: center; - margin-top: 1em; -} - -.SignTransactionPrivateKey-description { - font-size: 18px; - line-height: 1.5; - text-align: center; - margin-bottom: 1em; -} - -.SignTransactionPrivateKey-footer { - width: 100%; -} - -.SignTransactionPrivateKey-button { - width: 100%; -} - -.SignTransactionPrivateKey-help { - margin-top: 1em; - text-align: center; - font-size: 16px; -} diff --git a/common/v2/components/SignTransactionWallets/PrivateKey.tsx b/common/v2/components/SignTransactionWallets/PrivateKey.tsx deleted file mode 100644 index 35434f00276..00000000000 --- a/common/v2/components/SignTransactionWallets/PrivateKey.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import React, { Component } from 'react'; -import { stripHexPrefix } from 'ethjs-util'; -import { Button } from '@mycrypto/ui'; - -import { TogglablePassword, Input } from 'v2/components'; -import { isValidPrivKey, isValidEncryptedPrivKey } from 'v2/services/EthService'; -import { ISignComponentProps } from 'v2/types'; -import translate, { translateRaw } from 'v2/translations'; -import { WALLETS_CONFIG } from 'v2/config'; - -import './PrivateKey.scss'; -import PrivateKeyicon from 'common/assets/images/icn-privatekey-new.svg'; - -export interface SignWithPrivKeyState { - key: string; - password: string; - valid: boolean; -} - -interface Validated { - fixedPkey: string; - isValidPkey: boolean; - isPassRequired: boolean; - valid: boolean; -} - -function validatePkeyAndPass(pkey: string, pass: string): Validated { - const fixedPkey = stripHexPrefix(pkey).trim(); - const validPkey = isValidPrivKey(fixedPkey); - const validEncPkey = isValidEncryptedPrivKey(fixedPkey); - const isValidPkey = validPkey || validEncPkey; - - let isValidPass = false; - - if (validPkey) { - isValidPass = true; - } else if (validEncPkey) { - isValidPass = pass.length > 0; - } - - return { - fixedPkey, - isValidPkey, - isPassRequired: validEncPkey, - valid: isValidPkey && isValidPass - }; -} - -export default class SignTransactionPrivateKey extends Component< - ISignComponentProps, - SignWithPrivKeyState -> { - public state = { - key: '', - password: '', - valid: false - }; - public render() { - const { key, password } = this.state; - const { isValidPkey, isPassRequired } = validatePkeyAndPass(key, password); - return ( - <> -
- {translate('SIGN_TX_TITLE', { $walletName: WALLETS_CONFIG.PRIVATE_KEY.name })} -
-
-
- -
-
- - this.unlock} - /> - - {isValidPkey && isPassRequired && ( - - )} -
-
- {translateRaw('SIGN_TX_EXPLANATION')} -
-
- - {WALLETS_CONFIG.PRIVATE_KEY.helpLink && ( -
- {translate('SIGN_TX_HELP_LINK', { $helpLink: WALLETS_CONFIG.PRIVATE_KEY.helpLink })} -
- )} -
-
- - ); - } - - private onPkeyChange = (e: React.FormEvent) => { - const pkey = e.currentTarget.value; - const pass = this.state.password; - const { fixedPkey, valid } = validatePkeyAndPass(pkey, pass); - - this.setState({ - key: fixedPkey, - valid - }); - }; - - private onPasswordChange = (e: React.FormEvent) => { - // NOTE: Textareas don't support password type, so we replace the value - // with an equal length number of dots. On change, we replace - const pkey = this.state.key; - const pass = e.currentTarget.value; - const { valid } = validatePkeyAndPass(pkey, pass); - - this.setState({ - password: pass, - valid - }); - }; - - private onKeyDown = (e: React.KeyboardEvent) => { - console.debug(e); - }; - - private unlock = async (e: React.SyntheticEvent) => { - e.preventDefault(); - e.stopPropagation(); - }; -} diff --git a/common/v2/components/SignTransactionWallets/SafeTmini.tsx b/common/v2/components/SignTransactionWallets/SafeTmini.tsx deleted file mode 100644 index 47b3e72340d..00000000000 --- a/common/v2/components/SignTransactionWallets/SafeTmini.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -import { ISignComponentProps } from 'v2/types'; -import { translateRaw } from 'v2/translations'; - -import HardwareSignTransaction from './Hardware'; -import SafeTIcon from 'common/assets/images/icn-safet-mini-new.svg'; - -export default function SignTransactionSafeT({ - senderAccount, - rawTransaction, - onSuccess -}: ISignComponentProps) { - return ( - - ); -} diff --git a/common/v2/components/SignTransactionWallets/Trezor.tsx b/common/v2/components/SignTransactionWallets/Trezor.tsx deleted file mode 100644 index bd1698f2ab5..00000000000 --- a/common/v2/components/SignTransactionWallets/Trezor.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -import { ISignComponentProps } from 'v2/types'; -import { translateRaw } from 'v2/translations'; -import ConnectTrezor from 'common/assets/images/icn-connect-trezor-new.svg'; -import HardwareSignTransaction from './Hardware'; - -export default function SignTransactionTrezor({ - senderAccount, - rawTransaction, - onSuccess -}: ISignComponentProps) { - return ( - - ); -} diff --git a/common/v2/components/SignTransactionWallets/Web3.scss b/common/v2/components/SignTransactionWallets/Web3.scss deleted file mode 100644 index 78d2bfe948f..00000000000 --- a/common/v2/components/SignTransactionWallets/Web3.scss +++ /dev/null @@ -1,62 +0,0 @@ -.SignTransactionWeb3-title { - font-family: Lato; - font-size: 32px; - font-weight: bold; - text-align: center; - margin-top: 40px; -} - -.SignTransactionWeb3-instructions { - padding-top: 2em; - font-size: 18px; - line-height: 1.5; - text-align: center; -} - -.SignTransactionWeb3-img { - display: flex; - justify-content: center; - margin: 2em; -} - -.SignTransactionWeb3-errors { - padding: 1em 0em; -} - -.SignTransactionWeb3-wrong-address { - color: red; - text-align: center; - word-break: break-all; -} - -.SignTransactionWeb3-rejection { - color: red; - text-align: center; -} - -.SignTransactionWeb3-wrong-network { - color: red; - text-align: center; -} - -.SignTransactionWeb3-description { - font-size: 18px; - line-height: 1.5; - text-align: center; - margin-bottom: 1em; -} - -.SignTransactionWeb3-footer { - width: 100%; - margin-top: 2em; -} - -.SignTransactionWeb3-button { - width: 100%; -} - -.SignTransactionWeb3-help { - margin-top: 1em; - text-align: center; - font-size: 16px; -} diff --git a/common/v2/components/SignTransactionWallets/Web3.tsx b/common/v2/components/SignTransactionWallets/Web3.tsx deleted file mode 100644 index 45f16e44e47..00000000000 --- a/common/v2/components/SignTransactionWallets/Web3.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import React, { Component } from 'react'; -import { ethers, utils } from 'ethers'; -import { Web3Provider } from 'ethers/providers/web3-provider'; - -import { WALLETS_CONFIG } from 'v2/config'; -import { ISignComponentProps } from 'v2/types'; -import translate, { translateRaw } from 'v2/translations'; -import { withContext, getWeb3Config } from 'v2/utils'; -import { getNetworkByChainId, INetworkContext, NetworkContext } from 'v2/services/Store'; -import './Web3.scss'; - -declare global { - interface Window { - ethereum?: any; - web3: any; - Web3Provider: ethers.providers.Web3Provider; - Web3Signer: Web3Provider; - } -} - -enum WalletSigningState { - READY, //when signerWallet is ready to sendTransaction - NOT_READY, //use when signerWallet rejects transaction - UNKNOWN //used upon component initialization when wallet status is not determined -} - -interface Web3UserState { - account: string | undefined; - network: number | undefined; - accountMatches: boolean; - networkMatches: boolean; - submitting: boolean; - walletState: WalletSigningState; -} - -const ethereumProvider = window.ethereum; -let web3Provider: ethers.providers.Web3Provider; - -async function getWeb3Provider() { - await ethereumProvider.enable(); - return new ethers.providers.Web3Provider(ethereumProvider); -} - -class SignTransactionWeb3 extends Component { - public state: Web3UserState = { - account: undefined, - network: undefined, - accountMatches: false, - networkMatches: false, - submitting: false, - walletState: WalletSigningState.UNKNOWN - }; - - constructor(props: ISignComponentProps & INetworkContext) { - super(props); - this.getWeb3Account = this.getWeb3Account.bind(this); - } - - public async initProvider() { - web3Provider = await getWeb3Provider(); - - if (ethereumProvider) { - this.getWeb3Account(); - this.watchForAccountChanges(ethereumProvider); - } else { - throw Error('No web3 found'); - } - } - - public componentWillMount() { - this.initProvider(); - } - - public render() { - const { senderAccount, rawTransaction, networks } = this.props; - const detectedNetwork = getNetworkByChainId(rawTransaction.chainId, networks); - const networkName = detectedNetwork ? detectedNetwork.name : 'Unknown Network'; - const walletConfig = getWeb3Config(); - const { accountMatches, networkMatches, walletState, submitting } = this.state; - - return ( - <> -
- {translate('SIGN_TX_TITLE', { - $walletName: walletConfig.name || WALLETS_CONFIG.WEB3.name - })} -
-
- {translate('SIGN_TX_WEB3_PROMPT', { - $walletName: walletConfig.name || WALLETS_CONFIG.WEB3.name - })} -
-
- -
- {walletState === WalletSigningState.NOT_READY ? ( -
{translate('SIGN_TX_WEB3_REJECTED')}
- ) : null} - -
-
- {!networkMatches && ( -
- {translate('SIGN_TX_WEB3_FAILED_NETWORK', { - $walletName: walletConfig.name, - $networkName: networkName - })} -
- )} - {!accountMatches && ( -
- {translate('SIGN_TX_WEB3_FAILED_ACCOUNT', { - $walletName: walletConfig.name, - $address: senderAccount.address - })} -
- )} -
- {submitting && translate('SIGN_TX_SUBMITTING_PENDING')} -
- {translateRaw('SIGN_TX_EXPLANATION')} -
-
- {walletConfig.helpLink && ( -
- {translate('SIGN_TX_HELP_LINK', { $helpLink: walletConfig.helpLink })} -
- )} -
-
- - ); - } - - private async getWeb3Account() { - if (!Web3Provider) { - return; - } - - const web3Signer = web3Provider.getSigner(); - const web3Address = await web3Signer.getAddress(); - const checksumAddress = utils.getAddress(web3Address); - - this.setState({ account: checksumAddress }); - this.getWeb3Network(); - this.checkAddressMatches(checksumAddress); - } - - private async getWeb3Network() { - if (!Web3Provider) { - return; - } - - const web3Network = await web3Provider.getNetwork(); - this.setState({ network: web3Network.chainId }); - this.checkNetworkMatches(web3Network); - } - - private checkAddressMatches(Web3Address: string) { - const { senderAccount } = this.props; - const desiredAddress = utils.getAddress(senderAccount.address); - this.setState({ accountMatches: Web3Address === desiredAddress }); - } - - private checkNetworkMatches(Web3Network: ethers.utils.Network) { - const { - network: { name: networkName }, - networks - } = this.props; - const getWeb3NetworkbyChainId = getNetworkByChainId(Web3Network.chainId, networks); - if (!getWeb3NetworkbyChainId) { - return; - } - - const localCacheSenderNetwork = networkName; - if (getWeb3NetworkbyChainId.name === localCacheSenderNetwork) { - this.setState({ networkMatches: true }); - this.maybeSendTransaction(); - } else { - this.setState({ networkMatches: false }); - } - } - - private watchForAccountChanges(ethereum: NonNullable) { - ethereum.on('accountsChanged', this.getWeb3Account); - } - - private async maybeSendTransaction() { - const { rawTransaction, onSuccess } = this.props; - this.setState({ submitting: true }); - if (!this.state.accountMatches || !this.state.networkMatches) { - return; - } - - this.setState({ walletState: WalletSigningState.READY }); - const signerWallet = web3Provider.getSigner(); - - try { - signerWallet.sendUncheckedTransaction(rawTransaction).then(txHash => { - web3Provider.getTransactionReceipt(txHash).then(output => { - this.setState({ submitting: false }); - onSuccess(output !== null ? output : txHash); - }); - }); - } catch (err) { - console.debug(`[SignTransactionWeb3] ${err}`); - if (err.message.includes('User denied transaction signature')) { - this.setState({ walletState: WalletSigningState.NOT_READY }); - } - } - } -} - -export default withContext(NetworkContext)(SignTransactionWeb3); diff --git a/common/v2/components/SignTransactionWallets/index.ts b/common/v2/components/SignTransactionWallets/index.ts deleted file mode 100644 index d1969bc3a35..00000000000 --- a/common/v2/components/SignTransactionWallets/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { default as SignTransactionKeystore } from './Keystore'; -export { default as SignTransactionLedger } from './Ledger'; -export { default as SignTransactionWeb3 } from './Web3'; -export { default as SignTransactionMnemonic } from './Mnemonic'; -export { default as SignTransactionParity } from './Parity'; -export { default as SignTransactionPrivateKey } from './PrivateKey'; -export { default as SignTransactionSafeT } from './SafeTmini'; -export { default as SignTransactionTrezor } from './Trezor'; -export { default as HardwareSignTransaction } from './Hardware'; diff --git a/common/v2/components/Spinner.scss b/common/v2/components/Spinner.scss deleted file mode 100644 index b47b1c3df7d..00000000000 --- a/common/v2/components/Spinner.scss +++ /dev/null @@ -1,54 +0,0 @@ -@import 'common/sass/variables'; - -.Spinner { - animation: rotate 1.2s linear infinite; - - &-x1 { - height: 1em; - width: 1em; - } - - &-x2 { - height: 2em; - width: 2em; - } - - &-x3 { - height: 3em; - width: 3em; - } - - &-x4 { - height: 4em; - width: 4em; - } - - &-x5 { - height: 5em; - width: 5em; - } - - & .path { - stroke-linecap: round; - stroke-dasharray: 90, 150; - stroke-dashoffset: 0; - } - - &-light { - & .path { - stroke: color(loader-light); - } - } - - &-dark { - & .path { - stroke: #7c9ec3; - } - } -} - -@keyframes rotate { - 100% { - transform: rotate(360deg); - } -} diff --git a/common/v2/components/Spinner.tsx b/common/v2/components/Spinner.tsx deleted file mode 100644 index a1da40086f3..00000000000 --- a/common/v2/components/Spinner.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import './Spinner.scss'; - -type Size = 'x1' | 'x2' | 'x3' | 'x4' | 'x5'; - -interface SpinnerProps { - size?: Size; - light?: boolean; -} - -export const Spinner = ({ size = 'x1', light = false }: SpinnerProps) => { - const color = light ? 'Spinner-light' : 'Spinner-dark'; - return ( - - - - ); -}; - -export default Spinner; diff --git a/common/v2/components/StackedCard.tsx b/common/v2/components/StackedCard.tsx deleted file mode 100644 index f940b04137e..00000000000 --- a/common/v2/components/StackedCard.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import React, { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react'; -import styled, { StyledComponentClass, css } from 'styled-components'; -import { Theme, scale } from '@mycrypto/ui'; - -import Typography from './Typography'; - -type StackedCardEntry = string | ReactNode; - -export interface StackedCardData { - heading: ReactNode; - entries: StackedCardEntry[][]; - icons?: ReactNode[]; -} - -type Props = StackedCardData; - -const StackedCardContainer = styled.section` - padding: 0.9375em; - border-bottom: 0.0625em solid #dde3ee; -`; - -const StackedCardHead = styled.section` - display: flex; - align-items: center; - justify-content: space-between; -`; - -const StackedCardHeading = styled(Typography)` - display: flex; - align-items: center; - margin: 0; - font-weight: bold; - line-height: 1.31; - font-size: ${scale(1)}; -`; - -StackedCardHeading.defaultProps = { - as: 'header' -}; - -// Arbitrary positioning of icons to respect AddressBook and AccountList designs -const StackedCardIcons = styled.div` - height: 100%; - display: flex; - justify-content: flex-end; - align-items: center; - > * { - width: 15px; - } - ${props => - props.qty && - css` - width: calc(15px * ${props.qty || 1}); - `} - ${(props: { qty: number }) => - props.qty === 2 && - css` - > *:first-child { - margin-bottom: -2px; - } - > *:last-child { - margin-left: 8px; - } - `} -`; - -// Arbitrary left margin to allow AccountList rows to align to -// the Identicon + label display. -// @TODO: remove arbitrary style. -const StackedCardBody = styled.div` - display: flex; - flex-direction: column; - margin-top: 1.5625em; - margin-left: calc(14px + 2em); -`; - -const StackedCardEntry = styled.dl` - display: flex; - flex-direction: row; - align-items: center; -`; - -type StyledHTMLElement = StyledComponentClass< - DetailedHTMLProps, HTMLElement>, - Theme ->; - -const StackedCardLabel = styled(Typography)` - flex: 0.7; - margin: 0; - color: ${props => props.theme.cardText}; - letter-spacing: 0.106875em; - text-transform: uppercase; - font-weight: bold; - font-size: 0.856em; - line-height: 1em; -` as StyledHTMLElement; - -StackedCardLabel.defaultProps = { - as: 'dt' -}; - -const StackedCardValue = styled(Typography)` - flex: 1; - margin: 0; -` as StyledHTMLElement; - -StackedCardValue.defaultProps = { - as: 'dd' -}; - -export const StackedCard = ({ heading, icons = [], entries, ...rest }: Props) => { - return ( - - - {heading} - {icons} - - - {entries.map(([label, value], index) => ( - - {label} - {value} - - ))} - - - ); -}; - -export default StackedCard; diff --git a/common/v2/components/Table.tsx b/common/v2/components/Table.tsx deleted file mode 100644 index ac6f834d365..00000000000 --- a/common/v2/components/Table.tsx +++ /dev/null @@ -1,367 +0,0 @@ -/* - Imported and modified from @mycrypto/ui to style headers - In order to style the headers particularyly the Icon caret -*/ - -import React, { - ClassAttributes, - Component, - DetailedHTMLProps, - ReactNode, - TdHTMLAttributes, - ThHTMLAttributes -} from 'react'; -import styled, { StyledComponentClass } from 'styled-components'; -import { Theme } from '@mycrypto/ui'; - -import { noOp } from 'v2/utils'; -import { default as Typography } from './Typography'; -import { default as IconArrow } from './IconArrow'; - -export interface TableGroup { - title: string; - entries: ReactNode[][]; - offset?: number; -} - -export interface TableConfig { - sortableColumn?: string | null; - hiddenHeadings?: (string | JSX.Element)[]; //Hack to allow a head to include JSX.Elements - reversedColumns?: string[]; - sortFunction?(a: any, b: any): number; -} - -export interface TableContent { - body: ReactNode[][]; - groups?: TableGroup[]; -} - -export interface TableData extends TableContent { - head: (string | JSX.Element)[]; - overlay?: ReactNode; - overlayRows?: number[]; - config?: TableConfig; -} - -export enum ColumnDirections { - Forward, - Reverse -} - -type Props = TableData; - -interface State { - collapsedGroups: { - [title: string]: boolean; - }; - sortedColumnDirection: ColumnDirections; -} - -interface CellProps { - isReversed?: boolean; -} - -const sharedCellProperties = ({ isReversed }: CellProps) => ` - min-width: .75em; - padding: .75em; - text-align: ${isReversed ? 'right' : 'left'}; -`; - -const TableHead = styled.tr` - border-top: 0.0625em solid ${props => props.theme.tableHeadBorder}; - border-bottom: 0.0625em solid ${props => props.theme.tableHeadBorder}; - background: ${props => props.theme.tableHeadBackground}; - font-size: 0.9em; -`; - -interface HeadingProps extends CellProps { - isSortable?: boolean; - isHidden?: boolean; -} - -const TableHeading = styled(Typography)` - ${sharedCellProperties} - color: ${props => props.theme.headline}; - font-weight: normal; - text-transform: uppercase; - letter-spacing: 0.0625em; - cursor: ${props => (props.isSortable ? 'pointer' : 'inherit')} - ${props => - props.isHidden && - ` - position: fixed; - top: -9999em; - left: -9999em; - `}; -` as StyledComponentClass< - ClassAttributes & - ThHTMLAttributes & - HeadingProps, - Theme ->; - -TableHeading.defaultProps = { - as: 'th' -}; - -const TableRow = styled.tr` - border-bottom: 0.0625em solid ${props => props.theme.tableRowBorder}; -`; - -const TableGroupHead = styled(TableRow)` - text-transform: uppercase; - cursor: pointer; -`; - -const TableCell = styled(Typography)` - ${sharedCellProperties}; -` as StyledComponentClass< - DetailedHTMLProps< - TdHTMLAttributes & CellProps, - HTMLTableDataCellElement - >, - Theme ->; - -TableCell.defaultProps = { - as: 'td' -}; - -export const defaultColumnSort = (a: any, b: any): number => { - try { - const aText = a.props.children; - const bText = b.props.children; - - return aText.localeCompare(bText); - } catch (error) { - throw new Error(`The default column sort of
expects either a string or a single-nested React element for cell contents. - For anything else, provide a custom sortFunction in the config.`); - } -}; - -export const getSortedRows = ( - head: any[], - body: ReactNode[][], - config: TableConfig, - sortedColumnDirection: ColumnDirections -): ReactNode[][] => { - const { sortableColumn, sortFunction = defaultColumnSort } = config; - // Determine which column to order. - const sortableColumnIndex = head.indexOf(sortableColumn); - // Create an array containing the data from each row in the specified column. - const sortableColumnEntries = body - .map(row => row[sortableColumnIndex]) - .map(entry => - // If the entry is a string, wrap it. - typeof entry === 'string' ? {entry} : entry - ); - // Rearrange that array based on the selected sort. - const sortedColumnEntries = [...sortableColumnEntries].sort(sortFunction); - // Translate the new order into the indexes of the original order to determine the change. - const sortedColumnIndices = sortedColumnEntries.map(sortedEntry => - sortableColumnEntries.indexOf(sortedEntry) - ); - // Potentially reverse the new order depending on the sort direction. - const finalSortedColumnIndices = - sortedColumnDirection === ColumnDirections.Forward - ? sortedColumnIndices - : sortedColumnIndices.reverse(); - // Apply the new order to all of the rows. - const sortedRows = finalSortedColumnIndices.map(index => body[index]); - - return sortedRows; -}; - -class AbstractTable extends Component { - public static defaultProps = { - head: [], - body: [], - groups: [], - config: {} - }; - - public state: State = { - collapsedGroups: {}, - sortedColumnDirection: ColumnDirections.Forward - }; - - public componentDidMount() { - this.verifyTableLayout(); - } - - public render() { - const { head, config, overlay, overlayRows, ...rest } = this.props; - const { collapsedGroups, sortedColumnDirection } = this.state; - const { body, groups } = this.getSortedLayout(); - - const isReversedColumn = (heading: any) => - config && config.reversedColumns && config.reversedColumns.includes(heading); - - return ( -
- - - {head.map((heading, index) => { - const isSortableColumn = config && config.sortableColumn === heading; - const isHiddenHeading = - config && config.hiddenHeadings && config.hiddenHeadings.includes(heading); - - return ( - - {heading} - {isSortableColumn && ( - - )} - - ); - })} - - - - {/* Ungrouped rows are placed on top of grouped rows. */} - {body.map((row, rowIndex) => ( - - {overlay && overlayRows!.includes(rowIndex) ? ( - // TODO: Solve jump in th width when the overlay is toggled. - - ) : ( - row.map((cell, cellIndex) => ( - - {cell} - - )) - )} - - ))} - {groups!.map(({ title, entries, offset = 0 }) => ( - - - {/* Enter ghost cells to facilitate the offset. */} - {Array.from({ length: offset }, (_, index) => ( - -
{overlay} - ))} - - {title} - - - - {/* Display group rows if not collapsed. */} - {!collapsedGroups[title] && - entries.map((row, rowIndex) => ( - - {row.map((cell, cellIndex) => ( - - {cell} - - ))} - - ))} - - ))} -
- ); - } - - private readonly verifyTableLayout = () => { - const { head, body, groups, config } = this.props; - const columnCount = head.length; - - if (columnCount === 0) { - throw new Error('A must have at least one column.'); - } - - body.forEach((row, index) => { - if (row.length !== columnCount) { - throw new Error(`Unbalanced row found in
at position ${index}.`); - } - }); - - groups!.forEach(({ title, entries, offset }) => { - if (!title || title === '') { - throw new Error(`Untitled group in
-- all table groups must have a title.`); - } - - entries.forEach((row, index) => { - if (row.length !== columnCount) { - throw new Error( - `Unbalanced row in group "${title}" found in
at position ${index}.` - ); - } - }); - - if (offset && offset > columnCount - 1) { - throw new Error(`Bad offset in group "${title}" found in
.`); - } - }); - - const { sortableColumn, hiddenHeadings } = config!; - - if (sortableColumn) { - const sortedColumnExists = head.includes(sortableColumn); - - if (!sortedColumnExists) { - throw new Error(`Nonexistent sortable column provided to
.`); - } - } - - if (hiddenHeadings) { - hiddenHeadings.forEach(heading => { - if (!head.includes(heading)) { - throw new Error(`Unused heading ${heading} found in hiddenHeadings in
`); - } - }); - } - }; - - private readonly toggleCollapseGroup = (title: string) => - this.setState(prevState => ({ - collapsedGroups: { - ...prevState.collapsedGroups, - [title]: !prevState.collapsedGroups[title] - } - })); - - private readonly toggleSortedColumnDirection = () => - this.setState(prevState => ({ - sortedColumnDirection: - prevState.sortedColumnDirection === ColumnDirections.Forward - ? ColumnDirections.Reverse - : ColumnDirections.Forward - })); - - private readonly getSortedLayout = (): TableContent => { - const { head, body, groups, config } = this.props; - const { sortedColumnDirection } = this.state; - - return config && config.sortableColumn - ? { - body: getSortedRows(head, body, config, sortedColumnDirection), - groups: groups!.map(group => ({ - ...group, - entries: getSortedRows(head, group.entries, config, sortedColumnDirection) - })) - } - : { body, groups }; - }; -} - -export const Table = styled(AbstractTable)` - width: 100%; - border-collapse: collapse; - border-spacing: 0; -`; - -export default Table; diff --git a/common/v2/components/TabsNav.tsx b/common/v2/components/TabsNav.tsx deleted file mode 100644 index 1894bcc388e..00000000000 --- a/common/v2/components/TabsNav.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { Children, Component, ReactNode } from 'react'; - -import styled from 'styled-components'; -import { Typography } from '@mycrypto/ui'; - -interface Props { - children: ReactNode; -} - -const TabsContainer = styled.ul` - display: flex; - text-align: center; - margin: 0; - background-color: #163150; -`; - -const Tab = styled.li` - list-style: none; - flex: 1; - padding-top: 1.3125em; - padding-bottom: 1.5em; - p { - margin: 0; - /* stylelint-disable max-nesting-depth */ - a { - color: white; - letter-spacing: 1.1px; /* stylelint-disable-line unit-whitelist */ - padding-bottom: 0.15625em; - - :hover, - :focus { - color: white; - border-bottom: ${props => '.125em solid' + props.theme.primary}; - } - } - } -`; - -export default class TabsNav extends Component { - public render() { - const { children } = this.props; - - return ( - - {Children.map(children, child => ( - - {child} - - ))} - - ); - } -} diff --git a/common/v2/components/TextArea.tsx b/common/v2/components/TextArea.tsx deleted file mode 100644 index d96a7215e01..00000000000 --- a/common/v2/components/TextArea.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { HTMLProps } from 'react'; -import classnames from 'classnames'; - -import './Input.scss'; - -interface State { - hasBlurred: boolean; - /** - * @description when the input has not had any values inputted yet - * e.g. "Pristine" condition - */ - isStateless: boolean; -} - -interface OwnProps extends HTMLProps { - isValid: boolean; - showValidAsPlain?: boolean; -} - -class TextArea extends React.Component { - public state: State = { - hasBlurred: false, - isStateless: true - }; - - public render() { - const { showValidAsPlain, isValid, ...htmlProps } = this.props; - const classname = classnames( - this.props.className, - 'input-group-input', - this.state.isStateless ? '' : isValid ? (showValidAsPlain ? '' : '') : `invalid`, - this.state.hasBlurred && 'has-blurred' - ); - - return ( -