diff --git a/src/logic/contracts/methodIds.ts b/src/logic/contracts/methodIds.ts index e24a7e6621..02924ac7cc 100644 --- a/src/logic/contracts/methodIds.ts +++ b/src/logic/contracts/methodIds.ts @@ -80,10 +80,84 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => } } +export const SPENDING_LIMIT_METHODS_NAMES = { + ADD_DELEGATE: 'addDelegate', + SET_ALLOWANCE: 'setAllowance', + EXECUTE_ALLOWANCE_TRANSFER: 'executeAllowanceTransfer', +} + +export const SPENDING_LIMIT_METHOD_TO_ID = { + '0xe71bdf41': SPENDING_LIMIT_METHODS_NAMES.ADD_DELEGATE, + '0xbeaeb388': SPENDING_LIMIT_METHODS_NAMES.SET_ALLOWANCE, + '0x4515641a': SPENDING_LIMIT_METHODS_NAMES.EXECUTE_ALLOWANCE_TRANSFER, +} + +export const isSetAllowanceMethod = (data: string): boolean => { + const methodId = data.slice(0, 10) as keyof typeof SPENDING_LIMIT_METHOD_TO_ID + return SPENDING_LIMIT_METHOD_TO_ID[methodId] === SPENDING_LIMIT_METHODS_NAMES.SET_ALLOWANCE +} + +export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | null => { + const [methodId, params] = [data.slice(0, 10) as keyof typeof SPENDING_LIMIT_METHOD_TO_ID | string, data.slice(10)] + + switch (methodId) { + // addDelegate + case '0xe71bdf41': { + const decodedParameter = (web3.eth.abi.decodeParameter('address', params) as unknown) as string + return { + method: SPENDING_LIMIT_METHOD_TO_ID[methodId], + parameters: [ + { name: 'delegate', type: 'address', value: decodedParameter }, + ], + } + } + + // setAllowance + case '0xbeaeb388': { + const decodedParameters = web3.eth.abi.decodeParameters(['address', 'address', 'uint96', 'uint16', 'uint32'], params) + return { + method: SPENDING_LIMIT_METHOD_TO_ID[methodId], + parameters: [ + { name: 'delegate', type: 'address', value: decodedParameters[0] }, + { name: 'token', type: 'address', value: decodedParameters[1] }, + { name: 'allowanceAmount', type: 'uint96', value: decodedParameters[2] }, + { name: 'resetTimeMin', type: 'uint16', value: decodedParameters[3] }, + { name: 'resetBaseMin', type: 'uint32', value: decodedParameters[4] }, + ], + } + } + + // executeAllowanceTransfer + case '0x4515641a': { + const decodedParameters = web3.eth.abi.decodeParameters(['address', 'address', 'address', 'uint96', 'address', 'uint96', 'address', 'bytes'], params) + return { + method: SPENDING_LIMIT_METHOD_TO_ID[methodId], + parameters: [ + { name: "safe", type: "address", value: decodedParameters[0] }, + { name: "token", type: "address", value: decodedParameters[1] }, + { name: "to", type: "address", value: decodedParameters[2] }, + { name: "amount", type: "uint96", value: decodedParameters[3] }, + { name: "paymentToken", type: "address", value: decodedParameters[4] }, + { name: "payment", type: "uint96", value: decodedParameters[5] }, + { name: "delegate", type: "address", value: decodedParameters[6] }, + { name: "signature", type: "bytes", value: decodedParameters[7] } + ] + } + } + + default: + return null + } +} + const isSafeMethod = (methodId: string): boolean => { return !!METHOD_TO_ID[methodId] } +const isSpendingLimitMethod = (methodId: string): boolean => { + return !!SPENDING_LIMIT_METHOD_TO_ID[methodId] +} + export const decodeMethods = (data: string): DataDecoded | null => { if(!data.length) { return null @@ -95,6 +169,10 @@ export const decodeMethods = (data: string): DataDecoded | null => { return decodeParamsFromSafeMethod(data) } + if (isSpendingLimitMethod(methodId)) { + return decodeParamsFromSpendingLimit(data) + } + switch (methodId) { // a9059cbb - transfer(address,uint256) case '0xa9059cbb': { @@ -102,8 +180,8 @@ export const decodeMethods = (data: string): DataDecoded | null => { return { method: 'transfer', parameters: [ - { name: 'to', type: '', value: decodeParameters[0] }, - { name: 'value', type: '', value: decodeParameters[1] }, + { name: 'to', type: 'address', value: decodeParameters[0] }, + { name: 'value', type: 'uint', value: decodeParameters[1] }, ], } } @@ -114,9 +192,9 @@ export const decodeMethods = (data: string): DataDecoded | null => { return { method: 'transferFrom', parameters: [ - { name: 'from', type: '', value: decodeParameters[0] }, - { name: 'to', type: '', value: decodeParameters[1] }, - { name: 'value', type: '', value: decodeParameters[2] }, + { name: 'from', type: 'address', value: decodeParameters[0] }, + { name: 'to', type: 'address', value: decodeParameters[1] }, + { name: 'value', type: 'uint', value: decodeParameters[2] }, ], } } @@ -127,9 +205,9 @@ export const decodeMethods = (data: string): DataDecoded | null => { return { method: 'safeTransferFrom', parameters: [ - { name: 'from', type: '', value: decodedParameters[0] }, - { name: 'to', type: '', value: decodedParameters[1] }, - { name: 'value', type: '', value: decodedParameters[2] }, + { name: 'from', type: 'address', value: decodedParameters[0] }, + { name: 'to', type: 'address', value: decodedParameters[1] }, + { name: 'value', type: 'uint', value: decodedParameters[2] }, ], } } diff --git a/src/logic/notifications/notificationTypes.ts b/src/logic/notifications/notificationTypes.ts index 9a21310b71..b9f6c2dac9 100644 --- a/src/logic/notifications/notificationTypes.ts +++ b/src/logic/notifications/notificationTypes.ts @@ -206,51 +206,51 @@ export const NOTIFICATIONS: Record = { // Spending Limit SIGN_NEW_SPENDING_LIMIT_MSG: { - message: 'Please sign the new Spending Limit tx', + message: 'Please sign the new Spending Limit', options: { variant: INFO, persist: true }, }, NEW_SPENDING_LIMIT_PENDING_MSG: { - message: 'New Spending Limit tx pending', + message: 'New Spending Limit pending', options: { variant: INFO, persist: true }, }, NEW_SPENDING_LIMIT_REJECTED_MSG: { - message: 'New Spending Limit tx rejected', + message: 'New Spending Limit rejected', options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, }, NEW_SPENDING_LIMIT_EXECUTED_MSG: { - message: 'New Spending Limit tx successfully executed', + message: 'New Spending Limit successfully executed', options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, }, NEW_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: { - message: 'New Spending Limit tx successfully created. More confirmations needed to execute', + message: 'New Spending Limit successfully created. More confirmations needed to execute', options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, }, NEW_SPENDING_LIMIT_FAILED_MSG: { - message: 'New Spending Limit tx failed', + message: 'New Spending Limit failed', options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, }, SIGN_REMOVE_SPENDING_LIMIT_MSG: { - message: 'Please sign the remove Spending Limit tx', + message: 'Please sign the remove Spending Limit', options: { variant: INFO, persist: true }, }, REMOVE_SPENDING_LIMIT_PENDING_MSG: { - message: 'Remove Spending Limit tx pending', + message: 'Remove Spending Limit pending', options: { variant: INFO, persist: true }, }, REMOVE_SPENDING_LIMIT_REJECTED_MSG: { - message: 'Remove Spending Limit tx rejected', + message: 'Remove Spending Limit rejected', options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, }, REMOVE_SPENDING_LIMIT_EXECUTED_MSG: { - message: 'Remove Spending Limit tx successfully executed', + message: 'Remove Spending Limit successfully executed', options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, }, REMOVE_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: { - message: 'Remove Spending Limit tx successfully created. More confirmations needed to execute', + message: 'Remove Spending Limit successfully created. More confirmations needed to execute', options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration }, }, REMOVE_SPENDING_LIMIT_FAILED_MSG: { - message: 'Remove Spending Limit tx failed', + message: 'Remove Spending Limit failed', options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, }, 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 b6de2579c6..0db49ac7ee 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx @@ -1,15 +1,11 @@ +import StandardToken from '@gnosis.pm/util-contracts/build/contracts/GnosisStandardToken.json' 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, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import ArrowDown from '../assets/arrow-down.svg' - -import { styles } from './style' - import CopyBtn from 'src/components/CopyBtn' import EtherscanBtn from 'src/components/EtherscanBtn' import Identicon from 'src/components/Identicon' @@ -20,21 +16,28 @@ import Hairline from 'src/components/layout/Hairline' import Img from 'src/components/layout/Img' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' +import { getSpendingLimitContract } from 'src/logic/contracts/safeContracts' +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/gasNew' -import { getHumanFriendlyToken } from 'src/logic/tokens/store/actions/fetchTokens' import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { getWeb3 } from 'src/logic/wallets/getWeb3' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' +import { toTokenUnit } from 'src/routes/safe/components/Settings/SpendingLimit/utils' import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' -import createTransaction from 'src/logic/safe/store/actions/createTransaction' -import { safeSelector } from 'src/logic/safe/store/selectors' import { sm } from 'src/theme/variables' +import { AbiItem } from 'web3-utils' + +import ArrowDown from '../assets/arrow-down.svg' -const useStyles = makeStyles(styles as any) +import { styles } from './style' + +const useStyles = makeStyles(styles) const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { const classes = useStyles() @@ -44,25 +47,28 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { const [gasCosts, setGasCosts] = useState('< 0.001') const [data, setData] = useState('') - const txToken = tokens.find((token) => token.address === tx.token) - const isSendingETH = txToken.address === ETH_ADDRESS - const txRecipient = isSendingETH ? tx.recipientAddress : txToken.address + const txToken = React.useMemo(() => tokens.find((token) => token.address === tx.token), [tokens, tx.token]) + const isSendingETH = React.useMemo(() => txToken.address === ETH_ADDRESS, [txToken.address]) + const txRecipient = React.useMemo(() => (isSendingETH ? tx.recipientAddress : txToken.address), [ + isSendingETH, + tx.recipientAddress, + txToken.address, + ]) useEffect(() => { let isCurrent = true const estimateGas = async () => { - const { fromWei, toBN } = getWeb3().utils + const web3 = getWeb3() + const { fromWei, toBN } = web3.utils let txData = EMPTY_DATA - if (!isSendingETH) { - const StandardToken = await getHumanFriendlyToken() - const tokenInstance = await StandardToken.at(txToken.address) - const decimals = await tokenInstance.decimals() - const txAmount = new BigNumber(tx.amount).times(10 ** decimals.toNumber()).toString() - - txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI() + if (!isSendingETH && txToken) { + const ERC20Instance = new web3.eth.Contract(StandardToken.abi as AbiItem[], txToken.address) + txData = ERC20Instance.methods + .transfer(tx.recipientAddress, toTokenUnit(tx.amount, txToken.decimals)) + .encodeABI() } const estimatedGasCosts = await estimateTxGasCosts(safeAddress, txRecipient, txData) @@ -80,27 +86,46 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { return () => { isCurrent = false } - }, [isSendingETH, safeAddress, tx.amount, tx.recipientAddress, txRecipient, txToken.address]) + }, [isSendingETH, safeAddress, tx.amount, tx.recipientAddress, txRecipient, txToken]) const submitTx = async () => { + const isSpendingLimit = tx.txType === 'spendingLimit' const web3 = getWeb3() // txAmount should be 0 if we send tokens // the real value is encoded in txData and will be used by the contract // if txAmount > 0 it would send ETH from the Safe const txAmount = isSendingETH ? web3.utils.toWei(tx.amount, 'ether') : '0' - dispatch( - createTransaction({ - safeAddress, - to: txRecipient, - valueInWei: txAmount, - txData: data, - notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, - enqueueSnackbar, - closeSnackbar, - } as any), - ) - onClose() + if (isSpendingLimit) { + const spendingLimit = getSpendingLimitContract() + spendingLimit.methods + .executeAllowanceTransfer( + safeAddress, + txToken.address === ETH_ADDRESS ? ZERO_ADDRESS : txToken.address, + tx.recipientAddress, + toTokenUnit(tx.amount, txToken.decimals), + ZERO_ADDRESS, + 0, + tx.tokenSpendingLimit.delegate, + EMPTY_DATA, + ) + .send({ from: tx.tokenSpendingLimit.delegate }) + .on('transactionHash', () => onClose()) + .catch(console.error) + } else { + dispatch( + createTransaction({ + safeAddress, + to: txRecipient, + valueInWei: txAmount, + txData: data, + notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, + enqueueSnackbar, + closeSnackbar, + } as any), + ) + onClose() + } } return ( 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 52f70b5abc..abd3cbf013 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 { createStyles } from '@material-ui/core/styles' import { lg, md, secondaryText, sm } from 'src/theme/variables' -export const styles = () => ({ +export const styles = createStyles({ heading: { padding: `${md} ${lg}`, justifyContent: 'flex-start', diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/SpendingLimitRow.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/SpendingLimitRow.tsx new file mode 100644 index 0000000000..267d612a36 --- /dev/null +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/SpendingLimitRow.tsx @@ -0,0 +1,61 @@ +import { RadioButtons, Text } from '@gnosis.pm/safe-react-components' +import { BigNumber } from 'bignumber.js' +import React from 'react' +import styled from 'styled-components' + +import Field from 'src/components/forms/Field' +import Col from 'src/components/layout/Col' +import Row from 'src/components/layout/Row' +import { SpendingLimit } from 'src/logic/safe/store/models/safe' +import { Token } from 'src/logic/tokens/store/model/token' +import { fromTokenUnit } from 'src/routes/safe/components/Settings/SpendingLimit/utils' + +// TODO: propose refactor in safe-react-components based on this requirements +const SpendingLimitRadioButtons = styled(RadioButtons)` + & .MuiRadio-colorPrimary.Mui-checked { + color: ${({ theme }) => theme.colors.primary}; + } +` + +interface SpendingLimitRowProps { + onOptionSelect: () => void + tokenSpendingLimit: SpendingLimit + selectedToken: Token +} + +export const SpendingLimitRow = ({ + onOptionSelect, + tokenSpendingLimit, + selectedToken, +}: SpendingLimitRowProps): React.ReactElement => { + const availableAmount = React.useMemo(() => { + return fromTokenUnit( + new BigNumber(tokenSpendingLimit.amount).minus(tokenSpendingLimit.spent).toString(), + selectedToken.decimals, + ) + }, [selectedToken.decimals, tokenSpendingLimit.amount, tokenSpendingLimit.spent]) + + return ( + + + Send as + + {({ input: { name, value } }) => ( + + )} + + + + ) +} 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 8b78fc79b4..d92d9458c3 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -2,22 +2,18 @@ import IconButton from '@material-ui/core/IconButton' import InputAdornment from '@material-ui/core/InputAdornment' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' +import { BigNumber } from 'bignumber.js' import React, { useState } from 'react' import { OnChange } from 'react-final-form-listeners' import { useSelector } from 'react-redux' -import ArrowDown from '../assets/arrow-down.svg' - -import { styles } from './style' - import CopyBtn from 'src/components/CopyBtn' import EtherscanBtn from 'src/components/EtherscanBtn' -import Identicon from 'src/components/Identicon' -import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import Field from 'src/components/forms/Field' import GnoForm from 'src/components/forms/GnoForm' import TextField from 'src/components/forms/TextField' -import { composeValidators, minValue, maxValue, mustBeFloat, required } from 'src/components/forms/validator' +import { composeValidators, maxValue, minValue, mustBeFloat, required } from 'src/components/forms/validator' +import Identicon from 'src/components/Identicon' import Block from 'src/components/layout/Block' import Button from 'src/components/layout/Button' import ButtonLink from 'src/components/layout/ButtonLink' @@ -25,14 +21,23 @@ import Col from 'src/components/layout/Col' import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' +import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import { getAddressBook } from 'src/logic/addressBook/store/selectors' import { getNameFromAdbk } from 'src/logic/addressBook/utils' +import { safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors' +import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { userAccountSelector } from 'src/logic/wallets/store/selectors' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' +import { SpendingLimitRow } from 'src/routes/safe/components/Balances/SendModal/screens/SendFunds/SpendingLimitRow' import TokenSelectField from 'src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField' +import { fromTokenUnit } from 'src/routes/safe/components/Settings/SpendingLimit/utils' import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' import { sm } from 'src/theme/variables' +import ArrowDown from 'src/routes/safe/components/Balances/SendModal/screens/assets/arrow-down.svg' +import { styles } from './style' const formMutators = { setMax: (args, state, utils) => { @@ -44,9 +49,12 @@ const formMutators = { setRecipient: (args, state, utils) => { utils.changeValue(state, 'recipientAddress', () => args[0]) }, + setTxType: (args, state, utils) => { + utils.changeValue(state, 'txType', () => args[0]) + }, } -const useStyles = makeStyles(styles as any) +const useStyles = makeStyles(styles) const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = '' }): React.ReactElement => { const classes = useStyles() @@ -60,21 +68,25 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT const [pristine, setPristine] = useState(true) const [isValidAddress, setIsValidAddress] = useState(true) - React.useMemo(() => { + React.useEffect(() => { if (selectedEntry === null && pristine) { setPristine(false) } }, [selectedEntry, pristine]) + let tokenSpendingLimit const handleSubmit = (values) => { const submitValues = values // If the input wasn't modified, there was no mutation of the recipientAddress if (!values.recipientAddress) { submitValues.recipientAddress = selectedEntry.address } - onNext(submitValues) + onNext({ ...submitValues, tokenSpendingLimit }) } + const spendingLimits = useSelector(safeSpendingLimitsSelector) + const currentUser = useSelector(userAccountSelector) + return ( <> @@ -91,8 +103,16 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT {(...args) => { const formState = args[2] const mutators = args[3] - const { token: tokenAddress } = formState.values - const selectedTokenRecord = tokens.find((token) => token.address === tokenAddress) + const { token: tokenAddress, txType } = formState.values + const selectedTokenRecord = tokens?.find((token) => token.address === tokenAddress) + tokenSpendingLimit = + selectedTokenRecord && + spendingLimits?.find( + ({ delegate, token }) => + delegate.toLowerCase() === currentUser.toLowerCase() && + (token === ZERO_ADDRESS ? ETH_ADDRESS : token.toLowerCase()) === + selectedTokenRecord.address.toLowerCase(), + ) const handleScan = (value, closeQrModal) => { let scannedAddress = value @@ -198,13 +218,36 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT /> + {tokenSpendingLimit && ( + + )} Amount mutators.setMax(selectedTokenRecord.balance)} + onClick={() => + mutators.setMax( + tokenSpendingLimit && txType === 'spendingLimit' + ? new BigNumber(selectedTokenRecord.balance).gt( + fromTokenUnit( + new BigNumber(tokenSpendingLimit.amount).minus(tokenSpendingLimit.spent).toString(), + selectedTokenRecord.decimals, + ), + ) + ? fromTokenUnit( + new BigNumber(tokenSpendingLimit.amount).minus(tokenSpendingLimit.spent).toString(), + selectedTokenRecord.decimals, + ) + : selectedTokenRecord.balance + : selectedTokenRecord.balance, + ) + } weight="bold" testId="send-max-btn" > @@ -230,7 +273,21 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT required, mustBeFloat, minValue(0, false), - maxValue(selectedTokenRecord?.balance), + maxValue( + tokenSpendingLimit && txType === 'spendingLimit' + ? new BigNumber(selectedTokenRecord.balance).gt( + fromTokenUnit( + new BigNumber(tokenSpendingLimit.amount).minus(tokenSpendingLimit.spent).toString(), + selectedTokenRecord.decimals, + ), + ) + ? fromTokenUnit( + new BigNumber(tokenSpendingLimit.amount).minus(tokenSpendingLimit.spent).toString(), + selectedTokenRecord.decimals, + ) + : selectedTokenRecord.balance + : selectedTokenRecord?.balance, + ), )} /> 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 44d7d9bb97..5be884dc94 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 { createStyles } from '@material-ui/core/styles' import { lg, md, secondaryText } from 'src/theme/variables' -export const styles = () => ({ +export const styles = createStyles({ heading: { padding: `${md} ${lg}`, justifyContent: 'flex-start', diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx index a4a8199c16..b11a21b978 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx @@ -1,6 +1,11 @@ import { IconText, Text } from '@gnosis.pm/safe-react-components' import { makeStyles } from '@material-ui/core/styles' import React from 'react' +import Col from 'src/components/layout/Col' +import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime' +import useToken from 'src/routes/safe/components/Settings/SpendingLimit/hooks/useToken' +import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' +import { fromTokenUnit } from 'src/routes/safe/components/Settings/SpendingLimit/utils' import styled from 'styled-components' import { styles } from './styles' @@ -24,6 +29,7 @@ import { shortVersionOf } from 'src/logic/wallets/ethAddresses' import { Transaction } from 'src/logic/safe/store/models/types/transaction' import { DataDecoded } from 'src/routes/safe/store/models/types/transactions.d' import DividerLine from 'src/components/DividerLine' +import { decodeMethods, isSetAllowanceMethod } from 'src/logic/contracts/methodIds' export const TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID = 'tx-description-custom-value' export const TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID = 'tx-description-custom-data' @@ -40,6 +46,7 @@ const TxDetailsMethodParam = styled.div` ` const TxDetailsContent = styled.div` padding: 8px 8px 8px 16px; + overflow-wrap: break-word; ` const TxInfo = styled.div` @@ -63,9 +70,54 @@ const TxInfoDetails = ({ data }: { data: DataDecoded }): React.ReactElement => ( ) +const SpendingLimitDetailsContainer = styled.div` + padding-left: 24px; +` + +interface NewSpendingLimitDetailsProps { + data: DataDecoded +} + +const NewSpendingLimitDetails = ({ data }: NewSpendingLimitDetailsProps): React.ReactElement => { + const [beneficiary, tokenAddress, amount, resetTimeMin] = React.useMemo( + () => data.parameters.map(({ value }) => value), + [data.parameters], + ) + + const resetTimeLabel = React.useMemo( + () => RESET_TIME_OPTIONS.find(({ value }) => +value === +resetTimeMin / 24 / 60)?.label ?? '', + [resetTimeMin], + ) + + const tokenInfo = useToken(tokenAddress) + + return ( + <> + + New Spending Limit: + + + + + + + {tokenInfo && ( + + )} + + + + + + + ) +} + const MultiSendCustomDataAction = ({ tx, order }: { tx: MultiSendDetails; order: number }): React.ReactElement => { const classes = useStyles() - const methodName = tx.data?.method ? ` (${tx.data.method})` : '' + const methodName = tx.dataDecoded?.method ? ` (${tx.dataDecoded.method})` : '' + const data = tx.dataDecoded ?? decodeMethods(tx.data) + const isNewSpendingLimit = isSetAllowanceMethod(tx.data || '') return ( } > - - - Send {humanReadableValue(tx.value)} ETH to: - - - - {!!tx.data && } - + {isNewSpendingLimit ? ( + + + + ) : ( + + + Send {humanReadableValue(tx.value)} ETH to: + + + + {!!data ? : } + + )} ) } @@ -158,6 +216,21 @@ const TxActionData = ({ dataDecoded }: { dataDecoded: DataDecoded }): React.Reac ) } +interface HexEncodedDataProps { + data: string +} + +const HexEncodedData = ({ data }: HexEncodedDataProps): React.ReactElement => { + const classes = useStyles() + + return ( + + Data (hex encoded): + + + ) +} + interface GenericCustomDataProps { amount?: string data: string @@ -166,10 +239,13 @@ interface GenericCustomDataProps { } const GenericCustomData = ({ amount = '0', data, recipient, storedTx }: GenericCustomDataProps): React.ReactElement => { - const classes = useStyles() const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient)) + const txData = storedTx?.dataDecoded ?? decodeMethods(data) + const isNewSpendingLimit = isSetAllowanceMethod(data || '') - return ( + return isNewSpendingLimit ? ( + + ) : ( Send {amount} to: @@ -180,12 +256,7 @@ const GenericCustomData = ({ amount = '0', data, recipient, storedTx }: GenericC )} - {!!storedTx?.dataDecoded && } - - - Data (hex encoded): - - + {!!txData ? : } ) } diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/index.tsx index 5a99fbb520..7013b04095 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/index.tsx @@ -1,53 +1,51 @@ import { makeStyles } from '@material-ui/core/styles' import React from 'react' -import { styles } from './styles' -import { getTxData } from './utils' -import SettingsDescription from './SettingsDescription' +import Block from 'src/components/layout/Block' +import { Transaction, TransactionTypes } from 'src/logic/safe/store/models/types/transaction' +import { getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns' + import CustomDescription from './CustomDescription' +import SettingsDescription from './SettingsDescription' +import { styles } from './styles' import TransferDescription from './TransferDescription' - -import { getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns' -import Block from 'src/components/layout/Block' -import { Transaction } from 'src/logic/safe/store/models/types/transaction' +import { getTxData } from './utils' export const TRANSACTIONS_DESC_SEND_TEST_ID = 'tx-description-send' const useStyles = makeStyles(styles) +const SettingsDescriptionTx = ({ tx }: { tx: Transaction }): React.ReactElement => { + const { action, addedOwner, module, newThreshold, removedOwner } = getTxData(tx) + return +} + +const CustomDescriptionTx = ({ tx }: { tx: Transaction }): React.ReactElement => { + const amount = getTxAmount(tx, false) + const { data, recipient } = getTxData(tx) + return +} + +const UpgradeDescriptionTx = ({ tx }: { tx: Transaction }): React.ReactElement => { + const { data } = getTxData(tx) + return
{data}
+} + +const TransferDescriptionTx = ({ tx }: { tx: Transaction }): React.ReactElement => { + const amount = getTxAmount(tx, false) + const { recipient } = getTxData(tx) + return +} + const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => { const classes = useStyles() - const { - action, - addedOwner, - cancellationTx, - creationTx, - customTx, - data, - modifySettingsTx, - module, - newThreshold, - recipient, - removedOwner, - upgradeTx, - }: any = getTxData(tx) - const amount = getTxAmount(tx, false) + return ( - {modifySettingsTx && action && ( - - )} - {!upgradeTx && customTx && } - {upgradeTx &&
{data}
} - {!cancellationTx && !modifySettingsTx && !customTx && !creationTx && !upgradeTx && ( - - )} + {tx.type === TransactionTypes.SETTINGS && } + {tx.type === TransactionTypes.CUSTOM && } + {tx.type === TransactionTypes.UPGRADE && } + {[TransactionTypes.TOKEN, TransactionTypes.COLLECTIBLE].includes(tx.type) && }
) } diff --git a/src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts b/src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts index 461051c49b..7e4237c18e 100644 --- a/src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts +++ b/src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts @@ -19,7 +19,8 @@ import { Transaction } from 'src/logic/safe/store/models/types/transaction' export type MultiSendDetails = { operation: Operation to: string - data: DataDecoded | null + data: string | null + dataDecoded: DataDecoded | null value: number } @@ -48,7 +49,8 @@ export const extractMultiSendDetails = (parameter: Parameter): MultiSendDetails[ operation: valueDecoded.operation, to: valueDecoded.to, value: valueDecoded.value, - data: valueDecoded?.dataDecoded ?? null, + dataDecoded: valueDecoded?.dataDecoded ?? null, + data: valueDecoded?.data ?? null, } }) }