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 (
+
+
+
+ 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) => (
@@ -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.`}
-
-
+
-
- {`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.`}
-
+
@@ -118,5 +110,3 @@ const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElement => {
)
}
-
-export default RejectTxModal
diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx
index da836962..274acf47 100644
--- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx
+++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx
@@ -3,9 +3,9 @@ import React, { ReactElement, useMemo, useState } from 'react'
import { useSelector } from 'react-redux'
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
-import ApproveTxModal from './ApproveTxModal'
+import { ApproveTxModal } from './ApproveTxModal'
import OwnersColumn from './OwnersColumn'
-import RejectTxModal from './RejectTxModal'
+import { RejectTxModal } from './RejectTxModal'
import TxDescription from './TxDescription'
import { IncomingTx } from './IncomingTx'
import { CreationTx } from './CreationTx'
diff --git a/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts b/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts
index db0b015c..c10615b2 100644
--- a/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts
+++ b/src/routes/safe/store/actions/transactions/__tests__/utils.test.ts
@@ -66,7 +66,7 @@ describe('getNewTxNonce', () => {
const expectedResult = '2'
// when
- const result = await getNewTxNonce(undefined, lastTx, safeInstance)
+ const result = await getNewTxNonce(lastTx, safeInstance)
// then
expect(result).toBe(expectedResult)
@@ -82,7 +82,7 @@ describe('getNewTxNonce', () => {
safeInstance.methods.nonce = mockFnNonce
// when
- const result = await getNewTxNonce(undefined, null, safeInstance)
+ const result = await getNewTxNonce(null, safeInstance)
// then
expect(result).toBe(expectedResult)
@@ -98,19 +98,7 @@ describe('getNewTxNonce', () => {
const lastTx = getMockedTxServiceModel({ nonce: 10 })
// when
- const result = await getNewTxNonce(undefined, lastTx, safeInstance)
-
- // then
- expect(result).toBe(expectedResult)
- })
- it('Given a pre-calculated nonce number should return it', async () => {
- // given
- const safeInstance = getMockedSafeInstance({})
- const expectedResult = '114'
- const nextNonce = '114'
-
- // when
- const result = await getNewTxNonce(nextNonce, null, safeInstance)
+ const result = await getNewTxNonce(lastTx, safeInstance)
// then
expect(result).toBe(expectedResult)
diff --git a/src/test/signatures.blockchain.ts b/src/test/signatures.blockchain.ts
index 86b983df..1182eea1 100644
--- a/src/test/signatures.blockchain.ts
+++ b/src/test/signatures.blockchain.ts
@@ -1,8 +1,14 @@
-//
+//
import { List } from 'immutable'
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
+import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
+import { makeConfirmation } from 'src/logic/safe/store/models/confirmation'
-const makeMockConfirmation = (address) => ({ owner: { address } })
+const makeMockConfirmation = (address: string): Confirmation => {
+ return makeConfirmation({
+ owner: address
+ })
+}
describe('Signatures Blockchain Test', () => {
it('generates signatures in natural order even checksumed', async () => {