mirror of
https://github.com/status-im/safe-react.git
synced 2025-01-11 10:34:06 +00:00
* Makes getGasEstimationTxResponse exportable * Add check for failing txs on approveTxModal * Adds styles for reviewTx * Adds useTxSuccessCheck hook * Adds checkIfTxWillFail function * Uses checkIfTxWillFailAsync on reviewTx modal * Improves approveTx modal * Add check for failing transaction in contract interaction modal * Add check for reviewCollectible * Fix check on sendFunds reviewTx * Adds styling for contractInteraction modal * Fix gas calculation for native token transfers * Rename estimateDataGasCosts to parseRequiredTxGasResponse Adds getPreValidatedSignatures Refactor estimateTxGasCosts Refactor checkIfExecTxWillFail * Refactor checkIfExecTxWill usage * Refactor checkIfTxWillFailAsync in ReviewTx * Use getPreValidatedSignatures in createTransaction() * Refactor estimateTxGasCosts Rename estimateSafeTxGas to estimateExecTransactionGas * Refactor ReviewTx: extract useEffects to hooks * Remove unnecessary gas transfer amount * Refactor estimateTxGasCosts: extract checkIfTxIsExecution and estimateTxGas * Fix tx amount Remove console log * Moves useCheckIfTransactionWillFail to logic/hooks folder * Replaces useEffect usage with useCheckIfTransactionWillFail hook Also fix how some modals fetch the safeAddress * Improves modal's wording * Fix error parsing the cancel transaction error message from GETH nodes * Remove useCheckIfTransactionWillFail Adds useEstimateTransactionGas Renames estimateTxGas to estimateTransactionGas Removes estimateTxGasCosts Removes checkIfExecTxWillFail * Replace useCheckIfTransactionWillFail from modals with useEstimateTransactionGas * Replace estimateGasCosts from every review tx modal with useEstimateTransactionGas * Replace estimateGasCosts from every review tx modal with useEstimateTransactionGas * Extract isExecution calculation to useEstimateTransactionGas * Creates TransactionFailText * Uses TransactionFailText in the review modals * Fix wrong selector usage * Fix missing null check on cancel tx confirmations * Add guard for CLOSE_SNACKBAR action when tx was already dismissed * Improves useEstimateTransactionGas in review custom tx and contract interaction review * Fix review replace/remove/add owner modals styling * Refactor response of useEstimateTransactionGas * Remove safeAddress as param to the useEstimateTransactionGas * Improves how threshold is obtained in useEstimateTransactionGas.tsx * Rename gasCostHumanReadable to gasCostFormatted * Add operation to useEstimateTransactionGas * Refactor ConfirmTransactionModal to use useEstimateTransactionGas * Refactor proccessTransaction to use getPreValidatedSignatures method * Fix default export of ApproveTxModal * Rename estimateExecTransactionGas to estimateGasForTransactionCreation Remove estimateTransactionGas from gas.ts * Make estimateGasForTransactionCreation throw error instead of 0 gas * Adds estimateGasForTransactionExecution and estimateGasForTransactionApproval to gas.ts * Move estimateTransactionGas to useEstimateTransactionGas Refactors useEstimateTransactionGas to return isCreation and isOffChainSignature * Type and refactor generateSignaturesFromTxConfirmations Moves getPreValidatedSignatures to safeTxSigner.ts * Uses confirmations to estimateGasForTransactionExecution * Adds TransactionFeesText component Uses TransactionFeesText on ApproveTxModal * Pass more parameters to estimateGasForTransactionExecution * Removes unnecessary parameter in getNewTxNonce * Moves checkIfOffChainSignatureIsPossible to safeTxSigner.ts * Fix check for null confirmations * Uses checkIfOffChainSignatureIsPossible on createTransaction.ts * Move TransactionFailText inside TransactionFees component * Pass safeTxGas to useEstimateTransactionGas.tsx Improves how we use default params * Fix gas iteration on estimateGasForTransactionExecution * Fix estimateGasForTransactionExecution calculation * Fix generateSignaturesFromTxConfirmations calculation * Remove unnecessary Promise and await * Fix estimateGasForTransactionExecution for preApproving owner case * Improve logging * Uses operation in useEstimateTransactionGas Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm> Co-authored-by: Fernando <fernando.greco@gmail.com>
This commit is contained in:
parent
618888ed07
commit
8a774f2e66
@ -17,7 +17,7 @@ const useStyles = makeStyles(
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '120px',
|
top: '120px',
|
||||||
width: '500px',
|
width: '500px',
|
||||||
height: '540px',
|
height: '580px',
|
||||||
borderRadius: sm,
|
borderRadius: sm,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)',
|
boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)',
|
||||||
|
56
src/components/TransactionFailText/index.tsx
Normal file
56
src/components/TransactionFailText/index.tsx
Normal file
@ -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 (
|
||||||
|
<Row align="center">
|
||||||
|
<Paragraph color="error" className={classes.executionWarningRow}>
|
||||||
|
<Img alt="Info Tooltip" height={16} src={InfoIcon} className={classes.warningIcon} />
|
||||||
|
This transaction will most likely fail. {errorMessage}
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
43
src/components/TransactionsFees/index.tsx
Normal file
43
src/components/TransactionsFees/index.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<Paragraph>
|
||||||
|
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.`}
|
||||||
|
</Paragraph>
|
||||||
|
<TransactionFailText txEstimationExecutionStatus={txEstimationExecutionStatus} isExecution={isExecution} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
241
src/logic/hooks/useEstimateTransactionGas.tsx
Normal file
241
src/logic/hooks/useEstimateTransactionGas.tsx
Normal file
@ -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<Confirmation>
|
||||||
|
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<number> => {
|
||||||
|
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<Confirmation>
|
||||||
|
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<TransactionGasEstimationResult>({
|
||||||
|
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
|
||||||
|
}
|
@ -20,7 +20,7 @@ export default handleActions(
|
|||||||
const { dismissAll, key } = action.payload
|
const { dismissAll, key } = action.payload
|
||||||
|
|
||||||
if (key) {
|
if (key) {
|
||||||
return state.update(key, (prev) => prev.set('dismissed', true))
|
return state.update(key, (prev) => prev?.set('dismissed', true))
|
||||||
}
|
}
|
||||||
if (dismissAll) {
|
if (dismissAll) {
|
||||||
return state.withMutations((map) => {
|
return state.withMutations((map) => {
|
||||||
|
@ -1,31 +1,61 @@
|
|||||||
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
|
import { List } from 'immutable'
|
||||||
// https://github.com/gnosis/safe-contracts/blob/master/test/gnosisSafeTeamEdition.js#L26
|
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||||
export const generateSignaturesFromTxConfirmations = (confirmations, preApprovingOwner) => {
|
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||||
// The constant parts need to be sorted so that the recovered signers are sorted ascending
|
import semverSatisfies from 'semver/functions/satisfies'
|
||||||
// (natural order) by address (not checksummed).
|
import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES } from './transactions/offchainSigner'
|
||||||
const confirmationsMap = confirmations.reduce((map, obj) => {
|
|
||||||
map[obj.owner.toLowerCase()] = obj // eslint-disable-line no-param-reassign
|
// Here we're checking that safe contract version is greater or equal 1.1.1, but
|
||||||
return map
|
// 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<Confirmation>,
|
||||||
|
preApprovingOwner?: string,
|
||||||
|
): string => {
|
||||||
|
let confirmationsMap =
|
||||||
|
confirmations?.map((value) => {
|
||||||
|
return {
|
||||||
|
signature: value.signature,
|
||||||
|
owner: value.owner.toLowerCase(),
|
||||||
|
}
|
||||||
|
}) || List([])
|
||||||
|
|
||||||
if (preApprovingOwner) {
|
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'
|
let sigs = '0x'
|
||||||
Object.keys(confirmationsMap)
|
confirmationsMap.forEach(({ signature, owner }) => {
|
||||||
.sort()
|
if (signature) {
|
||||||
.forEach((addr) => {
|
sigs += signature.slice(2)
|
||||||
const conf = confirmationsMap[addr]
|
} else {
|
||||||
if (conf.signature) {
|
// https://docs.gnosis.io/safe/docs/contracts_signatures/#pre-validated-signatures
|
||||||
sigs += conf.signature.slice(2)
|
sigs += getPreValidatedSignatures(owner, '')
|
||||||
} else {
|
}
|
||||||
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
|
})
|
||||||
sigs += `000000000000000000000000${addr.replace(
|
|
||||||
'0x',
|
|
||||||
'',
|
|
||||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return sigs
|
return sigs
|
||||||
}
|
}
|
||||||
|
@ -3,22 +3,8 @@ import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
|||||||
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||||
|
|
||||||
describe('Store actions utils > getNewTxNonce', () => {
|
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 () => {
|
it(`Should return nonce of a last transaction + 1 if passed nonce is less than last transaction or invalid`, async () => {
|
||||||
// Given
|
// Given
|
||||||
const txNonce = ''
|
|
||||||
const lastTx = { nonce: 44 } as TxServiceModel
|
const lastTx = { nonce: 44 } as TxServiceModel
|
||||||
const safeInstance = {
|
const safeInstance = {
|
||||||
methods: {
|
methods: {
|
||||||
@ -29,7 +15,7 @@ describe('Store actions utils > getNewTxNonce', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// When
|
// When
|
||||||
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe)
|
const nonce = await getNewTxNonce(lastTx, safeInstance as GnosisSafe)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(nonce).toBe('45')
|
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 () => {
|
it(`Should retrieve contract's instance nonce value as a fallback, if txNonce and lastTx are not valid`, async () => {
|
||||||
// Given
|
// Given
|
||||||
const txNonce = ''
|
|
||||||
const lastTx = null
|
const lastTx = null
|
||||||
const safeInstance = {
|
const safeInstance = {
|
||||||
methods: {
|
methods: {
|
||||||
@ -48,7 +33,7 @@ describe('Store actions utils > getNewTxNonce', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// When
|
// When
|
||||||
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe)
|
const nonce = await getNewTxNonce(lastTx, safeInstance as GnosisSafe)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(nonce).toBe('45')
|
expect(nonce).toBe('45')
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { push } from 'connected-react-router'
|
import { push } from 'connected-react-router'
|
||||||
import semverSatisfies from 'semver/functions/satisfies'
|
|
||||||
import { ThunkAction } from 'redux-thunk'
|
import { ThunkAction } from 'redux-thunk'
|
||||||
|
|
||||||
import { onboardUser } from 'src/components/ConnectButton'
|
import { onboardUser } from 'src/components/ConnectButton'
|
||||||
@ -10,11 +9,10 @@ import {
|
|||||||
CALL,
|
CALL,
|
||||||
getApprovalTransaction,
|
getApprovalTransaction,
|
||||||
getExecutionTransaction,
|
getExecutionTransaction,
|
||||||
SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES,
|
|
||||||
saveTxToHistory,
|
saveTxToHistory,
|
||||||
tryOffchainSigning,
|
tryOffchainSigning,
|
||||||
} from 'src/logic/safe/transactions'
|
} 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 { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
|
||||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
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 { PayableTx } from 'src/types/contracts/types.d'
|
||||||
import { AppReduxState } from 'src/store'
|
import { AppReduxState } from 'src/store'
|
||||||
import { Dispatch, DispatchReturn } from './types'
|
import { Dispatch, DispatchReturn } from './types'
|
||||||
|
import { checkIfOffChainSignatureIsPossible, getPreValidatedSignatures } from 'src/logic/safe/safeTxSigner'
|
||||||
|
|
||||||
export interface CreateTransactionArgs {
|
export interface CreateTransactionArgs {
|
||||||
navigateToTransactionsTab?: boolean
|
navigateToTransactionsTab?: boolean
|
||||||
@ -87,18 +86,18 @@ const createTransaction = (
|
|||||||
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
|
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
|
||||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
const lastTx = await getLastTx(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 isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
|
||||||
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
||||||
const safeTxGas =
|
let safeTxGas
|
||||||
safeTxGasArg || (await estimateSafeTxGas(safeInstance, safeAddress, txData, to, valueInWei, operation))
|
try {
|
||||||
|
safeTxGas =
|
||||||
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
|
safeTxGasArg || (await estimateGasForTransactionCreation(safeAddress, txData, to, valueInWei, operation))
|
||||||
const sigs = `0x000000000000000000000000${from.replace(
|
} catch (error) {
|
||||||
'0x',
|
safeTxGas = safeTxGasArg || 0
|
||||||
'',
|
}
|
||||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
|
||||||
|
|
||||||
|
const sigs = getPreValidatedSignatures(from)
|
||||||
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin)
|
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin)
|
||||||
const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
|
const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
|
||||||
|
|
||||||
@ -123,11 +122,7 @@ const createTransaction = (
|
|||||||
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
|
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Here we're checking that safe contract version is greater or equal 1.1.1, but
|
if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) {
|
||||||
// theoretically EIP712 should also work for 1.0.0 contracts
|
|
||||||
const canTryOffchainSigning =
|
|
||||||
!isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
|
|
||||||
if (canTryOffchainSigning) {
|
|
||||||
const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
||||||
|
|
||||||
if (signature) {
|
if (signature) {
|
||||||
@ -141,9 +136,7 @@ const createTransaction = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = isExecution
|
const tx = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, safeTxHash)
|
||||||
? await getExecutionTransaction(txArgs)
|
|
||||||
: await getApprovalTransaction(safeInstance, safeTxHash)
|
|
||||||
const sendParams: PayableTx = { from, value: 0 }
|
const sendParams: PayableTx = { from, value: 0 }
|
||||||
|
|
||||||
// if not set owner management tests will fail on ganache
|
// if not set owner management tests will fail on ganache
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { AnyAction } from 'redux'
|
import { AnyAction } from 'redux'
|
||||||
import { ThunkAction } from 'redux-thunk'
|
import { ThunkAction } from 'redux-thunk'
|
||||||
import semverSatisfies from 'semver/functions/satisfies'
|
|
||||||
|
|
||||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||||
import { getNotificationsFromTxType } from 'src/logic/notifications'
|
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 { 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 { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
|
||||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||||
import { providerSelector } from 'src/logic/wallets/store/selectors'
|
import { providerSelector } from 'src/logic/wallets/store/selectors'
|
||||||
@ -33,7 +36,7 @@ interface ProcessTransactionArgs {
|
|||||||
|
|
||||||
type ProcessTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
|
type ProcessTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
|
||||||
|
|
||||||
const processTransaction = ({
|
export const processTransaction = ({
|
||||||
approveAndExecute,
|
approveAndExecute,
|
||||||
notifiedTransaction,
|
notifiedTransaction,
|
||||||
safeAddress,
|
safeAddress,
|
||||||
@ -49,17 +52,15 @@ const processTransaction = ({
|
|||||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
|
||||||
const lastTx = await getLastTx(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 isExecution = approveAndExecute || (await shouldExecuteTransaction(safeInstance, nonce, lastTx))
|
||||||
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
||||||
|
|
||||||
let sigs = generateSignaturesFromTxConfirmations(tx.confirmations, approveAndExecute && userAddress)
|
const preApprovingOwner = approveAndExecute ? userAddress : undefined
|
||||||
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
|
let sigs = generateSignaturesFromTxConfirmations(tx.confirmations, preApprovingOwner)
|
||||||
|
|
||||||
if (!sigs) {
|
if (!sigs) {
|
||||||
sigs = `0x000000000000000000000000${from.replace(
|
sigs = getPreValidatedSignatures(from)
|
||||||
'0x',
|
|
||||||
'',
|
|
||||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, tx.origin)
|
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, tx.origin)
|
||||||
@ -86,14 +87,7 @@ const processTransaction = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Here we're checking that safe contract version is greater or equal 1.1.1, but
|
if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) {
|
||||||
// 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) {
|
|
||||||
const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
||||||
|
|
||||||
if (signature) {
|
if (signature) {
|
||||||
@ -109,9 +103,7 @@ const processTransaction = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction = isExecution
|
transaction = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, tx.safeTxHash)
|
||||||
? await getExecutionTransaction(txArgs)
|
|
||||||
: await getApprovalTransaction(safeInstance, tx.safeTxHash)
|
|
||||||
|
|
||||||
const sendParams: any = { from, value: 0 }
|
const sendParams: any = { from, value: 0 }
|
||||||
|
|
||||||
@ -196,5 +188,3 @@ const processTransaction = ({
|
|||||||
|
|
||||||
return txHash
|
return txHash
|
||||||
}
|
}
|
||||||
|
|
||||||
export default processTransaction
|
|
||||||
|
@ -16,15 +16,7 @@ export const getLastTx = async (safeAddress: string): Promise<TxServiceModel | n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNewTxNonce = async (
|
export const getNewTxNonce = async (lastTx: TxServiceModel | null, safeInstance: GnosisSafe): Promise<string> => {
|
||||||
txNonce: string | undefined,
|
|
||||||
lastTx: TxServiceModel | null,
|
|
||||||
safeInstance: GnosisSafe,
|
|
||||||
): Promise<string> => {
|
|
||||||
if (txNonce) {
|
|
||||||
return txNonce
|
|
||||||
}
|
|
||||||
|
|
||||||
// use current's safe nonce as fallback
|
// use current's safe nonce as fallback
|
||||||
return lastTx ? `${lastTx.nonce + 1}` : (await safeInstance.methods.nonce().call()).toString()
|
return lastTx ? `${lastTx.nonce + 1}` : (await safeInstance.methods.nonce().call()).toString()
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
@ -1,19 +1,15 @@
|
|||||||
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
|
||||||
import { BigNumber } from 'bignumber.js'
|
import { BigNumber } from 'bignumber.js'
|
||||||
import { AbiItem } from 'web3-utils'
|
|
||||||
|
|
||||||
import { CALL } from '.'
|
|
||||||
|
|
||||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||||
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
|
import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
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 { sameString } from 'src/utils/strings'
|
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) => {
|
const reducer = (accumulator, currentValue) => {
|
||||||
if (currentValue === EMPTY_DATA) {
|
if (currentValue === EMPTY_DATA) {
|
||||||
return accumulator + 0
|
return accumulator + 0
|
||||||
@ -29,74 +25,15 @@ const estimateDataGasCosts = (data: string): number => {
|
|||||||
return data.match(/.{2}/g)?.reduce(reducer, 0)
|
return data.match(/.{2}/g)?.reduce(reducer, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const estimateTxGasCosts = async (
|
|
||||||
safeAddress: string,
|
|
||||||
to: string,
|
|
||||||
data: string,
|
|
||||||
tx?: Transaction,
|
|
||||||
preApprovingOwner?: string,
|
|
||||||
): Promise<number> => {
|
|
||||||
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
|
// Parses the result from the error message (GETH, OpenEthereum/Parity and Nethermind) and returns the data value
|
||||||
export const getDataFromNodeErrorMessage = (errorMessage: string): string | undefined => {
|
export const getDataFromNodeErrorMessage = (errorMessage: string): string | undefined => {
|
||||||
|
// Replace illegal characters that often comes within the error string (like <20> 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
|
// Extracts JSON object from the error message
|
||||||
const [, ...error] = errorMessage.split('\n')
|
const [, ...error] = normalizedErrorString.split('\n')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const errorAsJSON = JSON.parse(error.join(''))
|
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
|
to: string
|
||||||
from: string
|
from: string
|
||||||
data: string
|
data: string
|
||||||
@ -190,8 +127,7 @@ const calculateMinimumGasForTransaction = async (
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export const estimateSafeTxGas = async (
|
export const estimateGasForTransactionCreation = async (
|
||||||
safe: GnosisSafe | undefined,
|
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
data: string,
|
data: string,
|
||||||
to: string,
|
to: string,
|
||||||
@ -199,10 +135,7 @@ export const estimateSafeTxGas = async (
|
|||||||
operation: number,
|
operation: number,
|
||||||
): Promise<number> => {
|
): Promise<number> => {
|
||||||
try {
|
try {
|
||||||
let safeInstance = safe
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
if (!safeInstance) {
|
|
||||||
safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
const estimateData = safeInstance.methods.requiredTxGas(to, valueInWei, data, operation).encodeABI()
|
const estimateData = safeInstance.methods.requiredTxGas(to, valueInWei, data, operation).encodeABI()
|
||||||
const gasEstimationResponse = await getGasEstimationTxResponse({
|
const gasEstimationResponse = await getGasEstimationTxResponse({
|
||||||
@ -214,7 +147,7 @@ export const estimateSafeTxGas = async (
|
|||||||
const txGasEstimation = gasEstimationResponse + 10000
|
const txGasEstimation = gasEstimationResponse + 10000
|
||||||
|
|
||||||
// 21000 - additional gas costs (e.g. base tx costs, transfer costs)
|
// 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]
|
const additionalGasBatches = [0, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000]
|
||||||
|
|
||||||
return await calculateMinimumGasForTransaction(
|
return await calculateMinimumGasForTransaction(
|
||||||
@ -225,7 +158,98 @@ export const estimateSafeTxGas = async (
|
|||||||
dataGasEstimation,
|
dataGasEstimation,
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error calculating tx gas estimation', error)
|
console.info('Error calculating tx gas estimation', error.message)
|
||||||
return 0
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TransactionExecutionEstimationProps = {
|
||||||
|
txData: string
|
||||||
|
safeAddress: string
|
||||||
|
txRecipient: string
|
||||||
|
txConfirmations?: List<Confirmation>
|
||||||
|
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<number> => {
|
||||||
|
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<number> => {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
@ -30,10 +30,7 @@ export const getTransactionHash = async ({
|
|||||||
return txHash
|
return txHash
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getApprovalTransaction = async (
|
export const getApprovalTransaction = (safeInstance: GnosisSafe, txHash: string): NonPayableTransactionObject<void> => {
|
||||||
safeInstance: GnosisSafe,
|
|
||||||
txHash: string,
|
|
||||||
): Promise<NonPayableTransactionObject<void>> => {
|
|
||||||
try {
|
try {
|
||||||
return safeInstance.methods.approveHash(txHash)
|
return safeInstance.methods.approveHash(txHash)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -32,7 +32,7 @@ import { LoadingContainer } from 'src/components/LoaderContainer/index'
|
|||||||
import { TIMEOUT } from 'src/utils/constants'
|
import { TIMEOUT } from 'src/utils/constants'
|
||||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||||
|
|
||||||
import ConfirmTransactionModal from '../components/ConfirmTransactionModal'
|
import { ConfirmTransactionModal } from '../components/ConfirmTransactionModal'
|
||||||
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
||||||
import { useLegalConsent } from '../hooks/useLegalConsent'
|
import { useLegalConsent } from '../hooks/useLegalConsent'
|
||||||
import LegalDisclaimer from './LegalDisclaimer'
|
import LegalDisclaimer from './LegalDisclaimer'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
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 { Transaction } from '@gnosis.pm/safe-apps-sdk-v1'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { useDispatch } from 'react-redux'
|
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 { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||||
import { DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
import { DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||||
import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend'
|
import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend'
|
||||||
import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas'
|
|
||||||
|
|
||||||
import GasEstimationInfo from './GasEstimationInfo'
|
import GasEstimationInfo from './GasEstimationInfo'
|
||||||
import { getNetworkInfo } from 'src/config'
|
import { getNetworkInfo } from 'src/config'
|
||||||
import { TransactionParams } from './AppFrame'
|
import { TransactionParams } from './AppFrame'
|
||||||
|
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
|
|
||||||
const isTxValid = (t: Transaction): boolean => {
|
const isTxValid = (t: Transaction): boolean => {
|
||||||
if (!['string', 'number'].includes(typeof t.value)) {
|
if (!['string', 'number'].includes(typeof t.value)) {
|
||||||
@ -82,7 +82,7 @@ type OwnProps = {
|
|||||||
|
|
||||||
const { nativeCoin } = getNetworkInfo()
|
const { nativeCoin } = getNetworkInfo()
|
||||||
|
|
||||||
const ConfirmTransactionModal = ({
|
export const ConfirmTransactionModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
app,
|
app,
|
||||||
txs,
|
txs,
|
||||||
@ -95,32 +95,18 @@ const ConfirmTransactionModal = ({
|
|||||||
onTxReject,
|
onTxReject,
|
||||||
}: OwnProps): React.ReactElement | null => {
|
}: OwnProps): React.ReactElement | null => {
|
||||||
const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0)
|
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(() => {
|
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) {
|
if (params?.safeTxGas) {
|
||||||
estimateGas()
|
setEstimatedSafeTxGas(gasEstimation)
|
||||||
}
|
}
|
||||||
}, [params, safeAddress, txs])
|
}, [params, gasEstimation])
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@ -205,7 +191,7 @@ const ConfirmTransactionModal = ({
|
|||||||
<GasEstimationInfo
|
<GasEstimationInfo
|
||||||
appEstimation={params.safeTxGas}
|
appEstimation={params.safeTxGas}
|
||||||
internalEstimation={estimatedSafeTxGas}
|
internalEstimation={estimatedSafeTxGas}
|
||||||
loading={estimatingGas}
|
loading={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -229,5 +215,3 @@ const ConfirmTransactionModal = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConfirmTransactionModal
|
|
||||||
|
@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { getNetworkInfo } from 'src/config'
|
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 AddressInfo from 'src/components/AddressInfo'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Button from 'src/components/layout/Button'
|
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 Row from 'src/components/layout/Row'
|
||||||
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
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 { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
||||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
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 Header from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
||||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
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 { generateFormFieldKey, getValueFromTxInputs } from '../utils'
|
||||||
|
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
|
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
@ -45,41 +45,41 @@ const { nativeCoin } = getNetworkInfo()
|
|||||||
const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const { address: safeAddress } = useSelector(safeSelector) || {}
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
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(() => {
|
useEffect(() => {
|
||||||
let isCurrent = true
|
setTxParameters({
|
||||||
|
txRecipient: tx.contractAddress as string,
|
||||||
const estimateGas = async (): Promise<void> => {
|
txAmount: tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0',
|
||||||
const txData = tx.data ? tx.data.trim() : ''
|
txData: tx.data ? tx.data.trim() : '',
|
||||||
|
})
|
||||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.contractAddress as string, txData)
|
}, [tx.contractAddress, tx.value, tx.data, safeAddress])
|
||||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
|
||||||
const formattedGasCosts = formatAmount(gasCosts)
|
|
||||||
|
|
||||||
if (isCurrent) {
|
|
||||||
setGasCosts(formattedGasCosts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
estimateGas()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isCurrent = false
|
|
||||||
}
|
|
||||||
}, [safeAddress, tx.contractAddress, tx.data])
|
|
||||||
|
|
||||||
const submitTx = async () => {
|
const submitTx = async () => {
|
||||||
const txRecipient = tx.contractAddress
|
if (safeAddress && txParameters) {
|
||||||
const txData = tx.data ? tx.data.trim() : ''
|
|
||||||
const txValue = tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0'
|
|
||||||
if (safeAddress) {
|
|
||||||
dispatch(
|
dispatch(
|
||||||
createTransaction({
|
createTransaction({
|
||||||
safeAddress,
|
safeAddress,
|
||||||
to: txRecipient as string,
|
to: txParameters?.txRecipient,
|
||||||
valueInWei: txValue,
|
valueInWei: txParameters?.txAmount,
|
||||||
txData,
|
txData: txParameters?.txData,
|
||||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -162,9 +162,13 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Paragraph>
|
<TransactionFees
|
||||||
{`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.`}
|
gasCostFormatted={gasCostFormatted}
|
||||||
</Paragraph>
|
isExecution={isExecution}
|
||||||
|
isCreation={isCreation}
|
||||||
|
isOffChainSignature={isOffChainSignature}
|
||||||
|
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||||
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
|
|
||||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
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 CopyBtn from 'src/components/CopyBtn'
|
||||||
import Identicon from 'src/components/Identicon'
|
import Identicon from 'src/components/Identicon'
|
||||||
import Block from 'src/components/layout/Block'
|
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 Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
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 { 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 { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
||||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
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 { styles } from './style'
|
||||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||||
|
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
|
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||||
|
|
||||||
export type CustomTx = {
|
export type CustomTx = {
|
||||||
contractAddress?: string
|
contractAddress?: string
|
||||||
@ -49,29 +49,19 @@ const { nativeCoin } = getNetworkInfo()
|
|||||||
const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const { address: safeAddress } = useSelector(safeSelector) || {}
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
|
|
||||||
useEffect(() => {
|
|
||||||
let isCurrent = true
|
|
||||||
|
|
||||||
const estimateGas = async () => {
|
const {
|
||||||
const txData = tx.data ? tx.data.trim() : ''
|
gasCostFormatted,
|
||||||
|
txEstimationExecutionStatus,
|
||||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.contractAddress as string, txData)
|
isExecution,
|
||||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
isCreation,
|
||||||
const formattedGasCosts = formatAmount(gasCosts)
|
isOffChainSignature,
|
||||||
|
} = useEstimateTransactionGas({
|
||||||
if (isCurrent) {
|
txRecipient: tx.contractAddress as string,
|
||||||
setGasCosts(formattedGasCosts)
|
txData: tx.data ? tx.data.trim() : '',
|
||||||
}
|
txAmount: tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0',
|
||||||
}
|
})
|
||||||
|
|
||||||
estimateGas()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isCurrent = false
|
|
||||||
}
|
|
||||||
}, [safeAddress, tx.data, tx.contractAddress])
|
|
||||||
|
|
||||||
const submitTx = async (): Promise<void> => {
|
const submitTx = async (): Promise<void> => {
|
||||||
const txRecipient = tx.contractAddress
|
const txRecipient = tx.contractAddress
|
||||||
@ -161,9 +151,13 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Paragraph>
|
<TransactionFees
|
||||||
{`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.`}
|
gasCostFormatted={gasCostFormatted}
|
||||||
</Paragraph>
|
isExecution={isExecution}
|
||||||
|
isCreation={isCreation}
|
||||||
|
isOffChainSignature={isOffChainSignature}
|
||||||
|
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||||
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
|
@ -7,7 +7,7 @@ import GnoForm from 'src/components/forms/GnoForm'
|
|||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
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 Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Buttons from './Buttons'
|
import Buttons from './Buttons'
|
||||||
import ContractABI from './ContractABI'
|
import ContractABI from './ContractABI'
|
||||||
@ -53,14 +53,14 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({
|
|||||||
isABI,
|
isABI,
|
||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const { address: safeAddress = '' } = useSelector(safeSelector) || {}
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
let setCallResults
|
let setCallResults
|
||||||
|
|
||||||
React.useMemo(() => {
|
React.useMemo(() => {
|
||||||
if (contractAddress) {
|
if (contractAddress) {
|
||||||
initialValues.contractAddress = contractAddress
|
initialValues.contractAddress = contractAddress
|
||||||
}
|
}
|
||||||
}, [contractAddress, initialValues.contractAddress])
|
}, [contractAddress, initialValues])
|
||||||
|
|
||||||
const saveForm = async (values: CreatedTx): Promise<void> => {
|
const saveForm = async (values: CreatedTx): Promise<void> => {
|
||||||
await handleSubmit(values, false)
|
await handleSubmit(values, false)
|
||||||
|
@ -3,9 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'
|
|||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import { getExplorerInfo } from 'src/config'
|
||||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
|
||||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
|
||||||
import CopyBtn from 'src/components/CopyBtn'
|
import CopyBtn from 'src/components/CopyBtn'
|
||||||
import Identicon from 'src/components/Identicon'
|
import Identicon from 'src/components/Identicon'
|
||||||
import Block from 'src/components/layout/Block'
|
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 Row from 'src/components/layout/Row'
|
||||||
import { nftTokensSelector } from 'src/logic/collectibles/store/selectors'
|
import { nftTokensSelector } from 'src/logic/collectibles/store/selectors'
|
||||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
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 { 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 SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||||
import { sm } from 'src/theme/variables'
|
import { sm } from 'src/theme/variables'
|
||||||
@ -31,8 +27,8 @@ import ArrowDown from '../assets/arrow-down.svg'
|
|||||||
|
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||||
|
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
const { nativeCoin } = getNetworkInfo()
|
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
@ -53,34 +49,38 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
|||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const shortener = textShortener()
|
const shortener = textShortener()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const { address: safeAddress } = useSelector(safeSelector) || {}
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const nftTokens = useSelector(nftTokensSelector)
|
const nftTokens = useSelector(nftTokensSelector)
|
||||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
|
||||||
const txToken = nftTokens.find(
|
const txToken = nftTokens.find(
|
||||||
({ assetAddress, tokenId }) => assetAddress === tx.assetAddress && tokenId === tx.nftTokenId,
|
({ assetAddress, tokenId }) => assetAddress === tx.assetAddress && tokenId === tx.nftTokenId,
|
||||||
)
|
)
|
||||||
const [data, setData] = useState('')
|
const [data, setData] = useState('')
|
||||||
|
|
||||||
|
const {
|
||||||
|
gasCostFormatted,
|
||||||
|
txEstimationExecutionStatus,
|
||||||
|
isExecution,
|
||||||
|
isOffChainSignature,
|
||||||
|
isCreation,
|
||||||
|
} = useEstimateTransactionGas({
|
||||||
|
txData: data,
|
||||||
|
txRecipient: tx.recipientAddress,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isCurrent = true
|
let isCurrent = true
|
||||||
|
|
||||||
const estimateGas = async () => {
|
const calculateERC721TransferData = async () => {
|
||||||
try {
|
try {
|
||||||
const txData = await generateERC721TransferTxData(tx, safeAddress)
|
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) {
|
if (isCurrent) {
|
||||||
setGasCosts(formattedGasCosts)
|
|
||||||
setData(txData)
|
setData(txData)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error while calculating estimated gas:', error)
|
console.error('Error calculating ERC721 transfer data:', error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
calculateERC721TransferData()
|
||||||
estimateGas()
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isCurrent = false
|
isCurrent = false
|
||||||
@ -164,9 +164,13 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
|||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
<Row>
|
<Row>
|
||||||
<Paragraph>
|
<TransactionFees
|
||||||
{`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.`}
|
gasCostFormatted={gasCostFormatted}
|
||||||
</Paragraph>
|
isExecution={isExecution}
|
||||||
|
isCreation={isCreation}
|
||||||
|
isOffChainSignature={isOffChainSignature}
|
||||||
|
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||||
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||||
|
@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
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 { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||||
|
|
||||||
import CopyBtn from 'src/components/CopyBtn'
|
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 Row from 'src/components/layout/Row'
|
||||||
import { getSpendingLimitContract } from 'src/logic/contracts/safeContracts'
|
import { getSpendingLimitContract } from 'src/logic/contracts/safeContracts'
|
||||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
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 { 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 { 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 { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
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 { styles } from './style'
|
||||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
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 useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const { nativeCoin } = getNetworkInfo()
|
const { nativeCoin } = getNetworkInfo()
|
||||||
@ -55,59 +56,74 @@ type ReviewTxProps = {
|
|||||||
tx: ReviewTxProp
|
tx: ReviewTxProp
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => {
|
const useTxAmount = (tx: ReviewTxProp, isSendingNativeToken: boolean, txToken?: RecordOf<TokenProps>): string => {
|
||||||
const classes = useStyles()
|
const [txAmount, setTxAmount] = useState('0')
|
||||||
const dispatch = useDispatch()
|
|
||||||
const { address: safeAddress } = useSelector(safeSelector) || {}
|
// txAmount should be 0 if we send tokens
|
||||||
const tokens = useSelector(extendedSafeTokensSelector)
|
// the real value is encoded in txData and will be used by the contract
|
||||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
// 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<TokenProps>,
|
||||||
|
): string => {
|
||||||
const [data, setData] = useState('')
|
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(() => {
|
useEffect(() => {
|
||||||
let isCurrent = true
|
const updateTxDataAsync = async () => {
|
||||||
|
|
||||||
const estimateGas = async () => {
|
|
||||||
if (!txToken) {
|
if (!txToken) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let txData = EMPTY_DATA
|
let txData = EMPTY_DATA
|
||||||
|
if (!isSendingNativeToken) {
|
||||||
if (!isSendingETH) {
|
|
||||||
const StandardToken = await getHumanFriendlyToken()
|
const StandardToken = await getHumanFriendlyToken()
|
||||||
const tokenInstance = await StandardToken.at(txToken.address as string)
|
const tokenInstance = await StandardToken.at(txToken.address as string)
|
||||||
const txAmount = toTokenUnit(tx.amount, txToken.decimals)
|
txData = tokenInstance.contract.methods.transfer(recipientAddress, txAmount).encodeABI()
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
setData(txData)
|
||||||
}
|
}
|
||||||
|
|
||||||
estimateGas()
|
updateTxDataAsync()
|
||||||
|
}, [isSendingNativeToken, recipientAddress, txAmount, txToken])
|
||||||
|
|
||||||
return () => {
|
return data
|
||||||
isCurrent = false
|
}
|
||||||
}
|
|
||||||
}, [isSendingETH, safeAddress, tx.amount, tx.recipientAddress, txRecipient, txToken])
|
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 submitTx = async () => {
|
||||||
const isSpendingLimit = sameString(tx.txType, 'spendingLimit')
|
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) {
|
if (!safeAddress) {
|
||||||
console.error('There was an error trying to submit the transaction, the safeAddress was not found')
|
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) {
|
if (isSpendingLimit && txToken && tx.tokenSpendingLimit) {
|
||||||
|
const spendingLimitTokenAddress = isSendingNativeToken ? ZERO_ADDRESS : txToken.address
|
||||||
const spendingLimit = getSpendingLimitContract()
|
const spendingLimit = getSpendingLimitContract()
|
||||||
spendingLimit.methods
|
spendingLimit.methods
|
||||||
.executeAllowanceTransfer(
|
.executeAllowanceTransfer(
|
||||||
safeAddress,
|
safeAddress,
|
||||||
sameAddress(txToken.address, nativeCoin.address) ? ZERO_ADDRESS : txToken.address,
|
spendingLimitTokenAddress,
|
||||||
tx.recipientAddress,
|
tx.recipientAddress,
|
||||||
toTokenUnit(tx.amount, txToken.decimals),
|
toTokenUnit(tx.amount, txToken.decimals),
|
||||||
ZERO_ADDRESS,
|
ZERO_ADDRESS,
|
||||||
@ -207,9 +224,13 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement =>
|
|||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Paragraph data-testid="fee-meg-review-step">
|
<TransactionFees
|
||||||
{`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.`}
|
gasCostFormatted={gasCostFormatted}
|
||||||
</Paragraph>
|
isExecution={isExecution}
|
||||||
|
isCreation={isCreation}
|
||||||
|
isOffChainSignature={isOffChainSignature}
|
||||||
|
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||||
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||||
|
@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react'
|
|||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { OwnerForm } from './screens/OwnerForm'
|
import { OwnerForm } from './screens/OwnerForm'
|
||||||
import ReviewAddOwner from './screens/Review'
|
import { ReviewAddOwner } from './screens/Review'
|
||||||
import ThresholdForm from './screens/ThresholdForm'
|
import ThresholdForm from './screens/ThresholdForm'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import IconButton from '@material-ui/core/IconButton'
|
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 Close from '@material-ui/icons/Close'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
import { getExplorerInfo } from 'src/config'
|
||||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
|
||||||
import CopyBtn from 'src/components/CopyBtn'
|
import CopyBtn from 'src/components/CopyBtn'
|
||||||
import Identicon from 'src/components/Identicon'
|
import Identicon from 'src/components/Identicon'
|
||||||
import Block from 'src/components/layout/Block'
|
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 Row from 'src/components/layout/Row'
|
||||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||||
import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
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 { styles } from './style'
|
||||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
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'
|
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 }) => {
|
type ReviewAddOwnerProps = {
|
||||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
onClickBack: () => void
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
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 safeName = useSelector(safeNameSelector)
|
||||||
const owners = useSelector(safeOwnersSelector)
|
const owners = useSelector(safeOwnersSelector)
|
||||||
|
|
||||||
|
const {
|
||||||
|
gasCostFormatted,
|
||||||
|
txEstimationExecutionStatus,
|
||||||
|
isExecution,
|
||||||
|
isOffChainSignature,
|
||||||
|
isCreation,
|
||||||
|
} = useEstimateTransactionGas({
|
||||||
|
txData: data,
|
||||||
|
txRecipient: safeAddress,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isCurrent = true
|
let isCurrent = true
|
||||||
const estimateGas = async () => {
|
|
||||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
|
||||||
|
|
||||||
const txData = safeInstance.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI()
|
const calculateAddOwnerData = async () => {
|
||||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData)
|
try {
|
||||||
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
const txData = safeInstance.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI()
|
||||||
|
|
||||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
if (isCurrent) {
|
||||||
const formattedGasCosts = formatAmount(gasCosts)
|
setData(txData)
|
||||||
if (isCurrent) {
|
}
|
||||||
setGasCosts(formattedGasCosts)
|
} catch (error) {
|
||||||
|
console.error('Error calculating ERC721 transfer data:', error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
calculateAddOwnerData()
|
||||||
estimateGas()
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isCurrent = false
|
isCurrent = false
|
||||||
@ -68,7 +91,7 @@ const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) =>
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Block className={classes.formContainer}>
|
<Block>
|
||||||
<Row className={classes.root}>
|
<Row className={classes.root}>
|
||||||
<Col layout="column" xs={4}>
|
<Col layout="column" xs={4}>
|
||||||
<Block className={classes.details}>
|
<Block className={classes.details}>
|
||||||
@ -157,11 +180,13 @@ const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) =>
|
|||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Block className={classes.gasCostsContainer}>
|
<Block className={classes.gasCostsContainer}>
|
||||||
<Paragraph>
|
<TransactionFees
|
||||||
You're about to create a transaction and will have to confirm it with your currently connected wallet.
|
gasCostFormatted={gasCostFormatted}
|
||||||
<br />
|
isExecution={isExecution}
|
||||||
{`Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
isCreation={isCreation}
|
||||||
</Paragraph>
|
isOffChainSignature={isOffChainSignature}
|
||||||
|
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||||
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
@ -183,5 +208,3 @@ const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) =>
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles as any)(ReviewAddOwner)
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { background, border, lg, secondaryText, sm } from 'src/theme/variables'
|
import { background, border, lg, secondaryText, sm } from 'src/theme/variables'
|
||||||
|
import { createStyles } from '@material-ui/core'
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = createStyles({
|
||||||
root: {
|
root: {
|
||||||
height: '372px',
|
height: '372px',
|
||||||
},
|
},
|
||||||
@ -76,7 +77,9 @@ export const styles = () => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
gasCostsContainer: {
|
gasCostsContainer: {
|
||||||
padding: `0 ${lg}`,
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
|
@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react'
|
|||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import CheckOwner from './screens/CheckOwner'
|
import CheckOwner from './screens/CheckOwner'
|
||||||
import ReviewRemoveOwner from './screens/Review'
|
import { ReviewRemoveOwnerModal } from './screens/Review'
|
||||||
import ThresholdForm from './screens/ThresholdForm'
|
import ThresholdForm from './screens/ThresholdForm'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
@ -69,7 +69,12 @@ type RemoveOwnerProps = {
|
|||||||
ownerName: string
|
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 classes = useStyles()
|
||||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||||
const [values, setValues] = useState<any>({})
|
const [values, setValues] = useState<any>({})
|
||||||
@ -124,18 +129,16 @@ const RemoveOwner = ({ isOpen, onClose, ownerAddress, ownerName }: RemoveOwnerPr
|
|||||||
<ThresholdForm onClickBack={onClickBack} onClose={onClose} onSubmit={thresholdSubmitted} />
|
<ThresholdForm onClickBack={onClickBack} onClose={onClose} onSubmit={thresholdSubmitted} />
|
||||||
)}
|
)}
|
||||||
{activeScreen === 'reviewRemoveOwner' && (
|
{activeScreen === 'reviewRemoveOwner' && (
|
||||||
<ReviewRemoveOwner
|
<ReviewRemoveOwnerModal
|
||||||
onClickBack={onClickBack}
|
onClickBack={onClickBack}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={onRemoveOwner}
|
onSubmit={onRemoveOwner}
|
||||||
ownerAddress={ownerAddress}
|
ownerAddress={ownerAddress}
|
||||||
ownerName={ownerName}
|
ownerName={ownerName}
|
||||||
values={values}
|
threshold={threshold}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RemoveOwner
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import IconButton from '@material-ui/core/IconButton'
|
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 Close from '@material-ui/icons/Close'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
import { getExplorerInfo } from 'src/config'
|
||||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
|
||||||
import CopyBtn from 'src/components/CopyBtn'
|
import CopyBtn from 'src/components/CopyBtn'
|
||||||
import Identicon from 'src/components/Identicon'
|
import Identicon from 'src/components/Identicon'
|
||||||
import Block from 'src/components/layout/Block'
|
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 Row from 'src/components/layout/Row'
|
||||||
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||||
import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
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 { styles } from './style'
|
||||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||||
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
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'
|
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 }) => {
|
type ReviewRemoveOwnerProps = {
|
||||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
onClickBack: () => void
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
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 safeName = useSelector(safeNameSelector)
|
||||||
const owners = useSelector(safeOwnersSelector)
|
const owners = useSelector(safeOwnersSelector)
|
||||||
const addressBook = useSelector(addressBookSelector)
|
const addressBook = useSelector(addressBookSelector)
|
||||||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
||||||
|
|
||||||
|
const {
|
||||||
|
gasCostFormatted,
|
||||||
|
txEstimationExecutionStatus,
|
||||||
|
isExecution,
|
||||||
|
isCreation,
|
||||||
|
isOffChainSignature,
|
||||||
|
} = useEstimateTransactionGas({
|
||||||
|
txData: data,
|
||||||
|
txRecipient: safeAddress,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isCurrent = true
|
let isCurrent = true
|
||||||
|
|
||||||
const estimateGas = async () => {
|
if (!threshold) {
|
||||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
console.error("Threshold value was not define, tx can't be executed")
|
||||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
return
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 () => {
|
return () => {
|
||||||
isCurrent = false
|
isCurrent = false
|
||||||
}
|
}
|
||||||
}, [ownerAddress, safeAddress, values.threshold])
|
}, [safeAddress, ownerAddress, threshold])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -95,7 +128,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||||||
Any transaction requires the confirmation of:
|
Any transaction requires the confirmation of:
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||||
{`${values.threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`}
|
{`${threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
@ -165,11 +198,13 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Block className={classes.gasCostsContainer}>
|
<Block className={classes.gasCostsContainer}>
|
||||||
<Paragraph>
|
<TransactionFees
|
||||||
You're about to create a transaction and will have to confirm it with your currently connected wallet.
|
gasCostFormatted={gasCostFormatted}
|
||||||
<br />
|
isExecution={isExecution}
|
||||||
{`Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
isCreation={isCreation}
|
||||||
</Paragraph>
|
isOffChainSignature={isOffChainSignature}
|
||||||
|
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||||
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
@ -191,5 +226,3 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles as any)(ReviewRemoveOwner)
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { background, border, lg, secondaryText, sm } from 'src/theme/variables'
|
import { background, border, lg, secondaryText, sm } from 'src/theme/variables'
|
||||||
|
import { createStyles } from '@material-ui/core'
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = createStyles({
|
||||||
root: {
|
root: {
|
||||||
height: '372px',
|
height: '372px',
|
||||||
},
|
},
|
||||||
@ -76,7 +77,9 @@ export const styles = () => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
gasCostsContainer: {
|
gasCostsContainer: {
|
||||||
padding: `0 ${lg}`,
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
|
@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'
|
|||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import OwnerForm from './screens/OwnerForm'
|
import OwnerForm from './screens/OwnerForm'
|
||||||
import ReviewReplaceOwner from './screens/Review'
|
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
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 { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||||
|
import { ReviewReplaceOwnerModal } from './screens/Review'
|
||||||
|
|
||||||
const styles = createStyles({
|
const styles = createStyles({
|
||||||
biggerModalWindow: {
|
biggerModalWindow: {
|
||||||
@ -28,9 +28,8 @@ const styles = createStyles({
|
|||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
type OwnerValues = {
|
type OwnerValues = {
|
||||||
ownerAddress: string
|
newOwnerAddress: string
|
||||||
ownerName: string
|
newOwnerName: string
|
||||||
threshold: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendReplaceOwner = async (
|
export const sendReplaceOwner = async (
|
||||||
@ -44,7 +43,7 @@ export const sendReplaceOwner = async (
|
|||||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||||
const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove))
|
const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove))
|
||||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
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(
|
const txHash = await dispatch(
|
||||||
createTransaction({
|
createTransaction({
|
||||||
@ -61,8 +60,8 @@ export const sendReplaceOwner = async (
|
|||||||
replaceSafeOwner({
|
replaceSafeOwner({
|
||||||
safeAddress,
|
safeAddress,
|
||||||
oldOwnerAddress: ownerAddressToRemove,
|
oldOwnerAddress: ownerAddressToRemove,
|
||||||
ownerAddress: values.ownerAddress,
|
ownerAddress: values.newOwnerAddress,
|
||||||
ownerName: values.ownerName,
|
ownerName: values.newOwnerName,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -75,10 +74,18 @@ type ReplaceOwnerProps = {
|
|||||||
ownerName: string
|
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 classes = useStyles()
|
||||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||||
const [values, setValues] = useState<any>({})
|
const [values, setValues] = useState({
|
||||||
|
newOwnerAddress: '',
|
||||||
|
newOwnerName: '',
|
||||||
|
})
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const threshold = useSelector(safeThresholdSelector)
|
const threshold = useSelector(safeThresholdSelector)
|
||||||
@ -86,7 +93,10 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner
|
|||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
setActiveScreen('checkOwner')
|
setActiveScreen('checkOwner')
|
||||||
setValues({})
|
setValues({
|
||||||
|
newOwnerAddress: '',
|
||||||
|
newOwnerName: '',
|
||||||
|
})
|
||||||
},
|
},
|
||||||
[isOpen],
|
[isOpen],
|
||||||
)
|
)
|
||||||
@ -96,9 +106,10 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner
|
|||||||
const ownerSubmitted = (newValues) => {
|
const ownerSubmitted = (newValues) => {
|
||||||
const { ownerAddress, ownerName } = newValues
|
const { ownerAddress, ownerName } = newValues
|
||||||
const checksumAddr = checksumAddress(ownerAddress)
|
const checksumAddr = checksumAddress(ownerAddress)
|
||||||
values.ownerName = ownerName
|
setValues({
|
||||||
values.ownerAddress = checksumAddr
|
newOwnerAddress: checksumAddr,
|
||||||
setValues(values)
|
newOwnerName: ownerName,
|
||||||
|
})
|
||||||
setActiveScreen('reviewReplaceOwner')
|
setActiveScreen('reviewReplaceOwner')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +119,9 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner
|
|||||||
await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, threshold)
|
await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, threshold)
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: values.ownerAddress, name: values.ownerName })),
|
addOrUpdateAddressBookEntry(
|
||||||
|
makeAddressBookEntry({ address: values.newOwnerAddress, name: values.newOwnerName }),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error while removing an owner', error)
|
console.error('Error while removing an owner', error)
|
||||||
@ -128,7 +141,7 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner
|
|||||||
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} />
|
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} />
|
||||||
)}
|
)}
|
||||||
{activeScreen === 'reviewReplaceOwner' && (
|
{activeScreen === 'reviewReplaceOwner' && (
|
||||||
<ReviewReplaceOwner
|
<ReviewReplaceOwnerModal
|
||||||
onClickBack={onClickBack}
|
onClickBack={onClickBack}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={onReplaceOwner}
|
onSubmit={onReplaceOwner}
|
||||||
@ -141,5 +154,3 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner
|
|||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ReplaceOwner
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import IconButton from '@material-ui/core/IconButton'
|
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 Close from '@material-ui/icons/Close'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
@ -7,8 +7,7 @@ import { useSelector } from 'react-redux'
|
|||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
import { getExplorerInfo } from 'src/config'
|
||||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
|
||||||
import CopyBtn from 'src/components/CopyBtn'
|
import CopyBtn from 'src/components/CopyBtn'
|
||||||
import Identicon from 'src/components/Identicon'
|
import Identicon from 'src/components/Identicon'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
@ -24,47 +23,75 @@ import {
|
|||||||
safeParamAddressFromStateSelector,
|
safeParamAddressFromStateSelector,
|
||||||
safeThresholdSelector,
|
safeThresholdSelector,
|
||||||
} from 'src/logic/safe/store/selectors'
|
} 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 { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||||
|
|
||||||
import { styles } from './style'
|
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'
|
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 }) => {
|
type ReplaceOwnerProps = {
|
||||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
onClose: () => void
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
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 safeName = useSelector(safeNameSelector)
|
||||||
const owners = useSelector(safeOwnersSelector)
|
const owners = useSelector(safeOwnersSelector)
|
||||||
const threshold = useSelector(safeThresholdSelector)
|
const threshold = useSelector(safeThresholdSelector)
|
||||||
const addressBook = useSelector(addressBookSelector)
|
const addressBook = useSelector(addressBookSelector)
|
||||||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
||||||
|
|
||||||
|
const {
|
||||||
|
gasCostFormatted,
|
||||||
|
txEstimationExecutionStatus,
|
||||||
|
isExecution,
|
||||||
|
isCreation,
|
||||||
|
isOffChainSignature,
|
||||||
|
} = useEstimateTransactionGas({
|
||||||
|
txData: data,
|
||||||
|
txRecipient: safeAddress,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isCurrent = true
|
let isCurrent = true
|
||||||
const estimateGas = async () => {
|
const calculateReplaceOwnerData = async () => {
|
||||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||||
const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase())
|
const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase())
|
||||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||||
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddress, values.ownerAddress).encodeABI()
|
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddress, values.newOwnerAddress).encodeABI()
|
||||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData)
|
|
||||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
|
||||||
const formattedGasCosts = formatAmount(gasCosts)
|
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
setGasCosts(formattedGasCosts)
|
setData(txData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
estimateGas()
|
calculateReplaceOwnerData()
|
||||||
return () => {
|
return () => {
|
||||||
isCurrent = false
|
isCurrent = false
|
||||||
}
|
}
|
||||||
}, [ownerAddress, safeAddress, values.ownerAddress])
|
}, [ownerAddress, safeAddress, values.newOwnerAddress])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -78,7 +105,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Block className={classes.formContainer}>
|
<Block>
|
||||||
<Row className={classes.root}>
|
<Row className={classes.root}>
|
||||||
<Col layout="column" xs={4}>
|
<Col layout="column" xs={4}>
|
||||||
<Block className={classes.details}>
|
<Block className={classes.details}>
|
||||||
@ -172,19 +199,19 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||||||
<Hairline />
|
<Hairline />
|
||||||
<Row className={classes.selectedOwnerAdded}>
|
<Row className={classes.selectedOwnerAdded}>
|
||||||
<Col align="center" xs={1}>
|
<Col align="center" xs={1}>
|
||||||
<Identicon address={values.ownerAddress} diameter={32} />
|
<Identicon address={values.newOwnerAddress} diameter={32} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={11}>
|
<Col xs={11}>
|
||||||
<Block className={classNames(classes.name, classes.userName)}>
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
<Paragraph noMargin size="lg" weight="bolder">
|
<Paragraph noMargin size="lg" weight="bolder">
|
||||||
{values.ownerName}
|
{values.newOwnerName}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Block className={classes.user} justify="center">
|
<Block className={classes.user} justify="center">
|
||||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||||
{values.ownerAddress}
|
{values.newOwnerAddress}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<CopyBtn content={values.ownerAddress} />
|
<CopyBtn content={values.newOwnerAddress} />
|
||||||
<ExplorerButton explorerUrl={getExplorerInfo(values.ownerAddress)} />
|
<ExplorerButton explorerUrl={getExplorerInfo(values.newOwnerAddress)} />
|
||||||
</Block>
|
</Block>
|
||||||
</Block>
|
</Block>
|
||||||
</Col>
|
</Col>
|
||||||
@ -195,11 +222,13 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Block className={classes.gasCostsContainer}>
|
<Block className={classes.gasCostsContainer}>
|
||||||
<Paragraph>
|
<TransactionFees
|
||||||
You're about to create a transaction and will have to confirm it with your currently connected wallet.
|
gasCostFormatted={gasCostFormatted}
|
||||||
<br />
|
isExecution={isExecution}
|
||||||
{`Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
isCreation={isCreation}
|
||||||
</Paragraph>
|
isOffChainSignature={isOffChainSignature}
|
||||||
|
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||||
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
@ -221,5 +250,3 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles as any)(ReviewRemoveOwner)
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { background, border, lg, secondaryText, sm } from 'src/theme/variables'
|
import { background, border, lg, secondaryText, sm } from 'src/theme/variables'
|
||||||
|
import { createStyles } from '@material-ui/core'
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = createStyles({
|
||||||
root: {
|
root: {
|
||||||
height: '372px',
|
height: '372px',
|
||||||
},
|
},
|
||||||
@ -81,7 +82,9 @@ export const styles = () => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
gasCostsContainer: {
|
gasCostsContainer: {
|
||||||
padding: `0 ${lg}`,
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
|
@ -11,8 +11,8 @@ import RemoveOwnerIcon from '../assets/icons/bin.svg'
|
|||||||
import AddOwnerModal from './AddOwnerModal'
|
import AddOwnerModal from './AddOwnerModal'
|
||||||
import { EditOwnerModal } from './EditOwnerModal'
|
import { EditOwnerModal } from './EditOwnerModal'
|
||||||
import OwnerAddressTableCell from './OwnerAddressTableCell'
|
import OwnerAddressTableCell from './OwnerAddressTableCell'
|
||||||
import RemoveOwnerModal from './RemoveOwnerModal'
|
import { RemoveOwnerModal } from './RemoveOwnerModal'
|
||||||
import ReplaceOwnerModal from './ReplaceOwnerModal'
|
import { ReplaceOwnerModal } from './ReplaceOwnerModal'
|
||||||
import RenameOwnerIcon from './assets/icons/rename-owner.svg'
|
import RenameOwnerIcon from './assets/icons/rename-owner.svg'
|
||||||
import ReplaceOwnerIcon from './assets/icons/replace-owner.svg'
|
import ReplaceOwnerIcon from './assets/icons/replace-owner.svg'
|
||||||
import { OWNERS_TABLE_ADDRESS_ID, OWNERS_TABLE_NAME_ID, generateColumns, getOwnerData } from './dataFetcher'
|
import { OWNERS_TABLE_ADDRESS_ID, OWNERS_TABLE_NAME_ID, generateColumns, getOwnerData } from './dataFetcher'
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import MenuItem from '@material-ui/core/MenuItem'
|
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 Close from '@material-ui/icons/Close'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
|
||||||
import { getNetworkInfo } from 'src/config'
|
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
import Field from 'src/components/forms/Field'
|
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 Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
|
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
||||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
import { List } from 'immutable'
|
||||||
|
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
|
|
||||||
|
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||||
|
|
||||||
const THRESHOLD_FIELD_NAME = 'threshold'
|
const THRESHOLD_FIELD_NAME = 'threshold'
|
||||||
|
|
||||||
const { nativeCoin } = getNetworkInfo()
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddress, threshold }) => {
|
type ChangeThresholdModalProps = {
|
||||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
onChangeThreshold: (newThreshold: number) => void
|
||||||
|
onClose: () => void
|
||||||
|
owners?: List<SafeOwner>
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
let isCurrent = true
|
let isCurrent = true
|
||||||
const estimateGasCosts = async () => {
|
const calculateChangeThresholdData = async () => {
|
||||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
const txData = safeInstance.methods.changeThreshold('1').encodeABI()
|
const txData = safeInstance.methods.changeThreshold(threshold).encodeABI()
|
||||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData)
|
|
||||||
const gasCostsAsEth = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
|
||||||
const formattedGasCosts = formatAmount(gasCostsAsEth)
|
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
setGasCosts(formattedGasCosts)
|
setData(txData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
estimateGasCosts()
|
calculateChangeThresholdData()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isCurrent = false
|
isCurrent = false
|
||||||
}
|
}
|
||||||
}, [safeAddress])
|
}, [safeAddress, threshold])
|
||||||
|
|
||||||
const handleSubmit = (values) => {
|
const handleSubmit = (values) => {
|
||||||
const newThreshold = values[THRESHOLD_FIELD_NAME]
|
const newThreshold = values[THRESHOLD_FIELD_NAME]
|
||||||
@ -81,7 +104,7 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr
|
|||||||
render={(props) => (
|
render={(props) => (
|
||||||
<>
|
<>
|
||||||
<SelectField {...props} disableError>
|
<SelectField {...props} disableError>
|
||||||
{[...Array(Number(owners.size))].map((x, index) => (
|
{[...Array(Number(owners?.size))].map((x, index) => (
|
||||||
<MenuItem key={index} value={`${index + 1}`}>
|
<MenuItem key={index} value={`${index + 1}`}>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -99,14 +122,18 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs={10}>
|
<Col xs={10}>
|
||||||
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
||||||
{`out of ${owners.size} owner(s)`}
|
{`out of ${owners?.size} owner(s)`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Paragraph>
|
<TransactionFees
|
||||||
{`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.`}
|
gasCostFormatted={gasCostFormatted}
|
||||||
</Paragraph>
|
isExecution={isExecution}
|
||||||
|
isCreation={isCreation}
|
||||||
|
isOffChainSignature={isOffChainSignature}
|
||||||
|
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||||
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||||
@ -124,5 +151,3 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles as any)(ChangeThreshold)
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { lg, md, secondaryText, sm } from 'src/theme/variables'
|
import { lg, md, secondaryText, sm } from 'src/theme/variables'
|
||||||
|
import { createStyles } from '@material-ui/core'
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = createStyles({
|
||||||
heading: {
|
heading: {
|
||||||
padding: `${sm} ${lg}`,
|
padding: `${sm} ${lg}`,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import ChangeThreshold from './ChangeThreshold'
|
import { ChangeThresholdModal } from './ChangeThreshold'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
import Modal from 'src/components/Modal'
|
import Modal from 'src/components/Modal'
|
||||||
@ -30,7 +30,7 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||||||
const [isModalOpen, setModalOpen] = useState(false)
|
const [isModalOpen, setModalOpen] = useState(false)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const threshold = useSelector(safeThresholdSelector)
|
const threshold = useSelector(safeThresholdSelector)
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const owners = useSelector(safeOwnersSelector)
|
const owners = useSelector(safeOwnersSelector)
|
||||||
const granted = useSelector(grantedSelector)
|
const granted = useSelector(grantedSelector)
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||||||
setModalOpen((prevOpen) => !prevOpen)
|
setModalOpen((prevOpen) => !prevOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeThreshold = async (newThreshold) => {
|
const onChangeThreshold = async (newThreshold: number) => {
|
||||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
const txData = safeInstance.methods.changeThreshold(newThreshold).encodeABI()
|
const txData = safeInstance.methods.changeThreshold(newThreshold).encodeABI()
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||||||
open={isModalOpen}
|
open={isModalOpen}
|
||||||
title="Change Required Confirmations"
|
title="Change Required Confirmations"
|
||||||
>
|
>
|
||||||
<ChangeThreshold
|
<ChangeThresholdModal
|
||||||
onChangeThreshold={onChangeThreshold}
|
onChangeThreshold={onChangeThreshold}
|
||||||
onClose={toggleModal}
|
onClose={toggleModal}
|
||||||
owners={owners}
|
owners={owners}
|
||||||
|
@ -3,10 +3,8 @@ import FormControlLabel from '@material-ui/core/FormControlLabel'
|
|||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
|
||||||
import { getNetworkInfo } from 'src/config'
|
|
||||||
|
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
@ -18,13 +16,13 @@ import Hairline from 'src/components/layout/Hairline'
|
|||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
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 { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||||
import processTransaction from 'src/logic/safe/store/actions/processTransaction'
|
import { processTransaction } from 'src/logic/safe/store/actions/processTransaction'
|
||||||
|
|
||||||
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||||
|
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
|
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
@ -62,9 +60,8 @@ type Props = {
|
|||||||
thresholdReached: boolean
|
thresholdReached: boolean
|
||||||
tx: Transaction
|
tx: Transaction
|
||||||
}
|
}
|
||||||
const { nativeCoin } = getNetworkInfo()
|
|
||||||
|
|
||||||
const ApproveTxModal = ({
|
export const ApproveTxModal = ({
|
||||||
canExecute,
|
canExecute,
|
||||||
isCancelTx = false,
|
isCancelTx = false,
|
||||||
isOpen,
|
isOpen,
|
||||||
@ -76,37 +73,27 @@ const ApproveTxModal = ({
|
|||||||
const userAddress = useSelector(userAccountSelector)
|
const userAddress = useSelector(userAccountSelector)
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const threshold = useSelector(safeThresholdSelector)
|
const threshold = useSelector(safeThresholdSelector)
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const [approveAndExecute, setApproveAndExecute] = useState(canExecute)
|
const [approveAndExecute, setApproveAndExecute] = useState(canExecute)
|
||||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
|
||||||
const { description, title } = getModalTitleAndDescription(thresholdReached, isCancelTx)
|
const { description, title } = getModalTitleAndDescription(thresholdReached, isCancelTx)
|
||||||
const oneConfirmationLeft = !thresholdReached && tx.confirmations.size + 1 === threshold
|
const oneConfirmationLeft = !thresholdReached && tx.confirmations.size + 1 === threshold
|
||||||
const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached
|
const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
let isCurrent = true
|
gasCostFormatted,
|
||||||
|
txEstimationExecutionStatus,
|
||||||
const estimateGas = async () => {
|
isExecution,
|
||||||
const estimatedGasCosts = await estimateTxGasCosts(
|
isOffChainSignature,
|
||||||
safeAddress,
|
isCreation,
|
||||||
tx.recipient,
|
} = useEstimateTransactionGas({
|
||||||
tx.data as string,
|
txRecipient: tx.recipient,
|
||||||
tx,
|
txData: tx.data || '',
|
||||||
approveAndExecute ? userAddress : undefined,
|
txConfirmations: tx.confirmations,
|
||||||
)
|
txAmount: tx.value,
|
||||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
preApprovingOwner: approveAndExecute ? userAddress : undefined,
|
||||||
const formattedGasCosts = formatAmount(gasCosts)
|
safeTxGas: tx.safeTxGas,
|
||||||
if (isCurrent) {
|
operation: tx.operation,
|
||||||
setGasCosts(formattedGasCosts)
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
estimateGas()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isCurrent = false
|
|
||||||
}
|
|
||||||
}, [approveAndExecute, safeAddress, tx, userAddress])
|
|
||||||
|
|
||||||
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
|
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
|
||||||
|
|
||||||
@ -159,15 +146,13 @@ const ApproveTxModal = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<TransactionFees
|
||||||
<Paragraph>
|
gasCostFormatted={gasCostFormatted}
|
||||||
{`You're about to ${
|
isExecution={isExecution}
|
||||||
approveAndExecute ? 'execute' : 'approve'
|
isCreation={isCreation}
|
||||||
} a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${
|
isOffChainSignature={isOffChainSignature}
|
||||||
nativeCoin.name
|
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||||
} in this wallet to fund this confirmation.`}
|
/>
|
||||||
</Paragraph>
|
|
||||||
</Row>
|
|
||||||
</Block>
|
</Block>
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button minHeight={42} minWidth={140} onClick={onClose}>
|
<Button minHeight={42} minWidth={140} onClick={onClose}>
|
||||||
@ -188,5 +173,3 @@ const ApproveTxModal = ({
|
|||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApproveTxModal
|
|
||||||
|
@ -33,7 +33,7 @@ function getOwnersConfirmations(tx: Transaction, userAddress: string): [string[]
|
|||||||
const ownersWhoConfirmed: string[] = []
|
const ownersWhoConfirmed: string[] = []
|
||||||
let currentUserAlreadyConfirmed = false
|
let currentUserAlreadyConfirmed = false
|
||||||
|
|
||||||
tx.confirmations.forEach((conf) => {
|
tx.confirmations?.forEach((conf) => {
|
||||||
if (conf.owner === userAddress) {
|
if (conf.owner === userAddress) {
|
||||||
currentUserAlreadyConfirmed = true
|
currentUserAlreadyConfirmed = true
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
|
||||||
import { getNetworkInfo } from 'src/config'
|
|
||||||
|
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
@ -16,13 +14,13 @@ import Hairline from 'src/components/layout/Hairline'
|
|||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
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 { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||||
|
|
||||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||||
|
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||||
|
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
@ -32,31 +30,21 @@ type Props = {
|
|||||||
tx: Transaction
|
tx: Transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nativeCoin } = getNetworkInfo()
|
export const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElement => {
|
||||||
|
|
||||||
const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElement => {
|
|
||||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
let isCurrent = true
|
gasCostFormatted,
|
||||||
const estimateGasCosts = async () => {
|
txEstimationExecutionStatus,
|
||||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, EMPTY_DATA)
|
isExecution,
|
||||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
isOffChainSignature,
|
||||||
const formattedGasCosts = formatAmount(gasCosts)
|
isCreation,
|
||||||
if (isCurrent) {
|
} = useEstimateTransactionGas({
|
||||||
setGasCosts(formattedGasCosts)
|
txData: EMPTY_DATA,
|
||||||
}
|
txRecipient: safeAddress,
|
||||||
}
|
})
|
||||||
|
|
||||||
estimateGasCosts()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isCurrent = false
|
|
||||||
}
|
|
||||||
}, [safeAddress])
|
|
||||||
|
|
||||||
const sendReplacementTransaction = () => {
|
const sendReplacementTransaction = () => {
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -95,9 +83,13 @@ const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElement => {
|
|||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Paragraph>
|
<TransactionFees
|
||||||
{`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.`}
|
gasCostFormatted={gasCostFormatted}
|
||||||
</Paragraph>
|
isExecution={isExecution}
|
||||||
|
isCreation={isCreation}
|
||||||
|
isOffChainSignature={isOffChainSignature}
|
||||||
|
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||||
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</Block>
|
</Block>
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
@ -118,5 +110,3 @@ const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElement => {
|
|||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RejectTxModal
|
|
||||||
|
@ -3,9 +3,9 @@ import React, { ReactElement, useMemo, useState } from 'react'
|
|||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||||
|
|
||||||
import ApproveTxModal from './ApproveTxModal'
|
import { ApproveTxModal } from './ApproveTxModal'
|
||||||
import OwnersColumn from './OwnersColumn'
|
import OwnersColumn from './OwnersColumn'
|
||||||
import RejectTxModal from './RejectTxModal'
|
import { RejectTxModal } from './RejectTxModal'
|
||||||
import TxDescription from './TxDescription'
|
import TxDescription from './TxDescription'
|
||||||
import { IncomingTx } from './IncomingTx'
|
import { IncomingTx } from './IncomingTx'
|
||||||
import { CreationTx } from './CreationTx'
|
import { CreationTx } from './CreationTx'
|
||||||
|
@ -66,7 +66,7 @@ describe('getNewTxNonce', () => {
|
|||||||
const expectedResult = '2'
|
const expectedResult = '2'
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await getNewTxNonce(undefined, lastTx, safeInstance)
|
const result = await getNewTxNonce(lastTx, safeInstance)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result).toBe(expectedResult)
|
expect(result).toBe(expectedResult)
|
||||||
@ -82,7 +82,7 @@ describe('getNewTxNonce', () => {
|
|||||||
safeInstance.methods.nonce = mockFnNonce
|
safeInstance.methods.nonce = mockFnNonce
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await getNewTxNonce(undefined, null, safeInstance)
|
const result = await getNewTxNonce(null, safeInstance)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result).toBe(expectedResult)
|
expect(result).toBe(expectedResult)
|
||||||
@ -98,19 +98,7 @@ describe('getNewTxNonce', () => {
|
|||||||
const lastTx = getMockedTxServiceModel({ nonce: 10 })
|
const lastTx = getMockedTxServiceModel({ nonce: 10 })
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = await getNewTxNonce(undefined, lastTx, safeInstance)
|
const result = await getNewTxNonce(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)
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(result).toBe(expectedResult)
|
expect(result).toBe(expectedResult)
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
//
|
//
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
|
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', () => {
|
describe('Signatures Blockchain Test', () => {
|
||||||
it('generates signatures in natural order even checksumed', async () => {
|
it('generates signatures in natural order even checksumed', async () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user