diff --git a/.github/workflows/deploy-ewc.yml b/.github/workflows/deploy-ewc.yml index 39aaf20d..ea89aa90 100644 --- a/.github/workflows/deploy-ewc.yml +++ b/.github/workflows/deploy-ewc.yml @@ -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 diff --git a/.github/workflows/deploy-mainnet.yml b/.github/workflows/deploy-mainnet.yml index 5e12509b..b6159262 100644 --- a/.github/workflows/deploy-mainnet.yml +++ b/.github/workflows/deploy-mainnet.yml @@ -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 diff --git a/.github/workflows/deploy-rinkeby.yml b/.github/workflows/deploy-rinkeby.yml index 9bc51a03..4d35a09a 100644 --- a/.github/workflows/deploy-rinkeby.yml +++ b/.github/workflows/deploy-rinkeby.yml @@ -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 diff --git a/.github/workflows/deploy-volta.yml b/.github/workflows/deploy-volta.yml index 23f517c3..f22869e7 100644 --- a/.github/workflows/deploy-volta.yml +++ b/.github/workflows/deploy-volta.yml @@ -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 diff --git a/.github/workflows/deploy-xdai.yml b/.github/workflows/deploy-xdai.yml index bedc557c..74862320 100644 --- a/.github/workflows/deploy-xdai.yml +++ b/.github/workflows/deploy-xdai.yml @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 147dbfda..e251648e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/docs/networks.md b/docs/networks.md index c048955a..4ec60e0c 100644 --- a/docs/networks.md +++ b/docs/networks.md @@ -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, diff --git a/package.json b/package.json index 825b8173..dd954f4a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/index.html b/public/index.html index a2f08f03..933b0c24 100644 --- a/public/index.html +++ b/public/index.html @@ -7,7 +7,31 @@ Gnosis Safe Multisig + -
+
diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index fa9b73f5..ae64a97d 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -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 diff --git a/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg b/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg index 26f71351..62ed0fd9 100644 --- a/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg +++ b/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg @@ -1,6 +1,5 @@ - - - - - + + horizontal_left_small_black + + diff --git a/src/components/AppLayout/Header/components/Layout.tsx b/src/components/AppLayout/Header/components/Layout.tsx index 5845013f..85051334 100644 --- a/src/components/AppLayout/Header/components/Layout.tsx +++ b/src/components/AppLayout/Header/components/Layout.tsx @@ -38,7 +38,7 @@ const styles = () => ({ zIndex: 1301, }, logo: { - flexBasis: '114px', + flexBasis: '140px', flexShrink: '0', flexGrow: '0', maxWidth: '55px', diff --git a/src/components/DecodeTxs/index.tsx b/src/components/DecodeTxs/index.tsx new file mode 100644 index 00000000..8b242e99 --- /dev/null +++ b/src/components/DecodeTxs/index.tsx @@ -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 ( + + {/* TO */} + <> + + {`Send ${txValue} ${nativeCoin.symbol} to:`} + + + + <> + {/* Data */} + + Data (hex encoded): + + + {web3.utils.hexToBytes(txData).length} bytes + + + + + ) +} + +export const getParameterElement = (parameter: DecodedDataBasicParameter, index: number): ReactElement => { + let valueElement + + if (parameter.type === 'address') { + valueElement = ( + + ) + } + + if (parameter.type.startsWith('bytes')) { + valueElement = ( + + {web3.utils.hexToBytes(parameter.value).length} bytes + + + ) + } + + if (!valueElement) { + let value = parameter.value + if (parameter.type.endsWith('[]')) { + try { + value = JSON.stringify(parameter.value) + } catch (e) {} + } + valueElement = {value} + } + + return ( + + + {parameter.name} ({parameter.type}) + + {valueElement} + + ) +} + +const SingleTx = ({ + decodedData, + onTxItemClick, +}: { + decodedData: DecodedData | null + onTxItemClick: (decodedTxDetails: DecodedData) => void +}): ReactElement | null => { + if (!decodedData) { + return null + } + + return ( + + onTxItemClick(decodedData)}> + + + + {decodedData.method} + + + + + ) +} + +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 ( + + {txs.map((tx, index) => ( + onTxItemClick(tx)}> + + + + {tx.dataDecoded && {tx.dataDecoded.method}} + + + + ))} + + ) +} + +type Props = { + txs: Transaction[] + decodedData: DecodedData | null + onTxItemClick: (decodedTxDetails: DecodedTxDetail) => void +} + +export const DecodeTxs = ({ txs, decodedData, onTxItemClick }: Props): ReactElement => { + return txs.length > 1 ? ( + + ) : ( + + ) +} diff --git a/src/components/ModalTitle/index.tsx b/src/components/ModalTitle/index.tsx index f2af9051..4b2a7f64 100644 --- a/src/components/ModalTitle/index.tsx +++ b/src/components/ModalTitle/index.tsx @@ -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 ( + {goBack && ( + + + + + + )} {iconUrl && } {title} diff --git a/src/components/Stepper/Controls/index.tsx b/src/components/Stepper/Controls/index.tsx index 711d200b..02aa6d07 100644 --- a/src/components/Stepper/Controls/index.tsx +++ b/src/components/Stepper/Controls/index.tsx @@ -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 ( - - + + diff --git a/src/components/Stepper/OpenPaper/index.tsx b/src/components/Stepper/OpenPaper/index.tsx index 4e19dfbc..d013ed93 100644 --- a/src/components/Stepper/OpenPaper/index.tsx +++ b/src/components/Stepper/OpenPaper/index.tsx @@ -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)', }, diff --git a/src/components/forms/validator.test.ts b/src/components/forms/validator.test.ts index 38264deb..cd1918ba 100644 --- a/src/components/forms/validator.test.ts +++ b/src/components/forms/validator.test.ts @@ -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}` diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index 09db0431..09496afa 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -80,9 +80,7 @@ export const mustBeEthereumContractAddress = memoize( async (address: string): Promise => { 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 = []) => (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), diff --git a/src/config/networks/energy_web_chain.ts b/src/config/networks/energy_web_chain.ts index 1f908757..7ba62198 100644 --- a/src/config/networks/energy_web_chain.ts +++ b/src/config/networks/energy_web_chain.ts @@ -38,7 +38,7 @@ const mainnet: NetworkConfig = { label: 'EWC', isTestNet: false, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'Energy web token', symbol: 'EWT', decimals: 18, diff --git a/src/config/networks/local.ts b/src/config/networks/local.ts index a18d87bf..a2e9c8f8 100644 --- a/src/config/networks/local.ts +++ b/src/config/networks/local.ts @@ -29,7 +29,7 @@ const local: NetworkConfig = { label: 'LocalRPC', isTestNet: true, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'Ether', symbol: 'ETH', decimals: 18, diff --git a/src/config/networks/mainnet.ts b/src/config/networks/mainnet.ts index cd330a8c..b4333c05 100644 --- a/src/config/networks/mainnet.ts +++ b/src/config/networks/mainnet.ts @@ -38,7 +38,7 @@ const mainnet: NetworkConfig = { label: 'Mainnet', isTestNet: false, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'Ether', symbol: 'ETH', decimals: 18, diff --git a/src/config/networks/rinkeby.ts b/src/config/networks/rinkeby.ts index 0e035ecb..3ce8179f 100644 --- a/src/config/networks/rinkeby.ts +++ b/src/config/networks/rinkeby.ts @@ -38,7 +38,7 @@ const rinkeby: NetworkConfig = { label: 'Rinkeby', isTestNet: true, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'Ether', symbol: 'ETH', decimals: 18, diff --git a/src/config/networks/volta.ts b/src/config/networks/volta.ts index e41c18cf..154ba9e8 100644 --- a/src/config/networks/volta.ts +++ b/src/config/networks/volta.ts @@ -35,7 +35,7 @@ const mainnet: NetworkConfig = { label: 'Volta', isTestNet: true, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'Energy web token', symbol: 'EWT', decimals: 18, diff --git a/src/config/networks/xdai.ts b/src/config/networks/xdai.ts index e0763b78..3241806f 100644 --- a/src/config/networks/xdai.ts +++ b/src/config/networks/xdai.ts @@ -29,7 +29,7 @@ const xDai: NetworkConfig = { label: 'xDai', isTestNet: false, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'xDai', symbol: 'xDai', decimals: 18, diff --git a/src/index.tsx b/src/index.tsx index 64a80101..7a3aeb82 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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()) diff --git a/src/logic/collectibles/store/selectors/index.ts b/src/logic/collectibles/store/selectors/index.ts index 1c5e6412..85efdb98 100644 --- a/src/logic/collectibles/store/selectors/index.ts +++ b/src/logic/collectibles/store/selectors/index.ts @@ -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)) }, ) diff --git a/src/logic/currencyValues/api/fetchAvailableCurrencies.ts b/src/logic/currencyValues/api/fetchAvailableCurrencies.ts new file mode 100644 index 00000000..4be6a079 --- /dev/null +++ b/src/logic/currencyValues/api/fetchAvailableCurrencies.ts @@ -0,0 +1,8 @@ +import { getClientGatewayUrl } from 'src/config' +import axios from 'axios' + +export const fetchAvailableCurrencies = async (): Promise => { + const url = `${getClientGatewayUrl()}/balances/supported-fiat-codes` + + return axios.get(url).then(({ data }) => data) +} diff --git a/src/logic/currencyValues/api/fetchCurrenciesRates.ts b/src/logic/currencyValues/api/fetchCurrenciesRates.ts deleted file mode 100644 index 20d1d6b2..00000000 --- a/src/logic/currencyValues/api/fetchCurrenciesRates.ts +++ /dev/null @@ -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 => { - 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 diff --git a/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts b/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts deleted file mode 100644 index fd21f689..00000000 --- a/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts +++ /dev/null @@ -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 => { - const url = `${getSafeClientGatewayBaseUrl( - checksumAddress(safeAddress), - )}/balances/usd/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}` - - return axios.get(url).then(({ data }) => data) -} diff --git a/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts b/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts deleted file mode 100644 index 580b90e4..00000000 --- a/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts +++ /dev/null @@ -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>, -): Promise => { - 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 diff --git a/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts b/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts index 2297b676..b4f279a0 100644 --- a/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts +++ b/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts @@ -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>, +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>, ): Promise => { 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) } diff --git a/src/logic/currencyValues/store/actions/setAvailableCurrencies.ts b/src/logic/currencyValues/store/actions/setAvailableCurrencies.ts new file mode 100644 index 00000000..e510b494 --- /dev/null +++ b/src/logic/currencyValues/store/actions/setAvailableCurrencies.ts @@ -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(SET_AVAILABLE_CURRENCIES) diff --git a/src/logic/currencyValues/store/actions/setCurrencyBalances.ts b/src/logic/currencyValues/store/actions/setCurrencyBalances.ts deleted file mode 100644 index b7390166..00000000 --- a/src/logic/currencyValues/store/actions/setCurrencyBalances.ts +++ /dev/null @@ -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, - }), -) diff --git a/src/logic/currencyValues/store/actions/setCurrencyRate.ts b/src/logic/currencyValues/store/actions/setCurrencyRate.ts deleted file mode 100644 index 786a2db7..00000000 --- a/src/logic/currencyValues/store/actions/setCurrencyRate.ts +++ /dev/null @@ -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, -})) diff --git a/src/logic/currencyValues/store/actions/setSelectedCurrency.ts b/src/logic/currencyValues/store/actions/setSelectedCurrency.ts index 8a87126d..b1b7c577 100644 --- a/src/logic/currencyValues/store/actions/setSelectedCurrency.ts +++ b/src/logic/currencyValues/store/actions/setSelectedCurrency.ts @@ -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>, -): void => { - dispatch(setCurrentCurrency(safeAddress, selectedCurrency)) - dispatch(fetchCurrencyRate(safeAddress, selectedCurrency)) -} +export const setSelectedCurrency = createAction(SET_CURRENT_CURRENCY) diff --git a/src/logic/currencyValues/store/actions/updateAvailableCurrencies.ts b/src/logic/currencyValues/store/actions/updateAvailableCurrencies.ts new file mode 100644 index 00000000..77e24961 --- /dev/null +++ b/src/logic/currencyValues/store/actions/updateAvailableCurrencies.ts @@ -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>, +): Promise => { + try { + const availableCurrencies = await fetchAvailableCurrencies() + dispatch(setAvailableCurrencies({ availableCurrencies })) + } catch (err) { + console.error('Error fetching available currencies', err) + } + return Promise.resolve() +} diff --git a/src/logic/currencyValues/store/middleware/index.ts b/src/logic/currencyValues/store/middleware/currencyValuesStorageMiddleware.ts similarity index 61% rename from src/logic/currencyValues/store/middleware/index.ts rename to src/logic/currencyValues/store/middleware/currencyValuesStorageMiddleware.ts index a361f593..e03420b7 100644 --- a/src/logic/currencyValues/store/middleware/index.ts +++ b/src/logic/currencyValues/store/middleware/currencyValuesStorageMiddleware.ts @@ -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 diff --git a/src/logic/currencyValues/store/model/currencyValues.ts b/src/logic/currencyValues/store/model/currencyValues.ts deleted file mode 100644 index 26618349..00000000 --- a/src/logic/currencyValues/store/model/currencyValues.ts +++ /dev/null @@ -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({ - currencyName: '', - tokenAddress: '', - balanceInBaseCurrency: '', - balanceInSelectedCurrency: '', -}) - -export type CurrencyRateValueRecord = RecordOf - -export type BalanceCurrencyList = List - -export interface CurrencyRateValue { - currencyRate?: number - selectedCurrency?: string - currencyBalances?: BalanceCurrencyList -} diff --git a/src/logic/currencyValues/store/reducer/currencyValues.ts b/src/logic/currencyValues/store/reducer/currencyValues.ts index ee626001..4855aedf 100644 --- a/src/logic/currencyValues/store/reducer/currencyValues.ts +++ b/src/logic/currencyValues/store/reducer/currencyValues.ts @@ -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 { - get(key: K, notSetValue?: unknown): CurrencyRateValue[K] - setIn(keys: [string, K], value: CurrencyRateValue[K]): this +export type CurrencyValuesState = { + selectedCurrency: string + availableCurrencies: string[] } -export type CurrencyValuesState = Map +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( +export default handleActions( { - [SET_CURRENCY_RATE]: (state, action: Action) => { - const { currencyRate, safeAddress } = action.payload - - return state.setIn([safeAddress, 'currencyRate'], currencyRate) + [SET_CURRENT_CURRENCY]: (state, action: Action) => { + const { selectedCurrency } = action.payload + state.selectedCurrency = selectedCurrency + return state }, - [SET_CURRENCY_BALANCES]: (state, action: Action) => { - const { safeAddress, currencyBalances } = action.payload - - return state.setIn([safeAddress, 'currencyBalances'], currencyBalances) - }, - [SET_CURRENT_CURRENCY]: (state, action: Action) => { - const { safeAddress, selectedCurrency } = action.payload - - return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency) + [SET_AVAILABLE_CURRENCIES]: (state, action: Action) => { + const { availableCurrencies } = action.payload + state.availableCurrencies = availableCurrencies + return state }, }, - Map(), + initialState, ) diff --git a/src/logic/currencyValues/store/selectors/index.ts b/src/logic/currencyValues/store/selectors/index.ts index b1789174..e70adf05 100644 --- a/src/logic/currencyValues/store/selectors/index.ts +++ b/src/logic/currencyValues/store/selectors/index.ts @@ -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 = (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 +} diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx index 8cd5e268..567d7659 100644 --- a/src/logic/hooks/useEstimateTransactionGas.tsx +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -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, }) } } diff --git a/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts b/src/logic/safe/__tests__/fetchSafeTokens.test.ts similarity index 84% rename from src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts rename to src/logic/safe/__tests__/fetchSafeTokens.test.ts index adc85654..4b9a4901 100644 --- a/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts +++ b/src/logic/safe/__tests__/fetchSafeTokens.test.ts @@ -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}`) }) }) diff --git a/src/logic/safe/api/fetchTokenCurrenciesBalances.ts b/src/logic/safe/api/fetchTokenCurrenciesBalances.ts new file mode 100644 index 00000000..2fcc4a93 --- /dev/null +++ b/src/logic/safe/api/fetchTokenCurrenciesBalances.ts @@ -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 => { + 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 + }) +} diff --git a/src/logic/safe/hooks/useFetchTokens.tsx b/src/logic/safe/hooks/useFetchTokens.tsx index 7d19e373..829fefaf 100644 --- a/src/logic/safe/hooks/useFetchTokens.tsx +++ b/src/logic/safe/hooks/useFetchTokens.tsx @@ -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() 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]) } diff --git a/src/logic/safe/hooks/useLoadSafe.tsx b/src/logic/safe/hooks/useLoadSafe.tsx index 116bf24c..6adea9d6 100644 --- a/src/logic/safe/hooks/useLoadSafe.tsx +++ b/src/logic/safe/hooks/useLoadSafe.tsx @@ -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() @@ -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)) } } diff --git a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx index a18f7349..99d8bffe 100644 --- a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx +++ b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx @@ -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 => { - 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) } } diff --git a/src/logic/safe/hooks/useTokenInfo.tsx b/src/logic/safe/hooks/useTokenInfo.tsx index c05f161c..0919611b 100644 --- a/src/logic/safe/hooks/useTokenInfo.tsx +++ b/src/logic/safe/hooks/useTokenInfo.tsx @@ -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)) } } diff --git a/src/logic/safe/store/actions/activateTokenForAllSafes.ts b/src/logic/safe/store/actions/activateTokenForAllSafes.ts deleted file mode 100644 index 64b9b13c..00000000 --- a/src/logic/safe/store/actions/activateTokenForAllSafes.ts +++ /dev/null @@ -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 diff --git a/src/logic/safe/store/actions/addSafeOwner.ts b/src/logic/safe/store/actions/addSafeOwner.ts index 69e01898..2a341b4e 100644 --- a/src/logic/safe/store/actions/addSafeOwner.ts +++ b/src/logic/safe/store/actions/addSafeOwner.ts @@ -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) diff --git a/src/logic/safe/store/actions/createTransaction.ts b/src/logic/safe/store/actions/createTransaction.ts index 4ce9c211..b2e6b347 100644 --- a/src/logic/safe/store/actions/createTransaction.ts +++ b/src/logic/safe/store/actions/createTransaction.ts @@ -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 diff --git a/src/logic/safe/store/actions/fetchEtherBalance.ts b/src/logic/safe/store/actions/fetchEtherBalance.ts index 146a2033..8025f639 100644 --- a/src/logic/safe/store/actions/fetchEtherBalance.ts +++ b/src/logic/safe/store/actions/fetchEtherBalance.ts @@ -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 => { @@ -21,5 +21,3 @@ const fetchEtherBalance = (safeAddress: string) => async ( console.error('Error when fetching Ether balance:', err) } } - -export default fetchEtherBalance diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index 014e6e26..d8a014e6 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -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 => { 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 diff --git a/src/logic/safe/store/actions/processTransaction.ts b/src/logic/safe/store/actions/processTransaction.ts index f54d2c12..28934b23 100644 --- a/src/logic/safe/store/actions/processTransaction.ts +++ b/src/logic/safe/store/actions/processTransaction.ts @@ -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) { diff --git a/src/logic/safe/store/actions/removeSafeOwner.ts b/src/logic/safe/store/actions/removeSafeOwner.ts index fd18a3bd..5f06a6e6 100644 --- a/src/logic/safe/store/actions/removeSafeOwner.ts +++ b/src/logic/safe/store/actions/removeSafeOwner.ts @@ -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) diff --git a/src/logic/safe/store/actions/replaceSafeOwner.ts b/src/logic/safe/store/actions/replaceSafeOwner.ts index 6b4498a3..8850023c 100644 --- a/src/logic/safe/store/actions/replaceSafeOwner.ts +++ b/src/logic/safe/store/actions/replaceSafeOwner.ts @@ -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) diff --git a/src/logic/safe/store/actions/updateActiveAssets.ts b/src/logic/safe/store/actions/updateActiveAssets.ts deleted file mode 100644 index 80be2139..00000000 --- a/src/logic/safe/store/actions/updateActiveAssets.ts +++ /dev/null @@ -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) => (dispatch: Dispatch): void => { - dispatch(updateAssetsList({ safeAddress, activeAssets })) -} - -export default updateActiveAssets diff --git a/src/logic/safe/store/actions/updateActiveTokens.ts b/src/logic/safe/store/actions/updateActiveTokens.ts deleted file mode 100644 index 012836c9..00000000 --- a/src/logic/safe/store/actions/updateActiveTokens.ts +++ /dev/null @@ -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) => (dispatch: Dispatch): void => { - dispatch(updateTokensList({ safeAddress, activeTokens })) -} - -export default updateActiveTokens diff --git a/src/logic/safe/store/actions/updateAssetsList.ts b/src/logic/safe/store/actions/updateAssetsList.ts deleted file mode 100644 index 0f098e28..00000000 --- a/src/logic/safe/store/actions/updateAssetsList.ts +++ /dev/null @@ -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 diff --git a/src/logic/safe/store/actions/updateBlacklistedAssets.ts b/src/logic/safe/store/actions/updateBlacklistedAssets.ts deleted file mode 100644 index 8b52bbfd..00000000 --- a/src/logic/safe/store/actions/updateBlacklistedAssets.ts +++ /dev/null @@ -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) => (dispatch: Dispatch): void => { - dispatch(updateAssetsList({ safeAddress, blacklistedAssets })) -} - -export default updateBlacklistedAssets diff --git a/src/logic/safe/store/actions/updateBlacklistedTokens.ts b/src/logic/safe/store/actions/updateBlacklistedTokens.ts deleted file mode 100644 index e81293b9..00000000 --- a/src/logic/safe/store/actions/updateBlacklistedTokens.ts +++ /dev/null @@ -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) => (dispatch: Dispatch): void => { - dispatch(updateTokensList({ safeAddress, blacklistedTokens })) -} - -export default updateBlacklistedTokens diff --git a/src/logic/safe/store/actions/updateTokensList.ts b/src/logic/safe/store/actions/updateTokensList.ts deleted file mode 100644 index 91123bd6..00000000 --- a/src/logic/safe/store/actions/updateTokensList.ts +++ /dev/null @@ -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 diff --git a/src/logic/safe/store/middleware/notificationsMiddleware.ts b/src/logic/safe/store/middleware/notificationsMiddleware.ts index f516a141..1ad8cb52 100644 --- a/src/logic/safe/store/middleware/notificationsMiddleware.ts +++ b/src/logic/safe/store/middleware/notificationsMiddleware.ts @@ -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).payload + const transactions = values + .filter((tx) => isTransactionSummary(tx)) + .map((item: TransactionGatewayResult) => item.transaction) const userAddress: string = userAccountSelector(state) const awaitingTransactions = getAwaitingGatewayTransactions(transactions, userAddress) diff --git a/src/logic/safe/store/middleware/safeStorage.ts b/src/logic/safe/store/middleware/safeStorage.ts index 90b26346..d51be225 100644 --- a/src/logic/safe/store/middleware/safeStorage.ts +++ b/src/logic/safe/store/middleware/safeStorage.ts @@ -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 diff --git a/src/logic/safe/store/models/safe.ts b/src/logic/safe/store/models/safe.ts index 7d0547e0..7fb02abf 100644 --- a/src/logic/safe/store/models/safe.ts +++ b/src/logic/safe/store/models/safe.ts @@ -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 modules?: ModulePair[] | null spendingLimits?: SpendingLimit[] | null activeTokens: Set - activeAssets: Set - blacklistedTokens: Set - blacklistedAssets: Set - balances: Map + balances: Map nonce: number latestIncomingTxBlock: number recurringUser?: boolean @@ -49,13 +48,11 @@ const makeSafe = Record({ 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, diff --git a/src/logic/safe/store/reducer/gatewayTransactions.ts b/src/logic/safe/store/reducer/gatewayTransactions.ts index a3eada88..fbd7e314 100644 --- a/src/logic/safe/store/reducer/gatewayTransactions.ts +++ b/src/logic/safe/store/reducer/gatewayTransactions.ts @@ -344,6 +344,7 @@ export const gatewayTransactions = handleActions { + // 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 diff --git a/src/logic/safe/store/reducer/safe.ts b/src/logic/safe/store/reducer/safe.ts index d327e560..c75baef4 100644 --- a/src/logic/safe/store/reducer/safe.ts +++ b/src/logic/safe/store/reducer/safe.ts @@ -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( ) : state }, - [ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state, action: Action) => { - 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) => { const { safe } = action.payload const safeAddress = safe.address @@ -195,24 +171,6 @@ export default handleActions( return prevSafe.merge({ owners: updatedOwners }) }) }, - [UPDATE_TOKENS_LIST]: (state, action: Action) => { - // 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) => { - // 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) => state.set('defaultSafe', action.payload), [SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action) => state.set('latestMasterContractVersion', action.payload), diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index 7675d0a9..48728cf5 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -76,51 +76,6 @@ export const safeActiveTokensSelector = createSelector( }, ) -export const safeActiveAssetsSelector = createSelector( - safeSelector, - (safe): Set => { - 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 => { - if (!safe) { - return Set() - } - - return safe.blacklistedTokens - }, -) - -export const safeBlacklistedAssetsSelector = createSelector( - safeSelector, - (safe): Set => { - if (!safe) { - return Set() - } - - return safe.blacklistedAssets - }, -) - -export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set => - safes.get(safeAddress)?.get('activeAssets') || Set() - -export const safeBlacklistedAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set => - safes.get(safeAddress)?.get('blacklistedAssets') || Set() - const baseSafe = makeSafe() export const safeFieldSelector = (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 }) diff --git a/src/logic/safe/store/tests/safe.balances.test.ts b/src/logic/safe/store/tests/safe.balances.test.ts deleted file mode 100644 index 4238d4c3..00000000 --- a/src/logic/safe/store/tests/safe.balances.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts b/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts index 5d029744..0ed862bb 100644 --- a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts +++ b/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts @@ -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 = { - 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 = { - 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 = { - 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 = { diff --git a/src/logic/safe/utils/aboutToExecuteTx.ts b/src/logic/safe/utils/aboutToExecuteTx.ts new file mode 100644 index 00000000..7db235e3 --- /dev/null +++ b/src/logic/safe/utils/aboutToExecuteTx.ts @@ -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 + } + } +} diff --git a/src/logic/currencyValues/store/utils/currencyValuesStorage.ts b/src/logic/safe/utils/currencyValuesStorage.ts similarity index 100% rename from src/logic/currencyValues/store/utils/currencyValuesStorage.ts rename to src/logic/safe/utils/currencyValuesStorage.ts diff --git a/src/logic/safe/utils/spendingLimits.ts b/src/logic/safe/utils/spendingLimits.ts index 1c20e890..cbf438a3 100644 --- a/src/logic/safe/utils/spendingLimits.ts +++ b/src/logic/safe/utils/spendingLimits.ts @@ -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)) } diff --git a/src/logic/tokens/store/actions/activateAssetsByBalance.ts b/src/logic/tokens/store/actions/activateAssetsByBalance.ts deleted file mode 100644 index eae19074..00000000 --- a/src/logic/tokens/store/actions/activateAssetsByBalance.ts +++ /dev/null @@ -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 diff --git a/src/logic/tokens/store/actions/saveTokens.ts b/src/logic/tokens/store/actions/addTokens.ts similarity index 54% rename from src/logic/tokens/store/actions/saveTokens.ts rename to src/logic/tokens/store/actions/addTokens.ts index 0aa58838..f5367cce 100644 --- a/src/logic/tokens/store/actions/saveTokens.ts +++ b/src/logic/tokens/store/actions/addTokens.ts @@ -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 diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index fb585e94..0aebb5a6 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -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 - currencyList: List + balances: Map ethBalance: string tokens: List } -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 => { @@ -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( - extractDataFromResult(currentTokens, currencySelected || AVAILABLE_CURRENCIES.USD), + const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce( + 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 diff --git a/src/logic/tokens/store/actions/fetchTokens.ts b/src/logic/tokens/store/actions/fetchTokens.ts index 20e170af..d00dde76 100644 --- a/src/logic/tokens/store/actions/fetchTokens.ts +++ b/src/logic/tokens/store/actions/fetchTokens.ts @@ -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 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 diff --git a/src/logic/tokens/store/actions/loadActiveTokens.ts b/src/logic/tokens/store/actions/loadActiveTokens.ts deleted file mode 100644 index 7b4ae61b..00000000 --- a/src/logic/tokens/store/actions/loadActiveTokens.ts +++ /dev/null @@ -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 diff --git a/src/logic/tokens/store/model/token.ts b/src/logic/tokens/store/model/token.ts index e0c612bb..3e9b5bcd 100644 --- a/src/logic/tokens/store/model/token.ts +++ b/src/logic/tokens/store/model/token.ts @@ -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({ symbol: '', decimals: 0, logoUri: '', - balance: 0, + balance: { + fiatBalance: '0', + tokenBalance: '0', + }, }) // balance is only set in extendedSafeTokensSelector when we display user's token balances diff --git a/src/logic/tokens/store/reducer/tokens.ts b/src/logic/tokens/store/reducer/tokens.ts index c567e630..bd506670 100644 --- a/src/logic/tokens/store/reducer/tokens.ts +++ b/src/logic/tokens/store/reducer/tokens.ts @@ -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' diff --git a/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts b/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts index c211e8b4..142c2d6a 100644 --- a/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts +++ b/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts @@ -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, diff --git a/src/logic/tokens/utils/tokenHelpers.ts b/src/logic/tokens/utils/tokenHelpers.ts index 9dc7d305..43135dbd 100644 --- a/src/logic/tokens/utils/tokenHelpers.ts +++ b/src/logic/tokens/utils/tokenHelpers.ts @@ -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 } -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, } } diff --git a/src/logic/tokens/utils/tokensStorage.ts b/src/logic/tokens/utils/tokensStorage.ts deleted file mode 100644 index 27364f24..00000000 --- a/src/logic/tokens/utils/tokensStorage.ts +++ /dev/null @@ -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): Promise => { - try { - await saveToStorage(ACTIVE_TOKENS_KEY, tokens.toJS() as Record) - } catch (err) { - console.error('Error storing tokens in localstorage', err) - } -} - -export const getActiveTokens = async (): Promise | undefined> => { - const data = await loadFromStorage>(ACTIVE_TOKENS_KEY) - - return data -} diff --git a/src/routes/open/components/ReviewInformation/index.tsx b/src/routes/open/components/ReviewInformation/index.tsx index 8e899bee..0a400112 100644 --- a/src/routes/open/components/ReviewInformation/index.tsx +++ b/src/routes/open/components/ReviewInformation/index.tsx @@ -128,7 +128,7 @@ const ReviewComponent = ({ values, form }: ReviewComponentProps): ReactElement = - + You're about to create a new Safe and will have to confirm a transaction with your currently connected wallet. The creation will cost approximately {gasCostFormatted} {nativeCoin.name}. The exact amount will be determined by your wallet. diff --git a/src/routes/open/components/SafeNameForm/index.tsx b/src/routes/open/components/SafeNameForm/index.tsx index 82ebd944..5d7e1837 100644 --- a/src/routes/open/components/SafeNameForm/index.tsx +++ b/src/routes/open/components/SafeNameForm/index.tsx @@ -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 ( <> - + You are about to create a new Gnosis Safe wallet with one or more owners. First, let's give your new wallet a name. This name is only stored locally and will never be shared with Gnosis or any third parties. - /> - + By continuing you consent to the{' '} terms of use diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx b/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx index a23bfb4b..d5a868c1 100644 --- a/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx @@ -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 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 ( <> - + 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.
@@ -167,7 +173,7 @@ const SafeOwnersForm = (props): React.ReactElement => { rel="noreferrer" title="Learn about which Safe setup to use" > - + Learn about which Safe setup to use @@ -176,8 +182,8 @@ const SafeOwnersForm = (props): React.ReactElement => { - NAME - ADDRESS + NAME + ADDRESS @@ -187,7 +193,7 @@ const SafeOwnersForm = (props): React.ReactElement => { return ( - + { testId={`create-safe-owner-name-field-${index}`} /> - - + { const newOwnerName = getNameFromAddressBook(addressBook, newOwnerAddress, { filterOnlyValidName: true, @@ -246,7 +252,7 @@ const SafeOwnersForm = (props): React.ReactElement => { @@ -256,7 +262,7 @@ const SafeOwnersForm = (props): React.ReactElement => { Any transaction requires the confirmation of: - + { // 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', }) }) }) diff --git a/src/routes/open/components/fields.ts b/src/routes/open/components/fields.ts index 87d0f0b7..539076e2 100644 --- a/src/routes/open/components/fields.ts +++ b/src/routes/open/components/fields.ts @@ -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) => { diff --git a/src/routes/open/utils/padOwnerIndex.ts b/src/routes/open/utils/padOwnerIndex.ts new file mode 100644 index 00000000..b010fc6f --- /dev/null +++ b/src/routes/open/utils/padOwnerIndex.ts @@ -0,0 +1,3 @@ +export const padOwnerIndex = (index: number | string): string => { + return index.toString().padStart(4, '0') +} diff --git a/src/routes/opening/assets/creation-process.gif b/src/routes/opening/assets/creation-process.gif new file mode 100644 index 00000000..573740b0 Binary files /dev/null and b/src/routes/opening/assets/creation-process.gif differ diff --git a/src/routes/opening/assets/loader-dots.svg b/src/routes/opening/assets/loader-dots.svg deleted file mode 100644 index bd96324c..00000000 --- a/src/routes/opening/assets/loader-dots.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/routes/opening/assets/safe-created.svg b/src/routes/opening/assets/safe-created.svg new file mode 100644 index 00000000..2cfaaaa7 --- /dev/null +++ b/src/routes/opening/assets/safe-created.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/routes/opening/assets/success.svg b/src/routes/opening/assets/success.svg deleted file mode 100644 index d635672d..00000000 --- a/src/routes/opening/assets/success.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/routes/opening/components/Footer.tsx b/src/routes/opening/components/Footer.tsx index 8f213484..c4b30ac5 100644 --- a/src/routes/opening/components/Footer.tsx +++ b/src/routes/opening/components/Footer.tsx @@ -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 ( -

This process should take a couple of minutes.

-

+ This process should take a couple of minutes. + Follow the progress on{' '} - - {explorerDomain} - - . -

+ + {explorerDomain} + + + +
) } @@ -45,16 +65,19 @@ export const ContinueFooter = ({ }: { continueButtonDisabled: boolean onContinue: (event: SyntheticEvent) => void -}) => ( - +}): ReactElement => ( + + + + ) export const ErrorFooter = ({ @@ -63,13 +86,14 @@ export const ErrorFooter = ({ }: { onCancel: (event: SyntheticEvent) => void onRetry: (event: SyntheticEvent) => void -}) => ( - <> +}): ReactElement => ( + + Cancel - + ) diff --git a/src/routes/opening/index.tsx b/src/routes/opening/index.tsx index 22c961fc..7f3f79a3 100644 --- a/src/routes/opening/index.tsx +++ b/src/routes/opening/index.tsx @@ -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)` - 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 = ({ - Vault + Vault {steps[stepIndex].description || steps[stepIndex].label} - {!error && stepIndex <= 4 && Loader dots} - - - - {error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction} - - + {steps[stepIndex].instruction && ( + + + {error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction} + + + )} {FooterComponent ? ( @@ -354,9 +356,12 @@ export const SafeDeployment = ({ ) : null} - - Back - + + {stepIndex !== 0 && ( + + Back + + )} ) } diff --git a/src/routes/opening/steps.ts b/src/routes/opening/steps.ts index 8863d7a4..a2f6e2d7 100644 --- a/src/routes/opening/steps.ts +++ b/src/routes/opening/steps.ts @@ -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, }, ] diff --git a/src/routes/safe/components/AddressBook/index.tsx b/src/routes/safe/components/AddressBook/index.tsx index e6cbaafa..7460f663 100644 --- a/src/routes/safe/components/AddressBook/index.tsx +++ b/src/routes/safe/components/AddressBook/index.tsx @@ -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' diff --git a/src/routes/safe/components/AddressBook/style.ts b/src/routes/safe/components/AddressBook/style.ts index f0e6c980..a8314a31 100644 --- a/src/routes/safe/components/AddressBook/style.ts +++ b/src/routes/safe/components/AddressBook/style.ts @@ -14,7 +14,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Apps/communicator.ts b/src/routes/safe/components/Apps/communicator.ts index 8422d3d7..00155bec 100644 --- a/src/routes/safe/components/Apps/communicator.ts +++ b/src/routes/safe/components/Apps/communicator.ts @@ -15,12 +15,12 @@ type MessageHandler = ( ) => void | MethodToResponse[Methods] | ErrorResponse | Promise class AppCommunicator { - private iframe: HTMLIFrameElement + private iframeRef: MutableRefObject private handlers = new Map() private app: SafeApp - constructor(iframeRef: MutableRefObject, app: SafeApp) { - this.iframe = iframeRef.current + constructor(iframeRef: MutableRefObject, 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 => { @@ -83,7 +83,6 @@ const useAppCommunicator = ( app?: SafeApp, ): AppCommunicator | undefined => { const [communicator, setCommunicator] = useState(undefined) - useEffect(() => { let communicatorInstance const initCommunicator = (iframeRef: MutableRefObject, app: SafeApp) => { @@ -91,7 +90,7 @@ const useAppCommunicator = ( setCommunicator(communicatorInstance) } - if (app && iframeRef.current !== null) { + if (app) { initCommunicator(iframeRef as MutableRefObject, app) } diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index aebff2ab..02d41f29 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -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 => { /> )} - { - 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() - - 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 - ? () => ( - <> - - - Transaction error - - - This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of - this Safe App for more information. - - - ) - : (txParameters, toggleEditMode) => { - return ( - <> - - - - {txs.map((tx, index) => ( - - } title={`Transaction ${index + 1}`}> - -
- Value -
- Ether - - {fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name} - -
-
-
- Data (hex encoded)* - {tx.data} -
-
-
-
- ))} - - {params?.safeTxGas && ( -
- SafeTxGas - {params?.safeTxGas} - -
- )} - - {/* Tx Parameters */} - -
- {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( - - - - )} - - ) - } - - return ( - - - {(txParameters, toggleEditMode) => ( - <> - - - - - {body(txParameters, toggleEditMode)} - - - confirmTransactions(txParameters)} - okDisabled={areTxsMalformed} - okText="Submit" - /> - - - )} - - - ) -} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx new file mode 100644 index 00000000..8bf9bb6a --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx @@ -0,0 +1,62 @@ +import React, { ReactElement } from 'react' +import styled from 'styled-components' + +import { getNetworkInfo } from 'src/config' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { md, lg } from 'src/theme/variables' +import ModalTitle from 'src/components/ModalTitle' +import Hairline from 'src/components/layout/Hairline' +import { DecodedDataParameterValue, DecodedData } from 'src/types/transactions/decode.d' +import { BasicTxInfo, getParameterElement } from 'src/components/DecodeTxs' + +const { nativeCoin } = getNetworkInfo() + +const Container = styled.div` + max-width: 480px; + padding: ${md} ${lg}; + word-break: break-word; +` + +function isDataDecodedParameterValue(arg: any): arg is DecodedDataParameterValue { + return arg.operation !== undefined +} + +type Props = { + hideDecodedTxData: () => void + onClose: () => void + decodedTxData: DecodedDataParameterValue | DecodedData +} + +export const DecodedTxDetail = ({ hideDecodedTxData, onClose, decodedTxData: tx }: Props): ReactElement => { + let body + // If we are dealing with a multiSend + // decodedTxData is of type DataDecodedParameter + if (isDataDecodedParameterValue(tx)) { + const txValue = fromTokenUnit(tx.value, nativeCoin.decimals) + + body = ( + <> + + {tx.dataDecoded?.parameters.map((p, index) => getParameterElement(p, index))} + + ) + } else { + // If we are dealing with a single tx + // decodedTxData is of type DecodedData + body = <>{tx.parameters.map((p, index) => getParameterElement(p, index))} + } + + return ( + <> + + + + + {body} + + ) +} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx new file mode 100644 index 00000000..0a775471 --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx @@ -0,0 +1,260 @@ +import React, { useEffect, useMemo, useState } from 'react' +import { ModalFooterConfirmation } from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' +import { useDispatch } from 'react-redux' + +import DividerLine from 'src/components/DividerLine' +import TextBox from 'src/components/TextBox' +import ModalTitle from 'src/components/ModalTitle' +import Hairline from 'src/components/layout/Hairline' +import Heading from 'src/components/layout/Heading' +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 { getNetworkInfo } from 'src/config' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +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' +import AddressInfo from 'src/components/AddressInfo' +import { DecodeTxs, BasicTxInfo } from 'src/components/DecodeTxs' +import { fetchTxDecoder } from 'src/utils/decodeTx' +import { DecodedData } from 'src/types/transactions/decode.d' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' + +import GasEstimationInfo from '../GasEstimationInfo' +import { ConfirmTxModalProps, DecodedTxDetail } from '.' + +const { nativeCoin } = getNetworkInfo() + +const StyledTextBox = styled(TextBox)` + max-width: 444px; +` + +const Container = styled.div` + max-width: 480px; + padding: ${md} ${lg} 0; +` +const TransactionFeesWrapper = styled.div` + background-color: ${({ theme }) => theme.colors.background}; + padding: ${sm} ${lg}; + margin-bottom: 15px; +` + +const FooterWrapper = styled.div` + margin-bottom: 15px; +` + +const DecodeTxsWrapper = styled.div` + margin: 24px -24px; +` + +type Props = ConfirmTxModalProps & { + areTxsMalformed: boolean + showDecodedTxData: (decodedTxDetails: DecodedTxDetail) => void + hidden: boolean // used to prevent re-rendering the modal each time a tx is inspected +} + +export const ReviewConfirm = ({ + app, + txs, + safeAddress, + ethBalance, + safeName, + params, + hidden, + onUserConfirm, + onClose, + onTxReject, + areTxsMalformed, + showDecodedTxData, +}: Props): React.ReactElement => { + const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0) + const isMultiSend = txs.length > 1 + const [decodedData, setDecodedData] = useState(null) + const dispatch = useDispatch() + + const txRecipient: string | undefined = useMemo(() => (isMultiSend ? MULTI_SEND_ADDRESS : txs[0]?.to), [ + txs, + isMultiSend, + ]) + const txData: string | undefined = useMemo(() => (isMultiSend ? encodeMultiSendCall(txs) : txs[0]?.data), [ + txs, + isMultiSend, + ]) + const txValue: string | undefined = useMemo( + () => (isMultiSend ? '0' : txs[0]?.value && fromTokenUnit(txs[0]?.value, nativeCoin.decimals)), + [txs, isMultiSend], + ) + + const operation = useMemo(() => (isMultiSend ? DELEGATE_CALL : CALL), [isMultiSend]) + const [manualSafeTxGas, setManualSafeTxGas] = useState(0) + const [manualGasPrice, setManualGasPrice] = useState() + + 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]) + + // Decode tx data. + useEffect(() => { + const decodeTxData = async () => { + const res = await fetchTxDecoder(txData) + setDecodedData(res) + } + + decodeTxData() + }, [txData]) + + const handleTxRejection = () => { + onTxReject() + onClose() + } + + const handleUserConfirmation = (safeTxHash: string): void => { + onUserConfirm(safeTxHash) + onClose() + } + + 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) + } + } + + return ( + + {(txParameters, toggleEditMode) => ( + + )} + + ) +} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx new file mode 100644 index 00000000..feddb412 --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx @@ -0,0 +1,47 @@ +import React, { ReactElement } from 'react' +import { Icon, Text, Title, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' +import { ConfirmTxModalProps } from '.' + +const IconText = styled.div` + display: flex; + align-items: center; + + span { + margin-right: 4px; + } +` + +const FooterWrapper = styled.div` + margin-top: 15px; +` + +export const SafeAppLoadError = ({ onTxReject, onClose }: ConfirmTxModalProps): ReactElement => { + const handleTxRejection = () => { + onTxReject() + onClose() + } + + return ( + <> + + + Transaction error + + + This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of this + Safe App for more information. + + + + handleTxRejection()} + handleOk={() => {}} + okDisabled={true} + okText="Submit" + /> + + + ) +} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx new file mode 100644 index 00000000..ebadeded --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx @@ -0,0 +1,72 @@ +import React, { ReactElement, useState } from 'react' +import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' + +import Modal from 'src/components/Modal' +import { SafeApp } from 'src/routes/safe/components/Apps/types.d' +import { TransactionParams } from 'src/routes/safe/components/Apps/components/AppFrame' +import { mustBeEthereumAddress } from 'src/components/forms/validator' +import { SafeAppLoadError } from './SafeAppLoadError' +import { ReviewConfirm } from './ReviewConfirm' +import { DecodedDataParameterValue, DecodedData } from 'src/types/transactions/decode' +import { DecodedTxDetail } from './DecodedTxDetail' + +export type ConfirmTxModalProps = { + isOpen: boolean + app: SafeApp + txs: Transaction[] + params?: TransactionParams + safeAddress: string + safeName: string + ethBalance: string + onUserConfirm: (safeTxHash: string) => void + onTxReject: () => void + onClose: () => void +} + +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' +} + +export type DecodedTxDetail = DecodedDataParameterValue | DecodedData | undefined + +export const ConfirmTxModal = (props: ConfirmTxModalProps): ReactElement | null => { + const [decodedTxDetails, setDecodedTxDetails] = useState() + const areTxsMalformed = props.txs.some((t) => !isTxValid(t)) + + const showDecodedTxData = setDecodedTxDetails + const hideDecodedTxData = () => setDecodedTxDetails(undefined) + + const closeDecodedTxDetail = () => { + hideDecodedTxData() + props.onClose() + } + + return ( + + {areTxsMalformed && } + {decodedTxDetails && ( + + )} + + + ) +} diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 3fa9d1f3..fa66d807 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -3,7 +3,6 @@ import memoize from 'lodash.memoize' import { SafeApp, SAFE_APP_FETCH_STATUS } from './types.d' -import { getGnosisSafeAppsUrl } from 'src/config' import { getContentFromENS } from 'src/logic/wallets/getWeb3' import appsIconSvg from 'src/assets/icons/apps.svg' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' @@ -17,7 +16,6 @@ const removeLastTrailingSlash = (url) => { return url } -const gnosisAppsUrl = removeLastTrailingSlash(getGnosisSafeAppsUrl()) export type StaticAppInfo = { url: string disabled: boolean @@ -26,7 +24,7 @@ export type StaticAppInfo = { export const staticAppsList: Array = [ // 1inch { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRWtuktjfU6WMAEJFgzBC4cUfqp3FF5uN9QoWb55SdGG5`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUXF1yVGdqUfMbhNyfM3jpP6Bw66cYnKPoWq6iHkhd3Aw`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET], }, @@ -56,7 +54,11 @@ export const staticAppsList: Array = [ networks: [ETHEREUM_NETWORK.RINKEBY, ETHEREUM_NETWORK.XDAI], }, // Compound - { url: `${gnosisAppsUrl}/compound`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY] }, + { + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmX31xCdhFDmJzoVG33Y6kJtJ5Ujw8r5EJJBrsp8Fbjm7k`, + disabled: false, + networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], + }, // dHedge { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmaiemnumMaaK9wE1pbMfm3YSBUpcFNgDh3Bf6VZCZq57Q`, @@ -65,7 +67,7 @@ export const staticAppsList: Array = [ }, // Idle { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVkGHm6gfQumJhnRfFCh7m2oSYwLXb51EKHzChpcV9J3N`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmTvrLwJtyjG8QFHgvqdPhcV5DBMQ7oZceSU4uvPw9vQaj`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], }, @@ -77,7 +79,7 @@ export const staticAppsList: Array = [ }, // Mushrooms finance { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQs6CUbMUyKe3Sa3tU3HcnWWzsuCk8oJEk8CZKhRcJfEh`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmT96aES2YA9BssByc6DVizQDkofmKRErs8gJyqWipjyS8`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET], }, @@ -131,7 +133,7 @@ export const staticAppsList: Array = [ }, // Wallet-Connect { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRMGTA5ARMwfhYbdmK83zzMd13NnEUKFJSZEgEjKa8YQm`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmX9B982ZAaBzbm6yBoZUS3uLgcizYA6wW65RCXVRZkG6f`, disabled: false, networks: [ ETHEREUM_NETWORK.MAINNET, diff --git a/src/routes/safe/components/Balances/Coins/index.tsx b/src/routes/safe/components/Balances/Coins/index.tsx index 7b72922f..20335252 100644 --- a/src/routes/safe/components/Balances/Coins/index.tsx +++ b/src/routes/safe/components/Balances/Coins/index.tsx @@ -14,11 +14,6 @@ import Table from 'src/components/Table' import { cellWidth } from 'src/components/Table/TableHead' import Button from 'src/components/layout/Button' import Row from 'src/components/layout/Row' -import { - currencyRateSelector, - currentCurrencySelector, - safeFiatBalancesListSelector, -} from 'src/logic/currencyValues/store/selectors' import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances' import AssetTableCell from 'src/routes/safe/components/Balances/AssetTableCell' import { @@ -33,6 +28,7 @@ import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/con import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { makeStyles } from '@material-ui/core/styles' import { styles } from './styles' +import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' const useStyles = makeStyles(styles) @@ -69,9 +65,7 @@ const Coins = (props: Props): React.ReactElement => { const columns = generateColumns() const autoColumns = columns.filter((c) => !c.custom) const selectedCurrency = useSelector(currentCurrencySelector) - const currencyRate = useSelector(currencyRateSelector) const activeTokens = useSelector(extendedSafeTokensSelector) - const currencyValues = useSelector(safeFiatBalancesListSelector) const granted = useSelector(grantedSelector) const { trackEvent } = useAnalytics() @@ -79,10 +73,10 @@ const Coins = (props: Props): React.ReactElement => { trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Coins' }) }, [trackEvent]) - const filteredData: List = useMemo( - () => getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate), - [activeTokens, selectedCurrency, currencyValues, currencyRate], - ) + const filteredData: List = useMemo(() => getBalanceData(activeTokens, selectedCurrency), [ + activeTokens, + selectedCurrency, + ]) return ( diff --git a/src/routes/safe/components/Balances/Coins/styles.ts b/src/routes/safe/components/Balances/Coins/styles.ts index 3c10059d..ae4ae6ac 100644 --- a/src/routes/safe/components/Balances/Coins/styles.ts +++ b/src/routes/safe/components/Balances/Coins/styles.ts @@ -12,7 +12,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Balances/SendModal/index.tsx b/src/routes/safe/components/Balances/SendModal/index.tsx index 68a4e5ea..7623d9fa 100644 --- a/src/routes/safe/components/Balances/SendModal/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/index.tsx @@ -133,6 +133,7 @@ const SendModal = ({ {activeScreen === 'sendFunds' && (
diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx index d7e4e736..0a39b8dc 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx @@ -94,7 +94,13 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { } return ( - + {(txParameters, toggleEditMode) => ( <> @@ -168,6 +174,7 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { onEdit={toggleEditMode} isTransactionCreation={isCreation} isTransactionExecution={isExecution} + isOffChainSignature={isOffChainSignature} /> {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx index f46d897c..ea68ea2a 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx @@ -140,6 +140,8 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement = return (
diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx index 21e0075c..1e2ab83c 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx @@ -178,6 +178,8 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE return ( - - {/* Disclaimer */} + + {/* Disclaimer */} {txEstimationExecutionStatus !== EstimationStatus.LOADING && (
{ const classes = useStyles() - const nftAssets = useSelector(safeActiveSelectorMap) + const nftAssets = useSelector(nftAssetsSelector) const nftTokens = useSelector(nftTokensSelector) const addressBook = useSelector(addressBookSelector) const [selectedEntry, setSelectedEntry] = useState<{ address: string; name: string } | null>(() => { diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx index 454cb121..fbd745c4 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx @@ -34,7 +34,7 @@ const SelectedToken = ({ tokenAddress, tokens }: SelectTokenProps): ReactElement ) : ( @@ -73,7 +73,7 @@ const TokenSelectField = ({ initialValue, isValid = true, tokens }: TokenSelectF diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx index 85aeb91d..8a8db233 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -68,6 +68,7 @@ export type SendFundsTx = { } type SendFundsProps = { + initialValues: SendFundsTx onClose: () => void onReview: (txInfo: unknown) => void recipientAddress?: string @@ -80,6 +81,7 @@ const InputAdornmentChildSymbol = ({ symbol }: { symbol?: string }): ReactElemen } const SendFunds = ({ + initialValues, onClose, onReview, recipientAddress, @@ -93,12 +95,14 @@ const SendFunds = ({ const defaultEntry = { address: recipientAddress || '', name: '' } // if there's nothing to lookup for, we return the default entry - if (!recipientAddress) { + if (!initialValues?.recipientAddress && !recipientAddress) { return defaultEntry } + // if there's something to lookup for, `initialValues` has precedence over `recipientAddress` + const predefinedAddress = initialValues?.recipientAddress ?? recipientAddress const addressBookEntry = addressBook.find(({ address }) => { - return sameAddress(recipientAddress, address) + return sameAddress(predefinedAddress, address) }) // if found in the Address Book, then we return the entry @@ -170,7 +174,11 @@ const SendFunds = ({ @@ -208,7 +216,7 @@ const SendFunds = ({ const setMaxAllowedAmount = () => { const isSpendingLimit = tokenSpendingLimit && txType === 'spendingLimit' - let maxAmount = selectedToken?.balance ?? 0 + let maxAmount = selectedToken?.balance.tokenBalance ?? 0 if (isSpendingLimit) { const spendingLimitBalance = fromTokenUnit( diff --git a/src/routes/safe/components/Balances/Tokens/index.tsx b/src/routes/safe/components/Balances/Tokens/index.tsx deleted file mode 100644 index b7c01a4a..00000000 --- a/src/routes/safe/components/Balances/Tokens/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import IconButton from '@material-ui/core/IconButton' -import { makeStyles } from '@material-ui/core/styles' -import Close from '@material-ui/icons/Close' - -import React from 'react' -import { useSelector } from 'react-redux' - -import { styles } from './style' - -import Hairline from 'src/components/layout/Hairline' -import Paragraph from 'src/components/layout/Paragraph' -import Row from 'src/components/layout/Row' - -import { orderedTokenListSelector } from 'src/logic/tokens/store/selectors' -import { AssetsList } from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList' - -import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' -import { safeBlacklistedTokensSelector } from 'src/logic/safe/store/selectors' -import { TokenList } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList' - -export const MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID = 'manage-tokens-close-modal-btn' - -const useStyles = makeStyles(styles) - -type Props = { - safeAddress: string - modalScreen: string - onClose: () => void -} - -export const Tokens = (props: Props): React.ReactElement => { - const { modalScreen, onClose, safeAddress } = props - const tokens = useSelector(orderedTokenListSelector) - const activeTokens = useSelector(extendedSafeTokensSelector) - const blacklistedTokens = useSelector(safeBlacklistedTokensSelector) - const classes = useStyles() - - return ( - <> - - - Manage List - - - - - - - {modalScreen === 'tokenList' && ( - - )} - {modalScreen === 'assetsList' && } - - ) -} diff --git a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow.tsx b/src/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow.tsx deleted file mode 100644 index 9a927ad2..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import ListItem from '@material-ui/core/ListItem' -import ListItemIcon from '@material-ui/core/ListItemIcon' -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' -import ListItemText from '@material-ui/core/ListItemText' -import Switch from '@material-ui/core/Switch' -import React, { memo } from 'react' - -import { useStyles } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList/style' -import Img from 'src/components/layout/Img' -import { getNetworkInfo } from 'src/config' -import { setCollectibleImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' - -export const TOGGLE_ASSET_TEST_ID = 'toggle-asset-btn' - -const { nativeCoin } = getNetworkInfo() - -const AssetRow = memo(({ data, index, style }: any) => { - const classes = useStyles() - const { activeAssetsAddresses, assets, onSwitch } = data - const asset = assets[index] - const { address, image, name, symbol } = asset - const isActive = activeAssetsAddresses.includes(asset.address) - - return ( -
- - - {name} - - - {address !== nativeCoin.address && ( - - - - )} - -
- ) -}) - -AssetRow.displayName = 'AssetRow' - -export default AssetRow diff --git a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/index.tsx b/src/routes/safe/components/Balances/Tokens/screens/AssetsList/index.tsx deleted file mode 100644 index 5671a662..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import MuiList from '@material-ui/core/List' -import CircularProgress from '@material-ui/core/CircularProgress' -import Search from '@material-ui/icons/Search' -import cn from 'classnames' -import SearchBar from 'material-ui-search-bar' -import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { FixedSizeList } from 'react-window' -import Paragraph from 'src/components/layout/Paragraph' - -import { useStyles } from './style' - -import Block from 'src/components/layout/Block' -import Hairline from 'src/components/layout/Hairline' -import Row from 'src/components/layout/Row' -import { nftAssetsListSelector } from 'src/logic/collectibles/store/selectors' -import AssetRow from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow' -import updateActiveAssets from 'src/logic/safe/store/actions/updateActiveAssets' -import updateBlacklistedAssets from 'src/logic/safe/store/actions/updateBlacklistedAssets' -import { - safeActiveAssetsListSelector, - safeBlacklistedAssetsSelector, - safeParamAddressFromStateSelector, -} from 'src/logic/safe/store/selectors' - -const filterBy = (filter, nfts) => - nfts.filter( - (asset) => - !filter || - asset.description.toLowerCase().includes(filter.toLowerCase()) || - asset.name.toLowerCase().includes(filter.toLowerCase()) || - asset.symbol.toLowerCase().includes(filter.toLowerCase()), - ) - -export const AssetsList = (): React.ReactElement => { - const classes = useStyles() - const searchClasses = { - input: classes.searchInput, - root: classes.searchRoot, - iconButton: classes.searchIcon, - searchContainer: classes.searchContainer, - } - const dispatch = useDispatch() - const activeAssetsList = useSelector(safeActiveAssetsListSelector) - const blacklistedAssets = useSelector(safeBlacklistedAssetsSelector) - const safeAddress = useSelector(safeParamAddressFromStateSelector) - const [filterValue, setFilterValue] = useState('') - const [activeAssetsAddresses, setActiveAssetsAddresses] = useState(activeAssetsList) - const [blacklistedAssetsAddresses, setBlacklistedAssetsAddresses] = useState(blacklistedAssets) - const nftAssetsList = useSelector(nftAssetsListSelector) - - const onCancelSearch = () => { - setFilterValue('') - } - - const onChangeSearchBar = (value) => { - setFilterValue(value) - } - - const getItemKey = (index) => { - return index - } - - const onSwitch = (asset) => () => { - let newActiveAssetsAddresses - let newBlacklistedAssetsAddresses - if (activeAssetsAddresses.has(asset.address)) { - newActiveAssetsAddresses = activeAssetsAddresses.delete(asset.address) - newBlacklistedAssetsAddresses = blacklistedAssetsAddresses.add(asset.address) - } else { - newActiveAssetsAddresses = activeAssetsAddresses.add(asset.address) - newBlacklistedAssetsAddresses = blacklistedAssetsAddresses.delete(asset.address) - } - - // Set local state - setActiveAssetsAddresses(newActiveAssetsAddresses) - setBlacklistedAssetsAddresses(newBlacklistedAssetsAddresses) - // Dispatch to global state - dispatch(updateActiveAssets(safeAddress, newActiveAssetsAddresses)) - dispatch(updateBlacklistedAssets(safeAddress, newBlacklistedAssetsAddresses)) - } - - const createItemData = (assetsList) => { - return { - assets: assetsList, - activeAssetsAddresses, - onSwitch, - } - } - - const nftAssetsFilteredList = filterBy(filterValue, nftAssetsList) - const itemData = createItemData(nftAssetsFilteredList) - - return ( - <> - - - - } - value={filterValue} - /> - - - - {!nftAssetsList?.length && ( - - {!nftAssetsList ? : No collectibles available} - - )} - {nftAssetsFilteredList.length > 0 && ( - - - {AssetRow} - - - )} - - ) -} diff --git a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/style.ts b/src/routes/safe/components/Balances/Tokens/screens/AssetsList/style.ts deleted file mode 100644 index 65c0ed1b..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/style.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { createStyles, makeStyles } from '@material-ui/core' - -import { md, mediumFontSize, secondaryText, sm, xs } from 'src/theme/variables' - -export const useStyles = makeStyles( - createStyles({ - root: { - minHeight: '52px', - }, - search: { - color: secondaryText, - paddingLeft: sm, - }, - padding: { - padding: `0 ${md}`, - }, - add: { - fontSize: '11px', - fontWeight: 'normal', - paddingRight: md, - paddingLeft: md, - }, - addBtnLabel: { - fontSize: mediumFontSize, - }, - actions: { - height: '50px', - }, - list: { - overflow: 'hidden', - overflowY: 'scroll', - padding: 0, - height: '100%', - }, - tokenIcon: { - marginRight: sm, - height: '28px', - width: '28px', - }, - searchInput: { - backgroundColor: 'transparent', - lineHeight: 'initial', - fontSize: '13px', - padding: 0, - '& > input::placeholder': { - letterSpacing: '-0.5px', - fontSize: mediumFontSize, - color: 'black', - }, - '& > input': { - letterSpacing: '-0.5px', - }, - }, - progressContainer: { - width: '100%', - height: '100%', - alignItems: 'center', - }, - searchContainer: { - marginLeft: xs, - marginRight: xs, - }, - searchRoot: { - letterSpacing: '-0.5px', - fontSize: '13px', - border: 'none', - boxShadow: 'none', - '& > button': { - display: 'none', - }, - flex: 1, - }, - searchIcon: { - '&:hover': { - backgroundColor: 'transparent !important', - }, - }, - }), -) diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow.tsx b/src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow.tsx deleted file mode 100644 index 9b73f079..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import ListItem from '@material-ui/core/ListItem' -import ListItemIcon from '@material-ui/core/ListItemIcon' -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' -import ListItemText from '@material-ui/core/ListItemText' -import Switch from '@material-ui/core/Switch' -import React, { CSSProperties, memo, ReactElement } from 'react' - -import { useStyles } from './style' -import Img from 'src/components/layout/Img' -import { getNetworkInfo } from 'src/config' -import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' -import { ItemData } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList/index' - -export const TOGGLE_TOKEN_TEST_ID = 'toggle-token-btn' - -interface TokenRowProps { - data: ItemData - index: number - style: CSSProperties -} - -const { nativeCoin } = getNetworkInfo() - -const TokenRow = memo(({ data, index, style }: TokenRowProps): ReactElement | null => { - const classes = useStyles() - const { activeTokensAddresses, onSwitch, tokens } = data - const token = tokens.get(index) - - if (!token) { - return null - } - - const isActive = activeTokensAddresses.has(token.address) - - return ( -
- - - {token.name} - - - {token.address !== nativeCoin.address && ( - - - - )} - -
- ) -}) - -TokenRow.displayName = 'TokenRow' - -export default TokenRow diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx b/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx deleted file mode 100644 index aa7e12cc..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import CircularProgress from '@material-ui/core/CircularProgress' -import MuiList from '@material-ui/core/List' -import Search from '@material-ui/icons/Search' -import cn from 'classnames' -import { List, Set } from 'immutable' -import SearchBar from 'material-ui-search-bar' -import React, { useState } from 'react' -import { FixedSizeList } from 'react-window' - -import TokenRow from './TokenRow' -import { useStyles } from './style' -import Block from 'src/components/layout/Block' -import Hairline from 'src/components/layout/Hairline' -import Row from 'src/components/layout/Row' -import { Token } from 'src/logic/tokens/store/model/token' -import { useDispatch } from 'react-redux' -import updateBlacklistedTokens from 'src/logic/safe/store/actions/updateBlacklistedTokens' -import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens' - -export const ADD_CUSTOM_TOKEN_BUTTON_TEST_ID = 'add-custom-token-btn' - -const filterBy = (filter: string, tokens: List): List => - tokens.filter( - (token) => - !filter || - token.symbol.toLowerCase().includes(filter.toLowerCase()) || - token.name.toLowerCase().includes(filter.toLowerCase()), - ) - -type Props = { - tokens: List - activeTokens: List - blacklistedTokens: Set - safeAddress: string -} - -export type ItemData = { - tokens: List - activeTokensAddresses: Set - onSwitch: (token: Token) => () => void -} - -export const TokenList = (props: Props): React.ReactElement => { - const classes = useStyles() - const { tokens, activeTokens, blacklistedTokens, safeAddress } = props - const [activeTokensAddresses, setActiveTokensAddresses] = useState(Set(activeTokens.map(({ address }) => address))) - const [blacklistedTokensAddresses, setBlacklistedTokensAddresses] = useState>(blacklistedTokens) - const [filter, setFilter] = useState('') - const dispatch = useDispatch() - - const searchClasses = { - input: classes.searchInput, - root: classes.searchRoot, - iconButton: classes.searchIcon, - searchContainer: classes.searchContainer, - } - - const onCancelSearch = () => { - setFilter('') - } - - const onChangeSearchBar = (value: string) => { - setFilter(value) - } - - const onSwitch = (token: Token) => () => { - let newActiveTokensAddresses - let newBlacklistedTokensAddresses - if (activeTokensAddresses.has(token.address)) { - newActiveTokensAddresses = activeTokensAddresses.delete(token.address) - newBlacklistedTokensAddresses = blacklistedTokensAddresses.add(token.address) - } else { - newActiveTokensAddresses = activeTokensAddresses.add(token.address) - newBlacklistedTokensAddresses = blacklistedTokensAddresses.delete(token.address) - } - - // Set local state - setActiveTokensAddresses(newActiveTokensAddresses) - setBlacklistedTokensAddresses(newBlacklistedTokensAddresses) - // Dispatch to global state - dispatch(updateActiveTokens(safeAddress, newActiveTokensAddresses)) - dispatch(updateBlacklistedTokens(safeAddress, newBlacklistedTokensAddresses)) - } - - const createItemData = (tokens: List, activeTokensAddresses: Set): ItemData => ({ - tokens, - activeTokensAddresses, - onSwitch, - }) - - const getItemKey = (index: number, { tokens }): string => { - return tokens.get(index).address - } - - const filteredTokens = filterBy(filter, tokens) - const itemData = createItemData(filteredTokens, activeTokensAddresses) - - return ( - <> - - - - } - value={filter} - /> - - - - {!tokens.size && ( - - - - )} - {tokens.size > 0 && ( - - - {TokenRow} - - - )} - - ) -} diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.ts b/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.ts deleted file mode 100644 index dbf3500b..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { createStyles, makeStyles } from '@material-ui/core' - -import { border, md, mediumFontSize, secondaryText, sm, xs } from 'src/theme/variables' - -export const useStyles = makeStyles( - createStyles({ - root: { - minHeight: '52px', - }, - search: { - color: secondaryText, - paddingLeft: sm, - }, - padding: { - padding: `0 ${md}`, - }, - add: { - fontSize: '11px', - fontWeight: 'normal', - paddingRight: md, - paddingLeft: md, - }, - addBtnLabel: { - fontSize: mediumFontSize, - }, - actions: { - height: '50px', - }, - list: { - overflow: 'hidden', - overflowY: 'scroll', - padding: 0, - height: '100%', - }, - token: { - minHeight: '50px', - borderBottom: `1px solid ${border}`, - }, - tokenRoot: { - paddingTop: 0, - paddingBottom: 0, - }, - searchInput: { - backgroundColor: 'transparent', - lineHeight: 'initial', - fontSize: '13px', - padding: 0, - '& > input::placeholder': { - letterSpacing: '-0.5px', - fontSize: mediumFontSize, - color: 'black', - }, - '& > input': { - letterSpacing: '-0.5px', - }, - }, - tokenIcon: { - marginRight: md, - height: '28px', - width: '28px', - }, - progressContainer: { - width: '100%', - height: '100%', - alignItems: 'center', - }, - searchContainer: { - marginLeft: xs, - marginRight: xs, - }, - searchRoot: { - letterSpacing: '-0.5px', - fontSize: '13px', - border: 'none', - boxShadow: 'none', - '& > button': { - display: 'none', - }, - flex: 1, - }, - searchIcon: { - '&:hover': { - backgroundColor: 'transparent !important', - }, - }, - }), -) diff --git a/src/routes/safe/components/Balances/Tokens/style.ts b/src/routes/safe/components/Balances/Tokens/style.ts deleted file mode 100644 index b37e7b7f..00000000 --- a/src/routes/safe/components/Balances/Tokens/style.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { lg, md } from 'src/theme/variables' -import { createStyles } from '@material-ui/core' - -export const styles = createStyles({ - heading: { - padding: `${md} ${lg}`, - justifyContent: 'space-between', - maxHeight: '75px', - boxSizing: 'border-box', - }, - close: { - height: '35px', - width: '35px', - }, -}) diff --git a/src/routes/safe/components/Balances/dataFetcher.ts b/src/routes/safe/components/Balances/dataFetcher.ts index 7d48d54a..f9868540 100644 --- a/src/routes/safe/components/Balances/dataFetcher.ts +++ b/src/routes/safe/components/Balances/dataFetcher.ts @@ -1,32 +1,13 @@ -import { BigNumber } from 'bignumber.js' import { List } from 'immutable' import { getNetworkInfo } from 'src/config' import { FIXED } from 'src/components/Table/sorting' import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' import { TableColumn } from 'src/components/Table/types.d' -import { BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues' import { Token } from 'src/logic/tokens/store/model/token' -import { sameAddress } from 'src/logic/wallets/ethAddresses' - export const BALANCE_TABLE_ASSET_ID = 'asset' export const BALANCE_TABLE_BALANCE_ID = 'balance' export const BALANCE_TABLE_VALUE_ID = 'value' -const { nativeCoin } = getNetworkInfo() - -const getTokenValue = (token: Token, currencyValues: BalanceCurrencyList, currencyRate: number): string => { - const currencyValue = currencyValues.find( - ({ tokenAddress }) => sameAddress(token.address, tokenAddress) || sameAddress(token.address, nativeCoin.address), - ) - - if (!currencyValue) { - return '' - } - - const { balanceInBaseCurrency } = currencyValue - return new BigNumber(balanceInBaseCurrency).times(currencyRate).toString() -} - const getTokenPriceInCurrency = (balance: string, currencySelected?: string): string => { if (!currencySelected) { return Number('').toFixed(2) @@ -44,15 +25,10 @@ export interface BalanceData { valueOrder: number } -export const getBalanceData = ( - activeTokens: List, - currencySelected?: string, - currencyValues?: BalanceCurrencyList, - currencyRate?: number, -): List => { +export const getBalanceData = (activeTokens: List, currencySelected?: string): List => { const { nativeCoin } = getNetworkInfo() return activeTokens.map((token) => { - const balance = currencyRate && currencyValues ? getTokenValue(token, currencyValues, currencyRate) : '0' + const { tokenBalance, fiatBalance } = token.balance return { [BALANCE_TABLE_ASSET_ID]: { @@ -62,11 +38,11 @@ export const getBalanceData = ( symbol: token.symbol, }, assetOrder: token.name, - [BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance?.toString() || '0')} ${token.symbol}`, - balanceOrder: Number(token.balance), + [BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(tokenBalance?.toString() || '0')} ${token.symbol}`, + balanceOrder: Number(tokenBalance), [FIXED]: token.symbol === nativeCoin.symbol, - [BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(balance, currencySelected), - valueOrder: Number(balance), + [BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(fiatBalance || '0', currencySelected), + valueOrder: Number(tokenBalance), } }) } @@ -78,6 +54,7 @@ export const generateColumns = (): List => { disablePadding: false, label: 'Asset', custom: false, + static: true, width: 250, } @@ -88,6 +65,7 @@ export const generateColumns = (): List => { disablePadding: false, label: 'Balance', custom: false, + static: true, } const actions: TableColumn = { @@ -105,6 +83,7 @@ export const generateColumns = (): List => { order: true, label: 'Value', custom: false, + static: true, disablePadding: false, } diff --git a/src/routes/safe/components/Balances/index.tsx b/src/routes/safe/components/Balances/index.tsx index be80a8ba..d9dd2bdc 100644 --- a/src/routes/safe/components/Balances/index.tsx +++ b/src/routes/safe/components/Balances/index.tsx @@ -3,18 +3,16 @@ import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import ReceiveModal from 'src/components/App/ReceiveModal' -import { Tokens } from './Tokens' import { styles } from './style' import Modal from 'src/components/Modal' -import ButtonLink from 'src/components/layout/ButtonLink' import Col from 'src/components/layout/Col' import Divider from 'src/components/layout/Divider' import Row from 'src/components/layout/Row' import { SAFELIST_ADDRESS } from 'src/routes/routes' import SendModal from 'src/routes/safe/components/Balances/SendModal' -import CurrencyDropdown from 'src/routes/safe/components/CurrencyDropdown' +import { CurrencyDropdown } from 'src/routes/safe/components/CurrencyDropdown' import { safeFeaturesEnabledSelector, safeNameSelector, @@ -35,7 +33,6 @@ export const BALANCE_ROW_TEST_ID = 'balance-row' const INITIAL_STATE = { erc721Enabled: false, showToken: false, - showManageCollectibleModal: false, sendFunds: { isOpen: false, selectedToken: '', @@ -95,17 +92,8 @@ const Balances = (): React.ReactElement => { })) } - const { - assetDivider, - assetTab, - assetTabActive, - assetTabs, - controls, - manageTokensButton, - receiveModal, - tokenControls, - } = classes - const { erc721Enabled, sendFunds, showManageCollectibleModal, showReceive, showToken } = state + const { assetDivider, assetTab, assetTabActive, assetTabs, controls, receiveModal, tokenControls } = classes + const { erc721Enabled, sendFunds, showReceive } = state return ( <> @@ -140,32 +128,7 @@ const Balances = (): React.ReactElement => { path={`${SAFELIST_ADDRESS}/${address}/balances/collectibles`} exact render={() => { - return !erc721Enabled ? ( - - ) : ( - - onShow('ManageCollectibleModal')} - size="lg" - testId="manage-tokens-btn" - > - Manage List - - onHide('ManageCollectibleModal')} - open={showManageCollectibleModal} - title="Manage List" - > - onHide('ManageCollectibleModal')} - safeAddress={address} - /> - - - ) + return !erc721Enabled ? : null }} /> { <> - onShow('Token')} - size="lg" - testId="manage-tokens-btn" - > - Manage List - - onHide('Token')} - open={showToken} - title="Manage List" - > - onHide('Token')} safeAddress={address} /> - ) diff --git a/src/routes/safe/components/CurrencyDropdown/index.tsx b/src/routes/safe/components/CurrencyDropdown/index.tsx index 5aea152a..012ba0e8 100644 --- a/src/routes/safe/components/CurrencyDropdown/index.tsx +++ b/src/routes/safe/components/CurrencyDropdown/index.tsx @@ -13,26 +13,22 @@ import { useDispatch, useSelector } from 'react-redux' import CheckIcon from './img/check.svg' import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency' -import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues' -import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style' -import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { availableCurrenciesSelector, currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' import { DropdownListTheme } from 'src/theme/mui' -import { setImageToPlaceholder } from '../Balances/utils' +import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import Img from 'src/components/layout/Img/index' import { getNetworkInfo } from 'src/config' import { sameString } from 'src/utils/strings' const { nativeCoin } = getNetworkInfo() -const CurrencyDropdown = (): React.ReactElement | null => { - const safeAddress = useSelector(safeParamAddressFromStateSelector) as string +export const CurrencyDropdown = (): React.ReactElement | null => { const dispatch = useDispatch() const [anchorEl, setAnchorEl] = useState(null) const selectedCurrency = useSelector(currentCurrencySelector) const [searchParams, setSearchParams] = useState('') - - const currenciesList = Object.values(AVAILABLE_CURRENCIES) + const currenciesList = useSelector(availableCurrenciesSelector) const tokenImage = nativeCoin.logoUri const classes = useDropdownStyles({}) const currenciesListFiltered = currenciesList.filter((currency) => @@ -47,8 +43,8 @@ const CurrencyDropdown = (): React.ReactElement | null => { setAnchorEl(null) } - const onCurrentCurrencyChangedHandler = (newCurrencySelectedName) => { - dispatch(setSelectedCurrency(safeAddress, newCurrencySelectedName)) + const onCurrentCurrencyChangedHandler = (newCurrencySelectedName: string) => { + dispatch(setSelectedCurrency({ selectedCurrency: newCurrencySelectedName })) handleClose() } @@ -80,6 +76,7 @@ const CurrencyDropdown = (): React.ReactElement | null => { horizontal: 'center', vertical: 'top', }} + TransitionProps={{ mountOnEnter: true, unmountOnExit: true }} >
@@ -139,5 +136,3 @@ const CurrencyDropdown = (): React.ReactElement | null => { ) } - -export default CurrencyDropdown diff --git a/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx b/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx index ad66f1b1..042f47d3 100644 --- a/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx +++ b/src/routes/safe/components/Settings/Advanced/RemoveModuleModal.tsx @@ -127,6 +127,8 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM open > diff --git a/src/routes/safe/components/Settings/Advanced/style.ts b/src/routes/safe/components/Settings/Advanced/style.ts index 272453bb..69925f4f 100644 --- a/src/routes/safe/components/Settings/Advanced/style.ts +++ b/src/routes/safe/components/Settings/Advanced/style.ts @@ -8,7 +8,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx index f88db617..864e1918 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx @@ -6,7 +6,7 @@ import Modal from 'src/components/Modal' import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner' +import { addSafeOwner } from 'src/logic/safe/store/actions/addSafeOwner' import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { checksumAddress } from 'src/utils/checksumAddress' @@ -16,7 +16,7 @@ import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionPara import { OwnerForm } from './screens/OwnerForm' import { ReviewAddOwner } from './screens/Review' -import ThresholdForm from './screens/ThresholdForm' +import { ThresholdForm } from './screens/ThresholdForm' const styles = createStyles({ biggerModalWindow: { @@ -65,7 +65,7 @@ type Props = { onClose: () => void } -const AddOwner = ({ isOpen, onClose }: Props): React.ReactElement => { +export const AddOwnerModal = ({ isOpen, onClose }: Props): React.ReactElement => { const classes = useStyles() const [activeScreen, setActiveScreen] = useState('selectOwner') const [values, setValues] = useState({ ownerName: '', ownerAddress: '', threshold: '' }) @@ -138,5 +138,3 @@ const AddOwner = ({ isOpen, onClose }: Props): React.ReactElement => { ) } - -export default AddOwner diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx index 73083d62..dabe3b40 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.tsx @@ -11,14 +11,20 @@ import AddressInput from 'src/components/forms/AddressInput' import Field from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' import TextField from 'src/components/forms/TextField' -import { composeValidators, minMaxLength, required, uniqueAddress } from 'src/components/forms/validator' +import { + addressIsNotCurrentSafe, + composeValidators, + minMaxLength, + required, + uniqueAddress, +} from 'src/components/forms/validator' import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' import Col from 'src/components/layout/Col' import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' -import { safeOwnersAddressesListSelector } from 'src/logic/safe/store/selectors' +import { safeOwnersAddressesListSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' export const ADD_OWNER_NAME_INPUT_TEST_ID = 'add-owner-name-input' export const ADD_OWNER_ADDRESS_INPUT_TEST_ID = 'add-owner-address-testid' @@ -43,7 +49,9 @@ export const OwnerForm = ({ onClose, onSubmit }: OwnerFormProps): React.ReactEle onSubmit(values) } const owners = useSelector(safeOwnersAddressesListSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) const ownerDoesntExist = uniqueAddress(owners) + const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress) return ( <> @@ -98,7 +106,7 @@ export const OwnerForm = ({ onClose, onSubmit }: OwnerFormProps): React.ReactEle placeholder="Owner address*" testId={ADD_OWNER_ADDRESS_INPUT_TEST_ID} text="Owner address*" - validators={[ownerDoesntExist]} + validators={[ownerDoesntExist, ownerAddressIsNotSafeAddress]} /> diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx index dd53427a..1acbd6ef 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx @@ -101,6 +101,8 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie return ( diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts index 1a94ae9b..78644036 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts @@ -64,7 +64,7 @@ export const styles = createStyles({ selectedOwner: { padding: sm, alignItems: 'center', - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, user: { justifyContent: 'left', diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx index 651c9078..ba38c9d6 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/index.tsx @@ -1,8 +1,8 @@ import IconButton from '@material-ui/core/IconButton' import MenuItem from '@material-ui/core/MenuItem' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' -import React from 'react' +import React, { ReactElement } from 'react' import { useSelector } from 'react-redux' import { styles } from './style' @@ -21,10 +21,24 @@ import { safeOwnersSelector, safeThresholdSelector } from 'src/logic/safe/store/ export const ADD_OWNER_THRESHOLD_NEXT_BTN_TEST_ID = 'add-owner-threshold-next-btn' -const ThresholdForm = ({ classes, onClickBack, onClose, onSubmit }) => { +const useStyles = makeStyles(styles) + +type SubmitProps = { + threshold: number +} + +type Props = { + onClickBack: () => void + onClose: () => void + onSubmit: (values: SubmitProps) => void +} + +export const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactElement => { + const classes = useStyles() const threshold = useSelector(safeThresholdSelector) as number const owners = useSelector(safeOwnersSelector) - const handleSubmit = (values) => { + + const handleSubmit = (values: SubmitProps) => { onSubmit(values) } @@ -110,5 +124,3 @@ const ThresholdForm = ({ classes, onClickBack, onClose, onSubmit }) => { ) } - -export default withStyles(styles as any)(ThresholdForm) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/style.ts b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/style.ts index 331290a0..c9b06d0d 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/ThresholdForm/style.ts @@ -1,6 +1,7 @@ import { lg, md, secondaryText, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ heading: { padding: `${sm} ${lg}`, justifyContent: 'flex-start', diff --git a/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx b/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx index f626aabe..3ef0769f 100644 --- a/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useEffect, useState } from 'react' +import { ReactElement, useEffect, useState } from 'react' import Identicon from 'src/components/Identicon' import Block from 'src/components/layout/Block' @@ -16,7 +16,7 @@ type OwnerAddressTableCellProps = { sendModalOpenHandler?: () => void } -const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactElement => { +export const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): ReactElement => { const { address, knownAddress, showLinks, userName, sendModalOpenHandler } = props const [cut, setCut] = useState(0) const { width } = useWindowDimensions() @@ -50,5 +50,3 @@ const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactEl ) } - -export default OwnerAddressTableCell diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx index 436e233a..0875cac1 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx @@ -4,13 +4,13 @@ import { useDispatch, useSelector } from 'react-redux' import CheckOwner from './screens/CheckOwner' import { ReviewRemoveOwnerModal } from './screens/Review' -import ThresholdForm from './screens/ThresholdForm' +import { ThresholdForm } from './screens/ThresholdForm' import Modal from 'src/components/Modal' import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' -import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner' +import { removeSafeOwner } from 'src/logic/safe/store/actions/removeSafeOwner' import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors' import { Dispatch } from 'src/logic/safe/store/actions/types.d' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx index c69aba52..7d77084c 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx @@ -123,6 +123,8 @@ export const ReviewRemoveOwnerModal = ({ return ( {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx index a7d66ca3..480fbdef 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/ThresholdForm/index.tsx @@ -30,7 +30,7 @@ type Props = { onSubmit: (txParameters: TxParameters) => void } -const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactElement => { +export const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactElement => { const classes = useStyles() const owners = useSelector(safeOwnersSelector) const threshold = useSelector(safeThresholdSelector) as number @@ -120,5 +120,3 @@ const ThresholdForm = ({ onClickBack, onClose, onSubmit }: Props): ReactElement ) } - -export default ThresholdForm diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index 7c812f90..74cd9f81 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -7,15 +7,15 @@ import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' -import replaceSafeOwner from 'src/logic/safe/store/actions/replaceSafeOwner' +import { replaceSafeOwner } from 'src/logic/safe/store/actions/replaceSafeOwner' import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors' import { checksumAddress } from 'src/utils/checksumAddress' import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { Dispatch } from 'src/logic/safe/store/actions/types.d' -import OwnerForm from './screens/OwnerForm' -import { ReviewReplaceOwnerModal } from './screens/Review' +import { OwnerForm } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm' +import { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' const styles = createStyles({ diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx index 035a5e8f..e1ae1472 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.tsx @@ -1,8 +1,7 @@ import IconButton from '@material-ui/core/IconButton' -import { withStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import classNames from 'classnames/bind' -import React from 'react' +import React, { ReactElement } from 'react' import { useSelector } from 'react-redux' import CopyBtn from 'src/components/CopyBtn' @@ -10,7 +9,13 @@ import AddressInput from 'src/components/forms/AddressInput' import Field from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' import TextField from 'src/components/forms/TextField' -import { composeValidators, minMaxLength, required, uniqueAddress } from 'src/components/forms/validator' +import { + addressIsNotCurrentSafe, + composeValidators, + minMaxLength, + required, + uniqueAddress, +} from 'src/components/forms/validator' import Identicon from 'src/components/Identicon' import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' @@ -19,11 +24,12 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' -import { safeOwnersAddressesListSelector } from 'src/logic/safe/store/selectors' +import { safeOwnersAddressesListSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { styles } from './style' import { getExplorerInfo } from 'src/config' import { ExplorerButton } from '@gnosis.pm/safe-react-components' +import { makeStyles } from '@material-ui/core' export const REPLACE_OWNER_NAME_INPUT_TEST_ID = 'replace-owner-name-input' export const REPLACE_OWNER_ADDRESS_INPUT_TEST_ID = 'replace-owner-address-testid' @@ -35,12 +41,30 @@ const formMutators = { }, } -const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }) => { - const handleSubmit = (values) => { +const useStyles = makeStyles(styles) + +type NewOwnerProps = { + ownerAddress: string + ownerName: string +} + +type OwnerFormProps = { + onClose: () => void + onSubmit: (values: NewOwnerProps) => void + ownerAddress: string + ownerName: string +} + +export const OwnerForm = ({ onClose, onSubmit, ownerAddress, ownerName }: OwnerFormProps): ReactElement => { + const classes = useStyles() + + const handleSubmit = (values: NewOwnerProps) => { onSubmit(values) } const owners = useSelector(safeOwnersAddressesListSelector) + const safeAddress = useSelector(safeParamAddressFromStateSelector) const ownerDoesntExist = uniqueAddress(owners) + const ownerAddressIsNotSafeAddress = addressIsNotCurrentSafe(safeAddress) return ( <> @@ -106,7 +130,6 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }) => { { @@ -136,11 +158,10 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }) => { - diff --git a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx index bdac5ec3..2e9246f2 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx @@ -57,7 +57,7 @@ const ThresholdSettings = (): React.ReactElement => { onClick={toggleModal} variant="contained" > - Modify + Change )} diff --git a/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx b/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx index aea5f0ce..b56ae3d0 100644 --- a/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx +++ b/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx @@ -76,7 +76,13 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle }) return ( - + {(txParameters, toggleEditMode) => ( <> @@ -116,6 +122,7 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle compact={false} isTransactionCreation={isCreation} isTransactionExecution={isExecution} + isOffChainSignature={isOffChainSignature} /> {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( diff --git a/src/routes/safe/components/Transactions/TxList/HexEncodedData.tsx b/src/routes/safe/components/Transactions/TxList/HexEncodedData.tsx index 81955e08..fc48d716 100644 --- a/src/routes/safe/components/Transactions/TxList/HexEncodedData.tsx +++ b/src/routes/safe/components/Transactions/TxList/HexEncodedData.tsx @@ -4,7 +4,7 @@ import React, { ReactElement, useState } from 'react' import Paragraph from 'src/components/layout/Paragraph' import LinkWithRef from 'src/components/layout/Link' -import { shortVersionOf } from 'src/logic/wallets/ethAddresses' +import { textShortener } from 'src/utils/strings' export const styles = createStyles({ txDataParagraph: { @@ -18,16 +18,27 @@ export const styles = createStyles({ const useStyles = makeStyles(styles) -export const HexEncodedData = ({ hexData }: { hexData: string }): ReactElement => { +export const HexEncodedData = ({ + hexData, + title, + limit = 20, +}: { + hexData: string + title?: string + limit?: number +}): ReactElement => { const classes = useStyles() const [showTxData, setShowTxData] = useState(false) - const showExpandBtn = hexData.length > 20 + const showExpandBtn = hexData.length > limit + const shortener = textShortener({ charsStart: 40, charsEnd: 0 }) return (
- - Data (hex encoded): - + {title && ( + + {title}: + + )} {showExpandBtn ? ( <> @@ -46,7 +57,7 @@ export const HexEncodedData = ({ hexData }: { hexData: string }): ReactElement = ) : ( <> - {shortVersionOf(hexData, 20)}{' '} + {shortener(hexData)}{' '} { - const getTextValue = (value: string) => {value} + const getTextValue = (value: string) => const getArrayValue = (parentId: string, value: string[] | string) => (
@@ -31,10 +28,12 @@ const GenericValue = ({ method, type, value }: RenderValueProps): React.ReactEle {(value as string[]).map((currentValue, index) => { const key = `${parentId}-value-${index}` - return ( + return Array.isArray(currentValue) ? ( - {Array.isArray(currentValue) ? getArrayValue(key, currentValue) : getTextValue(currentValue)} + {getArrayValue(key, currentValue)} + ) : ( + getTextValue(currentValue) ) })} diff --git a/src/routes/safe/components/Transactions/TxList/MultiSendDetails.tsx b/src/routes/safe/components/Transactions/TxList/MultiSendDetails.tsx index 956d3f66..f3a87201 100644 --- a/src/routes/safe/components/Transactions/TxList/MultiSendDetails.tsx +++ b/src/routes/safe/components/Transactions/TxList/MultiSendDetails.tsx @@ -43,7 +43,7 @@ export const MultiSendDetails = ({ txData }: { txData: TransactionData }): React if (!txData.dataDecoded?.parameters) { // we render the hex encoded data if (txData.hexData) { - return + return } return null @@ -64,7 +64,7 @@ export const MultiSendDetails = ({ txData }: { txData: TransactionData }): React details = } else { // We couldn't decode it but we have data - details = data && + details = data && } return ( diff --git a/src/routes/safe/components/Transactions/TxList/TxData.tsx b/src/routes/safe/components/Transactions/TxList/TxData.tsx index 060cb6a9..d6c8dfa5 100644 --- a/src/routes/safe/components/Transactions/TxList/TxData.tsx +++ b/src/routes/safe/components/Transactions/TxList/TxData.tsx @@ -52,7 +52,7 @@ export const TxData = ({ txData }: TxDataProps): ReactElement | null => { // we render the hex encoded data return ( - + ) } diff --git a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx index eca47c1d..c8b24649 100644 --- a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx +++ b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx @@ -317,6 +317,8 @@ export const ApproveTxModal = ({ return ( )} @@ -396,8 +399,8 @@ export const ApproveTxModal = ({ {/* Footer */} -