(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 <daniel.sanchez@gnosis.pm>
This commit is contained in:
Agustin Pane 2020-10-26 10:11:52 -03:00 committed by GitHub
parent 95d102d337
commit 058fec3dbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 367 additions and 252 deletions

View File

@ -42,7 +42,7 @@ import { Transaction, TransactionStatus, TxArgs } from 'src/logic/safe/store/mod
import { AnyAction } from 'redux' import { AnyAction } from 'redux'
import { PayableTx } from 'src/types/contracts/types.d' import { PayableTx } from 'src/types/contracts/types.d'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
import { Dispatch } from './types' import { Dispatch, DispatchReturn } from './types'
export const removeTxFromStore = ( export const removeTxFromStore = (
tx: Transaction, tx: Transaction,
@ -110,7 +110,7 @@ interface CreateTransactionArgs {
safeTxGas?: number safeTxGas?: number
} }
type CreateTransactionAction = ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction> type CreateTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
type ConfirmEventHandler = (safeTxHash: string) => void type ConfirmEventHandler = (safeTxHash: string) => void
type ErrorEventHandler = () => void type ErrorEventHandler = () => void
@ -129,7 +129,7 @@ const createTransaction = (
}: CreateTransactionArgs, }: CreateTransactionArgs,
onUserConfirm?: ConfirmEventHandler, onUserConfirm?: ConfirmEventHandler,
onError?: ErrorEventHandler, onError?: ErrorEventHandler,
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<void> => { ): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<DispatchReturn> => {
const state = getState() const state = getState()
if (navigateToTransactionsTab) { if (navigateToTransactionsTab) {

View File

@ -3,4 +3,6 @@ import { AnyAction } from 'redux'
import { AppReduxState } from 'src/store' import { AppReduxState } from 'src/store'
export type Dispatch = ThunkDispatch<AppReduxState, undefined, AnyAction> export type DispatchReturn = string | undefined
export type Dispatch = ThunkDispatch<AppReduxState, DispatchReturn, AnyAction>

View File

@ -1,6 +1,6 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import Card from '@material-ui/core/Card' 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 { useSelector } from 'react-redux'
import Item from './components/Item' import Item from './components/Item'
@ -8,11 +8,12 @@ import Item from './components/Item'
import Paragraph from 'src/components/layout/Paragraph' import Paragraph from 'src/components/layout/Paragraph'
import { activeNftAssetsListSelector, nftTokensSelector } from 'src/logic/collectibles/store/selectors' import { activeNftAssetsListSelector, nftTokensSelector } from 'src/logic/collectibles/store/selectors'
import SendModal from 'src/routes/safe/components/Balances/SendModal' 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 { fontColor, lg, screenSm, screenXs } from 'src/theme/variables'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
import { NFTToken } from 'src/logic/collectibles/sources/collectibles.d'
const useStyles = makeStyles({ const useStyles = makeStyles(
createStyles({
cardInner: { cardInner: {
boxSizing: 'border-box', boxSizing: 'border-box',
maxWidth: '100%', maxWidth: '100%',
@ -65,7 +66,7 @@ const useStyles = makeStyles({
}, },
titleFiller: { titleFiller: {
backgroundColor: '#e8e7e6', backgroundColor: '#e8e7e6',
flexGrow: '1', flexGrow: 1,
height: '2px', height: '2px',
marginLeft: '40px', marginLeft: '40px',
}, },
@ -73,13 +74,13 @@ const useStyles = makeStyles({
fontSize: lg, fontSize: lg,
textAlign: 'center', textAlign: 'center',
}, },
} as any) }),
)
const Collectibles = (): React.ReactElement => { const Collectibles = (): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const [selectedToken, setSelectedToken] = React.useState({}) const [selectedToken, setSelectedToken] = React.useState<NFTToken | undefined>()
const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false) const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false)
const { address, ethBalance, name } = useSelector(safeSelector) || {}
const nftTokens = useSelector(nftTokensSelector) const nftTokens = useSelector(nftTokensSelector)
const activeAssetsList = useSelector(activeNftAssetsListSelector) const activeAssetsList = useSelector(activeNftAssetsListSelector)
const { trackEvent } = useAnalytics() const { trackEvent } = useAnalytics()
@ -88,7 +89,7 @@ const Collectibles = (): React.ReactElement => {
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Collectibles' }) trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Collectibles' })
}, [trackEvent]) }, [trackEvent])
const handleItemSend = (nftToken) => { const handleItemSend = (nftToken: NFTToken) => {
setSelectedToken(nftToken) setSelectedToken(nftToken)
setSendNFTsModalOpen(true) setSendNFTsModalOpen(true)
} }
@ -125,11 +126,8 @@ const Collectibles = (): React.ReactElement => {
</div> </div>
<SendModal <SendModal
activeScreenType="sendCollectible" activeScreenType="sendCollectible"
ethBalance={ethBalance}
isOpen={sendNFTsModalOpen} isOpen={sendNFTsModalOpen}
onClose={() => setSendNFTsModalOpen(false)} onClose={() => setSendNFTsModalOpen(false)}
safeAddress={address}
safeName={name}
selectedToken={selectedToken} selectedToken={selectedToken}
/> />
</Card> </Card>

View File

@ -4,6 +4,14 @@ import cn from 'classnames'
import React, { Suspense, useEffect, useState } from 'react' import React, { Suspense, useEffect, useState } from 'react'
import Modal from 'src/components/Modal' 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')) 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 classes = useStyles()
const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType') const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType')
const [tx, setTx] = useState({}) const [tx, setTx] = useState<unknown>({})
const [isABI, setIsABI] = useState(true) const [isABI, setIsABI] = useState(true)
useEffect(() => { useEffect(() => {
@ -53,7 +75,7 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
const scalableModalSize = activeScreen === 'chooseTxType' const scalableModalSize = activeScreen === 'chooseTxType'
const handleTxCreation = (txInfo) => { const handleTxCreation = (txInfo: SendCollectibleTxInfo) => {
setActiveScreen('reviewTx') setActiveScreen('reviewTx')
setTx(txInfo) setTx(txInfo)
} }
@ -97,22 +119,22 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
)} )}
{activeScreen === 'sendFunds' && ( {activeScreen === 'sendFunds' && (
<SendFunds <SendFunds
initialValues={tx} initialValues={tx as SendFundsTx}
onClose={onClose} onClose={onClose}
onNext={handleTxCreation} onNext={handleTxCreation}
recipientAddress={recipientAddress} recipientAddress={recipientAddress}
selectedToken={selectedToken} selectedToken={selectedToken as string}
/> />
)} )}
{activeScreen === 'reviewTx' && ( {activeScreen === 'reviewTx' && (
<ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx} /> <ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx as ReviewTxProp} />
)} )}
{activeScreen === 'contractInteraction' && isABI && ( {activeScreen === 'contractInteraction' && isABI && (
<ContractInteraction <ContractInteraction
isABI={isABI} isABI={isABI}
switchMethod={handleSwitchMethod} switchMethod={handleSwitchMethod}
contractAddress={recipientAddress} contractAddress={recipientAddress}
initialValues={tx} initialValues={tx as ContractInteractionTx}
onClose={onClose} onClose={onClose}
onNext={handleContractInteractionCreation} onNext={handleContractInteractionCreation}
/> />
@ -122,7 +144,7 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
)} )}
{activeScreen === 'contractInteraction' && !isABI && ( {activeScreen === 'contractInteraction' && !isABI && (
<SendCustomTx <SendCustomTx
initialValues={tx} initialValues={tx as CustomTxProps}
isABI={isABI} isABI={isABI}
switchMethod={handleSwitchMethod} switchMethod={handleSwitchMethod}
onClose={onClose} onClose={onClose}
@ -131,7 +153,7 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
/> />
)} )}
{activeScreen === 'reviewCustomTx' && ( {activeScreen === 'reviewCustomTx' && (
<ReviewCustomTx onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} /> <ReviewCustomTx onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx as CustomTx} />
)} )}
{activeScreen === 'sendCollectible' && ( {activeScreen === 'sendCollectible' && (
<SendCollectible <SendCollectible
@ -139,11 +161,15 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
onClose={onClose} onClose={onClose}
onNext={handleSendCollectible} onNext={handleSendCollectible}
recipientAddress={recipientAddress} recipientAddress={recipientAddress}
selectedToken={selectedToken} selectedToken={selectedToken as NFTToken}
/> />
)} )}
{activeScreen === 'reviewCollectible' && ( {activeScreen === 'reviewCollectible' && (
<ReviewCollectible onClose={onClose} onPrev={() => setActiveScreen('sendCollectible')} tx={tx} /> <ReviewCollectible
onClose={onClose}
onPrev={() => setActiveScreen('sendCollectible')}
tx={tx as CollectibleTx}
/>
)} )}
</Suspense> </Suspense>
</Modal> </Modal>

View File

@ -23,7 +23,7 @@ type ActiveScreen = 'sendFunds' | 'sendCollectible' | 'contractInteraction'
interface ChooseTxTypeProps { interface ChooseTxTypeProps {
onClose: () => void onClose: () => void
recipientAddress: string recipientAddress?: string
setActiveScreen: React.Dispatch<React.SetStateAction<ActiveScreen>> setActiveScreen: React.Dispatch<React.SetStateAction<ActiveScreen>>
} }

View File

@ -1,9 +1,9 @@
import { makeStyles } from '@material-ui/core/styles'
import { useSnackbar } from 'notistack'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import { useDispatch, useSelector } from 'react-redux' 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 AddressInfo from 'src/components/AddressInfo' import AddressInfo from 'src/components/AddressInfo'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button' import Button from 'src/components/layout/Button'
@ -43,7 +43,6 @@ type Props = {
const { nativeCoin } = getNetworkInfo() const { nativeCoin } = getNetworkInfo()
const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactElement => { const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
const classes = useStyles() const classes = useStyles()
const dispatch = useDispatch() const dispatch = useDispatch()
const { address: safeAddress } = useSelector(safeSelector) || {} const { address: safeAddress } = useSelector(safeSelector) || {}
@ -74,18 +73,19 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
const txRecipient = tx.contractAddress const txRecipient = tx.contractAddress
const txData = tx.data ? tx.data.trim() : '' const txData = tx.data ? tx.data.trim() : ''
const txValue = tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0' const txValue = tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0'
if (safeAddress) {
dispatch( dispatch(
createTransaction({ createTransaction({
safeAddress, safeAddress,
to: txRecipient, to: txRecipient as string,
valueInWei: txValue, valueInWei: txValue,
txData, txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
enqueueSnackbar, }),
closeSnackbar,
} as any),
) )
} else {
console.error('There was an error trying to submit the transaction, the safeAddress was not found')
}
onClose() onClose()
} }

View File

@ -1,11 +1,11 @@
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' 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 CopyBtn from 'src/components/CopyBtn'
import EtherscanBtn from 'src/components/EtherscanBtn' import EtherscanBtn from 'src/components/EtherscanBtn'
import Identicon from 'src/components/Identicon' import Identicon from 'src/components/Identicon'
@ -30,10 +30,16 @@ import ArrowDown from '../../assets/arrow-down.svg'
import { styles } from './style' import { styles } from './style'
export type CustomTx = {
contractAddress?: string
data?: string
value?: string
}
type Props = { type Props = {
onClose: () => void onClose: () => void
onPrev: () => void onPrev: () => void
tx: { contractAddress?: string; data?: string; value?: string } tx: CustomTx
} }
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
@ -72,15 +78,19 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
const txData = tx.data ? tx.data.trim() : '' const txData = tx.data ? tx.data.trim() : ''
const txValue = tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0' const txValue = tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0'
if (safeAddress) {
dispatch( dispatch(
createTransaction({ createTransaction({
safeAddress: safeAddress as string, safeAddress: safeAddress,
to: txRecipient as string, to: txRecipient as string,
valueInWei: txValue, valueInWei: txValue,
txData, txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
}), }),
) )
} else {
console.error('There was an error trying to submit the transaction, the safeAddress was not found')
}
onClose() onClose()
} }

View File

@ -1,10 +1,10 @@
import React, { useState } from 'react'
import { useSelector } from 'react-redux'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import InputAdornment from '@material-ui/core/InputAdornment' import InputAdornment from '@material-ui/core/InputAdornment'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Switch from '@material-ui/core/Switch' import Switch from '@material-ui/core/Switch'
import Close from '@material-ui/icons/Close' 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 QRIcon from 'src/assets/icons/qrcode.svg'
import CopyBtn from 'src/components/CopyBtn' import CopyBtn from 'src/components/CopyBtn'
@ -40,13 +40,17 @@ export interface CreatedTx {
value: string | number value: string | number
} }
export type CustomTxProps = {
contractAddress?: string
}
type Props = { type Props = {
initialValues: { contractAddress?: string } initialValues: CustomTxProps
onClose: () => void onClose: () => void
onNext: (tx: CreatedTx, submit: boolean) => void onNext: (tx: CreatedTx, submit: boolean) => void
isABI: boolean isABI: boolean
switchMethod: () => void switchMethod: () => void
contractAddress: string contractAddress?: string
} }
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)

View File

@ -31,9 +31,13 @@ export interface CreatedTx {
value: string | number value: string | number
} }
export type ContractInteractionTx = {
contractAddress?: string
}
export interface ContractInteractionProps { export interface ContractInteractionProps {
contractAddress: string contractAddress?: string
initialValues: { contractAddress?: string } initialValues: ContractInteractionTx
isABI: boolean isABI: boolean
onClose: () => void onClose: () => void
switchMethod: () => void switchMethod: () => void

View File

@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' 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 { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
import { getNetworkInfo } from 'src/config' import { getNetworkInfo } from 'src/config'
import CopyBtn from 'src/components/CopyBtn' 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 { safeSelector } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas' import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
import { import { getERC721TokenContract } from 'src/logic/tokens/store/actions/fetchTokens'
containsMethodByHash,
getERC721TokenContract,
getHumanFriendlyToken,
} from 'src/logic/tokens/store/actions/fetchTokens'
import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH } from 'src/logic/tokens/utils/tokenHelpers' import { SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH } from 'src/logic/tokens/utils/tokenHelpers'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
@ -39,9 +35,22 @@ import { styles } from './style'
const { nativeCoin } = getNetworkInfo() 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 classes = useStyles()
const shortener = textShortener() const shortener = textShortener()
const dispatch = useDispatch() const dispatch = useDispatch()
@ -57,12 +66,11 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx
let isCurrent = true let isCurrent = true
const estimateGas = async () => { const estimateGas = async () => {
const supportsSafeTransfer = await containsMethodByHash(tx.assetAddress, SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH) try {
const methodToCall = supportsSafeTransfer ? `0x${SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH}` : 'transfer' const methodToCall = `0x${SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH}`
const transferParams = [tx.recipientAddress, tx.nftTokenId] const transferParams = [tx.recipientAddress, tx.nftTokenId]
const params = methodToCall === 'transfer' ? transferParams : [safeAddress, ...transferParams] const params = [safeAddress, ...transferParams]
const ERC721Token = await getERC721TokenContract()
const ERC721Token = methodToCall === 'transfer' ? await getHumanFriendlyToken() : await getERC721TokenContract()
const tokenInstance = await ERC721Token.at(tx.assetAddress) const tokenInstance = await ERC721Token.at(tx.assetAddress)
const txData = tokenInstance.contract.methods[methodToCall](...params).encodeABI() const txData = tokenInstance.contract.methods[methodToCall](...params).encodeABI()
@ -74,6 +82,9 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx
setGasCosts(formattedGasCosts) setGasCosts(formattedGasCosts)
setData(txData) setData(txData)
} }
} catch (error) {
console.error('Error while calculating estimated gas:', error)
}
} }
estimateGas() estimateGas()
@ -84,6 +95,8 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx
}, [safeAddress, tx.assetAddress, tx.nftTokenId, tx.recipientAddress]) }, [safeAddress, tx.assetAddress, tx.nftTokenId, tx.recipientAddress])
const submitTx = async () => { const submitTx = async () => {
try {
if (safeAddress) {
dispatch( dispatch(
createTransaction({ createTransaction({
safeAddress, safeAddress,
@ -91,12 +104,17 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx
valueInWei: '0', valueInWei: '0',
txData: data, txData: data,
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
enqueueSnackbar, }),
closeSnackbar,
} as any),
) )
} 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() onClose()
} }
}
return ( return (
<> <>
@ -180,4 +198,4 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx
) )
} }
export default withSnackbar(ReviewCollectible) export default ReviewCollectible

View File

@ -1,6 +1,7 @@
import { lg, md, secondaryText, sm } from 'src/theme/variables' import { lg, md, secondaryText, sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({ export const styles = createStyles({
heading: { heading: {
padding: `${md} ${lg}`, padding: `${md} ${lg}`,
justifyContent: 'flex-start', justifyContent: 'flex-start',

View File

@ -2,7 +2,6 @@ import IconButton from '@material-ui/core/IconButton'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import { withSnackbar } from 'notistack'
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { toTokenUnit, fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import { toTokenUnit, fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
@ -34,11 +33,24 @@ import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style' import { styles } from './style'
const useStyles = makeStyles(styles as any) const useStyles = makeStyles(styles)
const { nativeCoin } = getNetworkInfo() 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 classes = useStyles()
const dispatch = useDispatch() const dispatch = useDispatch()
const { address: safeAddress } = useSelector(safeSelector) || {} 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() 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 gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
const formattedGasCosts = formatAmount(gasCosts) 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 // if txAmount > 0 it would send ETH from the Safe
const txAmount = isSendingETH ? toTokenUnit(tx.amount, nativeCoin.decimals) : '0' const txAmount = isSendingETH ? toTokenUnit(tx.amount, nativeCoin.decimals) : '0'
if (safeAddress) {
dispatch( dispatch(
createTransaction({ createTransaction({
safeAddress, safeAddress: safeAddress,
to: txRecipient, to: txRecipient as string,
valueInWei: txAmount, valueInWei: txAmount,
txData: data, txData: data,
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
enqueueSnackbar, }),
closeSnackbar,
} as any),
) )
} else {
console.error('There was an error trying to submit the transaction, the safeAddress was not found')
}
onClose() onClose()
} }
@ -196,4 +210,4 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => {
) )
} }
export default withSnackbar(ReviewTx) export default ReviewTx

View File

@ -1,6 +1,7 @@
import { lg, md, secondaryText, sm } from 'src/theme/variables' import { lg, md, secondaryText, sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({ export const styles = createStyles({
heading: { heading: {
padding: `${md} ${lg}`, padding: `${md} ${lg}`,
justifyContent: 'flex-start', justifyContent: 'flex-start',

View File

@ -28,6 +28,7 @@ import { sm } from 'src/theme/variables'
import ArrowDown from '../assets/arrow-down.svg' import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style' import { styles } from './style'
import { NFTToken } from 'src/logic/collectibles/sources/collectibles'
const formMutators = { const formMutators = {
setMax: (args, state, utils) => { setMax: (args, state, utils) => {
@ -43,13 +44,28 @@ const formMutators = {
const useStyles = makeStyles(styles) 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 = ({ const SendCollectible = ({
initialValues, initialValues,
onClose, onClose,
onNext, onNext,
recipientAddress, recipientAddress,
selectedToken = {}, selectedToken,
}): React.ReactElement => { }: SendCollectibleProps): React.ReactElement => {
const classes = useStyles() const classes = useStyles()
const nftAssets = useSelector(safeActiveSelectorMap) const nftAssets = useSelector(safeActiveSelectorMap)
const nftTokens = useSelector(nftTokensSelector) const nftTokens = useSelector(nftTokensSelector)
@ -67,7 +83,7 @@ const SendCollectible = ({
} }
}, [selectedEntry, pristine]) }, [selectedEntry, pristine])
const handleSubmit = (values) => { const handleSubmit = (values: SendCollectibleTxInfo) => {
// If the input wasn't modified, there was no mutation of the recipientAddress // If the input wasn't modified, there was no mutation of the recipientAddress
if (!values.recipientAddress) { if (!values.recipientAddress) {
values.recipientAddress = selectedEntry?.address values.recipientAddress = selectedEntry?.address

View File

@ -47,18 +47,20 @@ const formMutators = {
}, },
} }
const useStyles = makeStyles(styles as any) const useStyles = makeStyles(styles)
type SendFundsProps = { export type SendFundsTx = {
initialValues: {
amount?: string amount?: string
recipientAddress?: string recipientAddress?: string
token?: string token?: string
} }
type SendFundsProps = {
initialValues: SendFundsTx
onClose: () => void onClose: () => void
onNext: (txInfo: unknown) => void onNext: (txInfo: unknown) => void
recipientAddress: string recipientAddress?: string
selectedToken: string selectedToken?: string
} }
const { nativeCoin } = getNetworkInfo() const { nativeCoin } = getNetworkInfo()

View File

@ -1,6 +1,7 @@
import { lg, md, secondaryText } from 'src/theme/variables' import { lg, md, secondaryText } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({ export const styles = createStyles({
heading: { heading: {
padding: `${md} ${lg}`, padding: `${md} ${lg}`,
justifyContent: 'flex-start', justifyContent: 'flex-start',

View File

@ -1,5 +1,4 @@
import { withStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import { withSnackbar } from 'notistack'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' 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 addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
import createTransaction from 'src/logic/safe/store/actions/createTransaction' 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 { checksumAddress } from 'src/utils/checksumAddress'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { Dispatch } from 'src/logic/safe/store/actions/types'
const styles = () => ({ const styles = createStyles({
biggerModalWindow: { biggerModalWindow: {
width: '775px', width: '775px',
minHeight: '500px', 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<void> => {
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const txData = gnosisSafe.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI() const txData = gnosisSafe.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI()
@ -37,9 +45,7 @@ export const sendAddOwner = async (values, safeAddress, ownersOld, enqueueSnackb
valueInWei: '0', valueInWei: '0',
txData, txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar, }),
closeSnackbar,
} as any),
) )
if (txHash) { 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 [activeScreen, setActiveScreen] = useState('selectOwner')
const [values, setValues] = useState<any>({}) const [values, setValues] = useState<any>({})
const dispatch = useDispatch() const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector)
const owners = useSelector(safeOwnersSelector)
useEffect( useEffect(
() => () => { () => () => {
@ -91,7 +102,7 @@ const AddOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose })
onClose() onClose()
try { try {
await sendAddOwner(values, safeAddress, owners, enqueueSnackbar, closeSnackbar, dispatch) await sendAddOwner(values, safeAddress, dispatch)
dispatch( dispatch(
addOrUpdateAddressBookEntry(makeAddressBookEntry({ name: values.ownerName, address: values.ownerAddress })), 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

View File

@ -1,5 +1,4 @@
import { withStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import { withSnackbar } from 'notistack'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' 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 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 { import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
safeOwnersSelector, import { Dispatch } from 'src/logic/safe/store/actions/types'
safeParamAddressFromStateSelector,
safeThresholdSelector,
} from 'src/logic/safe/store/selectors'
const styles = () => ({ const styles = createStyles({
biggerModalWindow: { biggerModalWindow: {
width: '775px', width: '775px',
minHeight: '500px', minHeight: '500px',
@ -27,17 +23,22 @@ const styles = () => ({
}, },
}) })
const useStyles = makeStyles(styles)
type OwnerValues = {
ownerAddress: string
ownerName: string
threshold: string
}
export const sendRemoveOwner = async ( export const sendRemoveOwner = async (
values, values: OwnerValues,
safeAddress, safeAddress: string,
ownerAddressToRemove, ownerAddressToRemove: string,
ownerNameToRemove, ownerNameToRemove: string,
ownersOld, dispatch: Dispatch,
enqueueSnackbar, threshold?: number,
closeSnackbar, ): Promise<void> => {
threshold,
dispatch,
) => {
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const safeOwners = await gnosisSafe.methods.getOwners().call() const safeOwners = await gnosisSafe.methods.getOwners().call()
const index = safeOwners.findIndex( const index = safeOwners.findIndex(
@ -53,9 +54,7 @@ export const sendRemoveOwner = async (
valueInWei: '0', valueInWei: '0',
txData, txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar, }),
closeSnackbar,
} as any),
) )
if (txHash && threshold === 1) { 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 [activeScreen, setActiveScreen] = useState('checkOwner')
const [values, setValues] = useState<any>({}) const [values, setValues] = useState<any>({})
const dispatch = useDispatch() const dispatch = useDispatch()
const owners = useSelector(safeOwnersSelector)
const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeAddress = useSelector(safeParamAddressFromStateSelector)
const threshold = useSelector(safeThresholdSelector) const threshold = useSelector(safeThresholdSelector)
@ -99,17 +105,7 @@ const RemoveOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose,
const onRemoveOwner = () => { const onRemoveOwner = () => {
onClose() onClose()
sendRemoveOwner( sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, threshold)
values,
safeAddress,
ownerAddress,
ownerName,
owners,
enqueueSnackbar,
closeSnackbar,
threshold,
dispatch,
)
} }
return ( return (
@ -142,4 +138,4 @@ const RemoveOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose,
) )
} }
export default withStyles(styles as any)(withSnackbar(RemoveOwner)) export default RemoveOwner

View File

@ -1,5 +1,4 @@
import { withStyles } from '@material-ui/core/styles' import { createStyles, makeStyles } from '@material-ui/core/styles'
import { withSnackbar } from 'notistack'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' 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 { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress' import { checksumAddress } from 'src/utils/checksumAddress'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' 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: { biggerModalWindow: {
width: '775px', width: '775px',
minHeight: '500px', minHeight: '500px',
@ -24,20 +25,24 @@ const styles = () => ({
}, },
}) })
const useStyles = makeStyles(styles)
type OwnerValues = {
ownerAddress: string
ownerName: string
threshold: string
}
export const sendReplaceOwner = async ( export const sendReplaceOwner = async (
values, values: OwnerValues,
safeAddress, safeAddress: string,
ownerAddressToRemove, ownerAddressToRemove: string,
enqueueSnackbar, dispatch: Dispatch,
closeSnackbar, threshold?: number,
threshold, ): Promise<void> => {
dispatch,
) => {
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const safeOwners = await gnosisSafe.methods.getOwners().call() const safeOwners = await gnosisSafe.methods.getOwners().call()
const index = safeOwners.findIndex( const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove))
(ownerAddress) => ownerAddress.toLowerCase() === ownerAddressToRemove.toLowerCase(),
)
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress).encodeABI() const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress).encodeABI()
@ -48,9 +53,7 @@ export const sendReplaceOwner = async (
valueInWei: '0', valueInWei: '0',
txData, txData,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
enqueueSnackbar, }),
closeSnackbar,
} as any),
) )
if (txHash && threshold === 1) { 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 [activeScreen, setActiveScreen] = useState('checkOwner')
const [values, setValues] = useState<any>({}) const [values, setValues] = useState<any>({})
const dispatch = useDispatch() const dispatch = useDispatch()
@ -94,7 +105,7 @@ const ReplaceOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose
const onReplaceOwner = async () => { const onReplaceOwner = async () => {
onClose() onClose()
try { try {
await sendReplaceOwner(values, safeAddress, ownerAddress, enqueueSnackbar, closeSnackbar, threshold, dispatch) await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, threshold)
dispatch( dispatch(
addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: values.ownerAddress, name: values.ownerName })), 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