diff --git a/src/components/Stepper/index.tsx b/src/components/Stepper/index.tsx index 07ee8497..824a1c77 100644 --- a/src/components/Stepper/index.tsx +++ b/src/components/Stepper/index.tsx @@ -11,6 +11,7 @@ import Controls from './Controls' import GnoForm from 'src/components/forms/GnoForm' import Hairline from 'src/components/layout/Hairline' import { history } from 'src/store' +import { LoadFormValues } from 'src/routes/load/container/Load' const transitionProps = { timeout: { @@ -20,7 +21,7 @@ const transitionProps = { } export interface StepperPageFormProps { - values: Record + values: LoadFormValues errors: Record form: FormApi } diff --git a/src/logic/contracts/safeContracts.ts b/src/logic/contracts/safeContracts.ts index 384fcc8e..17345da0 100644 --- a/src/logic/contracts/safeContracts.ts +++ b/src/logic/contracts/safeContracts.ts @@ -5,7 +5,7 @@ import Web3 from 'web3' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions' +import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3' import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' import { GnosisSafeProxyFactory } from 'src/types/contracts/GnosisSafeProxyFactory.d' @@ -99,7 +99,7 @@ export const getSafeDeploymentTransaction = ( safeAccounts, numConfirmations, ZERO_ADDRESS, - '0x', + EMPTY_DATA, DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, @@ -120,7 +120,7 @@ export const estimateGasForDeployingSafe = async ( safeAccounts, numConfirmations, ZERO_ADDRESS, - '0x', + EMPTY_DATA, DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, @@ -130,14 +130,11 @@ export const estimateGasForDeployingSafe = async ( const proxyFactoryData = proxyFactoryMaster.methods .createProxyWithNonce(safeMaster.options.address, gnosisSafeData, safeCreationSalt) .encodeABI() - const gas = await calculateGasOf({ + return calculateGasOf({ data: proxyFactoryData, from: userAccount, to: proxyFactoryMaster.options.address, }) - const gasPrice = await calculateGasPrice() - - return gas * parseInt(gasPrice, 10) } export const getGnosisSafeInstanceAt = (safeAddress: string): GnosisSafe => { diff --git a/src/logic/hooks/useEstimateSafeCreationGas.tsx b/src/logic/hooks/useEstimateSafeCreationGas.tsx new file mode 100644 index 00000000..40a712d0 --- /dev/null +++ b/src/logic/hooks/useEstimateSafeCreationGas.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' +import { userAccountSelector } from '../wallets/store/selectors' +import { estimateGasForDeployingSafe } from 'src/logic/contracts/safeContracts' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { formatAmount } from 'src/logic/tokens/utils/formatAmount' +import { getNetworkInfo } from 'src/config' +import { calculateGasPrice } from 'src/logic/wallets/ethTransactions' + +type EstimateSafeCreationGasProps = { + addresses: string[] + numOwners: number + safeCreationSalt: number +} + +type SafeCreationEstimationResult = { + gasEstimation: number // Amount of gas needed for execute or approve the transaction + gasCostFormatted: string // Cost of gas in format '< | > 100' + gasLimit: number // Minimum gas requited to execute the Tx +} + +const { nativeCoin } = getNetworkInfo() + +export const useEstimateSafeCreationGas = ({ + addresses, + numOwners, + safeCreationSalt, +}: EstimateSafeCreationGasProps): SafeCreationEstimationResult => { + const [gasEstimation, setGasEstimation] = useState({ + gasEstimation: 0, + gasCostFormatted: '< 0.001', + gasLimit: 0, + }) + const userAccount = useSelector(userAccountSelector) + + useEffect(() => { + const estimateGas = async () => { + if (!addresses.length || !numOwners || !userAccount) { + return + } + + const gasEstimation = await estimateGasForDeployingSafe(addresses, numOwners, userAccount, safeCreationSalt) + const gasPrice = await calculateGasPrice() + const estimatedGasCosts = gasEstimation * parseInt(gasPrice, 10) + const gasCost = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) + const gasCostFormatted = formatAmount(gasCost) + + setGasEstimation({ + gasEstimation, + gasCostFormatted, + gasLimit: gasEstimation, + }) + } + + estimateGas() + }, [numOwners, userAccount, safeCreationSalt, addresses]) + + return gasEstimation +} diff --git a/src/routes/load/components/ReviewInformation/index.tsx b/src/routes/load/components/ReviewInformation/index.tsx index edf41823..ae729f36 100644 --- a/src/routes/load/components/ReviewInformation/index.tsx +++ b/src/routes/load/components/ReviewInformation/index.tsx @@ -17,8 +17,9 @@ import { getAccountsFrom } from 'src/routes/open/utils/safeDataExtractor' import { useStyles } from './styles' import { getExplorerInfo } from 'src/config' import { ExplorerButton } from '@gnosis.pm/safe-react-components' +import { LoadFormValues } from 'src/routes/load/container/Load' -const checkIfUserAddressIsAnOwner = (values: Record, userAddress: string): boolean => { +const checkIfUserAddressIsAnOwner = (values: LoadFormValues, userAddress: string): boolean => { let isOwner = false for (let i = 0; i < getNumOwnersFrom(values); i += 1) { @@ -33,7 +34,7 @@ const checkIfUserAddressIsAnOwner = (values: Record, userAddress interface Props { userAddress: string - values: Record + values: LoadFormValues } const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement => { @@ -138,7 +139,7 @@ const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement => } const Review = ({ userAddress }: { userAddress: string }) => - function ReviewPage(controls: React.ReactNode, { values }: { values: Record }): React.ReactElement { + function ReviewPage(controls: React.ReactNode, { values }: { values: LoadFormValues }): React.ReactElement { return ( <> diff --git a/src/routes/load/container/Load.tsx b/src/routes/load/container/Load.tsx index 6e4b5305..1557ba3a 100644 --- a/src/routes/load/container/Load.tsx +++ b/src/routes/load/container/Load.tsx @@ -35,12 +35,24 @@ export const loadSafe = async ( await addSafe(safeProps) } -export interface LoadFormValues { +interface ReviewSafeCreationValues { + confirmations: string + name: string + owner0Address: string + owner0Name: string + safeCreationSalt: number +} + +interface LoadForm { name: string address: string threshold: string + owner0Address: string + owner0Name: string } +export type LoadFormValues = ReviewSafeCreationValues | LoadForm + const Load = (): React.ReactElement => { const dispatch = useDispatch() const provider = useSelector(providerNameSelector) diff --git a/src/routes/open/components/Layout.tsx b/src/routes/open/components/Layout.tsx index 9620c05b..e7e1689d 100644 --- a/src/routes/open/components/Layout.tsx +++ b/src/routes/open/components/Layout.tsx @@ -20,7 +20,7 @@ import { import { WelcomeLayout } from 'src/routes/welcome/components/index' import { history } from 'src/store' import { secondary, sm } from 'src/theme/variables' -import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors' +import { providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors' import { useSelector } from 'react-redux' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { getNameFromAddressBook } from 'src/logic/addressBook/utils' @@ -94,7 +94,6 @@ export const Layout = (props: LayoutProps): React.ReactElement => { const { onCallSafeContractSubmit, safeProps } = props const provider = useSelector(providerNameSelector) - const network = useSelector(networkSelector) const userAccount = useSelector(userAccountSelector) useEffect(() => { @@ -107,33 +106,31 @@ export const Layout = (props: LayoutProps): React.ReactElement => { const initialValues = useInitialValuesFrom(userAccount, safeProps) + if (!provider) { + return + } + return ( - <> - {provider ? ( - - - - - - - Create New Safe - - - - - - - - - ) : ( - - )} - + + + + + + + Create New Safe + + + + + + + + ) } diff --git a/src/routes/open/components/ReviewInformation/index.tsx b/src/routes/open/components/ReviewInformation/index.tsx index 4f08ff61..8e899bee 100644 --- a/src/routes/open/components/ReviewInformation/index.tsx +++ b/src/routes/open/components/ReviewInformation/index.tsx @@ -1,7 +1,6 @@ import TableContainer from '@material-ui/core/TableContainer' import classNames from 'classnames' -import React, { useEffect, useState } from 'react' -import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import React, { ReactElement, useEffect, useMemo } from 'react' import { getExplorerInfo, getNetworkInfo } from 'src/config' import CopyBtn from 'src/components/CopyBtn' import Identicon from 'src/components/Identicon' @@ -11,45 +10,41 @@ import Hairline from 'src/components/layout/Hairline' import Paragraph from 'src/components/layout/Paragraph' import Row from 'src/components/layout/Row' import OpenPaper from 'src/components/Stepper/OpenPaper' -import { estimateGasForDeployingSafe } from 'src/logic/contracts/safeContracts' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' -import { getAccountsFrom, getNamesFrom, getSafeCreationSaltFrom } from 'src/routes/open/utils/safeDataExtractor' +import { + CreateSafeValues, + getAccountsFrom, + getNamesFrom, + getSafeCreationSaltFrom, +} from 'src/routes/open/utils/safeDataExtractor' import { FIELD_CONFIRMATIONS, FIELD_NAME, getNumOwnersFrom } from '../fields' import { useStyles } from './styles' import { ExplorerButton } from '@gnosis.pm/safe-react-components' +import { useEstimateSafeCreationGas } from 'src/logic/hooks/useEstimateSafeCreationGas' +import { FormApi } from 'final-form' +import { StepperPageFormProps } from 'src/components/Stepper' +import { LoadFormValues } from 'src/routes/load/container/Load' type ReviewComponentProps = { - userAccount: string - values: any + values: LoadFormValues + form: FormApi } const { nativeCoin } = getNetworkInfo() -const ReviewComponent = ({ userAccount, values }: ReviewComponentProps) => { +const ReviewComponent = ({ values, form }: ReviewComponentProps): ReactElement => { const classes = useStyles() - const [gasCosts, setGasCosts] = useState('< 0.001') const names = getNamesFrom(values) - const addresses = getAccountsFrom(values) + const addresses = useMemo(() => getAccountsFrom(values), [values]) + const numOwners = getNumOwnersFrom(values) - const safeCreationSalt = getSafeCreationSaltFrom(values) + const safeCreationSalt = getSafeCreationSaltFrom(values as CreateSafeValues) + const { gasCostFormatted, gasLimit } = useEstimateSafeCreationGas({ addresses, numOwners, safeCreationSalt }) useEffect(() => { - const estimateGas = async () => { - if (!addresses.length || !numOwners || !userAccount) { - return - } - const estimatedGasCosts = ( - await estimateGasForDeployingSafe(addresses, numOwners, userAccount, safeCreationSalt) - ).toString() - const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals) - const formattedGasCosts = formatAmount(gasCosts) - setGasCosts(formattedGasCosts) - } - - estimateGas() - }, [addresses, numOwners, safeCreationSalt, userAccount]) + form.mutators.setValue('gasLimit', gasLimit) + }, [gasLimit, form.mutators]) return ( <> @@ -135,8 +130,8 @@ const ReviewComponent = ({ userAccount, values }: ReviewComponentProps) => { You're about to create a new Safe and will have to confirm a transaction with your currently connected - wallet. The creation will cost approximately {gasCosts} {nativeCoin.name}. The exact amount will be determined - by your wallet. + wallet. The creation will cost approximately {gasCostFormatted} {nativeCoin.name}. The exact amount will be + determined by your wallet. @@ -144,7 +139,7 @@ const ReviewComponent = ({ userAccount, values }: ReviewComponentProps) => { } export const Review = () => - function ReviewPage(controls, props): React.ReactElement { + function ReviewPage(controls: React.ReactNode, props: StepperPageFormProps): React.ReactElement { return ( <> diff --git a/src/routes/open/components/fields.ts b/src/routes/open/components/fields.ts index 4df7b36e..87d0f0b7 100644 --- a/src/routes/open/components/fields.ts +++ b/src/routes/open/components/fields.ts @@ -4,13 +4,17 @@ export const FIELD_OWNERS = 'owners' export const FIELD_SAFE_NAME = 'safeName' export const FIELD_CREATION_PROXY_SALT = 'safeCreationSalt' -export const getOwnerNameBy = (index) => `owner${index}Name` -export const getOwnerAddressBy = (index) => `owner${index}Address` +export const getOwnerNameBy = (index: number): string => `owner${index}Name` +export const getOwnerAddressBy = (index: number): string => `owner${index}Address` export const getNumOwnersFrom = (values) => { const accounts = Object.keys(values) .sort() - .filter((key) => /^owner\d+Address$/.test(key) && !!values[key]) + .filter((key) => { + const res = /^owner\d+Address$/.test(key) + + return res && !!values[key] + }) return accounts.length } diff --git a/src/routes/open/container/Open.tsx b/src/routes/open/container/Open.tsx index 9744f4f3..51136910 100644 --- a/src/routes/open/container/Open.tsx +++ b/src/routes/open/container/Open.tsx @@ -6,11 +6,12 @@ import { useLocation } from 'react-router-dom' import { PromiEvent, TransactionReceipt } from 'web3-core' import { SafeDeployment } from 'src/routes/opening' -import { InitialValuesForm, Layout } from 'src/routes/open/components/Layout' +import { Layout } from 'src/routes/open/components/Layout' import Page from 'src/components/layout/Page' import { getSafeDeploymentTransaction } from 'src/logic/contracts/safeContracts' import { checkReceiptStatus } from 'src/logic/wallets/ethTransactions' import { + CreateSafeValues, getAccountsFrom, getNamesFrom, getOwnersFrom, @@ -29,6 +30,8 @@ import { useAnalytics } from 'src/utils/googleAnalytics' const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY' +type LoadedSafeType = CreateSafeValues & { txHash: string } + interface SafeCreationQueryParams { ownerAddresses: string | string[] | null ownerNames: string | string[] | null @@ -85,7 +88,7 @@ export const getSafeProps = async ( return safeProps } -export const createSafe = (values: InitialValuesForm, userAccount: string): PromiEvent => { +export const createSafe = (values: CreateSafeValues, userAccount: string): PromiEvent => { const confirmations = getThresholdFrom(values) const name = getSafeNameFrom(values) const ownersNames = getNamesFrom(values) @@ -93,7 +96,10 @@ export const createSafe = (values: InitialValuesForm, userAccount: string): Prom const safeCreationSalt = getSafeCreationSaltFrom(values) const deploymentTx = getSafeDeploymentTransaction(ownerAddresses, confirmations, safeCreationSalt) - const promiEvent = deploymentTx.send({ from: userAccount }) + const promiEvent = deploymentTx.send({ + from: userAccount, + gas: values?.gasLimit, + }) promiEvent .once('transactionHash', (txHash) => { @@ -155,28 +161,28 @@ const Open = (): React.ReactElement => { load() }, []) - const createSafeProxy = async (formValues?: InitialValuesForm) => { + const createSafeProxy = async (formValues?: CreateSafeValues) => { let values = formValues // save form values, used when the user rejects the TX and wants to retry - if (formValues) { - const copy = { ...formValues } + if (values) { + const copy = { ...values } saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, copy) } else { - values = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + values = (await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)) as CreateSafeValues } - const promiEvent = createSafe(values as InitialValuesForm, userAccount) + const promiEvent = createSafe(values, userAccount) setCreationTxPromise(promiEvent) setShowProgress(true) } const onSafeCreated = async (safeAddress): Promise => { - const pendingCreation = await loadFromStorage<{ txHash: string }>(SAFE_PENDING_CREATION_STORAGE_KEY) + const pendingCreation = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) - const name = getSafeNameFrom(pendingCreation) - const ownersNames = getNamesFrom(pendingCreation) - const ownerAddresses = getAccountsFrom(pendingCreation) + const name = pendingCreation ? getSafeNameFrom(pendingCreation) : '' + const ownersNames = getNamesFrom(pendingCreation as CreateSafeValues) + const ownerAddresses = pendingCreation ? getAccountsFrom(pendingCreation) : [] const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses) await dispatch(addOrUpdateSafe(safeProps)) diff --git a/src/routes/open/utils/safeDataExtractor.spec.ts b/src/routes/open/utils/safeDataExtractor.spec.ts index ff1946b3..a4add102 100644 --- a/src/routes/open/utils/safeDataExtractor.spec.ts +++ b/src/routes/open/utils/safeDataExtractor.spec.ts @@ -8,6 +8,9 @@ describe('Test JS', () => { owner1Address: 'bar', owner2Address: 'baz', owners: 3, + confirmations: '0', + name: '', + safeCreationSalt: 0, } expect(getAccountsFrom(safe)).toEqual(['foo', 'bar', 'baz']) @@ -15,9 +18,15 @@ describe('Test JS', () => { it('return the names of owners', () => { const safe = { owner0Name: 'foo', + owner0Address: '0x', owner1Name: 'bar', + owner1Address: '0x', owner2Name: 'baz', + owner2Address: '0x', owners: 3, + confirmations: '0', + name: '', + safeCreationSalt: 0, } expect(getNamesFrom(safe)).toEqual(['foo', 'bar', 'baz']) @@ -31,12 +40,15 @@ describe('Test JS', () => { owner2Name: 'bazName', owner2Address: 'bazAddress', owners: 1, + confirmations: '0', + name: '', + safeCreationSalt: 0, } - expect(getNamesFrom(safe)).toEqual(['fooName']) - expect(getAccountsFrom(safe)).toEqual(['fooAddress']) + expect(getNamesFrom(safe)).toEqual(['fooName', 'barName', 'bazName']) + expect(getAccountsFrom(safe)).toEqual(['fooAddress', 'barAddress', 'bazAddress']) }) - it('return name and address ordered alphabetically', () => { + it('return name and address keys ordered alphabetically', () => { const safe = { owner1Name: 'barName', owner1Address: 'barAddress', @@ -45,14 +57,19 @@ describe('Test JS', () => { owner2Address: 'bazAddress', owner0Address: 'fooAddress', owners: 1, + confirmations: '0', + name: '', + safeCreationSalt: 0, } - expect(getNamesFrom(safe)).toEqual(['fooName']) - expect(getAccountsFrom(safe)).toEqual(['fooAddress']) + expect(getNamesFrom(safe)).toEqual(['fooName', 'barName', 'bazName']) + expect(getAccountsFrom(safe)).toEqual(['fooAddress', 'barAddress', 'bazAddress']) }) it('return the number of required confirmations', () => { const safe = { confirmations: '1', + name: '', + safeCreationSalt: 0, } expect(getThresholdFrom(safe)).toEqual(1) diff --git a/src/routes/open/utils/safeDataExtractor.ts b/src/routes/open/utils/safeDataExtractor.ts index fd380228..e44452c8 100644 --- a/src/routes/open/utils/safeDataExtractor.ts +++ b/src/routes/open/utils/safeDataExtractor.ts @@ -2,31 +2,45 @@ import { List } from 'immutable' import { makeOwner } from 'src/logic/safe/store/models/owner' import { SafeOwner } from 'src/logic/safe/store/models/safe' +import { LoadFormValues } from 'src/routes/load/container/Load' +import { getNumOwnersFrom } from 'src/routes/open/components/fields' -export const getAccountsFrom = (values) => { +export type CreateSafeValues = { + confirmations: string + name: string + owner0Address?: string + owner0Name?: string + safeCreationSalt: number + gasLimit?: number + owners?: number | string +} + +export const getAccountsFrom = (values: CreateSafeValues | LoadFormValues): string[] => { const accounts = Object.keys(values) .sort() .filter((key) => /^owner\d+Address$/.test(key)) - return accounts.map((account) => values[account]).slice(0, values.owners) + const numOwners = getNumOwnersFrom(values) + return accounts.map((account) => values[account]).slice(0, numOwners) } -export const getNamesFrom = (values) => { +export const getNamesFrom = (values: CreateSafeValues | LoadFormValues): string[] => { const accounts = Object.keys(values) .sort() .filter((key) => /^owner\d+Name$/.test(key)) - return accounts.map((account) => values[account]).slice(0, values.owners) + const numOwners = getNumOwnersFrom(values) + return accounts.map((account) => values[account]).slice(0, numOwners) } -export const getOwnersFrom = (names, addresses): List => { +export const getOwnersFrom = (names: string[], addresses: string[]): List => { const owners = names.map((name, index) => makeOwner({ name, address: addresses[index] })) return List(owners) } -export const getThresholdFrom = (values) => Number(values.confirmations) +export const getThresholdFrom = (values: CreateSafeValues): number => Number(values.confirmations) -export const getSafeNameFrom = (values) => values.name +export const getSafeNameFrom = (values: CreateSafeValues): string => values.name -export const getSafeCreationSaltFrom = (values) => values.safeCreationSalt +export const getSafeCreationSaltFrom = (values: CreateSafeValues): number => values.safeCreationSalt