From 058fec3dbcb71c811c94d4622af29ebe9b392f6b Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Mon, 26 Oct 2020 10:11:52 -0300 Subject: [PATCH] (Feature) - Send erc721 collectible with proxy contact (#1508) * Removed containsMethodByHash condition check, now we always expect that safeTransferFrom is defined on the erc721 contract * Types * More types * Add try catch on estimateGas * Add try catch on submit transaction * More types * More types * More types * ReviewTx modal props * Fix SendCollectible modal types * Add guard for safeAddress * Move some imports * Fix DispatchReturn types * Fix import of Dispatch * Remove console log * Adds logs * Fix import Co-authored-by: Daniel Sanchez --- .../safe/store/actions/createTransaction.ts | 6 +- src/logic/safe/store/actions/types.d.ts | 4 +- .../Balances/Collectibles/index.tsx | 130 +++++++++--------- .../components/Balances/SendModal/index.tsx | 48 +++++-- .../SendModal/screens/ChooseTxType/index.tsx | 2 +- .../ContractInteraction/Review/index.tsx | 32 ++--- .../ReviewCustomTx/index.tsx | 38 +++-- .../SendCustomTx/index.tsx | 12 +- .../screens/ContractInteraction/index.tsx | 8 +- .../screens/ReviewCollectible/index.tsx | 92 ++++++++----- .../screens/ReviewCollectible/style.ts | 3 +- .../SendModal/screens/ReviewTx/index.tsx | 46 ++++--- .../SendModal/screens/ReviewTx/style.ts | 3 +- .../screens/SendCollectible/index.tsx | 22 ++- .../SendModal/screens/SendFunds/index.tsx | 18 +-- .../SendModal/screens/SendFunds/style.ts | 3 +- .../ManageOwners/AddOwnerModal/index.tsx | 35 +++-- .../ManageOwners/RemoveOwnerModal/index.tsx | 66 +++++---- .../ManageOwners/ReplaceOwnerModal/index.tsx | 51 ++++--- 19 files changed, 367 insertions(+), 252 deletions(-) diff --git a/src/logic/safe/store/actions/createTransaction.ts b/src/logic/safe/store/actions/createTransaction.ts index 44bb4f89..841d1c90 100644 --- a/src/logic/safe/store/actions/createTransaction.ts +++ b/src/logic/safe/store/actions/createTransaction.ts @@ -42,7 +42,7 @@ import { Transaction, TransactionStatus, TxArgs } from 'src/logic/safe/store/mod import { AnyAction } from 'redux' import { PayableTx } from 'src/types/contracts/types.d' import { AppReduxState } from 'src/store' -import { Dispatch } from './types' +import { Dispatch, DispatchReturn } from './types' export const removeTxFromStore = ( tx: Transaction, @@ -110,7 +110,7 @@ interface CreateTransactionArgs { safeTxGas?: number } -type CreateTransactionAction = ThunkAction, AppReduxState, undefined, AnyAction> +type CreateTransactionAction = ThunkAction, AppReduxState, DispatchReturn, AnyAction> type ConfirmEventHandler = (safeTxHash: string) => void type ErrorEventHandler = () => void @@ -129,7 +129,7 @@ const createTransaction = ( }: CreateTransactionArgs, onUserConfirm?: ConfirmEventHandler, onError?: ErrorEventHandler, -): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { +): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise => { const state = getState() if (navigateToTransactionsTab) { diff --git a/src/logic/safe/store/actions/types.d.ts b/src/logic/safe/store/actions/types.d.ts index 0006a041..d95ce82c 100644 --- a/src/logic/safe/store/actions/types.d.ts +++ b/src/logic/safe/store/actions/types.d.ts @@ -3,4 +3,6 @@ import { AnyAction } from 'redux' import { AppReduxState } from 'src/store' -export type Dispatch = ThunkDispatch +export type DispatchReturn = string | undefined + +export type Dispatch = ThunkDispatch diff --git a/src/routes/safe/components/Balances/Collectibles/index.tsx b/src/routes/safe/components/Balances/Collectibles/index.tsx index c06b7227..fa2185da 100644 --- a/src/routes/safe/components/Balances/Collectibles/index.tsx +++ b/src/routes/safe/components/Balances/Collectibles/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react' import Card from '@material-ui/core/Card' -import { makeStyles } from '@material-ui/core/styles' +import { createStyles, makeStyles } from '@material-ui/core/styles' import { useSelector } from 'react-redux' import Item from './components/Item' @@ -8,78 +8,79 @@ import Item from './components/Item' import Paragraph from 'src/components/layout/Paragraph' import { activeNftAssetsListSelector, nftTokensSelector } from 'src/logic/collectibles/store/selectors' import SendModal from 'src/routes/safe/components/Balances/SendModal' -import { safeSelector } from 'src/logic/safe/store/selectors' import { fontColor, lg, screenSm, screenXs } from 'src/theme/variables' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' +import { NFTToken } from 'src/logic/collectibles/sources/collectibles.d' -const useStyles = makeStyles({ - cardInner: { - boxSizing: 'border-box', - maxWidth: '100%', - padding: '52px 54px', - }, - cardOuter: { - boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)', - }, - gridRow: { - boxSizing: 'border-box', - columnGap: '30px', - display: 'grid', - gridTemplateColumns: '1fr', - marginBottom: '45px', - maxWidth: '100%', - rowGap: '45px', - - '&:last-child': { - marginBottom: '0', +const useStyles = makeStyles( + createStyles({ + cardInner: { + boxSizing: 'border-box', + maxWidth: '100%', + padding: '52px 54px', }, - - [`@media (min-width: ${screenXs}px)`]: { - gridTemplateColumns: '1fr 1fr', + cardOuter: { + boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)', }, + gridRow: { + boxSizing: 'border-box', + columnGap: '30px', + display: 'grid', + gridTemplateColumns: '1fr', + marginBottom: '45px', + maxWidth: '100%', + rowGap: '45px', - [`@media (min-width: ${screenSm}px)`]: { - gridTemplateColumns: '1fr 1fr 1fr 1fr', + '&:last-child': { + marginBottom: '0', + }, + + [`@media (min-width: ${screenXs}px)`]: { + gridTemplateColumns: '1fr 1fr', + }, + + [`@media (min-width: ${screenSm}px)`]: { + gridTemplateColumns: '1fr 1fr 1fr 1fr', + }, }, - }, - title: { - alignItems: 'center', - display: 'flex', - margin: '0 0 18px', - }, - titleImg: { - backgroundPosition: '50% 50%', - backgroundRepeat: 'no-repeat', - backgroundSize: 'contain', - borderRadius: '50%', - height: '45px', - margin: '0 10px 0 0', - width: '45px', - }, - titleText: { - color: fontColor, - fontSize: '18px', - fontWeight: 'normal', - lineHeight: '1.2', - margin: '0', - }, - titleFiller: { - backgroundColor: '#e8e7e6', - flexGrow: '1', - height: '2px', - marginLeft: '40px', - }, - noData: { - fontSize: lg, - textAlign: 'center', - }, -} as any) + title: { + alignItems: 'center', + display: 'flex', + margin: '0 0 18px', + }, + titleImg: { + backgroundPosition: '50% 50%', + backgroundRepeat: 'no-repeat', + backgroundSize: 'contain', + borderRadius: '50%', + height: '45px', + margin: '0 10px 0 0', + width: '45px', + }, + titleText: { + color: fontColor, + fontSize: '18px', + fontWeight: 'normal', + lineHeight: '1.2', + margin: '0', + }, + titleFiller: { + backgroundColor: '#e8e7e6', + flexGrow: 1, + height: '2px', + marginLeft: '40px', + }, + noData: { + fontSize: lg, + textAlign: 'center', + }, + }), +) const Collectibles = (): React.ReactElement => { const classes = useStyles() - const [selectedToken, setSelectedToken] = React.useState({}) + const [selectedToken, setSelectedToken] = React.useState() const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false) - const { address, ethBalance, name } = useSelector(safeSelector) || {} const nftTokens = useSelector(nftTokensSelector) const activeAssetsList = useSelector(activeNftAssetsListSelector) const { trackEvent } = useAnalytics() @@ -88,7 +89,7 @@ const Collectibles = (): React.ReactElement => { trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Collectibles' }) }, [trackEvent]) - const handleItemSend = (nftToken) => { + const handleItemSend = (nftToken: NFTToken) => { setSelectedToken(nftToken) setSendNFTsModalOpen(true) } @@ -125,11 +126,8 @@ const Collectibles = (): React.ReactElement => { setSendNFTsModalOpen(false)} - safeAddress={address} - safeName={name} selectedToken={selectedToken} /> diff --git a/src/routes/safe/components/Balances/SendModal/index.tsx b/src/routes/safe/components/Balances/SendModal/index.tsx index 15df1404..ded4b980 100644 --- a/src/routes/safe/components/Balances/SendModal/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/index.tsx @@ -4,6 +4,14 @@ import cn from 'classnames' import React, { Suspense, useEffect, useState } from 'react' import Modal from 'src/components/Modal' +import { CollectibleTx } from './screens/ReviewCollectible' +import { CustomTx } from './screens/ContractInteraction/ReviewCustomTx' +import { SendFundsTx } from './screens/SendFunds' +import { ContractInteractionTx } from './screens/ContractInteraction' +import { CustomTxProps } from './screens/ContractInteraction/SendCustomTx' +import { ReviewTxProp } from './screens/ReviewTx' +import { NFTToken } from 'src/logic/collectibles/sources/collectibles' +import { SendCollectibleTxInfo } from './screens/SendCollectible' const ChooseTxType = React.lazy(() => import('./screens/ChooseTxType')) @@ -39,10 +47,24 @@ const useStyles = makeStyles({ }, }) -const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, selectedToken }: any) => { +type Props = { + activeScreenType: string + isOpen: boolean + onClose: () => void + recipientAddress?: string + selectedToken?: string | NFTToken +} + +const SendModal = ({ + activeScreenType, + isOpen, + onClose, + recipientAddress, + selectedToken, +}: Props): React.ReactElement => { const classes = useStyles() const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType') - const [tx, setTx] = useState({}) + const [tx, setTx] = useState({}) const [isABI, setIsABI] = useState(true) useEffect(() => { @@ -53,7 +75,7 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select const scalableModalSize = activeScreen === 'chooseTxType' - const handleTxCreation = (txInfo) => { + const handleTxCreation = (txInfo: SendCollectibleTxInfo) => { setActiveScreen('reviewTx') setTx(txInfo) } @@ -97,22 +119,22 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select )} {activeScreen === 'sendFunds' && ( )} {activeScreen === 'reviewTx' && ( - setActiveScreen('sendFunds')} tx={tx} /> + setActiveScreen('sendFunds')} tx={tx as ReviewTxProp} /> )} {activeScreen === 'contractInteraction' && isABI && ( @@ -122,7 +144,7 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select )} {activeScreen === 'contractInteraction' && !isABI && ( )} {activeScreen === 'reviewCustomTx' && ( - setActiveScreen('contractInteraction')} tx={tx} /> + setActiveScreen('contractInteraction')} tx={tx as CustomTx} /> )} {activeScreen === 'sendCollectible' && ( )} {activeScreen === 'reviewCollectible' && ( - setActiveScreen('sendCollectible')} tx={tx} /> + setActiveScreen('sendCollectible')} + tx={tx as CollectibleTx} + /> )} diff --git a/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx index ebbdc9fe..6bf71077 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx @@ -23,7 +23,7 @@ type ActiveScreen = 'sendFunds' | 'sendCollectible' | 'contractInteraction' interface ChooseTxTypeProps { onClose: () => void - recipientAddress: string + recipientAddress?: string setActiveScreen: React.Dispatch> } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx index 144bf87b..e67d8ae0 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx @@ -1,9 +1,9 @@ -import { makeStyles } from '@material-ui/core/styles' -import { useSnackbar } from 'notistack' import React, { useEffect, useState } from 'react' +import { makeStyles } from '@material-ui/core/styles' import { useDispatch, useSelector } from 'react-redux' -import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' + import { getNetworkInfo } from 'src/config' +import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import AddressInfo from 'src/components/AddressInfo' import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' @@ -43,7 +43,6 @@ type Props = { const { nativeCoin } = getNetworkInfo() const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactElement => { - const { enqueueSnackbar, closeSnackbar } = useSnackbar() const classes = useStyles() const dispatch = useDispatch() const { address: safeAddress } = useSelector(safeSelector) || {} @@ -74,18 +73,19 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE const txRecipient = tx.contractAddress const txData = tx.data ? tx.data.trim() : '' const txValue = tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0' - dispatch( - createTransaction({ - safeAddress, - to: txRecipient, - valueInWei: txValue, - txData, - notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, - enqueueSnackbar, - closeSnackbar, - } as any), - ) - + if (safeAddress) { + dispatch( + createTransaction({ + safeAddress, + to: txRecipient as string, + valueInWei: txValue, + txData, + notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, + }), + ) + } else { + console.error('There was an error trying to submit the transaction, the safeAddress was not found') + } onClose() } 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 060a23bf..bbe1d84c 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 @@ -1,11 +1,11 @@ +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' import IconButton from '@material-ui/core/IconButton' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' -import React, { useEffect, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' -import { getNetworkInfo } from 'src/config' +import { getNetworkInfo } from 'src/config' +import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import CopyBtn from 'src/components/CopyBtn' import EtherscanBtn from 'src/components/EtherscanBtn' import Identicon from 'src/components/Identicon' @@ -30,10 +30,16 @@ import ArrowDown from '../../assets/arrow-down.svg' import { styles } from './style' +export type CustomTx = { + contractAddress?: string + data?: string + value?: string +} + type Props = { onClose: () => void onPrev: () => void - tx: { contractAddress?: string; data?: string; value?: string } + tx: CustomTx } const useStyles = makeStyles(styles) @@ -72,15 +78,19 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { const txData = tx.data ? tx.data.trim() : '' const txValue = tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0' - dispatch( - createTransaction({ - safeAddress: safeAddress as string, - to: txRecipient as string, - valueInWei: txValue, - txData, - notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, - }), - ) + if (safeAddress) { + dispatch( + createTransaction({ + safeAddress: safeAddress, + to: txRecipient as string, + valueInWei: txValue, + txData, + notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, + }), + ) + } else { + console.error('There was an error trying to submit the transaction, the safeAddress was not found') + } onClose() } diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx index e99edda8..9b96d130 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/SendCustomTx/index.tsx @@ -1,10 +1,10 @@ +import React, { useState } from 'react' +import { useSelector } from 'react-redux' import IconButton from '@material-ui/core/IconButton' import InputAdornment from '@material-ui/core/InputAdornment' import { makeStyles } from '@material-ui/core/styles' import Switch from '@material-ui/core/Switch' import Close from '@material-ui/icons/Close' -import React, { useState } from 'react' -import { useSelector } from 'react-redux' import QRIcon from 'src/assets/icons/qrcode.svg' import CopyBtn from 'src/components/CopyBtn' @@ -40,13 +40,17 @@ export interface CreatedTx { value: string | number } +export type CustomTxProps = { + contractAddress?: string +} + type Props = { - initialValues: { contractAddress?: string } + initialValues: CustomTxProps onClose: () => void onNext: (tx: CreatedTx, submit: boolean) => void isABI: boolean switchMethod: () => void - contractAddress: string + contractAddress?: string } const useStyles = makeStyles(styles) diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx index e1c4d55e..bf44d55c 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx @@ -31,9 +31,13 @@ export interface CreatedTx { value: string | number } +export type ContractInteractionTx = { + contractAddress?: string +} + export interface ContractInteractionProps { - contractAddress: string - initialValues: { contractAddress?: string } + contractAddress?: string + initialValues: ContractInteractionTx isABI: boolean onClose: () => void switchMethod: () => void 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 f118a9ed..d073fd57 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx @@ -1,9 +1,9 @@ +import React, { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' import IconButton from '@material-ui/core/IconButton' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' -import { withSnackbar } from 'notistack' -import React, { useEffect, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' + import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import { getNetworkInfo } from 'src/config' import CopyBtn from 'src/components/CopyBtn' @@ -21,11 +21,7 @@ import createTransaction from 'src/logic/safe/store/actions/createTransaction' import { safeSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas' -import { - containsMethodByHash, - getERC721TokenContract, - getHumanFriendlyToken, -} from 'src/logic/tokens/store/actions/fetchTokens' +import { getERC721TokenContract } from 'src/logic/tokens/store/actions/fetchTokens' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH } from 'src/logic/tokens/utils/tokenHelpers' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' @@ -39,9 +35,22 @@ import { styles } from './style' const { nativeCoin } = getNetworkInfo() -const useStyles = makeStyles(styles as any) +const useStyles = makeStyles(styles) -const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { +export type CollectibleTx = { + recipientAddress: string + assetAddress: string + assetName: string + nftTokenId: string +} + +type Props = { + onClose: () => void + onPrev: () => void + tx: CollectibleTx +} + +const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement => { const classes = useStyles() const shortener = textShortener() const dispatch = useDispatch() @@ -57,22 +66,24 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx let isCurrent = true const estimateGas = async () => { - const supportsSafeTransfer = await containsMethodByHash(tx.assetAddress, SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH) - const methodToCall = supportsSafeTransfer ? `0x${SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH}` : 'transfer' - const transferParams = [tx.recipientAddress, tx.nftTokenId] - const params = methodToCall === 'transfer' ? transferParams : [safeAddress, ...transferParams] + try { + const methodToCall = `0x${SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH}` + const transferParams = [tx.recipientAddress, tx.nftTokenId] + const params = [safeAddress, ...transferParams] + const ERC721Token = await getERC721TokenContract() + const tokenInstance = await ERC721Token.at(tx.assetAddress) + const txData = tokenInstance.contract.methods[methodToCall](...params).encodeABI() - const ERC721Token = methodToCall === 'transfer' ? await getHumanFriendlyToken() : await getERC721TokenContract() - const tokenInstance = await ERC721Token.at(tx.assetAddress) - const txData = tokenInstance.contract.methods[methodToCall](...params).encodeABI() + const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.recipientAddress, txData) + const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) + const formattedGasCosts = formatAmount(gasCosts) - const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.recipientAddress, txData) - const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) - const formattedGasCosts = formatAmount(gasCosts) - - if (isCurrent) { - setGasCosts(formattedGasCosts) - setData(txData) + if (isCurrent) { + setGasCosts(formattedGasCosts) + setData(txData) + } + } catch (error) { + console.error('Error while calculating estimated gas:', error) } } @@ -84,18 +95,25 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }, [safeAddress, tx.assetAddress, tx.nftTokenId, tx.recipientAddress]) const submitTx = async () => { - dispatch( - createTransaction({ - safeAddress, - to: tx.assetAddress, - valueInWei: '0', - txData: data, - notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, - enqueueSnackbar, - closeSnackbar, - } as any), - ) - onClose() + try { + if (safeAddress) { + dispatch( + createTransaction({ + safeAddress, + to: tx.assetAddress, + valueInWei: '0', + txData: data, + notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, + }), + ) + } else { + console.error('There was an error trying to submit the transaction, the safeAddress was not found') + } + } catch (error) { + console.error('Error creating sendCollectible Tx:', error) + } finally { + onClose() + } } return ( @@ -180,4 +198,4 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx ) } -export default withSnackbar(ReviewCollectible) +export default ReviewCollectible diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/style.ts b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/style.ts index 52f70b5a..e6e4fbcf 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/style.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/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: `${md} ${lg}`, justifyContent: 'flex-start', diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx index 6278d9af..81a48eb7 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx @@ -2,7 +2,6 @@ import IconButton from '@material-ui/core/IconButton' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import { BigNumber } from 'bignumber.js' -import { withSnackbar } from 'notistack' import React, { useEffect, useMemo, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { toTokenUnit, fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' @@ -34,11 +33,24 @@ import ArrowDown from '../assets/arrow-down.svg' import { styles } from './style' -const useStyles = makeStyles(styles as any) +const useStyles = makeStyles(styles) const { nativeCoin } = getNetworkInfo() -const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { +export type ReviewTxProp = { + recipientAddress: string + amount: string + txRecipient: string + token: string +} + +type ReviewTxProps = { + onClose: () => void + onPrev: () => void + tx: ReviewTxProp +} + +const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => { const classes = useStyles() const dispatch = useDispatch() const { address: safeAddress } = useSelector(safeSelector) || {} @@ -69,7 +81,7 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI() } - const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, txRecipient, txData) + const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, txRecipient as string, txData) const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) const formattedGasCosts = formatAmount(gasCosts) @@ -92,17 +104,19 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { // if txAmount > 0 it would send ETH from the Safe const txAmount = isSendingETH ? toTokenUnit(tx.amount, nativeCoin.decimals) : '0' - dispatch( - createTransaction({ - safeAddress, - to: txRecipient, - valueInWei: txAmount, - txData: data, - notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, - enqueueSnackbar, - closeSnackbar, - } as any), - ) + if (safeAddress) { + dispatch( + createTransaction({ + safeAddress: safeAddress, + to: txRecipient as string, + valueInWei: txAmount, + txData: data, + notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, + }), + ) + } else { + console.error('There was an error trying to submit the transaction, the safeAddress was not found') + } onClose() } @@ -196,4 +210,4 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { ) } -export default withSnackbar(ReviewTx) +export default ReviewTx diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/style.ts b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/style.ts index 52f70b5a..e6e4fbcf 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/style.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/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: `${md} ${lg}`, justifyContent: 'flex-start', diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx index 833f4e99..2bbb17fa 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx @@ -28,6 +28,7 @@ import { sm } from 'src/theme/variables' import ArrowDown from '../assets/arrow-down.svg' import { styles } from './style' +import { NFTToken } from 'src/logic/collectibles/sources/collectibles' const formMutators = { setMax: (args, state, utils) => { @@ -43,13 +44,28 @@ const formMutators = { const useStyles = makeStyles(styles) +type SendCollectibleProps = { + initialValues: any + onClose: () => void + onNext: (txInfo: SendCollectibleTxInfo) => void + recipientAddress?: string + selectedToken: NFTToken +} + +export type SendCollectibleTxInfo = { + assetAddress: string + assetName: string + nftTokenId: string + recipientAddress?: string +} + const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, - selectedToken = {}, -}): React.ReactElement => { + selectedToken, +}: SendCollectibleProps): React.ReactElement => { const classes = useStyles() const nftAssets = useSelector(safeActiveSelectorMap) const nftTokens = useSelector(nftTokensSelector) @@ -67,7 +83,7 @@ const SendCollectible = ({ } }, [selectedEntry, pristine]) - const handleSubmit = (values) => { + const handleSubmit = (values: SendCollectibleTxInfo) => { // If the input wasn't modified, there was no mutation of the recipientAddress if (!values.recipientAddress) { values.recipientAddress = selectedEntry?.address 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 14bdc61f..92d34615 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -47,18 +47,20 @@ const formMutators = { }, } -const useStyles = makeStyles(styles as any) +const useStyles = makeStyles(styles) + +export type SendFundsTx = { + amount?: string + recipientAddress?: string + token?: string +} type SendFundsProps = { - initialValues: { - amount?: string - recipientAddress?: string - token?: string - } + initialValues: SendFundsTx onClose: () => void onNext: (txInfo: unknown) => void - recipientAddress: string - selectedToken: string + recipientAddress?: string + selectedToken?: string } const { nativeCoin } = getNetworkInfo() diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.ts b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.ts index 44d7d9bb..776dea04 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.ts +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/style.ts @@ -1,6 +1,7 @@ import { lg, md, secondaryText } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ heading: { padding: `${md} ${lg}`, justifyContent: 'flex-start', diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx index 883690a8..88a170fa 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx @@ -1,5 +1,4 @@ -import { withStyles } from '@material-ui/core/styles' -import { withSnackbar } from 'notistack' +import { createStyles, makeStyles } from '@material-ui/core/styles' import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -14,11 +13,12 @@ import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner' import createTransaction from 'src/logic/safe/store/actions/createTransaction' -import { safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { checksumAddress } from 'src/utils/checksumAddress' import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' +import { Dispatch } from 'src/logic/safe/store/actions/types' -const styles = () => ({ +const styles = createStyles({ biggerModalWindow: { width: '775px', minHeight: '500px', @@ -26,7 +26,15 @@ const styles = () => ({ }, }) -export const sendAddOwner = async (values, safeAddress, ownersOld, enqueueSnackbar, closeSnackbar, dispatch) => { +const useStyles = makeStyles(styles) + +type OwnerValues = { + ownerAddress: string + ownerName: string + threshold: string +} + +export const sendAddOwner = async (values: OwnerValues, safeAddress: string, dispatch: Dispatch): Promise => { const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const txData = gnosisSafe.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI() @@ -37,9 +45,7 @@ export const sendAddOwner = async (values, safeAddress, ownersOld, enqueueSnackb valueInWei: '0', txData, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, - enqueueSnackbar, - closeSnackbar, - } as any), + }), ) if (txHash) { @@ -47,12 +53,17 @@ export const sendAddOwner = async (values, safeAddress, ownersOld, enqueueSnackb } } -const AddOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose }) => { +type Props = { + isOpen: boolean + onClose: () => void +} + +const AddOwner = ({ isOpen, onClose }: Props): React.ReactElement => { + const classes = useStyles() const [activeScreen, setActiveScreen] = useState('selectOwner') const [values, setValues] = useState({}) const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) - const owners = useSelector(safeOwnersSelector) useEffect( () => () => { @@ -91,7 +102,7 @@ const AddOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose }) onClose() try { - await sendAddOwner(values, safeAddress, owners, enqueueSnackbar, closeSnackbar, dispatch) + await sendAddOwner(values, safeAddress, dispatch) dispatch( addOrUpdateAddressBookEntry(makeAddressBookEntry({ name: values.ownerName, address: values.ownerAddress })), ) @@ -121,4 +132,4 @@ const AddOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose }) ) } -export default withStyles(styles as any)(withSnackbar(AddOwner)) +export default AddOwner diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx index e60d396f..347c131d 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx @@ -1,5 +1,4 @@ -import { withStyles } from '@material-ui/core/styles' -import { withSnackbar } from 'notistack' +import { createStyles, makeStyles } from '@material-ui/core/styles' import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -13,13 +12,10 @@ 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 { - safeOwnersSelector, - safeParamAddressFromStateSelector, - safeThresholdSelector, -} from 'src/logic/safe/store/selectors' +import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors' +import { Dispatch } from 'src/logic/safe/store/actions/types' -const styles = () => ({ +const styles = createStyles({ biggerModalWindow: { width: '775px', minHeight: '500px', @@ -27,17 +23,22 @@ const styles = () => ({ }, }) +const useStyles = makeStyles(styles) + +type OwnerValues = { + ownerAddress: string + ownerName: string + threshold: string +} + export const sendRemoveOwner = async ( - values, - safeAddress, - ownerAddressToRemove, - ownerNameToRemove, - ownersOld, - enqueueSnackbar, - closeSnackbar, - threshold, - dispatch, -) => { + values: OwnerValues, + safeAddress: string, + ownerAddressToRemove: string, + ownerNameToRemove: string, + dispatch: Dispatch, + threshold?: number, +): Promise => { const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const safeOwners = await gnosisSafe.methods.getOwners().call() const index = safeOwners.findIndex( @@ -53,9 +54,7 @@ export const sendRemoveOwner = async ( valueInWei: '0', txData, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, - enqueueSnackbar, - closeSnackbar, - } as any), + }), ) if (txHash && threshold === 1) { @@ -63,11 +62,18 @@ export const sendRemoveOwner = async ( } } -const RemoveOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose, ownerAddress, ownerName }) => { +type RemoveOwnerProps = { + isOpen: boolean + onClose: () => void + ownerAddress: string + ownerName: string +} + +const RemoveOwner = ({ isOpen, onClose, ownerAddress, ownerName }: RemoveOwnerProps): React.ReactElement => { + const classes = useStyles() const [activeScreen, setActiveScreen] = useState('checkOwner') const [values, setValues] = useState({}) const dispatch = useDispatch() - const owners = useSelector(safeOwnersSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector) const threshold = useSelector(safeThresholdSelector) @@ -99,17 +105,7 @@ const RemoveOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose, const onRemoveOwner = () => { onClose() - sendRemoveOwner( - values, - safeAddress, - ownerAddress, - ownerName, - owners, - enqueueSnackbar, - closeSnackbar, - threshold, - dispatch, - ) + sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, threshold) } return ( @@ -142,4 +138,4 @@ const RemoveOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose, ) } -export default withStyles(styles as any)(withSnackbar(RemoveOwner)) +export default RemoveOwner diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index 8b743074..122a20a0 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -1,5 +1,4 @@ -import { withStyles } from '@material-ui/core/styles' -import { withSnackbar } from 'notistack' +import { createStyles, makeStyles } from '@material-ui/core/styles' import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -15,8 +14,10 @@ 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' -const styles = () => ({ +const styles = createStyles({ biggerModalWindow: { width: '775px', minHeight: '500px', @@ -24,20 +25,24 @@ const styles = () => ({ }, }) +const useStyles = makeStyles(styles) + +type OwnerValues = { + ownerAddress: string + ownerName: string + threshold: string +} + export const sendReplaceOwner = async ( - values, - safeAddress, - ownerAddressToRemove, - enqueueSnackbar, - closeSnackbar, - threshold, - dispatch, -) => { + values: OwnerValues, + safeAddress: string, + ownerAddressToRemove: string, + dispatch: Dispatch, + threshold?: number, +): Promise => { const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const safeOwners = await gnosisSafe.methods.getOwners().call() - const index = safeOwners.findIndex( - (ownerAddress) => ownerAddress.toLowerCase() === ownerAddressToRemove.toLowerCase(), - ) + const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove)) const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress).encodeABI() @@ -48,9 +53,7 @@ export const sendReplaceOwner = async ( valueInWei: '0', txData, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, - enqueueSnackbar, - closeSnackbar, - } as any), + }), ) if (txHash && threshold === 1) { @@ -65,7 +68,15 @@ export const sendReplaceOwner = async ( } } -const ReplaceOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose, ownerAddress, ownerName }) => { +type ReplaceOwnerProps = { + isOpen: boolean + onClose: () => void + ownerAddress: string + ownerName: string +} + +const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwnerProps): React.ReactElement => { + const classes = useStyles() const [activeScreen, setActiveScreen] = useState('checkOwner') const [values, setValues] = useState({}) const dispatch = useDispatch() @@ -94,7 +105,7 @@ const ReplaceOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose const onReplaceOwner = async () => { onClose() try { - await sendReplaceOwner(values, safeAddress, ownerAddress, enqueueSnackbar, closeSnackbar, threshold, dispatch) + await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, threshold) dispatch( addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: values.ownerAddress, name: values.ownerName })), @@ -131,4 +142,4 @@ const ReplaceOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose ) } -export default withStyles(styles as any)(withSnackbar(ReplaceOwner)) +export default ReplaceOwner