Skip to content
This repository was archived by the owner on Nov 10, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b41ac8e
add a way to identify SpendingLimit methods and its data
fernandomg Aug 24, 2020
e10dbb2
add `data` hash to MultiSendDetails
fernandomg Aug 24, 2020
50d3931
add `NewSpendingLimitDetails` component to display SL transaction inf…
fernandomg Aug 24, 2020
79281f8
fix: avoid overflow of long strings inside multiSend details
fernandomg Aug 24, 2020
4a4d9bf
refactor: make it easy to identify what Tx is being displayed
fernandomg Aug 24, 2020
329f0b7
WIP: feature: display New Spending Limit details in txsTable
fernandomg Aug 24, 2020
b984c3d
move NewSpendingLimitDetails under TxDescription umbrella
fernandomg Aug 25, 2020
5939520
WIP: first attempt to send funds using spending limit module
fernandomg Aug 25, 2020
c466c95
Merge branch 'feature/#1039-list-SpendingLimit' into feature/#691-txL…
fernandomg Aug 26, 2020
ba7e8d3
Merge branch 'feature/#691-txList-SpendingLimit' into feature/#693-se…
fernandomg Aug 26, 2020
7bb8bae
fix: avoid signature request
fernandomg Aug 26, 2020
946b01d
Merge branch 'feature/#1039-list-SpendingLimit' into feature/#693-sen…
fernandomg Aug 28, 2020
36d96ec
fixes after merge
fernandomg Aug 28, 2020
bdb2c0a
fix send funds flow
fernandomg Aug 28, 2020
43a3150
remove "tx" from spending limit notifications
fernandomg Aug 28, 2020
a6fcf86
Merge branch 'feature/#413-SpendingLimit-in-app' into feature/#693-se…
fernandomg Sep 1, 2020
0e52cf5
Update style initialization
Sep 2, 2020
809d301
fix: use `SPENDING_LIMIT_METHODS_NAME` values
fernandomg Sep 2, 2020
4bfd280
refactor: extract SpendingLimitRow to its own component
fernandomg Sep 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 86 additions & 8 deletions src/logic/contracts/methodIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -95,15 +169,19 @@ export const decodeMethods = (data: string): DataDecoded | null => {
return decodeParamsFromSafeMethod(data)
}

if (isSpendingLimitMethod(methodId)) {
return decodeParamsFromSpendingLimit(data)
}

switch (methodId) {
// a9059cbb - transfer(address,uint256)
case '0xa9059cbb': {
const decodeParameters = web3.eth.abi.decodeParameters(['address', 'uint'], params)
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] },
],
}
}
Expand All @@ -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] },
],
}
}
Expand All @@ -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] },
],
}
}
Expand Down
24 changes: 12 additions & 12 deletions src/logic/notifications/notificationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,51 +206,51 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {

// 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 },
},

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
Loading