Merge pull request #2079 from gnosis/release/v3.3.0

Release v3.3.0
This commit is contained in:
Daniel Sanchez 2021-03-30 11:20:17 +02:00 committed by GitHub
commit df90896940
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
171 changed files with 1697 additions and 2433 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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",

View File

@ -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>

View File

@ -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

View File

@ -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',

View 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} />
)
}

View File

@ -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}

View File

@ -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>

View File

@ -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)',
}, },

View File

@ -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}`

View File

@ -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),

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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())

View File

@ -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
}, {})
}, },
) )

View 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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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)
} }

View File

@ -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)

View File

@ -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,
}),
)

View File

@ -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,
}))

View File

@ -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))
}

View File

@ -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()
}

View File

@ -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

View File

@ -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
}

View File

@ -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,
) )

View File

@ -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)
},
)

View File

@ -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,
}) })
} }
} }

View File

@ -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}`)
}) })
}) })

View 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
})
}

View File

@ -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])
} }

View File

@ -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))
} }
} }

View File

@ -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)
} }
} }

View File

@ -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
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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),

View File

@ -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
}) })

View File

@ -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)
})
})

View File

@ -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> = {

View 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
}
}
}

View File

@ -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)
})
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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,

View File

@ -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,
} }
} }

View File

@ -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
}

View File

@ -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&apos;re about to create a new Safe and will have to confirm a transaction with your currently connected You&apos;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.

View File

@ -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&apos;s give your new You are about to create a new Gnosis Safe wallet with one or more owners. First, let&apos;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

View File

@ -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"

View File

@ -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',
}) })
}) })
}) })

View File

@ -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) => {

View File

@ -0,0 +1,3 @@
export const padOwnerIndex = (index: number | string): string => {
return index.toString().padStart(4, '0')
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -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

View 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

View File

@ -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

View File

@ -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>
) )

View File

@ -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>
) )
} }

View File

@ -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,
}, },
] ]

View File

@ -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'

View File

@ -14,7 +14,7 @@ export const styles = createStyles({
}, },
hide: { hide: {
'&:hover': { '&:hover': {
backgroundColor: '#fff3e2', backgroundColor: '#f7f5f5',
}, },
'&:hover $actions': { '&:hover $actions': {
visibility: 'initial', visibility: 'initial',

View File

@ -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)
} }

View File

@ -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}

View File

@ -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