From 18a6525bc660e11df37670d50d12b3c9f2010b93 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 26 Mar 2020 10:53:20 -0300 Subject: [PATCH] (Feature) Safe Deployment #605 #111 #395 #606 #396 (#659) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Stepper component * proxyfactory web3 contract * add styles to body steps * Steps info * Open component: moving from class to function * remove opening route and rendering it in Open component instead * recover safe creation from txHash in localStorage * remove commented code * restore commented code * creatign TX fix * fix createSafe then function * fixing stepper * remove unused code * remove opening route and finishin both variants of create * add loader dots svg * add error state design and loader dots * fix error section * add description to steps * adding etherscan link * taking values from variables * fix heigh in body content * add success svg * add check image on last step * fix margin and heigt to body rows * remove commented code * remove commented code * fix for #396 * Fix empty_code * set error if getReceipt fails * fixes * Fix: remove txHash from pendingInfo on retry Co-authored-by: Mikhail Mikheev Co-authored-by: Agustín Longoni Co-authored-by: Fernando --- .../dataDisplay/IconText/index.js | 6 +- .../Loader-dots/assets/loader-dots.svg | 18 + src/components-v2/index.js | 1 + .../navigation/Stepper/DotStep.jsx | 48 +++ .../navigation/Stepper/index.jsx | 69 ++++ src/components-v2/navigation/index.js | 2 + src/components/forms/validator.js | 4 +- src/logic/contracts/safeContracts.js | 33 +- src/logic/wallets/getWeb3.js | 4 +- src/routes/index.js | 12 +- .../components/ReviewInformation/index.jsx | 14 +- src/routes/open/container/Open.jsx | 186 ++++++--- src/routes/opening/assets/success.svg | 4 + src/routes/opening/assets/vault-error.svg | 13 + src/routes/opening/component/index.jsx | 79 ---- src/routes/opening/container/index.jsx | 6 +- src/routes/opening/index.jsx | 387 ++++++++++++++++++ src/routes/routes.js | 8 - yarn.lock | 1 + 19 files changed, 705 insertions(+), 190 deletions(-) create mode 100644 src/components-v2/feedback/Loader-dots/assets/loader-dots.svg create mode 100644 src/components-v2/navigation/Stepper/DotStep.jsx create mode 100644 src/components-v2/navigation/Stepper/index.jsx create mode 100644 src/components-v2/navigation/index.js create mode 100644 src/routes/opening/assets/success.svg create mode 100644 src/routes/opening/assets/vault-error.svg delete mode 100644 src/routes/opening/component/index.jsx create mode 100644 src/routes/opening/index.jsx diff --git a/src/components-v2/dataDisplay/IconText/index.js b/src/components-v2/dataDisplay/IconText/index.js index f2e8ded2..d57b28a7 100644 --- a/src/components-v2/dataDisplay/IconText/index.js +++ b/src/components-v2/dataDisplay/IconText/index.js @@ -9,16 +9,16 @@ const Wrapper = styled.div` const Icon = styled.img` max-width: 15px; max-height: 15px; - margin-right: 5px; ` const Text = styled.span` + margin-left: 5px; height: 17px; ` -const IconText = ({ iconUrl, text }: { iconUrl: string, text: string }) => ( +const IconText = ({ iconUrl, text }: { iconUrl: string, text?: string }) => ( - {text} + {text && {text}} ) diff --git a/src/components-v2/feedback/Loader-dots/assets/loader-dots.svg b/src/components-v2/feedback/Loader-dots/assets/loader-dots.svg new file mode 100644 index 00000000..bd96324c --- /dev/null +++ b/src/components-v2/feedback/Loader-dots/assets/loader-dots.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components-v2/index.js b/src/components-v2/index.js index fbe142d0..201bd707 100644 --- a/src/components-v2/index.js +++ b/src/components-v2/index.js @@ -2,6 +2,7 @@ export * from './dataDisplay' export * from './feedback' export * from './layouts' +export * from './navigation' export * from './safeUtils' export * from './surfaces' export * from './utils' diff --git a/src/components-v2/navigation/Stepper/DotStep.jsx b/src/components-v2/navigation/Stepper/DotStep.jsx new file mode 100644 index 00000000..f25e1f3c --- /dev/null +++ b/src/components-v2/navigation/Stepper/DotStep.jsx @@ -0,0 +1,48 @@ +// @flow +import React from 'react' +import styled from 'styled-components' + +import { IconText } from '~/components-v2' +import CheckIcon from '~/components/layout/PageFrame/assets/check.svg' +import { + background as backgroundColor, + secondaryText as disabledColor, + error as errorColor, + secondary, +} from '~/theme/variables' + +const Circle = styled.div` + background-color: ${({ disabled, error }) => { + if (error) { + return errorColor + } + if (disabled) { + return disabledColor + } + + return secondary + }}; + color: ${backgroundColor}; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 5px; +` + +type Props = { + dotIndex: number, + currentIndex: number, + error?: boolean, +} +const DotStep = ({ currentIndex, dotIndex, error }: Props) => { + return ( + currentIndex} error={error}> + {dotIndex < currentIndex ? : dotIndex + 1} + + ) +} + +export default DotStep diff --git a/src/components-v2/navigation/Stepper/index.jsx b/src/components-v2/navigation/Stepper/index.jsx new file mode 100644 index 00000000..90c250c4 --- /dev/null +++ b/src/components-v2/navigation/Stepper/index.jsx @@ -0,0 +1,69 @@ +// @flow +import StepMUI from '@material-ui/core/Step' +import StepLabelMUI from '@material-ui/core/StepLabel' +import StepperMUI from '@material-ui/core/Stepper' +import React from 'react' +import styled from 'styled-components' + +import DotStep from './DotStep' + +import { secondaryText as disabled, error as errorColor, primary, secondary } from '~/theme/variables' + +const StyledStepper = styled(StepperMUI)` + background-color: transparent; +` + +const StyledStepLabel = styled.p` + && { + color: ${({ activeStepIndex, error, index }) => { + if (error) { + return errorColor + } + + if (index === activeStepIndex) { + return secondary + } + + if (index < activeStepIndex) { + return disabled + } + + return primary + }}; + } +` + +type Props = { + steps: Array<{ id: string | number, label: string }>, + activeStepIndex: number, + error?: boolean, + orientation: 'vertical' | 'horizontal', +} + +const Stepper = ({ activeStepIndex, error, orientation, steps }: Props) => { + return ( + + {steps.map((s, index) => { + return ( + + + } + > + + {s.label} + + + + ) + })} + + ) +} + +export default Stepper diff --git a/src/components-v2/navigation/index.js b/src/components-v2/navigation/index.js new file mode 100644 index 00000000..c819890f --- /dev/null +++ b/src/components-v2/navigation/index.js @@ -0,0 +1,2 @@ +// @flow +export { default as Stepper } from './Stepper' diff --git a/src/components/forms/validator.js b/src/components/forms/validator.js index a992d8e2..e1ecf5de 100644 --- a/src/components/forms/validator.js +++ b/src/components/forms/validator.js @@ -8,10 +8,10 @@ import { getWeb3 } from '~/logic/wallets/getWeb3' export const simpleMemoize = (fn: Function) => { let lastArg let lastResult - return (arg: any) => { + return (arg: any, ...args: any) => { if (arg !== lastArg) { lastArg = arg - lastResult = fn(arg) + lastResult = fn(arg, ...args) } return lastResult } diff --git a/src/logic/contracts/safeContracts.js b/src/logic/contracts/safeContracts.js index 395015b3..f5d85586 100644 --- a/src/logic/contracts/safeContracts.js +++ b/src/logic/contracts/safeContracts.js @@ -5,7 +5,7 @@ import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe. import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json' import { ensureOnce } from '~/utils/singleton' import { simpleMemoize } from '~/components/forms/validator' -import { getWeb3 } from '~/logic/wallets/getWeb3' +import { getWeb3, getNetworkIdFrom } from '~/logic/wallets/getWeb3' import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions' import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses' import { isProxyCode } from '~/logic/contracts/historicProxyCode' @@ -27,9 +27,9 @@ const createGnosisSafeContract = (web3: any) => { return gnosisSafe } -const createProxyFactoryContract = (web3: any) => { - const proxyFactory = contract(ProxyFactorySol) - proxyFactory.setProvider(web3.currentProvider) +const createProxyFactoryContract = (web3: any, networkId: number) => { + const contractAddress = ProxyFactorySol.networks[networkId].address + const proxyFactory = new web3.eth.Contract(ProxyFactorySol.abi, contractAddress) return proxyFactory } @@ -39,10 +39,10 @@ const getCreateProxyFactoryContract = simpleMemoize(createProxyFactoryContract) const instantiateMasterCopies = async () => { const web3 = getWeb3() + const networkId = await getNetworkIdFrom(web3) // Create ProxyFactory Master Copy - const ProxyFactory = getCreateProxyFactoryContract(web3) - proxyFactoryMaster = await ProxyFactory.deployed() + proxyFactoryMaster = getCreateProxyFactoryContract(web3, networkId) // Initialize Safe master copy const GnosisSafe = getGnosisSafeContract(web3) @@ -70,22 +70,12 @@ export const getSafeMasterContract = async () => { return safeMaster } -export const deploySafeContract = async (safeAccounts: string[], numConfirmations: number, userAccount: string) => { - const gnosisSafeData = await safeMaster.contract.methods +export const getSafeDeploymentTransaction = (safeAccounts: string[], numConfirmations: number, userAccount: string) => { + const gnosisSafeData = safeMaster.contract.methods .setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS) - .encodeABI() - const proxyFactoryData = proxyFactoryMaster.contract.methods - .createProxy(safeMaster.address, gnosisSafeData) - .encodeABI() - const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.address) - const gasPrice = await calculateGasPrice() + .encodeABI() - return proxyFactoryMaster.createProxy(safeMaster.address, gnosisSafeData, { - from: userAccount, - gas, - gasPrice, - value: 0, - }) + return proxyFactoryMaster.methods.createProxy(safeMaster.address, gnosisSafeData) } export const estimateGasForDeployingSafe = async ( @@ -93,10 +83,11 @@ export const estimateGasForDeployingSafe = async ( numConfirmations: number, userAccount: string, ) => { + console.log(proxyFactoryMaster) const gnosisSafeData = await safeMaster.contract.methods .setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS) .encodeABI() - const proxyFactoryData = proxyFactoryMaster.contract.methods + const proxyFactoryData = proxyFactoryMaster.methods .createProxy(safeMaster.address, gnosisSafeData) .encodeABI() const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.address) diff --git a/src/logic/wallets/getWeb3.js b/src/logic/wallets/getWeb3.js index 7464dfbf..275369a8 100644 --- a/src/logic/wallets/getWeb3.js +++ b/src/logic/wallets/getWeb3.js @@ -88,9 +88,7 @@ export const getAccountFrom: Function = async (web3Provider): Promise 0 ? accounts[0] : null } -const getNetworkIdFrom = async web3Provider => { - return await web3Provider.eth.net.getId() -} +export const getNetworkIdFrom = web3Provider => web3Provider.eth.net.getId() export const getProviderInfo: Function = async ( web3Provider, diff --git a/src/routes/index.js b/src/routes/index.js index d3c8b8fa..496fdc9f 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -3,14 +3,7 @@ import React, { useEffect, useState } from 'react' import { connect } from 'react-redux' import { Redirect, Route, Switch, withRouter } from 'react-router-dom' -import { - LOAD_ADDRESS, - OPENING_ADDRESS, - OPEN_ADDRESS, - SAFELIST_ADDRESS, - SAFE_PARAM_ADDRESS, - WELCOME_ADDRESS, -} from './routes' +import { LOAD_ADDRESS, OPEN_ADDRESS, SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS } from './routes' import Welcome from './welcome/container' import Loader from '~/components/Loader' @@ -21,8 +14,6 @@ const Safe = React.lazy(() => import('./safe/container')) const Open = React.lazy(() => import('./open/container/Open')) -const Opening = React.lazy(() => import('./opening/container')) - const Load = React.lazy(() => import('./load/container/Load')) const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}` @@ -66,7 +57,6 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => { - diff --git a/src/routes/open/components/ReviewInformation/index.jsx b/src/routes/open/components/ReviewInformation/index.jsx index b0711b0d..13298cec 100644 --- a/src/routes/open/components/ReviewInformation/index.jsx +++ b/src/routes/open/components/ReviewInformation/index.jsx @@ -98,24 +98,20 @@ const ReviewComponent = ({ classes, userAccount, values }: Props) => { const numOwners = getNumOwnersFrom(values) useEffect(() => { - let isCurrent = true const estimateGas = async () => { + if (!addresses.length || !numOwners || !userAccount) { + return + } const web3 = getWeb3() const { fromWei, toBN } = web3.utils const estimatedGasCosts = await estimateGasForDeployingSafe(addresses, numOwners, userAccount) const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') const formattedGasCosts = formatAmount(gasCostsAsEth) - if (isCurrent) { - setGasCosts(formattedGasCosts) - } + setGasCosts(formattedGasCosts) } estimateGas() - - return () => { - isCurrent = false - } - }, []) + }, [addresses, numOwners, userAccount]) return ( <> diff --git a/src/routes/open/container/Open.jsx b/src/routes/open/container/Open.jsx index 8f4ea5e7..b9ab6c7a 100644 --- a/src/routes/open/container/Open.jsx +++ b/src/routes/open/container/Open.jsx @@ -1,16 +1,18 @@ // @flow import queryString from 'query-string' -import * as React from 'react' +import React, { useEffect, useState } from 'react' import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' +import Opening from '../../opening' import Layout from '../components/Layout' -import actions, { type Actions, type AddSafe } from './actions' +import actions, { type Actions } from './actions' import selector from './selector' +import { Loader } from '~/components-v2' import Page from '~/components/layout/Page' -import { deploySafeContract, getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' +import { getSafeDeploymentTransaction } from '~/logic/contracts/safeContracts' import { checkReceiptStatus } from '~/logic/wallets/ethTransactions' import { getAccountsFrom, @@ -19,9 +21,12 @@ import { getSafeNameFrom, getThresholdFrom, } from '~/routes/open/utils/safeDataExtractor' -import { OPENING_ADDRESS, SAFELIST_ADDRESS, stillInOpeningView } from '~/routes/routes' +import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from '~/routes/routes' import { buildSafe } from '~/routes/safe/store/actions/fetchSafe' import { history } from '~/store' +import { loadFromStorage, removeFromStorage, saveToStorage } from '~/utils/storage' + +const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY' type Props = Actions & { provider: string, @@ -62,76 +67,155 @@ const validateQueryParams = ( return true } -export const createSafe = async (values: Object, userAccount: string, addSafe: AddSafe): Promise => { - const numConfirmations = getThresholdFrom(values) +export const getSafeProps = async (safeAddress, safeName, ownersNames, ownerAddresses) => { + const safeProps = await buildSafe(safeAddress, safeName) + const owners = getOwnersFrom(ownersNames, ownerAddresses) + safeProps.owners = owners + + return safeProps +} + +export const createSafe = (values: Object, userAccount: string): Promise => { + const confirmations = getThresholdFrom(values) const name = getSafeNameFrom(values) const ownersNames = getNamesFrom(values) const ownerAddresses = getAccountsFrom(values) - const safe = await deploySafeContract(ownerAddresses, numConfirmations, userAccount) - await checkReceiptStatus(safe.tx) + const deploymentTxMethod = getSafeDeploymentTransaction(ownerAddresses, confirmations, userAccount) - const safeAddress = safe.logs[0].args.proxy - const safeContract = await getGnosisSafeInstanceAt(safeAddress) - const safeProps = await buildSafe(safeAddress, name) - const owners = getOwnersFrom(ownersNames, ownerAddresses) - safeProps.owners = owners + const promiEvent = deploymentTxMethod.send({ from: userAccount, value: 0 }) - addSafe(safeProps) - if (stillInOpeningView()) { + promiEvent + .once('transactionHash', txHash => { + saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, { txHash, ...values }) + }) + .then(async receipt => { + await checkReceiptStatus(receipt.transactionHash) + + const safeAddress = receipt.events.ProxyCreation.returnValues.proxy + const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses) + // returning info for testing purposes, in app is fully async + return { safeAddress: safeProps.address, safeTx: receipt } + }) + + return promiEvent +} + +const Open = ({ addSafe, network, provider, userAccount }: Props) => { + const [loading, setLoading] = useState(false) + const [showProgress, setShowProgress] = useState() + const [creationTxPromise, setCreationTxPromise] = useState() + const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState() + const [safePropsFromUrl, setSafePropsFromUrl] = useState() + + useEffect(() => { + // #122: Allow to migrate an old Multisig by passing the parameters to the URL. + const query: SafePropsType = queryString.parse(location.search, { arrayFormat: 'comma' }) + const { name, owneraddresses, ownernames, threshold } = query + if (validateQueryParams(owneraddresses, ownernames, threshold, name)) { + setSafePropsFromUrl({ + name, + ownerAddresses: owneraddresses, + ownerNames: ownernames, + threshold, + }) + } + }) + + // check if there is a safe being created + useEffect(() => { + const load = async () => { + const pendingCreation = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + if (pendingCreation && pendingCreation.txHash) { + setSafeCreationPendingInfo(pendingCreation) + setShowProgress(true) + } else { + setShowProgress(false) + } + setLoading(false) + } + + load() + }, []) + + const createSafeProxy = async formValues => { + let values = formValues + + // save form values, used when the user rejects the TX and wants to retry + if (formValues) { + const copy = { ...formValues } + saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, copy) + } else { + values = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + } + + const promiEvent = createSafe(values, userAccount, addSafe) + setCreationTxPromise(promiEvent) + setShowProgress(true) + } + + const onSafeCreated = async safeAddress => { + const pendingCreation = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + + const name = getSafeNameFrom(pendingCreation) + const ownersNames = getNamesFrom(pendingCreation) + const ownerAddresses = getAccountsFrom(pendingCreation) + const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses) + addSafe(safeProps) + + removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) const url = { - pathname: `${SAFELIST_ADDRESS}/${safeContract.address}/balances`, + pathname: `${SAFELIST_ADDRESS}/${safeProps.address}/balances`, state: { name, - tx: safe.tx, + tx: pendingCreation.txHash, }, } history.push(url) } - // returning info for testing purposes, in app is fully async - return { safeAddress: safeContract.address, safeTx: safe } -} - -class Open extends React.Component { - onCallSafeContractSubmit = async values => { - try { - const { addSafe, userAccount } = this.props - createSafe(values, userAccount, addSafe) - history.push(OPENING_ADDRESS) - } catch (error) { - // eslint-disable-next-line - console.error('Error while creating the Safe: ' + error) - } + const onCancel = () => { + removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + history.push({ + pathname: `${WELCOME_ADDRESS}`, + }) } - render() { - const { location, network, provider, userAccount } = this.props - const query: SafePropsType = queryString.parse(location.search, { arrayFormat: 'comma' }) - const { name, owneraddresses, ownernames, threshold } = query + const onRetry = async () => { + const values = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + delete values.txHash + await saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, values) + setSafeCreationPendingInfo(values) + createSafeProxy() + } - let safeProps = null - if (validateQueryParams(owneraddresses, ownernames, threshold, name)) { - safeProps = { - name, - ownerAddresses: owneraddresses, - ownerNames: ownernames, - threshold, - } - } - return ( - + if (loading || showProgress === undefined) { + return + } + + return ( + + {showProgress ? ( + + ) : ( - - ) - } + )} + + ) } export default connect(selector, actions)(withRouter(Open)) diff --git a/src/routes/opening/assets/success.svg b/src/routes/opening/assets/success.svg new file mode 100644 index 00000000..d635672d --- /dev/null +++ b/src/routes/opening/assets/success.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/routes/opening/assets/vault-error.svg b/src/routes/opening/assets/vault-error.svg new file mode 100644 index 00000000..a25c46b7 --- /dev/null +++ b/src/routes/opening/assets/vault-error.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/routes/opening/component/index.jsx b/src/routes/opening/component/index.jsx deleted file mode 100644 index 395bb481..00000000 --- a/src/routes/opening/component/index.jsx +++ /dev/null @@ -1,79 +0,0 @@ -// @flow -import LinearProgress from '@material-ui/core/LinearProgress' -import { withStyles } from '@material-ui/core/styles' -import OpenInNew from '@material-ui/icons/OpenInNew' -import * as React from 'react' - -import { type SelectorProps } from '../container/selector' - -import Block from '~/components/layout/Block' -import Img from '~/components/layout/Img' -import Page from '~/components/layout/Page' -import Paragraph from '~/components/layout/Paragraph' -import { getEtherScanLink } from '~/logic/wallets/getWeb3' -import { mediumFontSize, secondary, xs } from '~/theme/variables' - -type Props = SelectorProps & { - name: string, - tx: string, - classes: Object, -} - -const vault = require('../assets/vault.svg') - -const styles = { - icon: { - height: mediumFontSize, - color: secondary, - }, - follow: { - display: 'flex', - justifyContent: 'center', - marginTop: xs, - }, - etherscan: { - color: secondary, - textDecoration: 'underline', - display: 'flex', - alignItems: 'center', - marginLeft: xs, - }, -} - -const Opening = ({ classes, name = 'Safe creation process', tx }: Props) => ( - - - {name} - - - Vault - - - - - - - Transaction submitted - - - Deploying your new Safe... - - - - - This process should take a couple of minutes.
-
- {tx && ( - - Follow progress on{' '} - - Etherscan.io - - - - )} -
-
-) - -export default withStyles(styles)(Opening) diff --git a/src/routes/opening/container/index.jsx b/src/routes/opening/container/index.jsx index b52906f2..a8c1673a 100644 --- a/src/routes/opening/container/index.jsx +++ b/src/routes/opening/container/index.jsx @@ -1,8 +1,8 @@ // @flow import { connect } from 'react-redux' -import Layout from '../component' - import selector from './selector' -export default connect(selector)(Layout) +import Opening from './index' + +export default connect(selector)(Opening) diff --git a/src/routes/opening/index.jsx b/src/routes/opening/index.jsx new file mode 100644 index 00000000..6ed21901 --- /dev/null +++ b/src/routes/opening/index.jsx @@ -0,0 +1,387 @@ +// @flow +import React, { useEffect, useState } from 'react' +import styled from 'styled-components' + +import { Loader, Stepper } from '~/components-v2' +import LoaderDots from '~/components-v2/feedback/Loader-dots/assets/loader-dots.svg' +import Button from '~/components/layout/Button' +import Heading from '~/components/layout/Heading' +import Img from '~/components/layout/Img' +import Paragraph from '~/components/layout/Paragraph' +import { initContracts } from '~/logic/contracts/safeContracts' +import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' +import { getEtherScanLink, getWeb3 } from '~/logic/wallets/getWeb3' +import { background, connected } from '~/theme/variables' + +const successSvg = require('./assets/success.svg') +const vaultErrorSvg = require('./assets/vault-error.svg') +const vaultSvg = require('./assets/vault.svg') + +const Wrapper = styled.div` + display: grid; + grid-template-columns: 250px auto; + grid-template-rows: 62px auto; + margin-bottom: 30px; +` + +const Title = styled(Heading)` + grid-column: 1/3; + grid-row: 1; +` + +const Nav = styled.div` + grid-column: 1; + grid-row: 2; +` + +const Body = styled.div` + grid-column: 2; + grid-row: 2; + text-align: center; + background-color: #ffffff; + border-radius: 5px; + min-width: 700px; + padding-top: 50px; + box-shadow: 0 0 10px 0 rgba(33, 48, 77, 0.1); + + display: grid; + grid-template-rows: 100px 50px 70px 60px 100px; +` +const EtherScanLink = styled.a` + color: ${connected}; +` + +const CardTitle = styled.div` + font-size: 20px; +` +const FullParagraph = styled(Paragraph)` + background-color: ${background}; + padding: 24px; + font-size: 16px; + margin-bottom: 16px; +` +const ButtonMargin = styled(Button)` + margin-right: 16px; +` + +const BodyImage = styled.div` + grid-row: 1; +` +const BodyDescription = styled.div` + grid-row: 2; +` +const BodyLoader = styled.div` + grid-row: 3; + display: flex; + justify-content: center; + align-items: center; +` +const BodyInstruction = styled.div` + grid-row: 4; +` +const BodyFooter = styled.div` + grid-row: 5; + + padding: 10px 0; + display: flex; + justify-content: center; + align-items: flex-end; +` + +type Props = { + provider: string, + creationTxHash: Promise, + submittedPromise: Promise, + onRetry: () => void, + onSuccess: () => void, + onCancel: () => void, +} + +const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider, submittedPromise }: Props) => { + const [loading, setLoading] = useState(true) + const [stepIndex, setStepIndex] = useState() + const [safeCreationTxHash, setSafeCreationTxHash] = useState() + const [createdSafeAddress, setCreatedSafeAddress] = useState() + + const [error, setError] = useState(false) + const [intervalStarted, setIntervalStarted] = useState(false) + const [waitingSafeDeployed, setWaitingSafeDeployed] = useState(false) + const [continueButtonDisabled, setContinueButtonDisabled] = useState(false) + + const genericFooter = ( + +

This process should take a couple of minutes.

+

+ Follow the progress on{' '} + + Etherscan.io + + . +

+
+ ) + + const navigateToSafe = () => { + setContinueButtonDisabled(true) + onSuccess(createdSafeAddress) + } + + const steps = [ + { + id: '1', + label: 'Waiting fot transaction confirmation', + description: undefined, + instruction: 'Please confirm the Safe creation in your wallet', + footer: null, + }, + { + id: '2', + label: 'Transaction submitted', + description: undefined, + instruction: 'Please do not leave the page', + footer: genericFooter, + }, + { + id: '3', + label: 'Validating transaction', + description: undefined, + instruction: 'Please do not leave the page', + footer: genericFooter, + }, + { + id: '4', + label: 'Deploying smart contract', + description: undefined, + instruction: 'Please do not leave the page', + footer: genericFooter, + }, + { + id: '5', + label: 'Generating your Safe', + description: undefined, + instruction: 'Please do not leave the page', + footer: genericFooter, + }, + { + id: '6', + label: 'Success', + description: 'Your Safe was created successfully', + instruction: 'Click Below to get started', + footer: ( + + ), + }, + ] + + const onError = error => { + setIntervalStarted(false) + setWaitingSafeDeployed(false) + setContinueButtonDisabled(false) + setError(true) + console.error(error) + } + + // discard click event value + const onRetryTx = () => { + setStepIndex(0) + setError(false) + onRetry() + } + + const getImage = () => { + if (error) { + return vaultErrorSvg + } + + if (stepIndex <= 4) { + return vaultSvg + } + + return successSvg + } + + useEffect(() => { + const loadContracts = async () => { + await initContracts() + setLoading(false) + } + + if (provider) { + loadContracts() + } + }, [provider]) + + // creating safe from from submission + useEffect(() => { + if (submittedPromise === undefined) { + return + } + + setStepIndex(0) + submittedPromise + .once('transactionHash', txHash => { + setSafeCreationTxHash(txHash) + setStepIndex(1) + setIntervalStarted(true) + }) + .on('error', onError) + }, [submittedPromise]) + + // recovering safe creation from txHash + useEffect(() => { + if (creationTxHash === undefined) { + return + } + setSafeCreationTxHash(creationTxHash) + setStepIndex(1) + setIntervalStarted(true) + }, [creationTxHash]) + + useEffect(() => { + if (!intervalStarted) { + return + } + + const isTxMined = async txHash => { + const web3 = getWeb3() + + const receipt = await web3.eth.getTransactionReceipt(txHash) + if (!receipt.status) { + throw Error('TX status reverted') + } + const txResult = await web3.eth.getTransaction(txHash) + return txResult.blockNumber !== null + } + + let interval = setInterval(async () => { + if (stepIndex < 4) { + setStepIndex(stepIndex + 1) + } + + // safe created using the form + if (submittedPromise !== undefined) { + submittedPromise.then(() => { + setStepIndex(4) + setWaitingSafeDeployed(true) + setIntervalStarted(false) + }) + } + + // safe pending creation recovered from storage + if (creationTxHash !== undefined) { + try { + const res = await isTxMined(creationTxHash) + if (res) { + setStepIndex(4) + setWaitingSafeDeployed(true) + setIntervalStarted(false) + } + } catch (error) { + onError(error) + } + } + }, 3000) + + return () => { + clearInterval(interval) + } + }, [creationTxHash, submittedPromise, intervalStarted, stepIndex, error]) + + useEffect(() => { + let interval + + const awaitUntilSafeIsDeployed = async () => { + try { + const web3 = getWeb3() + const receipt = await web3.eth.getTransactionReceipt(safeCreationTxHash) + + // get the address for the just created safe + const events = web3.eth.abi.decodeLog( + [ + { + type: 'address', + name: 'ProxyCreation', + }, + ], + receipt.logs[0].data, + receipt.logs[0].topics, + ) + const safeAddress = events[0] + setCreatedSafeAddress(safeAddress) + + interval = setInterval(async () => { + const code = await web3.eth.getCode(safeAddress) + if (code !== EMPTY_DATA) { + setStepIndex(5) + } + }, 1000) + } catch (error) { + onError(error) + } + } + + if (!waitingSafeDeployed) { + return + } + + awaitUntilSafeIsDeployed() + + return () => { + clearInterval(interval) + } + }, [waitingSafeDeployed]) + + if (loading || stepIndex === undefined) { + return + } + + return ( + + Safe creation process + + + + Vault + + + + {steps[stepIndex].description || steps[stepIndex].label} + + + {!error && stepIndex <= 4 && LoaderDots} + + + + {error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction} + + + + + {error ? ( + <> + + Cancel + + + + ) : ( + steps[stepIndex].footer + )} + + + + ) +} + +export default SafeDeployment diff --git a/src/routes/routes.js b/src/routes/routes.js index df991cb3..a1e6cfcb 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -1,14 +1,6 @@ // @flow -import { history } from '~/store' - export const SAFE_PARAM_ADDRESS = 'address' export const SAFELIST_ADDRESS = '/safes' export const OPEN_ADDRESS = '/open' export const LOAD_ADDRESS = '/load' export const WELCOME_ADDRESS = '/welcome' -export const OPENING_ADDRESS = '/opening' - -export const stillInOpeningView = () => { - const path = history.location.pathname - return path === OPENING_ADDRESS -} diff --git a/yarn.lock b/yarn.lock index 77df07bb..dfc2ae20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17587,6 +17587,7 @@ websocket@1.0.29, "websocket@github:web3-js/WebSocket-Node#polyfill/globalThis": dependencies: debug "^2.2.0" es5-ext "^0.10.50" + gulp "^4.0.2" nan "^2.14.0" typedarray-to-buffer "^3.1.5" yaeti "^0.0.6"