diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 40b6d1f7..69ff6ecc 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -17,7 +17,7 @@ const useStyles = makeStyles( position: 'absolute', top: '120px', width: '500px', - height: '540px', + height: '580px', borderRadius: sm, backgroundColor: '#ffffff', boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)', diff --git a/src/components/TransactionFailText/index.tsx b/src/components/TransactionFailText/index.tsx new file mode 100644 index 00000000..d61664cc --- /dev/null +++ b/src/components/TransactionFailText/index.tsx @@ -0,0 +1,56 @@ +import { createStyles, makeStyles } from '@material-ui/core' +import { sm } from 'src/theme/variables' +import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' +import Row from 'src/components/layout/Row' +import Paragraph from 'src/components/layout/Paragraph' +import Img from 'src/components/layout/Img' +import InfoIcon from 'src/assets/icons/info_red.svg' +import React from 'react' +import { useSelector } from 'react-redux' +import { safeThresholdSelector } from 'src/logic/safe/store/selectors' + +const styles = createStyles({ + executionWarningRow: { + display: 'flex', + alignItems: 'center', + }, + warningIcon: { + marginRight: sm, + }, +}) + +const useStyles = makeStyles(styles) + +type TransactionFailTextProps = { + txEstimationExecutionStatus: EstimationStatus + isExecution: boolean +} + +export const TransactionFailText = ({ + txEstimationExecutionStatus, + isExecution, +}: TransactionFailTextProps): React.ReactElement | null => { + const classes = useStyles() + const threshold = useSelector(safeThresholdSelector) + + if (txEstimationExecutionStatus !== EstimationStatus.FAILURE) { + return null + } + + let errorMessage = 'To save gas costs, avoid creating the transaction.' + if (isExecution) { + errorMessage = + threshold && threshold > 1 + ? `To save gas costs, cancel this transaction` + : `To save gas costs, avoid executing the transaction.` + } + + return ( + + + Info Tooltip + This transaction will most likely fail. {errorMessage} + + + ) +} diff --git a/src/components/TransactionsFees/index.tsx b/src/components/TransactionsFees/index.tsx new file mode 100644 index 00000000..c5e54f19 --- /dev/null +++ b/src/components/TransactionsFees/index.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas' +import Paragraph from 'src/components/layout/Paragraph' +import { getNetworkInfo } from 'src/config' +import { TransactionFailText } from 'src/components/TransactionFailText' + +type TransactionFailTextProps = { + txEstimationExecutionStatus: EstimationStatus + gasCostFormatted: string + isExecution: boolean + isCreation: boolean + isOffChainSignature: boolean +} +const { nativeCoin } = getNetworkInfo() + +export const TransactionFees = ({ + gasCostFormatted, + isExecution, + isCreation, + isOffChainSignature, + txEstimationExecutionStatus, +}: TransactionFailTextProps): React.ReactElement | null => { + let transactionAction + if (isCreation) { + transactionAction = 'create' + } else if (isExecution) { + transactionAction = 'execute' + } else { + transactionAction = 'approve' + } + + return ( + <> + + You're about to {transactionAction} a transaction and will have to confirm it with your currently connected + wallet. + {!isOffChainSignature && + ` Make sure you have ${gasCostFormatted} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`} + + + + ) +} diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx new file mode 100644 index 00000000..0acb86d8 --- /dev/null +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -0,0 +1,241 @@ +import { useEffect, useState } from 'react' +import { + estimateGasForTransactionApproval, + estimateGasForTransactionCreation, + estimateGasForTransactionExecution, +} from 'src/logic/safe/transactions/gas' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { formatAmount } from 'src/logic/tokens/utils/formatAmount' +import { calculateGasPrice } from 'src/logic/wallets/ethTransactions' +import { getNetworkInfo } from 'src/config' +import { useSelector } from 'react-redux' +import { + safeCurrentVersionSelector, + safeParamAddressFromStateSelector, + safeThresholdSelector, +} from 'src/logic/safe/store/selectors' +import { CALL } from 'src/logic/safe/transactions' +import { providerSelector } from '../wallets/store/selectors' + +import { List } from 'immutable' +import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' +import { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' + +export enum EstimationStatus { + LOADING = 'LOADING', + FAILURE = 'FAILURE', + SUCCESS = 'SUCCESS', +} + +const checkIfTxIsExecution = (threshold: number, preApprovingOwner?: string, txConfirmations?: number): boolean => + txConfirmations === threshold || !!preApprovingOwner || threshold === 1 + +const checkIfTxIsApproveAndExecution = (threshold: number, txConfirmations: number): boolean => + txConfirmations + 1 === threshold + +const checkIfTxIsCreation = (txConfirmations: number): boolean => txConfirmations === 0 + +type TransactionEstimationProps = { + txData: string + safeAddress: string + txRecipient: string + txConfirmations?: List + txAmount?: string + operation?: number + gasPrice?: string + gasToken?: string + refundReceiver?: string // Address of receiver of gas payment (or 0 if tx.origin). + safeTxGas?: number + from?: string + isExecution: boolean + isCreation: boolean + isOffChainSignature?: boolean + approvalAndExecution?: boolean +} + +const estimateTransactionGas = async ({ + txData, + safeAddress, + txRecipient, + txConfirmations, + txAmount, + operation, + gasPrice, + gasToken, + refundReceiver, + safeTxGas, + from, + isExecution, + isCreation, + isOffChainSignature = false, + approvalAndExecution, +}: TransactionEstimationProps): Promise => { + if (isCreation) { + return estimateGasForTransactionCreation(safeAddress, txData, txRecipient, txAmount || '0', operation || CALL) + } + + if (!from) { + throw new Error('No from provided for approving or execute transaction') + } + + if (isExecution) { + return estimateGasForTransactionExecution({ + safeAddress, + txRecipient, + txConfirmations, + txAmount: txAmount || '0', + txData, + operation: operation || CALL, + from, + gasPrice: gasPrice || '0', + gasToken: gasToken || ZERO_ADDRESS, + refundReceiver: refundReceiver || ZERO_ADDRESS, + safeTxGas: safeTxGas || 0, + approvalAndExecution, + }) + } + + return estimateGasForTransactionApproval({ + safeAddress, + operation: operation || CALL, + txData, + txAmount: txAmount || '0', + txRecipient, + from, + isOffChainSignature, + }) +} + +type UseEstimateTransactionGasProps = { + txData: string + txRecipient: string + txConfirmations?: List + txAmount?: string + preApprovingOwner?: string + operation?: number + safeTxGas?: number +} + +type TransactionGasEstimationResult = { + txEstimationExecutionStatus: EstimationStatus + gasEstimation: number // Amount of gas needed for execute or approve the transaction + gasCost: string // Cost of gas in raw format (estimatedGas * gasPrice) + gasCostFormatted: string // Cost of gas in format '< | > 100' + gasPrice: string // Current price of gas unit + isExecution: boolean // Returns true if the user will execute the tx or false if it just signs it + isCreation: boolean // Returns true if the transaction is a creation transaction + isOffChainSignature: boolean // Returns true if offChainSignature is available +} + +export const useEstimateTransactionGas = ({ + txRecipient, + txData, + txConfirmations, + txAmount, + preApprovingOwner, + operation, + safeTxGas, +}: UseEstimateTransactionGasProps): TransactionGasEstimationResult => { + const [gasEstimation, setGasEstimation] = useState({ + txEstimationExecutionStatus: EstimationStatus.LOADING, + gasEstimation: 0, + gasCost: '0', + gasCostFormatted: '< 0.001', + gasPrice: '0', + isExecution: false, + isCreation: false, + isOffChainSignature: false, + }) + const { nativeCoin } = getNetworkInfo() + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const threshold = useSelector(safeThresholdSelector) + const safeVersion = useSelector(safeCurrentVersionSelector) + const { account: from, smartContractWallet } = useSelector(providerSelector) + + useEffect(() => { + const estimateGas = async () => { + if (!txData.length) { + return + } + + const isExecution = checkIfTxIsExecution(Number(threshold), preApprovingOwner, txConfirmations?.size) + const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0) + const approvalAndExecution = checkIfTxIsApproveAndExecution(Number(threshold), txConfirmations?.size || 0) + + try { + const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion) + + const gasEstimation = await estimateTransactionGas({ + safeAddress, + txRecipient, + txData, + txAmount, + txConfirmations, + isExecution, + isCreation, + isOffChainSignature, + operation, + from, + safeTxGas, + approvalAndExecution, + }) + const gasPrice = await calculateGasPrice() + const estimatedGasCosts = gasEstimation * parseInt(gasPrice, 10) + const gasCost = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) + const gasCostFormatted = formatAmount(gasCost) + + let txEstimationExecutionStatus = EstimationStatus.SUCCESS + + if (gasEstimation <= 0) { + txEstimationExecutionStatus = isOffChainSignature ? EstimationStatus.SUCCESS : EstimationStatus.FAILURE + } + + setGasEstimation({ + txEstimationExecutionStatus, + gasEstimation, + gasCost, + gasCostFormatted, + gasPrice, + isExecution, + isCreation, + isOffChainSignature, + }) + } catch (error) { + console.warn(error.message) + // We put a fixed the amount of gas to let the user try to execute the tx, but it's not accurate so it will probably fail + const gasEstimation = 10000 + const gasCost = fromTokenUnit(gasEstimation, nativeCoin.decimals) + const gasCostFormatted = formatAmount(gasCost) + setGasEstimation({ + txEstimationExecutionStatus: EstimationStatus.FAILURE, + gasEstimation, + gasCost, + gasCostFormatted, + gasPrice: '1', + isExecution, + isCreation, + isOffChainSignature: false, + }) + } + } + + estimateGas() + }, [ + txData, + safeAddress, + txRecipient, + txConfirmations, + txAmount, + preApprovingOwner, + nativeCoin.decimals, + threshold, + from, + operation, + safeVersion, + smartContractWallet, + safeTxGas, + ]) + + return gasEstimation +} diff --git a/src/logic/notifications/store/reducer/notifications.ts b/src/logic/notifications/store/reducer/notifications.ts index 802ab9bd..bb05f747 100644 --- a/src/logic/notifications/store/reducer/notifications.ts +++ b/src/logic/notifications/store/reducer/notifications.ts @@ -20,7 +20,7 @@ export default handleActions( const { dismissAll, key } = action.payload if (key) { - return state.update(key, (prev) => prev.set('dismissed', true)) + return state.update(key, (prev) => prev?.set('dismissed', true)) } if (dismissAll) { return state.withMutations((map) => { diff --git a/src/logic/safe/safeTxSigner.ts b/src/logic/safe/safeTxSigner.ts index 52de1a3b..4fce148d 100644 --- a/src/logic/safe/safeTxSigner.ts +++ b/src/logic/safe/safeTxSigner.ts @@ -1,31 +1,61 @@ -// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures -// https://github.com/gnosis/safe-contracts/blob/master/test/gnosisSafeTeamEdition.js#L26 -export const generateSignaturesFromTxConfirmations = (confirmations, preApprovingOwner) => { - // The constant parts need to be sorted so that the recovered signers are sorted ascending - // (natural order) by address (not checksummed). - const confirmationsMap = confirmations.reduce((map, obj) => { - map[obj.owner.toLowerCase()] = obj // eslint-disable-line no-param-reassign - return map - }, {}) +import { List } from 'immutable' +import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' +import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' +import semverSatisfies from 'semver/functions/satisfies' +import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES } from './transactions/offchainSigner' + +// Here we're checking that safe contract version is greater or equal 1.1.1, but +// theoretically EIP712 should also work for 1.0.0 contracts +// Also, offchain signatures are not working for ledger/trezor wallet because of a bug in their library: +// https://github.com/LedgerHQ/ledgerjs/issues/378 +// Couldn't find an issue for trezor but the error is almost the same +export const checkIfOffChainSignatureIsPossible = ( + isExecution: boolean, + isSmartContractWallet: boolean, + safeVersion?: string, +): boolean => + !isExecution && + !isSmartContractWallet && + !!safeVersion && + semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES) + +// https://docs.gnosis.io/safe/docs/contracts_signatures/#pre-validated-signatures +export const getPreValidatedSignatures = (from: string, initialString: string = EMPTY_DATA): string => { + return `${initialString}000000000000000000000000${from.replace( + EMPTY_DATA, + '', + )}000000000000000000000000000000000000000000000000000000000000000001` +} + +export const generateSignaturesFromTxConfirmations = ( + confirmations?: List, + preApprovingOwner?: string, +): string => { + let confirmationsMap = + confirmations?.map((value) => { + return { + signature: value.signature, + owner: value.owner.toLowerCase(), + } + }) || List([]) if (preApprovingOwner) { - confirmationsMap[preApprovingOwner.toLowerCase()] = { owner: preApprovingOwner } + confirmationsMap = confirmationsMap.push({ owner: preApprovingOwner, signature: null }) } + // The constant parts need to be sorted so that the recovered signers are sorted ascending + // (natural order) by address (not checksummed). + confirmationsMap = confirmationsMap.sort((ownerA, ownerB) => ownerA.owner.localeCompare(ownerB.owner)) + let sigs = '0x' - Object.keys(confirmationsMap) - .sort() - .forEach((addr) => { - const conf = confirmationsMap[addr] - if (conf.signature) { - sigs += conf.signature.slice(2) - } else { - // https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures - sigs += `000000000000000000000000${addr.replace( - '0x', - '', - )}000000000000000000000000000000000000000000000000000000000000000001` - } - }) + confirmationsMap.forEach(({ signature, owner }) => { + if (signature) { + sigs += signature.slice(2) + } else { + // https://docs.gnosis.io/safe/docs/contracts_signatures/#pre-validated-signatures + sigs += getPreValidatedSignatures(owner, '') + } + }) + return sigs } diff --git a/src/logic/safe/store/actions/__tests__/utils.test.ts b/src/logic/safe/store/actions/__tests__/utils.test.ts index db063f9a..b11ebcb3 100644 --- a/src/logic/safe/store/actions/__tests__/utils.test.ts +++ b/src/logic/safe/store/actions/__tests__/utils.test.ts @@ -3,22 +3,8 @@ import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions' describe('Store actions utils > getNewTxNonce', () => { - it(`Should return passed predicted transaction nonce if it's a valid value`, async () => { - // Given - const txNonce = '45' - const lastTx = { nonce: 44 } as TxServiceModel - const safeInstance = {} - - // When - const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe) - - // Then - expect(nonce).toBe('45') - }) - it(`Should return nonce of a last transaction + 1 if passed nonce is less than last transaction or invalid`, async () => { // Given - const txNonce = '' const lastTx = { nonce: 44 } as TxServiceModel const safeInstance = { methods: { @@ -29,7 +15,7 @@ describe('Store actions utils > getNewTxNonce', () => { } // When - const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe) + const nonce = await getNewTxNonce(lastTx, safeInstance as GnosisSafe) // Then expect(nonce).toBe('45') @@ -37,7 +23,6 @@ describe('Store actions utils > getNewTxNonce', () => { it(`Should retrieve contract's instance nonce value as a fallback, if txNonce and lastTx are not valid`, async () => { // Given - const txNonce = '' const lastTx = null const safeInstance = { methods: { @@ -48,7 +33,7 @@ describe('Store actions utils > getNewTxNonce', () => { } // When - const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe) + const nonce = await getNewTxNonce(lastTx, safeInstance as GnosisSafe) // Then expect(nonce).toBe('45') diff --git a/src/logic/safe/store/actions/createTransaction.ts b/src/logic/safe/store/actions/createTransaction.ts index 0106e56f..0e318e33 100644 --- a/src/logic/safe/store/actions/createTransaction.ts +++ b/src/logic/safe/store/actions/createTransaction.ts @@ -1,5 +1,4 @@ import { push } from 'connected-react-router' -import semverSatisfies from 'semver/functions/satisfies' import { ThunkAction } from 'redux-thunk' import { onboardUser } from 'src/components/ConnectButton' @@ -10,11 +9,10 @@ import { CALL, getApprovalTransaction, getExecutionTransaction, - SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES, saveTxToHistory, tryOffchainSigning, } from 'src/logic/safe/transactions' -import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas' +import { estimateGasForTransactionCreation } from 'src/logic/safe/transactions/gas' import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' @@ -40,6 +38,7 @@ import { AnyAction } from 'redux' import { PayableTx } from 'src/types/contracts/types.d' import { AppReduxState } from 'src/store' import { Dispatch, DispatchReturn } from './types' +import { checkIfOffChainSignatureIsPossible, getPreValidatedSignatures } from 'src/logic/safe/safeTxSigner' export interface CreateTransactionArgs { navigateToTransactionsTab?: boolean @@ -87,18 +86,18 @@ const createTransaction = ( const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state) const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const lastTx = await getLastTx(safeAddress) - const nonce = await getNewTxNonce(txNonce?.toString(), lastTx, safeInstance) + const nonce = txNonce ? txNonce.toString() : await getNewTxNonce(lastTx, safeInstance) const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx) const safeVersion = await getCurrentSafeVersion(safeInstance) - const safeTxGas = - safeTxGasArg || (await estimateSafeTxGas(safeInstance, safeAddress, txData, to, valueInWei, operation)) - - // https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures - const sigs = `0x000000000000000000000000${from.replace( - '0x', - '', - )}000000000000000000000000000000000000000000000000000000000000000001` + let safeTxGas + try { + safeTxGas = + safeTxGasArg || (await estimateGasForTransactionCreation(safeAddress, txData, to, valueInWei, operation)) + } catch (error) { + safeTxGas = safeTxGasArg || 0 + } + const sigs = getPreValidatedSignatures(from) const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin) const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution)) @@ -123,11 +122,7 @@ const createTransaction = ( const safeTxHash = generateSafeTxHash(safeAddress, txArgs) try { - // Here we're checking that safe contract version is greater or equal 1.1.1, but - // theoretically EIP712 should also work for 1.0.0 contracts - const canTryOffchainSigning = - !isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES) - if (canTryOffchainSigning) { + if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) { const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet) if (signature) { @@ -141,9 +136,7 @@ const createTransaction = ( } } - const tx = isExecution - ? await getExecutionTransaction(txArgs) - : await getApprovalTransaction(safeInstance, safeTxHash) + const tx = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, safeTxHash) const sendParams: PayableTx = { from, value: 0 } // if not set owner management tests will fail on ganache diff --git a/src/logic/safe/store/actions/processTransaction.ts b/src/logic/safe/store/actions/processTransaction.ts index eced5a75..8d31d343 100644 --- a/src/logic/safe/store/actions/processTransaction.ts +++ b/src/logic/safe/store/actions/processTransaction.ts @@ -1,12 +1,15 @@ import { AnyAction } from 'redux' import { ThunkAction } from 'redux-thunk' -import semverSatisfies from 'semver/functions/satisfies' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { getNotificationsFromTxType } from 'src/logic/notifications' -import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner' +import { + checkIfOffChainSignatureIsPossible, + generateSignaturesFromTxConfirmations, + getPreValidatedSignatures, +} from 'src/logic/safe/safeTxSigner' import { getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions' -import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES, tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner' +import { tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner' import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { providerSelector } from 'src/logic/wallets/store/selectors' @@ -33,7 +36,7 @@ interface ProcessTransactionArgs { type ProcessTransactionAction = ThunkAction, AppReduxState, DispatchReturn, AnyAction> -const processTransaction = ({ +export const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddress, @@ -49,17 +52,15 @@ const processTransaction = ({ const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const lastTx = await getLastTx(safeAddress) - const nonce = await getNewTxNonce(undefined, lastTx, safeInstance) + const nonce = await getNewTxNonce(lastTx, safeInstance) const isExecution = approveAndExecute || (await shouldExecuteTransaction(safeInstance, nonce, lastTx)) const safeVersion = await getCurrentSafeVersion(safeInstance) - let sigs = generateSignaturesFromTxConfirmations(tx.confirmations, approveAndExecute && userAddress) - // https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures + const preApprovingOwner = approveAndExecute ? userAddress : undefined + let sigs = generateSignaturesFromTxConfirmations(tx.confirmations, preApprovingOwner) + if (!sigs) { - sigs = `0x000000000000000000000000${from.replace( - '0x', - '', - )}000000000000000000000000000000000000000000000000000000000000000001` + sigs = getPreValidatedSignatures(from) } const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, tx.origin) @@ -86,14 +87,7 @@ const processTransaction = ({ } try { - // Here we're checking that safe contract version is greater or equal 1.1.1, but - // theoretically EIP712 should also work for 1.0.0 contracts - // Also, offchain signatures are not working for ledger/trezor wallet because of a bug in their library: - // https://github.com/LedgerHQ/ledgerjs/issues/378 - // Couldn't find an issue for trezor but the error is almost the same - const canTryOffchainSigning = - !isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES) - if (canTryOffchainSigning) { + if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) { const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet) if (signature) { @@ -109,9 +103,7 @@ const processTransaction = ({ } } - transaction = isExecution - ? await getExecutionTransaction(txArgs) - : await getApprovalTransaction(safeInstance, tx.safeTxHash) + transaction = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, tx.safeTxHash) const sendParams: any = { from, value: 0 } @@ -196,5 +188,3 @@ const processTransaction = ({ return txHash } - -export default processTransaction diff --git a/src/logic/safe/store/actions/utils.ts b/src/logic/safe/store/actions/utils.ts index 74b59b0d..828c6e92 100644 --- a/src/logic/safe/store/actions/utils.ts +++ b/src/logic/safe/store/actions/utils.ts @@ -16,15 +16,7 @@ export const getLastTx = async (safeAddress: string): Promise => { - if (txNonce) { - return txNonce - } - +export const getNewTxNonce = async (lastTx: TxServiceModel | null, safeInstance: GnosisSafe): Promise => { // use current's safe nonce as fallback return lastTx ? `${lastTx.nonce + 1}` : (await safeInstance.methods.nonce().call()).toString() } diff --git a/src/logic/safe/transactions/__tests__/gas.test.ts b/src/logic/safe/transactions/__tests__/gas.test.ts index 4c698240..828d1e64 100644 Binary files a/src/logic/safe/transactions/__tests__/gas.test.ts and b/src/logic/safe/transactions/__tests__/gas.test.ts differ diff --git a/src/logic/safe/transactions/gas.ts b/src/logic/safe/transactions/gas.ts index 92982f12..fa220bac 100644 --- a/src/logic/safe/transactions/gas.ts +++ b/src/logic/safe/transactions/gas.ts @@ -1,19 +1,15 @@ -import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json' import { BigNumber } from 'bignumber.js' -import { AbiItem } from 'web3-utils' - -import { CALL } from '.' - import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' -import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner' -import { Transaction } from 'src/logic/safe/store/models/types/transaction' -import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { EMPTY_DATA, calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions' -import { getAccountFrom, getWeb3 } from 'src/logic/wallets/getWeb3' -import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' +import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions' +import { getWeb3 } from 'src/logic/wallets/getWeb3' import { sameString } from 'src/utils/strings' +import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner' +import { List } from 'immutable' +import { Confirmation } from 'src/logic/safe/store/models/types/confirmation' -const estimateDataGasCosts = (data: string): number => { +// Receives the response data of the safe method requiredTxGas() and parses it to get the gas amount +const parseRequiredTxGasResponse = (data: string): number => { const reducer = (accumulator, currentValue) => { if (currentValue === EMPTY_DATA) { return accumulator + 0 @@ -29,74 +25,15 @@ const estimateDataGasCosts = (data: string): number => { return data.match(/.{2}/g)?.reduce(reducer, 0) } -export const estimateTxGasCosts = async ( - safeAddress: string, - to: string, - data: string, - tx?: Transaction, - preApprovingOwner?: string, -): Promise => { - try { - const web3 = getWeb3() - const from = await getAccountFrom(web3) - - if (!from) { - return 0 - } - - const safeInstance = (new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown) as GnosisSafe - const nonce = await safeInstance.methods.nonce().call() - const threshold = await safeInstance.methods.getThreshold().call() - const isExecution = tx?.confirmations.size === Number(threshold) || !!preApprovingOwner || threshold === '1' - - let txData - if (isExecution) { - // https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures - const signatures = tx?.confirmations - ? generateSignaturesFromTxConfirmations(tx.confirmations, preApprovingOwner) - : `0x000000000000000000000000${from.replace( - EMPTY_DATA, - '', - )}000000000000000000000000000000000000000000000000000000000000000001` - txData = await safeInstance.methods - .execTransaction( - to, - tx?.value || 0, - data, - CALL, - tx?.safeTxGas || 0, - 0, - 0, - ZERO_ADDRESS, - ZERO_ADDRESS, - signatures, - ) - .encodeABI() - } else { - const txHash = await safeInstance.methods - .getTransactionHash(to, tx?.value || 0, data, CALL, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, nonce) - .call({ - from, - }) - txData = await safeInstance.methods.approveHash(txHash).encodeABI() - } - - const gas = await calculateGasOf(txData, from, safeAddress) - const gasPrice = await calculateGasPrice() - - return gas * parseInt(gasPrice, 10) - } catch (err) { - console.error('Error while estimating transaction execution gas costs:') - console.error(err) - - return 10000 - } -} - // Parses the result from the error message (GETH, OpenEthereum/Parity and Nethermind) and returns the data value export const getDataFromNodeErrorMessage = (errorMessage: string): string | undefined => { + // Replace illegal characters that often comes within the error string (like � for example) + // https://stackoverflow.com/questions/12754256/removing-invalid-characters-in-javascript + const normalizedErrorString = errorMessage.replace(/\uFFFD/g, '') + // Extracts JSON object from the error message - const [, ...error] = errorMessage.split('\n') + const [, ...error] = normalizedErrorString.split('\n') + try { const errorAsJSON = JSON.parse(error.join('')) @@ -130,7 +67,7 @@ export const getDataFromNodeErrorMessage = (errorMessage: string): string | unde } } -const getGasEstimationTxResponse = async (txConfig: { +export const getGasEstimationTxResponse = async (txConfig: { to: string from: string data: string @@ -190,8 +127,7 @@ const calculateMinimumGasForTransaction = async ( return 0 } -export const estimateSafeTxGas = async ( - safe: GnosisSafe | undefined, +export const estimateGasForTransactionCreation = async ( safeAddress: string, data: string, to: string, @@ -199,10 +135,7 @@ export const estimateSafeTxGas = async ( operation: number, ): Promise => { try { - let safeInstance = safe - if (!safeInstance) { - safeInstance = await getGnosisSafeInstanceAt(safeAddress) - } + const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const estimateData = safeInstance.methods.requiredTxGas(to, valueInWei, data, operation).encodeABI() const gasEstimationResponse = await getGasEstimationTxResponse({ @@ -214,7 +147,7 @@ export const estimateSafeTxGas = async ( const txGasEstimation = gasEstimationResponse + 10000 // 21000 - additional gas costs (e.g. base tx costs, transfer costs) - const dataGasEstimation = estimateDataGasCosts(estimateData) + 21000 + const dataGasEstimation = parseRequiredTxGasResponse(estimateData) + 21000 const additionalGasBatches = [0, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000] return await calculateMinimumGasForTransaction( @@ -225,7 +158,98 @@ export const estimateSafeTxGas = async ( dataGasEstimation, ) } catch (error) { - console.error('Error calculating tx gas estimation', error) - return 0 + console.info('Error calculating tx gas estimation', error.message) + throw error } } + +type TransactionExecutionEstimationProps = { + txData: string + safeAddress: string + txRecipient: string + txConfirmations?: List + txAmount: string + operation: number + gasPrice: string + gasToken: string + refundReceiver: string // Address of receiver of gas payment (or 0 if tx.origin). + safeTxGas: number + from: string + approvalAndExecution?: boolean +} + +export const estimateGasForTransactionExecution = async ({ + safeAddress, + txRecipient, + txConfirmations, + txAmount, + txData, + operation, + gasPrice, + gasToken, + refundReceiver, + safeTxGas, + approvalAndExecution, +}: TransactionExecutionEstimationProps): Promise => { + const safeInstance = await getGnosisSafeInstanceAt(safeAddress) + try { + if (approvalAndExecution) { + console.info(`Estimating transaction success for execution & approval...`) + // @todo (agustin) once we solve the problem with the preApprovingOwner, we need to use the method bellow (execTransaction) with sigs = generateSignaturesFromTxConfirmations(txConfirmations,from) + const gasEstimation = await estimateGasForTransactionCreation( + safeAddress, + txData, + txRecipient, + txAmount, + operation, + ) + console.info(`Gas estimation successfully finished with gas amount: ${gasEstimation}`) + return gasEstimation + } + const sigs = generateSignaturesFromTxConfirmations(txConfirmations) + console.info(`Estimating transaction success for with gas amount: ${safeTxGas}...`) + await safeInstance.methods + .execTransaction(txRecipient, txAmount, txData, operation, safeTxGas, 0, gasPrice, gasToken, refundReceiver, sigs) + .call() + + console.info(`Gas estimation successfully finished with gas amount: ${safeTxGas}`) + return safeTxGas + } catch (error) { + throw new Error(`Gas estimation failed with gas amount: ${safeTxGas}`) + } +} + +type TransactionApprovalEstimationProps = { + txData: string + safeAddress: string + txRecipient: string + txAmount: string + operation: number + from: string + isOffChainSignature: boolean +} + +export const estimateGasForTransactionApproval = async ({ + safeAddress, + txRecipient, + txAmount, + txData, + operation, + from, + isOffChainSignature, +}: TransactionApprovalEstimationProps): Promise => { + if (isOffChainSignature) { + return 0 + } + + const safeInstance = await getGnosisSafeInstanceAt(safeAddress) + + const nonce = await safeInstance.methods.nonce().call() + const txHash = await safeInstance.methods + .getTransactionHash(txRecipient, txAmount, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, nonce) + .call({ + from, + }) + const approveTransactionTxData = await safeInstance.methods.approveHash(txHash).encodeABI() + return calculateGasOf(approveTransactionTxData, from, safeAddress) +} diff --git a/src/logic/safe/transactions/send.ts b/src/logic/safe/transactions/send.ts index 67bbab2f..289c8fe3 100644 --- a/src/logic/safe/transactions/send.ts +++ b/src/logic/safe/transactions/send.ts @@ -30,10 +30,7 @@ export const getTransactionHash = async ({ return txHash } -export const getApprovalTransaction = async ( - safeInstance: GnosisSafe, - txHash: string, -): Promise> => { +export const getApprovalTransaction = (safeInstance: GnosisSafe, txHash: string): NonPayableTransactionObject => { try { return safeInstance.methods.approveHash(txHash) } catch (err) { diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index be761418..28c47ff0 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -32,7 +32,7 @@ import { LoadingContainer } from 'src/components/LoaderContainer/index' import { TIMEOUT } from 'src/utils/constants' import { web3ReadOnly } from 'src/logic/wallets/getWeb3' -import ConfirmTransactionModal from '../components/ConfirmTransactionModal' +import { ConfirmTransactionModal } from '../components/ConfirmTransactionModal' import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler' import { useLegalConsent } from '../hooks/useLegalConsent' import LegalDisclaimer from './LegalDisclaimer' diff --git a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx index 80eb88de..06b95a08 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react' -import { Icon, Text, Title, GenericModal, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components' +import { GenericModal, Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components' import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' import styled from 'styled-components' import { useDispatch } from 'react-redux' @@ -20,11 +20,11 @@ import createTransaction from 'src/logic/safe/store/actions/createTransaction' import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts' import { DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend' -import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas' import GasEstimationInfo from './GasEstimationInfo' import { getNetworkInfo } from 'src/config' import { TransactionParams } from './AppFrame' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' const isTxValid = (t: Transaction): boolean => { if (!['string', 'number'].includes(typeof t.value)) { @@ -82,7 +82,7 @@ type OwnProps = { const { nativeCoin } = getNetworkInfo() -const ConfirmTransactionModal = ({ +export const ConfirmTransactionModal = ({ isOpen, app, txs, @@ -95,32 +95,18 @@ const ConfirmTransactionModal = ({ onTxReject, }: OwnProps): React.ReactElement | null => { const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0) - const [estimatingGas, setEstimatingGas] = useState(false) + + const { gasEstimation, txEstimationExecutionStatus } = useEstimateTransactionGas({ + txData: encodeMultiSendCall(txs), + txRecipient: MULTI_SEND_ADDRESS, + operation: DELEGATE_CALL, + }) useEffect(() => { - const estimateGas = async () => { - try { - setEstimatingGas(true) - const safeTxGas = await estimateSafeTxGas( - undefined, - safeAddress, - encodeMultiSendCall(txs), - MULTI_SEND_ADDRESS, - '0', - DELEGATE_CALL, - ) - - setEstimatedSafeTxGas(safeTxGas) - } catch (err) { - console.error(err) - } finally { - setEstimatingGas(false) - } - } if (params?.safeTxGas) { - estimateGas() + setEstimatedSafeTxGas(gasEstimation) } - }, [params, safeAddress, txs]) + }, [params, gasEstimation]) const dispatch = useDispatch() if (!isOpen) { @@ -205,7 +191,7 @@ const ConfirmTransactionModal = ({ )} @@ -229,5 +215,3 @@ const ConfirmTransactionModal = ({ /> ) } - -export default ConfirmTransactionModal diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx index e67d8ae0..532bf33b 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx @@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/core/styles' import { useDispatch, useSelector } from 'react-redux' import { getNetworkInfo } from 'src/config' -import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { 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' @@ -14,15 +14,15 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style' import Header from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import createTransaction from 'src/logic/safe/store/actions/createTransaction' -import { safeSelector } from 'src/logic/safe/store/selectors' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { generateFormFieldKey, getValueFromTxInputs } from '../utils' +import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFees } from 'src/components/TransactionsFees' const useStyles = makeStyles(styles) @@ -45,41 +45,41 @@ const { nativeCoin } = getNetworkInfo() const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactElement => { const classes = useStyles() const dispatch = useDispatch() - const { address: safeAddress } = useSelector(safeSelector) || {} - const [gasCosts, setGasCosts] = useState('< 0.001') + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const [txParameters, setTxParameters] = useState<{ + txRecipient: string + txData: string + txAmount: string + }>({ txData: '', txAmount: '', txRecipient: '' }) + + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isOffChainSignature, + isCreation, + } = useEstimateTransactionGas({ + txRecipient: txParameters?.txRecipient, + txAmount: txParameters?.txAmount, + txData: txParameters?.txData, + }) + useEffect(() => { - let isCurrent = true - - const estimateGas = async (): Promise => { - const txData = tx.data ? tx.data.trim() : '' - - const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.contractAddress as string, txData) - const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) - const formattedGasCosts = formatAmount(gasCosts) - - if (isCurrent) { - setGasCosts(formattedGasCosts) - } - } - - estimateGas() - - return () => { - isCurrent = false - } - }, [safeAddress, tx.contractAddress, tx.data]) + setTxParameters({ + txRecipient: tx.contractAddress as string, + txAmount: tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0', + txData: tx.data ? tx.data.trim() : '', + }) + }, [tx.contractAddress, tx.value, tx.data, safeAddress]) const submitTx = async () => { - const txRecipient = tx.contractAddress - const txData = tx.data ? tx.data.trim() : '' - const txValue = tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0' - if (safeAddress) { + if (safeAddress && txParameters) { dispatch( createTransaction({ safeAddress, - to: txRecipient as string, - valueInWei: txValue, - txData, + to: txParameters?.txRecipient, + valueInWei: txParameters?.txAmount, + txData: txParameters?.txData, notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, }), ) @@ -162,9 +162,13 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE - - {`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`} - + diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx index 2512c69e..cda1aead 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx @@ -1,11 +1,11 @@ -import React, { useEffect, useState } from 'react' +import React 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 { getExplorerInfo, getNetworkInfo } from 'src/config' -import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import CopyBtn from 'src/components/CopyBtn' import Identicon from 'src/components/Identicon' import Block from 'src/components/layout/Block' @@ -16,10 +16,8 @@ import Img from 'src/components/layout/Img' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import createTransaction from 'src/logic/safe/store/actions/createTransaction' -import { safeSelector } from 'src/logic/safe/store/selectors' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' @@ -29,6 +27,8 @@ import ArrowDown from '../../assets/arrow-down.svg' import { styles } from './style' import { ExplorerButton } from '@gnosis.pm/safe-react-components' +import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFees } from 'src/components/TransactionsFees' export type CustomTx = { contractAddress?: string @@ -49,29 +49,19 @@ const { nativeCoin } = getNetworkInfo() const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { const classes = useStyles() const dispatch = useDispatch() - const { address: safeAddress } = useSelector(safeSelector) || {} - const [gasCosts, setGasCosts] = useState('< 0.001') - useEffect(() => { - let isCurrent = true + const safeAddress = useSelector(safeParamAddressFromStateSelector) - const estimateGas = async () => { - const txData = tx.data ? tx.data.trim() : '' - - const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.contractAddress as string, txData) - const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) - const formattedGasCosts = formatAmount(gasCosts) - - if (isCurrent) { - setGasCosts(formattedGasCosts) - } - } - - estimateGas() - - return () => { - isCurrent = false - } - }, [safeAddress, tx.data, tx.contractAddress]) + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isCreation, + isOffChainSignature, + } = useEstimateTransactionGas({ + txRecipient: tx.contractAddress as string, + txData: tx.data ? tx.data.trim() : '', + txAmount: tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0', + }) const submitTx = async (): Promise => { const txRecipient = tx.contractAddress @@ -161,9 +151,13 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { - - {`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`} - + diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx index d6428bef..c3ea4a7d 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/index.tsx @@ -7,7 +7,7 @@ import GnoForm from 'src/components/forms/GnoForm' import Block from 'src/components/layout/Block' import Hairline from 'src/components/layout/Hairline' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' -import { safeSelector } from 'src/logic/safe/store/selectors' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import Paragraph from 'src/components/layout/Paragraph' import Buttons from './Buttons' import ContractABI from './ContractABI' @@ -53,14 +53,14 @@ const ContractInteraction: React.FC = ({ isABI, }) => { const classes = useStyles() - const { address: safeAddress = '' } = useSelector(safeSelector) || {} + const safeAddress = useSelector(safeParamAddressFromStateSelector) let setCallResults React.useMemo(() => { if (contractAddress) { initialValues.contractAddress = contractAddress } - }, [contractAddress, initialValues.contractAddress]) + }, [contractAddress, initialValues]) const saveForm = async (values: CreatedTx): Promise => { await handleSubmit(values, false) diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx index c1d5cd18..63d4c942 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx @@ -3,9 +3,7 @@ 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 { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' -import { getExplorerInfo, getNetworkInfo } from 'src/config' +import { getExplorerInfo } from 'src/config' import CopyBtn from 'src/components/CopyBtn' import Identicon from 'src/components/Identicon' import Block from 'src/components/layout/Block' @@ -17,10 +15,8 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { nftTokensSelector } from 'src/logic/collectibles/store/selectors' import createTransaction from 'src/logic/safe/store/actions/createTransaction' -import { safeSelector } from 'src/logic/safe/store/selectors' +import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import { sm } from 'src/theme/variables' @@ -31,8 +27,8 @@ import ArrowDown from '../assets/arrow-down.svg' import { styles } from './style' import { ExplorerButton } from '@gnosis.pm/safe-react-components' - -const { nativeCoin } = getNetworkInfo() +import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFees } from 'src/components/TransactionsFees' const useStyles = makeStyles(styles) @@ -53,34 +49,38 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement = const classes = useStyles() const shortener = textShortener() const dispatch = useDispatch() - const { address: safeAddress } = useSelector(safeSelector) || {} + const safeAddress = useSelector(safeParamAddressFromStateSelector) const nftTokens = useSelector(nftTokensSelector) - const [gasCosts, setGasCosts] = useState('< 0.001') const txToken = nftTokens.find( ({ assetAddress, tokenId }) => assetAddress === tx.assetAddress && tokenId === tx.nftTokenId, ) const [data, setData] = useState('') + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isOffChainSignature, + isCreation, + } = useEstimateTransactionGas({ + txData: data, + txRecipient: tx.recipientAddress, + }) + useEffect(() => { let isCurrent = true - const estimateGas = async () => { + const calculateERC721TransferData = async () => { try { const txData = await generateERC721TransferTxData(tx, safeAddress) - const estimatedGasCosts = await estimateTxGasCosts(safeAddress ?? '', tx.recipientAddress, txData) - const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) - const formattedGasCosts = formatAmount(gasCosts) - if (isCurrent) { - setGasCosts(formattedGasCosts) setData(txData) } } catch (error) { - console.error('Error while calculating estimated gas:', error) + console.error('Error calculating ERC721 transfer data:', error.message) } } - - estimateGas() + calculateERC721TransferData() return () => { isCurrent = false @@ -164,9 +164,13 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement = )} - - {`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`} - + 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 17b0bcd7..8dbb6b3c 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx @@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import React, { useEffect, useMemo, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { toTokenUnit, fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' import { getExplorerInfo, getNetworkInfo } from 'src/config' import CopyBtn from 'src/components/CopyBtn' @@ -17,11 +17,9 @@ 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 { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' -import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas' import { getHumanFriendlyToken } from 'src/logic/tokens/store/actions/fetchTokens' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' @@ -35,7 +33,10 @@ import ArrowDown from '../assets/arrow-down.svg' import { styles } from './style' import { ExplorerButton } from '@gnosis.pm/safe-react-components' - +import { TokenProps } from 'src/logic/tokens/store/model/token' +import { RecordOf } from 'immutable' +import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFees } from 'src/components/TransactionsFees' const useStyles = makeStyles(styles) const { nativeCoin } = getNetworkInfo() @@ -55,59 +56,74 @@ type ReviewTxProps = { tx: ReviewTxProp } -const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => { - const classes = useStyles() - const dispatch = useDispatch() - const { address: safeAddress } = useSelector(safeSelector) || {} - const tokens = useSelector(extendedSafeTokensSelector) - const [gasCosts, setGasCosts] = useState('< 0.001') +const useTxAmount = (tx: ReviewTxProp, isSendingNativeToken: boolean, txToken?: RecordOf): string => { + const [txAmount, setTxAmount] = useState('0') + + // 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 (and the data will be empty) + useEffect(() => { + const txAmount = isSendingNativeToken ? toTokenUnit(tx.amount, nativeCoin.decimals) : '0' + setTxAmount(txAmount) + }, [tx.amount, txToken, isSendingNativeToken]) + + return txAmount +} + +const useTxData = ( + isSendingNativeToken: boolean, + txAmount: string, + recipientAddress: string, + txToken?: RecordOf, +): string => { const [data, setData] = useState('') - const txToken = useMemo(() => tokens.find((token) => sameAddress(token.address, tx.token)), [tokens, tx.token]) - const isSendingETH = sameAddress(txToken?.address, nativeCoin.address) - const txRecipient = isSendingETH ? tx.recipientAddress : txToken?.address - useEffect(() => { - let isCurrent = true - - const estimateGas = async () => { + const updateTxDataAsync = async () => { if (!txToken) { return } let txData = EMPTY_DATA - - if (!isSendingETH) { + if (!isSendingNativeToken) { const StandardToken = await getHumanFriendlyToken() const tokenInstance = await StandardToken.at(txToken.address as string) - const txAmount = toTokenUnit(tx.amount, txToken.decimals) - - txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI() - } - - const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, txRecipient as string, txData) - const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) - const formattedGasCosts = formatAmount(gasCosts) - - if (isCurrent) { - setGasCosts(formattedGasCosts) - setData(txData) + txData = tokenInstance.contract.methods.transfer(recipientAddress, txAmount).encodeABI() } + setData(txData) } - estimateGas() + updateTxDataAsync() + }, [isSendingNativeToken, recipientAddress, txAmount, txToken]) - return () => { - isCurrent = false - } - }, [isSendingETH, safeAddress, tx.amount, tx.recipientAddress, txRecipient, txToken]) + return data +} + +const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => { + const classes = useStyles() + const dispatch = useDispatch() + const safeAddress = useSelector(safeParamAddressFromStateSelector) + const tokens = useSelector(extendedSafeTokensSelector) + const txToken = useMemo(() => tokens.find((token) => sameAddress(token.address, tx.token)), [tokens, tx.token]) + const isSendingNativeToken = sameAddress(txToken?.address, nativeCoin.address) + const txRecipient = isSendingNativeToken ? tx.recipientAddress : txToken?.address || '' + + const txAmount = useTxAmount(tx, isSendingNativeToken, txToken) + const data = useTxData(isSendingNativeToken, txAmount, tx.recipientAddress, txToken) + + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isCreation, + isOffChainSignature, + } = useEstimateTransactionGas({ + txData: data, + txRecipient, + }) const submitTx = async () => { const isSpendingLimit = sameString(tx.txType, 'spendingLimit') - // 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 ? toTokenUnit(tx.amount, nativeCoin.decimals) : '0' if (!safeAddress) { console.error('There was an error trying to submit the transaction, the safeAddress was not found') @@ -115,11 +131,12 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => } if (isSpendingLimit && txToken && tx.tokenSpendingLimit) { + const spendingLimitTokenAddress = isSendingNativeToken ? ZERO_ADDRESS : txToken.address const spendingLimit = getSpendingLimitContract() spendingLimit.methods .executeAllowanceTransfer( safeAddress, - sameAddress(txToken.address, nativeCoin.address) ? ZERO_ADDRESS : txToken.address, + spendingLimitTokenAddress, tx.recipientAddress, toTokenUnit(tx.amount, txToken.decimals), ZERO_ADDRESS, @@ -207,9 +224,13 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => - - {`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`} - + diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx index a4e73801..23f1f973 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/index.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { OwnerForm } from './screens/OwnerForm' -import ReviewAddOwner from './screens/Review' +import { ReviewAddOwner } from './screens/Review' import ThresholdForm from './screens/ThresholdForm' import Modal from 'src/components/Modal' diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx index 2b74e8b0..9329feeb 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx @@ -1,11 +1,10 @@ import IconButton from '@material-ui/core/IconButton' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import classNames from 'classnames' import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' -import { getExplorerInfo, getNetworkInfo } from 'src/config' +import { getExplorerInfo } from 'src/config' import CopyBtn from 'src/components/CopyBtn' import Identicon from 'src/components/Identicon' import Block from 'src/components/layout/Block' @@ -16,37 +15,61 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' -import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { styles } from './style' import { ExplorerButton } from '@gnosis.pm/safe-react-components' +import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFees } from 'src/components/TransactionsFees' export const ADD_OWNER_SUBMIT_BTN_TEST_ID = 'add-owner-submit-btn' -const { nativeCoin } = getNetworkInfo() +const useStyles = makeStyles(styles) -const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) => { - const [gasCosts, setGasCosts] = useState('< 0.001') - const safeAddress = useSelector(safeParamAddressFromStateSelector) as string +type ReviewAddOwnerProps = { + onClickBack: () => void + onClose: () => void + onSubmit: () => void + values: { + ownerAddress: string + threshold: string + ownerName: string + } +} + +export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: ReviewAddOwnerProps): React.ReactElement => { + const classes = useStyles() + const [data, setData] = useState('') + const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeName = useSelector(safeNameSelector) const owners = useSelector(safeOwnersSelector) + + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isOffChainSignature, + isCreation, + } = useEstimateTransactionGas({ + txData: data, + txRecipient: safeAddress, + }) + useEffect(() => { let isCurrent = true - const estimateGas = async () => { - const safeInstance = await getGnosisSafeInstanceAt(safeAddress) - const txData = safeInstance.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI() - const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData) + const calculateAddOwnerData = async () => { + try { + const safeInstance = await getGnosisSafeInstanceAt(safeAddress) + const txData = safeInstance.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI() - const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) - const formattedGasCosts = formatAmount(gasCosts) - if (isCurrent) { - setGasCosts(formattedGasCosts) + if (isCurrent) { + setData(txData) + } + } catch (error) { + console.error('Error calculating ERC721 transfer data:', error.message) } } - - estimateGas() + calculateAddOwnerData() return () => { isCurrent = false @@ -68,7 +91,7 @@ const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) => - + @@ -157,11 +180,13 @@ const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) => - - You're about to create a transaction and will have to confirm it with your currently connected wallet. -
- {`Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`} -
+
@@ -183,5 +208,3 @@ const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) => ) } - -export default withStyles(styles as any)(ReviewAddOwner) diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts index 537bd9d9..6b8f83c3 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts @@ -1,6 +1,7 @@ import { background, border, lg, secondaryText, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ root: { height: '372px', }, @@ -76,7 +77,9 @@ export const styles = () => ({ }, }, gasCostsContainer: { - padding: `0 ${lg}`, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', textAlign: 'center', width: '100%', }, diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx index 9147afb7..3fe84603 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/index.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import CheckOwner from './screens/CheckOwner' -import ReviewRemoveOwner from './screens/Review' +import { ReviewRemoveOwnerModal } from './screens/Review' import ThresholdForm from './screens/ThresholdForm' import Modal from 'src/components/Modal' @@ -69,7 +69,12 @@ type RemoveOwnerProps = { ownerName: string } -const RemoveOwner = ({ isOpen, onClose, ownerAddress, ownerName }: RemoveOwnerProps): React.ReactElement => { +export const RemoveOwnerModal = ({ + isOpen, + onClose, + ownerAddress, + ownerName, +}: RemoveOwnerProps): React.ReactElement => { const classes = useStyles() const [activeScreen, setActiveScreen] = useState('checkOwner') const [values, setValues] = useState({}) @@ -124,18 +129,16 @@ const RemoveOwner = ({ isOpen, onClose, ownerAddress, ownerName }: RemoveOwnerPr )} {activeScreen === 'reviewRemoveOwner' && ( - )} ) } - -export default RemoveOwner diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx index 7829a950..191e7c89 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx @@ -1,11 +1,10 @@ import IconButton from '@material-ui/core/IconButton' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import classNames from 'classnames' import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' -import { getExplorerInfo, getNetworkInfo } from 'src/config' +import { getExplorerInfo } from 'src/config' import CopyBtn from 'src/components/CopyBtn' import Identicon from 'src/components/Identicon' import Block from 'src/components/layout/Block' @@ -16,50 +15,84 @@ import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts' import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' -import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { styles } from './style' import { ExplorerButton } from '@gnosis.pm/safe-react-components' import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' import { List } from 'immutable' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' +import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFees } from 'src/components/TransactionsFees' export const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn' -const { nativeCoin } = getNetworkInfo() +const useStyles = makeStyles(styles) -const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddress, ownerName, values }) => { - const [gasCosts, setGasCosts] = useState('< 0.001') - const safeAddress = useSelector(safeParamAddressFromStateSelector) as string +type ReviewRemoveOwnerProps = { + onClickBack: () => void + onClose: () => void + onSubmit: () => void + ownerAddress: string + ownerName: string + threshold?: number +} + +export const ReviewRemoveOwnerModal = ({ + onClickBack, + onClose, + onSubmit, + ownerAddress, + ownerName, + threshold, +}: ReviewRemoveOwnerProps): React.ReactElement => { + const classes = useStyles() + const [data, setData] = useState('') + const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeName = useSelector(safeNameSelector) const owners = useSelector(safeOwnersSelector) const addressBook = useSelector(addressBookSelector) const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([]) + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isCreation, + isOffChainSignature, + } = useEstimateTransactionGas({ + txData: data, + txRecipient: safeAddress, + }) + useEffect(() => { let isCurrent = true - const estimateGas = async () => { - const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) - const safeOwners = await gnosisSafe.methods.getOwners().call() - const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase()) - const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] - const txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddress, values.threshold).encodeABI() - const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData) - const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) - const formattedGasCosts = formatAmount(gasCosts) - - if (isCurrent) { - setGasCosts(formattedGasCosts) - } + if (!threshold) { + console.error("Threshold value was not define, tx can't be executed") + return } - estimateGas() + const calculateRemoveOwnerData = async () => { + try { + const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) + const safeOwners = await gnosisSafe.methods.getOwners().call() + const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase()) + const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] + const txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddress, threshold).encodeABI() + + if (isCurrent) { + setData(txData) + } + } catch (error) { + console.error('Error calculating ERC721 transfer data:', error.message) + } + } + calculateRemoveOwnerData() + return () => { isCurrent = false } - }, [ownerAddress, safeAddress, values.threshold]) + }, [safeAddress, ownerAddress, threshold]) return ( <> @@ -95,7 +128,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre Any transaction requires the confirmation of: - {`${values.threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`} + {`${threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`}
@@ -165,11 +198,13 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre - - You're about to create a transaction and will have to confirm it with your currently connected wallet. -
- {`Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`} -
+
@@ -191,5 +226,3 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre ) } - -export default withStyles(styles as any)(ReviewRemoveOwner) diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/style.ts b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/style.ts index 90b7a749..090072fa 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/style.ts @@ -1,6 +1,7 @@ import { background, border, lg, secondaryText, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ root: { height: '372px', }, @@ -76,7 +77,9 @@ export const styles = () => ({ }, }, gasCostsContainer: { - padding: `0 ${lg}`, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', textAlign: 'center', width: '100%', }, diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx index 1968fcc4..10266f78 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/index.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import OwnerForm from './screens/OwnerForm' -import ReviewReplaceOwner from './screens/Review' import Modal from 'src/components/Modal' import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry' @@ -16,6 +15,7 @@ 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.d' +import { ReviewReplaceOwnerModal } from './screens/Review' const styles = createStyles({ biggerModalWindow: { @@ -28,9 +28,8 @@ const styles = createStyles({ const useStyles = makeStyles(styles) type OwnerValues = { - ownerAddress: string - ownerName: string - threshold: string + newOwnerAddress: string + newOwnerName: string } export const sendReplaceOwner = async ( @@ -44,7 +43,7 @@ export const sendReplaceOwner = async ( const safeOwners = await gnosisSafe.methods.getOwners().call() 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() + const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddressToRemove, values.newOwnerAddress).encodeABI() const txHash = await dispatch( createTransaction({ @@ -61,8 +60,8 @@ export const sendReplaceOwner = async ( replaceSafeOwner({ safeAddress, oldOwnerAddress: ownerAddressToRemove, - ownerAddress: values.ownerAddress, - ownerName: values.ownerName, + ownerAddress: values.newOwnerAddress, + ownerName: values.newOwnerName, }), ) } @@ -75,10 +74,18 @@ type ReplaceOwnerProps = { ownerName: string } -const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwnerProps): React.ReactElement => { +export const ReplaceOwnerModal = ({ + isOpen, + onClose, + ownerAddress, + ownerName, +}: ReplaceOwnerProps): React.ReactElement => { const classes = useStyles() const [activeScreen, setActiveScreen] = useState('checkOwner') - const [values, setValues] = useState({}) + const [values, setValues] = useState({ + newOwnerAddress: '', + newOwnerName: '', + }) const dispatch = useDispatch() const safeAddress = useSelector(safeParamAddressFromStateSelector) const threshold = useSelector(safeThresholdSelector) @@ -86,7 +93,10 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner useEffect( () => () => { setActiveScreen('checkOwner') - setValues({}) + setValues({ + newOwnerAddress: '', + newOwnerName: '', + }) }, [isOpen], ) @@ -96,9 +106,10 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner const ownerSubmitted = (newValues) => { const { ownerAddress, ownerName } = newValues const checksumAddr = checksumAddress(ownerAddress) - values.ownerName = ownerName - values.ownerAddress = checksumAddr - setValues(values) + setValues({ + newOwnerAddress: checksumAddr, + newOwnerName: ownerName, + }) setActiveScreen('reviewReplaceOwner') } @@ -108,7 +119,9 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, threshold) dispatch( - addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: values.ownerAddress, name: values.ownerName })), + addOrUpdateAddressBookEntry( + makeAddressBookEntry({ address: values.newOwnerAddress, name: values.newOwnerName }), + ), ) } catch (error) { console.error('Error while removing an owner', error) @@ -128,7 +141,7 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner )} {activeScreen === 'reviewReplaceOwner' && ( - ) } - -export default ReplaceOwner diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx index 231b8d7e..a56a069f 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx @@ -1,5 +1,5 @@ import IconButton from '@material-ui/core/IconButton' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import classNames from 'classnames' import React, { useEffect, useState } from 'react' @@ -7,8 +7,7 @@ import { useSelector } from 'react-redux' import { List } from 'immutable' import { ExplorerButton } from '@gnosis.pm/safe-react-components' -import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' -import { getExplorerInfo, getNetworkInfo } from 'src/config' +import { getExplorerInfo } from 'src/config' import CopyBtn from 'src/components/CopyBtn' import Identicon from 'src/components/Identicon' import Block from 'src/components/layout/Block' @@ -24,47 +23,75 @@ import { safeParamAddressFromStateSelector, safeThresholdSelector, } from 'src/logic/safe/store/selectors' -import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { styles } from './style' +import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFees } from 'src/components/TransactionsFees' export const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn' -const { nativeCoin } = getNetworkInfo() +const useStyles = makeStyles(styles) -const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddress, ownerName, values }) => { - const [gasCosts, setGasCosts] = useState('< 0.001') - const safeAddress = useSelector(safeParamAddressFromStateSelector) as string +type ReplaceOwnerProps = { + onClose: () => void + onClickBack: () => void + onSubmit: () => void + ownerAddress: string + ownerName: string + values: { + newOwnerAddress: string + newOwnerName: string + } +} + +export const ReviewReplaceOwnerModal = ({ + onClickBack, + onClose, + onSubmit, + ownerAddress, + ownerName, + values, +}: ReplaceOwnerProps): React.ReactElement => { + const classes = useStyles() + const [data, setData] = useState('') + const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeName = useSelector(safeNameSelector) const owners = useSelector(safeOwnersSelector) const threshold = useSelector(safeThresholdSelector) const addressBook = useSelector(addressBookSelector) const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([]) + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isCreation, + isOffChainSignature, + } = useEstimateTransactionGas({ + txData: data, + txRecipient: safeAddress, + }) + useEffect(() => { let isCurrent = true - const estimateGas = async () => { + const calculateReplaceOwnerData = async () => { const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) const safeOwners = await gnosisSafe.methods.getOwners().call() const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase()) const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1] - const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddress, values.ownerAddress).encodeABI() - const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData) - const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) - const formattedGasCosts = formatAmount(gasCosts) + const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddress, values.newOwnerAddress).encodeABI() if (isCurrent) { - setGasCosts(formattedGasCosts) + setData(txData) } } - estimateGas() + calculateReplaceOwnerData() return () => { isCurrent = false } - }, [ownerAddress, safeAddress, values.ownerAddress]) + }, [ownerAddress, safeAddress, values.newOwnerAddress]) return ( <> @@ -78,7 +105,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre - + @@ -172,19 +199,19 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre - + - {values.ownerName} + {values.newOwnerName} - {values.ownerAddress} + {values.newOwnerAddress} - - + + @@ -195,11 +222,13 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre - - You're about to create a transaction and will have to confirm it with your currently connected wallet. -
- {`Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`} -
+
@@ -221,5 +250,3 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre ) } - -export default withStyles(styles as any)(ReviewRemoveOwner) diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts index 07df003a..5bbbe831 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts @@ -1,6 +1,7 @@ import { background, border, lg, secondaryText, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ root: { height: '372px', }, @@ -81,7 +82,9 @@ export const styles = () => ({ }, }, gasCostsContainer: { - padding: `0 ${lg}`, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', textAlign: 'center', width: '100%', }, diff --git a/src/routes/safe/components/Settings/ManageOwners/index.tsx b/src/routes/safe/components/Settings/ManageOwners/index.tsx index df255ca5..f4cb8f19 100644 --- a/src/routes/safe/components/Settings/ManageOwners/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/index.tsx @@ -11,8 +11,8 @@ import RemoveOwnerIcon from '../assets/icons/bin.svg' import AddOwnerModal from './AddOwnerModal' import { EditOwnerModal } from './EditOwnerModal' import OwnerAddressTableCell from './OwnerAddressTableCell' -import RemoveOwnerModal from './RemoveOwnerModal' -import ReplaceOwnerModal from './ReplaceOwnerModal' +import { RemoveOwnerModal } from './RemoveOwnerModal' +import { ReplaceOwnerModal } from './ReplaceOwnerModal' import RenameOwnerIcon from './assets/icons/rename-owner.svg' import ReplaceOwnerIcon from './assets/icons/replace-owner.svg' import { OWNERS_TABLE_ADDRESS_ID, OWNERS_TABLE_NAME_ID, generateColumns, getOwnerData } from './dataFetcher' diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx index 58fb82e3..d28cbb5d 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx @@ -1,10 +1,8 @@ import IconButton from '@material-ui/core/IconButton' import MenuItem from '@material-ui/core/MenuItem' -import { withStyles } from '@material-ui/core/styles' +import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' import React, { useEffect, useState } from 'react' -import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' -import { getNetworkInfo } from 'src/config' import { styles } from './style' import Field from 'src/components/forms/Field' @@ -18,35 +16,60 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts' -import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' +import { SafeOwner } from 'src/logic/safe/store/models/safe' +import { List } from 'immutable' +import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' + +import { TransactionFees } from 'src/components/TransactionsFees' const THRESHOLD_FIELD_NAME = 'threshold' -const { nativeCoin } = getNetworkInfo() +const useStyles = makeStyles(styles) -const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddress, threshold }) => { - const [gasCosts, setGasCosts] = useState('< 0.001') +type ChangeThresholdModalProps = { + onChangeThreshold: (newThreshold: number) => void + onClose: () => void + owners?: List + safeAddress: string + threshold?: number +} + +export const ChangeThresholdModal = ({ + onChangeThreshold, + onClose, + owners, + safeAddress, + threshold = 1, +}: ChangeThresholdModalProps): React.ReactElement => { + const classes = useStyles() + const [data, setData] = useState('') + + const { + gasCostFormatted, + txEstimationExecutionStatus, + isCreation, + isExecution, + isOffChainSignature, + } = useEstimateTransactionGas({ + txData: data, + txRecipient: safeAddress, + }) useEffect(() => { let isCurrent = true - const estimateGasCosts = async () => { + const calculateChangeThresholdData = async () => { const safeInstance = await getGnosisSafeInstanceAt(safeAddress) - const txData = safeInstance.methods.changeThreshold('1').encodeABI() - const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData) - const gasCostsAsEth = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) - const formattedGasCosts = formatAmount(gasCostsAsEth) + const txData = safeInstance.methods.changeThreshold(threshold).encodeABI() if (isCurrent) { - setGasCosts(formattedGasCosts) + setData(txData) } } - estimateGasCosts() - + calculateChangeThresholdData() return () => { isCurrent = false } - }, [safeAddress]) + }, [safeAddress, threshold]) const handleSubmit = (values) => { const newThreshold = values[THRESHOLD_FIELD_NAME] @@ -81,7 +104,7 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr render={(props) => ( <> - {[...Array(Number(owners.size))].map((x, index) => ( + {[...Array(Number(owners?.size))].map((x, index) => ( {index + 1} @@ -99,14 +122,18 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr - {`out of ${owners.size} owner(s)`} + {`out of ${owners?.size} owner(s)`} - - {`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`} - +
@@ -124,5 +151,3 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr ) } - -export default withStyles(styles as any)(ChangeThreshold) diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts index e0f9c000..e3f31c05 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts +++ b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/style.ts @@ -1,6 +1,7 @@ import { lg, md, secondaryText, sm } from 'src/theme/variables' +import { createStyles } from '@material-ui/core' -export const styles = () => ({ +export const styles = createStyles({ heading: { padding: `${sm} ${lg}`, justifyContent: 'space-between', diff --git a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx index ccc3f4e5..753f758c 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx @@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles' import React, { useState, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' -import ChangeThreshold from './ChangeThreshold' +import { ChangeThresholdModal } from './ChangeThreshold' import { styles } from './style' import Modal from 'src/components/Modal' @@ -30,7 +30,7 @@ const ThresholdSettings = (): React.ReactElement => { const [isModalOpen, setModalOpen] = useState(false) const dispatch = useDispatch() const threshold = useSelector(safeThresholdSelector) - const safeAddress = useSelector(safeParamAddressFromStateSelector) as string + const safeAddress = useSelector(safeParamAddressFromStateSelector) const owners = useSelector(safeOwnersSelector) const granted = useSelector(grantedSelector) @@ -38,7 +38,7 @@ const ThresholdSettings = (): React.ReactElement => { setModalOpen((prevOpen) => !prevOpen) } - const onChangeThreshold = async (newThreshold) => { + const onChangeThreshold = async (newThreshold: number) => { const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const txData = safeInstance.methods.changeThreshold(newThreshold).encodeABI() @@ -87,7 +87,7 @@ const ThresholdSettings = (): React.ReactElement => { open={isModalOpen} title="Change Required Confirmations" > - { - let isCurrent = true - - const estimateGas = async () => { - const estimatedGasCosts = await estimateTxGasCosts( - safeAddress, - tx.recipient, - tx.data as string, - tx, - approveAndExecute ? userAddress : undefined, - ) - const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) - const formattedGasCosts = formatAmount(gasCosts) - if (isCurrent) { - setGasCosts(formattedGasCosts) - } - } - - estimateGas() - - return () => { - isCurrent = false - } - }, [approveAndExecute, safeAddress, tx, userAddress]) + const { + gasCostFormatted, + txEstimationExecutionStatus, + isExecution, + isOffChainSignature, + isCreation, + } = useEstimateTransactionGas({ + txRecipient: tx.recipient, + txData: tx.data || '', + txConfirmations: tx.confirmations, + txAmount: tx.value, + preApprovingOwner: approveAndExecute ? userAddress : undefined, + safeTxGas: tx.safeTxGas, + operation: tx.operation, + }) const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute) @@ -159,15 +146,13 @@ const ApproveTxModal = ({ )} - - - {`You're about to ${ - approveAndExecute ? 'execute' : 'approve' - } a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${ - nativeCoin.name - } in this wallet to fund this confirmation.`} - - +