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
# Run on pushes to master
# Run on pushes to master or PRs to master
on:
push:
branches:
- master
pull_request:
branches:
- master
# Launches build when release is published
release:
types: [published]
@ -33,8 +36,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.8.0
with:
access_token: ${{ github.token }}
- name: Remove broken apt repos [Ubuntu]
if: matrix.os == 'ubuntu-latest'
if: ${{ matrix.os }} == 'ubuntu-latest'
run: |
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
- uses: actions/checkout@v2
@ -62,9 +69,9 @@ jobs:
yarn cache clean
# 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
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
run: yarn build
@ -98,7 +105,6 @@ jobs:
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
repo-token: ${{ secrets.GITHUB_TOKEN }}
repo-token-user-login: 'github-actions[bot]'
allow-repeats: true
if: success() && github.event.number
env:
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com

View File

@ -1,7 +1,9 @@
name: Deploy to Mainnet network
# Run on pushes to master
# Run on pushes to master or PRs
on:
# Pull request hook without any config. Launches for every pull request
pull_request:
push:
branches:
- master
@ -33,8 +35,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.8.0
with:
access_token: ${{ github.token }}
- name: Remove broken apt repos [Ubuntu]
if: matrix.os == 'ubuntu-latest'
if: ${{ matrix.os }} == 'ubuntu-latest'
run: |
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
- uses: actions/checkout@v2
@ -62,9 +68,9 @@ jobs:
yarn cache clean
# 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
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
run: yarn build
@ -84,7 +90,26 @@ jobs:
aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
# 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
# Mainnet build is never created in development branch

View File

@ -37,6 +37,10 @@ jobs:
name: Deployment
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.8.0
with:
access_token: ${{ github.token }}
- name: Remove broken apt repos [Ubuntu]
if: ${{ matrix.os }} == 'ubuntu-latest'
run: |
@ -66,9 +70,9 @@ jobs:
yarn cache clean
# 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
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 }}
run: yarn build
@ -103,7 +107,6 @@ jobs:
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
repo-token: ${{ secrets.GITHUB_TOKEN }}
repo-token-user-login: 'github-actions[bot]'
allow-repeats: true
if: success() && github.event.number
env:
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com

View File

@ -36,8 +36,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.8.0
with:
access_token: ${{ github.token }}
- name: Remove broken apt repos [Ubuntu]
if: matrix.os == 'ubuntu-latest'
if: ${{ matrix.os }} == 'ubuntu-latest'
run: |
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
- uses: actions/checkout@v2
@ -65,9 +69,9 @@ jobs:
yarn cache clean
# 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
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
run: yarn build
@ -101,7 +105,6 @@ jobs:
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
repo-token: ${{ secrets.GITHUB_TOKEN }}
repo-token-user-login: 'github-actions[bot]'
allow-repeats: true
if: success() && github.event.number
env:
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com

View File

@ -36,8 +36,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.8.0
with:
access_token: ${{ github.token }}
- name: Remove broken apt repos [Ubuntu]
if: matrix.os == 'ubuntu-latest'
if: ${{ matrix.os }} == 'ubuntu-latest'
run: |
for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done
- uses: actions/checkout@v2
@ -65,9 +69,9 @@ jobs:
yarn cache clean
# 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
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
run: yarn build
@ -101,7 +105,6 @@ jobs:
* [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/)
repo-token: ${{ secrets.GITHUB_TOKEN }}
repo-token-user-login: 'github-actions[bot]'
allow-repeats: true
if: success() && github.event.number
env:
REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com

View File

@ -10,6 +10,10 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.8.0
with:
access_token: ${{ github.token }}
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2

View File

@ -286,7 +286,7 @@ const xDai: NetworkConfig = {
label: 'xDai',
isTestNet: false,
nativeCoin: {
address: '0x000',
address: '0x0000000000000000000000000000000000000000',
name: 'xDai',
symbol: 'xDai',
decimals: 18,
@ -343,7 +343,7 @@ const mainnet: NetworkConfig = {
label: 'Mainnet',
isTestNet: false,
nativeCoin: {
address: '0x000',
address: '0x0000000000000000000000000000000000000000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,

View File

@ -1,6 +1,6 @@
{
"name": "safe-react",
"version": "3.2.0",
"version": "3.3.0",
"description": "Allowing crypto users manage funds in a safer way",
"website": "https://github.com/gnosis/safe-react#readme",
"bugs": {
@ -161,7 +161,7 @@
"@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-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",
"@ledgerhq/hw-transport-node-hid-singleton": "5.45.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">
<title>Gnosis Safe Multisig</title>
</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>
<div id="root" style="overflow: hidden;"></div>
<div id="root" style="overflow: hidden;"><img class="safe-preloader-animation" src="./resources/safe.png" /></div>
</body>
</html>

View File

@ -20,13 +20,17 @@ import { getNetworkId } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { networkSelector } from 'src/logic/wallets/store/selectors'
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 SendModal from 'src/routes/safe/components/Balances/SendModal'
import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
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 { grantedSelector } from 'src/routes/safe/container/selector'
@ -75,7 +79,7 @@ const App: React.FC = ({ children }) => {
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const safeName = useSelector(safeNameSelector) ?? ''
const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions()
const currentSafeBalance = useSelector(safeFiatBalancesTotalSelector)
const currentSafeBalance = useSelector(safeTotalFiatBalanceSelector)
const currentCurrency = useSelector(currentCurrencySelector)
const granted = useSelector(grantedSelector)
const sidebarItems = useSidebarItems()
@ -84,7 +88,7 @@ const App: React.FC = ({ children }) => {
useSafeScheduledUpdates(safeLoaded, safeAddress)
const sendFunds = safeActionsState.sendFunds
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance.toString()) : ''
const balance =
!!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,
},
logo: {
flexBasis: '114px',
flexBasis: '140px',
flexShrink: '0',
flexGrow: '0',
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 IconButton from '@material-ui/core/IconButton'
import Close from '@material-ui/icons/Close'
import { Icon } from '@gnosis.pm/safe-react-components'
import Paragraph from 'src/components/layout/Paragraph'
import { md, lg } from 'src/theme/variables'
@ -33,18 +34,28 @@ const StyledClose = styled(Close)`
width: 35px;
`
const ModalTitle = ({
iconUrl,
title,
onClose,
}: {
const GoBackWrapper = styled.div`
margin-right: 15px;
`
type Props = {
title: string
iconUrl: string
goBack?: () => void
iconUrl?: string
onClose?: () => void
}): React.ReactElement => {
}
const ModalTitle = ({ goBack, iconUrl, title, onClose }: Props): React.ReactElement => {
return (
<StyledRow align="center" grow>
<TitleWrapper>
{goBack && (
<GoBackWrapper>
<IconButton onClick={goBack}>
<Icon type="arrowLeft" size="md" />
</IconButton>
</GoBackWrapper>
)}
{iconUrl && <IconImg alt={title} src={iconUrl} />}
<StyledParagraph noMargin weight="bolder">
{title}

View File

@ -3,17 +3,18 @@ import * as React from 'react'
import Button from 'src/components/layout/Button'
import Col from 'src/components/layout/Col'
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 = {
backgroundColor: 'white',
padding: sm,
padding: lg,
borderRadius: sm,
}
const firstButtonStyle = {
marginRight: sm,
fontWeight: boldFont,
color: secondary,
}
const secondButtonStyle = {
@ -50,8 +51,8 @@ const Controls = ({
}
return (
<Row align="end" grow style={controlStyle}>
<Col end="xs" xs={12}>
<Row align="center" grow style={controlStyle}>
<Col center="xs" xs={12}>
<Button onClick={onPrevious} size="small" style={firstButtonStyle} type="button">
{back}
</Button>

View File

@ -7,7 +7,7 @@ import { lg } from 'src/theme/variables'
const useStyles = makeStyles({
root: {
margin: '10px',
margin: '10px 0 10px 10px',
maxWidth: '770px',
boxShadow: '0 0 10px 0 rgba(33,48,77,0.10)',
},

View File

@ -10,6 +10,8 @@ import {
uniqueAddress,
differentFrom,
ADDRESS_REPEATED_ERROR,
addressIsNotCurrentSafe,
OWNER_ADDRESS_IS_SAFE_ADDRESS_ERROR,
} from 'src/components/forms/validator'
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', () => {
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> => {
const contractCode = await getWeb3().eth.getCode(address)
const errorMessage = `Input must be a valid Ethereum contract address${
isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : ''
}`
const errorMessage = `Must resolve to a valid smart contract address.`
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`
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 => {
const addressExists = addresses.some((addressFromList) => sameAddress(addressFromList, address))
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 =>
validators.reduce(
(error: string | undefined, validator: GenericValidatorType): ValidatorReturnType => error || validator(value),

View File

@ -38,7 +38,7 @@ const mainnet: NetworkConfig = {
label: 'EWC',
isTestNet: false,
nativeCoin: {
address: '0x000',
address: '0x0000000000000000000000000000000000000000',
name: 'Energy web token',
symbol: 'EWT',
decimals: 18,

View File

@ -29,7 +29,7 @@ const local: NetworkConfig = {
label: 'LocalRPC',
isTestNet: true,
nativeCoin: {
address: '0x000',
address: '0x0000000000000000000000000000000000000000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,

View File

@ -38,7 +38,7 @@ const mainnet: NetworkConfig = {
label: 'Mainnet',
isTestNet: false,
nativeCoin: {
address: '0x000',
address: '0x0000000000000000000000000000000000000000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,

View File

@ -38,7 +38,7 @@ const rinkeby: NetworkConfig = {
label: 'Rinkeby',
isTestNet: true,
nativeCoin: {
address: '0x000',
address: '0x0000000000000000000000000000000000000000',
name: 'Ether',
symbol: 'ETH',
decimals: 18,

View File

@ -35,7 +35,7 @@ const mainnet: NetworkConfig = {
label: 'Volta',
isTestNet: true,
nativeCoin: {
address: '0x000',
address: '0x0000000000000000000000000000000000000000',
name: 'Energy web token',
symbol: 'EWT',
decimals: 18,

View File

@ -29,7 +29,7 @@ const xDai: NetworkConfig = {
label: 'xDai',
isTestNet: false,
nativeCoin: {
address: '0x000',
address: '0x0000000000000000000000000000000000000000',
name: 'xDai',
symbol: 'xDai',
decimals: 18,

View File

@ -6,7 +6,6 @@ import { Integrations } from '@sentry/tracing'
import Root from 'src/components/Root'
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 loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
import { store } from 'src/store'
@ -17,7 +16,6 @@ disableMMAutoRefreshWarning()
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
store.dispatch(loadActiveTokens())
store.dispatch(loadSafesFromStorage())
store.dispatch(loadDefaultSafe())
store.dispatch(loadCurrentSessionFromStorage())

View File

@ -3,8 +3,6 @@ import { NFTAsset, NFTAssets, NFTToken, NFTTokens } from 'src/logic/collectibles
import { AppReduxState } from 'src/store'
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 nftTokens = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
@ -26,21 +24,8 @@ export const orderedNFTAssets = createSelector(nftTokensSelector, (userNftTokens
export const activeNftAssetsListSelector = createSelector(
nftAssetsListSelector,
safeActiveAssetsSelector,
availableNftAssetsAddresses,
(assets, activeAssetsList, availableNftAssetsAddresses): NFTAsset[] => {
return assets
.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
}, {})
(assets, availableNftAssetsAddresses): NFTAsset[] => {
return assets.filter(({ address }) => availableNftAssetsAddresses.includes(address))
},
)

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 { 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 (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrentCurrencyPayload>>,
import { loadSelectedCurrency } from 'src/logic/safe/utils/currencyValuesStorage'
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> => {
try {
const storedSelectedCurrency = await loadSelectedCurrency()
dispatch(setSelectedCurrency(safeAddress, storedSelectedCurrency || AVAILABLE_CURRENCIES.USD))
dispatch(setSelectedCurrency({ selectedCurrency: storedSelectedCurrency || 'USD' }))
} catch (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 { ThunkDispatch } from 'redux-thunk'
import { CurrencyPayloads } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { AppReduxState } from 'src/store'
import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate'
import { createAction } from 'redux-actions'
import { SelectedCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY'
const setCurrentCurrency = createAction(SET_CURRENT_CURRENCY, (safeAddress: string, selectedCurrency: string) => ({
safeAddress,
selectedCurrency,
}))
export const setSelectedCurrency = (safeAddress: string, selectedCurrency: string) => (
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrencyPayloads>>,
): void => {
dispatch(setCurrentCurrency(safeAddress, selectedCurrency))
dispatch(fetchCurrencyRate(safeAddress, selectedCurrency))
}
export const setSelectedCurrency = createAction<SelectedCurrencyPayload>(SET_CURRENT_CURRENCY)

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 { saveSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
import { saveSelectedCurrency } from 'src/logic/safe/utils/currencyValuesStorage'
const watchedActions = [SET_CURRENT_CURRENCY]
const currencyValuesStorageMiddleware = () => (next) => async (action) => {
export const currencyValuesStorageMiddleware = () => (next) => async (action) => {
const handledAction = next(action)
if (watchedActions.includes(action.type)) {
switch (action.type) {
case SET_CURRENT_CURRENCY: {
const { selectedCurrency } = action.payload
saveSelectedCurrency(selectedCurrency)
await saveSelectedCurrency(selectedCurrency)
break
}
@ -21,5 +20,3 @@ const currencyValuesStorageMiddleware = () => (next) => async (action) => {
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 { 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 { 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 interface CurrencyReducerMap extends Map<string, any> {
get<K extends keyof CurrencyRateValue>(key: K, notSetValue?: unknown): CurrencyRateValue[K]
setIn<K extends keyof CurrencyRateValue>(keys: [string, K], value: CurrencyRateValue[K]): this
export type CurrencyValuesState = {
selectedCurrency: string
availableCurrencies: string[]
}
export type CurrencyValuesState = Map<string, CurrencyReducerMap>
export const initialState = {
selectedCurrency: 'USD',
availableCurrencies: ['USD'],
}
type CurrencyBasePayload = { safeAddress: string }
export type CurrencyRatePayload = CurrencyBasePayload & { currencyRate: number }
export type CurrencyBalancesPayload = CurrencyBasePayload & { currencyBalances: BalanceCurrencyList }
export type CurrentCurrencyPayload = CurrencyBasePayload & { selectedCurrency: string }
export type SelectedCurrencyPayload = { selectedCurrency: string }
export type AvailableCurrenciesPayload = { availableCurrencies: string[] }
export type CurrencyPayloads = CurrencyRatePayload | CurrencyBalancesPayload | CurrentCurrencyPayload
export default handleActions<CurrencyReducerMap, CurrencyPayloads>(
export default handleActions<AppReduxState['currencyValues'], CurrencyValuesState>(
{
[SET_CURRENCY_RATE]: (state, action: Action<CurrencyRatePayload>) => {
const { currencyRate, safeAddress } = action.payload
return state.setIn([safeAddress, 'currencyRate'], currencyRate)
[SET_CURRENT_CURRENCY]: (state, action: Action<SelectedCurrencyPayload>) => {
const { selectedCurrency } = action.payload
state.selectedCurrency = selectedCurrency
return state
},
[SET_CURRENCY_BALANCES]: (state, action: Action<CurrencyBalancesPayload>) => {
const { safeAddress, currencyBalances } = action.payload
return state.setIn([safeAddress, 'currencyBalances'], currencyBalances)
},
[SET_CURRENT_CURRENCY]: (state, action: Action<CurrentCurrencyPayload>) => {
const { safeAddress, selectedCurrency } = action.payload
return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency)
[SET_AVAILABLE_CURRENCIES]: (state, action: Action<AvailableCurrenciesPayload>) => {
const { availableCurrencies } = action.payload
state.availableCurrencies = availableCurrencies
return state
},
},
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 { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
import { BigNumber } from 'bignumber.js'
import { CURRENCY_VALUES_KEY, CurrencyValuesState } from 'src/logic/currencyValues/store/reducer/currencyValues'
export const currencyValuesSelector = (state: AppReduxState): CurrencyValuesState => state[CURRENCY_VALUES_KEY]
export const safeFiatBalancesSelector = createSelector(
currencyValuesSelector,
safeParamAddressFromStateSelector,
(currencyValues, safeAddress): CurrencyReducerMap | undefined => {
if (!currencyValues || !safeAddress) return
return currencyValues.get(safeAddress)
},
)
export const currentCurrencySelector = (state: AppReduxState): string => {
return state[CURRENCY_VALUES_KEY].selectedCurrency
}
const currencyValueSelector = <K extends keyof CurrencyRateValue>(key: K) => (
currencyValuesMap?: CurrencyReducerMap,
): 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)
},
)
export const availableCurrenciesSelector = (state: AppReduxState): string[] => {
return state[CURRENCY_VALUES_KEY].availableCurrencies
}

View File

@ -218,10 +218,9 @@ export const useEstimateTransactionGas = ({
)
const fixedGasCosts = getFixedGasCosts(Number(threshold))
const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)
try {
const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)
const gasEstimation = await estimateTransactionGas({
safeAddress,
txRecipient,
@ -279,7 +278,7 @@ export const useEstimateTransactionGas = ({
gasLimit: '0',
isExecution,
isCreation,
isOffChainSignature: false,
isOffChainSignature,
})
}
}

View File

@ -1,10 +1,7 @@
import axios from 'axios'
import { getSafeClientGatewayBaseUrl } from 'src/config'
import {
fetchTokenCurrenciesBalances,
BalanceEndpoint,
} from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import { fetchTokenCurrenciesBalances } from 'src/logic/safe/api/fetchTokenCurrenciesBalances'
import { aNewStore } from 'src/store'
jest.mock('axios')
@ -52,11 +49,15 @@ describe('fetchTokenCurrenciesBalances', () => {
axios.get.mockImplementationOnce(() => Promise.resolve({ data: expectedResult }))
// when
const result = await fetchTokenCurrenciesBalances(safeAddress, excludeSpamTokens)
const result = await fetchTokenCurrenciesBalances({
safeAddress,
excludeSpamTokens,
selectedCurrency: 'USD',
})
// then
expect(result).toStrictEqual(expectedResult)
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 { batch, useDispatch } from 'react-redux'
import { batch, useDispatch, useSelector } from 'react-redux'
import { useLocation } from 'react-router-dom'
import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles'
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 { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
export const useFetchTokens = (safeAddress: string): void => {
const dispatch = useDispatch<Dispatch>()
const location = useLocation()
const currentCurrency = useSelector(currentCurrencySelector)
useMemo(() => {
if (COINS_LOCATION_REGEX.test(location.pathname)) {
batch(() => {
// fetch tokens there to get symbols for tokens in TXs list
dispatch(fetchTokens())
dispatch(fetchSelectedCurrency(safeAddress))
dispatch(fetchSafeTokens(safeAddress))
dispatch(fetchSelectedCurrency())
dispatch(fetchSafeTokens(safeAddress, currentCurrency))
})
}
if (COLLECTIBLES_LOCATION_REGEX.test(location.pathname)) {
batch(() => {
dispatch(fetchCollectibles(safeAddress)).then(() => {
dispatch(activateAssetsByBalance(safeAddress))
})
})
dispatch(fetchCollectibles(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 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 fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
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 => {
const dispatch = useDispatch<Dispatch>()
@ -20,7 +21,8 @@ export const useLoadSafe = (safeAddress?: string): boolean => {
await dispatch(fetchSafe(safeAddress))
setIsSafeLoaded(true)
await dispatch(fetchSafeTokens(safeAddress))
dispatch(fetchTransactions(safeAddress))
await dispatch(updateAvailableCurrencies())
await dispatch(fetchTransactions(safeAddress))
dispatch(addViewedSafe(safeAddress))
}
}

View File

@ -2,8 +2,7 @@ import { useEffect, useRef } from 'react'
import { batch, useDispatch } from 'react-redux'
import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles'
import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens'
import fetchEtherBalance from 'src/logic/safe/store/actions/fetchEtherBalance'
import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens'
import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import { TIMEOUT } from 'src/utils/constants'
@ -17,9 +16,8 @@ export const useSafeScheduledUpdates = (safeLoaded: boolean, safeAddress?: strin
// has to run again
let mounted = true
const fetchSafeData = async (address: string): Promise<void> => {
await batch(async () => {
batch(async () => {
await Promise.all([
dispatch(fetchEtherBalance(address)),
dispatch(fetchSafeTokens(address)),
dispatch(fetchTransactions(address)),
dispatch(fetchCollectibles(address)),
@ -28,9 +26,7 @@ export const useSafeScheduledUpdates = (safeLoaded: boolean, safeAddress?: strin
})
if (mounted) {
timer.current = window.setTimeout(() => {
fetchSafeData(address)
}, TIMEOUT * 3)
timer.current = window.setTimeout(() => fetchSafeData(address), TIMEOUT * 3)
}
}

View File

@ -1,18 +1,14 @@
import { useSelector } from 'react-redux'
import { getNetworkInfo } from 'src/config'
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'
const { nativeCoin } = getNetworkInfo()
const useTokenInfo = (address: string): Token | undefined => {
const tokens = useSelector(safeKnownCoins)
if (tokens) {
const tokenAddress = sameAddress(address, ZERO_ADDRESS) ? nativeCoin.address : address
return tokens.find((token) => sameAddress(token.address, tokenAddress)) ?? undefined
return tokens.find((token) => sameAddress(token.address, address))
}
}

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'
const addSafeOwner = createAction(ADD_SAFE_OWNER)
export default addSafeOwner
export const addSafeOwner = createAction(ADD_SAFE_OWNER)

View File

@ -12,6 +12,7 @@ import {
tryOffchainSigning,
} from 'src/logic/safe/transactions'
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 { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
@ -148,6 +149,9 @@ export const createTransaction = (
await saveTxToHistory({ ...txArgs, txHash, origin })
// store the pending transaction's nonce
isExecution && aboutToExecuteTx.setNonce(txArgs.nonce)
dispatch(fetchTransactions(safeAddress))
})
.on('error', (error) => {
@ -156,10 +160,6 @@ export const createTransaction = (
onError?.()
})
.then(async (receipt) => {
if (isExecution) {
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded))
}
dispatch(fetchTransactions(safeAddress))
return receipt.transactionHash

View File

@ -5,7 +5,7 @@ import { Dispatch } from 'redux'
import { backOff } from 'exponential-backoff'
import { AppReduxState } from 'src/store'
const fetchEtherBalance = (safeAddress: string) => async (
export const fetchEtherBalance = (safeAddress: string) => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<void> => {
@ -21,5 +21,3 @@ const fetchEtherBalance = (safeAddress: string) => async (
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 { sameAddress } from 'src/logic/wallets/ethAddresses'
import { getBalanceInEtherOf } from 'src/logic/wallets/getWeb3'
import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
import { addSafeOwner } from 'src/logic/safe/store/actions/addSafeOwner'
import { removeSafeOwner } from 'src/logic/safe/store/actions/removeSafeOwner'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { makeOwner } from 'src/logic/safe/store/models/owner'
import { checksumAddress } from 'src/utils/checksumAddress'
import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe'
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 { getModules } from 'src/logic/safe/utils/modules'
import { getSpendingLimits } from 'src/logic/safe/utils/spendingLimits'
@ -46,6 +46,7 @@ export const buildSafe = async (
safeAdd: string,
safeName: string,
latestMasterContractVersion?: string,
totalFiatBalance?: number,
): Promise<SafeRecordProps> => {
const safeAddress = checksumAddress(safeAdd)
@ -80,16 +81,14 @@ export const buildSafe = async (
threshold,
owners,
ethBalance,
totalFiatBalance: totalFiatBalance || 0,
nonce,
currentVersion: currentVersion ?? '',
needsUpdate,
featuresEnabled,
balances: localSafe?.balances || Map(),
latestIncomingTxBlock: 0,
activeAssets: Set(),
activeTokens: Set(),
blacklistedAssets: Set(),
blacklistedTokens: Set(),
modules,
spendingLimits,
}
@ -162,7 +161,8 @@ export default (safeAdd: string) => async (
const safeAddress = checksumAddress(safeAdd)
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'
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
// and `addSafe` will only add a newly non-existent safe

View File

@ -11,6 +11,7 @@ import {
} from 'src/logic/safe/safeTxSigner'
import { getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions'
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 { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
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 }))
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))
return
@ -154,6 +153,10 @@ export const processTransaction = ({
try {
await saveTxToHistory({ ...txArgs, txHash })
// store the pending transaction's nonce
isExecution && aboutToExecuteTx.setNonce(txArgs.nonce)
dispatch(fetchTransactions(safeAddress))
} catch (e) {
console.error(e)
@ -172,10 +175,6 @@ export const processTransaction = ({
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
if (isExecution) {
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded))
}
dispatch(fetchTransactions(safeAddress))
if (isExecution) {

View File

@ -2,6 +2,4 @@ import { createAction } from 'redux-actions'
export const REMOVE_SAFE_OWNER = 'REMOVE_SAFE_OWNER'
const removeSafeOwner = createAction(REMOVE_SAFE_OWNER)
export default removeSafeOwner
export const removeSafeOwner = createAction(REMOVE_SAFE_OWNER)

View File

@ -2,6 +2,4 @@ import { createAction } from 'redux-actions'
export const REPLACE_SAFE_OWNER = 'REPLACE_SAFE_OWNER'
const replaceSafeOwner = createAction(REPLACE_SAFE_OWNER)
export default replaceSafeOwner
export const replaceSafeOwner = createAction(REPLACE_SAFE_OWNER)

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 { Action } from 'redux-actions'
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
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 { userAccountSelector } from 'src/logic/wallets/store/selectors'
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 { 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 { 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'
@ -70,9 +76,21 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
const state = store.getState()
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: {
const { safeAddress, values } = action.payload
const transactions = values.filter((tx) => isTransactionSummary(tx)).map((item) => item.transaction)
const { safeAddress, values } = (action as Action<QueuedPayload>).payload
const transactions = values
.filter((tx) => isTransactionSummary(tx))
.map((item: TransactionGatewayResult) => item.transaction)
const userAddress: string = userAccountSelector(state)
const awaitingTransactions = getAwaitingGatewayTransactions(transactions, userAddress)

View File

@ -1,7 +1,4 @@
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 { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
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 { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
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 { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors'
import { safesMapSelector } from 'src/logic/safe/store/selectors'
import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { checksumAddress } from 'src/utils/checksumAddress'
@ -26,28 +21,10 @@ const watchedActions = [
REMOVE_SAFE_OWNER,
REPLACE_SAFE_OWNER,
EDIT_SAFE_OWNER,
ACTIVATE_TOKEN_FOR_ALL_SAFES,
UPDATE_TOKENS_LIST,
UPDATE_ASSETS_LIST,
SET_DEFAULT_SAFE,
]
const recalculateActiveTokens = (state) => {
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) => {
export const safeStorageMiddleware = (store) => (next) => async (action) => {
const handledAction = next(action)
if (watchedActions.includes(action.type)) {
@ -57,10 +34,6 @@ const safeStorageMware = (store) => (next) => async (action) => {
await saveSafes(safes.toJSON())
switch (action.type) {
case ACTIVATE_TOKEN_FOR_ALL_SAFES: {
recalculateActiveTokens(state)
break
}
case ADD_OR_UPDATE_SAFE: {
const { safe } = action.payload
safe.owners.forEach((owner) => {
@ -72,10 +45,7 @@ const safeStorageMware = (store) => (next) => async (action) => {
break
}
case UPDATE_SAFE: {
const { activeTokens, name, address } = action.payload
if (activeTokens) {
recalculateActiveTokens(state)
}
const { name, address } = action.payload
if (name) {
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address })))
}
@ -94,5 +64,3 @@ const safeStorageMware = (store) => (next) => async (action) => {
return handledAction
}
export default safeStorageMware

View File

@ -1,5 +1,6 @@
import { List, Map, Record, RecordOf, Set } from 'immutable'
import { FEATURES } from 'src/config/networks/network.d'
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
export type SafeOwner = {
name: string
@ -28,14 +29,12 @@ export type SafeRecordProps = {
address: string
threshold: number
ethBalance: string
totalFiatBalance: number
owners: List<SafeOwner>
modules?: ModulePair[] | null
spendingLimits?: SpendingLimit[] | null
activeTokens: Set<string>
activeAssets: Set<string>
blacklistedTokens: Set<string>
blacklistedAssets: Set<string>
balances: Map<string, string>
balances: Map<string, BalanceRecord>
nonce: number
latestIncomingTxBlock: number
recurringUser?: boolean
@ -49,13 +48,11 @@ const makeSafe = Record<SafeRecordProps>({
address: '',
threshold: 0,
ethBalance: '0',
totalFiatBalance: 0,
owners: List([]),
modules: [],
spendingLimits: [],
activeTokens: Set(),
activeAssets: Set(),
blacklistedTokens: Set(),
blacklistedAssets: Set(),
balances: Map(),
nonce: 0,
latestIncomingTxBlock: 0,

View File

@ -344,6 +344,7 @@ export const gatewayTransactions = handleActions<AppReduxState['gatewayTransacti
}
case 'queued.queued': {
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`
if (txStatus === 'PENDING_FAILED' && txToUpdate.txStatus !== 'PENDING') {
return txToUpdate

View File

@ -1,7 +1,6 @@
import { Map, Set, List } from 'immutable'
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 { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner'
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_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion'
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 makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe'
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 owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
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)
return {
@ -38,9 +32,6 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => {
owners,
balances,
activeTokens,
blacklistedTokens,
activeAssets,
blacklistedAssets,
latestIncomingTxBlock: 0,
modules: null,
}
@ -102,21 +93,6 @@ export default handleActions<AppReduxState['safes'], Payloads>(
)
: 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>) => {
const { safe } = action.payload
const safeAddress = safe.address
@ -195,24 +171,6 @@ export default handleActions<AppReduxState['safes'], Payloads>(
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_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action<SafeRecord>) =>
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()
export const safeFieldSelector = <K extends keyof SafeRecordProps>(field: K) => (
@ -172,14 +127,6 @@ export const getActiveTokensAddressesForAllSafes = createSelector(safesListSelec
return addresses
})
export const getBlacklistedTokensAddressesForAllSafes = createSelector(safesListSelector, (safes) => {
const addresses = Set().withMutations((set) => {
safes.forEach((safe) => {
safe.blacklistedTokens.forEach((tokenAddress) => {
set.add(tokenAddress)
})
})
})
return addresses
export const safeTotalFiatBalanceSelector = createSelector(safeSelector, (currentSafe) => {
return currentSafe?.totalFiatBalance
})

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,
balances,
recurringUser,
blacklistedAssets,
blacklistedTokens,
activeAssets,
activeTokens,
owners,
featuresEnabled,
@ -32,10 +29,6 @@ const getMockedOldSafe = ({
}
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const mockedActiveAssetsAddress1 = '0x503ab2a6A70c6C6ec8b25a4C87C784e1c8f8e8CD'
const mockedActiveAssetsAddress2 = '0xfdd4E685361CB7E89a4D27e03DCd0001448d731F'
const mockedBlacklistedTokenAddress1 = '0xc7d892dca37a244Fb1A7461e6141e58Ead460282'
const mockedBlacklistedAssetAddress1 = '0x0ac539137c4c99001f16Dd132E282F99A02Ddc3F'
return {
name: name || 'MockedSafe',
@ -46,14 +39,11 @@ const getMockedOldSafe = ({
modules: modules || [],
spendingLimits: spendingLimits || [],
activeTokens: activeTokens || Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]),
activeAssets: activeAssets || Set([mockedActiveAssetsAddress1, mockedActiveAssetsAddress2]),
blacklistedTokens: blacklistedTokens || Set([mockedBlacklistedTokenAddress1]),
blacklistedAssets: blacklistedAssets || Set([mockedBlacklistedAssetAddress1]),
balances:
balances ||
Map({
[mockedActiveTokenAddress1]: '100',
[mockedActiveTokenAddress2]: '10',
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
[mockedActiveTokenAddress2]: { tokenBalance: '10' },
}),
nonce: nonce || 2,
latestIncomingTxBlock: latestIncomingTxBlock || 1,
@ -61,6 +51,7 @@ const getMockedOldSafe = ({
currentVersion: currentVersion || 'v1.1.1',
needsUpdate: needsUpdate || false,
featuresEnabled: featuresEnabled || [],
totalFiatBalance: 110,
}
}
@ -203,67 +194,16 @@ describe('shouldSafeStoreBeUpdated', () => {
// Then
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`, () => {
// given
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
const oldBalances = Map({
[mockedActiveTokenAddress1]: '100',
[mockedActiveTokenAddress2]: '10',
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
[mockedActiveTokenAddress2]: { tokenBalance: '100' },
})
const newBalances = Map({
[mockedActiveTokenAddress1]: '100',
[mockedActiveTokenAddress1]: { tokenBalance: '100' },
})
const oldSafe = getMockedOldSafe({ balances: oldBalances })
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 { getNetworkInfo } from 'src/config'
import { AbiItem } from 'web3-utils'
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 { getSpendingLimitContract, MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
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 { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants'
import { getEncodedMultiSendCallData, MultiSendTx } from './upgradeSafe'
@ -138,16 +137,13 @@ type DeleteAllowanceParams = {
}
export const getDeleteAllowanceTxData = ({ beneficiary, tokenAddress }: DeleteAllowanceParams): string => {
const { nativeCoin } = getNetworkInfo()
const token = sameAddress(tokenAddress, nativeCoin.address) ? ZERO_ADDRESS : tokenAddress
const web3 = getWeb3()
const spendingLimitContract = new web3.eth.Contract(
SpendingLimitModule.abi as AbiItem[],
SPENDING_LIMIT_MODULE_ADDRESS,
)
return spendingLimitContract.methods.deleteAllowance(beneficiary, token).encodeABI()
return spendingLimitContract.methods.deleteAllowance(beneficiary, tokenAddress).encodeABI()
}
export const enableSpendingLimitModuleMultiSendTx = (safeAddress: string): MultiSendTx => {
@ -188,20 +184,13 @@ export const setSpendingLimitTx = ({
safeAddress,
}: SpendingLimitTxParams): CreateTransactionArgs => {
const spendingLimitContract = getSpendingLimitContract()
const { nativeCoin } = getNetworkInfo()
const txArgs: CreateTransactionArgs = {
safeAddress,
to: SPENDING_LIMIT_MODULE_ADDRESS,
valueInWei: ZERO_VALUE,
txData: spendingLimitContract.methods
.setAllowance(
beneficiary,
token === nativeCoin.address ? ZERO_ADDRESS : token,
spendingLimitInWei,
resetTimeMin,
resetBaseMin,
)
.setAllowance(beneficiary, token, spendingLimitInWei, resetTimeMin, resetBaseMin)
.encodeABI(),
operation: CALL,
notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX,
@ -285,12 +274,5 @@ export const getSpendingLimitByTokenAddress = ({
return
}
const { nativeCoin } = getNetworkInfo()
return spendingLimits.find(({ token: spendingLimitTokenAddress }) => {
spendingLimitTokenAddress = sameAddress(spendingLimitTokenAddress, ZERO_ADDRESS)
? nativeCoin.address
: spendingLimitTokenAddress
return sameAddress(spendingLimitTokenAddress, tokenAddress)
})
return spendingLimits.find(({ token }) => sameAddress(token, 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'
const addTokens = createAction(ADD_TOKENS, (tokens) => ({
export const addTokens = createAction(ADD_TOKENS, (tokens) => ({
tokens,
}))
export default addTokens

View File

@ -2,63 +2,56 @@ import { backOff } from 'exponential-backoff'
import { List, Map } from 'immutable'
import { Dispatch } from 'redux'
import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
import {
AVAILABLE_CURRENCIES,
CurrencyRateValueRecord,
makeBalanceCurrency,
} from 'src/logic/currencyValues/store/model/currencyValues'
import addTokens from 'src/logic/tokens/store/actions/saveTokens'
import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/safe/api/fetchTokenCurrenciesBalances'
import { addTokens } from 'src/logic/tokens/store/actions/addTokens'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
import { AppReduxState } from 'src/store'
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 BigNumber from 'bignumber.js'
import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors'
import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
import { getNetworkInfo } from 'src/config'
import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses'
export type BalanceRecord = {
tokenBalance: string
fiatBalance?: string
}
interface ExtractedData {
balances: Map<string, string>
currencyList: List<CurrencyRateValueRecord>
balances: Map<string, BalanceRecord>
ethBalance: string
tokens: List<Token>
}
const { nativeCoin } = getNetworkInfo()
const extractDataFromResult = (currentTokens: TokenState, fiatCode: string) => (
const extractDataFromResult = (currentTokens: TokenState) => (
acc: ExtractedData,
{ balance, fiatBalance, tokenInfo }: TokenBalance,
): ExtractedData => {
const { address: tokenAddress, 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)) })
const { address, decimals } = tokenInfo
if (currentTokens && !currentTokens.get(tokenAddress)) {
acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo }))
}
acc.balances = acc.balances.merge({
[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(
makeBalanceCurrency({
currencyName: fiatCode,
tokenAddress,
balanceInBaseCurrency: fiatBalance,
balanceInSelectedCurrency: fiatBalance,
}),
)
if (currentTokens && !currentTokens.get(address)) {
acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo }))
}
return acc
}
const fetchSafeTokens = (safeAddress: string) => async (
export const fetchSafeTokens = (safeAddress: string, currencySelected?: string) => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<void> => {
@ -66,38 +59,40 @@ const fetchSafeTokens = (safeAddress: string) => async (
const state = getState()
const safe = safeSelector(state)
const currentTokens = tokensSelector(state)
const currencySelected = currentCurrencySelector(state)
if (!safe) {
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 blacklistedTokens = safeBlacklistedTokensSelector(state)
const { balances, currencyList, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>(
extractDataFromResult(currentTokens, currencySelected || AVAILABLE_CURRENCIES.USD),
const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce<ExtractedData>(
extractDataFromResult(currentTokens),
{
balances: Map(),
currencyList: List(),
ethBalance: '0',
tokens: List(),
},
)
// need to persist those already active tokens, despite its balances
const activeTokens = alreadyActiveTokens.union(
// active tokens by balance, excluding those already blacklisted and the `null` address
balances.keySeq().toSet().subtract(blacklistedTokens),
)
const activeTokens = alreadyActiveTokens.union(balances.keySeq().toSet())
dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance }))
dispatch(setCurrencyBalances(safeAddress, currencyList))
dispatch(
updateSafe({
address: safeAddress,
activeTokens,
balances,
ethBalance,
totalFiatBalance: new BigNumber(tokenCurrenciesBalances.fiatTotal).toFixed(2),
}),
)
dispatch(addTokens(tokens))
} catch (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 contract from '@truffle/contract/index.js'
import { AbiItem } from 'web3-utils'
import saveTokens from './saveTokens'
import { addTokens } from 'src/logic/tokens/store/actions/addTokens'
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
import { fetchErc20AndErc721AssetsList } from 'src/logic/tokens/api'
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)
store.dispatch(saveTokens(newTokens))
store.dispatch(addTokens(newTokens))
return token
}
@ -109,10 +107,8 @@ export const fetchTokens = () => async (
const tokens = List(erc20Tokens.map((token) => makeToken(token)))
dispatch(saveTokens(tokens))
dispatch(addTokens(tokens))
} catch (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 { TokenType } from 'src/logic/safe/store/models/types/gateway'
import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens'
export type TokenProps = {
address: string
@ -7,7 +8,7 @@ export type TokenProps = {
symbol: string
decimals: number | string
logoUri: string
balance: number | string
balance: BalanceRecord
type?: TokenType
}
@ -17,7 +18,10 @@ export const makeToken = Record<TokenProps>({
symbol: '',
decimals: 0,
logoUri: '',
balance: 0,
balance: {
fiatBalance: '0',
tokenBalance: '0',
},
})
// 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 { 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 { AppReduxState } from 'src/store'

View File

@ -58,7 +58,7 @@ describe('getERC20DecimalsAndSymbol', () => {
symbol,
decimals,
logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png',
balance: 0,
balance: { tokenBalance: '0', fiatBalance: '0' },
})
const expectedResult = {
decimals,

View File

@ -15,7 +15,9 @@ export const getEthAsToken = (balance: string | number): Token => {
const { nativeCoin } = getNetworkInfo()
return makeToken({
...nativeCoin,
balance,
balance: {
tokenBalance: balance.toString(),
},
})
}
@ -73,7 +75,7 @@ export type GetTokenByAddress = {
tokens: List<Token>
}
export type TokenFound = {
type TokenFound = {
balance: string | number
decimals: string | number
}
@ -92,7 +94,7 @@ export const getBalanceAndDecimalsFromToken = ({ tokenAddress, tokens }: GetToke
}
return {
balance: token.balance ?? 0,
balance: token.balance.tokenBalance ?? 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>
</Row>
<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
wallet. The creation will cost approximately {gasCostFormatted} {nativeCoin.name}. The exact amount will be
determined by your wallet.

View File

@ -1,5 +1,6 @@
import { createStyles, makeStyles } from '@material-ui/core/styles'
import * as React from 'react'
import styled from 'styled-components'
import OpenPaper from 'src/components/Stepper/OpenPaper'
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 SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement => {
@ -36,13 +43,13 @@ const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement =>
return (
<>
<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
wallet a name. This name is only stored locally and will never be shared with Gnosis or any third parties.
</Paragraph>
</Block>
<Block className={classes.root} margin="lg">
<Field
<StyledField
component={TextField}
defaultValue={safeName}
name={FIELD_NAME}
@ -54,7 +61,7 @@ const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement =>
/>
</Block>
<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{' '}
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
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 CheckCircle from '@material-ui/icons/CheckCircle'
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 trash from 'src/assets/icons/trash.svg'
import { ScanQRModal } from 'src/components/ScanQRModal'
@ -45,6 +47,10 @@ const { useState } = React
export const ADD_OWNER_BUTTON = '+ Add another owner'
const StyledAddressInput = styled(AddressInput)`
width: 460px;
`
/**
* Validates the whole OwnersForm, specially checks for non-repeated addresses
*
@ -83,7 +89,7 @@ export const calculateValuesAfterRemoving = (index: number, values: Record<strin
return newValues
}
const ownerToRemove = new RegExp(`owner${index}(Name|Address)`)
const ownerToRemove = new RegExp(`owner${padOwnerIndex(index)}(Name|Address)`)
if (ownerToRemove.test(key)) {
// skip, doing anything with the removed field
@ -96,7 +102,7 @@ export const calculateValuesAfterRemoving = (index: number, values: Record<strin
if (Number(ownerOrder) > index) {
// 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 {
// previous owners to the deleted row
newValues[key] = values[key]
@ -152,7 +158,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
return (
<>
<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,
but you are free to change this to a different owner.
<br />
@ -167,7 +173,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
rel="noreferrer"
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
</Text>
<Icon size="sm" type="externalLink" color="primary" />
@ -176,8 +182,8 @@ const SafeOwnersForm = (props): React.ReactElement => {
</Block>
<Hairline />
<Row className={classes.header}>
<Col xs={4}>NAME</Col>
<Col xs={8}>ADDRESS</Col>
<Col xs={3}>NAME</Col>
<Col xs={7}>ADDRESS</Col>
</Row>
<Hairline />
<Block margin="md" padding="md">
@ -187,7 +193,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
return (
<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
className={classes.name}
component={TextField}
@ -199,8 +205,8 @@ const SafeOwnersForm = (props): React.ReactElement => {
testId={`create-safe-owner-name-field-${index}`}
/>
</Col>
<Col className={classes.ownerAddress} xs={6}>
<AddressInput
<Col className={classes.ownerAddress} xs={7}>
<StyledAddressInput
fieldMutator={(newOwnerAddress) => {
const newOwnerName = getNameFromAddressBook(addressBook, newOwnerAddress, {
filterOnlyValidName: true,
@ -246,7 +252,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
</Block>
<Row align="center" className={classes.add} grow margin="xl">
<Button color="secondary" data-testid="add-owner-btn" onClick={onAddOwner}>
<Paragraph noMargin size="md">
<Paragraph noMargin size="lg">
{ADD_OWNER_BUTTON}
</Paragraph>
</Button>
@ -256,7 +262,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
Any transaction requires the confirmation of:
</Paragraph>
<Row align="center" className={classes.ownersAmount} margin="xl">
<Col className={classes.ownersAmountItem} xs={2}>
<Col className={classes.ownersAmountItem} xs={1}>
<Field
component={SelectField}
data-testid="threshold-select-input"

View File

@ -5,10 +5,10 @@ describe('calculateValuesAfterRemoving', () => {
// Given
const formContent = {
name: 'My Safe',
owner0Name: 'Owner 0',
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
owner1Name: 'Owner 1',
owner1Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0',
owner0000Name: 'Owner 0',
owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
owner0001Name: 'Owner 1',
owner0001Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0',
}
// When
@ -17,8 +17,8 @@ describe('calculateValuesAfterRemoving', () => {
// Then
expect(newFormContent).toStrictEqual({
name: 'My Safe',
owner0Name: 'Owner 0',
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
owner0000Name: 'Owner 0',
owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
})
})
@ -26,12 +26,12 @@ describe('calculateValuesAfterRemoving', () => {
// Given
const formContent = {
name: 'My Safe',
owner0Name: 'Owner 0',
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
owner1Name: 'Owner 1',
owner1Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0',
owner2Name: 'Owner 2',
owner2Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b',
owner0000Name: 'Owner 0',
owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
owner0001Name: 'Owner 1',
owner0001Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0',
owner0002Name: 'Owner 2',
owner0002Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b',
}
// When
@ -40,10 +40,10 @@ describe('calculateValuesAfterRemoving', () => {
// Then
expect(newFormContent).toStrictEqual({
name: 'My Safe',
owner0Name: 'Owner 0',
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
owner1Name: 'Owner 2',
owner1Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b',
owner0000Name: 'Owner 0',
owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
owner0001Name: 'Owner 2',
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_CONFIRMATIONS = 'confirmations'
export const FIELD_OWNERS = 'owners'
export const FIELD_SAFE_NAME = 'safeName'
export const FIELD_CREATION_PROXY_SALT = 'safeCreationSalt'
export const getOwnerNameBy = (index: number): string => `owner${index}Name`
export const getOwnerAddressBy = (index: number): string => `owner${index}Address`
export const getOwnerNameBy = (index: number): string => `owner${padOwnerIndex(index)}Name`
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)
.sort()
.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 { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
import Button from 'src/components/layout/Button'
import { connected } from 'src/theme/variables'
import { getExplorerInfo } from 'src/config'
import Hairline from 'src/components/layout/Hairline'
const ExplorerLink = styled.a`
color: ${connected};
const StyledText = styled(Text)`
display: inline-flex;
a {
margin-left: 4px;
}
svg {
position: relative;
top: 4px;
left: 4px;
}
`
const ButtonWithMargin = styled(Button)`
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 { url, alt } = explorerInfo()
const match = /(http|https):\/\/(\w+\.\w+)\/.*/i.exec(url)
@ -21,20 +38,23 @@ export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: stri
return (
<span>
<p>This process should take a couple of minutes.</p>
<p>
<Text size="xl">This process should take a couple of minutes.</Text>
<StyledText size="xl">
Follow the progress on{' '}
<ExplorerLink
aria-label={alt}
<Link
href={url}
rel="noopener noreferrer"
aria-label={alt}
target="_blank"
rel="noopener noreferrer"
data-testid="safe-create-explorer-link"
title="More info about this in Etherscan"
>
{explorerDomain}
</ExplorerLink>
.
</p>
<Text size="xl" as="span" color="primary">
{explorerDomain}
</Text>
<Icon size="sm" type="externalLink" color="primary" />
</Link>
</StyledText>
</span>
)
}
@ -45,16 +65,19 @@ export const ContinueFooter = ({
}: {
continueButtonDisabled: boolean
onContinue: (event: SyntheticEvent) => void
}) => (
<Button
color="primary"
disabled={continueButtonDisabled}
onClick={onContinue}
variant="contained"
data-testid="continue-btn"
>
Continue
</Button>
}): ReactElement => (
<FooterContainer>
<Hairline />
<Button
color="primary"
disabled={continueButtonDisabled}
onClick={onContinue}
variant="contained"
data-testid="continue-btn"
>
Get started
</Button>
</FooterContainer>
)
export const ErrorFooter = ({
@ -63,13 +86,14 @@ export const ErrorFooter = ({
}: {
onCancel: (event: SyntheticEvent) => void
onRetry: (event: SyntheticEvent) => void
}) => (
<>
}): ReactElement => (
<FooterContainer>
<Hairline />
<ButtonWithMargin onClick={onCancel} variant="contained">
Cancel
</ButtonWithMargin>
<Button color="primary" onClick={onRetry} variant="contained">
Retry
</Button>
</>
</FooterContainer>
)

View File

@ -12,20 +12,19 @@ import Paragraph from 'src/components/layout/Paragraph'
import { instantiateSafeContracts } from 'src/logic/contracts/safeContracts'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
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 { useSelector } from 'react-redux'
import LoaderDotsSvg from './assets/loader-dots.svg'
import SuccessSvg from './assets/success.svg'
import SuccessSvg from './assets/safe-created.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'
const Wrapper = styled.div`
display: grid;
grid-template-columns: 250px auto;
grid-template-rows: 62px auto;
grid-template-rows: 43px auto;
margin-bottom: 30px;
`
@ -44,29 +43,31 @@ const Body = styled.div`
grid-column: 2;
grid-row: 2;
text-align: center;
background-color: #ffffff;
background-color: ${({ theme }) => theme.colors.white};
border-radius: 5px;
min-width: 700px;
padding-top: 50px;
padding-top: 70px;
box-shadow: 0 0 10px 0 rgba(33, 48, 77, 0.1);
display: grid;
grid-template-rows: 100px 50px 70px 60px 100px;
grid-template-rows: 100px 50px 110px 1fr;
`
const CardTitle = styled.div`
font-size: 20px;
padding-top: 10px;
`
interface FullParagraphProps {
inversecolors: string
stepIndex: number
}
const FullParagraph = styled(Paragraph)<FullParagraphProps>`
background-color: ${(p) => (p.inversecolors ? connected : background)};
color: ${(p) => (p.inversecolors ? background : connected)};
padding: 24px;
font-size: 16px;
background-color: ${({ stepIndex }) => (stepIndex === 0 ? connected : background)};
color: ${({ theme, stepIndex }) => (stepIndex === 0 ? theme.colors.white : fontColor)};
padding: 28px;
font-size: 20px;
margin-bottom: 16px;
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`
grid-row: 2;
`
const BodyLoader = styled.div`
grid-row: 3;
display: flex;
justify-content: center;
align-items: center;
`
const BodyInstruction = styled.div`
grid-row: 4;
grid-row: 3;
margin: 27px 0;
`
const BodyFooter = styled.div`
grid-row: 5;
grid-row: 4;
padding: 10px 0;
display: flex;
@ -154,7 +150,7 @@ export const SafeDeployment = ({
}
if (stepIndex <= 4) {
return VaultSvg
return VaultLoading
}
return SuccessSvg
@ -326,20 +322,26 @@ export const SafeDeployment = ({
</Nav>
<Body>
<BodyImage>
<Img alt="Vault" height={75} src={getImage()} />
<Img alt="Vault" height={92} src={getImage()} />
</BodyImage>
<BodyDescription>
<CardTitle>{steps[stepIndex].description || steps[stepIndex].label}</CardTitle>
</BodyDescription>
<BodyLoader>{!error && stepIndex <= 4 && <Img alt="Loader dots" src={LoaderDotsSvg} />}</BodyLoader>
<BodyInstruction>
<FullParagraph color="primary" inversecolors={confirmationStep.toString()} noMargin size="md">
{error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction}
</FullParagraph>
</BodyInstruction>
{steps[stepIndex].instruction && (
<BodyInstruction>
<FullParagraph
color="primary"
inversecolors={confirmationStep.toString()}
noMargin
size="md"
stepIndex={stepIndex}
>
{error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction}
</FullParagraph>
</BodyInstruction>
)}
<BodyFooter>
{FooterComponent ? (
@ -354,9 +356,12 @@ export const SafeDeployment = ({
) : null}
</BodyFooter>
</Body>
<BackButton color="primary" minWidth={140} onClick={onCancel} data-testid="safe-creation-back-btn">
Back
</BackButton>
{stepIndex !== 0 && (
<BackButton color="primary" minWidth={140} onClick={onCancel} data-testid="safe-creation-back-btn">
Back
</BackButton>
)}
</Wrapper>
)
}

View File

@ -1,6 +1,6 @@
import { ContinueFooter, GenericFooter } from './components/Footer'
export const isConfirmationStep = (stepIndex?: number) => stepIndex === 0
export const isConfirmationStep = (stepIndex?: number): boolean => stepIndex === 0
export const steps = [
{
@ -42,7 +42,7 @@ export const steps = [
id: '6',
label: 'Success',
description: 'Your Safe was created successfully',
instruction: 'Click below to get started',
instruction: undefined,
footerComponent: ContinueFooter,
},
]

View File

@ -33,7 +33,7 @@ import {
generateColumns,
} from 'src/routes/safe/components/AddressBook/columns'
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 RemoveOwnerIcon from 'src/routes/safe/components/Settings/assets/icons/bin.svg'
import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors'

View File

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

View File

@ -15,12 +15,12 @@ type MessageHandler = (
) => void | MethodToResponse[Methods] | ErrorResponse | Promise<MethodToResponse[Methods] | ErrorResponse | void>
class AppCommunicator {
private iframe: HTMLIFrameElement
private iframeRef: MutableRefObject<HTMLIFrameElement | null>
private handlers = new Map<Methods, MessageHandler>()
private app: SafeApp
constructor(iframeRef: MutableRefObject<HTMLIFrameElement>, app: SafeApp) {
this.iframe = iframeRef.current
constructor(iframeRef: MutableRefObject<HTMLIFrameElement | null>, app: SafeApp) {
this.iframeRef = iframeRef
this.app = app
window.addEventListener('message', this.handleIncomingMessage)
@ -49,7 +49,7 @@ class AppCommunicator {
? MessageFormatter.makeErrorResponse(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> => {
@ -83,7 +83,6 @@ const useAppCommunicator = (
app?: SafeApp,
): AppCommunicator | undefined => {
const [communicator, setCommunicator] = useState<AppCommunicator | undefined>(undefined)
useEffect(() => {
let communicatorInstance
const initCommunicator = (iframeRef: MutableRefObject<HTMLIFrameElement>, app: SafeApp) => {
@ -91,7 +90,7 @@ const useAppCommunicator = (
setCommunicator(communicatorInstance)
}
if (app && iframeRef.current !== null) {
if (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 { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { ConfirmTransactionModal } from '../components/ConfirmTransactionModal'
import { ConfirmTxModal } from '../components/ConfirmTxModal'
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
import { useLegalConsent } from '../hooks/useLegalConsent'
import LegalDisclaimer from './LegalDisclaimer'
@ -56,6 +56,7 @@ const AppWrapper = styled.div`
const StyledCard = styled(Card)`
flex-grow: 1;
padding: 0;
`
const StyledIframe = styled.iframe`
@ -354,7 +355,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
/>
)}
<ConfirmTransactionModal
<ConfirmTxModal
isOpen={confirmTransactionModal.isOpen}
app={safeApp as SafeApp}
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