(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 { 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<Promise<void>, AppReduxState, undefined, AnyAction>
type CreateTransactionAction = ThunkAction<Promise<void | string>, 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<void> => {
): CreateTransactionAction => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<DispatchReturn> => {
const state = getState()
if (navigateToTransactionsTab) {

View File

@ -3,4 +3,6 @@ import { AnyAction } from 'redux'
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 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<NFTToken | undefined>()
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 => {
</div>
<SendModal
activeScreenType="sendCollectible"
ethBalance={ethBalance}
isOpen={sendNFTsModalOpen}
onClose={() => setSendNFTsModalOpen(false)}
safeAddress={address}
safeName={name}
selectedToken={selectedToken}
/>
</Card>

View File

@ -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<unknown>({})
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' && (
<SendFunds
initialValues={tx}
initialValues={tx as SendFundsTx}
onClose={onClose}
onNext={handleTxCreation}
recipientAddress={recipientAddress}
selectedToken={selectedToken}
selectedToken={selectedToken as string}
/>
)}
{activeScreen === 'reviewTx' && (
<ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx} />
<ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx as ReviewTxProp} />
)}
{activeScreen === 'contractInteraction' && isABI && (
<ContractInteraction
isABI={isABI}
switchMethod={handleSwitchMethod}
contractAddress={recipientAddress}
initialValues={tx}
initialValues={tx as ContractInteractionTx}
onClose={onClose}
onNext={handleContractInteractionCreation}
/>
@ -122,7 +144,7 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
)}
{activeScreen === 'contractInteraction' && !isABI && (
<SendCustomTx
initialValues={tx}
initialValues={tx as CustomTxProps}
isABI={isABI}
switchMethod={handleSwitchMethod}
onClose={onClose}
@ -131,7 +153,7 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
/>
)}
{activeScreen === 'reviewCustomTx' && (
<ReviewCustomTx onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} />
<ReviewCustomTx onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx as CustomTx} />
)}
{activeScreen === 'sendCollectible' && (
<SendCollectible
@ -139,11 +161,15 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
onClose={onClose}
onNext={handleSendCollectible}
recipientAddress={recipientAddress}
selectedToken={selectedToken}
selectedToken={selectedToken as NFTToken}
/>
)}
{activeScreen === 'reviewCollectible' && (
<ReviewCollectible onClose={onClose} onPrev={() => setActiveScreen('sendCollectible')} tx={tx} />
<ReviewCollectible
onClose={onClose}
onPrev={() => setActiveScreen('sendCollectible')}
tx={tx as CollectibleTx}
/>
)}
</Suspense>
</Modal>

View File

@ -23,7 +23,7 @@ type ActiveScreen = 'sendFunds' | 'sendCollectible' | 'contractInteraction'
interface ChooseTxTypeProps {
onClose: () => void
recipientAddress: string
recipientAddress?: string
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 { 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()
}

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<void> => {
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<any>({})
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

View File

@ -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<void> => {
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<any>({})
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

View File

@ -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<void> => {
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<any>({})
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