Merge branch 'development' of github.com:gnosis/safe-react into development
This commit is contained in:
commit
5e1f380968
|
@ -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 }}
|
|
@ -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 }}
|
||||||
|
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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
|
|
@ -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()
|
||||||
|
|
174
.travis.yml
174
.travis.yml
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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 (
|
<InfiniteScrollContext.Provider value={{ ref, lastItemId, setLastItemId }}>
|
||||||
<ReactInfiniteScroll
|
{children}
|
||||||
style={{ overflow: 'hidden' }}
|
</InfiniteScrollContext.Provider>
|
||||||
dataLength={dataLength}
|
)
|
||||||
next={next}
|
},
|
||||||
hasMore={hasMore}
|
)
|
||||||
loader={
|
|
||||||
<Centered>
|
InfiniteScrollProvider.displayName = 'InfiniteScrollProvider'
|
||||||
<Loader size="md" />
|
|
||||||
</Centered>
|
type InfiniteScrollProps = {
|
||||||
}
|
children: ReactNode
|
||||||
scrollThreshold="120px"
|
hasMore: boolean
|
||||||
scrollableTarget={SCROLLABLE_TARGET_ID}
|
next: () => Promise<void>
|
||||||
>
|
config?: InViewHookResponse
|
||||||
{props.children}
|
}
|
||||||
</ReactInfiniteScroll>
|
|
||||||
)
|
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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
const {
|
case 'fulfilled':
|
||||||
data: { results: assets = [] },
|
const {
|
||||||
} = await fetchErc20AndErc721AssetsList()
|
data: { results = [] },
|
||||||
collectibles.erc721Assets = assets.filter((token) => token.type.toLowerCase() === 'erc721')
|
} = assets.value
|
||||||
} catch (e) {
|
collectibles.erc721Assets = results.filter((token) => sameString(token.type, 'erc721'))
|
||||||
console.error('no erc721 assets could be fetched', e)
|
break
|
||||||
|
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
|
|
||||||
*/
|
return {
|
||||||
constructor(options: { rps: number }) {
|
address: mainAssetAddress,
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
description: asset.name,
|
||||||
this._rateLimit = RateLimit(options.rps, { timeUnit: 60 * 1000, uniformDistribution: true })
|
image: asset.logoUri || NFTIcon,
|
||||||
|
name: asset.name,
|
||||||
|
numberOfTokens,
|
||||||
|
slug: `${mainAssetAddress}_${asset.name}`,
|
||||||
|
symbol: asset.symbol,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static extractAssets(assets: TokenResult[], nftTokens: NFTTokens): NFTAssets {
|
static extractAssets(assets: TokenResult[], nftTokens: NFTTokens): NFTAssets {
|
||||||
const extractNFTAsset = (asset: TokenResult): NFTAsset => {
|
const extractedAssets = {}
|
||||||
const numberOfTokens = nftTokens.filter(({ assetAddress }) => assetAddress === asset.address).length
|
|
||||||
|
|
||||||
return {
|
assets.forEach((asset) => {
|
||||||
address: asset.address,
|
|
||||||
description: asset.name,
|
|
||||||
image: asset.logoUri || NFTIcon,
|
|
||||||
name: asset.name,
|
|
||||||
numberOfTokens,
|
|
||||||
slug: `${asset.address}_${asset.name}`,
|
|
||||||
symbol: asset.symbol,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return assets.reduce((acc, 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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 */}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,46 +1,35 @@
|
||||||
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}>
|
{transactions?.map(([timestamp, txs]) => (
|
||||||
<InfiniteScroll dataLength={transactions.length} next={next} hasMore={hasMore}>
|
<StyledTransactionsGroup key={timestamp}>
|
||||||
{transactions?.map(([timestamp, txs]) => (
|
<SubTitle size="lg">{formatWithSchema(Number(timestamp), 'MMM d, yyyy')}</SubTitle>
|
||||||
<StyledTransactionsGroup key={timestamp}>
|
<StyledTransactions>
|
||||||
<SubTitle size="lg">{formatWithSchema(Number(timestamp), 'MMM d, yyyy')}</SubTitle>
|
{txs.map((transaction) => (
|
||||||
<StyledTransactions>
|
<TxHistoryRow key={transaction.id} transaction={transaction} />
|
||||||
{txs.map((transaction) => (
|
))}
|
||||||
<TxHistoryRow key={transaction.id} transaction={transaction} />
|
</StyledTransactions>
|
||||||
))}
|
</StyledTransactionsGroup>
|
||||||
</StyledTransactions>
|
))}
|
||||||
</StyledTransactionsGroup>
|
|
||||||
))}
|
|
||||||
</InfiniteScroll>
|
|
||||||
</ScrollableTransactionsContainer>
|
|
||||||
</TxLocationContext.Provider>
|
</TxLocationContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,19 +42,17 @@ 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} />}
|
</TxLocationContext.Provider>
|
||||||
</TxLocationContext.Provider>
|
|
||||||
|
|
||||||
{/* Queue list */}
|
{/* Queue list */}
|
||||||
<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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -32,22 +32,26 @@ 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">
|
||||||
<IconButton
|
<span>
|
||||||
size="small"
|
<IconButton
|
||||||
type="button"
|
size="small"
|
||||||
onClick={handleConfirmButtonClick}
|
type="button"
|
||||||
disabled={disabledActions}
|
onClick={handleConfirmButtonClick}
|
||||||
onMouseEnter={handleOnMouseEnter}
|
disabled={disabledActions}
|
||||||
onMouseLeave={handleOnMouseLeave}
|
onMouseEnter={handleOnMouseEnter}
|
||||||
>
|
onMouseLeave={handleOnMouseLeave}
|
||||||
<Icon type={transaction.txStatus === 'AWAITING_EXECUTION' ? 'rocket' : 'check'} color="primary" size="sm" />
|
>
|
||||||
</IconButton>
|
<Icon type={transaction.txStatus === 'AWAITING_EXECUTION' ? 'rocket' : 'check'} color="primary" size="sm" />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{canCancel && (
|
{canCancel && (
|
||||||
<Tooltip title="Cancel" placement="top">
|
<Tooltip title="Cancel" placement="top">
|
||||||
<IconButton size="small" type="button" onClick={handleCancelButtonClick} disabled={isPending}>
|
<span>
|
||||||
<Icon type="circleCross" color="error" size="sm" />
|
<IconButton size="small" type="button" onClick={handleCancelButtonClick} disabled={isPending}>
|
||||||
</IconButton>
|
<Icon type="circleCross" color="error" size="sm" />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue