mirror of
https://github.com/status-im/safe-react.git
synced 2025-02-28 09:20:39 +00:00
commit
df90896940
16
.github/workflows/deploy-ewc.yml
vendored
16
.github/workflows/deploy-ewc.yml
vendored
@ -1,10 +1,13 @@
|
|||||||
name: Deploy to EWC network
|
name: Deploy to EWC network
|
||||||
|
|
||||||
# Run on pushes to master
|
# Run on pushes to master or PRs to master
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
# Launches build when release is published
|
# Launches build when release is published
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
@ -33,8 +36,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Cancel Previous Runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.8.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
- name: Remove broken apt repos [Ubuntu]
|
- name: Remove broken apt repos [Ubuntu]
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: ${{ matrix.os }} == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
|
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@ -62,9 +69,9 @@ jobs:
|
|||||||
yarn cache clean
|
yarn cache clean
|
||||||
|
|
||||||
# Set production flag
|
# Set production flag
|
||||||
- name: Set production flag for tag build
|
- name: Set production flag for release PR or tagged build
|
||||||
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
|
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'master'
|
||||||
|
|
||||||
- name: Build ${{ env.REACT_APP_NETWORK }} app
|
- name: Build ${{ env.REACT_APP_NETWORK }} app
|
||||||
run: yarn build
|
run: yarn build
|
||||||
@ -98,7 +105,6 @@ jobs:
|
|||||||
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
|
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
repo-token-user-login: 'github-actions[bot]'
|
repo-token-user-login: 'github-actions[bot]'
|
||||||
allow-repeats: true
|
|
||||||
if: success() && github.event.number
|
if: success() && github.event.number
|
||||||
env:
|
env:
|
||||||
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
|
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
|
||||||
|
35
.github/workflows/deploy-mainnet.yml
vendored
35
.github/workflows/deploy-mainnet.yml
vendored
@ -1,7 +1,9 @@
|
|||||||
name: Deploy to Mainnet network
|
name: Deploy to Mainnet network
|
||||||
|
|
||||||
# Run on pushes to master
|
# Run on pushes to master or PRs
|
||||||
on:
|
on:
|
||||||
|
# Pull request hook without any config. Launches for every pull request
|
||||||
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
@ -33,8 +35,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Cancel Previous Runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.8.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
- name: Remove broken apt repos [Ubuntu]
|
- name: Remove broken apt repos [Ubuntu]
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: ${{ matrix.os }} == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
|
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@ -62,9 +68,9 @@ jobs:
|
|||||||
yarn cache clean
|
yarn cache clean
|
||||||
|
|
||||||
# Set production flag
|
# Set production flag
|
||||||
- name: Set production flag for tag build
|
- name: Set production flag for release PR or tagged build
|
||||||
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
|
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'master'
|
||||||
|
|
||||||
- name: Build ${{ env.REACT_APP_NETWORK }} app
|
- name: Build ${{ env.REACT_APP_NETWORK }} app
|
||||||
run: yarn build
|
run: yarn build
|
||||||
@ -84,7 +90,26 @@ jobs:
|
|||||||
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
|
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
|
||||||
|
|
||||||
# Script to deploy Pull Requests
|
# Script to deploy Pull Requests
|
||||||
# Mainnet build is never created in 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]'
|
||||||
|
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
|
# Script to deploy to development environment
|
||||||
# Mainnet build is never created in development branch
|
# Mainnet build is never created in development branch
|
||||||
|
9
.github/workflows/deploy-rinkeby.yml
vendored
9
.github/workflows/deploy-rinkeby.yml
vendored
@ -37,6 +37,10 @@ jobs:
|
|||||||
name: Deployment
|
name: Deployment
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Cancel Previous Runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.8.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
- name: Remove broken apt repos [Ubuntu]
|
- name: Remove broken apt repos [Ubuntu]
|
||||||
if: ${{ matrix.os }} == 'ubuntu-latest'
|
if: ${{ matrix.os }} == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
@ -66,9 +70,9 @@ jobs:
|
|||||||
yarn cache clean
|
yarn cache clean
|
||||||
|
|
||||||
# Set production flag
|
# Set production flag
|
||||||
- name: Set production flag for tag build
|
- name: Set production flag for release PR or tagged build
|
||||||
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
|
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'master'
|
||||||
|
|
||||||
- name: Build ${{ env.REACT_APP_NETWORK }} app ${{ env.REACT_APP_ENV }}
|
- name: Build ${{ env.REACT_APP_NETWORK }} app ${{ env.REACT_APP_ENV }}
|
||||||
run: yarn build
|
run: yarn build
|
||||||
@ -103,7 +107,6 @@ jobs:
|
|||||||
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
|
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
repo-token-user-login: 'github-actions[bot]'
|
repo-token-user-login: 'github-actions[bot]'
|
||||||
allow-repeats: true
|
|
||||||
if: success() && github.event.number
|
if: success() && github.event.number
|
||||||
env:
|
env:
|
||||||
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
|
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
|
||||||
|
11
.github/workflows/deploy-volta.yml
vendored
11
.github/workflows/deploy-volta.yml
vendored
@ -36,8 +36,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Cancel Previous Runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.8.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
- name: Remove broken apt repos [Ubuntu]
|
- name: Remove broken apt repos [Ubuntu]
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: ${{ matrix.os }} == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
|
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@ -65,9 +69,9 @@ jobs:
|
|||||||
yarn cache clean
|
yarn cache clean
|
||||||
|
|
||||||
# Set production flag
|
# Set production flag
|
||||||
- name: Set production flag for tag build
|
- name: Set production flag for release PR or tagged build
|
||||||
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
|
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'master'
|
||||||
|
|
||||||
- name: Build ${{ env.REACT_APP_NETWORK }} app
|
- name: Build ${{ env.REACT_APP_NETWORK }} app
|
||||||
run: yarn build
|
run: yarn build
|
||||||
@ -101,7 +105,6 @@ jobs:
|
|||||||
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
|
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
repo-token-user-login: 'github-actions[bot]'
|
repo-token-user-login: 'github-actions[bot]'
|
||||||
allow-repeats: true
|
|
||||||
if: success() && github.event.number
|
if: success() && github.event.number
|
||||||
env:
|
env:
|
||||||
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
|
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
|
||||||
|
11
.github/workflows/deploy-xdai.yml
vendored
11
.github/workflows/deploy-xdai.yml
vendored
@ -36,8 +36,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Cancel Previous Runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.8.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
- name: Remove broken apt repos [Ubuntu]
|
- name: Remove broken apt repos [Ubuntu]
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: ${{ matrix.os }} == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
|
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@ -65,9 +69,9 @@ jobs:
|
|||||||
yarn cache clean
|
yarn cache clean
|
||||||
|
|
||||||
# Set production flag
|
# Set production flag
|
||||||
- name: Set production flag for tag build
|
- name: Set production flag for release PR or tagged build
|
||||||
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
|
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'master'
|
||||||
|
|
||||||
- name: Build ${{ env.REACT_APP_NETWORK }} app
|
- name: Build ${{ env.REACT_APP_NETWORK }} app
|
||||||
run: yarn build
|
run: yarn build
|
||||||
@ -101,7 +105,6 @@ jobs:
|
|||||||
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
|
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
repo-token-user-login: 'github-actions[bot]'
|
repo-token-user-login: 'github-actions[bot]'
|
||||||
allow-repeats: true
|
|
||||||
if: success() && github.event.number
|
if: success() && github.event.number
|
||||||
env:
|
env:
|
||||||
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
|
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com
|
||||||
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -10,6 +10,10 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Cancel Previous Runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.8.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
|
@ -286,7 +286,7 @@ const xDai: NetworkConfig = {
|
|||||||
label: 'xDai',
|
label: 'xDai',
|
||||||
isTestNet: false,
|
isTestNet: false,
|
||||||
nativeCoin: {
|
nativeCoin: {
|
||||||
address: '0x000',
|
address: '0x0000000000000000000000000000000000000000',
|
||||||
name: 'xDai',
|
name: 'xDai',
|
||||||
symbol: 'xDai',
|
symbol: 'xDai',
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
@ -343,7 +343,7 @@ const mainnet: NetworkConfig = {
|
|||||||
label: 'Mainnet',
|
label: 'Mainnet',
|
||||||
isTestNet: false,
|
isTestNet: false,
|
||||||
nativeCoin: {
|
nativeCoin: {
|
||||||
address: '0x000',
|
address: '0x0000000000000000000000000000000000000000',
|
||||||
name: 'Ether',
|
name: 'Ether',
|
||||||
symbol: 'ETH',
|
symbol: 'ETH',
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "safe-react",
|
"name": "safe-react",
|
||||||
"version": "3.2.0",
|
"version": "3.3.0",
|
||||||
"description": "Allowing crypto users manage funds in a safer way",
|
"description": "Allowing crypto users manage funds in a safer way",
|
||||||
"website": "https://github.com/gnosis/safe-react#readme",
|
"website": "https://github.com/gnosis/safe-react#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
@ -161,7 +161,7 @@
|
|||||||
"@gnosis.pm/safe-apps-sdk": "1.0.3",
|
"@gnosis.pm/safe-apps-sdk": "1.0.3",
|
||||||
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
|
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
|
||||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
||||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#f610327",
|
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#80f5db6",
|
||||||
"@gnosis.pm/util-contracts": "2.0.6",
|
"@gnosis.pm/util-contracts": "2.0.6",
|
||||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.45.0",
|
"@ledgerhq/hw-transport-node-hid-singleton": "5.45.0",
|
||||||
"@material-ui/core": "^4.11.0",
|
"@material-ui/core": "^4.11.0",
|
||||||
|
@ -7,7 +7,31 @@
|
|||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||||
<title>Gnosis Safe Multisig</title>
|
<title>Gnosis Safe Multisig</title>
|
||||||
</head>
|
</head>
|
||||||
|
<style>
|
||||||
|
.safe-preloader-animation {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin:-60px 0 0 -60px;
|
||||||
|
animation: sk-bounce 2.0s infinite ease-in-out;
|
||||||
|
animation-delay: -1.0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sk-bounce {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
-webkit-transform: scale(0.8);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.0);
|
||||||
|
-webkit-transform: scale(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<div id="root" style="overflow: hidden;"></div>
|
<div id="root" style="overflow: hidden;"><img class="safe-preloader-animation" src="./resources/safe.png" /></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -20,13 +20,17 @@ import { getNetworkId } from 'src/config'
|
|||||||
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||||
import { networkSelector } from 'src/logic/wallets/store/selectors'
|
import { networkSelector } from 'src/logic/wallets/store/selectors'
|
||||||
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
|
import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes'
|
||||||
import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
import {
|
||||||
|
safeTotalFiatBalanceSelector,
|
||||||
|
safeNameSelector,
|
||||||
|
safeParamAddressFromStateSelector,
|
||||||
|
} from 'src/logic/safe/store/selectors'
|
||||||
|
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import SendModal from 'src/routes/safe/components/Balances/SendModal'
|
import SendModal from 'src/routes/safe/components/Balances/SendModal'
|
||||||
import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
|
import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
|
||||||
import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
|
import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
|
||||||
import useSafeActions from 'src/logic/safe/hooks/useSafeActions'
|
import useSafeActions from 'src/logic/safe/hooks/useSafeActions'
|
||||||
import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logic/currencyValues/store/selectors'
|
|
||||||
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
|
import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount'
|
||||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||||
|
|
||||||
@ -75,7 +79,7 @@ const App: React.FC = ({ children }) => {
|
|||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const safeName = useSelector(safeNameSelector) ?? ''
|
const safeName = useSelector(safeNameSelector) ?? ''
|
||||||
const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions()
|
const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions()
|
||||||
const currentSafeBalance = useSelector(safeFiatBalancesTotalSelector)
|
const currentSafeBalance = useSelector(safeTotalFiatBalanceSelector)
|
||||||
const currentCurrency = useSelector(currentCurrencySelector)
|
const currentCurrency = useSelector(currentCurrencySelector)
|
||||||
const granted = useSelector(grantedSelector)
|
const granted = useSelector(grantedSelector)
|
||||||
const sidebarItems = useSidebarItems()
|
const sidebarItems = useSidebarItems()
|
||||||
@ -84,7 +88,7 @@ const App: React.FC = ({ children }) => {
|
|||||||
useSafeScheduledUpdates(safeLoaded, safeAddress)
|
useSafeScheduledUpdates(safeLoaded, safeAddress)
|
||||||
|
|
||||||
const sendFunds = safeActionsState.sendFunds
|
const sendFunds = safeActionsState.sendFunds
|
||||||
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
|
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance.toString()) : ''
|
||||||
const balance =
|
const balance =
|
||||||
!!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : undefined
|
!!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : undefined
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.0 KiB |
@ -38,7 +38,7 @@ const styles = () => ({
|
|||||||
zIndex: 1301,
|
zIndex: 1301,
|
||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
flexBasis: '114px',
|
flexBasis: '140px',
|
||||||
flexShrink: '0',
|
flexShrink: '0',
|
||||||
flexGrow: '0',
|
flexGrow: '0',
|
||||||
maxWidth: '55px',
|
maxWidth: '55px',
|
||||||
|
202
src/components/DecodeTxs/index.tsx
Normal file
202
src/components/DecodeTxs/index.tsx
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1'
|
||||||
|
import { Text, EthHashInfo, CopyToClipboardBtn, IconText, FixedIcon } from '@gnosis.pm/safe-react-components'
|
||||||
|
import get from 'lodash.get'
|
||||||
|
|
||||||
|
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||||
|
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||||
|
import { DecodedData, DecodedDataBasicParameter, DecodedDataParameterValue } from 'src/types/transactions/decode.d'
|
||||||
|
import { DecodedTxDetail } from 'src/routes/safe/components/Apps/components/ConfirmTxModal'
|
||||||
|
|
||||||
|
const FlexWrapper = styled.div<{ margin: number }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> :nth-child(2) {
|
||||||
|
margin-left: ${({ margin }) => margin}px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const BasicTxInfoWrapper = styled.div`
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
> :nth-child(2) {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TxList = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
max-height: 260px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-top: 2px solid ${({ theme }) => theme.colors.separator};
|
||||||
|
`
|
||||||
|
|
||||||
|
const TxListItem = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
padding: 0 24px;
|
||||||
|
height: 50px;
|
||||||
|
border-bottom: 2px solid ${({ theme }) => theme.colors.separator};
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const ElementWrapper = styled.div`
|
||||||
|
margin-bottom: 15px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const BasicTxInfo = ({
|
||||||
|
txRecipient,
|
||||||
|
txData,
|
||||||
|
txValue,
|
||||||
|
}: {
|
||||||
|
txRecipient: string
|
||||||
|
txData: string
|
||||||
|
txValue: string
|
||||||
|
}): ReactElement => {
|
||||||
|
const { nativeCoin } = getNetworkInfo()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BasicTxInfoWrapper>
|
||||||
|
{/* TO */}
|
||||||
|
<>
|
||||||
|
<Text size="lg" strong>
|
||||||
|
{`Send ${txValue} ${nativeCoin.symbol} to:`}
|
||||||
|
</Text>
|
||||||
|
<EthHashInfo
|
||||||
|
hash={txRecipient}
|
||||||
|
showIdenticon
|
||||||
|
textSize="lg"
|
||||||
|
showCopyBtn
|
||||||
|
explorerUrl={getExplorerInfo(txRecipient)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
<>
|
||||||
|
{/* Data */}
|
||||||
|
<Text size="lg" strong>
|
||||||
|
Data (hex encoded):
|
||||||
|
</Text>
|
||||||
|
<FlexWrapper margin={5}>
|
||||||
|
<Text size="lg">{web3.utils.hexToBytes(txData).length} bytes</Text>
|
||||||
|
<CopyToClipboardBtn textToCopy={txData} />
|
||||||
|
</FlexWrapper>
|
||||||
|
</>
|
||||||
|
</BasicTxInfoWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getParameterElement = (parameter: DecodedDataBasicParameter, index: number): ReactElement => {
|
||||||
|
let valueElement
|
||||||
|
|
||||||
|
if (parameter.type === 'address') {
|
||||||
|
valueElement = (
|
||||||
|
<EthHashInfo
|
||||||
|
hash={parameter.value}
|
||||||
|
showIdenticon
|
||||||
|
textSize="lg"
|
||||||
|
showCopyBtn
|
||||||
|
explorerUrl={getExplorerInfo(parameter.value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter.type.startsWith('bytes')) {
|
||||||
|
valueElement = (
|
||||||
|
<FlexWrapper margin={5}>
|
||||||
|
<Text size="lg">{web3.utils.hexToBytes(parameter.value).length} bytes</Text>
|
||||||
|
<CopyToClipboardBtn textToCopy={parameter.value} />
|
||||||
|
</FlexWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!valueElement) {
|
||||||
|
let value = parameter.value
|
||||||
|
if (parameter.type.endsWith('[]')) {
|
||||||
|
try {
|
||||||
|
value = JSON.stringify(parameter.value)
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
valueElement = <Text size="lg">{value}</Text>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ElementWrapper key={index}>
|
||||||
|
<Text size="lg" strong>
|
||||||
|
{parameter.name} ({parameter.type})
|
||||||
|
</Text>
|
||||||
|
{valueElement}
|
||||||
|
</ElementWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SingleTx = ({
|
||||||
|
decodedData,
|
||||||
|
onTxItemClick,
|
||||||
|
}: {
|
||||||
|
decodedData: DecodedData | null
|
||||||
|
onTxItemClick: (decodedTxDetails: DecodedData) => void
|
||||||
|
}): ReactElement | null => {
|
||||||
|
if (!decodedData) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TxList>
|
||||||
|
<TxListItem onClick={() => onTxItemClick(decodedData)}>
|
||||||
|
<IconText iconSize="sm" iconType="code" text="Contract interaction" textSize="xl" />
|
||||||
|
|
||||||
|
<FlexWrapper margin={20}>
|
||||||
|
<Text size="xl">{decodedData.method}</Text>
|
||||||
|
<FixedIcon type="chevronRight" />
|
||||||
|
</FlexWrapper>
|
||||||
|
</TxListItem>
|
||||||
|
</TxList>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiSendTx = ({
|
||||||
|
decodedData,
|
||||||
|
onTxItemClick,
|
||||||
|
}: {
|
||||||
|
decodedData: DecodedData | null
|
||||||
|
onTxItemClick: (decodedTxDetails: DecodedDataParameterValue) => void
|
||||||
|
}): ReactElement | null => {
|
||||||
|
const txs: DecodedDataParameterValue[] | undefined = get(decodedData, 'parameters[0].valueDecoded')
|
||||||
|
|
||||||
|
if (!txs) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TxList>
|
||||||
|
{txs.map((tx, index) => (
|
||||||
|
<TxListItem key={index} onClick={() => onTxItemClick(tx)}>
|
||||||
|
<IconText iconSize="sm" iconType="code" text="Contract interaction" textSize="xl" />
|
||||||
|
|
||||||
|
<FlexWrapper margin={20}>
|
||||||
|
{tx.dataDecoded && <Text size="xl">{tx.dataDecoded.method}</Text>}
|
||||||
|
<FixedIcon type="chevronRight" />
|
||||||
|
</FlexWrapper>
|
||||||
|
</TxListItem>
|
||||||
|
))}
|
||||||
|
</TxList>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
txs: Transaction[]
|
||||||
|
decodedData: DecodedData | null
|
||||||
|
onTxItemClick: (decodedTxDetails: DecodedTxDetail) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DecodeTxs = ({ txs, decodedData, onTxItemClick }: Props): ReactElement => {
|
||||||
|
return txs.length > 1 ? (
|
||||||
|
<MultiSendTx decodedData={decodedData} onTxItemClick={onTxItemClick} />
|
||||||
|
) : (
|
||||||
|
<SingleTx decodedData={decodedData} onTxItemClick={onTxItemClick} />
|
||||||
|
)
|
||||||
|
}
|
@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import { Icon } from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import { md, lg } from 'src/theme/variables'
|
import { md, lg } from 'src/theme/variables'
|
||||||
@ -33,18 +34,28 @@ const StyledClose = styled(Close)`
|
|||||||
width: 35px;
|
width: 35px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ModalTitle = ({
|
const GoBackWrapper = styled.div`
|
||||||
iconUrl,
|
margin-right: 15px;
|
||||||
title,
|
`
|
||||||
onClose,
|
|
||||||
}: {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
iconUrl: string
|
goBack?: () => void
|
||||||
|
iconUrl?: string
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
}): React.ReactElement => {
|
}
|
||||||
|
|
||||||
|
const ModalTitle = ({ goBack, iconUrl, title, onClose }: Props): React.ReactElement => {
|
||||||
return (
|
return (
|
||||||
<StyledRow align="center" grow>
|
<StyledRow align="center" grow>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
|
{goBack && (
|
||||||
|
<GoBackWrapper>
|
||||||
|
<IconButton onClick={goBack}>
|
||||||
|
<Icon type="arrowLeft" size="md" />
|
||||||
|
</IconButton>
|
||||||
|
</GoBackWrapper>
|
||||||
|
)}
|
||||||
{iconUrl && <IconImg alt={title} src={iconUrl} />}
|
{iconUrl && <IconImg alt={title} src={iconUrl} />}
|
||||||
<StyledParagraph noMargin weight="bolder">
|
<StyledParagraph noMargin weight="bolder">
|
||||||
{title}
|
{title}
|
||||||
|
@ -3,17 +3,18 @@ import * as React from 'react'
|
|||||||
import Button from 'src/components/layout/Button'
|
import Button from 'src/components/layout/Button'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { boldFont, sm } from 'src/theme/variables'
|
import { boldFont, sm, lg, secondary } from 'src/theme/variables'
|
||||||
|
|
||||||
const controlStyle = {
|
const controlStyle = {
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
padding: sm,
|
padding: lg,
|
||||||
borderRadius: sm,
|
borderRadius: sm,
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstButtonStyle = {
|
const firstButtonStyle = {
|
||||||
marginRight: sm,
|
marginRight: sm,
|
||||||
fontWeight: boldFont,
|
fontWeight: boldFont,
|
||||||
|
color: secondary,
|
||||||
}
|
}
|
||||||
|
|
||||||
const secondButtonStyle = {
|
const secondButtonStyle = {
|
||||||
@ -50,8 +51,8 @@ const Controls = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row align="end" grow style={controlStyle}>
|
<Row align="center" grow style={controlStyle}>
|
||||||
<Col end="xs" xs={12}>
|
<Col center="xs" xs={12}>
|
||||||
<Button onClick={onPrevious} size="small" style={firstButtonStyle} type="button">
|
<Button onClick={onPrevious} size="small" style={firstButtonStyle} type="button">
|
||||||
{back}
|
{back}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -7,7 +7,7 @@ import { lg } from 'src/theme/variables'
|
|||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
margin: '10px',
|
margin: '10px 0 10px 10px',
|
||||||
maxWidth: '770px',
|
maxWidth: '770px',
|
||||||
boxShadow: '0 0 10px 0 rgba(33,48,77,0.10)',
|
boxShadow: '0 0 10px 0 rgba(33,48,77,0.10)',
|
||||||
},
|
},
|
||||||
|
@ -10,6 +10,8 @@ import {
|
|||||||
uniqueAddress,
|
uniqueAddress,
|
||||||
differentFrom,
|
differentFrom,
|
||||||
ADDRESS_REPEATED_ERROR,
|
ADDRESS_REPEATED_ERROR,
|
||||||
|
addressIsNotCurrentSafe,
|
||||||
|
OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR,
|
||||||
} from 'src/components/forms/validator'
|
} from 'src/components/forms/validator'
|
||||||
|
|
||||||
describe('Forms > Validators', () => {
|
describe('Forms > Validators', () => {
|
||||||
@ -179,6 +181,22 @@ describe('Forms > Validators', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('addressIsNotSafe validator', () => {
|
||||||
|
it('Returns undefined if the given `address` it not the given `safeAddress`', async () => {
|
||||||
|
const address = '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe'
|
||||||
|
const safeAddress = '0x2D6F2B448b0F711Eb81f2929566504117d67E44F'
|
||||||
|
|
||||||
|
expect(addressIsNotCurrentSafe(safeAddress)(address)).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Returns an error message if the given `address` is the same as the `safeAddress`', async () => {
|
||||||
|
const address = '0x2D6F2B448b0F711Eb81f2929566504117d67E44F'
|
||||||
|
const safeAddress = '0x2D6F2B448b0F711Eb81f2929566504117d67E44F'
|
||||||
|
|
||||||
|
expect(addressIsNotCurrentSafe(safeAddress)(address)).toEqual(OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('differentFrom validator', () => {
|
describe('differentFrom validator', () => {
|
||||||
const getDifferentFromErrMsg = (diffValue: string): string => `Value should be different than ${diffValue}`
|
const getDifferentFromErrMsg = (diffValue: string): string => `Value should be different than ${diffValue}`
|
||||||
|
|
||||||
|
@ -80,9 +80,7 @@ export const mustBeEthereumContractAddress = memoize(
|
|||||||
async (address: string): Promise<ValidatorReturnType> => {
|
async (address: string): Promise<ValidatorReturnType> => {
|
||||||
const contractCode = await getWeb3().eth.getCode(address)
|
const contractCode = await getWeb3().eth.getCode(address)
|
||||||
|
|
||||||
const errorMessage = `Input must be a valid Ethereum contract address${
|
const errorMessage = `Must resolve to a valid smart contract address.`
|
||||||
isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : ''
|
|
||||||
}`
|
|
||||||
|
|
||||||
return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' ? errorMessage : undefined
|
return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' ? errorMessage : undefined
|
||||||
},
|
},
|
||||||
@ -92,12 +90,16 @@ export const minMaxLength = (minLen: number, maxLen: number) => (value: string):
|
|||||||
value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols`
|
value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols`
|
||||||
|
|
||||||
export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
|
export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
|
||||||
|
export const OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR = 'Cannot use Safe itself as owner.'
|
||||||
|
|
||||||
export const uniqueAddress = (addresses: string[] | List<string> = []) => (address?: string): string | undefined => {
|
export const uniqueAddress = (addresses: string[] | List<string> = []) => (address?: string): string | undefined => {
|
||||||
const addressExists = addresses.some((addressFromList) => sameAddress(addressFromList, address))
|
const addressExists = addresses.some((addressFromList) => sameAddress(addressFromList, address))
|
||||||
return addressExists ? ADDRESS_REPEATED_ERROR : undefined
|
return addressExists ? ADDRESS_REPEATED_ERROR : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const addressIsNotCurrentSafe = (safeAddress: string) => (address?: string): string | undefined =>
|
||||||
|
sameAddress(safeAddress, address) ? OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR : undefined
|
||||||
|
|
||||||
export const composeValidators = (...validators: Validator[]) => (value: unknown): ValidatorReturnType =>
|
export const composeValidators = (...validators: Validator[]) => (value: unknown): ValidatorReturnType =>
|
||||||
validators.reduce(
|
validators.reduce(
|
||||||
(error: string | undefined, validator: GenericValidatorType): ValidatorReturnType => error || validator(value),
|
(error: string | undefined, validator: GenericValidatorType): ValidatorReturnType => error || validator(value),
|
||||||
|
@ -38,7 +38,7 @@ const mainnet: NetworkConfig = {
|
|||||||
label: 'EWC',
|
label: 'EWC',
|
||||||
isTestNet: false,
|
isTestNet: false,
|
||||||
nativeCoin: {
|
nativeCoin: {
|
||||||
address: '0x000',
|
address: '0x0000000000000000000000000000000000000000',
|
||||||
name: 'Energy web token',
|
name: 'Energy web token',
|
||||||
symbol: 'EWT',
|
symbol: 'EWT',
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
|
@ -29,7 +29,7 @@ const local: NetworkConfig = {
|
|||||||
label: 'LocalRPC',
|
label: 'LocalRPC',
|
||||||
isTestNet: true,
|
isTestNet: true,
|
||||||
nativeCoin: {
|
nativeCoin: {
|
||||||
address: '0x000',
|
address: '0x0000000000000000000000000000000000000000',
|
||||||
name: 'Ether',
|
name: 'Ether',
|
||||||
symbol: 'ETH',
|
symbol: 'ETH',
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
|
@ -38,7 +38,7 @@ const mainnet: NetworkConfig = {
|
|||||||
label: 'Mainnet',
|
label: 'Mainnet',
|
||||||
isTestNet: false,
|
isTestNet: false,
|
||||||
nativeCoin: {
|
nativeCoin: {
|
||||||
address: '0x000',
|
address: '0x0000000000000000000000000000000000000000',
|
||||||
name: 'Ether',
|
name: 'Ether',
|
||||||
symbol: 'ETH',
|
symbol: 'ETH',
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
|
@ -38,7 +38,7 @@ const rinkeby: NetworkConfig = {
|
|||||||
label: 'Rinkeby',
|
label: 'Rinkeby',
|
||||||
isTestNet: true,
|
isTestNet: true,
|
||||||
nativeCoin: {
|
nativeCoin: {
|
||||||
address: '0x000',
|
address: '0x0000000000000000000000000000000000000000',
|
||||||
name: 'Ether',
|
name: 'Ether',
|
||||||
symbol: 'ETH',
|
symbol: 'ETH',
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
|
@ -35,7 +35,7 @@ const mainnet: NetworkConfig = {
|
|||||||
label: 'Volta',
|
label: 'Volta',
|
||||||
isTestNet: true,
|
isTestNet: true,
|
||||||
nativeCoin: {
|
nativeCoin: {
|
||||||
address: '0x000',
|
address: '0x0000000000000000000000000000000000000000',
|
||||||
name: 'Energy web token',
|
name: 'Energy web token',
|
||||||
symbol: 'EWT',
|
symbol: 'EWT',
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
|
@ -29,7 +29,7 @@ const xDai: NetworkConfig = {
|
|||||||
label: 'xDai',
|
label: 'xDai',
|
||||||
isTestNet: false,
|
isTestNet: false,
|
||||||
nativeCoin: {
|
nativeCoin: {
|
||||||
address: '0x000',
|
address: '0x0000000000000000000000000000000000000000',
|
||||||
name: 'xDai',
|
name: 'xDai',
|
||||||
symbol: 'xDai',
|
symbol: 'xDai',
|
||||||
decimals: 18,
|
decimals: 18,
|
||||||
|
@ -6,7 +6,6 @@ import { Integrations } from '@sentry/tracing'
|
|||||||
|
|
||||||
import Root from 'src/components/Root'
|
import Root from 'src/components/Root'
|
||||||
import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage'
|
import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage'
|
||||||
import loadActiveTokens from 'src/logic/tokens/store/actions/loadActiveTokens'
|
|
||||||
import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe'
|
import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe'
|
||||||
import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
|
import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
|
||||||
import { store } from 'src/store'
|
import { store } from 'src/store'
|
||||||
@ -17,7 +16,6 @@ disableMMAutoRefreshWarning()
|
|||||||
|
|
||||||
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
|
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
|
||||||
|
|
||||||
store.dispatch(loadActiveTokens())
|
|
||||||
store.dispatch(loadSafesFromStorage())
|
store.dispatch(loadSafesFromStorage())
|
||||||
store.dispatch(loadDefaultSafe())
|
store.dispatch(loadDefaultSafe())
|
||||||
store.dispatch(loadCurrentSessionFromStorage())
|
store.dispatch(loadCurrentSessionFromStorage())
|
||||||
|
@ -3,8 +3,6 @@ import { NFTAsset, NFTAssets, NFTToken, NFTTokens } from 'src/logic/collectibles
|
|||||||
|
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
|
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
|
||||||
import { safeActiveAssetsSelector } from 'src/logic/safe/store/selectors'
|
|
||||||
|
|
||||||
export const nftAssets = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
|
export const nftAssets = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
|
||||||
export const nftTokens = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
|
export const nftTokens = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
|
||||||
|
|
||||||
@ -26,21 +24,8 @@ export const orderedNFTAssets = createSelector(nftTokensSelector, (userNftTokens
|
|||||||
|
|
||||||
export const activeNftAssetsListSelector = createSelector(
|
export const activeNftAssetsListSelector = createSelector(
|
||||||
nftAssetsListSelector,
|
nftAssetsListSelector,
|
||||||
safeActiveAssetsSelector,
|
|
||||||
availableNftAssetsAddresses,
|
availableNftAssetsAddresses,
|
||||||
(assets, activeAssetsList, availableNftAssetsAddresses): NFTAsset[] => {
|
(assets, availableNftAssetsAddresses): NFTAsset[] => {
|
||||||
return assets
|
return assets.filter(({ address }) => availableNftAssetsAddresses.includes(address))
|
||||||
.filter(({ address }) => activeAssetsList.has(address))
|
|
||||||
.filter(({ address }) => availableNftAssetsAddresses.includes(address))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const safeActiveSelectorMap = createSelector(
|
|
||||||
activeNftAssetsListSelector,
|
|
||||||
(activeAssets): NFTAssets => {
|
|
||||||
return activeAssets.reduce((acc, asset) => {
|
|
||||||
acc[asset.address] = asset
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
8
src/logic/currencyValues/api/fetchAvailableCurrencies.ts
Normal file
8
src/logic/currencyValues/api/fetchAvailableCurrencies.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { getClientGatewayUrl } from 'src/config'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export const fetchAvailableCurrencies = async (): Promise<string[]> => {
|
||||||
|
const url = `${getClientGatewayUrl()}/balances/supported-fiat-codes`
|
||||||
|
|
||||||
|
return axios.get(url).then(({ data }) => data)
|
||||||
|
}
|
@ -1,41 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import BigNumber from 'bignumber.js'
|
|
||||||
|
|
||||||
import { EXCHANGE_RATE_URL } from 'src/utils/constants'
|
|
||||||
import { fetchTokenCurrenciesBalances } from './fetchTokenCurrenciesBalances'
|
|
||||||
import { sameString } from 'src/utils/strings'
|
|
||||||
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
|
|
||||||
|
|
||||||
const fetchCurrenciesRates = async (
|
|
||||||
baseCurrency: string,
|
|
||||||
targetCurrencyValue: string,
|
|
||||||
safeAddress: string,
|
|
||||||
): Promise<number> => {
|
|
||||||
let rate = 0
|
|
||||||
if (sameString(targetCurrencyValue, AVAILABLE_CURRENCIES.NETWORK)) {
|
|
||||||
try {
|
|
||||||
const tokenCurrenciesBalances = await fetchTokenCurrenciesBalances(safeAddress)
|
|
||||||
if (tokenCurrenciesBalances.items.length) {
|
|
||||||
rate = new BigNumber(1).div(tokenCurrenciesBalances.items[0].fiatConversion).toNumber()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Fetching ${AVAILABLE_CURRENCIES.NETWORK} data from the relayer errored`, error)
|
|
||||||
}
|
|
||||||
return rate
|
|
||||||
}
|
|
||||||
|
|
||||||
// National currencies
|
|
||||||
try {
|
|
||||||
const url = `${EXCHANGE_RATE_URL}?base=${baseCurrency}&symbols=${targetCurrencyValue}`
|
|
||||||
const result = await axios.get(url)
|
|
||||||
if (result?.data) {
|
|
||||||
const { rates } = result.data
|
|
||||||
rate = rates[targetCurrencyValue] ? rates[targetCurrencyValue] : 0
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fetching data from getExchangeRatesUrl errored', error)
|
|
||||||
}
|
|
||||||
return rate
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fetchCurrenciesRates
|
|
@ -1,29 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import { getSafeClientGatewayBaseUrl } from 'src/config'
|
|
||||||
import { TokenProps } from 'src/logic/tokens/store/model/token'
|
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
|
||||||
|
|
||||||
export type TokenBalance = {
|
|
||||||
tokenInfo: TokenProps
|
|
||||||
balance: string
|
|
||||||
fiatBalance: string
|
|
||||||
fiatConversion: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BalanceEndpoint = {
|
|
||||||
fiatTotal: string
|
|
||||||
items: TokenBalance[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchTokenCurrenciesBalances = (
|
|
||||||
safeAddress: string,
|
|
||||||
excludeSpamTokens = true,
|
|
||||||
trustedTokens = false,
|
|
||||||
): Promise<BalanceEndpoint> => {
|
|
||||||
const url = `${getSafeClientGatewayBaseUrl(
|
|
||||||
checksumAddress(safeAddress),
|
|
||||||
)}/balances/usd/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}`
|
|
||||||
|
|
||||||
return axios.get(url).then(({ data }) => data)
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
import { Action } from 'redux-actions'
|
|
||||||
import { ThunkDispatch } from 'redux-thunk'
|
|
||||||
|
|
||||||
import fetchCurrenciesRates from 'src/logic/currencyValues/api/fetchCurrenciesRates'
|
|
||||||
import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
|
|
||||||
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
|
|
||||||
import { CurrencyRatePayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
|
||||||
import { AppReduxState } from 'src/store'
|
|
||||||
|
|
||||||
const fetchCurrencyRate = (safeAddress: string, selectedCurrency: string) => async (
|
|
||||||
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrencyRatePayload>>,
|
|
||||||
): Promise<void> => {
|
|
||||||
if (AVAILABLE_CURRENCIES.USD === selectedCurrency) {
|
|
||||||
dispatch(setCurrencyRate(safeAddress, 1))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedCurrencyRateInBaseCurrency: number = await fetchCurrenciesRates(
|
|
||||||
AVAILABLE_CURRENCIES.USD,
|
|
||||||
selectedCurrency,
|
|
||||||
safeAddress,
|
|
||||||
)
|
|
||||||
|
|
||||||
dispatch(setCurrencyRate(safeAddress, selectedCurrencyRateInBaseCurrency))
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fetchCurrencyRate
|
|
@ -2,18 +2,17 @@ import { Action } from 'redux-actions'
|
|||||||
import { ThunkDispatch } from 'redux-thunk'
|
import { ThunkDispatch } from 'redux-thunk'
|
||||||
|
|
||||||
import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
|
import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
|
||||||
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
|
|
||||||
import { CurrentCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
|
||||||
import { loadSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
|
|
||||||
import { AppReduxState } from 'src/store'
|
|
||||||
|
|
||||||
export const fetchSelectedCurrency = (safeAddress: string) => async (
|
import { loadSelectedCurrency } from 'src/logic/safe/utils/currencyValuesStorage'
|
||||||
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrentCurrencyPayload>>,
|
import { AppReduxState } from 'src/store'
|
||||||
|
import { SelectedCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||||
|
|
||||||
|
export const fetchSelectedCurrency = () => async (
|
||||||
|
dispatch: ThunkDispatch<AppReduxState, undefined, Action<SelectedCurrencyPayload>>,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const storedSelectedCurrency = await loadSelectedCurrency()
|
const storedSelectedCurrency = await loadSelectedCurrency()
|
||||||
|
dispatch(setSelectedCurrency({ selectedCurrency: storedSelectedCurrency || 'USD' }))
|
||||||
dispatch(setSelectedCurrency(safeAddress, storedSelectedCurrency || AVAILABLE_CURRENCIES.USD))
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching currency values', err)
|
console.error('Error fetching currency values', err)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
import { AvailableCurrenciesPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||||
|
|
||||||
|
export const SET_AVAILABLE_CURRENCIES = 'SET_AVAILABLE_CURRENCIES'
|
||||||
|
|
||||||
|
export const setAvailableCurrencies = createAction<AvailableCurrenciesPayload>(SET_AVAILABLE_CURRENCIES)
|
@ -1,12 +0,0 @@
|
|||||||
import { createAction } from 'redux-actions'
|
|
||||||
import { BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues'
|
|
||||||
|
|
||||||
export const SET_CURRENCY_BALANCES = 'SET_CURRENCY_BALANCES'
|
|
||||||
|
|
||||||
export const setCurrencyBalances = createAction(
|
|
||||||
SET_CURRENCY_BALANCES,
|
|
||||||
(safeAddress: string, currencyBalances: BalanceCurrencyList) => ({
|
|
||||||
safeAddress,
|
|
||||||
currencyBalances,
|
|
||||||
}),
|
|
||||||
)
|
|
@ -1,9 +0,0 @@
|
|||||||
import { createAction } from 'redux-actions'
|
|
||||||
|
|
||||||
export const SET_CURRENCY_RATE = 'SET_CURRENCY_RATE'
|
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
export const setCurrencyRate = createAction(SET_CURRENCY_RATE, (safeAddress: string, currencyRate: number) => ({
|
|
||||||
safeAddress,
|
|
||||||
currencyRate,
|
|
||||||
}))
|
|
@ -1,20 +1,6 @@
|
|||||||
import { Action, createAction } from 'redux-actions'
|
import { createAction } from 'redux-actions'
|
||||||
import { ThunkDispatch } from 'redux-thunk'
|
import { SelectedCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||||
|
|
||||||
import { CurrencyPayloads } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
|
||||||
import { AppReduxState } from 'src/store'
|
|
||||||
import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate'
|
|
||||||
|
|
||||||
export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY'
|
export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY'
|
||||||
|
|
||||||
const setCurrentCurrency = createAction(SET_CURRENT_CURRENCY, (safeAddress: string, selectedCurrency: string) => ({
|
export const setSelectedCurrency = createAction<SelectedCurrencyPayload>(SET_CURRENT_CURRENCY)
|
||||||
safeAddress,
|
|
||||||
selectedCurrency,
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const setSelectedCurrency = (safeAddress: string, selectedCurrency: string) => (
|
|
||||||
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrencyPayloads>>,
|
|
||||||
): void => {
|
|
||||||
dispatch(setCurrentCurrency(safeAddress, selectedCurrency))
|
|
||||||
dispatch(fetchCurrencyRate(safeAddress, selectedCurrency))
|
|
||||||
}
|
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Action } from 'redux-actions'
|
||||||
|
import { ThunkDispatch } from 'redux-thunk'
|
||||||
|
import { AppReduxState } from 'src/store'
|
||||||
|
import { AvailableCurrenciesPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||||
|
import { setAvailableCurrencies } from 'src/logic/currencyValues/store/actions/setAvailableCurrencies'
|
||||||
|
import { fetchAvailableCurrencies } from 'src/logic/currencyValues/api/fetchAvailableCurrencies'
|
||||||
|
|
||||||
|
export const updateAvailableCurrencies = () => async (
|
||||||
|
dispatch: ThunkDispatch<AppReduxState, undefined, Action<AvailableCurrenciesPayload>>,
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const availableCurrencies = await fetchAvailableCurrencies()
|
||||||
|
dispatch(setAvailableCurrencies({ availableCurrencies }))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching available currencies', err)
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
@ -1,16 +1,15 @@
|
|||||||
import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
|
import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
|
||||||
import { saveSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
|
import { saveSelectedCurrency } from 'src/logic/safe/utils/currencyValuesStorage'
|
||||||
|
|
||||||
const watchedActions = [SET_CURRENT_CURRENCY]
|
const watchedActions = [SET_CURRENT_CURRENCY]
|
||||||
|
|
||||||
const currencyValuesStorageMiddleware = () => (next) => async (action) => {
|
export const currencyValuesStorageMiddleware = () => (next) => async (action) => {
|
||||||
const handledAction = next(action)
|
const handledAction = next(action)
|
||||||
if (watchedActions.includes(action.type)) {
|
if (watchedActions.includes(action.type)) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SET_CURRENT_CURRENCY: {
|
case SET_CURRENT_CURRENCY: {
|
||||||
const { selectedCurrency } = action.payload
|
const { selectedCurrency } = action.payload
|
||||||
|
await saveSelectedCurrency(selectedCurrency)
|
||||||
saveSelectedCurrency(selectedCurrency)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,5 +20,3 @@ const currencyValuesStorageMiddleware = () => (next) => async (action) => {
|
|||||||
|
|
||||||
return handledAction
|
return handledAction
|
||||||
}
|
}
|
||||||
|
|
||||||
export default currencyValuesStorageMiddleware
|
|
@ -1,66 +0,0 @@
|
|||||||
import { List, Record, RecordOf } from 'immutable'
|
|
||||||
|
|
||||||
import { getNetworkInfo } from 'src/config'
|
|
||||||
|
|
||||||
const { nativeCoin } = getNetworkInfo()
|
|
||||||
|
|
||||||
export const AVAILABLE_CURRENCIES = {
|
|
||||||
NETWORK: nativeCoin.symbol.toLocaleUpperCase(),
|
|
||||||
USD: 'USD',
|
|
||||||
EUR: 'EUR',
|
|
||||||
AUD: 'AUD',
|
|
||||||
BGN: 'BGN',
|
|
||||||
BRL: 'BRL',
|
|
||||||
CAD: 'CAD',
|
|
||||||
CHF: 'CHF',
|
|
||||||
CNY: 'CNY',
|
|
||||||
CZK: 'CZK',
|
|
||||||
DKK: 'DKK',
|
|
||||||
GBP: 'GBP',
|
|
||||||
HKD: 'HKD',
|
|
||||||
HRK: 'HRK',
|
|
||||||
HUF: 'HUF',
|
|
||||||
IDR: 'IDR',
|
|
||||||
ILS: 'ILS',
|
|
||||||
INR: 'INR',
|
|
||||||
ISK: 'ISK',
|
|
||||||
JPY: 'JPY',
|
|
||||||
KRW: 'KRW',
|
|
||||||
MXN: 'MXN',
|
|
||||||
MYR: 'MYR',
|
|
||||||
NOK: 'NOK',
|
|
||||||
NZD: 'NZD',
|
|
||||||
PHP: 'PHP',
|
|
||||||
PLN: 'PLN',
|
|
||||||
RON: 'RON',
|
|
||||||
RUB: 'RUB',
|
|
||||||
SEK: 'SEK',
|
|
||||||
SGD: 'SGD',
|
|
||||||
THB: 'THB',
|
|
||||||
TRY: 'TRY',
|
|
||||||
ZAR: 'ZAR',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export type BalanceCurrencyRecord = {
|
|
||||||
currencyName?: string
|
|
||||||
tokenAddress?: string
|
|
||||||
balanceInBaseCurrency: string
|
|
||||||
balanceInSelectedCurrency: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeBalanceCurrency = Record<BalanceCurrencyRecord>({
|
|
||||||
currencyName: '',
|
|
||||||
tokenAddress: '',
|
|
||||||
balanceInBaseCurrency: '',
|
|
||||||
balanceInSelectedCurrency: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
export type CurrencyRateValueRecord = RecordOf<BalanceCurrencyRecord>
|
|
||||||
|
|
||||||
export type BalanceCurrencyList = List<CurrencyRateValueRecord>
|
|
||||||
|
|
||||||
export interface CurrencyRateValue {
|
|
||||||
currencyRate?: number
|
|
||||||
selectedCurrency?: string
|
|
||||||
currencyBalances?: BalanceCurrencyList
|
|
||||||
}
|
|
@ -1,44 +1,35 @@
|
|||||||
import { Map } from 'immutable'
|
|
||||||
import { Action, handleActions } from 'redux-actions'
|
import { Action, handleActions } from 'redux-actions'
|
||||||
|
|
||||||
import { SET_CURRENCY_BALANCES } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
|
|
||||||
import { SET_CURRENCY_RATE } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
|
|
||||||
import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
|
import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
|
||||||
import { BalanceCurrencyList, CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
|
import { AppReduxState } from 'src/store'
|
||||||
|
import { SET_AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/actions/setAvailableCurrencies'
|
||||||
|
|
||||||
export const CURRENCY_VALUES_KEY = 'currencyValues'
|
export const CURRENCY_VALUES_KEY = 'currencyValues'
|
||||||
|
|
||||||
export interface CurrencyReducerMap extends Map<string, any> {
|
export type CurrencyValuesState = {
|
||||||
get<K extends keyof CurrencyRateValue>(key: K, notSetValue?: unknown): CurrencyRateValue[K]
|
selectedCurrency: string
|
||||||
setIn<K extends keyof CurrencyRateValue>(keys: [string, K], value: CurrencyRateValue[K]): this
|
availableCurrencies: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CurrencyValuesState = Map<string, CurrencyReducerMap>
|
export const initialState = {
|
||||||
|
selectedCurrency: 'USD',
|
||||||
|
availableCurrencies: ['USD'],
|
||||||
|
}
|
||||||
|
|
||||||
type CurrencyBasePayload = { safeAddress: string }
|
export type SelectedCurrencyPayload = { selectedCurrency: string }
|
||||||
export type CurrencyRatePayload = CurrencyBasePayload & { currencyRate: number }
|
export type AvailableCurrenciesPayload = { availableCurrencies: string[] }
|
||||||
export type CurrencyBalancesPayload = CurrencyBasePayload & { currencyBalances: BalanceCurrencyList }
|
|
||||||
export type CurrentCurrencyPayload = CurrencyBasePayload & { selectedCurrency: string }
|
|
||||||
|
|
||||||
export type CurrencyPayloads = CurrencyRatePayload | CurrencyBalancesPayload | CurrentCurrencyPayload
|
export default handleActions<AppReduxState['currencyValues'], CurrencyValuesState>(
|
||||||
|
|
||||||
export default handleActions<CurrencyReducerMap, CurrencyPayloads>(
|
|
||||||
{
|
{
|
||||||
[SET_CURRENCY_RATE]: (state, action: Action<CurrencyRatePayload>) => {
|
[SET_CURRENT_CURRENCY]: (state, action: Action<SelectedCurrencyPayload>) => {
|
||||||
const { currencyRate, safeAddress } = action.payload
|
const { selectedCurrency } = action.payload
|
||||||
|
state.selectedCurrency = selectedCurrency
|
||||||
return state.setIn([safeAddress, 'currencyRate'], currencyRate)
|
return state
|
||||||
},
|
},
|
||||||
[SET_CURRENCY_BALANCES]: (state, action: Action<CurrencyBalancesPayload>) => {
|
[SET_AVAILABLE_CURRENCIES]: (state, action: Action<AvailableCurrenciesPayload>) => {
|
||||||
const { safeAddress, currencyBalances } = action.payload
|
const { availableCurrencies } = action.payload
|
||||||
|
state.availableCurrencies = availableCurrencies
|
||||||
return state.setIn([safeAddress, 'currencyBalances'], currencyBalances)
|
return state
|
||||||
},
|
|
||||||
[SET_CURRENT_CURRENCY]: (state, action: Action<CurrentCurrencyPayload>) => {
|
|
||||||
const { safeAddress, selectedCurrency } = action.payload
|
|
||||||
|
|
||||||
return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Map(),
|
initialState,
|
||||||
)
|
)
|
||||||
|
@ -1,53 +1,12 @@
|
|||||||
import { createSelector } from 'reselect'
|
|
||||||
|
|
||||||
import {
|
|
||||||
CURRENCY_VALUES_KEY,
|
|
||||||
CurrencyReducerMap,
|
|
||||||
CurrencyValuesState,
|
|
||||||
} from 'src/logic/currencyValues/store/reducer/currencyValues'
|
|
||||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
import { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
|
import { CURRENCY_VALUES_KEY, CurrencyValuesState } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||||
import { BigNumber } from 'bignumber.js'
|
|
||||||
|
|
||||||
export const currencyValuesSelector = (state: AppReduxState): CurrencyValuesState => state[CURRENCY_VALUES_KEY]
|
export const currencyValuesSelector = (state: AppReduxState): CurrencyValuesState => state[CURRENCY_VALUES_KEY]
|
||||||
|
|
||||||
export const safeFiatBalancesSelector = createSelector(
|
export const currentCurrencySelector = (state: AppReduxState): string => {
|
||||||
currencyValuesSelector,
|
return state[CURRENCY_VALUES_KEY].selectedCurrency
|
||||||
safeParamAddressFromStateSelector,
|
}
|
||||||
(currencyValues, safeAddress): CurrencyReducerMap | undefined => {
|
|
||||||
if (!currencyValues || !safeAddress) return
|
|
||||||
return currencyValues.get(safeAddress)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const currencyValueSelector = <K extends keyof CurrencyRateValue>(key: K) => (
|
export const availableCurrenciesSelector = (state: AppReduxState): string[] => {
|
||||||
currencyValuesMap?: CurrencyReducerMap,
|
return state[CURRENCY_VALUES_KEY].availableCurrencies
|
||||||
): CurrencyRateValue[K] => currencyValuesMap?.get(key)
|
}
|
||||||
|
|
||||||
export const safeFiatBalancesListSelector = createSelector(
|
|
||||||
safeFiatBalancesSelector,
|
|
||||||
currencyValueSelector('currencyBalances'),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const currentCurrencySelector = createSelector(
|
|
||||||
safeFiatBalancesSelector,
|
|
||||||
currencyValueSelector('selectedCurrency'),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const currencyRateSelector = createSelector(safeFiatBalancesSelector, currencyValueSelector('currencyRate'))
|
|
||||||
|
|
||||||
export const safeFiatBalancesTotalSelector = createSelector(
|
|
||||||
safeFiatBalancesListSelector,
|
|
||||||
currencyRateSelector,
|
|
||||||
(currencyBalances, currencyRate): string | null => {
|
|
||||||
if (!currencyBalances) return '0'
|
|
||||||
if (!currencyRate) return null
|
|
||||||
|
|
||||||
const totalInBaseCurrency = currencyBalances.reduce((total, balanceCurrencyRecord) => {
|
|
||||||
return total.plus(balanceCurrencyRecord.balanceInBaseCurrency)
|
|
||||||
}, new BigNumber(0))
|
|
||||||
|
|
||||||
return totalInBaseCurrency.times(currencyRate).toFixed(2)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
@ -218,10 +218,9 @@ export const useEstimateTransactionGas = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const fixedGasCosts = getFixedGasCosts(Number(threshold))
|
const fixedGasCosts = getFixedGasCosts(Number(threshold))
|
||||||
|
const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)
|
|
||||||
|
|
||||||
const gasEstimation = await estimateTransactionGas({
|
const gasEstimation = await estimateTransactionGas({
|
||||||
safeAddress,
|
safeAddress,
|
||||||
txRecipient,
|
txRecipient,
|
||||||
@ -279,7 +278,7 @@ export const useEstimateTransactionGas = ({
|
|||||||
gasLimit: '0',
|
gasLimit: '0',
|
||||||
isExecution,
|
isExecution,
|
||||||
isCreation,
|
isCreation,
|
||||||
isOffChainSignature: false,
|
isOffChainSignature,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import { getSafeClientGatewayBaseUrl } from 'src/config'
|
import { getSafeClientGatewayBaseUrl } from 'src/config'
|
||||||
import {
|
import { fetchTokenCurrenciesBalances } from 'src/logic/safe/api/fetchTokenCurrenciesBalances'
|
||||||
fetchTokenCurrenciesBalances,
|
|
||||||
BalanceEndpoint,
|
|
||||||
} from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
|
|
||||||
import { aNewStore } from 'src/store'
|
import { aNewStore } from 'src/store'
|
||||||
|
|
||||||
jest.mock('axios')
|
jest.mock('axios')
|
||||||
@ -52,11 +49,15 @@ describe('fetchTokenCurrenciesBalances', () => {
|
|||||||
axios.get.mockImplementationOnce(() => Promise.resolve({ data: expectedResult }))
|
axios.get.mockImplementationOnce(() => Promise.resolve({ data: expectedResult }))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await fetchTokenCurrenciesBalances(safeAddress, excludeSpamTokens)
|
const result = await fetchTokenCurrenciesBalances({
|
||||||
|
safeAddress,
|
||||||
|
excludeSpamTokens,
|
||||||
|
selectedCurrency: 'USD',
|
||||||
|
})
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result).toStrictEqual(expectedResult)
|
expect(result).toStrictEqual(expectedResult)
|
||||||
expect(axios.get).toHaveBeenCalled()
|
expect(axios.get).toHaveBeenCalled()
|
||||||
expect(axios.get).toBeCalledWith(`${apiUrl}/balances/usd/?trusted=false&exclude_spam=${excludeSpamTokens}`)
|
expect(axios.get).toBeCalledWith(`${apiUrl}/balances/USD/?trusted=false&exclude_spam=${excludeSpamTokens}`)
|
||||||
})
|
})
|
||||||
})
|
})
|
58
src/logic/safe/api/fetchTokenCurrenciesBalances.ts
Normal file
58
src/logic/safe/api/fetchTokenCurrenciesBalances.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { getSafeClientGatewayBaseUrl, getNetworkInfo } from 'src/config'
|
||||||
|
import { TokenProps } from 'src/logic/tokens/store/model/token'
|
||||||
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
|
|
||||||
|
import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
|
|
||||||
|
export type TokenBalance = {
|
||||||
|
tokenInfo: TokenProps
|
||||||
|
balance: string
|
||||||
|
fiatBalance: string
|
||||||
|
fiatConversion: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BalanceEndpoint = {
|
||||||
|
fiatTotal: string
|
||||||
|
items: TokenBalance[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetchTokenCurrenciesBalancesProps = {
|
||||||
|
safeAddress: string
|
||||||
|
selectedCurrency: string
|
||||||
|
excludeSpamTokens?: boolean
|
||||||
|
trustedTokens?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchTokenCurrenciesBalances = async ({
|
||||||
|
safeAddress,
|
||||||
|
selectedCurrency,
|
||||||
|
excludeSpamTokens = true,
|
||||||
|
trustedTokens = false,
|
||||||
|
}: FetchTokenCurrenciesBalancesProps): Promise<BalanceEndpoint> => {
|
||||||
|
const url = `${getSafeClientGatewayBaseUrl(
|
||||||
|
checksumAddress(safeAddress),
|
||||||
|
)}/balances/${selectedCurrency}/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}`
|
||||||
|
|
||||||
|
return axios.get(url).then(({ data }) => {
|
||||||
|
// Currently the client-gateway is not returning the balance using network token symbol and name
|
||||||
|
// FIXME remove this logic and return data directly once this is fixed
|
||||||
|
const { nativeCoin } = getNetworkInfo()
|
||||||
|
|
||||||
|
if (data.items && data.items.length) {
|
||||||
|
data.items = data.items.map((element) => {
|
||||||
|
const { tokenInfo } = element
|
||||||
|
if (sameAddress(ZERO_ADDRESS, tokenInfo.address)) {
|
||||||
|
// If it's native coin we swap symbol and name
|
||||||
|
tokenInfo.symbol = nativeCoin.symbol
|
||||||
|
tokenInfo.name = nativeCoin.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return element
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
}
|
@ -1,35 +1,32 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { batch, useDispatch } from 'react-redux'
|
import { batch, useDispatch, useSelector } from 'react-redux'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles'
|
import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles'
|
||||||
import { fetchSelectedCurrency } from 'src/logic/currencyValues/store/actions/fetchSelectedCurrency'
|
import { fetchSelectedCurrency } from 'src/logic/currencyValues/store/actions/fetchSelectedCurrency'
|
||||||
import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAssetsByBalance'
|
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||||
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
|
||||||
import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens'
|
import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens'
|
||||||
import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances'
|
import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances'
|
||||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||||
|
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
|
||||||
|
|
||||||
export const useFetchTokens = (safeAddress: string): void => {
|
export const useFetchTokens = (safeAddress: string): void => {
|
||||||
const dispatch = useDispatch<Dispatch>()
|
const dispatch = useDispatch<Dispatch>()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const currentCurrency = useSelector(currentCurrencySelector)
|
||||||
|
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
if (COINS_LOCATION_REGEX.test(location.pathname)) {
|
if (COINS_LOCATION_REGEX.test(location.pathname)) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
// fetch tokens there to get symbols for tokens in TXs list
|
// fetch tokens there to get symbols for tokens in TXs list
|
||||||
dispatch(fetchTokens())
|
dispatch(fetchTokens())
|
||||||
dispatch(fetchSelectedCurrency(safeAddress))
|
dispatch(fetchSelectedCurrency())
|
||||||
dispatch(fetchSafeTokens(safeAddress))
|
dispatch(fetchSafeTokens(safeAddress, currentCurrency))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (COLLECTIBLES_LOCATION_REGEX.test(location.pathname)) {
|
if (COLLECTIBLES_LOCATION_REGEX.test(location.pathname)) {
|
||||||
batch(() => {
|
dispatch(fetchCollectibles(safeAddress))
|
||||||
dispatch(fetchCollectibles(safeAddress)).then(() => {
|
|
||||||
dispatch(activateAssetsByBalance(safeAddress))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, [dispatch, location.pathname, safeAddress])
|
}, [dispatch, location.pathname, safeAddress, currentCurrency])
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,12 @@ import { useDispatch } from 'react-redux'
|
|||||||
|
|
||||||
import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage'
|
import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage'
|
||||||
import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe'
|
import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe'
|
||||||
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||||
import fetchLatestMasterContractVersion from 'src/logic/safe/store/actions/fetchLatestMasterContractVersion'
|
import fetchLatestMasterContractVersion from 'src/logic/safe/store/actions/fetchLatestMasterContractVersion'
|
||||||
import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
|
import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
|
||||||
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
|
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
|
||||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||||
|
import { updateAvailableCurrencies } from 'src/logic/currencyValues/store/actions/updateAvailableCurrencies'
|
||||||
|
|
||||||
export const useLoadSafe = (safeAddress?: string): boolean => {
|
export const useLoadSafe = (safeAddress?: string): boolean => {
|
||||||
const dispatch = useDispatch<Dispatch>()
|
const dispatch = useDispatch<Dispatch>()
|
||||||
@ -20,7 +21,8 @@ export const useLoadSafe = (safeAddress?: string): boolean => {
|
|||||||
await dispatch(fetchSafe(safeAddress))
|
await dispatch(fetchSafe(safeAddress))
|
||||||
setIsSafeLoaded(true)
|
setIsSafeLoaded(true)
|
||||||
await dispatch(fetchSafeTokens(safeAddress))
|
await dispatch(fetchSafeTokens(safeAddress))
|
||||||
dispatch(fetchTransactions(safeAddress))
|
await dispatch(updateAvailableCurrencies())
|
||||||
|
await dispatch(fetchTransactions(safeAddress))
|
||||||
dispatch(addViewedSafe(safeAddress))
|
dispatch(addViewedSafe(safeAddress))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,7 @@ import { useEffect, useRef } from 'react'
|
|||||||
import { batch, useDispatch } from 'react-redux'
|
import { batch, useDispatch } from 'react-redux'
|
||||||
|
|
||||||
import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles'
|
import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles'
|
||||||
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||||
import fetchEtherBalance from 'src/logic/safe/store/actions/fetchEtherBalance'
|
|
||||||
import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe'
|
import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe'
|
||||||
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
|
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
|
||||||
import { TIMEOUT } from 'src/utils/constants'
|
import { TIMEOUT } from 'src/utils/constants'
|
||||||
@ -17,9 +16,8 @@ export const useSafeScheduledUpdates = (safeLoaded: boolean, safeAddress?: strin
|
|||||||
// has to run again
|
// has to run again
|
||||||
let mounted = true
|
let mounted = true
|
||||||
const fetchSafeData = async (address: string): Promise<void> => {
|
const fetchSafeData = async (address: string): Promise<void> => {
|
||||||
await batch(async () => {
|
batch(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
dispatch(fetchEtherBalance(address)),
|
|
||||||
dispatch(fetchSafeTokens(address)),
|
dispatch(fetchSafeTokens(address)),
|
||||||
dispatch(fetchTransactions(address)),
|
dispatch(fetchTransactions(address)),
|
||||||
dispatch(fetchCollectibles(address)),
|
dispatch(fetchCollectibles(address)),
|
||||||
@ -28,9 +26,7 @@ export const useSafeScheduledUpdates = (safeLoaded: boolean, safeAddress?: strin
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
timer.current = window.setTimeout(() => {
|
timer.current = window.setTimeout(() => fetchSafeData(address), TIMEOUT * 3)
|
||||||
fetchSafeData(address)
|
|
||||||
}, TIMEOUT * 3)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { getNetworkInfo } from 'src/config'
|
|
||||||
import { Token } from 'src/logic/tokens/store/model/token'
|
import { Token } from 'src/logic/tokens/store/model/token'
|
||||||
import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import { safeKnownCoins } from 'src/routes/safe/container/selector'
|
import { safeKnownCoins } from 'src/routes/safe/container/selector'
|
||||||
|
|
||||||
const { nativeCoin } = getNetworkInfo()
|
|
||||||
|
|
||||||
const useTokenInfo = (address: string): Token | undefined => {
|
const useTokenInfo = (address: string): Token | undefined => {
|
||||||
const tokens = useSelector(safeKnownCoins)
|
const tokens = useSelector(safeKnownCoins)
|
||||||
|
|
||||||
if (tokens) {
|
if (tokens) {
|
||||||
const tokenAddress = sameAddress(address, ZERO_ADDRESS) ? nativeCoin.address : address
|
return tokens.find((token) => sameAddress(token.address, address))
|
||||||
return tokens.find((token) => sameAddress(token.address, tokenAddress)) ?? undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { createAction } from 'redux-actions'
|
|
||||||
|
|
||||||
export const ACTIVATE_TOKEN_FOR_ALL_SAFES = 'ACTIVATE_TOKEN_FOR_ALL_SAFES'
|
|
||||||
|
|
||||||
const activateTokenForAllSafes = createAction(ACTIVATE_TOKEN_FOR_ALL_SAFES)
|
|
||||||
|
|
||||||
export default activateTokenForAllSafes
|
|
@ -2,6 +2,4 @@ import { createAction } from 'redux-actions'
|
|||||||
|
|
||||||
export const ADD_SAFE_OWNER = 'ADD_SAFE_OWNER'
|
export const ADD_SAFE_OWNER = 'ADD_SAFE_OWNER'
|
||||||
|
|
||||||
const addSafeOwner = createAction(ADD_SAFE_OWNER)
|
export const addSafeOwner = createAction(ADD_SAFE_OWNER)
|
||||||
|
|
||||||
export default addSafeOwner
|
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
tryOffchainSigning,
|
tryOffchainSigning,
|
||||||
} from 'src/logic/safe/transactions'
|
} from 'src/logic/safe/transactions'
|
||||||
import { estimateGasForTransactionCreation } from 'src/logic/safe/transactions/gas'
|
import { estimateGasForTransactionCreation } from 'src/logic/safe/transactions/gas'
|
||||||
|
import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx'
|
||||||
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
|
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
|
||||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||||
@ -148,6 +149,9 @@ export const createTransaction = (
|
|||||||
|
|
||||||
await saveTxToHistory({ ...txArgs, txHash, origin })
|
await saveTxToHistory({ ...txArgs, txHash, origin })
|
||||||
|
|
||||||
|
// store the pending transaction's nonce
|
||||||
|
isExecution && aboutToExecuteTx.setNonce(txArgs.nonce)
|
||||||
|
|
||||||
dispatch(fetchTransactions(safeAddress))
|
dispatch(fetchTransactions(safeAddress))
|
||||||
})
|
})
|
||||||
.on('error', (error) => {
|
.on('error', (error) => {
|
||||||
@ -156,10 +160,6 @@ export const createTransaction = (
|
|||||||
onError?.()
|
onError?.()
|
||||||
})
|
})
|
||||||
.then(async (receipt) => {
|
.then(async (receipt) => {
|
||||||
if (isExecution) {
|
|
||||||
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded))
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(fetchTransactions(safeAddress))
|
dispatch(fetchTransactions(safeAddress))
|
||||||
|
|
||||||
return receipt.transactionHash
|
return receipt.transactionHash
|
||||||
|
@ -5,7 +5,7 @@ import { Dispatch } from 'redux'
|
|||||||
import { backOff } from 'exponential-backoff'
|
import { backOff } from 'exponential-backoff'
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
|
|
||||||
const fetchEtherBalance = (safeAddress: string) => async (
|
export const fetchEtherBalance = (safeAddress: string) => async (
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
getState: () => AppReduxState,
|
getState: () => AppReduxState,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
@ -21,5 +21,3 @@ const fetchEtherBalance = (safeAddress: string) => async (
|
|||||||
console.error('Error when fetching Ether balance:', err)
|
console.error('Error when fetching Ether balance:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default fetchEtherBalance
|
|
||||||
|
@ -8,14 +8,14 @@ import { getLocalSafe, getSafeName } from 'src/logic/safe/utils'
|
|||||||
import { enabledFeatures, safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion'
|
import { enabledFeatures, safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion'
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3'
|
import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3'
|
||||||
import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
|
import { addSafeOwner } from 'src/logic/safe/store/actions/addSafeOwner'
|
||||||
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
|
import { removeSafeOwner } from 'src/logic/safe/store/actions/removeSafeOwner'
|
||||||
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
||||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
import { latestMasterContractVersionSelector } from 'src/logic/safe/store/selectors'
|
import { latestMasterContractVersionSelector, safeTotalFiatBalanceSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { getSafeInfo } from 'src/logic/safe/utils/safeInformation'
|
import { getSafeInfo } from 'src/logic/safe/utils/safeInformation'
|
||||||
import { getModules } from 'src/logic/safe/utils/modules'
|
import { getModules } from 'src/logic/safe/utils/modules'
|
||||||
import { getSpendingLimits } from 'src/logic/safe/utils/spendingLimits'
|
import { getSpendingLimits } from 'src/logic/safe/utils/spendingLimits'
|
||||||
@ -46,6 +46,7 @@ export const buildSafe = async (
|
|||||||
safeAdd: string,
|
safeAdd: string,
|
||||||
safeName: string,
|
safeName: string,
|
||||||
latestMasterContractVersion?: string,
|
latestMasterContractVersion?: string,
|
||||||
|
totalFiatBalance?: number,
|
||||||
): Promise<SafeRecordProps> => {
|
): Promise<SafeRecordProps> => {
|
||||||
const safeAddress = checksumAddress(safeAdd)
|
const safeAddress = checksumAddress(safeAdd)
|
||||||
|
|
||||||
@ -80,16 +81,14 @@ export const buildSafe = async (
|
|||||||
threshold,
|
threshold,
|
||||||
owners,
|
owners,
|
||||||
ethBalance,
|
ethBalance,
|
||||||
|
totalFiatBalance: totalFiatBalance || 0,
|
||||||
nonce,
|
nonce,
|
||||||
currentVersion: currentVersion ?? '',
|
currentVersion: currentVersion ?? '',
|
||||||
needsUpdate,
|
needsUpdate,
|
||||||
featuresEnabled,
|
featuresEnabled,
|
||||||
balances: localSafe?.balances || Map(),
|
balances: localSafe?.balances || Map(),
|
||||||
latestIncomingTxBlock: 0,
|
latestIncomingTxBlock: 0,
|
||||||
activeAssets: Set(),
|
|
||||||
activeTokens: Set(),
|
activeTokens: Set(),
|
||||||
blacklistedAssets: Set(),
|
|
||||||
blacklistedTokens: Set(),
|
|
||||||
modules,
|
modules,
|
||||||
spendingLimits,
|
spendingLimits,
|
||||||
}
|
}
|
||||||
@ -162,7 +161,8 @@ export default (safeAdd: string) => async (
|
|||||||
const safeAddress = checksumAddress(safeAdd)
|
const safeAddress = checksumAddress(safeAdd)
|
||||||
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'
|
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'
|
||||||
const latestMasterContractVersion = latestMasterContractVersionSelector(getState())
|
const latestMasterContractVersion = latestMasterContractVersionSelector(getState())
|
||||||
const safeProps = await buildSafe(safeAddress, safeName, latestMasterContractVersion)
|
const totalFiatBalance = safeTotalFiatBalanceSelector(getState())
|
||||||
|
const safeProps = await buildSafe(safeAddress, safeName, latestMasterContractVersion, totalFiatBalance)
|
||||||
|
|
||||||
// `updateSafe`, as `loadSafesFromStorage` will populate the store previous to this call
|
// `updateSafe`, as `loadSafesFromStorage` will populate the store previous to this call
|
||||||
// and `addSafe` will only add a newly non-existent safe
|
// and `addSafe` will only add a newly non-existent safe
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
} from 'src/logic/safe/safeTxSigner'
|
} from 'src/logic/safe/safeTxSigner'
|
||||||
import { getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions'
|
import { getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions'
|
||||||
import { tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner'
|
import { tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner'
|
||||||
|
import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx'
|
||||||
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
|
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
|
||||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||||
import { providerSelector } from 'src/logic/wallets/store/selectors'
|
import { providerSelector } from 'src/logic/wallets/store/selectors'
|
||||||
@ -117,8 +118,6 @@ export const processTransaction = ({
|
|||||||
|
|
||||||
dispatch(updateTransactionStatus({ txStatus: 'PENDING', safeAddress, nonce: tx.nonce, id: tx.id }))
|
dispatch(updateTransactionStatus({ txStatus: 'PENDING', safeAddress, nonce: tx.nonce, id: tx.id }))
|
||||||
await saveTxToHistory({ ...txArgs, signature })
|
await saveTxToHistory({ ...txArgs, signature })
|
||||||
// TODO: while we wait for the tx to be stored in the service and later update the tx info
|
|
||||||
// we should update the tx status in the store to disable owners' action buttons
|
|
||||||
|
|
||||||
dispatch(fetchTransactions(safeAddress))
|
dispatch(fetchTransactions(safeAddress))
|
||||||
return
|
return
|
||||||
@ -154,6 +153,10 @@ export const processTransaction = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await saveTxToHistory({ ...txArgs, txHash })
|
await saveTxToHistory({ ...txArgs, txHash })
|
||||||
|
|
||||||
|
// store the pending transaction's nonce
|
||||||
|
isExecution && aboutToExecuteTx.setNonce(txArgs.nonce)
|
||||||
|
|
||||||
dispatch(fetchTransactions(safeAddress))
|
dispatch(fetchTransactions(safeAddress))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@ -172,10 +175,6 @@ export const processTransaction = ({
|
|||||||
console.error('Processing transaction error: ', error)
|
console.error('Processing transaction error: ', error)
|
||||||
})
|
})
|
||||||
.then(async (receipt) => {
|
.then(async (receipt) => {
|
||||||
if (isExecution) {
|
|
||||||
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded))
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(fetchTransactions(safeAddress))
|
dispatch(fetchTransactions(safeAddress))
|
||||||
|
|
||||||
if (isExecution) {
|
if (isExecution) {
|
||||||
|
@ -2,6 +2,4 @@ import { createAction } from 'redux-actions'
|
|||||||
|
|
||||||
export const REMOVE_SAFE_OWNER = 'REMOVE_SAFE_OWNER'
|
export const REMOVE_SAFE_OWNER = 'REMOVE_SAFE_OWNER'
|
||||||
|
|
||||||
const removeSafeOwner = createAction(REMOVE_SAFE_OWNER)
|
export const removeSafeOwner = createAction(REMOVE_SAFE_OWNER)
|
||||||
|
|
||||||
export default removeSafeOwner
|
|
||||||
|
@ -2,6 +2,4 @@ import { createAction } from 'redux-actions'
|
|||||||
|
|
||||||
export const REPLACE_SAFE_OWNER = 'REPLACE_SAFE_OWNER'
|
export const REPLACE_SAFE_OWNER = 'REPLACE_SAFE_OWNER'
|
||||||
|
|
||||||
const replaceSafeOwner = createAction(REPLACE_SAFE_OWNER)
|
export const replaceSafeOwner = createAction(REPLACE_SAFE_OWNER)
|
||||||
|
|
||||||
export default replaceSafeOwner
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import { Set } from 'immutable'
|
|
||||||
import updateAssetsList from './updateAssetsList'
|
|
||||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
|
||||||
|
|
||||||
const updateActiveAssets = (safeAddress: string, activeAssets: Set<string>) => (dispatch: Dispatch): void => {
|
|
||||||
dispatch(updateAssetsList({ safeAddress, activeAssets }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updateActiveAssets
|
|
@ -1,19 +0,0 @@
|
|||||||
import { Set } from 'immutable'
|
|
||||||
import updateTokensList from './updateTokensList'
|
|
||||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
|
||||||
|
|
||||||
// the selector uses ownProps argument/router props to get the address of the safe
|
|
||||||
// so in order to use it I had to recreate the same structure
|
|
||||||
// const generateMatchProps = (safeAddress: string) => ({
|
|
||||||
// match: {
|
|
||||||
// params: {
|
|
||||||
// [SAFE_PARAM_ADDRESS]: safeAddress,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
|
|
||||||
const updateActiveTokens = (safeAddress: string, activeTokens: Set<string>) => (dispatch: Dispatch): void => {
|
|
||||||
dispatch(updateTokensList({ safeAddress, activeTokens }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updateActiveTokens
|
|
@ -1,7 +0,0 @@
|
|||||||
import { createAction } from 'redux-actions'
|
|
||||||
|
|
||||||
export const UPDATE_ASSETS_LIST = 'UPDATE_ASSETS_LIST'
|
|
||||||
|
|
||||||
const updateAssetsList = createAction(UPDATE_ASSETS_LIST)
|
|
||||||
|
|
||||||
export default updateAssetsList
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Set } from 'immutable'
|
|
||||||
import updateAssetsList from './updateAssetsList'
|
|
||||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
|
||||||
|
|
||||||
const updateBlacklistedAssets = (safeAddress: string, blacklistedAssets: Set<string>) => (dispatch: Dispatch): void => {
|
|
||||||
dispatch(updateAssetsList({ safeAddress, blacklistedAssets }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updateBlacklistedAssets
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Set } from 'immutable'
|
|
||||||
import updateTokensList from './updateTokensList'
|
|
||||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
|
||||||
|
|
||||||
const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set<string>) => (dispatch: Dispatch): void => {
|
|
||||||
dispatch(updateTokensList({ safeAddress, blacklistedTokens }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updateBlacklistedTokens
|
|
@ -1,7 +0,0 @@
|
|||||||
import { createAction } from 'redux-actions'
|
|
||||||
|
|
||||||
export const UPDATE_TOKENS_LIST = 'UPDATE_TOKENS_LIST'
|
|
||||||
|
|
||||||
const updateTokenList = createAction(UPDATE_TOKENS_LIST)
|
|
||||||
|
|
||||||
export default updateTokenList
|
|
@ -1,4 +1,5 @@
|
|||||||
import { push } from 'connected-react-router'
|
import { push } from 'connected-react-router'
|
||||||
|
import { Action } from 'redux-actions'
|
||||||
|
|
||||||
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
|
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
|
||||||
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
|
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
|
||||||
@ -8,14 +9,19 @@ import { getSafeVersionInfo } from 'src/logic/safe/utils/safeVersion'
|
|||||||
import { isUserAnOwner } from 'src/logic/wallets/ethAddresses'
|
import { isUserAnOwner } from 'src/logic/wallets/ethAddresses'
|
||||||
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||||
import { ADD_QUEUED_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
|
import {
|
||||||
|
ADD_QUEUED_TRANSACTIONS,
|
||||||
|
ADD_HISTORY_TRANSACTIONS,
|
||||||
|
} from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
|
||||||
|
import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx'
|
||||||
|
import { QueuedPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'
|
||||||
import { safeParamAddressFromStateSelector, safesMapSelector } from 'src/logic/safe/store/selectors'
|
import { safeParamAddressFromStateSelector, safesMapSelector } from 'src/logic/safe/store/selectors'
|
||||||
|
|
||||||
import { isTransactionSummary } from 'src/logic/safe/store/models/types/gateway.d'
|
import { isTransactionSummary, TransactionGatewayResult } from 'src/logic/safe/store/models/types/gateway.d'
|
||||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||||
import { ADD_OR_UPDATE_SAFE } from '../actions/addOrUpdateSafe'
|
import { ADD_OR_UPDATE_SAFE } from '../actions/addOrUpdateSafe'
|
||||||
|
|
||||||
const watchedActions = [ADD_OR_UPDATE_SAFE, ADD_QUEUED_TRANSACTIONS]
|
const watchedActions = [ADD_OR_UPDATE_SAFE, ADD_QUEUED_TRANSACTIONS, ADD_HISTORY_TRANSACTIONS]
|
||||||
|
|
||||||
const LAST_TIME_USED_LOGGED_IN_ID = 'LAST_TIME_USED_LOGGED_IN_ID'
|
const LAST_TIME_USED_LOGGED_IN_ID = 'LAST_TIME_USED_LOGGED_IN_ID'
|
||||||
|
|
||||||
@ -70,9 +76,21 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
|
|||||||
const state = store.getState()
|
const state = store.getState()
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case ADD_HISTORY_TRANSACTIONS: {
|
||||||
|
const userAddress: string = userAccountSelector(state)
|
||||||
|
const safes = safesMapSelector(state)
|
||||||
|
|
||||||
|
const executedTxNotification = aboutToExecuteTx.getNotification(action.payload, userAddress, safes)
|
||||||
|
// if we have a notification, dispatch it depending on transaction's status
|
||||||
|
executedTxNotification && dispatch(enqueueSnackbar(executedTxNotification))
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
case ADD_QUEUED_TRANSACTIONS: {
|
case ADD_QUEUED_TRANSACTIONS: {
|
||||||
const { safeAddress, values } = action.payload
|
const { safeAddress, values } = (action as Action<QueuedPayload>).payload
|
||||||
const transactions = values.filter((tx) => isTransactionSummary(tx)).map((item) => item.transaction)
|
const transactions = values
|
||||||
|
.filter((tx) => isTransactionSummary(tx))
|
||||||
|
.map((item: TransactionGatewayResult) => item.transaction)
|
||||||
const userAddress: string = userAccountSelector(state)
|
const userAddress: string = userAccountSelector(state)
|
||||||
const awaitingTransactions = getAwaitingGatewayTransactions(transactions, userAddress)
|
const awaitingTransactions = getAwaitingGatewayTransactions(transactions, userAddress)
|
||||||
|
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
|
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
|
||||||
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
|
||||||
import { saveActiveTokens } from 'src/logic/tokens/utils/tokensStorage'
|
|
||||||
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
|
|
||||||
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
|
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
|
||||||
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
|
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
|
||||||
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
||||||
@ -9,9 +6,7 @@ import { REMOVE_SAFE_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner'
|
|||||||
import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner'
|
import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner'
|
||||||
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||||
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
||||||
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
|
import { safesMapSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
|
|
||||||
import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors'
|
|
||||||
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||||
@ -26,28 +21,10 @@ const watchedActions = [
|
|||||||
REMOVE_SAFE_OWNER,
|
REMOVE_SAFE_OWNER,
|
||||||
REPLACE_SAFE_OWNER,
|
REPLACE_SAFE_OWNER,
|
||||||
EDIT_SAFE_OWNER,
|
EDIT_SAFE_OWNER,
|
||||||
ACTIVATE_TOKEN_FOR_ALL_SAFES,
|
|
||||||
UPDATE_TOKENS_LIST,
|
|
||||||
UPDATE_ASSETS_LIST,
|
|
||||||
SET_DEFAULT_SAFE,
|
SET_DEFAULT_SAFE,
|
||||||
]
|
]
|
||||||
|
|
||||||
const recalculateActiveTokens = (state) => {
|
export const safeStorageMiddleware = (store) => (next) => async (action) => {
|
||||||
const tokens = tokensSelector(state)
|
|
||||||
const activeTokenAddresses = getActiveTokensAddressesForAllSafes(state)
|
|
||||||
|
|
||||||
const activeTokens = tokens.withMutations((map) => {
|
|
||||||
map.forEach((token) => {
|
|
||||||
if (!activeTokenAddresses.has(token.address)) {
|
|
||||||
map.remove(token.address)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
saveActiveTokens(activeTokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
const safeStorageMware = (store) => (next) => async (action) => {
|
|
||||||
const handledAction = next(action)
|
const handledAction = next(action)
|
||||||
|
|
||||||
if (watchedActions.includes(action.type)) {
|
if (watchedActions.includes(action.type)) {
|
||||||
@ -57,10 +34,6 @@ const safeStorageMware = (store) => (next) => async (action) => {
|
|||||||
await saveSafes(safes.toJSON())
|
await saveSafes(safes.toJSON())
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ACTIVATE_TOKEN_FOR_ALL_SAFES: {
|
|
||||||
recalculateActiveTokens(state)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case ADD_OR_UPDATE_SAFE: {
|
case ADD_OR_UPDATE_SAFE: {
|
||||||
const { safe } = action.payload
|
const { safe } = action.payload
|
||||||
safe.owners.forEach((owner) => {
|
safe.owners.forEach((owner) => {
|
||||||
@ -72,10 +45,7 @@ const safeStorageMware = (store) => (next) => async (action) => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case UPDATE_SAFE: {
|
case UPDATE_SAFE: {
|
||||||
const { activeTokens, name, address } = action.payload
|
const { name, address } = action.payload
|
||||||
if (activeTokens) {
|
|
||||||
recalculateActiveTokens(state)
|
|
||||||
}
|
|
||||||
if (name) {
|
if (name) {
|
||||||
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address })))
|
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address })))
|
||||||
}
|
}
|
||||||
@ -94,5 +64,3 @@ const safeStorageMware = (store) => (next) => async (action) => {
|
|||||||
|
|
||||||
return handledAction
|
return handledAction
|
||||||
}
|
}
|
||||||
|
|
||||||
export default safeStorageMware
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { List, Map, Record, RecordOf, Set } from 'immutable'
|
import { List, Map, Record, RecordOf, Set } from 'immutable'
|
||||||
import { FEATURES } from 'src/config/networks/network.d'
|
import { FEATURES } from 'src/config/networks/network.d'
|
||||||
|
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||||
|
|
||||||
export type SafeOwner = {
|
export type SafeOwner = {
|
||||||
name: string
|
name: string
|
||||||
@ -28,14 +29,12 @@ export type SafeRecordProps = {
|
|||||||
address: string
|
address: string
|
||||||
threshold: number
|
threshold: number
|
||||||
ethBalance: string
|
ethBalance: string
|
||||||
|
totalFiatBalance: number
|
||||||
owners: List<SafeOwner>
|
owners: List<SafeOwner>
|
||||||
modules?: ModulePair[] | null
|
modules?: ModulePair[] | null
|
||||||
spendingLimits?: SpendingLimit[] | null
|
spendingLimits?: SpendingLimit[] | null
|
||||||
activeTokens: Set<string>
|
activeTokens: Set<string>
|
||||||
activeAssets: Set<string>
|
balances: Map<string, BalanceRecord>
|
||||||
blacklistedTokens: Set<string>
|
|
||||||
blacklistedAssets: Set<string>
|
|
||||||
balances: Map<string, string>
|
|
||||||
nonce: number
|
nonce: number
|
||||||
latestIncomingTxBlock: number
|
latestIncomingTxBlock: number
|
||||||
recurringUser?: boolean
|
recurringUser?: boolean
|
||||||
@ -49,13 +48,11 @@ const makeSafe = Record<SafeRecordProps>({
|
|||||||
address: '',
|
address: '',
|
||||||
threshold: 0,
|
threshold: 0,
|
||||||
ethBalance: '0',
|
ethBalance: '0',
|
||||||
|
totalFiatBalance: 0,
|
||||||
owners: List([]),
|
owners: List([]),
|
||||||
modules: [],
|
modules: [],
|
||||||
spendingLimits: [],
|
spendingLimits: [],
|
||||||
activeTokens: Set(),
|
activeTokens: Set(),
|
||||||
activeAssets: Set(),
|
|
||||||
blacklistedTokens: Set(),
|
|
||||||
blacklistedAssets: Set(),
|
|
||||||
balances: Map(),
|
balances: Map(),
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
latestIncomingTxBlock: 0,
|
latestIncomingTxBlock: 0,
|
||||||
|
@ -344,6 +344,7 @@ export const gatewayTransactions = handleActions<AppReduxState['gatewayTransacti
|
|||||||
}
|
}
|
||||||
case 'queued.queued': {
|
case 'queued.queued': {
|
||||||
queued.queued[nonce] = queued.queued[nonce].map((txToUpdate) => {
|
queued.queued[nonce] = queued.queued[nonce].map((txToUpdate) => {
|
||||||
|
// TODO: review if is this `PENDING` status required under `queued.queued` list
|
||||||
// prevent setting `PENDING_FAILED` status, if previous status wasn't `PENDING`
|
// prevent setting `PENDING_FAILED` status, if previous status wasn't `PENDING`
|
||||||
if (txStatus === 'PENDING_FAILED' && txToUpdate.txStatus !== 'PENDING') {
|
if (txStatus === 'PENDING_FAILED' && txToUpdate.txStatus !== 'PENDING') {
|
||||||
return txToUpdate
|
return txToUpdate
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { Map, Set, List } from 'immutable'
|
import { Map, Set, List } from 'immutable'
|
||||||
import { Action, handleActions } from 'redux-actions'
|
import { Action, handleActions } from 'redux-actions'
|
||||||
|
|
||||||
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
|
|
||||||
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
|
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
|
||||||
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
|
import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
|
||||||
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe'
|
||||||
@ -10,8 +9,6 @@ import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwne
|
|||||||
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||||
import { SET_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion'
|
import { SET_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion'
|
||||||
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
||||||
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
|
|
||||||
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
|
|
||||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
||||||
import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
@ -28,9 +25,6 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
|
|||||||
const addresses = storedSafe.owners.map((owner) => checksumAddress(owner.address))
|
const addresses = storedSafe.owners.map((owner) => checksumAddress(owner.address))
|
||||||
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
||||||
const activeTokens = Set(storedSafe.activeTokens)
|
const activeTokens = Set(storedSafe.activeTokens)
|
||||||
const activeAssets = Set(storedSafe.activeAssets)
|
|
||||||
const blacklistedTokens = Set(storedSafe.blacklistedTokens)
|
|
||||||
const blacklistedAssets = Set(storedSafe.blacklistedAssets)
|
|
||||||
const balances = Map(storedSafe.balances)
|
const balances = Map(storedSafe.balances)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -38,9 +32,6 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
|
|||||||
owners,
|
owners,
|
||||||
balances,
|
balances,
|
||||||
activeTokens,
|
activeTokens,
|
||||||
blacklistedTokens,
|
|
||||||
activeAssets,
|
|
||||||
blacklistedAssets,
|
|
||||||
latestIncomingTxBlock: 0,
|
latestIncomingTxBlock: 0,
|
||||||
modules: null,
|
modules: null,
|
||||||
}
|
}
|
||||||
@ -102,21 +93,6 @@ export default handleActions<AppReduxState['safes'], Payloads>(
|
|||||||
)
|
)
|
||||||
: state
|
: state
|
||||||
},
|
},
|
||||||
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state, action: Action<SafeRecord>) => {
|
|
||||||
const tokenAddress = action.payload
|
|
||||||
|
|
||||||
return state.withMutations((map) => {
|
|
||||||
map
|
|
||||||
.get('safes')
|
|
||||||
.keySeq()
|
|
||||||
.forEach((safeAddress) => {
|
|
||||||
const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens'])
|
|
||||||
const activeTokens = safeActiveTokens.add(tokenAddress)
|
|
||||||
|
|
||||||
map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.mergeDeep({ activeTokens }))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[ADD_OR_UPDATE_SAFE]: (state, action: Action<SafePayload>) => {
|
[ADD_OR_UPDATE_SAFE]: (state, action: Action<SafePayload>) => {
|
||||||
const { safe } = action.payload
|
const { safe } = action.payload
|
||||||
const safeAddress = safe.address
|
const safeAddress = safe.address
|
||||||
@ -195,24 +171,6 @@ export default handleActions<AppReduxState['safes'], Payloads>(
|
|||||||
return prevSafe.merge({ owners: updatedOwners })
|
return prevSafe.merge({ owners: updatedOwners })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[UPDATE_TOKENS_LIST]: (state, action: Action<SafeWithAddressPayload>) => {
|
|
||||||
// Only activeTokens or blackListedTokens is required
|
|
||||||
const { safeAddress, activeTokens, blacklistedTokens } = action.payload
|
|
||||||
|
|
||||||
const key = activeTokens ? 'activeTokens' : 'blacklistedTokens'
|
|
||||||
const list = activeTokens ?? blacklistedTokens
|
|
||||||
|
|
||||||
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
|
|
||||||
},
|
|
||||||
[UPDATE_ASSETS_LIST]: (state, action: Action<SafeWithAddressPayload>) => {
|
|
||||||
// Only activeAssets or blackListedAssets is required
|
|
||||||
const { safeAddress, activeAssets, blacklistedAssets } = action.payload
|
|
||||||
|
|
||||||
const key = activeAssets ? 'activeAssets' : 'blacklistedAssets'
|
|
||||||
const list = activeAssets ?? blacklistedAssets
|
|
||||||
|
|
||||||
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
|
|
||||||
},
|
|
||||||
[SET_DEFAULT_SAFE]: (state, action: Action<SafeRecord>) => state.set('defaultSafe', action.payload),
|
[SET_DEFAULT_SAFE]: (state, action: Action<SafeRecord>) => state.set('defaultSafe', action.payload),
|
||||||
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action<SafeRecord>) =>
|
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action<SafeRecord>) =>
|
||||||
state.set('latestMasterContractVersion', action.payload),
|
state.set('latestMasterContractVersion', action.payload),
|
||||||
|
@ -76,51 +76,6 @@ export const safeActiveTokensSelector = createSelector(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export const safeActiveAssetsSelector = createSelector(
|
|
||||||
safeSelector,
|
|
||||||
(safe): Set<string> => {
|
|
||||||
if (!safe) {
|
|
||||||
return Set()
|
|
||||||
}
|
|
||||||
return safe.activeAssets
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const safeActiveAssetsListSelector = createSelector(safeActiveAssetsSelector, (safeList) => {
|
|
||||||
if (!safeList) {
|
|
||||||
return Set([])
|
|
||||||
}
|
|
||||||
return Set(safeList)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const safeBlacklistedTokensSelector = createSelector(
|
|
||||||
safeSelector,
|
|
||||||
(safe): Set<string> => {
|
|
||||||
if (!safe) {
|
|
||||||
return Set()
|
|
||||||
}
|
|
||||||
|
|
||||||
return safe.blacklistedTokens
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const safeBlacklistedAssetsSelector = createSelector(
|
|
||||||
safeSelector,
|
|
||||||
(safe): Set<string> => {
|
|
||||||
if (!safe) {
|
|
||||||
return Set()
|
|
||||||
}
|
|
||||||
|
|
||||||
return safe.blacklistedAssets
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
|
|
||||||
safes.get(safeAddress)?.get('activeAssets') || Set()
|
|
||||||
|
|
||||||
export const safeBlacklistedAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set<string> =>
|
|
||||||
safes.get(safeAddress)?.get('blacklistedAssets') || Set()
|
|
||||||
|
|
||||||
const baseSafe = makeSafe()
|
const baseSafe = makeSafe()
|
||||||
|
|
||||||
export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) => (
|
export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) => (
|
||||||
@ -172,14 +127,6 @@ export const getActiveTokensAddressesForAllSafes = createSelector(safesListSelec
|
|||||||
return addresses
|
return addresses
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getBlacklistedTokensAddressesForAllSafes = createSelector(safesListSelector, (safes) => {
|
export const safeTotalFiatBalanceSelector = createSelector(safeSelector, (currentSafe) => {
|
||||||
const addresses = Set().withMutations((set) => {
|
return currentSafe?.totalFiatBalance
|
||||||
safes.forEach((safe) => {
|
|
||||||
safe.blacklistedTokens.forEach((tokenAddress) => {
|
|
||||||
set.add(tokenAddress)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return addresses
|
|
||||||
})
|
})
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
import { Set, Map } from 'immutable'
|
|
||||||
import { aNewStore } from 'src/store'
|
|
||||||
import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens'
|
|
||||||
import '@testing-library/jest-dom/extend-expect'
|
|
||||||
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
|
||||||
import { makeToken } from 'src/logic/tokens/store/model/token'
|
|
||||||
import { safesMapSelector } from 'src/logic/safe/store/selectors'
|
|
||||||
|
|
||||||
describe('Feature > Balances', () => {
|
|
||||||
let store
|
|
||||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
|
||||||
beforeEach(async () => {
|
|
||||||
store = aNewStore()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('It should return an updated balance when updates active tokens', async () => {
|
|
||||||
// given
|
|
||||||
const tokensAmount = '100'
|
|
||||||
const token = makeToken({
|
|
||||||
address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9',
|
|
||||||
name: 'OmiseGo',
|
|
||||||
symbol: 'OMG',
|
|
||||||
decimals: 18,
|
|
||||||
logoUri:
|
|
||||||
'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
|
|
||||||
})
|
|
||||||
const balances = Map({
|
|
||||||
[token.address]: tokensAmount,
|
|
||||||
})
|
|
||||||
const expectedResult = '100'
|
|
||||||
|
|
||||||
// when
|
|
||||||
store.dispatch(updateSafe({ address: safeAddress, balances }))
|
|
||||||
store.dispatch(updateActiveTokens(safeAddress, Set([token.address])))
|
|
||||||
|
|
||||||
const safe = safesMapSelector(store.getState()).get(safeAddress)
|
|
||||||
const balanceResult = safe?.get('balances').get(token.address)
|
|
||||||
const activeTokens = safe?.get('activeTokens')
|
|
||||||
const tokenIsActive = activeTokens?.has(token.address)
|
|
||||||
|
|
||||||
// then
|
|
||||||
expect(balanceResult).toBe(expectedResult)
|
|
||||||
expect(tokenIsActive).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('The store should have an updated ether balance after updating the value', async () => {
|
|
||||||
// given
|
|
||||||
const etherAmount = '1'
|
|
||||||
const expectedResult = '1'
|
|
||||||
|
|
||||||
// when
|
|
||||||
store.dispatch(updateSafe({ address: safeAddress, ethBalance: etherAmount }))
|
|
||||||
const safe = safesMapSelector(store.getState()).get(safeAddress)
|
|
||||||
const balanceResult = safe?.get('ethBalance')
|
|
||||||
|
|
||||||
// then
|
|
||||||
expect(balanceResult).toBe(expectedResult)
|
|
||||||
})
|
|
||||||
})
|
|
@ -7,9 +7,6 @@ const getMockedOldSafe = ({
|
|||||||
needsUpdate,
|
needsUpdate,
|
||||||
balances,
|
balances,
|
||||||
recurringUser,
|
recurringUser,
|
||||||
blacklistedAssets,
|
|
||||||
blacklistedTokens,
|
|
||||||
activeAssets,
|
|
||||||
activeTokens,
|
activeTokens,
|
||||||
owners,
|
owners,
|
||||||
featuresEnabled,
|
featuresEnabled,
|
||||||
@ -32,10 +29,6 @@ const getMockedOldSafe = ({
|
|||||||
}
|
}
|
||||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
||||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
||||||
const mockedActiveAssetsAddress1 = '0x503ab2a6A70c6C6ec8b25a4C87C784e1c8f8e8CD'
|
|
||||||
const mockedActiveAssetsAddress2 = '0xfdd4E685361CB7E89a4D27e03DCd0001448d731F'
|
|
||||||
const mockedBlacklistedTokenAddress1 = '0xc7d892dca37a244Fb1A7461e6141e58Ead460282'
|
|
||||||
const mockedBlacklistedAssetAddress1 = '0x0ac539137c4c99001f16Dd132E282F99A02Ddc3F'
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: name || 'MockedSafe',
|
name: name || 'MockedSafe',
|
||||||
@ -46,14 +39,11 @@ const getMockedOldSafe = ({
|
|||||||
modules: modules || [],
|
modules: modules || [],
|
||||||
spendingLimits: spendingLimits || [],
|
spendingLimits: spendingLimits || [],
|
||||||
activeTokens: activeTokens || Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]),
|
activeTokens: activeTokens || Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]),
|
||||||
activeAssets: activeAssets || Set([mockedActiveAssetsAddress1, mockedActiveAssetsAddress2]),
|
|
||||||
blacklistedTokens: blacklistedTokens || Set([mockedBlacklistedTokenAddress1]),
|
|
||||||
blacklistedAssets: blacklistedAssets || Set([mockedBlacklistedAssetAddress1]),
|
|
||||||
balances:
|
balances:
|
||||||
balances ||
|
balances ||
|
||||||
Map({
|
Map({
|
||||||
[mockedActiveTokenAddress1]: '100',
|
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
|
||||||
[mockedActiveTokenAddress2]: '10',
|
[mockedActiveTokenAddress2]: { tokenBalance: '10' },
|
||||||
}),
|
}),
|
||||||
nonce: nonce || 2,
|
nonce: nonce || 2,
|
||||||
latestIncomingTxBlock: latestIncomingTxBlock || 1,
|
latestIncomingTxBlock: latestIncomingTxBlock || 1,
|
||||||
@ -61,6 +51,7 @@ const getMockedOldSafe = ({
|
|||||||
currentVersion: currentVersion || 'v1.1.1',
|
currentVersion: currentVersion || 'v1.1.1',
|
||||||
needsUpdate: needsUpdate || false,
|
needsUpdate: needsUpdate || false,
|
||||||
featuresEnabled: featuresEnabled || [],
|
featuresEnabled: featuresEnabled || [],
|
||||||
|
totalFiatBalance: 110,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,67 +194,16 @@ describe('shouldSafeStoreBeUpdated', () => {
|
|||||||
// Then
|
// Then
|
||||||
expect(expectedResult).toEqual(true)
|
expect(expectedResult).toEqual(true)
|
||||||
})
|
})
|
||||||
it(`Given an old activeAssets list and a new activeAssets list for the safe, should return true`, () => {
|
|
||||||
// given
|
|
||||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
|
||||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
|
||||||
const oldActiveAssets = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
|
|
||||||
const newActiveAssets = Set([mockedActiveTokenAddress1])
|
|
||||||
const oldSafe = getMockedOldSafe({ activeAssets: oldActiveAssets })
|
|
||||||
const newSafeProps: Partial<SafeRecordProps> = {
|
|
||||||
activeAssets: newActiveAssets,
|
|
||||||
}
|
|
||||||
|
|
||||||
// When
|
|
||||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(expectedResult).toEqual(true)
|
|
||||||
})
|
|
||||||
it(`Given an old blacklistedTokens list and a new blacklistedTokens list for the safe, should return true`, () => {
|
|
||||||
// given
|
|
||||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
|
||||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
|
||||||
const oldBlacklistedTokens = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
|
|
||||||
const newBlacklistedTokens = Set([mockedActiveTokenAddress1])
|
|
||||||
const oldSafe = getMockedOldSafe({ blacklistedTokens: oldBlacklistedTokens })
|
|
||||||
const newSafeProps: Partial<SafeRecordProps> = {
|
|
||||||
blacklistedTokens: newBlacklistedTokens,
|
|
||||||
}
|
|
||||||
|
|
||||||
// When
|
|
||||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(expectedResult).toEqual(true)
|
|
||||||
})
|
|
||||||
it(`Given an old blacklistedAssets list and a new blacklistedAssets list for the safe, should return true`, () => {
|
|
||||||
// given
|
|
||||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
|
||||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
|
||||||
const oldBlacklistedAssets = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
|
|
||||||
const newBlacklistedAssets = Set([mockedActiveTokenAddress1])
|
|
||||||
const oldSafe = getMockedOldSafe({ blacklistedAssets: oldBlacklistedAssets })
|
|
||||||
const newSafeProps: Partial<SafeRecordProps> = {
|
|
||||||
blacklistedAssets: newBlacklistedAssets,
|
|
||||||
}
|
|
||||||
|
|
||||||
// When
|
|
||||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(expectedResult).toEqual(true)
|
|
||||||
})
|
|
||||||
it(`Given an old balances list and a new balances list for the safe, should return true`, () => {
|
it(`Given an old balances list and a new balances list for the safe, should return true`, () => {
|
||||||
// given
|
// given
|
||||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
||||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
||||||
const oldBalances = Map({
|
const oldBalances = Map({
|
||||||
[mockedActiveTokenAddress1]: '100',
|
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
|
||||||
[mockedActiveTokenAddress2]: '10',
|
[mockedActiveTokenAddress2]: { tokenBalance: '100' },
|
||||||
})
|
})
|
||||||
const newBalances = Map({
|
const newBalances = Map({
|
||||||
[mockedActiveTokenAddress1]: '100',
|
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
|
||||||
})
|
})
|
||||||
const oldSafe = getMockedOldSafe({ balances: oldBalances })
|
const oldSafe = getMockedOldSafe({ balances: oldBalances })
|
||||||
const newSafeProps: Partial<SafeRecordProps> = {
|
const newSafeProps: Partial<SafeRecordProps> = {
|
||||||
|
51
src/logic/safe/utils/aboutToExecuteTx.ts
Normal file
51
src/logic/safe/utils/aboutToExecuteTx.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { getNotificationsFromTxType } from 'src/logic/notifications'
|
||||||
|
import {
|
||||||
|
isStatusFailed,
|
||||||
|
isTransactionSummary,
|
||||||
|
TransactionGatewayResult,
|
||||||
|
} from 'src/logic/safe/store/models/types/gateway.d'
|
||||||
|
import { HistoryPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'
|
||||||
|
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||||
|
import { isUserAnOwner } from 'src/logic/wallets/ethAddresses'
|
||||||
|
import { SafesMap } from 'src/routes/safe/store/reducer/types/safe'
|
||||||
|
|
||||||
|
let nonce: number | undefined
|
||||||
|
|
||||||
|
export const setNonce = (newNonce: typeof nonce): void => {
|
||||||
|
nonce = newNonce
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getNotification = (
|
||||||
|
{ safeAddress, values }: HistoryPayload,
|
||||||
|
userAddress: string,
|
||||||
|
safes: SafesMap,
|
||||||
|
): undefined => {
|
||||||
|
const currentSafe = safes.get(safeAddress)
|
||||||
|
|
||||||
|
// no notification if not in the current safe or if its not an owner
|
||||||
|
if (!currentSafe || !isUserAnOwner(currentSafe, userAddress)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have a nonce, then we have a tx that is about to be executed
|
||||||
|
if (nonce !== undefined) {
|
||||||
|
const executedTx = values
|
||||||
|
.filter(isTransactionSummary)
|
||||||
|
.map((item: TransactionGatewayResult) => item.transaction)
|
||||||
|
.find((transaction) => transaction.executionInfo?.nonce === nonce)
|
||||||
|
|
||||||
|
// transaction that was pending, was moved into history
|
||||||
|
// that is: it was executed
|
||||||
|
if (executedTx !== undefined) {
|
||||||
|
const notificationsQueue = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.STANDARD_TX)
|
||||||
|
const notification = isStatusFailed(executedTx.txStatus)
|
||||||
|
? notificationsQueue.afterExecutionError
|
||||||
|
: notificationsQueue.afterExecution.noMoreConfirmationsNeeded
|
||||||
|
|
||||||
|
// reset nonce value
|
||||||
|
setNonce(undefined)
|
||||||
|
|
||||||
|
return notification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import { BigNumber } from 'bignumber.js'
|
import { BigNumber } from 'bignumber.js'
|
||||||
import { getNetworkInfo } from 'src/config'
|
|
||||||
import { AbiItem } from 'web3-utils'
|
import { AbiItem } from 'web3-utils'
|
||||||
|
|
||||||
import { CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction'
|
import { CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction'
|
||||||
@ -9,7 +8,7 @@ import SpendingLimitModule from 'src/logic/contracts/artifacts/AllowanceModule.j
|
|||||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||||
import { getSpendingLimitContract, MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
|
import { getSpendingLimitContract, MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||||
import { SpendingLimit } from 'src/logic/safe/store/models/safe'
|
import { SpendingLimit } from 'src/logic/safe/store/models/safe'
|
||||||
import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||||
import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants'
|
import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants'
|
||||||
import { getEncodedMultiSendCallData, MultiSendTx } from './upgradeSafe'
|
import { getEncodedMultiSendCallData, MultiSendTx } from './upgradeSafe'
|
||||||
@ -138,16 +137,13 @@ type DeleteAllowanceParams = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getDeleteAllowanceTxData = ({ beneficiary, tokenAddress }: DeleteAllowanceParams): string => {
|
export const getDeleteAllowanceTxData = ({ beneficiary, tokenAddress }: DeleteAllowanceParams): string => {
|
||||||
const { nativeCoin } = getNetworkInfo()
|
|
||||||
const token = sameAddress(tokenAddress, nativeCoin.address) ? ZERO_ADDRESS : tokenAddress
|
|
||||||
|
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
const spendingLimitContract = new web3.eth.Contract(
|
const spendingLimitContract = new web3.eth.Contract(
|
||||||
SpendingLimitModule.abi as AbiItem[],
|
SpendingLimitModule.abi as AbiItem[],
|
||||||
SPENDING_LIMIT_MODULE_ADDRESS,
|
SPENDING_LIMIT_MODULE_ADDRESS,
|
||||||
)
|
)
|
||||||
|
|
||||||
return spendingLimitContract.methods.deleteAllowance(beneficiary, token).encodeABI()
|
return spendingLimitContract.methods.deleteAllowance(beneficiary, tokenAddress).encodeABI()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enableSpendingLimitModuleMultiSendTx = (safeAddress: string): MultiSendTx => {
|
export const enableSpendingLimitModuleMultiSendTx = (safeAddress: string): MultiSendTx => {
|
||||||
@ -188,20 +184,13 @@ export const setSpendingLimitTx = ({
|
|||||||
safeAddress,
|
safeAddress,
|
||||||
}: SpendingLimitTxParams): CreateTransactionArgs => {
|
}: SpendingLimitTxParams): CreateTransactionArgs => {
|
||||||
const spendingLimitContract = getSpendingLimitContract()
|
const spendingLimitContract = getSpendingLimitContract()
|
||||||
const { nativeCoin } = getNetworkInfo()
|
|
||||||
|
|
||||||
const txArgs: CreateTransactionArgs = {
|
const txArgs: CreateTransactionArgs = {
|
||||||
safeAddress,
|
safeAddress,
|
||||||
to: SPENDING_LIMIT_MODULE_ADDRESS,
|
to: SPENDING_LIMIT_MODULE_ADDRESS,
|
||||||
valueInWei: ZERO_VALUE,
|
valueInWei: ZERO_VALUE,
|
||||||
txData: spendingLimitContract.methods
|
txData: spendingLimitContract.methods
|
||||||
.setAllowance(
|
.setAllowance(beneficiary, token, spendingLimitInWei, resetTimeMin, resetBaseMin)
|
||||||
beneficiary,
|
|
||||||
token === nativeCoin.address ? ZERO_ADDRESS : token,
|
|
||||||
spendingLimitInWei,
|
|
||||||
resetTimeMin,
|
|
||||||
resetBaseMin,
|
|
||||||
)
|
|
||||||
.encodeABI(),
|
.encodeABI(),
|
||||||
operation: CALL,
|
operation: CALL,
|
||||||
notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX,
|
notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX,
|
||||||
@ -285,12 +274,5 @@ export const getSpendingLimitByTokenAddress = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nativeCoin } = getNetworkInfo()
|
return spendingLimits.find(({ token }) => sameAddress(token, tokenAddress))
|
||||||
|
|
||||||
return spendingLimits.find(({ token: spendingLimitTokenAddress }) => {
|
|
||||||
spendingLimitTokenAddress = sameAddress(spendingLimitTokenAddress, ZERO_ADDRESS)
|
|
||||||
? nativeCoin.address
|
|
||||||
: spendingLimitTokenAddress
|
|
||||||
return sameAddress(spendingLimitTokenAddress, tokenAddress)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
import { nftAssetsSelector } from 'src/logic/collectibles/store/selectors'
|
|
||||||
import updateActiveAssets from 'src/logic/safe/store/actions/updateActiveAssets'
|
|
||||||
import {
|
|
||||||
safeActiveAssetsSelectorBySafe,
|
|
||||||
safeBlacklistedAssetsSelectorBySafe,
|
|
||||||
safesMapSelector,
|
|
||||||
} from 'src/logic/safe/store/selectors'
|
|
||||||
|
|
||||||
const activateAssetsByBalance = (safeAddress) => async (dispatch, getState) => {
|
|
||||||
try {
|
|
||||||
const state = getState()
|
|
||||||
const safes = safesMapSelector(state)
|
|
||||||
|
|
||||||
if (safes.size === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableAssets = nftAssetsSelector(state)
|
|
||||||
const alreadyActiveAssets = safeActiveAssetsSelectorBySafe(safeAddress, safes)
|
|
||||||
const blacklistedAssets = safeBlacklistedAssetsSelectorBySafe(safeAddress, safes)
|
|
||||||
|
|
||||||
// active tokens by balance, excluding those already blacklisted and the `null` address
|
|
||||||
const activeByBalance = Object.entries(availableAssets)
|
|
||||||
.filter((asset) => {
|
|
||||||
const { address, numberOfTokens }: any = asset[1]
|
|
||||||
return address !== null && !blacklistedAssets.has(address) && numberOfTokens > 0
|
|
||||||
})
|
|
||||||
.map((asset) => {
|
|
||||||
return asset[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
// need to persist those already active assets, despite its balances
|
|
||||||
const activeAssets = alreadyActiveAssets.union(activeByBalance)
|
|
||||||
|
|
||||||
// update list of active tokens
|
|
||||||
dispatch(updateActiveAssets(safeAddress, activeAssets))
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching active assets list', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export default activateAssetsByBalance
|
|
@ -2,8 +2,6 @@ import { createAction } from 'redux-actions'
|
|||||||
|
|
||||||
export const ADD_TOKENS = 'ADD_TOKENS'
|
export const ADD_TOKENS = 'ADD_TOKENS'
|
||||||
|
|
||||||
const addTokens = createAction(ADD_TOKENS, (tokens) => ({
|
export const addTokens = createAction(ADD_TOKENS, (tokens) => ({
|
||||||
tokens,
|
tokens,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export default addTokens
|
|
@ -2,63 +2,56 @@ import { backOff } from 'exponential-backoff'
|
|||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
|
|
||||||
import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
|
import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/safe/api/fetchTokenCurrenciesBalances'
|
||||||
|
import { addTokens } from 'src/logic/tokens/store/actions/addTokens'
|
||||||
import {
|
|
||||||
AVAILABLE_CURRENCIES,
|
|
||||||
CurrencyRateValueRecord,
|
|
||||||
makeBalanceCurrency,
|
|
||||||
} from 'src/logic/currencyValues/store/model/currencyValues'
|
|
||||||
import addTokens from 'src/logic/tokens/store/actions/saveTokens'
|
|
||||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||||
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||||
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
|
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
|
||||||
import { safeActiveTokensSelector, safeBlacklistedTokensSelector, safeSelector } from 'src/logic/safe/store/selectors'
|
import { safeActiveTokensSelector, safeSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
|
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
|
||||||
import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
|
|
||||||
import { getNetworkInfo } from 'src/config'
|
export type BalanceRecord = {
|
||||||
|
tokenBalance: string
|
||||||
|
fiatBalance?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface ExtractedData {
|
interface ExtractedData {
|
||||||
balances: Map<string, string>
|
balances: Map<string, BalanceRecord>
|
||||||
currencyList: List<CurrencyRateValueRecord>
|
|
||||||
ethBalance: string
|
ethBalance: string
|
||||||
tokens: List<Token>
|
tokens: List<Token>
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nativeCoin } = getNetworkInfo()
|
const extractDataFromResult = (currentTokens: TokenState) => (
|
||||||
|
|
||||||
const extractDataFromResult = (currentTokens: TokenState, fiatCode: string) => (
|
|
||||||
acc: ExtractedData,
|
acc: ExtractedData,
|
||||||
{ balance, fiatBalance, tokenInfo }: TokenBalance,
|
{ balance, fiatBalance, tokenInfo }: TokenBalance,
|
||||||
): ExtractedData => {
|
): ExtractedData => {
|
||||||
const { address: tokenAddress, decimals } = tokenInfo
|
const { address, decimals } = tokenInfo
|
||||||
if (sameAddress(tokenAddress, ZERO_ADDRESS) || sameAddress(tokenAddress, nativeCoin.address)) {
|
|
||||||
acc.ethBalance = humanReadableValue(balance, 18)
|
|
||||||
} else {
|
|
||||||
acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableValue(balance, Number(decimals)) })
|
|
||||||
|
|
||||||
if (currentTokens && !currentTokens.get(tokenAddress)) {
|
acc.balances = acc.balances.merge({
|
||||||
acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo }))
|
[address]: {
|
||||||
}
|
fiatBalance,
|
||||||
|
tokenBalance: humanReadableValue(balance, Number(decimals)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Extract network token balance from backend balances
|
||||||
|
if (sameAddress(address, ZERO_ADDRESS)) {
|
||||||
|
acc.ethBalance = humanReadableValue(balance, Number(decimals))
|
||||||
}
|
}
|
||||||
|
|
||||||
acc.currencyList = acc.currencyList.push(
|
if (currentTokens && !currentTokens.get(address)) {
|
||||||
makeBalanceCurrency({
|
acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo }))
|
||||||
currencyName: fiatCode,
|
}
|
||||||
tokenAddress,
|
|
||||||
balanceInBaseCurrency: fiatBalance,
|
|
||||||
balanceInSelectedCurrency: fiatBalance,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchSafeTokens = (safeAddress: string) => async (
|
export const fetchSafeTokens = (safeAddress: string, currencySelected?: string) => async (
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
getState: () => AppReduxState,
|
getState: () => AppReduxState,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
@ -66,38 +59,40 @@ const fetchSafeTokens = (safeAddress: string) => async (
|
|||||||
const state = getState()
|
const state = getState()
|
||||||
const safe = safeSelector(state)
|
const safe = safeSelector(state)
|
||||||
const currentTokens = tokensSelector(state)
|
const currentTokens = tokensSelector(state)
|
||||||
const currencySelected = currentCurrencySelector(state)
|
|
||||||
|
|
||||||
if (!safe) {
|
if (!safe) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const selectedCurrency = currentCurrencySelector(state)
|
||||||
|
|
||||||
const tokenCurrenciesBalances = await backOff(() => fetchTokenCurrenciesBalances(safeAddress))
|
const tokenCurrenciesBalances = await backOff(() =>
|
||||||
|
fetchTokenCurrenciesBalances({ safeAddress, selectedCurrency: currencySelected ?? selectedCurrency }),
|
||||||
|
)
|
||||||
const alreadyActiveTokens = safeActiveTokensSelector(state)
|
const alreadyActiveTokens = safeActiveTokensSelector(state)
|
||||||
const blacklistedTokens = safeBlacklistedTokensSelector(state)
|
|
||||||
|
|
||||||
const { balances, currencyList, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>(
|
const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>(
|
||||||
extractDataFromResult(currentTokens, currencySelected || AVAILABLE_CURRENCIES.USD),
|
extractDataFromResult(currentTokens),
|
||||||
{
|
{
|
||||||
balances: Map(),
|
balances: Map(),
|
||||||
currencyList: List(),
|
|
||||||
ethBalance: '0',
|
ethBalance: '0',
|
||||||
tokens: List(),
|
tokens: List(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// need to persist those already active tokens, despite its balances
|
// need to persist those already active tokens, despite its balances
|
||||||
const activeTokens = alreadyActiveTokens.union(
|
const activeTokens = alreadyActiveTokens.union(balances.keySeq().toSet())
|
||||||
// active tokens by balance, excluding those already blacklisted and the `null` address
|
|
||||||
balances.keySeq().toSet().subtract(blacklistedTokens),
|
|
||||||
)
|
|
||||||
|
|
||||||
dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance }))
|
dispatch(
|
||||||
dispatch(setCurrencyBalances(safeAddress, currencyList))
|
updateSafe({
|
||||||
|
address: safeAddress,
|
||||||
|
activeTokens,
|
||||||
|
balances,
|
||||||
|
ethBalance,
|
||||||
|
totalFiatBalance: new BigNumber(tokenCurrenciesBalances.fiatTotal).toFixed(2),
|
||||||
|
}),
|
||||||
|
)
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default fetchSafeTokens
|
|
||||||
|
@ -5,9 +5,7 @@ import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721.json'
|
|||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import contract from '@truffle/contract/index.js'
|
import contract from '@truffle/contract/index.js'
|
||||||
import { AbiItem } from 'web3-utils'
|
import { AbiItem } from 'web3-utils'
|
||||||
|
import { addTokens } from 'src/logic/tokens/store/actions/addTokens'
|
||||||
import saveTokens from './saveTokens'
|
|
||||||
|
|
||||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||||
import { fetchErc20AndErc721AssetsList } from 'src/logic/tokens/api'
|
import { fetchErc20AndErc721AssetsList } from 'src/logic/tokens/api'
|
||||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||||
@ -85,7 +83,7 @@ export const getTokenInfos = async (tokenAddress: string): Promise<Token | undef
|
|||||||
})
|
})
|
||||||
|
|
||||||
const newTokens = tokens.set(tokenAddress, token)
|
const newTokens = tokens.set(tokenAddress, token)
|
||||||
store.dispatch(saveTokens(newTokens))
|
store.dispatch(addTokens(newTokens))
|
||||||
|
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
@ -109,10 +107,8 @@ export const fetchTokens = () => async (
|
|||||||
|
|
||||||
const tokens = List(erc20Tokens.map((token) => makeToken(token)))
|
const tokens = List(erc20Tokens.map((token) => makeToken(token)))
|
||||||
|
|
||||||
dispatch(saveTokens(tokens))
|
dispatch(addTokens(tokens))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching token list', err)
|
console.error('Error fetching token list', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default fetchTokens
|
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
import { List } from 'immutable'
|
|
||||||
|
|
||||||
import saveTokens from './saveTokens'
|
|
||||||
|
|
||||||
import { makeToken } from 'src/logic/tokens/store/model/token'
|
|
||||||
import { getActiveTokens } from 'src/logic/tokens/utils/tokensStorage'
|
|
||||||
|
|
||||||
const loadActiveTokens = () => async (dispatch) => {
|
|
||||||
try {
|
|
||||||
const tokens = (await getActiveTokens()) || {}
|
|
||||||
// The filter of strings was made because of the issue #751. Please see: https://github.com/gnosis/safe-react/pull/755#issuecomment-612969340
|
|
||||||
const tokenRecordsList = List(
|
|
||||||
Object.values(tokens)
|
|
||||||
.filter((t: any) => typeof t.decimals !== 'string')
|
|
||||||
.map((token) => makeToken(token)),
|
|
||||||
)
|
|
||||||
|
|
||||||
dispatch(saveTokens(tokenRecordsList))
|
|
||||||
} catch (err) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.error('Error while loading active tokens from storage:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default loadActiveTokens
|
|
@ -1,5 +1,6 @@
|
|||||||
import { Record, RecordOf } from 'immutable'
|
import { Record, RecordOf } from 'immutable'
|
||||||
import { TokenType } from 'src/logic/safe/store/models/types/gateway'
|
import { TokenType } from 'src/logic/safe/store/models/types/gateway'
|
||||||
|
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
|
||||||
|
|
||||||
export type TokenProps = {
|
export type TokenProps = {
|
||||||
address: string
|
address: string
|
||||||
@ -7,7 +8,7 @@ export type TokenProps = {
|
|||||||
symbol: string
|
symbol: string
|
||||||
decimals: number | string
|
decimals: number | string
|
||||||
logoUri: string
|
logoUri: string
|
||||||
balance: number | string
|
balance: BalanceRecord
|
||||||
type?: TokenType
|
type?: TokenType
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +18,10 @@ export const makeToken = Record<TokenProps>({
|
|||||||
symbol: '',
|
symbol: '',
|
||||||
decimals: 0,
|
decimals: 0,
|
||||||
logoUri: '',
|
logoUri: '',
|
||||||
balance: 0,
|
balance: {
|
||||||
|
fiatBalance: '0',
|
||||||
|
tokenBalance: '0',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
// balance is only set in extendedSafeTokensSelector when we display user's token balances
|
// balance is only set in extendedSafeTokensSelector when we display user's token balances
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { List, Map } from 'immutable'
|
|||||||
import { Action, handleActions } from 'redux-actions'
|
import { Action, handleActions } from 'redux-actions'
|
||||||
|
|
||||||
import { ADD_TOKEN } from 'src/logic/tokens/store/actions/addToken'
|
import { ADD_TOKEN } from 'src/logic/tokens/store/actions/addToken'
|
||||||
import { ADD_TOKENS } from 'src/logic/tokens/store/actions/saveTokens'
|
import { ADD_TOKENS } from 'src/logic/tokens/store/actions/addTokens'
|
||||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ describe('getERC20DecimalsAndSymbol', () => {
|
|||||||
symbol,
|
symbol,
|
||||||
decimals,
|
decimals,
|
||||||
logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png',
|
logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png',
|
||||||
balance: 0,
|
balance: { tokenBalance: '0', fiatBalance: '0' },
|
||||||
})
|
})
|
||||||
const expectedResult = {
|
const expectedResult = {
|
||||||
decimals,
|
decimals,
|
||||||
|
@ -15,7 +15,9 @@ export const getEthAsToken = (balance: string | number): Token => {
|
|||||||
const { nativeCoin } = getNetworkInfo()
|
const { nativeCoin } = getNetworkInfo()
|
||||||
return makeToken({
|
return makeToken({
|
||||||
...nativeCoin,
|
...nativeCoin,
|
||||||
balance,
|
balance: {
|
||||||
|
tokenBalance: balance.toString(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +75,7 @@ export type GetTokenByAddress = {
|
|||||||
tokens: List<Token>
|
tokens: List<Token>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TokenFound = {
|
type TokenFound = {
|
||||||
balance: string | number
|
balance: string | number
|
||||||
decimals: string | number
|
decimals: string | number
|
||||||
}
|
}
|
||||||
@ -92,7 +94,7 @@ export const getBalanceAndDecimalsFromToken = ({ tokenAddress, tokens }: GetToke
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
balance: token.balance ?? 0,
|
balance: token.balance.tokenBalance ?? 0,
|
||||||
decimals: token.decimals ?? 0,
|
decimals: token.decimals ?? 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
import { Map } from 'immutable'
|
|
||||||
|
|
||||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
|
||||||
import { TokenProps, Token } from './../store/model/token'
|
|
||||||
|
|
||||||
export const ACTIVE_TOKENS_KEY = 'ACTIVE_TOKENS'
|
|
||||||
export const CUSTOM_TOKENS_KEY = 'CUSTOM_TOKENS'
|
|
||||||
|
|
||||||
// Tokens which are active at least in one of used Safes in the app should be saved to localstorage
|
|
||||||
// to avoid iterating a large amount of data of tokens from the backend
|
|
||||||
// Custom tokens should be saved too unless they're deleted (marking them as inactive doesn't count)
|
|
||||||
|
|
||||||
export const saveActiveTokens = async (tokens: Map<string, Token>): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await saveToStorage(ACTIVE_TOKENS_KEY, tokens.toJS() as Record<string, TokenProps>)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error storing tokens in localstorage', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getActiveTokens = async (): Promise<Record<string, TokenProps> | undefined> => {
|
|
||||||
const data = await loadFromStorage<Record<string, TokenProps>>(ACTIVE_TOKENS_KEY)
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
@ -128,7 +128,7 @@ const ReviewComponent = ({ values, form }: ReviewComponentProps): ReactElement =
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row align="center" className={classes.info}>
|
<Row align="center" className={classes.info}>
|
||||||
<Paragraph color="primary" noMargin size="md">
|
<Paragraph color="primary" noMargin size="lg">
|
||||||
You're about to create a new Safe and will have to confirm a transaction with your currently connected
|
You're about to create a new Safe and will have to confirm a transaction with your currently connected
|
||||||
wallet. The creation will cost approximately {gasCostFormatted} {nativeCoin.name}. The exact amount will be
|
wallet. The creation will cost approximately {gasCostFormatted} {nativeCoin.name}. The exact amount will be
|
||||||
determined by your wallet.
|
determined by your wallet.
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import OpenPaper from 'src/components/Stepper/OpenPaper'
|
import OpenPaper from 'src/components/Stepper/OpenPaper'
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
@ -28,6 +29,12 @@ const styles = createStyles({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const StyledField = styled(Field)`
|
||||||
|
&.MuiTextField-root {
|
||||||
|
width: 460px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const useSafeNameStyles = makeStyles(styles)
|
const useSafeNameStyles = makeStyles(styles)
|
||||||
|
|
||||||
const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement => {
|
const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement => {
|
||||||
@ -36,13 +43,13 @@ const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement =>
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Block margin="lg">
|
<Block margin="lg">
|
||||||
<Paragraph color="primary" noMargin size="md">
|
<Paragraph color="primary" noMargin size="lg">
|
||||||
You are about to create a new Gnosis Safe wallet with one or more owners. First, let's give your new
|
You are about to create a new Gnosis Safe wallet with one or more owners. First, let's give your new
|
||||||
wallet a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
|
wallet a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
<Block className={classes.root} margin="lg">
|
<Block className={classes.root} margin="lg">
|
||||||
<Field
|
<StyledField
|
||||||
component={TextField}
|
component={TextField}
|
||||||
defaultValue={safeName}
|
defaultValue={safeName}
|
||||||
name={FIELD_NAME}
|
name={FIELD_NAME}
|
||||||
@ -54,7 +61,7 @@ const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement =>
|
|||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
<Block margin="lg">
|
<Block margin="lg">
|
||||||
<Paragraph className={classes.links} color="primary" noMargin size="md">
|
<Paragraph className={classes.links} color="primary" noMargin size="lg">
|
||||||
By continuing you consent to the{' '}
|
By continuing you consent to the{' '}
|
||||||
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
|
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
|
||||||
terms of use
|
terms of use
|
||||||
|
@ -4,8 +4,10 @@ import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
|
|||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import CheckCircle from '@material-ui/icons/CheckCircle'
|
import CheckCircle from '@material-ui/icons/CheckCircle'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { styles } from './style'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { styles } from './style'
|
||||||
|
import { padOwnerIndex } from 'src/routes/open/utils/padOwnerIndex'
|
||||||
import QRIcon from 'src/assets/icons/qrcode.svg'
|
import QRIcon from 'src/assets/icons/qrcode.svg'
|
||||||
import trash from 'src/assets/icons/trash.svg'
|
import trash from 'src/assets/icons/trash.svg'
|
||||||
import { ScanQRModal } from 'src/components/ScanQRModal'
|
import { ScanQRModal } from 'src/components/ScanQRModal'
|
||||||
@ -45,6 +47,10 @@ const { useState } = React
|
|||||||
|
|
||||||
export const ADD_OWNER_BUTTON = '+ Add another owner'
|
export const ADD_OWNER_BUTTON = '+ Add another owner'
|
||||||
|
|
||||||
|
const StyledAddressInput = styled(AddressInput)`
|
||||||
|
width: 460px;
|
||||||
|
`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the whole OwnersForm, specially checks for non-repeated addresses
|
* Validates the whole OwnersForm, specially checks for non-repeated addresses
|
||||||
*
|
*
|
||||||
@ -83,7 +89,7 @@ export const calculateValuesAfterRemoving = (index: number, values: Record<strin
|
|||||||
return newValues
|
return newValues
|
||||||
}
|
}
|
||||||
|
|
||||||
const ownerToRemove = new RegExp(`owner${index}(Name|Address)`)
|
const ownerToRemove = new RegExp(`owner${padOwnerIndex(index)}(Name|Address)`)
|
||||||
|
|
||||||
if (ownerToRemove.test(key)) {
|
if (ownerToRemove.test(key)) {
|
||||||
// skip, doing anything with the removed field
|
// skip, doing anything with the removed field
|
||||||
@ -96,7 +102,7 @@ export const calculateValuesAfterRemoving = (index: number, values: Record<strin
|
|||||||
|
|
||||||
if (Number(ownerOrder) > index) {
|
if (Number(ownerOrder) > index) {
|
||||||
// reduce by one the order of the owner
|
// reduce by one the order of the owner
|
||||||
newValues[`owner${Number(ownerOrder) - 1}${ownerField}`] = values[key]
|
newValues[`owner${padOwnerIndex(Number(ownerOrder) - 1)}${ownerField}`] = values[key]
|
||||||
} else {
|
} else {
|
||||||
// previous owners to the deleted row
|
// previous owners to the deleted row
|
||||||
newValues[key] = values[key]
|
newValues[key] = values[key]
|
||||||
@ -152,7 +158,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Block className={classes.title}>
|
<Block className={classes.title}>
|
||||||
<Paragraph color="primary" noMargin size="md" data-testid="create-safe-step-two">
|
<Paragraph color="primary" noMargin size="lg" data-testid="create-safe-step-two">
|
||||||
Your Safe will have one or more owners. We have prefilled the first owner with your connected wallet details,
|
Your Safe will have one or more owners. We have prefilled the first owner with your connected wallet details,
|
||||||
but you are free to change this to a different owner.
|
but you are free to change this to a different owner.
|
||||||
<br />
|
<br />
|
||||||
@ -167,7 +173,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
title="Learn about which Safe setup to use"
|
title="Learn about which Safe setup to use"
|
||||||
>
|
>
|
||||||
<Text size="lg" as="span" color="primary">
|
<Text size="xl" as="span" color="primary">
|
||||||
Learn about which Safe setup to use
|
Learn about which Safe setup to use
|
||||||
</Text>
|
</Text>
|
||||||
<Icon size="sm" type="externalLink" color="primary" />
|
<Icon size="sm" type="externalLink" color="primary" />
|
||||||
@ -176,8 +182,8 @@ const SafeOwnersForm = (props): React.ReactElement => {
|
|||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row className={classes.header}>
|
<Row className={classes.header}>
|
||||||
<Col xs={4}>NAME</Col>
|
<Col xs={3}>NAME</Col>
|
||||||
<Col xs={8}>ADDRESS</Col>
|
<Col xs={7}>ADDRESS</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Block margin="md" padding="md">
|
<Block margin="md" padding="md">
|
||||||
@ -187,7 +193,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={classes.owner} key={`owner${index}`} data-testid={`create-safe-owner-row`}>
|
<Row className={classes.owner} key={`owner${index}`} data-testid={`create-safe-owner-row`}>
|
||||||
<Col className={classes.ownerName} xs={4}>
|
<Col className={classes.ownerName} xs={3}>
|
||||||
<Field
|
<Field
|
||||||
className={classes.name}
|
className={classes.name}
|
||||||
component={TextField}
|
component={TextField}
|
||||||
@ -199,8 +205,8 @@ const SafeOwnersForm = (props): React.ReactElement => {
|
|||||||
testId={`create-safe-owner-name-field-${index}`}
|
testId={`create-safe-owner-name-field-${index}`}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className={classes.ownerAddress} xs={6}>
|
<Col className={classes.ownerAddress} xs={7}>
|
||||||
<AddressInput
|
<StyledAddressInput
|
||||||
fieldMutator={(newOwnerAddress) => {
|
fieldMutator={(newOwnerAddress) => {
|
||||||
const newOwnerName = getNameFromAddressBook(addressBook, newOwnerAddress, {
|
const newOwnerName = getNameFromAddressBook(addressBook, newOwnerAddress, {
|
||||||
filterOnlyValidName: true,
|
filterOnlyValidName: true,
|
||||||
@ -246,7 +252,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
|
|||||||
</Block>
|
</Block>
|
||||||
<Row align="center" className={classes.add} grow margin="xl">
|
<Row align="center" className={classes.add} grow margin="xl">
|
||||||
<Button color="secondary" data-testid="add-owner-btn" onClick={onAddOwner}>
|
<Button color="secondary" data-testid="add-owner-btn" onClick={onAddOwner}>
|
||||||
<Paragraph noMargin size="md">
|
<Paragraph noMargin size="lg">
|
||||||
{ADD_OWNER_BUTTON}
|
{ADD_OWNER_BUTTON}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Button>
|
</Button>
|
||||||
@ -256,7 +262,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
|
|||||||
Any transaction requires the confirmation of:
|
Any transaction requires the confirmation of:
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Row align="center" className={classes.ownersAmount} margin="xl">
|
<Row align="center" className={classes.ownersAmount} margin="xl">
|
||||||
<Col className={classes.ownersAmountItem} xs={2}>
|
<Col className={classes.ownersAmountItem} xs={1}>
|
||||||
<Field
|
<Field
|
||||||
component={SelectField}
|
component={SelectField}
|
||||||
data-testid="threshold-select-input"
|
data-testid="threshold-select-input"
|
||||||
|
@ -5,10 +5,10 @@ describe('calculateValuesAfterRemoving', () => {
|
|||||||
// Given
|
// Given
|
||||||
const formContent = {
|
const formContent = {
|
||||||
name: 'My Safe',
|
name: 'My Safe',
|
||||||
owner0Name: 'Owner 0',
|
owner0000Name: 'Owner 0',
|
||||||
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
||||||
owner1Name: 'Owner 1',
|
owner0001Name: 'Owner 1',
|
||||||
owner1Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0',
|
owner0001Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0',
|
||||||
}
|
}
|
||||||
|
|
||||||
// When
|
// When
|
||||||
@ -17,8 +17,8 @@ describe('calculateValuesAfterRemoving', () => {
|
|||||||
// Then
|
// Then
|
||||||
expect(newFormContent).toStrictEqual({
|
expect(newFormContent).toStrictEqual({
|
||||||
name: 'My Safe',
|
name: 'My Safe',
|
||||||
owner0Name: 'Owner 0',
|
owner0000Name: 'Owner 0',
|
||||||
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -26,12 +26,12 @@ describe('calculateValuesAfterRemoving', () => {
|
|||||||
// Given
|
// Given
|
||||||
const formContent = {
|
const formContent = {
|
||||||
name: 'My Safe',
|
name: 'My Safe',
|
||||||
owner0Name: 'Owner 0',
|
owner0000Name: 'Owner 0',
|
||||||
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
||||||
owner1Name: 'Owner 1',
|
owner0001Name: 'Owner 1',
|
||||||
owner1Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0',
|
owner0001Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0',
|
||||||
owner2Name: 'Owner 2',
|
owner0002Name: 'Owner 2',
|
||||||
owner2Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b',
|
owner0002Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b',
|
||||||
}
|
}
|
||||||
|
|
||||||
// When
|
// When
|
||||||
@ -40,10 +40,10 @@ describe('calculateValuesAfterRemoving', () => {
|
|||||||
// Then
|
// Then
|
||||||
expect(newFormContent).toStrictEqual({
|
expect(newFormContent).toStrictEqual({
|
||||||
name: 'My Safe',
|
name: 'My Safe',
|
||||||
owner0Name: 'Owner 0',
|
owner0000Name: 'Owner 0',
|
||||||
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
||||||
owner1Name: 'Owner 2',
|
owner0001Name: 'Owner 2',
|
||||||
owner1Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b',
|
owner0001Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
|
import { LoadFormValues } from 'src/routes/load/container/Load'
|
||||||
|
import { padOwnerIndex } from 'src/routes/open/utils/padOwnerIndex'
|
||||||
|
import { CreateSafeValues } from 'src/routes/open/utils/safeDataExtractor'
|
||||||
|
|
||||||
export const FIELD_NAME = 'name'
|
export const FIELD_NAME = 'name'
|
||||||
export const FIELD_CONFIRMATIONS = 'confirmations'
|
export const FIELD_CONFIRMATIONS = 'confirmations'
|
||||||
export const FIELD_OWNERS = 'owners'
|
export const FIELD_OWNERS = 'owners'
|
||||||
export const FIELD_SAFE_NAME = 'safeName'
|
export const FIELD_SAFE_NAME = 'safeName'
|
||||||
export const FIELD_CREATION_PROXY_SALT = 'safeCreationSalt'
|
export const FIELD_CREATION_PROXY_SALT = 'safeCreationSalt'
|
||||||
|
|
||||||
export const getOwnerNameBy = (index: number): string => `owner${index}Name`
|
export const getOwnerNameBy = (index: number): string => `owner${padOwnerIndex(index)}Name`
|
||||||
export const getOwnerAddressBy = (index: number): string => `owner${index}Address`
|
export const getOwnerAddressBy = (index: number): string => `owner${padOwnerIndex(index)}Address`
|
||||||
|
|
||||||
export const getNumOwnersFrom = (values) => {
|
export const getNumOwnersFrom = (values: CreateSafeValues | LoadFormValues): number => {
|
||||||
const accounts = Object.keys(values)
|
const accounts = Object.keys(values)
|
||||||
.sort()
|
.sort()
|
||||||
.filter((key) => {
|
.filter((key) => {
|
||||||
|
3
src/routes/open/utils/padOwnerIndex.ts
Normal file
3
src/routes/open/utils/padOwnerIndex.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const padOwnerIndex = (index: number | string): string => {
|
||||||
|
return index.toString().padStart(4, '0')
|
||||||
|
}
|
BIN
src/routes/opening/assets/creation-process.gif
Normal file
BIN
src/routes/opening/assets/creation-process.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
@ -1,18 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="91px" height="91px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
|
||||||
<circle cx="84" cy="50" r="0.271746" fill="#d4d5d3">
|
|
||||||
<animate attributeName="r" repeatCount="indefinite" dur="1.7857142857142856s" calcMode="spline" keyTimes="0;1" values="10;0" keySplines="0 0.5 0.5 1" begin="0s"></animate>
|
|
||||||
<animate attributeName="fill" repeatCount="indefinite" dur="7.142857142857142s" calcMode="discrete" keyTimes="0;0.25;0.5;0.75;1" values="#d4d5d3;#d4d5d3;#d4d5d3;#d4d5d3;#d4d5d3" begin="0s"></animate>
|
|
||||||
</circle><circle cx="49.076" cy="50" r="10" fill="#d4d5d3">
|
|
||||||
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
|
|
||||||
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
|
|
||||||
</circle><circle cx="83.076" cy="50" r="10" fill="#d4d5d3">
|
|
||||||
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.7857142857142856s"></animate>
|
|
||||||
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.7857142857142856s"></animate>
|
|
||||||
</circle><circle cx="16" cy="50" r="0" fill="#d4d5d3">
|
|
||||||
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-3.571428571428571s"></animate>
|
|
||||||
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-3.571428571428571s"></animate>
|
|
||||||
</circle><circle cx="16" cy="50" r="9.72825" fill="#d4d5d3">
|
|
||||||
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-5.357142857142857s"></animate>
|
|
||||||
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-5.357142857142857s"></animate>
|
|
||||||
</circle>
|
|
||||||
<!-- [ldio] generated by https://loading.io/ --></svg>
|
|
Before Width: | Height: | Size: 2.9 KiB |
16
src/routes/opening/assets/safe-created.svg
Normal file
16
src/routes/opening/assets/safe-created.svg
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="114" height="92" viewBox="0 0 114 92">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path fill="#F7F5F5" d="M59.004 0c25.405 0 46 20.595 46 46 0 25.406-20.595 46-46 46s-46-20.594-46-46c0-25.405 20.595-46 46-46" transform="translate(-796 -178) translate(796 178)"/>
|
||||||
|
<path fill="#008C73" d="M26 30.002H16v-10c0-1.105-.896-2-2-2s-2 .895-2 2v10H2c-1.104 0-2 .896-2 2s.896 2 2 2h10v10c0 1.104.896 2 2 2s2-.896 2-2v-10h10c1.104 0 2-.896 2-2s-.896-2-2-2" transform="translate(-796 -178) translate(796 178)"/>
|
||||||
|
<path fill="#B2B5B2" d="M109.991 66.798c0 1.22-.992 2.211-2.211 2.211H41.202c-1.218 0-2.211-.992-2.211-2.21v-48.58c0-1.218.993-2.21 2.211-2.21h66.578c1.219 0 2.211.992 2.211 2.21V66.8zm-14 9.2h8V73.01h-8V76zm-50.996.006h8V73.01h-8v2.995zm62.785-63.996H41.202c-3.424 0-6.211 2.787-6.211 6.211V66.8c0 3.353 2.676 6.09 6.004 6.2v5.005c0 1.105.896 2 2 2h12c1.105 0 2-.895 2-2V73.01h34.996V78c0 1.104.896 2 2 2h12c1.104 0 2-.896 2-2v-5c3.327-.114 6-2.847 6-6.2v-48.58c0-3.424-2.786-6.21-6.211-6.21z" transform="translate(-796 -178) translate(796 178)"/>
|
||||||
|
<path fill="#008C73" d="M84.995 26.006c8.822 0 16 7.178 16 16 0 8.823-7.178 16-16 16s-16-7.177-16-16c0-8.822 7.178-16 16-16zm1.793 4.133c-.001.948-.738 1.732-1.675 1.808l-.15.006c-.956 0-1.74-.736-1.816-1.673l-.006-.131c-1.954.304-3.753 1.081-5.277 2.21l-.032-.03c.54.477.743 1.224.53 1.902l-.053.144c-.273.667-.912 1.104-1.594 1.125l-.146-.001c-.435.007-.855-.143-1.184-.42l-.111-.102c-1.093 1.507-1.844 3.278-2.14 5.197h-.016c.957 0 1.741.736 1.817 1.673l.006.15c0 .956-.737 1.74-1.673 1.817l-.136.006c.296 1.947 1.062 3.742 2.178 5.265.682-.59 1.679-.591 2.357-.029l.124.112c.636.633.712 1.627.179 2.371l-.095.121c1.524 1.127 3.322 1.902 5.275 2.204l.003.054c-.056-.635.223-1.248.727-1.624l.131-.089c.587-.362 1.329-.362 1.916 0 .542.335.866.925.867 1.52l-.006.147c1.962-.295 3.77-1.067 5.302-2.193l-.024-.023c-.658-.688-.672-1.755-.058-2.458l.115-.12c.687-.658 1.754-.671 2.45-.065l.093.09c1.12-1.522 1.89-3.317 2.19-5.264l.036.001c-1.007 0-1.823-.816-1.823-1.823 0-.956.737-1.74 1.674-1.817l.116-.005c-.298-1.96-1.072-3.766-2.2-5.295l-.012.012c-.299.292-.686.472-1.093.515l-.176.01c-.49.01-.963-.182-1.304-.528-.342-.34-.534-.803-.534-1.285 0-.429.152-.841.425-1.166l.12-.129c-1.531-1.124-3.338-1.895-5.297-2.19z" transform="translate(-796 -178) translate(796 178)"/>
|
||||||
|
<path fill="#008C73" d="M84.995 39.006c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3m0 10c-3.86 0-7-3.141-7-7 0-3.86 3.14-7 7-7 3.859 0 7 3.14 7 7 0 3.859-3.141 7-7 7" transform="translate(-796 -178) translate(796 178)"/>
|
||||||
|
<path fill="#B2B5B2" d="M46.996 61.002c-1.104 0-2-.896-2-2v-33c0-1.105.896-2 2-2s2 .895 2 2v33c0 1.104-.896 2-2 2M55 61.002c-1.104 0-2-.896-2-2v-33c0-1.105.896-2 2-2s2 .895 2 2v33c0 1.104-.896 2-2 2" transform="translate(-796 -178) translate(796 178)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
@ -1,4 +0,0 @@
|
|||||||
<svg width="89" height="89" viewBox="0 0 89 89" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M44.25 0C53.0018 0 61.5571 2.59522 68.834 7.45747C76.1109 12.3197 81.7825 19.2306 85.1317 27.3163C88.4808 35.4019 89.3571 44.2991 87.6497 52.8827C85.9424 61.4664 81.728 69.351 75.5395 75.5395C69.351 81.728 61.4664 85.9424 52.8827 87.6497C44.2991 89.3571 35.4019 88.4808 27.3163 85.1317C19.2306 81.7825 12.3197 76.1109 7.45747 68.834C2.59522 61.5571 0 53.0018 0 44.25C0.0164019 32.5192 4.68371 21.2736 12.9786 12.9786C21.2736 4.68371 32.5192 0.0164019 44.25 0ZM44.25 4.445C36.3785 4.445 28.6838 6.7791 22.1388 11.1522C15.5939 15.5252 10.4926 21.7408 7.48007 29.013C4.46756 36.2853 3.67909 44.2874 5.21438 52.0078C6.74967 59.7281 10.5398 66.8198 16.1054 72.3861C21.671 77.9524 28.7622 81.7434 36.4823 83.2796C44.2025 84.8159 52.2048 84.0284 59.4773 81.0168C66.7499 78.0052 72.9662 72.9048 77.3401 66.3603C81.7139 59.8159 84.049 52.1215 84.05 44.25C84.0323 33.6998 79.8334 23.5868 72.3733 16.1267C64.9132 8.66661 54.8002 4.46772 44.25 4.45V4.445Z" fill="#D4D5D3"/>
|
|
||||||
<path d="M66.077 31.405L69.3 34.465L40.146 65.174L19.2 43.111L22.423 40.05L40.146 58.718L66.077 31.405Z" fill="#008C73"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,19 +1,36 @@
|
|||||||
import React, { SyntheticEvent } from 'react'
|
import React, { ReactElement, SyntheticEvent } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
import Button from 'src/components/layout/Button'
|
import Button from 'src/components/layout/Button'
|
||||||
import { connected } from 'src/theme/variables'
|
|
||||||
import { getExplorerInfo } from 'src/config'
|
import { getExplorerInfo } from 'src/config'
|
||||||
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
|
|
||||||
const ExplorerLink = styled.a`
|
const StyledText = styled(Text)`
|
||||||
color: ${connected};
|
display: inline-flex;
|
||||||
|
a {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
position: relative;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const ButtonWithMargin = styled(Button)`
|
const ButtonWithMargin = styled(Button)`
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
`
|
`
|
||||||
|
const FooterContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 76px;
|
||||||
|
|
||||||
export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: string }) => {
|
button {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: string }): ReactElement => {
|
||||||
const explorerInfo = getExplorerInfo(safeCreationTxHash)
|
const explorerInfo = getExplorerInfo(safeCreationTxHash)
|
||||||
const { url, alt } = explorerInfo()
|
const { url, alt } = explorerInfo()
|
||||||
const match = /(http|https):\/\/(\w+\.\w+)\/.*/i.exec(url)
|
const match = /(http|https):\/\/(\w+\.\w+)\/.*/i.exec(url)
|
||||||
@ -21,20 +38,23 @@ export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: stri
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<p>This process should take a couple of minutes.</p>
|
<Text size="xl">This process should take a couple of minutes.</Text>
|
||||||
<p>
|
<StyledText size="xl">
|
||||||
Follow the progress on{' '}
|
Follow the progress on{' '}
|
||||||
<ExplorerLink
|
<Link
|
||||||
aria-label={alt}
|
|
||||||
href={url}
|
href={url}
|
||||||
rel="noopener noreferrer"
|
aria-label={alt}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
data-testid="safe-create-explorer-link"
|
data-testid="safe-create-explorer-link"
|
||||||
|
title="More info about this in Etherscan"
|
||||||
>
|
>
|
||||||
{explorerDomain}
|
<Text size="xl" as="span" color="primary">
|
||||||
</ExplorerLink>
|
{explorerDomain}
|
||||||
.
|
</Text>
|
||||||
</p>
|
<Icon size="sm" type="externalLink" color="primary" />
|
||||||
|
</Link>
|
||||||
|
</StyledText>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -45,16 +65,19 @@ export const ContinueFooter = ({
|
|||||||
}: {
|
}: {
|
||||||
continueButtonDisabled: boolean
|
continueButtonDisabled: boolean
|
||||||
onContinue: (event: SyntheticEvent) => void
|
onContinue: (event: SyntheticEvent) => void
|
||||||
}) => (
|
}): ReactElement => (
|
||||||
<Button
|
<FooterContainer>
|
||||||
color="primary"
|
<Hairline />
|
||||||
disabled={continueButtonDisabled}
|
<Button
|
||||||
onClick={onContinue}
|
color="primary"
|
||||||
variant="contained"
|
disabled={continueButtonDisabled}
|
||||||
data-testid="continue-btn"
|
onClick={onContinue}
|
||||||
>
|
variant="contained"
|
||||||
Continue
|
data-testid="continue-btn"
|
||||||
</Button>
|
>
|
||||||
|
Get started
|
||||||
|
</Button>
|
||||||
|
</FooterContainer>
|
||||||
)
|
)
|
||||||
|
|
||||||
export const ErrorFooter = ({
|
export const ErrorFooter = ({
|
||||||
@ -63,13 +86,14 @@ export const ErrorFooter = ({
|
|||||||
}: {
|
}: {
|
||||||
onCancel: (event: SyntheticEvent) => void
|
onCancel: (event: SyntheticEvent) => void
|
||||||
onRetry: (event: SyntheticEvent) => void
|
onRetry: (event: SyntheticEvent) => void
|
||||||
}) => (
|
}): ReactElement => (
|
||||||
<>
|
<FooterContainer>
|
||||||
|
<Hairline />
|
||||||
<ButtonWithMargin onClick={onCancel} variant="contained">
|
<ButtonWithMargin onClick={onCancel} variant="contained">
|
||||||
Cancel
|
Cancel
|
||||||
</ButtonWithMargin>
|
</ButtonWithMargin>
|
||||||
<Button color="primary" onClick={onRetry} variant="contained">
|
<Button color="primary" onClick={onRetry} variant="contained">
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</FooterContainer>
|
||||||
)
|
)
|
||||||
|
@ -12,20 +12,19 @@ import Paragraph from 'src/components/layout/Paragraph'
|
|||||||
import { instantiateSafeContracts } from 'src/logic/contracts/safeContracts'
|
import { instantiateSafeContracts } from 'src/logic/contracts/safeContracts'
|
||||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
import { background, connected } from 'src/theme/variables'
|
import { background, connected, fontColor } from 'src/theme/variables'
|
||||||
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
|
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import LoaderDotsSvg from './assets/loader-dots.svg'
|
import SuccessSvg from './assets/safe-created.svg'
|
||||||
import SuccessSvg from './assets/success.svg'
|
|
||||||
import VaultErrorSvg from './assets/vault-error.svg'
|
import VaultErrorSvg from './assets/vault-error.svg'
|
||||||
import VaultSvg from './assets/vault.svg'
|
import VaultLoading from './assets/creation-process.gif'
|
||||||
import { PromiEvent, TransactionReceipt } from 'web3-core'
|
import { PromiEvent, TransactionReceipt } from 'web3-core'
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 250px auto;
|
grid-template-columns: 250px auto;
|
||||||
grid-template-rows: 62px auto;
|
grid-template-rows: 43px auto;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -44,29 +43,31 @@ const Body = styled.div`
|
|||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: #ffffff;
|
background-color: ${({ theme }) => theme.colors.white};
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
min-width: 700px;
|
min-width: 700px;
|
||||||
padding-top: 50px;
|
padding-top: 70px;
|
||||||
box-shadow: 0 0 10px 0 rgba(33, 48, 77, 0.1);
|
box-shadow: 0 0 10px 0 rgba(33, 48, 77, 0.1);
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 100px 50px 70px 60px 100px;
|
grid-template-rows: 100px 50px 110px 1fr;
|
||||||
`
|
`
|
||||||
|
|
||||||
const CardTitle = styled.div`
|
const CardTitle = styled.div`
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
`
|
`
|
||||||
|
|
||||||
interface FullParagraphProps {
|
interface FullParagraphProps {
|
||||||
inversecolors: string
|
inversecolors: string
|
||||||
|
stepIndex: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const FullParagraph = styled(Paragraph)<FullParagraphProps>`
|
const FullParagraph = styled(Paragraph)<FullParagraphProps>`
|
||||||
background-color: ${(p) => (p.inversecolors ? connected : background)};
|
background-color: ${({ stepIndex }) => (stepIndex === 0 ? connected : background)};
|
||||||
color: ${(p) => (p.inversecolors ? background : connected)};
|
color: ${({ theme, stepIndex }) => (stepIndex === 0 ? theme.colors.white : fontColor)};
|
||||||
padding: 24px;
|
padding: 28px;
|
||||||
font-size: 16px;
|
font-size: 20px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||||
`
|
`
|
||||||
@ -77,17 +78,12 @@ const BodyImage = styled.div`
|
|||||||
const BodyDescription = styled.div`
|
const BodyDescription = styled.div`
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
`
|
`
|
||||||
const BodyLoader = styled.div`
|
|
||||||
grid-row: 3;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
const BodyInstruction = styled.div`
|
const BodyInstruction = styled.div`
|
||||||
grid-row: 4;
|
grid-row: 3;
|
||||||
|
margin: 27px 0;
|
||||||
`
|
`
|
||||||
const BodyFooter = styled.div`
|
const BodyFooter = styled.div`
|
||||||
grid-row: 5;
|
grid-row: 4;
|
||||||
|
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -154,7 +150,7 @@ export const SafeDeployment = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stepIndex <= 4) {
|
if (stepIndex <= 4) {
|
||||||
return VaultSvg
|
return VaultLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
return SuccessSvg
|
return SuccessSvg
|
||||||
@ -326,20 +322,26 @@ export const SafeDeployment = ({
|
|||||||
</Nav>
|
</Nav>
|
||||||
<Body>
|
<Body>
|
||||||
<BodyImage>
|
<BodyImage>
|
||||||
<Img alt="Vault" height={75} src={getImage()} />
|
<Img alt="Vault" height={92} src={getImage()} />
|
||||||
</BodyImage>
|
</BodyImage>
|
||||||
|
|
||||||
<BodyDescription>
|
<BodyDescription>
|
||||||
<CardTitle>{steps[stepIndex].description || steps[stepIndex].label}</CardTitle>
|
<CardTitle>{steps[stepIndex].description || steps[stepIndex].label}</CardTitle>
|
||||||
</BodyDescription>
|
</BodyDescription>
|
||||||
|
|
||||||
<BodyLoader>{!error && stepIndex <= 4 && <Img alt="Loader dots" src={LoaderDotsSvg} />}</BodyLoader>
|
{steps[stepIndex].instruction && (
|
||||||
|
<BodyInstruction>
|
||||||
<BodyInstruction>
|
<FullParagraph
|
||||||
<FullParagraph color="primary" inversecolors={confirmationStep.toString()} noMargin size="md">
|
color="primary"
|
||||||
{error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction}
|
inversecolors={confirmationStep.toString()}
|
||||||
</FullParagraph>
|
noMargin
|
||||||
</BodyInstruction>
|
size="md"
|
||||||
|
stepIndex={stepIndex}
|
||||||
|
>
|
||||||
|
{error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction}
|
||||||
|
</FullParagraph>
|
||||||
|
</BodyInstruction>
|
||||||
|
)}
|
||||||
|
|
||||||
<BodyFooter>
|
<BodyFooter>
|
||||||
{FooterComponent ? (
|
{FooterComponent ? (
|
||||||
@ -354,9 +356,12 @@ export const SafeDeployment = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</BodyFooter>
|
</BodyFooter>
|
||||||
</Body>
|
</Body>
|
||||||
<BackButton color="primary" minWidth={140} onClick={onCancel} data-testid="safe-creation-back-btn">
|
|
||||||
Back
|
{stepIndex !== 0 && (
|
||||||
</BackButton>
|
<BackButton color="primary" minWidth={140} onClick={onCancel} data-testid="safe-creation-back-btn">
|
||||||
|
Back
|
||||||
|
</BackButton>
|
||||||
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ContinueFooter, GenericFooter } from './components/Footer'
|
import { ContinueFooter, GenericFooter } from './components/Footer'
|
||||||
|
|
||||||
export const isConfirmationStep = (stepIndex?: number) => stepIndex === 0
|
export const isConfirmationStep = (stepIndex?: number): boolean => stepIndex === 0
|
||||||
|
|
||||||
export const steps = [
|
export const steps = [
|
||||||
{
|
{
|
||||||
@ -42,7 +42,7 @@ export const steps = [
|
|||||||
id: '6',
|
id: '6',
|
||||||
label: 'Success',
|
label: 'Success',
|
||||||
description: 'Your Safe was created successfully',
|
description: 'Your Safe was created successfully',
|
||||||
instruction: 'Click below to get started',
|
instruction: undefined,
|
||||||
footerComponent: ContinueFooter,
|
footerComponent: ContinueFooter,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -33,7 +33,7 @@ import {
|
|||||||
generateColumns,
|
generateColumns,
|
||||||
} from 'src/routes/safe/components/AddressBook/columns'
|
} from 'src/routes/safe/components/AddressBook/columns'
|
||||||
import SendModal from 'src/routes/safe/components/Balances/SendModal'
|
import SendModal from 'src/routes/safe/components/Balances/SendModal'
|
||||||
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
import { OwnerAddressTableCell } from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||||
import RenameOwnerIcon from 'src/routes/safe/components/Settings/ManageOwners/assets/icons/rename-owner.svg'
|
import RenameOwnerIcon from 'src/routes/safe/components/Settings/ManageOwners/assets/icons/rename-owner.svg'
|
||||||
import RemoveOwnerIcon from 'src/routes/safe/components/Settings/assets/icons/bin.svg'
|
import RemoveOwnerIcon from 'src/routes/safe/components/Settings/assets/icons/bin.svg'
|
||||||
import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors'
|
import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors'
|
||||||
|
@ -14,7 +14,7 @@ export const styles = createStyles({
|
|||||||
},
|
},
|
||||||
hide: {
|
hide: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#fff3e2',
|
backgroundColor: '#f7f5f5',
|
||||||
},
|
},
|
||||||
'&:hover $actions': {
|
'&:hover $actions': {
|
||||||
visibility: 'initial',
|
visibility: 'initial',
|
||||||
|
@ -15,12 +15,12 @@ type MessageHandler = (
|
|||||||
) => void | MethodToResponse[Methods] | ErrorResponse | Promise<MethodToResponse[Methods] | ErrorResponse | void>
|
) => void | MethodToResponse[Methods] | ErrorResponse | Promise<MethodToResponse[Methods] | ErrorResponse | void>
|
||||||
|
|
||||||
class AppCommunicator {
|
class AppCommunicator {
|
||||||
private iframe: HTMLIFrameElement
|
private iframeRef: MutableRefObject<HTMLIFrameElement | null>
|
||||||
private handlers = new Map<Methods, MessageHandler>()
|
private handlers = new Map<Methods, MessageHandler>()
|
||||||
private app: SafeApp
|
private app: SafeApp
|
||||||
|
|
||||||
constructor(iframeRef: MutableRefObject<HTMLIFrameElement>, app: SafeApp) {
|
constructor(iframeRef: MutableRefObject<HTMLIFrameElement | null>, app: SafeApp) {
|
||||||
this.iframe = iframeRef.current
|
this.iframeRef = iframeRef
|
||||||
this.app = app
|
this.app = app
|
||||||
|
|
||||||
window.addEventListener('message', this.handleIncomingMessage)
|
window.addEventListener('message', this.handleIncomingMessage)
|
||||||
@ -49,7 +49,7 @@ class AppCommunicator {
|
|||||||
? MessageFormatter.makeErrorResponse(requestId, data, sdkVersion)
|
? MessageFormatter.makeErrorResponse(requestId, data, sdkVersion)
|
||||||
: MessageFormatter.makeResponse(requestId, data, sdkVersion)
|
: MessageFormatter.makeResponse(requestId, data, sdkVersion)
|
||||||
|
|
||||||
this.iframe.contentWindow?.postMessage(msg, this.app.url)
|
this.iframeRef.current?.contentWindow?.postMessage(msg, this.app.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleIncomingMessage = async (msg: SDKMessageEvent): Promise<void> => {
|
handleIncomingMessage = async (msg: SDKMessageEvent): Promise<void> => {
|
||||||
@ -83,7 +83,6 @@ const useAppCommunicator = (
|
|||||||
app?: SafeApp,
|
app?: SafeApp,
|
||||||
): AppCommunicator | undefined => {
|
): AppCommunicator | undefined => {
|
||||||
const [communicator, setCommunicator] = useState<AppCommunicator | undefined>(undefined)
|
const [communicator, setCommunicator] = useState<AppCommunicator | undefined>(undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let communicatorInstance
|
let communicatorInstance
|
||||||
const initCommunicator = (iframeRef: MutableRefObject<HTMLIFrameElement>, app: SafeApp) => {
|
const initCommunicator = (iframeRef: MutableRefObject<HTMLIFrameElement>, app: SafeApp) => {
|
||||||
@ -91,7 +90,7 @@ const useAppCommunicator = (
|
|||||||
setCommunicator(communicatorInstance)
|
setCommunicator(communicatorInstance)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app && iframeRef.current !== null) {
|
if (app) {
|
||||||
initCommunicator(iframeRef as MutableRefObject<HTMLIFrameElement>, app)
|
initCommunicator(iframeRef as MutableRefObject<HTMLIFrameElement>, app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ import { LoadingContainer } from 'src/components/LoaderContainer/index'
|
|||||||
import { TIMEOUT } from 'src/utils/constants'
|
import { TIMEOUT } from 'src/utils/constants'
|
||||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||||
|
|
||||||
import { ConfirmTransactionModal } from '../components/ConfirmTransactionModal'
|
import { ConfirmTxModal } from '../components/ConfirmTxModal'
|
||||||
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
||||||
import { useLegalConsent } from '../hooks/useLegalConsent'
|
import { useLegalConsent } from '../hooks/useLegalConsent'
|
||||||
import LegalDisclaimer from './LegalDisclaimer'
|
import LegalDisclaimer from './LegalDisclaimer'
|
||||||
@ -56,6 +56,7 @@ const AppWrapper = styled.div`
|
|||||||
|
|
||||||
const StyledCard = styled(Card)`
|
const StyledCard = styled(Card)`
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
padding: 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledIframe = styled.iframe`
|
const StyledIframe = styled.iframe`
|
||||||
@ -354,7 +355,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ConfirmTransactionModal
|
<ConfirmTxModal
|
||||||
isOpen={confirmTransactionModal.isOpen}
|
isOpen={confirmTransactionModal.isOpen}
|
||||||
app={safeApp as SafeApp}
|
app={safeApp as SafeApp}
|
||||||
safeAddress={safeAddress}
|
safeAddress={safeAddress}
|
||||||
|
@ -1,328 +0,0 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react'
|
|
||||||
import { Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components'
|
|
||||||
import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
import AddressInfo from 'src/components/AddressInfo'
|
|
||||||
import DividerLine from 'src/components/DividerLine'
|
|
||||||
import Collapse from 'src/components/Collapse'
|
|
||||||
import TextBox from 'src/components/TextBox'
|
|
||||||
import ModalTitle from 'src/components/ModalTitle'
|
|
||||||
import { mustBeEthereumAddress } from 'src/components/forms/validator'
|
|
||||||
import Bold from 'src/components/layout/Bold'
|
|
||||||
import Heading from 'src/components/layout/Heading'
|
|
||||||
import Img from 'src/components/layout/Img'
|
|
||||||
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
|
||||||
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
|
|
||||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
|
||||||
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
|
||||||
import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
|
|
||||||
import { DELEGATE_CALL, TX_NOTIFICATION_TYPES, CALL } from 'src/logic/safe/transactions'
|
|
||||||
import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend'
|
|
||||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
|
||||||
|
|
||||||
import GasEstimationInfo from './GasEstimationInfo'
|
|
||||||
import { getNetworkInfo } from 'src/config'
|
|
||||||
import { TransactionParams } from './AppFrame'
|
|
||||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
|
||||||
import { safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
|
||||||
import Modal from 'src/components/Modal'
|
|
||||||
import Row from 'src/components/layout/Row'
|
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
|
||||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
|
||||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
|
||||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
|
||||||
import { md, lg, sm } from 'src/theme/variables'
|
|
||||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
|
||||||
|
|
||||||
const isTxValid = (t: Transaction): boolean => {
|
|
||||||
if (!['string', 'number'].includes(typeof t.value)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof t.value === 'string' && !/^(0x)?[0-9a-f]+$/i.test(t.value)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAddressValid = mustBeEthereumAddress(t.to) === undefined
|
|
||||||
return isAddressValid && !!t.data && typeof t.data === 'string'
|
|
||||||
}
|
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
|
||||||
margin-bottom: 15px;
|
|
||||||
`
|
|
||||||
const CollapseContent = styled.div`
|
|
||||||
padding: 15px 0;
|
|
||||||
|
|
||||||
.section {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const IconText = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const StyledTextBox = styled(TextBox)`
|
|
||||||
max-width: 444px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
max-width: 480px;
|
|
||||||
padding: ${md} ${lg};
|
|
||||||
`
|
|
||||||
|
|
||||||
const ModalFooter = styled(Row)`
|
|
||||||
padding: ${md} ${lg};
|
|
||||||
justify-content: center;
|
|
||||||
`
|
|
||||||
const TransactionFeesWrapper = styled.div`
|
|
||||||
background-color: ${({ theme }) => theme.colors.background};
|
|
||||||
padding: ${sm} ${lg};
|
|
||||||
`
|
|
||||||
|
|
||||||
type OwnProps = {
|
|
||||||
isOpen: boolean
|
|
||||||
app: SafeApp
|
|
||||||
txs: Transaction[]
|
|
||||||
params?: TransactionParams
|
|
||||||
safeAddress: string
|
|
||||||
safeName: string
|
|
||||||
ethBalance: string
|
|
||||||
onUserConfirm: (safeTxHash: string) => void
|
|
||||||
onTxReject: () => void
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const { nativeCoin } = getNetworkInfo()
|
|
||||||
|
|
||||||
const parseTxValue = (value: string | number): string => {
|
|
||||||
return web3ReadOnly.utils.toBN(value).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ConfirmTransactionModal = ({
|
|
||||||
isOpen,
|
|
||||||
app,
|
|
||||||
txs,
|
|
||||||
safeAddress,
|
|
||||||
ethBalance,
|
|
||||||
safeName,
|
|
||||||
params,
|
|
||||||
onUserConfirm,
|
|
||||||
onClose,
|
|
||||||
onTxReject,
|
|
||||||
}: OwnProps): React.ReactElement | null => {
|
|
||||||
const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0)
|
|
||||||
const threshold = useSelector(safeThresholdSelector) || 1
|
|
||||||
|
|
||||||
const txRecipient: string | undefined = useMemo(() => (txs.length > 1 ? MULTI_SEND_ADDRESS : txs[0]?.to), [txs])
|
|
||||||
const txData: string | undefined = useMemo(() => (txs.length > 1 ? encodeMultiSendCall(txs) : txs[0]?.data), [txs])
|
|
||||||
const txValue: string | undefined = useMemo(
|
|
||||||
() => (txs.length > 1 ? '0' : txs[0]?.value && parseTxValue(txs[0]?.value)),
|
|
||||||
[txs],
|
|
||||||
)
|
|
||||||
const operation = useMemo(() => (txs.length > 1 ? DELEGATE_CALL : CALL), [txs])
|
|
||||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
|
||||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
|
||||||
|
|
||||||
const {
|
|
||||||
gasLimit,
|
|
||||||
gasPriceFormatted,
|
|
||||||
gasEstimation,
|
|
||||||
isOffChainSignature,
|
|
||||||
isCreation,
|
|
||||||
isExecution,
|
|
||||||
gasCostFormatted,
|
|
||||||
txEstimationExecutionStatus,
|
|
||||||
} = useEstimateTransactionGas({
|
|
||||||
txData: txData || '',
|
|
||||||
txRecipient,
|
|
||||||
operation,
|
|
||||||
txAmount: txValue,
|
|
||||||
safeTxGas: manualSafeTxGas,
|
|
||||||
manualGasPrice,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (params?.safeTxGas) {
|
|
||||||
setEstimatedSafeTxGas(gasEstimation)
|
|
||||||
}
|
|
||||||
}, [params, gasEstimation])
|
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
|
||||||
if (!isOpen) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTxRejection = () => {
|
|
||||||
onTxReject()
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUserConfirmation = (safeTxHash: string): void => {
|
|
||||||
onUserConfirm(safeTxHash)
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED')
|
|
||||||
|
|
||||||
const confirmTransactions = async (txParameters: TxParameters) => {
|
|
||||||
await dispatch(
|
|
||||||
createTransaction(
|
|
||||||
{
|
|
||||||
safeAddress,
|
|
||||||
to: txRecipient,
|
|
||||||
valueInWei: txValue,
|
|
||||||
txData,
|
|
||||||
operation,
|
|
||||||
origin: app.id,
|
|
||||||
navigateToTransactionsTab: false,
|
|
||||||
txNonce: txParameters.safeNonce,
|
|
||||||
safeTxGas: txParameters.safeTxGas
|
|
||||||
? Number(txParameters.safeTxGas)
|
|
||||||
: Math.max(params?.safeTxGas || 0, estimatedSafeTxGas),
|
|
||||||
ethParameters: txParameters,
|
|
||||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
|
||||||
},
|
|
||||||
handleUserConfirmation,
|
|
||||||
handleTxRejection,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeEditModalCallback = (txParameters: TxParameters) => {
|
|
||||||
const oldGasPrice = Number(gasPriceFormatted)
|
|
||||||
const newGasPrice = Number(txParameters.ethGasPrice)
|
|
||||||
const oldSafeTxGas = Number(gasEstimation)
|
|
||||||
const newSafeTxGas = Number(txParameters.safeTxGas)
|
|
||||||
|
|
||||||
if (newGasPrice && oldGasPrice !== newGasPrice) {
|
|
||||||
setManualGasPrice(txParameters.ethGasPrice)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
|
||||||
setManualSafeTxGas(newSafeTxGas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const areTxsMalformed = txs.some((t) => !isTxValid(t))
|
|
||||||
|
|
||||||
const body = areTxsMalformed
|
|
||||||
? () => (
|
|
||||||
<>
|
|
||||||
<IconText>
|
|
||||||
<Icon color="error" size="md" type="info" />
|
|
||||||
<Title size="xs">Transaction error</Title>
|
|
||||||
</IconText>
|
|
||||||
<Text size="lg">
|
|
||||||
This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of
|
|
||||||
this Safe App for more information.
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: (txParameters, toggleEditMode) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Container>
|
|
||||||
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
|
||||||
<DividerLine withArrow />
|
|
||||||
{txs.map((tx, index) => (
|
|
||||||
<Wrapper key={index}>
|
|
||||||
<Collapse description={<AddressInfo safeAddress={tx.to} />} title={`Transaction ${index + 1}`}>
|
|
||||||
<CollapseContent>
|
|
||||||
<div className="section">
|
|
||||||
<Heading tag="h3">Value</Heading>
|
|
||||||
<div className="value-section">
|
|
||||||
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
|
|
||||||
<Bold>
|
|
||||||
{fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name}
|
|
||||||
</Bold>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="section">
|
|
||||||
<Heading tag="h3">Data (hex encoded)*</Heading>
|
|
||||||
<StyledTextBox>{tx.data}</StyledTextBox>
|
|
||||||
</div>
|
|
||||||
</CollapseContent>
|
|
||||||
</Collapse>
|
|
||||||
</Wrapper>
|
|
||||||
))}
|
|
||||||
<DividerLine withArrow={false} />
|
|
||||||
{params?.safeTxGas && (
|
|
||||||
<div className="section">
|
|
||||||
<Heading tag="h3">SafeTxGas</Heading>
|
|
||||||
<StyledTextBox>{params?.safeTxGas}</StyledTextBox>
|
|
||||||
<GasEstimationInfo
|
|
||||||
appEstimation={params.safeTxGas}
|
|
||||||
internalEstimation={estimatedSafeTxGas}
|
|
||||||
loading={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tx Parameters */}
|
|
||||||
<TxParametersDetail
|
|
||||||
txParameters={txParameters}
|
|
||||||
onEdit={toggleEditMode}
|
|
||||||
parametersStatus={getParametersStatus()}
|
|
||||||
isTransactionCreation={isCreation}
|
|
||||||
isTransactionExecution={isExecution}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
{txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (
|
|
||||||
<TransactionFeesWrapper>
|
|
||||||
<TransactionFees
|
|
||||||
gasCostFormatted={gasCostFormatted}
|
|
||||||
isExecution={isExecution}
|
|
||||||
isCreation={isCreation}
|
|
||||||
isOffChainSignature={isOffChainSignature}
|
|
||||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
|
||||||
/>
|
|
||||||
</TransactionFeesWrapper>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal description="Safe App transaction" title="Safe App transaction" open>
|
|
||||||
<EditableTxParameters
|
|
||||||
ethGasLimit={gasLimit}
|
|
||||||
ethGasPrice={gasPriceFormatted}
|
|
||||||
safeTxGas={gasEstimation.toString()}
|
|
||||||
parametersStatus={getParametersStatus()}
|
|
||||||
closeEditModalCallback={closeEditModalCallback}
|
|
||||||
>
|
|
||||||
{(txParameters, toggleEditMode) => (
|
|
||||||
<>
|
|
||||||
<ModalTitle title={app.name} iconUrl={app.iconUrl} onClose={handleTxRejection} />
|
|
||||||
|
|
||||||
<Hairline />
|
|
||||||
|
|
||||||
{body(txParameters, toggleEditMode)}
|
|
||||||
|
|
||||||
<ModalFooter align="center" grow>
|
|
||||||
<ModalFooterConfirmation
|
|
||||||
cancelText="Cancel"
|
|
||||||
handleCancel={handleTxRejection}
|
|
||||||
handleOk={() => confirmTransactions(txParameters)}
|
|
||||||
okDisabled={areTxsMalformed}
|
|
||||||
okText="Submit"
|
|
||||||
/>
|
|
||||||
</ModalFooter>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</EditableTxParameters>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user