Skip to content
This repository was archived by the owner on Nov 10, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f768d4c
Refactor proccessTransaction to use getPreValidatedSignatures method
Agupane Jan 4, 2021
694e450
Fix default export of ApproveTxModal
Agupane Jan 4, 2021
2c5f21d
Rename estimateExecTransactionGas to estimateGasForTransactionCreation
Agupane Jan 5, 2021
40ec65d
Make estimateGasForTransactionCreation throw error instead of 0 gas
Agupane Jan 5, 2021
a1ac679
Adds estimateGasForTransactionExecution and estimateGasForTransaction…
Agupane Jan 5, 2021
25bbd33
Move estimateTransactionGas to useEstimateTransactionGas
Agupane Jan 5, 2021
7895924
Type and refactor generateSignaturesFromTxConfirmations
Agupane Jan 6, 2021
d8d64c9
Uses confirmations to estimateGasForTransactionExecution
Agupane Jan 6, 2021
88d36f8
Adds TransactionFeesText component
Agupane Jan 6, 2021
7c6ba58
Pass more parameters to estimateGasForTransactionExecution
Agupane Jan 6, 2021
9c5bb19
Merge with 1048-tx-will-fail-warning
Agupane Jan 6, 2021
c6efa91
Removes unnecessary parameter in getNewTxNonce
Agupane Jan 6, 2021
f9fb706
Moves checkIfOffChainSignatureIsPossible to safeTxSigner.ts
Agupane Jan 6, 2021
e0b3af6
Fix check for null confirmations
Agupane Jan 6, 2021
d819531
Uses checkIfOffChainSignatureIsPossible on createTransaction.ts
Agupane Jan 6, 2021
4ab94ac
Move TransactionFailText inside TransactionFees component
Agupane Jan 6, 2021
bee96c5
Pass safeTxGas to useEstimateTransactionGas.tsx
Agupane Jan 8, 2021
63cab3f
Fix gas iteration on estimateGasForTransactionExecution
Agupane Jan 8, 2021
00919c7
Fix estimateGasForTransactionExecution calculation
Agupane Jan 8, 2021
d25e907
Fix generateSignaturesFromTxConfirmations calculation
Agupane Jan 8, 2021
fccd8d1
Remove unnecessary Promise and await
Jan 8, 2021
289b6e2
Fix estimateGasForTransactionExecution for preApproving owner case
Agupane Jan 8, 2021
b8d0878
Merge branch 'feature/refactor-gas-calculation' of https://github.com…
Agupane Jan 8, 2021
7ca4b64
Improve logging
Agupane Jan 8, 2021
7f29a64
Merge branch 'feature/1048-tx-will-fail-warning' into feature/refacto…
Agupane Jan 8, 2021
71fb956
Remove log
Agupane Jan 11, 2021
40dabdd
Uses operation in useEstimateTransactionGas
Agupane Jan 11, 2021
64d4fc3
Merge branch 'feature/1048-tx-will-fail-warning' into feature/refacto…
Agupane Jan 11, 2021
0a77aa3
Fix missing dependency
Agupane Jan 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/components/TransactionsFees/index.tsx
Original file line number Diff line number Diff line change
@@ -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&apos;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} />
</>
)
}
136 changes: 130 additions & 6 deletions src/logic/hooks/useEstimateTransactionGas.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { useEffect, useState } from 'react'
import { estimateTransactionGas } from 'src/logic/safe/transactions/gas'
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 { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
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',
Expand All @@ -16,13 +31,90 @@ export enum EstimationStatus {
const checkIfTxIsExecution = (threshold: number, preApprovingOwner?: string, txConfirmations?: number): boolean =>
txConfirmations === threshold || !!preApprovingOwner || threshold === 1

const checkIfTxIsApproveAndExecution = (threshold: number, txConfirmations: number): boolean =>
txConfirmations + 1 === threshold
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure to understand this, are we checking the checkbox status somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not needed, it's the same logic that uses the checkbox in ApproveTxModal here:

  const oneConfirmationLeft = !thresholdReached && tx.confirmations.size + 1 === threshold
  const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached


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?: number
txConfirmations?: List<Confirmation>
txAmount?: string
preApprovingOwner?: string
operation?: number
safeTxGas?: number
}

type TransactionGasEstimationResult = {
Expand All @@ -32,6 +124,8 @@ type TransactionGasEstimationResult = {
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 = ({
Expand All @@ -41,6 +135,7 @@ export const useEstimateTransactionGas = ({
txAmount,
preApprovingOwner,
operation,
safeTxGas,
}: UseEstimateTransactionGasProps): TransactionGasEstimationResult => {
const [gasEstimation, setGasEstimation] = useState<TransactionGasEstimationResult>({
txEstimationExecutionStatus: EstimationStatus.LOADING,
Expand All @@ -49,42 +144,65 @@ export const useEstimateTransactionGas = ({
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 isExecution = checkIfTxIsExecution(Number(threshold), preApprovingOwner, txConfirmations)
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 <= 0 ? EstimationStatus.FAILURE : EstimationStatus.SUCCESS,
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)
Expand All @@ -95,7 +213,9 @@ export const useEstimateTransactionGas = ({
gasCost,
gasCostFormatted,
gasPrice: '1',
isExecution: false,
isExecution,
isCreation,
isOffChainSignature: false,
})
}
}
Expand All @@ -110,7 +230,11 @@ export const useEstimateTransactionGas = ({
preApprovingOwner,
nativeCoin.decimals,
threshold,
from,
operation,
safeVersion,
smartContractWallet,
safeTxGas,
])

return gasEstimation
Expand Down
78 changes: 54 additions & 24 deletions src/logic/safe/safeTxSigner.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,61 @@
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
// https://github.com/gnosis/safe-contracts/blob/master/test/gnosisSafeTeamEdition.js#L26
export const generateSignaturesFromTxConfirmations = (confirmations, preApprovingOwner) => {
// The constant parts need to be sorted so that the recovered signers are sorted ascending
// (natural order) by address (not checksummed).
const confirmationsMap = confirmations.reduce((map, obj) => {
map[obj.owner.toLowerCase()] = obj // eslint-disable-line no-param-reassign
return map
}, {})
import { List } from 'immutable'
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import semverSatisfies from 'semver/functions/satisfies'
import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES } from './transactions/offchainSigner'

// Here we're checking that safe contract version is greater or equal 1.1.1, but
// theoretically EIP712 should also work for 1.0.0 contracts
// Also, offchain signatures are not working for ledger/trezor wallet because of a bug in their library:
// https://github.com/LedgerHQ/ledgerjs/issues/378
// Couldn't find an issue for trezor but the error is almost the same
export const checkIfOffChainSignatureIsPossible = (
isExecution: boolean,
isSmartContractWallet: boolean,
safeVersion?: string,
): boolean =>
!isExecution &&
!isSmartContractWallet &&
!!safeVersion &&
semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)

// https://docs.gnosis.io/safe/docs/contracts_signatures/#pre-validated-signatures
export const getPreValidatedSignatures = (from: string, initialString: string = EMPTY_DATA): string => {
return `${initialString}000000000000000000000000${from.replace(
EMPTY_DATA,
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
}

export const generateSignaturesFromTxConfirmations = (
confirmations?: List<Confirmation>,
preApprovingOwner?: string,
): string => {
let confirmationsMap =
confirmations?.map((value) => {
return {
signature: value.signature,
owner: value.owner.toLowerCase(),
}
}) || List([])

if (preApprovingOwner) {
confirmationsMap[preApprovingOwner.toLowerCase()] = { owner: preApprovingOwner }
confirmationsMap = confirmationsMap.push({ owner: preApprovingOwner, signature: null })
}

// The constant parts need to be sorted so that the recovered signers are sorted ascending
// (natural order) by address (not checksummed).
confirmationsMap = confirmationsMap.sort((ownerA, ownerB) => ownerA.owner.localeCompare(ownerB.owner))

let sigs = '0x'
Object.keys(confirmationsMap)
.sort()
.forEach((addr) => {
const conf = confirmationsMap[addr]
if (conf.signature) {
sigs += conf.signature.slice(2)
} else {
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
sigs += `000000000000000000000000${addr.replace(
'0x',
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
}
})
confirmationsMap.forEach(({ signature, owner }) => {
if (signature) {
sigs += signature.slice(2)
} else {
// https://docs.gnosis.io/safe/docs/contracts_signatures/#pre-validated-signatures
sigs += getPreValidatedSignatures(owner, '')
}
})

return sigs
}
Loading