Merge branch 'development' of github.com:gnosis/safe-react into development

This commit is contained in:
Mati Dastugue 2021-03-01 10:59:32 -03:00
commit 5e1f380968
42 changed files with 3576 additions and 1945 deletions

141
.github/workflows/deploy-ewc.yml vendored Normal file
View File

@ -0,0 +1,141 @@
name: Deploy to EWC network
# Run on pushes to master
on:
push:
branches:
- master
# Launches build when release is published
release:
types: [published]
env:
REPO_NAME_ALPHANUMERIC: safereact
REACT_APP_NETWORK: 'ewc'
STAGING_BUCKET_NAME: ${{ secrets.STAGING_EWC_BUCKET_NAME }}
REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN_EWC }}
REACT_APP_GOOGLE_ANALYTICS: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID_EWC }}
REACT_APP_GNOSIS_APPS_URL: ${{ secrets.REACT_APP_GNOSIS_APPS_URL_PROD }}
jobs:
debug:
name: Debug
runs-on: ubuntu-latest
steps:
- name: Dump env
run: env | sort
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
deploy:
name: Deployment
runs-on: ubuntu-latest
steps:
- name: Remove broken apt repos [Ubuntu]
if: matrix.os == 'ubuntu-latest'
run: |
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 14
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- run: |
sudo apt-get update
sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
pip install awscli --upgrade --user
# Due to some dependencies yarn may randomly throw an error about invalid cache
# This approach is taken from https://github.com/yarnpkg/yarn/issues/7212#issuecomment-506155894 to fix the issue
# Another approach is to install with flag --network-concurrency 1, but this will make the installation pretty slow (default value is 8)
mkdir .yarncache
yarn install --cache-folder ./.yarncache --frozen-lockfile
rm -rf .yarncache
yarn cache clean
# Set production flag
- name: Set production flag for tag build
run: echo "REACT_APP_ENV='production'" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v')
- name: Build ${{ env.REACT_APP_NETWORK }} app
run: yarn build
env:
PUBLIC_URL: './'
REACT_APP_FORTMATIC_KEY: ${{ secrets.REACT_APP_FORTMATIC_KEY }}
REACT_APP_INFURA_TOKEN: ${{ secrets.REACT_APP_INFURA_TOKEN }}
REACT_APP_PORTIS_ID: ${{ secrets.REACT_APP_PORTIS_ID }}
REACT_APP_INTERCOM_ID: ${{ secrets.REACT_APP_INTERCOM_ID }}
REACT_APP_IPFS_GATEWAY: ${{ secrets.REACT_APP_IPFS_GATEWAY }}
# Script to deploy Pull Requests
- run: bash ./scripts/github/deploy_pull_request.sh
if: success() && github.event.number
env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
PR_NUMBER: ${{ github.event.number }}
REVIEW_BUCKET_NAME: ${{ secrets.AWS_REVIEW_BUCKET_NAME }}
REACT_APP_NETWORK: ${{ env.REACT_APP_NETWORK }}
- name: 'PRaul: Comment PR with app URLs'
uses: mshick/add-pr-comment@v1
with:
message: |
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
repo-token: ${{ secrets.GITHUB_TOKEN }}
repo-token-user-login: 'github-actions[bot]'
allow-repeats: true
if: success() && github.event.number
env:
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
# Script to deploy to development environment
# EWC build is never created in development branch
# Script to deploy to staging environment
- name: 'Deploy to S3: Staging'
if: github.ref == 'refs/heads/master'
run: aws s3 sync build s3://${{ secrets.STAGING_BUCKET_NAME }}/current/app --delete
# Script to upload release files
- run: bash ./scripts/github/deploy_release.sh
if: startsWith(github.ref, 'refs/tags/v')
env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
PR_NUMBER: ${{ github.event.number }}
REVIEW_BUCKET_NAME: ${{ secrets.AWS_REVIEW_BUCKET_NAME }}
REACT_APP_NETWORK: ${{ env.REACT_APP_NETWORK }}
VERSION_TAG: ${{ github.event.release.tag_name }}
# Script to prepare production deployments
- run: bash ./scripts/github/prepare_production_deployment.sh
if: success() && startsWith(github.ref, 'refs/tags/v')
env:
PROD_DEPLOYMENT_HOOK_TOKEN: ${{ secrets.PROD_DEPLOYMENT_HOOK_TOKEN }}
PROD_DEPLOYMENT_HOOK_URL: ${{ secrets.PROD_DEPLOYMENT_HOOK_URL }}
VERSION_TAG: ${{ github.event.release.tag_name }}
# Upload Sentry source maps when sending to staging or production
- run: yarn sentry-upload-sourcemaps
if: success() && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG}}
SENTRY_PROJECT: gnosis-safe-multisig-${{ env.REACT_APP_NETWORK }}

123
.github/workflows/deploy-mainnet.yml vendored Normal file
View File

@ -0,0 +1,123 @@
name: Deploy to Mainnet network
# Run on pushes to master
on:
push:
branches:
- master
# Launches build when release is published
release:
types: [published]
env:
REPO_NAME_ALPHANUMERIC: safereact
REACT_APP_NETWORK: 'mainnet'
STAGING_BUCKET_NAME: ${{ secrets.STAGING_MAINNET_BUCKET_NAME }}
REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN_MAINNET }}
REACT_APP_GOOGLE_ANALYTICS: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET }}
REACT_APP_GNOSIS_APPS_URL: ${{ secrets.REACT_APP_GNOSIS_APPS_URL_PROD }}
jobs:
debug:
name: Debug
runs-on: ubuntu-latest
steps:
- name: Dump env
run: env | sort
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
deploy:
name: Deployment
runs-on: ubuntu-latest
steps:
- name: Remove broken apt repos [Ubuntu]
if: matrix.os == 'ubuntu-latest'
run: |
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 14
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- run: |
sudo apt-get update
sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
pip install awscli --upgrade --user
# Due to some dependencies yarn may randomly throw an error about invalid cache
# This approach is taken from https://github.com/yarnpkg/yarn/issues/7212#issuecomment-506155894 to fix the issue
# Another approach is to install with flag --network-concurrency 1, but this will make the installation pretty slow (default value is 8)
mkdir .yarncache
yarn install --cache-folder ./.yarncache --frozen-lockfile
rm -rf .yarncache
yarn cache clean
# Set production flag
- name: Set production flag for tag build
run: echo "REACT_APP_ENV='production'" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v')
- name: Build ${{ env.REACT_APP_NETWORK }} app
run: yarn build
env:
PUBLIC_URL: './'
REACT_APP_FORTMATIC_KEY: ${{ secrets.REACT_APP_FORTMATIC_KEY }}
REACT_APP_INFURA_TOKEN: ${{ secrets.REACT_APP_INFURA_TOKEN }}
REACT_APP_PORTIS_ID: ${{ secrets.REACT_APP_PORTIS_ID }}
REACT_APP_INTERCOM_ID: ${{ secrets.REACT_APP_INTERCOM_ID }}
REACT_APP_IPFS_GATEWAY: ${{ secrets.REACT_APP_IPFS_GATEWAY }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
# Script to deploy Pull Requests
# Mainnet build is never created in Pull Requests
# Script to deploy to development environment
# Mainnet build is never created in development branch
# Script to deploy to staging environment
- name: 'Deploy to S3: Staging'
if: github.ref == 'refs/heads/master' # Or refs/heads/main
run: aws s3 sync build s3://${{ secrets.STAGING_BUCKET_NAME }}/current/app --delete
# Script to upload release files
- run: bash ./scripts/github/deploy_release.sh
if: startsWith(github.ref, 'refs/tags/v')
env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
PR_NUMBER: ${{ github.event.number }}
REVIEW_BUCKET_NAME: ${{ secrets.AWS_REVIEW_BUCKET_NAME }}
REACT_APP_NETWORK: ${{ env.REACT_APP_NETWORK }}
VERSION_TAG: ${{ github.event.release.tag_name }}
# Script to prepare production deployments
- run: bash ./scripts/github/prepare_production_deployment.sh
if: success() && startsWith(github.ref, 'refs/tags/v')
env:
PROD_DEPLOYMENT_HOOK_TOKEN: ${{ secrets.PROD_DEPLOYMENT_HOOK_TOKEN }}
PROD_DEPLOYMENT_HOOK_URL: ${{ secrets.PROD_DEPLOYMENT_HOOK_URL }}
VERSION_TAG: ${{ github.event.release.tag_name }}
# Upload Sentry source maps when sending to staging or production
- run: yarn sentry-upload-sourcemaps
if: success() && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG}}
SENTRY_PROJECT: gnosis-safe-multisig-${{ env.REACT_APP_NETWORK }}

148
.github/workflows/deploy-rinkeby.yml vendored Normal file
View File

@ -0,0 +1,148 @@
name: Deploy to Rinkeby network
# Run on pushes to dev/master or PR
on:
# Pull request hook without any config. Launches for every pull request
pull_request:
# Launches for pushes to master or development
push:
branches:
- master
- development
# Launches build when release is published
release:
types: [published]
workflow_dispatch:
env:
REPO_NAME_ALPHANUMERIC: safereact
REACT_APP_NETWORK: 'rinkeby'
STAGING_BUCKET_NAME: ${{ secrets.STAGING_RINKEBY_BUCKET_NAME }}
REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN_RINKEBY }}
REACT_APP_GOOGLE_ANALYTICS: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY }}
REACT_APP_GNOSIS_APPS_URL: ${{ secrets.REACT_APP_GNOSIS_APPS_URL_PROD }}
jobs:
debug:
name: Debug
runs-on: ubuntu-latest
steps:
- name: Dump env
run: env | sort
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
deploy:
name: Deployment
runs-on: ubuntu-latest
steps:
- name: Remove broken apt repos [Ubuntu]
if: matrix.os == 'ubuntu-latest'
run: |
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 14
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- run: |
sudo apt-get update
sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
pip install awscli --upgrade --user
# Due to some dependencies yarn may randomly throw an error about invalid cache
# This approach is taken from https://github.com/yarnpkg/yarn/issues/7212#issuecomment-506155894 to fix the issue
# Another approach is to install with flag --network-concurrency 1, but this will make the installation pretty slow (default value is 8)
mkdir .yarncache
yarn install --cache-folder ./.yarncache --frozen-lockfile
rm -rf .yarncache
yarn cache clean
# Set production flag
- name: Set production flag for tag build
run: echo "REACT_APP_ENV='production'" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v')
- name: Build ${{ env.REACT_APP_NETWORK }} app ${{ env.REACT_APP_ENV }}
run: yarn build
env:
PUBLIC_URL: './'
REACT_APP_FORTMATIC_KEY: ${{ secrets.REACT_APP_FORTMATIC_KEY }}
REACT_APP_INFURA_TOKEN: ${{ secrets.REACT_APP_INFURA_TOKEN }}
REACT_APP_PORTIS_ID: ${{ secrets.REACT_APP_PORTIS_ID }}
REACT_APP_INTERCOM_ID: ${{ secrets.REACT_APP_INTERCOM_ID }}
REACT_APP_IPFS_GATEWAY: ${{ secrets.REACT_APP_IPFS_GATEWAY }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
# Script to deploy Pull Requests
- run: bash ./scripts/github/deploy_pull_request.sh
if: success() && github.event.number
env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
PR_NUMBER: ${{ github.event.number }}
REVIEW_BUCKET_NAME: ${{ secrets.AWS_REVIEW_BUCKET_NAME }}
REACT_APP_NETWORK: ${{ env.REACT_APP_NETWORK }}
TRAVIS_TAG: ${{ github.event.release.tag_name }}
- name: 'PRaul: Comment PR with app URLs'
uses: mshick/add-pr-comment@v1
with:
message: |
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
repo-token: ${{ secrets.GITHUB_TOKEN }}
repo-token-user-login: 'github-actions[bot]'
allow-repeats: true
if: success() && github.event.number
env:
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
# Script to deploy to development environment
- name: 'Deploy to S3: Develop'
if: github.ref == 'refs/heads/development'
run: aws s3 sync build s3://${{ secrets.AWS_DEV_BUCKET_NAME }}/app --delete
# Script to deploy to staging environment
- name: 'Deploy to S3: Staging'
if: github.ref == 'refs/heads/master'
run: aws s3 sync build s3://${{ secrets.STAGING_BUCKET_NAME }}/current/app --delete
# Script to upload release files
- run: bash ./scripts/github/deploy_release.sh
if: startsWith(github.ref, 'refs/tags/v')
env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
PR_NUMBER: ${{ github.event.number }}
REVIEW_BUCKET_NAME: ${{ secrets.AWS_REVIEW_BUCKET_NAME }}
REACT_APP_NETWORK: ${{ env.REACT_APP_NETWORK }}
VERSION_TAG: ${{ github.event.release.tag_name }}
# Script to prepare production deployments
- run: bash ./scripts/github/prepare_production_deployment.sh
if: success() && startsWith(github.ref, 'refs/tags/v')
env:
PROD_DEPLOYMENT_HOOK_TOKEN: ${{ secrets.PROD_DEPLOYMENT_HOOK_TOKEN }}
PROD_DEPLOYMENT_HOOK_URL: ${{ secrets.PROD_DEPLOYMENT_HOOK_URL }}
VERSION_TAG: ${{ github.event.release.tag_name }}
# Upload Sentry source maps when sending to staging or production
- run: yarn sentry-upload-sourcemaps
if: success() && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG}}
SENTRY_PROJECT: gnosis-safe-multisig-${{ env.REACT_APP_NETWORK }}

145
.github/workflows/deploy-volta.yml vendored Normal file
View File

@ -0,0 +1,145 @@
name: Deploy to Volta network
# Run on pushes to master or PRs to master
on:
push:
branches:
- master
pull_request:
branches:
- master
# Launches build when release is published
release:
types: [published]
env:
REPO_NAME_ALPHANUMERIC: safereact
REACT_APP_NETWORK: 'volta'
STAGING_BUCKET_NAME: ${{ secrets.STAGING_VOLTA_BUCKET_NAME }}
REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN_VOLTA }}
REACT_APP_GOOGLE_ANALYTICS: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA }}
REACT_APP_GNOSIS_APPS_URL: ${{ secrets.REACT_APP_GNOSIS_APPS_URL_PROD }}
jobs:
debug:
name: Debug
runs-on: ubuntu-latest
steps:
- name: Dump env
run: env | sort
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
deploy:
name: Deployment
runs-on: ubuntu-latest
steps:
- name: Remove broken apt repos [Ubuntu]
if: matrix.os == 'ubuntu-latest'
run: |
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 14
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- run: |
sudo apt-get update
sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
pip install awscli --upgrade --user
# Due to some dependencies yarn may randomly throw an error about invalid cache
# This approach is taken from https://github.com/yarnpkg/yarn/issues/7212#issuecomment-506155894 to fix the issue
# Another approach is to install with flag --network-concurrency 1, but this will make the installation pretty slow (default value is 8)
mkdir .yarncache
yarn install --cache-folder ./.yarncache --frozen-lockfile
rm -rf .yarncache
yarn cache clean
# Set production flag
- name: Set production flag for tag build
run: echo "REACT_APP_ENV='production'" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v')
- name: Build ${{ env.REACT_APP_NETWORK }} app
run: yarn build
env:
PUBLIC_URL: './'
REACT_APP_FORTMATIC_KEY: ${{ secrets.REACT_APP_FORTMATIC_KEY }}
REACT_APP_INFURA_TOKEN: ${{ secrets.REACT_APP_INFURA_TOKEN }}
REACT_APP_PORTIS_ID: ${{ secrets.REACT_APP_PORTIS_ID }}
REACT_APP_INTERCOM_ID: ${{ secrets.REACT_APP_INTERCOM_ID }}
REACT_APP_IPFS_GATEWAY: ${{ secrets.REACT_APP_IPFS_GATEWAY }}
# Script to deploy Pull Requests
- run: bash ./scripts/github/deploy_pull_request.sh
if: success() && github.event.number
env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
PR_NUMBER: ${{ github.event.number }}
REVIEW_BUCKET_NAME: ${{ secrets.AWS_REVIEW_BUCKET_NAME }}
REACT_APP_NETWORK: ${{ env.REACT_APP_NETWORK }}
- name: 'PRaul: Comment PR with app URLs'
uses: mshick/add-pr-comment@v1
with:
message: |
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
repo-token: ${{ secrets.GITHUB_TOKEN }}
repo-token-user-login: 'github-actions[bot]'
allow-repeats: true
if: success() && github.event.number
env:
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
# Script to deploy to development environment
# Volta build is never created in development branch
# Script to deploy to staging environment
- name: 'Deploy to S3: Staging'
if: github.ref == 'refs/heads/master'
run: aws s3 sync build s3://${{ secrets.STAGING_BUCKET_NAME }}/current/app --delete
# Script to upload release files
- run: bash ./scripts/github/deploy_release.sh
if: startsWith(github.ref, 'refs/tags/v')
env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
PR_NUMBER: ${{ github.event.number }}
REVIEW_BUCKET_NAME: ${{ secrets.AWS_REVIEW_BUCKET_NAME }}
REACT_APP_NETWORK: ${{ env.REACT_APP_NETWORK }}
VERSION_TAG: ${{ github.event.release.tag_name }}
# Script to prepare production deployments
- run: bash ./scripts/github/prepare_production_deployment.sh
if: success() && startsWith(github.ref, 'refs/tags/v')
env:
PROD_DEPLOYMENT_HOOK_TOKEN: ${{ secrets.PROD_DEPLOYMENT_HOOK_TOKEN }}
PROD_DEPLOYMENT_HOOK_URL: ${{ secrets.PROD_DEPLOYMENT_HOOK_URL }}
VERSION_TAG: ${{ github.event.release.tag_name }}
# Upload Sentry source maps when sending to staging or production
- run: yarn sentry-upload-sourcemaps
if: success() && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG}}
SENTRY_PROJECT: gnosis-safe-multisig-${{ env.REACT_APP_NETWORK }}

144
.github/workflows/deploy-xdai.yml vendored Normal file
View File

@ -0,0 +1,144 @@
name: Deploy to xDai network
# Run on pushes to master or PRs to master
on:
push:
branches:
- master
pull_request:
branches:
- master
# Launches build when release is published
release:
types: [published]
env:
REPO_NAME_ALPHANUMERIC: safereact
REACT_APP_NETWORK: 'xdai'
STAGING_BUCKET_NAME: ${{ secrets.STAGING_XDAI_BUCKET_NAME }}
REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN_XDAI }}
REACT_APP_GOOGLE_ANALYTICS: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID_XDAI }}
REACT_APP_GNOSIS_APPS_URL: ${{ secrets.REACT_APP_GNOSIS_APPS_URL_PROD }}
jobs:
debug:
name: Debug
runs-on: ubuntu-latest
steps:
- name: Dump env
run: env | sort
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
deploy:
name: Deployment
runs-on: ubuntu-latest
steps:
- name: Remove broken apt repos [Ubuntu]
if: matrix.os == 'ubuntu-latest'
run: |
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 14
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- run: |
sudo apt-get update
sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
pip install awscli --upgrade --user
# Due to some dependencies yarn may randomly throw an error about invalid cache
# This approach is taken from https://github.com/yarnpkg/yarn/issues/7212#issuecomment-506155894 to fix the issue
# Another approach is to install with flag --network-concurrency 1, but this will make the installation pretty slow (default value is 8)
mkdir .yarncache
yarn install --cache-folder ./.yarncache --frozen-lockfile
rm -rf .yarncache
yarn cache clean
# Set production flag
- name: Set production flag for tag build
run: echo "REACT_APP_ENV='production'" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v')
- name: Build ${{ env.REACT_APP_NETWORK }} app
run: yarn build
env:
PUBLIC_URL: './'
REACT_APP_FORTMATIC_KEY: ${{ secrets.REACT_APP_FORTMATIC_KEY }}
REACT_APP_INFURA_TOKEN: ${{ secrets.REACT_APP_INFURA_TOKEN }}
REACT_APP_PORTIS_ID: ${{ secrets.REACT_APP_PORTIS_ID }}
REACT_APP_INTERCOM_ID: ${{ secrets.REACT_APP_INTERCOM_ID }}
REACT_APP_IPFS_GATEWAY: ${{ secrets.REACT_APP_IPFS_GATEWAY }}
# Script to deploy Pull Requests
- run: bash ./scripts/github/deploy_pull_request.sh
if: success() && github.event.number
env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
PR_NUMBER: ${{ github.event.number }}
REVIEW_BUCKET_NAME: ${{ secrets.AWS_REVIEW_BUCKET_NAME }}
REACT_APP_NETWORK: ${{ env.REACT_APP_NETWORK }}
- name: 'PRaul: Comment PR with app URLs'
uses: mshick/add-pr-comment@v1
with:
message: |
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
repo-token: ${{ secrets.GITHUB_TOKEN }}
repo-token-user-login: 'github-actions[bot]'
allow-repeats: true
if: success() && github.event.number
env:
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
# Script to deploy to development environment
# xDai build is never created in development branch
# Script to deploy to staging environment
- name: 'Deploy to S3: Staging'
if: github.ref == 'refs/heads/master'
run: aws s3 sync build s3://${{ secrets.STAGING_BUCKET_NAME }}/current/app --delete
# Script to upload release files
- run: bash ./scripts/github/deploy_release.sh
if: startsWith(github.ref, 'refs/tags/v')
env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
PR_NUMBER: ${{ github.event.number }}
REVIEW_BUCKET_NAME: ${{ secrets.AWS_REVIEW_BUCKET_NAME }}
REACT_APP_NETWORK: ${{ env.REACT_APP_NETWORK }}
VERSION_TAG: ${{ github.event.release.tag_name }}
# Script to prepare production deployments
- run: bash ./scripts/github/prepare_production_deployment.sh
if: success() && startsWith(github.ref, 'refs/tags/v')
env:
PROD_DEPLOYMENT_HOOK_TOKEN: ${{ secrets.PROD_DEPLOYMENT_HOOK_TOKEN }}
PROD_DEPLOYMENT_HOOK_URL: ${{ secrets.PROD_DEPLOYMENT_HOOK_URL }}
VERSION_TAG: ${{ github.event.release.tag_name }}
# Upload Sentry source maps when sending to staging or production
- run: yarn sentry-upload-sourcemaps
if: success() && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG}}
SENTRY_PROJECT: gnosis-safe-multisig-${{ env.REACT_APP_NETWORK }}

View File

@ -1,4 +1,4 @@
name: Build/Release Desktop app name: Build/Release Mainnet desktop app
# this will help you specify where to run # this will help you specify where to run
on: on:
@ -6,10 +6,9 @@ on:
env: env:
REACT_APP_FORTMATIC_KEY: ${{ secrets.REACT_APP_FORTMATIC_KEY }} REACT_APP_FORTMATIC_KEY: ${{ secrets.REACT_APP_FORTMATIC_KEY }}
REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET }}
REACT_APP_INFURA_TOKEN: ${{ secrets.REACT_APP_INFURA_TOKEN }} REACT_APP_INFURA_TOKEN: ${{ secrets.REACT_APP_INFURA_TOKEN }}
REACT_APP_PORTIS_ID: ${{ secrets.REACT_APP_PORTIS_ID }} REACT_APP_PORTIS_ID: ${{ secrets.REACT_APP_PORTIS_ID }}
REACT_APP_GNOSIS_APPS_URL: ${{ secrets.REACT_APP_GNOSIS_APPS_URL }} REACT_APP_GNOSIS_APPS_URL: ${{ secrets.REACT_APP_GNOSIS_APPS_URL_PROD }}
REACT_APP_INTERCOM_ID: ${{ secrets.REACT_APP_INTERCOM_ID }} REACT_APP_INTERCOM_ID: ${{ secrets.REACT_APP_INTERCOM_ID }}
REACT_APP_IPFS_GATEWAY: ${{ secrets.REACT_APP_IPFS_GATEWAY }} REACT_APP_IPFS_GATEWAY: ${{ secrets.REACT_APP_IPFS_GATEWAY }}
jobs: jobs:
@ -26,18 +25,10 @@ jobs:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v2 uses: actions/checkout@v2
# Add cache for yarn directory - uses: actions/cache@v2
# - name: Get yarn cache directory path with:
# id: yarn-cache-dir-path path: '**/node_modules'
# run: echo "::set-output name=dir::$(yarn cache dir)" key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
# - uses: actions/cache@v2
# id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
# with:
# path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
# key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
# restore-keys: |
# ${{ runner.os }}-yarn-
- name: Patch node gyp on windows to support Visual Studio 2019 - name: Patch node gyp on windows to support Visual Studio 2019
if: startsWith(matrix.os, 'windows') if: startsWith(matrix.os, 'windows')
@ -54,7 +45,15 @@ jobs:
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 14 node-version: 14
- run: yarn install --frozen-lockfile --network-concurrency 1 - run: |
# Due to some dependencies yarn may randomly throw an error about invalid cache
# This approach is taken from https://github.com/yarnpkg/yarn/issues/7212#issuecomment-506155894 to fix the issue
# Another approach is to install with flag --network-concurrency 1, but this will make the installation pretty slow (default value is 8)
mkdir .yarncache
yarn install --cache-folder ./.yarncache --frozen-lockfile
rm -rf .yarncache
yarn cache clean
- name: Build/Release Desktop App - name: Build/Release Desktop App
env: env:
# macOS notarization API key # macOS notarization API key

43
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: Run app tests
on:
pull_request:
push:
branches:
- master
- development
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 14
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- run: |
sudo apt-get update
sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
pip install awscli --upgrade --user
# Due to some dependencies yarn may randomly throw an error about invalid cache
# This approach is taken from https://github.com/yarnpkg/yarn/issues/7212#issuecomment-506155894 to fix the issue
# Another approach is to install with flag --network-concurrency 1, but this will make the installation pretty slow (default value is 8)
mkdir .yarncache
yarn install --cache-folder ./.yarncache --frozen-lockfile
rm -rf .yarncache
yarn cache clean
- run: yarn test:coverage
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
if: success()

View File

@ -1,4 +1,5 @@
if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present) # if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present)
if: (branch = master) OR (tag IS present)
dist: focal dist: focal
language: node_js language: node_js
node_js: node_js:
@ -6,42 +7,43 @@ node_js:
os: os:
- linux - linux
matrix: matrix:
include: # include:
- env: # - env:
- REACT_APP_NETWORK='mainnet' # - REACT_APP_NETWORK='mainnet'
- STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME} # - STAGING_BUCKET_NAME=${STAGING_MAINNET_BUCKET_NAME}
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_MAINNET} # - REACT_APP_SENTRY_DSN=${SENTRY_DSN_MAINNET}
- SENTRY_PROJECT=${SENTRY_PROJECT_MAINNET} # - SENTRY_PROJECT=${SENTRY_PROJECT_MAINNET}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET} # - REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET}
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_PROD} # - REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_PROD}
if: (branch = master AND NOT type = pull_request) OR tag IS present # if: (branch = master AND NOT type = pull_request) OR tag IS present
- env: # - env:
- REACT_APP_NETWORK='rinkeby' # - REACT_APP_NETWORK='rinkeby'
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_RINKEBY} # - REACT_APP_SENTRY_DSN=${SENTRY_DSN_RINKEBY}
- SENTRY_PROJECT=${SENTRY_PROJECT_RINKEBY} # - SENTRY_PROJECT=${SENTRY_PROJECT_RINKEBY}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY} # - REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY}
- REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_STAGING} # - REACT_APP_GNOSIS_APPS_URL=${REACT_APP_GNOSIS_APPS_URL_STAGING}
- env: # if: (branch = development AND NOT type = pull_request) OR (branch = master) OR tag IS present
- REACT_APP_NETWORK='xdai' # - env:
- STAGING_BUCKET_NAME=${STAGING_XDAI_BUCKET_NAME} # - REACT_APP_NETWORK='xdai'
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_XDAI} # - STAGING_BUCKET_NAME=${STAGING_XDAI_BUCKET_NAME}
- SENTRY_PROJECT=${SENTRY_PROJECT_XDAI} # - REACT_APP_SENTRY_DSN=${SENTRY_DSN_XDAI}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_XDAI} # - SENTRY_PROJECT=${SENTRY_PROJECT_XDAI}
if: (branch = master) OR tag IS present # - REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_XDAI}
- env: # if: (branch = master) OR tag IS present
- REACT_APP_NETWORK='volta' # - env:
- STAGING_BUCKET_NAME=${STAGING_VOLTA_BUCKET_NAME} # - REACT_APP_NETWORK='volta'
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_VOLTA} # - STAGING_BUCKET_NAME=${STAGING_VOLTA_BUCKET_NAME}
- SENTRY_PROJECT=${SENTRY_PROJECT_VOLTA} # - REACT_APP_SENTRY_DSN=${SENTRY_DSN_VOLTA}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA} # - SENTRY_PROJECT=${SENTRY_PROJECT_VOLTA}
if: (branch = master) OR tag IS present # - REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA}
- env: # if: (branch = master) OR tag IS present
- REACT_APP_NETWORK='energy_web_chain' # - env:
- STAGING_BUCKET_NAME=${STAGING_EWC_BUCKET_NAME} # - REACT_APP_NETWORK='energy_web_chain'
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_EWC} # - STAGING_BUCKET_NAME=${STAGING_EWC_BUCKET_NAME}
- SENTRY_PROJECT=${SENTRY_PROJECT_EWC} # - REACT_APP_SENTRY_DSN=${SENTRY_DSN_EWC}
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_EWC} # - SENTRY_PROJECT=${SENTRY_PROJECT_EWC}
if: (branch = master AND NOT type = pull_request) OR tag IS present # - REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_EWC}
# if: (branch = master AND NOT type = pull_request) OR tag IS present
cache: cache:
npm: false npm: false
yarn: true yarn: true
@ -50,65 +52,65 @@ before_script:
- if [ $TRAVIS_PULL_REQUEST != "false" ]; then export PUBLIC_URL="/${REACT_APP_NETWORK}/app"; fi; - if [ $TRAVIS_PULL_REQUEST != "false" ]; then export PUBLIC_URL="/${REACT_APP_NETWORK}/app"; fi;
before_install: before_install:
# Needed to deploy pull request and releases # Needed to deploy pull request and releases
- sudo apt-get update # - sudo apt-get update
- sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev # - sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
- pip install awscli --upgrade --user # - pip install awscli --upgrade --user
script: script:
- yarn prettier:check - yarn prettier:check
- yarn test:coverage # - yarn test:coverage
- yarn build # - yarn build
- if [[ $TRAVIS_BRANCH == "master" && $TRAVIS_PULL_REQUEST == "false" ]] || [ -n "$TRAVIS_TAG" ]; then # - if [[ $TRAVIS_BRANCH == "master" && $TRAVIS_PULL_REQUEST == "false" ]] || [ -n "$TRAVIS_TAG" ]; then
echo "Upload sentry source maps"; # echo "Upload sentry source maps";
yarn sentry-upload-sourcemaps; # yarn sentry-upload-sourcemaps;
else # else
echo "Skip source map upload"; # echo "Skip source map upload";
fi; # fi;
after_success: after_success:
# Pull Request - Deploy it to a review environment # Pull Request - Deploy it to a review environment
# Travis doesn't do deploy step with pull requests builds # Travis doesn't do deploy step with pull requests builds
- ./config/travis/deploy_pull_request.sh # - ./config/travis/deploy_pull_request.sh
# Releases (tagged commits) - Deploy it to a release environment # Releases (tagged commits) - Deploy it to a release environment
- ./config/travis/deploy_release.sh # - ./config/travis/deploy_release.sh
- yarn coveralls # - yarn coveralls
deploy: deploy:
# Development environment only on rinkeby # Development environment only on rinkeby
- provider: s3 # - provider: s3
bucket: $DEV_BUCKET_NAME # bucket: $DEV_BUCKET_NAME
access_key_id: $AWS_ACCESS_KEY_ID # access_key_id: $AWS_ACCESS_KEY_ID
secret_access_key: $AWS_SECRET_ACCESS_KEY # secret_access_key: $AWS_SECRET_ACCESS_KEY
skip_cleanup: true # skip_cleanup: true
local_dir: build # local_dir: build
upload_dir: app # upload_dir: app
region: $AWS_DEFAULT_REGION # region: $AWS_DEFAULT_REGION
on: # on:
branch: development # branch: development
condition: $REACT_APP_NETWORK = rinkeby # condition: $REACT_APP_NETWORK = rinkeby
# Staging environment # Staging environment
- provider: s3 # - provider: s3
bucket: $STAGING_BUCKET_NAME # bucket: $STAGING_BUCKET_NAME
access_key_id: $AWS_ACCESS_KEY_ID # access_key_id: $AWS_ACCESS_KEY_ID
secret_access_key: $AWS_SECRET_ACCESS_KEY # secret_access_key: $AWS_SECRET_ACCESS_KEY
skip_cleanup: true # skip_cleanup: true
local_dir: build # local_dir: build
upload_dir: current/app # upload_dir: current/app
region: $AWS_DEFAULT_REGION # region: $AWS_DEFAULT_REGION
on: # on:
branch: master # branch: master
# Prepare production deployment # Prepare production deployment
- provider: s3 # - provider: s3
bucket: $STAGING_BUCKET_NAME # bucket: $STAGING_BUCKET_NAME
secret_access_key: $AWS_SECRET_ACCESS_KEY # secret_access_key: $AWS_SECRET_ACCESS_KEY
access_key_id: $AWS_ACCESS_KEY_ID # access_key_id: $AWS_ACCESS_KEY_ID
skip_cleanup: true # skip_cleanup: true
local_dir: build # local_dir: build
upload_dir: releases/$TRAVIS_TAG # upload_dir: releases/$TRAVIS_TAG
region: $AWS_DEFAULT_REGION # region: $AWS_DEFAULT_REGION
on: # on:
tags: true # tags: true
- provider: script # - provider: script
script: ./config/travis/prepare_production_deployment.sh # script: ./config/travis/prepare_production_deployment.sh
on: # on:
tags: true # tags: true

View File

@ -215,7 +215,7 @@
"react-final-form-listeners": "^1.0.2", "react-final-form-listeners": "^1.0.2",
"react-ga": "3.3.0", "react-ga": "3.3.0",
"react-hot-loader": "4.13.0", "react-hot-loader": "4.13.0",
"react-infinite-scroll-component": "^5.1.0", "react-intersection-observer": "^8.31.0",
"react-qr-reader": "^2.2.1", "react-qr-reader": "^2.2.1",
"react-redux": "7.2.2", "react-redux": "7.2.2",
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",

View File

@ -0,0 +1,22 @@
#!/bin/bash
function deploy_pull_request {
# Pull request number with "pr" prefix
PULL_REQUEST_NUMBER="pr$PR_NUMBER"
# Only alphanumeric characters. Example safe-react -> safereact
REVIEW_FEATURE_FOLDER="$REPO_NAME_ALPHANUMERIC/$PULL_REQUEST_NUMBER"
# App build path
APP_PATH="./build"
# Deploy pull request
aws s3 sync $APP_PATH s3://${REVIEW_BUCKET_NAME}/${REVIEW_FEATURE_FOLDER}/${REACT_APP_NETWORK}/app --delete
}
# Only:
# - Pull requests
# - Security env variables are available. PR from forks don't have them.
if [ -n "$AWS_ACCESS_KEY_ID" ]; then
deploy_pull_request
fi

View File

@ -3,19 +3,12 @@
# Only: # Only:
# - Tagged commits # - Tagged commits
# - Security env variables are available. # - Security env variables are available.
if [ -n "$TRAVIS_TAG" ] && [ -n "$AWS_ACCESS_KEY_ID" ] if [ -n "$VERSION_TAG" ] && [ -n "$AWS_ACCESS_KEY_ID" ]
then then
REVIEW_ENVIRONMENT_DOMAIN='review.gnosisdev.com'
# Feature name without all path. Example gnosis/pm-trading-ui -> pm-trading-ui
REPO_NAME=$(basename $TRAVIS_REPO_SLUG)
# Only alphanumeric characters. Example pm-trading-ui -> pmtradingui
REPO_NAME_ALPHANUMERIC=$(echo $REPO_NAME | sed 's/[^a-zA-Z0-9]//g')
# Only alphanumeric characters. Example v1.0.0 -> v100 # Only alphanumeric characters. Example v1.0.0 -> v100
TRAVIS_TAG_ALPHANUMERIC=$(echo $TRAVIS_TAG | sed 's/[^a-zA-Z0-9]//g') VERSION_TAG_ALPHANUMERIC=$(echo $VERSION_TAG | sed 's/[^a-zA-Z0-9]//g')
REVIEW_RELEASE_FOLDER="$REPO_NAME_ALPHANUMERIC/$TRAVIS_TAG_ALPHANUMERIC" REVIEW_RELEASE_FOLDER="$REPO_NAME_ALPHANUMERIC/$VERSION_TAG_ALPHANUMERIC"
# Deploy safe-team release project # Deploy safe-team release project
aws s3 sync build s3://${REVIEW_BUCKET_NAME}/${REVIEW_RELEASE_FOLDER}/app --delete --exclude "*.html" --exclude "/page-data" --cache-control max-age=31536000,public aws s3 sync build s3://${REVIEW_BUCKET_NAME}/${REVIEW_RELEASE_FOLDER}/app --delete --exclude "*.html" --exclude "/page-data" --cache-control max-age=31536000,public

View File

@ -5,12 +5,12 @@ set -ev
# Only: # Only:
# - Tagged commits # - Tagged commits
# - Security env variables are available. # - Security env variables are available.
if [ -n "$TRAVIS_TAG" ] && [ -n "$PROD_DEPLOYMENT_HOOK_TOKEN" ] && [ -n "$PROD_DEPLOYMENT_HOOK_URL" ] if [ -n "$VERSION_TAG" ] && [ -n "$PROD_DEPLOYMENT_HOOK_TOKEN" ] && [ -n "$PROD_DEPLOYMENT_HOOK_URL" ]
then then
curl --silent --output /dev/null --write-out "%{http_code}" -X POST \ curl --silent --output /dev/null --write-out "%{http_code}" -X POST \
-F token="$PROD_DEPLOYMENT_HOOK_TOKEN" \ -F token="$PROD_DEPLOYMENT_HOOK_TOKEN" \
-F ref=master \ -F ref=master \
-F "variables[TRIGGER_RELEASE_COMMIT_TAG]=$TRAVIS_TAG" \ -F "variables[TRIGGER_RELEASE_COMMIT_TAG]=$VERSION_TAG" \
$PROD_DEPLOYMENT_HOOK_URL $PROD_DEPLOYMENT_HOOK_URL
else else
echo "[ERROR] Production deployment could not be prepared" echo "[ERROR] Production deployment could not be prepared"

View File

@ -1,39 +1,53 @@
import { Loader } from '@gnosis.pm/safe-react-components' import React, { createContext, forwardRef, MutableRefObject, ReactElement, ReactNode, useEffect, useState } from 'react'
import React, { ReactElement } from 'react' import { InViewHookResponse, useInView } from 'react-intersection-observer'
import { default as ReactInfiniteScroll, Props as ReactInfiniteScrollProps } from 'react-infinite-scroll-component'
import styled from 'styled-components'
import { Overwrite } from 'src/types/helpers' export const INFINITE_SCROLL_CONTAINER = 'infinite-scroll-container'
export const Centered = styled.div<{ padding?: number }>` export const InfiniteScrollContext = createContext<{
width: 100%; ref: MutableRefObject<HTMLDivElement | null> | ((instance: HTMLDivElement | null) => void) | null
height: 100%; lastItemId?: string
display: flex; setLastItemId: (itemId?: string) => void
padding: ${({ padding }) => `${padding}px`}; }>({ setLastItemId: () => {}, ref: null })
justify-content: center;
align-items: center;
`
export const SCROLLABLE_TARGET_ID = 'scrollableDiv' export const InfiniteScrollProvider = forwardRef<HTMLDivElement, { children: ReactNode }>(
({ children }, ref): ReactElement => {
const [lastItemId, _setLastItemId] = useState<string>()
type InfiniteScrollProps = Overwrite<ReactInfiniteScrollProps, { loader?: ReactInfiniteScrollProps['loader'] }> const setLastItemId = (itemId?: string) => {
setTimeout(() => _setLastItemId(itemId), 0)
}
export const InfiniteScroll = ({ dataLength, next, hasMore, ...props }: InfiniteScrollProps): ReactElement => {
return ( return (
<ReactInfiniteScroll <InfiniteScrollContext.Provider value={{ ref, lastItemId, setLastItemId }}>
style={{ overflow: 'hidden' }} {children}
dataLength={dataLength} </InfiniteScrollContext.Provider>
next={next}
hasMore={hasMore}
loader={
<Centered>
<Loader size="md" />
</Centered>
}
scrollThreshold="120px"
scrollableTarget={SCROLLABLE_TARGET_ID}
>
{props.children}
</ReactInfiniteScroll>
) )
},
)
InfiniteScrollProvider.displayName = 'InfiniteScrollProvider'
type InfiniteScrollProps = {
children: ReactNode
hasMore: boolean
next: () => Promise<void>
config?: InViewHookResponse
}
export const InfiniteScroll = ({ children, hasMore, next, config }: InfiniteScrollProps): ReactElement => {
const { ref, inView } = useInView({
threshold: 0,
root: document.querySelector(`#${INFINITE_SCROLL_CONTAINER}`),
rootMargin: '0px 0px 200px 0px',
triggerOnce: true,
...config,
})
useEffect(() => {
if (inView && hasMore) {
next()
}
}, [inView, hasMore, next])
return <InfiniteScrollProvider ref={ref}>{children}</InfiniteScrollProvider>
} }

View File

@ -1,10 +1,10 @@
import { RateLimit } from 'async-sema'
import { Collectibles, NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d' import { Collectibles, NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png' import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png'
import { fetchErc20AndErc721AssetsList, fetchSafeCollectibles } from 'src/logic/tokens/api' import { fetchErc20AndErc721AssetsList, fetchSafeCollectibles } from 'src/logic/tokens/api'
import { TokenResult } from 'src/logic/tokens/api/fetchErc20AndErc721AssetsList' import { TokenResult } from 'src/logic/tokens/api/fetchErc20AndErc721AssetsList'
import { CollectibleResult } from 'src/logic/tokens/api/fetchSafeCollectibles' import { CollectibleResult } from 'src/logic/tokens/api/fetchSafeCollectibles'
import { sameString } from 'src/utils/strings'
type FetchResult = { type FetchResult = {
erc721Assets: TokenResult[] erc721Assets: TokenResult[]
@ -12,67 +12,70 @@ type FetchResult = {
} }
class Gnosis { class Gnosis {
_rateLimit = async (): Promise<void> => {}
_fetch = async (safeAddress: string): Promise<FetchResult> => { _fetch = async (safeAddress: string): Promise<FetchResult> => {
const collectibles: FetchResult = { const collectibles: FetchResult = {
erc721Assets: [], erc721Assets: [],
erc721Tokens: [], erc721Tokens: [],
} }
const [assets, tokens] = await Promise.allSettled([
fetchErc20AndErc721AssetsList(),
fetchSafeCollectibles(safeAddress),
])
try { switch (assets.status) {
case 'fulfilled':
const { const {
data: { results: assets = [] }, data: { results = [] },
} = await fetchErc20AndErc721AssetsList() } = assets.value
collectibles.erc721Assets = assets.filter((token) => token.type.toLowerCase() === 'erc721') collectibles.erc721Assets = results.filter((token) => sameString(token.type, 'erc721'))
} catch (e) { break
console.error('no erc721 assets could be fetched', e) case 'rejected':
console.error('no erc721 assets could be fetched', assets.reason)
break
} }
try { switch (tokens.status) {
const { data: tokens = [] } = await fetchSafeCollectibles(safeAddress) case 'fulfilled':
collectibles.erc721Tokens = tokens const {
} catch (e) { data: { results = [] },
console.error('no erc721 tokens for the current safe', e) } = tokens.value
collectibles.erc721Tokens = results
break
case 'rejected':
console.error('no erc721 tokens for the current safe', tokens.reason)
break
} }
return collectibles return collectibles
} }
/** static extractNFTAsset = (asset: TokenResult, nftTokens: NFTTokens): NFTAsset => {
* OpenSea class constructor const mainAssetAddress = asset.address
* @param {object} options const numberOfTokens = nftTokens.filter(({ assetAddress }) => sameAddress(assetAddress, mainAssetAddress)).length
* @param {number} options.rps - requests per second
*/
constructor(options: { rps: number }) {
// eslint-disable-next-line no-underscore-dangle
this._rateLimit = RateLimit(options.rps, { timeUnit: 60 * 1000, uniformDistribution: true })
}
static extractAssets(assets: TokenResult[], nftTokens: NFTTokens): NFTAssets {
const extractNFTAsset = (asset: TokenResult): NFTAsset => {
const numberOfTokens = nftTokens.filter(({ assetAddress }) => assetAddress === asset.address).length
return { return {
address: asset.address, address: mainAssetAddress,
description: asset.name, description: asset.name,
image: asset.logoUri || NFTIcon, image: asset.logoUri || NFTIcon,
name: asset.name, name: asset.name,
numberOfTokens, numberOfTokens,
slug: `${asset.address}_${asset.name}`, slug: `${mainAssetAddress}_${asset.name}`,
symbol: asset.symbol, symbol: asset.symbol,
} }
} }
return assets.reduce((acc, asset) => { static extractAssets(assets: TokenResult[], nftTokens: NFTTokens): NFTAssets {
const extractedAssets = {}
assets.forEach((asset) => {
const address = asset.address const address = asset.address
if (acc[address] === undefined) { if (extractedAssets[address] === undefined) {
acc[address] = extractNFTAsset(asset) extractedAssets[address] = Gnosis.extractNFTAsset(asset, nftTokens)
} }
})
return acc return extractedAssets
}, {})
} }
static extractTokens(tokens: CollectibleResult[]): NFTTokens { static extractTokens(tokens: CollectibleResult[]): NFTTokens {
@ -94,12 +97,11 @@ class Gnosis {
*/ */
async fetchCollectibles(safeAddress: string): Promise<Collectibles> { async fetchCollectibles(safeAddress: string): Promise<Collectibles> {
const { erc721Assets, erc721Tokens } = await this._fetch(safeAddress) const { erc721Assets, erc721Tokens } = await this._fetch(safeAddress)
const nftTokens = Gnosis.extractTokens(erc721Tokens)
return { const nftTokens = Gnosis.extractTokens(erc721Tokens)
nftTokens, const nftAssets = Gnosis.extractAssets(erc721Assets, nftTokens)
nftAssets: Gnosis.extractAssets(erc721Assets, nftTokens),
} return { nftTokens, nftAssets }
} }
} }

View File

@ -5,7 +5,7 @@ import { COLLECTIBLES_SOURCE } from 'src/utils/constants'
const SOURCES = { const SOURCES = {
opensea: new OpenSea({ rps: 4 }), opensea: new OpenSea({ rps: 4 }),
gnosis: new Gnosis({ rps: 4 }), gnosis: new Gnosis(),
mockedopensea: new MockedOpenSea({ rps: 4 }), mockedopensea: new MockedOpenSea({ rps: 4 }),
} }

View File

@ -1,4 +1,3 @@
import { batch } from 'react-redux'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import { getConfiguredSource } from 'src/logic/collectibles/sources' import { getConfiguredSource } from 'src/logic/collectibles/sources'
@ -9,10 +8,8 @@ export const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispa
const source = getConfiguredSource() const source = getConfiguredSource()
const collectibles = await source.fetchCollectibles(safeAddress) const collectibles = await source.fetchCollectibles(safeAddress)
batch(() => {
dispatch(addNftAssets(collectibles.nftAssets)) dispatch(addNftAssets(collectibles.nftAssets))
dispatch(addNftTokens(collectibles.nftTokens)) dispatch(addNftTokens(collectibles.nftTokens))
})
} catch (error) { } catch (error) {
console.log('Error fetching collectibles:', error) console.log('Error fetching collectibles:', error)
} }

View File

@ -5,6 +5,7 @@ import {
estimateGasForTransactionCreation, estimateGasForTransactionCreation,
estimateGasForTransactionExecution, estimateGasForTransactionExecution,
MINIMUM_TRANSACTION_GAS, MINIMUM_TRANSACTION_GAS,
GAS_REQUIRED_PER_SIGNATURE,
} from 'src/logic/safe/transactions/gas' } from 'src/logic/safe/transactions/gas'
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
@ -229,12 +230,13 @@ export const useEstimateTransactionGas = ({
safeTxGas, safeTxGas,
approvalAndExecution, approvalAndExecution,
}) })
const gasPrice = manualGasPrice ? web3.utils.toWei(manualGasPrice, 'gwei') : await calculateGasPrice() const gasPrice = manualGasPrice ? web3.utils.toWei(manualGasPrice, 'gwei') : await calculateGasPrice()
const gasPriceFormatted = web3.utils.fromWei(gasPrice, 'gwei') const gasPriceFormatted = web3.utils.fromWei(gasPrice, 'gwei')
const estimatedGasCosts = gasEstimation * parseInt(gasPrice, 10) const estimatedGasCosts = gasEstimation * parseInt(gasPrice, 10)
const gasCost = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) const gasCost = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
const gasCostFormatted = formatAmount(gasCost) const gasCostFormatted = formatAmount(gasCost)
const gasLimit = (gasEstimation * 2 + MINIMUM_TRANSACTION_GAS).toString() const gasLimit = (gasEstimation * 2).toString()
let txEstimationExecutionStatus = EstimationStatus.SUCCESS let txEstimationExecutionStatus = EstimationStatus.SUCCESS
@ -257,7 +259,7 @@ export const useEstimateTransactionGas = ({
} catch (error) { } catch (error) {
console.warn(error.message) console.warn(error.message)
// We put a fixed the amount of gas to let the user try to execute the tx, but it's not accurate so it will probably fail // We put a fixed the amount of gas to let the user try to execute the tx, but it's not accurate so it will probably fail
const gasEstimation = MINIMUM_TRANSACTION_GAS const gasEstimation = MINIMUM_TRANSACTION_GAS + (threshold || 1) * GAS_REQUIRED_PER_SIGNATURE
const gasCost = fromTokenUnit(gasEstimation, nativeCoin.decimals) const gasCost = fromTokenUnit(gasEstimation, nativeCoin.decimals)
const gasCostFormatted = formatAmount(gasCost) const gasCostFormatted = formatAmount(gasCost)
setGasEstimation({ setGasEstimation({

View File

@ -28,7 +28,7 @@ export const useSafeScheduledUpdates = (safeLoaded: boolean, safeAddress?: strin
}) })
if (mounted) { if (mounted) {
timer.current = setTimeout(() => { timer.current = window.setTimeout(() => {
fetchSafeData(address) fetchSafeData(address)
}, TIMEOUT * 3) }, TIMEOUT * 3)
} }

View File

@ -122,13 +122,16 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
name: localSafe?.name, name: localSafe?.name,
modules, modules,
spendingLimits, spendingLimits,
nonce: Number(remoteNonce), nonce: remoteNonce ? Number(remoteNonce) : undefined,
threshold: Number(remoteThreshold), threshold: remoteThreshold ? Number(remoteThreshold) : undefined,
featuresEnabled: localSafe?.currentVersion ? enabledFeatures(localSafe.currentVersion) : localSafe?.featuresEnabled, featuresEnabled: localSafe?.currentVersion ? enabledFeatures(localSafe.currentVersion) : localSafe?.featuresEnabled,
} }
dispatch(updateSafe(updatedSafe)) dispatch(updateSafe(updatedSafe))
if (!remoteOwners.length) {
return
}
// If the remote owners does not contain a local address, we remove that local owner // If the remote owners does not contain a local address, we remove that local owner
localOwners.forEach((localAddress) => { localOwners.forEach((localAddress) => {
const remoteOwnerIndex = remoteOwners.findIndex((remoteAddress) => sameAddress(remoteAddress, localAddress)) const remoteOwnerIndex = remoteOwners.findIndex((remoteAddress) => sameAddress(remoteAddress, localAddress))

View File

@ -11,7 +11,7 @@ export const gatewayTransactions = (state: AppReduxState): AppReduxState['gatewa
return state[GATEWAY_TRANSACTIONS_ID] return state[GATEWAY_TRANSACTIONS_ID]
} }
export const historyTransactions = createSelector( export const historyTransactions = createHashBasedSelector(
gatewayTransactions, gatewayTransactions,
safeParamAddressFromStateSelector, safeParamAddressFromStateSelector,
(gatewayTransactions, safeAddress): StoreStructure['history'] | undefined => { (gatewayTransactions, safeAddress): StoreStructure['history'] | undefined => {

View File

@ -8,6 +8,6 @@ import { AppReduxState } from 'src/store'
export const createIsEqualSelector = createSelectorCreator(defaultMemoize, isEqual) export const createIsEqualSelector = createSelectorCreator(defaultMemoize, isEqual)
const hashFn = (gatewayTransactions: AppReduxState['gatewayTransactions'], safeAddress: string): string => const hashFn = (gatewayTransactions: AppReduxState['gatewayTransactions'], safeAddress: string): string =>
hash(gatewayTransactions[safeAddress]) hash(gatewayTransactions[safeAddress] ?? {})
export const createHashBasedSelector = createSelectorCreator(memoize as any, hashFn) export const createHashBasedSelector = createSelectorCreator(memoize as any, hashFn)

View File

@ -1,4 +1,5 @@
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3' import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3'
@ -12,6 +13,8 @@ import { sameString } from 'src/utils/strings'
// 21000 - additional gas costs (e.g. base tx costs, transfer costs) // 21000 - additional gas costs (e.g. base tx costs, transfer costs)
export const MINIMUM_TRANSACTION_GAS = 21000 export const MINIMUM_TRANSACTION_GAS = 21000
// Estimation of gas required for each signature (aproximately 7800, roundup to 8000)
export const GAS_REQUIRED_PER_SIGNATURE = 8000
// Receives the response data of the safe method requiredTxGas() and parses it to get the gas amount // Receives the response data of the safe method requiredTxGas() and parses it to get the gas amount
const parseRequiredTxGasResponse = (data: string): number => { const parseRequiredTxGasResponse = (data: string): number => {
@ -177,9 +180,10 @@ const calculateMinimumGasForTransaction = async (
estimateData: string, estimateData: string,
txGasEstimation: number, txGasEstimation: number,
dataGasEstimation: number, dataGasEstimation: number,
fixedGasCosts: number,
): Promise<number> => { ): Promise<number> => {
for (const additionalGas of additionalGasBatches) { for (const additionalGas of additionalGasBatches) {
const amountOfGasToTryTx = txGasEstimation + dataGasEstimation + additionalGas const amountOfGasToTryTx = txGasEstimation + dataGasEstimation + fixedGasCosts + additionalGas
console.info(`Estimating transaction creation with gas amount: ${amountOfGasToTryTx}`) console.info(`Estimating transaction creation with gas amount: ${amountOfGasToTryTx}`)
try { try {
const estimation = await getGasEstimationTxResponse({ const estimation = await getGasEstimationTxResponse({
@ -224,7 +228,13 @@ export const estimateGasForTransactionCreation = async (
return gasEstimationResponse return gasEstimationResponse
} }
const threshold = await safeInstance.methods.getThreshold().call()
const dataGasEstimation = parseRequiredTxGasResponse(estimateData) const dataGasEstimation = parseRequiredTxGasResponse(estimateData)
// We add the minimum required gas for a transaction
// TODO: This fix will be more accurate when we have a service for estimation.
// This fix takes the safe threshold and multiplies it by GAS_REQUIRED_PER_SIGNATURE.
const fixedGasCosts = MINIMUM_TRANSACTION_GAS + (Number(threshold) || 1) * GAS_REQUIRED_PER_SIGNATURE
const additionalGasBatches = [0, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000] const additionalGasBatches = [0, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000]
return await calculateMinimumGasForTransaction( return await calculateMinimumGasForTransaction(
@ -233,6 +243,7 @@ export const estimateGasForTransactionCreation = async (
estimateData, estimateData,
gasEstimationResponse, gasEstimationResponse,
dataGasEstimation, dataGasEstimation,
fixedGasCosts,
) )
} catch (error) { } catch (error) {
console.info('Error calculating tx gas estimation', error.message) console.info('Error calculating tx gas estimation', error.message)

View File

@ -46,7 +46,7 @@ const calculateBodyFrom = async (
export const buildTxServiceUrl = (safeAddress: string): string => { export const buildTxServiceUrl = (safeAddress: string): string => {
const address = checksumAddress(safeAddress) const address = checksumAddress(safeAddress)
return `${getSafeServiceBaseUrl(address)}/transactions/?has_confirmations=True` return `${getSafeServiceBaseUrl(address)}/multisig-transactions/?has_confirmations=True`
} }
const SUCCESS_STATUS = 201 // CREATED status const SUCCESS_STATUS = 201 // CREATED status

View File

@ -11,10 +11,10 @@ export type TokenResult = {
type: string type: string
} }
export const fetchErc20AndErc721AssetsList = async (): Promise<AxiosResponse<{ results: TokenResult[] }>> => { export const fetchErc20AndErc721AssetsList = (): Promise<AxiosResponse<{ results: TokenResult[] }>> => {
const url = getTokensServiceBaseUrl() const url = getTokensServiceBaseUrl()
return axios.get<{ results: TokenResult[] }>(`${url}/`, { return axios.get<{ results: TokenResult[] }, AxiosResponse<{ results: TokenResult[] }>>(`${url}/`, {
params: { params: {
limit: 3000, limit: 3000,
}, },

View File

@ -16,9 +16,11 @@ export type CollectibleResult = {
uri: string | null uri: string | null
} }
export const fetchSafeCollectibles = async (safeAddress: string): Promise<AxiosResponse<CollectibleResult[]>> => { export const fetchSafeCollectibles = async (
safeAddress: string,
): Promise<AxiosResponse<{ results: CollectibleResult[] }>> => {
const address = checksumAddress(safeAddress) const address = checksumAddress(safeAddress)
const url = `${getSafeServiceBaseUrl(address)}/collectibles/` const url = `${getSafeServiceBaseUrl(address)}/collectibles/`
return axios.get(url) return axios.get<CollectibleResult[], AxiosResponse<{ results: CollectibleResult[] }>>(url)
} }

View File

@ -1,6 +1,5 @@
import { backOff } from 'exponential-backoff' import { backOff } from 'exponential-backoff'
import { List, Map } from 'immutable' import { List, Map } from 'immutable'
import { batch } from 'react-redux'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import { import {
@ -84,11 +83,9 @@ const fetchSafeTokens = (safeAddress: string) => async (
balances.keySeq().toSet().subtract(blacklistedTokens), balances.keySeq().toSet().subtract(blacklistedTokens),
) )
batch(() => {
dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance })) dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance }))
dispatch(setCurrencyBalances(safeAddress, currencyList)) dispatch(setCurrencyBalances(safeAddress, currencyList))
dispatch(addTokens(tokens)) dispatch(addTokens(tokens))
})
} catch (err) { } catch (err) {
console.error('Error fetching active token list', err) console.error('Error fetching active token list', err)
} }

View File

@ -5,6 +5,7 @@ import styled from 'styled-components'
import { SafeApp } from 'src/routes/safe/components/Apps/types.d' import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
import GnoForm from 'src/components/forms/GnoForm' import GnoForm from 'src/components/forms/GnoForm'
import Img from 'src/components/layout/Img' import Img from 'src/components/layout/Img'
import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
import AppAgreement from './AppAgreement' import AppAgreement from './AppAgreement'
import AppUrl, { AppInfoUpdater, appUrlResolver } from './AppUrl' import AppUrl, { AppInfoUpdater, appUrlResolver } from './AppUrl'
@ -29,6 +30,16 @@ const AppInfo = styled.div`
margin-right: 10px; margin-right: 10px;
} }
` `
const AppDocsInfo = styled.div`
display: flex;
margin-bottom: 10px;
flex-direction: column;
svg {
position: relative;
top: 4px;
left: 4px;
}
`
export interface AddAppFormValues { export interface AddAppFormValues {
appUrl: string appUrl: string
@ -66,6 +77,22 @@ const AddApp = ({ appList, closeModal }: AddAppProps): ReactElement => {
<GnoForm decorators={[appUrlResolver]} initialValues={INITIAL_VALUES} onSubmit={handleSubmit} testId={FORM_ID}> <GnoForm decorators={[appUrlResolver]} initialValues={INITIAL_VALUES} onSubmit={handleSubmit} testId={FORM_ID}>
{() => ( {() => (
<> <>
<AppDocsInfo>
<Text size="xl" as="span" color="secondary">
Safe Apps are third-party extensions.
</Text>
<Link
href="https://docs.gnosis.io/safe/docs/sdks_safe_apps/"
target="_blank"
rel="noreferrer"
title="Learn more about building Safe Apps"
>
<Text size="xl" as="span" color="primary">
Learn more about building Safe Apps.
</Text>
<Icon size="sm" type="externalLink" color="primary" />
</Link>
</AppDocsInfo>
<AppUrl appList={appList} /> <AppUrl appList={appList} />
{/* Fetch app from url and return a SafeApp */} {/* Fetch app from url and return a SafeApp */}

View File

@ -119,7 +119,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
useEffect(() => { useEffect(() => {
if (appIsLoading) { if (appIsLoading) {
timer.current = setTimeout(() => { timer.current = window.setTimeout(() => {
setAppTimeout(true) setAppTimeout(true)
}, TIMEOUT) }, TIMEOUT)
} else { } else {

View File

@ -113,7 +113,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
}, },
// Wallet-Connect // Wallet-Connect
{ {
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmT3VxxfFtfEcvq8AeaoFAyUGxePRa2zisvnxLTrQXU5Uf`, url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRMGTA5ARMwfhYbdmK83zzMd13NnEUKFJSZEgEjKa8YQm`,
disabled: false, disabled: false,
networks: [ networks: [
ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.MAINNET,
@ -129,6 +129,12 @@ export const staticAppsList: Array<StaticAppInfo> = [
disabled: false, disabled: false,
networks: [ETHEREUM_NETWORK.MAINNET], networks: [ETHEREUM_NETWORK.MAINNET],
}, },
// Mushrooms finance
{
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmdRLqtVW9nkjZmQiryoBfpvbqXrqYT59EawZcF5WXoLCY`,
disabled: false,
networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY],
},
] ]
export const getAppInfoFromOrigin = (origin: string): { url: string; name: string } | null => { export const getAppInfoFromOrigin = (origin: string): { url: string; name: string } | null => {

View File

@ -0,0 +1,25 @@
import { Loader } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import { usePagedHistoryTransactions } from './hooks/usePagedHistoryTransactions'
import { Centered } from './styled'
import { HistoryTxList } from './HistoryTxList'
import { TxsInfiniteScroll } from './TxsInfiniteScroll'
export const HistoryTransactions = (): ReactElement => {
const { count, hasMore, next, transactions, isLoading } = usePagedHistoryTransactions()
if (count === 0) {
return (
<Centered>
<Loader size="md" />
</Centered>
)
}
return (
<TxsInfiniteScroll next={next} hasMore={hasMore} isLoading={isLoading}>
<HistoryTxList transactions={transactions} />
</TxsInfiniteScroll>
)
}

View File

@ -1,34 +1,25 @@
import { Loader } from '@gnosis.pm/safe-react-components' import React, { ReactElement, useContext } from 'react'
import React, { ReactElement } from 'react'
import { InfiniteScroll, SCROLLABLE_TARGET_ID } from 'src/components/InfiniteScroll' import { TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d'
import { usePagedHistoryTransactions } from './hooks/usePagedHistoryTransactions' import { TxsInfiniteScrollContext } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxsInfiniteScroll'
import { import { formatWithSchema } from 'src/utils/date'
SubTitle, import { sameString } from 'src/utils/strings'
ScrollableTransactionsContainer, import { StyledTransactions, StyledTransactionsGroup, SubTitle } from './styled'
StyledTransactions,
StyledTransactionsGroup,
Centered,
} from './styled'
import { TxHistoryRow } from './TxHistoryRow' import { TxHistoryRow } from './TxHistoryRow'
import { TxLocationContext } from './TxLocationProvider' import { TxLocationContext } from './TxLocationProvider'
import { formatWithSchema } from 'src/utils/date'
export const HistoryTxList = (): ReactElement => { export const HistoryTxList = ({ transactions }: { transactions: TransactionDetails['transactions'] }): ReactElement => {
const { count, hasMore, next, transactions } = usePagedHistoryTransactions() const { lastItemId, setLastItemId } = useContext(TxsInfiniteScrollContext)
if (count === 0) { const [, lastTransactionsGroup] = transactions[transactions.length - 1]
return ( const lastTransaction = lastTransactionsGroup[lastTransactionsGroup.length - 1]
<Centered>
<Loader size="md" /> if (!sameString(lastItemId, lastTransaction.id)) {
</Centered> setLastItemId(lastTransaction.id)
)
} }
return ( return (
<TxLocationContext.Provider value={{ txLocation: 'history' }}> <TxLocationContext.Provider value={{ txLocation: 'history' }}>
<ScrollableTransactionsContainer id={SCROLLABLE_TARGET_ID}>
<InfiniteScroll dataLength={transactions.length} next={next} hasMore={hasMore}>
{transactions?.map(([timestamp, txs]) => ( {transactions?.map(([timestamp, txs]) => (
<StyledTransactionsGroup key={timestamp}> <StyledTransactionsGroup key={timestamp}>
<SubTitle size="lg">{formatWithSchema(Number(timestamp), 'MMM d, yyyy')}</SubTitle> <SubTitle size="lg">{formatWithSchema(Number(timestamp), 'MMM d, yyyy')}</SubTitle>
@ -39,8 +30,6 @@ export const HistoryTxList = (): ReactElement => {
</StyledTransactions> </StyledTransactions>
</StyledTransactionsGroup> </StyledTransactionsGroup>
))} ))}
</InfiniteScroll>
</ScrollableTransactionsContainer>
</TxLocationContext.Provider> </TxLocationContext.Provider>
) )
} }

View File

@ -2,15 +2,15 @@ import { Loader, Title } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import style from 'styled-components' import style from 'styled-components'
import { InfiniteScroll, SCROLLABLE_TARGET_ID } from 'src/components/InfiniteScroll'
import Img from 'src/components/layout/Img' import Img from 'src/components/layout/Img'
import { usePagedQueuedTransactions } from './hooks/usePagedQueuedTransactions'
import { ActionModal } from './ActionModal' import { ActionModal } from './ActionModal'
import { TxActionProvider } from './TxActionProvider'
import { TxLocationContext } from './TxLocationProvider'
import { QueueTxList } from './QueueTxList'
import { ScrollableTransactionsContainer, Centered } from './styled'
import NoTransactionsImage from './assets/no-transactions.svg' import NoTransactionsImage from './assets/no-transactions.svg'
import { usePagedQueuedTransactions } from './hooks/usePagedQueuedTransactions'
import { QueueTxList } from './QueueTxList'
import { Centered } from './styled'
import { TxActionProvider } from './TxActionProvider'
import { TxsInfiniteScroll } from './TxsInfiniteScroll'
import { TxLocationContext } from './TxLocationProvider'
const NoTransactions = style.div` const NoTransactions = style.div`
display: flex; display: flex;
@ -19,11 +19,11 @@ const NoTransactions = style.div`
` `
export const QueueTransactions = (): ReactElement => { export const QueueTransactions = (): ReactElement => {
const { count, loading, hasMore, next, transactions } = usePagedQueuedTransactions() const { count, isLoading, hasMore, next, transactions } = usePagedQueuedTransactions()
// `loading` is, actually `!transactions` // `loading` is, actually `!transactions`
// added the `transaction` verification to prevent `Object is possibly 'undefined'` error // added the `transaction` verification to prevent `Object is possibly 'undefined'` error
if (loading || !transactions) { if (isLoading || !transactions) {
return ( return (
<Centered> <Centered>
<Loader size="md" /> <Loader size="md" />
@ -42,8 +42,7 @@ export const QueueTransactions = (): ReactElement => {
return ( return (
<TxActionProvider> <TxActionProvider>
<ScrollableTransactionsContainer id={SCROLLABLE_TARGET_ID}> <TxsInfiniteScroll next={next} hasMore={hasMore} isLoading={isLoading}>
<InfiniteScroll dataLength={count} next={next} hasMore={hasMore}>
{/* Next list */} {/* Next list */}
<TxLocationContext.Provider value={{ txLocation: 'queued.next' }}> <TxLocationContext.Provider value={{ txLocation: 'queued.next' }}>
{transactions.next.count !== 0 && <QueueTxList transactions={transactions.next.transactions} />} {transactions.next.count !== 0 && <QueueTxList transactions={transactions.next.transactions} />}
@ -53,8 +52,7 @@ export const QueueTransactions = (): ReactElement => {
<TxLocationContext.Provider value={{ txLocation: 'queued.queued' }}> <TxLocationContext.Provider value={{ txLocation: 'queued.queued' }}>
{transactions.queue.count !== 0 && <QueueTxList transactions={transactions.queue.transactions} />} {transactions.queue.count !== 0 && <QueueTxList transactions={transactions.queue.transactions} />}
</TxLocationContext.Provider> </TxLocationContext.Provider>
</InfiniteScroll> </TxsInfiniteScroll>
</ScrollableTransactionsContainer>
<ActionModal /> <ActionModal />
</TxActionProvider> </TxActionProvider>
) )

View File

@ -2,6 +2,7 @@ import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
import React, { Fragment, ReactElement, useContext } from 'react' import React, { Fragment, ReactElement, useContext } from 'react'
import { Transaction, TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d' import { Transaction, TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d'
import { sameString } from 'src/utils/strings'
import { import {
DisclaimerContainer, DisclaimerContainer,
GroupedTransactions, GroupedTransactions,
@ -14,6 +15,7 @@ import {
import { TxHoverProvider } from './TxHoverProvider' import { TxHoverProvider } from './TxHoverProvider'
import { TxLocationContext } from './TxLocationProvider' import { TxLocationContext } from './TxLocationProvider'
import { TxQueueRow } from './TxQueueRow' import { TxQueueRow } from './TxQueueRow'
import { TxsInfiniteScrollContext } from './TxsInfiniteScroll'
const TreeView = ({ firstElement }: { firstElement: boolean }): ReactElement => { const TreeView = ({ firstElement }: { firstElement: boolean }): ReactElement => {
return <p className="tree-lines">{firstElement ? <span className="first-node" /> : null}</p> return <p className="tree-lines">{firstElement ? <span className="first-node" /> : null}</p>
@ -52,8 +54,8 @@ type QueueTransactionProps = {
transactions: Transaction[] transactions: Transaction[]
} }
const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): ReactElement => { const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): ReactElement =>
return transactions.length > 1 ? ( transactions.length > 1 ? (
<GroupedTransactionsCard> <GroupedTransactionsCard>
<TxHoverProvider> <TxHoverProvider>
<Disclaimer nonce={nonce} /> <Disclaimer nonce={nonce} />
@ -70,7 +72,6 @@ const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): React
) : ( ) : (
<TxQueueRow transaction={transactions[0]} /> <TxQueueRow transaction={transactions[0]} />
) )
}
type QueueTxListProps = { type QueueTxListProps = {
transactions: TransactionDetails['transactions'] transactions: TransactionDetails['transactions']
@ -80,6 +81,14 @@ export const QueueTxList = ({ transactions }: QueueTxListProps): ReactElement =>
const { txLocation } = useContext(TxLocationContext) const { txLocation } = useContext(TxLocationContext)
const title = txLocation === 'queued.next' ? 'NEXT TRANSACTION' : 'QUEUE' const title = txLocation === 'queued.next' ? 'NEXT TRANSACTION' : 'QUEUE'
const { lastItemId, setLastItemId } = useContext(TxsInfiniteScrollContext)
const [, lastTransactionsGroup] = transactions[transactions.length - 1]
const lastTransaction = lastTransactionsGroup[lastTransactionsGroup.length - 1]
if (txLocation === 'queued.queued' && !sameString(lastItemId, lastTransaction.id)) {
setLastItemId(lastTransaction.id)
}
return ( return (
<StyledTransactionsGroup> <StyledTransactionsGroup>
<SubTitle size="lg">{title}</SubTitle> <SubTitle size="lg">{title}</SubTitle>

View File

@ -11,7 +11,7 @@ import {
isSettingsChangeTxInfo, isSettingsChangeTxInfo,
Transaction, Transaction,
} from 'src/logic/safe/store/models/types/gateway.d' } from 'src/logic/safe/store/models/types/gateway.d'
import { TxCollapsedActions } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions' import { TxCollapsedActions } from './TxCollapsedActions'
import { formatDateTime, formatTime, formatTimeInWords } from 'src/utils/date' import { formatDateTime, formatTime, formatTimeInWords } from 'src/utils/date'
import { KNOWN_MODULES } from 'src/utils/constants' import { KNOWN_MODULES } from 'src/utils/constants'
import { sameString } from 'src/utils/strings' import { sameString } from 'src/utils/strings'
@ -21,6 +21,7 @@ import { TransactionStatusProps } from './hooks/useTransactionStatus'
import { TxTypeProps } from './hooks/useTransactionType' import { TxTypeProps } from './hooks/useTransactionType'
import { StyledGroupedTransactions, StyledTransaction } from './styled' import { StyledGroupedTransactions, StyledTransaction } from './styled'
import { TokenTransferAmount } from './TokenTransferAmount' import { TokenTransferAmount } from './TokenTransferAmount'
import { TxsInfiniteScrollContext } from './TxsInfiniteScroll'
import { TxLocationContext } from './TxLocationProvider' import { TxLocationContext } from './TxLocationProvider'
import { CalculatedVotes } from './TxQueueCollapsed' import { CalculatedVotes } from './TxQueueCollapsed'
@ -89,7 +90,7 @@ const TooltipContent = styled.div`
` `
type TxCollapsedProps = { type TxCollapsedProps = {
transaction?: Transaction transaction: Transaction
isGrouped?: boolean isGrouped?: boolean
nonce?: number nonce?: number
type: TxTypeProps type: TxTypeProps
@ -112,6 +113,7 @@ export const TxCollapsed = ({
status, status,
}: TxCollapsedProps): ReactElement => { }: TxCollapsedProps): ReactElement => {
const { txLocation } = useContext(TxLocationContext) const { txLocation } = useContext(TxLocationContext)
const { ref, lastItemId } = useContext(TxsInfiniteScrollContext)
const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : '' const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : ''
@ -161,8 +163,9 @@ export const TxCollapsed = ({
</div> </div>
) )
// attaching ref to a div element as it was causing troubles to add a `ref` to a FunctionComponent
const txCollapsedStatus = ( const txCollapsedStatus = (
<div className="tx-status"> <div className="tx-status" ref={sameString(lastItemId, transaction.id) ? ref : null}>
{transaction?.txStatus === 'PENDING' || transaction?.txStatus === 'PENDING_FAILED' ? ( {transaction?.txStatus === 'PENDING' || transaction?.txStatus === 'PENDING_FAILED' ? (
<CircularProgressPainter color={status.color}> <CircularProgressPainter color={status.color}>
<CircularProgress size={14} color="inherit" /> <CircularProgress size={14} color="inherit" />

View File

@ -32,6 +32,7 @@ export const TxCollapsedActions = ({ transaction }: TxCollapsedActionsProps): Re
return ( return (
<> <>
<Tooltip title={transaction.txStatus === 'AWAITING_EXECUTION' ? 'Execute' : 'Confirm'} placement="top"> <Tooltip title={transaction.txStatus === 'AWAITING_EXECUTION' ? 'Execute' : 'Confirm'} placement="top">
<span>
<IconButton <IconButton
size="small" size="small"
type="button" type="button"
@ -42,12 +43,15 @@ export const TxCollapsedActions = ({ transaction }: TxCollapsedActionsProps): Re
> >
<Icon type={transaction.txStatus === 'AWAITING_EXECUTION' ? 'rocket' : 'check'} color="primary" size="sm" /> <Icon type={transaction.txStatus === 'AWAITING_EXECUTION' ? 'rocket' : 'check'} color="primary" size="sm" />
</IconButton> </IconButton>
</span>
</Tooltip> </Tooltip>
{canCancel && ( {canCancel && (
<Tooltip title="Cancel" placement="top"> <Tooltip title="Cancel" placement="top">
<span>
<IconButton size="small" type="button" onClick={handleCancelButtonClick} disabled={isPending}> <IconButton size="small" type="button" onClick={handleCancelButtonClick} disabled={isPending}>
<Icon type="circleCross" color="error" size="sm" /> <Icon type="circleCross" color="error" size="sm" />
</IconButton> </IconButton>
</span>
</Tooltip> </Tooltip>
)} )}
</> </>

View File

@ -12,5 +12,14 @@ export const TxHistoryCollapsed = ({ transaction }: { transaction: Transaction }
const info = useAssetInfo(transaction.txInfo) const info = useAssetInfo(transaction.txInfo)
const status = useTransactionStatus(transaction) const status = useTransactionStatus(transaction)
return <TxCollapsed nonce={nonce} type={type} info={info} time={transaction.timestamp} status={status} /> return (
<TxCollapsed
nonce={nonce}
type={type}
info={info}
time={transaction.timestamp}
status={status}
transaction={transaction}
/>
)
} }

View File

@ -0,0 +1,27 @@
import { Loader } from '@gnosis.pm/safe-react-components'
import React, { ReactElement, ReactNode } from 'react'
import { INFINITE_SCROLL_CONTAINER, InfiniteScroll } from 'src/components/InfiniteScroll'
import { HorizontallyCentered, ScrollableTransactionsContainer } from './styled'
type TxsInfiniteScrollProps = {
children: ReactNode
next: () => Promise<void>
hasMore: boolean
isLoading: boolean
}
export const TxsInfiniteScroll = ({ children, next, hasMore, isLoading }: TxsInfiniteScrollProps): ReactElement => {
return (
<InfiniteScroll next={next} hasMore={hasMore}>
<ScrollableTransactionsContainer id={INFINITE_SCROLL_CONTAINER}>
{children}
<HorizontallyCentered isVisible={isLoading}>
<Loader size="md" />
</HorizontallyCentered>
</ScrollableTransactionsContainer>
</InfiniteScroll>
)
}
export { InfiniteScrollContext as TxsInfiniteScrollContext } from 'src/components/InfiniteScroll'

View File

@ -1,4 +1,4 @@
import { useState } from 'react' import { useCallback, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { loadPagedHistoryTransactions } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions' import { loadPagedHistoryTransactions } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions'
@ -12,17 +12,20 @@ type PagedTransactions = {
transactions: TransactionDetails['transactions'] transactions: TransactionDetails['transactions']
hasMore: boolean hasMore: boolean
next: () => Promise<void> next: () => Promise<void>
isLoading: boolean
} }
export const usePagedHistoryTransactions = (): PagedTransactions => { export const usePagedHistoryTransactions = (): PagedTransactions => {
const { count, transactions } = useHistoryTransactions() const { count, transactions } = useHistoryTransactions()
const dispatch = useDispatch() const dispatch = useRef(useDispatch())
const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeAddress = useRef(useSelector(safeParamAddressFromStateSelector))
const [hasMore, setHasMore] = useState(true) const [hasMore, setHasMore] = useState(true)
const [isLoading, setIsLoading] = useState(false)
const next = async () => { const next = useCallback(async () => {
const results = await loadPagedHistoryTransactions(safeAddress) setIsLoading(true)
const results = await loadPagedHistoryTransactions(safeAddress.current)
if (!results) { if (!results) {
setHasMore(false) setHasMore(false)
@ -36,11 +39,12 @@ export const usePagedHistoryTransactions = (): PagedTransactions => {
} }
if (values) { if (values) {
dispatch(addHistoryTransactions({ safeAddress, values, isTail: true })) dispatch.current(addHistoryTransactions({ safeAddress: safeAddress.current, values, isTail: true }))
} else { } else {
setHasMore(false) setHasMore(false)
} }
} setIsLoading(false)
}, [])
return { count, transactions, hasMore, next } return { count, transactions, hasMore, next, isLoading }
} }

View File

@ -8,7 +8,7 @@ import { QueueTransactionsInfo, useQueueTransactions } from './useQueueTransacti
type PagedQueuedTransactions = { type PagedQueuedTransactions = {
count: number count: number
loading: boolean isLoading: boolean
transactions?: QueueTransactionsInfo transactions?: QueueTransactionsInfo
hasMore: boolean hasMore: boolean
next: () => Promise<void> next: () => Promise<void>
@ -46,7 +46,7 @@ export const usePagedQueuedTransactions = (): PagedQueuedTransactions => {
count = transactions.next.count + transactions.queue.count count = transactions.next.count + transactions.queue.count
} }
const loading = typeof transactions === 'undefined' || typeof count === 'undefined' const isLoading = typeof transactions === 'undefined' || typeof count === 'undefined'
return { count, loading, transactions, hasMore, next: nextPage } return { count, isLoading, transactions, hasMore, next: nextPage }
} }

View File

@ -3,7 +3,7 @@ import { Item } from '@gnosis.pm/safe-react-components/dist/navigation/Tab'
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { HistoryTxList } from './HistoryTxList' import { HistoryTransactions } from './HistoryTransactions'
import { QueueTransactions } from './QueueTransactions' import { QueueTransactions } from './QueueTransactions'
import { Breadcrumb, ContentWrapper, Wrapper } from './styled' import { Breadcrumb, ContentWrapper, Wrapper } from './styled'
@ -27,7 +27,7 @@ const GatewayTransactions = (): ReactElement => {
<Tab items={items} onChange={setTab} selectedTab={tab} /> <Tab items={items} onChange={setTab} selectedTab={tab} />
<ContentWrapper> <ContentWrapper>
{tab === 'queue' && <QueueTransactions />} {tab === 'queue' && <QueueTransactions />}
{tab === 'history' && <HistoryTxList />} {tab === 'history' && <HistoryTransactions />}
</ContentWrapper> </ContentWrapper>
</Wrapper> </Wrapper>
) )

View File

@ -91,7 +91,6 @@ export const StyledTransactions = styled.div`
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
// '^' to prevent applying rules to the 'Actions' accordion components
& > .MuiAccordion-root { & > .MuiAccordion-root {
&:first-child { &:first-child {
border-top: none; border-top: none;
@ -485,6 +484,11 @@ export const Centered = styled.div<{ padding?: number }>`
align-items: center; align-items: center;
` `
export const HorizontallyCentered = styled(Centered)<{ isVisible: boolean }>`
visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')};
height: auto;
`
export const StyledAccordionSummary = styled(AccordionSummary)` export const StyledAccordionSummary = styled(AccordionSummary)`
height: 52px; height: 52px;
.tx-nonce { .tx-nonce {

4008
yarn.lock

File diff suppressed because it is too large Load Diff