(Feature) Safe Deployment #605 #111 #395 #606 #396 (#659)

* 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 <mmvsha73@gmail.com>
Co-authored-by: Agustín Longoni <agustin.longoni@altoros.com>
Co-authored-by: Fernando <fernando.greco@gmail.com>
This commit is contained in:
nicolas 2020-03-26 10:53:20 -03:00 committed by GitHub
parent c19b29854f
commit 18a6525bc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 705 additions and 190 deletions

View File

@ -9,16 +9,16 @@ const Wrapper = styled.div`
const Icon = styled.img` const Icon = styled.img`
max-width: 15px; max-width: 15px;
max-height: 15px; max-height: 15px;
margin-right: 5px;
` `
const Text = styled.span` const Text = styled.span`
margin-left: 5px;
height: 17px; height: 17px;
` `
const IconText = ({ iconUrl, text }: { iconUrl: string, text: string }) => ( const IconText = ({ iconUrl, text }: { iconUrl: string, text?: string }) => (
<Wrapper> <Wrapper>
<Icon alt={text} src={iconUrl} /> <Icon alt={text} src={iconUrl} />
<Text>{text}</Text> {text && <Text>{text}</Text>}
</Wrapper> </Wrapper>
) )

View File

@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="91px" height="91px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="84" cy="50" r="0.271746" fill="#d4d5d3">
<animate attributeName="r" repeatCount="indefinite" dur="1.7857142857142856s" calcMode="spline" keyTimes="0;1" values="10;0" keySplines="0 0.5 0.5 1" begin="0s"></animate>
<animate attributeName="fill" repeatCount="indefinite" dur="7.142857142857142s" calcMode="discrete" keyTimes="0;0.25;0.5;0.75;1" values="#d4d5d3;#d4d5d3;#d4d5d3;#d4d5d3;#d4d5d3" begin="0s"></animate>
</circle><circle cx="49.076" cy="50" r="10" fill="#d4d5d3">
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
</circle><circle cx="83.076" cy="50" r="10" fill="#d4d5d3">
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.7857142857142856s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.7857142857142856s"></animate>
</circle><circle cx="16" cy="50" r="0" fill="#d4d5d3">
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-3.571428571428571s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-3.571428571428571s"></animate>
</circle><circle cx="16" cy="50" r="9.72825" fill="#d4d5d3">
<animate attributeName="r" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-5.357142857142857s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="7.142857142857142s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-5.357142857142857s"></animate>
</circle>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -2,6 +2,7 @@
export * from './dataDisplay' export * from './dataDisplay'
export * from './feedback' export * from './feedback'
export * from './layouts' export * from './layouts'
export * from './navigation'
export * from './safeUtils' export * from './safeUtils'
export * from './surfaces' export * from './surfaces'
export * from './utils' export * from './utils'

View File

@ -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 (
<Circle disabled={dotIndex > currentIndex} error={error}>
{dotIndex < currentIndex ? <IconText iconUrl={CheckIcon} /> : dotIndex + 1}
</Circle>
)
}
export default DotStep

View File

@ -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 (
<StyledStepper activeStep={activeStepIndex} orientation={orientation}>
{steps.map((s, index) => {
return (
<StepMUI key={s.id}>
<StepLabelMUI
icon={
<DotStep currentIndex={activeStepIndex} dotIndex={index} error={index === activeStepIndex && error} />
}
>
<StyledStepLabel
activeStepIndex={activeStepIndex}
error={index === activeStepIndex && error}
index={index}
>
{s.label}
</StyledStepLabel>
</StepLabelMUI>
</StepMUI>
)
})}
</StyledStepper>
)
}
export default Stepper

View File

@ -0,0 +1,2 @@
// @flow
export { default as Stepper } from './Stepper'

View File

@ -8,10 +8,10 @@ import { getWeb3 } from '~/logic/wallets/getWeb3'
export const simpleMemoize = (fn: Function) => { export const simpleMemoize = (fn: Function) => {
let lastArg let lastArg
let lastResult let lastResult
return (arg: any) => { return (arg: any, ...args: any) => {
if (arg !== lastArg) { if (arg !== lastArg) {
lastArg = arg lastArg = arg
lastResult = fn(arg) lastResult = fn(arg, ...args)
} }
return lastResult return lastResult
} }

View File

@ -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 SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
import { ensureOnce } from '~/utils/singleton' import { ensureOnce } from '~/utils/singleton'
import { simpleMemoize } from '~/components/forms/validator' 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 { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions'
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses' import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
import { isProxyCode } from '~/logic/contracts/historicProxyCode' import { isProxyCode } from '~/logic/contracts/historicProxyCode'
@ -27,9 +27,9 @@ const createGnosisSafeContract = (web3: any) => {
return gnosisSafe return gnosisSafe
} }
const createProxyFactoryContract = (web3: any) => { const createProxyFactoryContract = (web3: any, networkId: number) => {
const proxyFactory = contract(ProxyFactorySol) const contractAddress = ProxyFactorySol.networks[networkId].address
proxyFactory.setProvider(web3.currentProvider) const proxyFactory = new web3.eth.Contract(ProxyFactorySol.abi, contractAddress)
return proxyFactory return proxyFactory
} }
@ -39,10 +39,10 @@ const getCreateProxyFactoryContract = simpleMemoize(createProxyFactoryContract)
const instantiateMasterCopies = async () => { const instantiateMasterCopies = async () => {
const web3 = getWeb3() const web3 = getWeb3()
const networkId = await getNetworkIdFrom(web3)
// Create ProxyFactory Master Copy // Create ProxyFactory Master Copy
const ProxyFactory = getCreateProxyFactoryContract(web3) proxyFactoryMaster = getCreateProxyFactoryContract(web3, networkId)
proxyFactoryMaster = await ProxyFactory.deployed()
// Initialize Safe master copy // Initialize Safe master copy
const GnosisSafe = getGnosisSafeContract(web3) const GnosisSafe = getGnosisSafeContract(web3)
@ -70,22 +70,12 @@ export const getSafeMasterContract = async () => {
return safeMaster return safeMaster
} }
export const deploySafeContract = async (safeAccounts: string[], numConfirmations: number, userAccount: string) => { export const getSafeDeploymentTransaction = (safeAccounts: string[], numConfirmations: number, userAccount: string) => {
const gnosisSafeData = await safeMaster.contract.methods const gnosisSafeData = safeMaster.contract.methods
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS) .setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
.encodeABI() .encodeABI()
const proxyFactoryData = proxyFactoryMaster.contract.methods
.createProxy(safeMaster.address, gnosisSafeData)
.encodeABI()
const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.address)
const gasPrice = await calculateGasPrice()
return proxyFactoryMaster.createProxy(safeMaster.address, gnosisSafeData, { return proxyFactoryMaster.methods.createProxy(safeMaster.address, gnosisSafeData)
from: userAccount,
gas,
gasPrice,
value: 0,
})
} }
export const estimateGasForDeployingSafe = async ( export const estimateGasForDeployingSafe = async (
@ -93,10 +83,11 @@ export const estimateGasForDeployingSafe = async (
numConfirmations: number, numConfirmations: number,
userAccount: string, userAccount: string,
) => { ) => {
console.log(proxyFactoryMaster)
const gnosisSafeData = await safeMaster.contract.methods const gnosisSafeData = await safeMaster.contract.methods
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS) .setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
.encodeABI() .encodeABI()
const proxyFactoryData = proxyFactoryMaster.contract.methods const proxyFactoryData = proxyFactoryMaster.methods
.createProxy(safeMaster.address, gnosisSafeData) .createProxy(safeMaster.address, gnosisSafeData)
.encodeABI() .encodeABI()
const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.address) const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.address)

View File

@ -88,9 +88,7 @@ export const getAccountFrom: Function = async (web3Provider): Promise<string | n
return accounts && accounts.length > 0 ? accounts[0] : null return accounts && accounts.length > 0 ? accounts[0] : null
} }
const getNetworkIdFrom = async web3Provider => { export const getNetworkIdFrom = web3Provider => web3Provider.eth.net.getId()
return await web3Provider.eth.net.getId()
}
export const getProviderInfo: Function = async ( export const getProviderInfo: Function = async (
web3Provider, web3Provider,

View File

@ -3,14 +3,7 @@ import React, { useEffect, useState } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Redirect, Route, Switch, withRouter } from 'react-router-dom' import { Redirect, Route, Switch, withRouter } from 'react-router-dom'
import { import { LOAD_ADDRESS, OPEN_ADDRESS, SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS } from './routes'
LOAD_ADDRESS,
OPENING_ADDRESS,
OPEN_ADDRESS,
SAFELIST_ADDRESS,
SAFE_PARAM_ADDRESS,
WELCOME_ADDRESS,
} from './routes'
import Welcome from './welcome/container' import Welcome from './welcome/container'
import Loader from '~/components/Loader' 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 Open = React.lazy(() => import('./open/container/Open'))
const Opening = React.lazy(() => import('./opening/container'))
const Load = React.lazy(() => import('./load/container/Load')) const Load = React.lazy(() => import('./load/container/Load'))
const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}` const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}`
@ -66,7 +57,6 @@ const Routes = ({ defaultSafe, location }: RoutesProps) => {
<Route component={withTracker(Welcome)} exact path={WELCOME_ADDRESS} /> <Route component={withTracker(Welcome)} exact path={WELCOME_ADDRESS} />
<Route component={withTracker(Open)} exact path={OPEN_ADDRESS} /> <Route component={withTracker(Open)} exact path={OPEN_ADDRESS} />
<Route component={withTracker(Safe)} path={SAFE_ADDRESS} /> <Route component={withTracker(Safe)} path={SAFE_ADDRESS} />
<Route component={withTracker(Opening)} exact path={OPENING_ADDRESS} />
<Route component={withTracker(Load)} exact path={LOAD_ADDRESS} /> <Route component={withTracker(Load)} exact path={LOAD_ADDRESS} />
<Redirect to="/" /> <Redirect to="/" />
</Switch> </Switch>

View File

@ -98,24 +98,20 @@ const ReviewComponent = ({ classes, userAccount, values }: Props) => {
const numOwners = getNumOwnersFrom(values) const numOwners = getNumOwnersFrom(values)
useEffect(() => { useEffect(() => {
let isCurrent = true
const estimateGas = async () => { const estimateGas = async () => {
if (!addresses.length || !numOwners || !userAccount) {
return
}
const web3 = getWeb3() const web3 = getWeb3()
const { fromWei, toBN } = web3.utils const { fromWei, toBN } = web3.utils
const estimatedGasCosts = await estimateGasForDeployingSafe(addresses, numOwners, userAccount) const estimatedGasCosts = await estimateGasForDeployingSafe(addresses, numOwners, userAccount)
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether') const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
const formattedGasCosts = formatAmount(gasCostsAsEth) const formattedGasCosts = formatAmount(gasCostsAsEth)
if (isCurrent) { setGasCosts(formattedGasCosts)
setGasCosts(formattedGasCosts)
}
} }
estimateGas() estimateGas()
}, [addresses, numOwners, userAccount])
return () => {
isCurrent = false
}
}, [])
return ( return (
<> <>

View File

@ -1,16 +1,18 @@
// @flow // @flow
import queryString from 'query-string' import queryString from 'query-string'
import * as React from 'react' import React, { useEffect, useState } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom' import { withRouter } from 'react-router-dom'
import Opening from '../../opening'
import Layout from '../components/Layout' import Layout from '../components/Layout'
import actions, { type Actions, type AddSafe } from './actions' import actions, { type Actions } from './actions'
import selector from './selector' import selector from './selector'
import { Loader } from '~/components-v2'
import Page from '~/components/layout/Page' 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 { checkReceiptStatus } from '~/logic/wallets/ethTransactions'
import { import {
getAccountsFrom, getAccountsFrom,
@ -19,9 +21,12 @@ import {
getSafeNameFrom, getSafeNameFrom,
getThresholdFrom, getThresholdFrom,
} from '~/routes/open/utils/safeDataExtractor' } 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 { buildSafe } from '~/routes/safe/store/actions/fetchSafe'
import { history } from '~/store' 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 & { type Props = Actions & {
provider: string, provider: string,
@ -62,76 +67,155 @@ const validateQueryParams = (
return true return true
} }
export const createSafe = async (values: Object, userAccount: string, addSafe: AddSafe): Promise<OpenState> => { export const getSafeProps = async (safeAddress, safeName, ownersNames, ownerAddresses) => {
const numConfirmations = getThresholdFrom(values) const safeProps = await buildSafe(safeAddress, safeName)
const owners = getOwnersFrom(ownersNames, ownerAddresses)
safeProps.owners = owners
return safeProps
}
export const createSafe = (values: Object, userAccount: string): Promise<OpenState> => {
const confirmations = getThresholdFrom(values)
const name = getSafeNameFrom(values) const name = getSafeNameFrom(values)
const ownersNames = getNamesFrom(values) const ownersNames = getNamesFrom(values)
const ownerAddresses = getAccountsFrom(values) const ownerAddresses = getAccountsFrom(values)
const safe = await deploySafeContract(ownerAddresses, numConfirmations, userAccount) const deploymentTxMethod = getSafeDeploymentTransaction(ownerAddresses, confirmations, userAccount)
await checkReceiptStatus(safe.tx)
const safeAddress = safe.logs[0].args.proxy const promiEvent = deploymentTxMethod.send({ from: userAccount, value: 0 })
const safeContract = await getGnosisSafeInstanceAt(safeAddress)
const safeProps = await buildSafe(safeAddress, name)
const owners = getOwnersFrom(ownersNames, ownerAddresses)
safeProps.owners = owners
addSafe(safeProps) promiEvent
if (stillInOpeningView()) { .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 = { const url = {
pathname: `${SAFELIST_ADDRESS}/${safeContract.address}/balances`, pathname: `${SAFELIST_ADDRESS}/${safeProps.address}/balances`,
state: { state: {
name, name,
tx: safe.tx, tx: pendingCreation.txHash,
}, },
} }
history.push(url) history.push(url)
} }
// returning info for testing purposes, in app is fully async const onCancel = () => {
return { safeAddress: safeContract.address, safeTx: safe } removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
} history.push({
pathname: `${WELCOME_ADDRESS}`,
class Open extends React.Component<Props> { })
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)
}
} }
render() { const onRetry = async () => {
const { location, network, provider, userAccount } = this.props const values = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
const query: SafePropsType = queryString.parse(location.search, { arrayFormat: 'comma' }) delete values.txHash
const { name, owneraddresses, ownernames, threshold } = query await saveToStorage(SAFE_PENDING_CREATION_STORAGE_KEY, values)
setSafeCreationPendingInfo(values)
createSafeProxy()
}
let safeProps = null if (loading || showProgress === undefined) {
if (validateQueryParams(owneraddresses, ownernames, threshold, name)) { return <Loader />
safeProps = { }
name,
ownerAddresses: owneraddresses, return (
ownerNames: ownernames, <Page>
threshold, {showProgress ? (
} <Opening
} creationTxHash={safeCreationPendingInfo ? safeCreationPendingInfo.txHash : undefined}
return ( onCancel={onCancel}
<Page> onRetry={onRetry}
onSuccess={onSafeCreated}
provider={provider}
submittedPromise={creationTxPromise}
/>
) : (
<Layout <Layout
network={network} network={network}
onCallSafeContractSubmit={this.onCallSafeContractSubmit} onCallSafeContractSubmit={createSafeProxy}
provider={provider} provider={provider}
safeProps={safeProps} safeProps={safePropsFromUrl}
userAccount={userAccount} userAccount={userAccount}
/> />
</Page> )}
) </Page>
} )
} }
export default connect(selector, actions)(withRouter(Open)) export default connect(selector, actions)(withRouter(Open))

View File

@ -0,0 +1,4 @@
<svg width="89" height="89" viewBox="0 0 89 89" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M44.25 0C53.0018 0 61.5571 2.59522 68.834 7.45747C76.1109 12.3197 81.7825 19.2306 85.1317 27.3163C88.4808 35.4019 89.3571 44.2991 87.6497 52.8827C85.9424 61.4664 81.728 69.351 75.5395 75.5395C69.351 81.728 61.4664 85.9424 52.8827 87.6497C44.2991 89.3571 35.4019 88.4808 27.3163 85.1317C19.2306 81.7825 12.3197 76.1109 7.45747 68.834C2.59522 61.5571 0 53.0018 0 44.25C0.0164019 32.5192 4.68371 21.2736 12.9786 12.9786C21.2736 4.68371 32.5192 0.0164019 44.25 0ZM44.25 4.445C36.3785 4.445 28.6838 6.7791 22.1388 11.1522C15.5939 15.5252 10.4926 21.7408 7.48007 29.013C4.46756 36.2853 3.67909 44.2874 5.21438 52.0078C6.74967 59.7281 10.5398 66.8198 16.1054 72.3861C21.671 77.9524 28.7622 81.7434 36.4823 83.2796C44.2025 84.8159 52.2048 84.0284 59.4773 81.0168C66.7499 78.0052 72.9662 72.9048 77.3401 66.3603C81.7139 59.8159 84.049 52.1215 84.05 44.25C84.0323 33.6998 79.8334 23.5868 72.3733 16.1267C64.9132 8.66661 54.8002 4.46772 44.25 4.45V4.445Z" fill="#D4D5D3"/>
<path d="M66.077 31.405L69.3 34.465L40.146 65.174L19.2 43.111L22.423 40.05L40.146 58.718L66.077 31.405Z" fill="#008C73"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="111" height="91" viewBox="0 0 111 91" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<g fill="#B2B5B2">
<path d="M6.535 81.666h9.8V78.4h-9.8v3.266zm64.131-6.535h-67.4V3.266H94.73l.003 47.754c.203-.004.403-.015.608-.015.898 0 1.785.051 2.659.144V0H0v78.4h3.265v6.535H19.6V78.4h51.202c-.097-.888-.15-1.79-.15-2.705 0-.189.01-.376.014-.564z"/>
<path d="M9.8 68.582L9.816 9.8l78.385.015v4.885h-1.635a1.64 1.64 0 0 0-1.635 1.634c0 .9.736 1.635 1.635 1.635H88.2v1.634h-1.635a1.64 1.64 0 0 0-1.635 1.635c0 .9.736 1.635 1.635 1.635H88.2v29.184c1.06-.32 2.15-.572 3.266-.748V22.866c.899 0 1.634-.736 1.634-1.634 0-.9-.735-1.635-1.634-1.635v-1.635a1.64 1.64 0 0 0 1.634-1.635c0-.9-.735-1.635-1.634-1.635V9.816a3.284 3.284 0 0 0-3.281-3.285H9.816a3.283 3.283 0 0 0-3.281 3.285v58.766a3.283 3.283 0 0 0 3.28 3.284H70.95c.173-1.114.423-2.2.74-3.26L9.8 68.582z"/>
<path d="M60.435 42.466A3.276 3.276 0 0 1 57.17 39.2a3.276 3.276 0 0 1 3.265-3.266 3.277 3.277 0 0 1 3.266 3.266 3.276 3.276 0 0 1-3.266 3.265m0-9.8a6.533 6.533 0 0 0-6.535 6.535 6.534 6.534 0 0 0 6.535 6.535 6.535 6.535 0 0 0 0-13.069"/>
<path d="M75.136 40.832h1.55a16.242 16.242 0 0 1-3.61 8.689l-1.095-1.095a1.633 1.633 0 0 0-2.305 0 1.634 1.634 0 0 0 0 2.305l1.095 1.095a16.303 16.303 0 0 1-8.69 3.61l.004-1.535c0-.9-.735-1.635-1.634-1.635-.9 0-1.635.736-1.635 1.635v1.55a16.249 16.249 0 0 1-8.69-3.61l1.095-1.095a1.631 1.631 0 0 0 0-2.304 1.63 1.63 0 0 0-2.305 0l-1.095 1.095a16.303 16.303 0 0 1-3.61-8.69l1.524.004c.9 0 1.634-.735 1.634-1.635s-.734-1.634-1.634-1.634h-1.55a16.25 16.25 0 0 1 3.61-8.69l1.095 1.094c.325.325.734.475 1.16.475.424 0 .834-.165 1.159-.475a1.63 1.63 0 0 0 0-2.305l-1.095-1.095a16.309 16.309 0 0 1 8.69-3.61l-.003 1.524c0 .9.734 1.635 1.634 1.635.9 0 1.635-.734 1.635-1.635v-1.55a16.254 16.254 0 0 1 8.69 3.61l-1.095 1.095a1.63 1.63 0 0 0 0 2.304c.326.326.735.475 1.16.475a1.67 1.67 0 0 0 1.16-.475l1.095-1.094a16.3 16.3 0 0 1 3.61 8.69l-1.554-.004c-.9 0-1.635.734-1.635 1.634a1.65 1.65 0 0 0 1.635 1.647M74.3 25.347c-.015-.016-.035-.016-.05-.035-3.56-3.525-8.426-5.71-13.816-5.71-5.39 0-10.256 2.189-13.8 5.7-.016.014-.035.014-.05.033-.016.015-.016.034-.035.05-3.53 3.557-5.715 8.426-5.715 13.816 0 5.39 2.19 10.256 5.7 13.8.015.016.015.034.034.05.016.015.035.015.05.034 3.557 3.526 8.426 5.716 13.816 5.716 5.39 0 10.256-2.19 13.8-5.7.016-.016.035-.016.051-.035.015-.015.015-.034.034-.049 3.525-3.56 5.716-8.427 5.716-13.816 0-5.39-2.19-10.26-5.701-13.804-.015-.016-.015-.035-.034-.05M17.966 22.862c-.9 0-1.635.735-1.635 1.634v32.666c0 .9.734 1.635 1.635 1.635.899 0 1.634-.735 1.634-1.635v-1.634c.9 0 1.635-.736 1.635-1.635 0-.9-.735-1.635-1.635-1.635V29.4c.9 0 1.635-.734 1.635-1.634 0-.9-.735-1.635-1.635-1.635v-1.635c0-.898-.735-1.634-1.634-1.634M26.135 22.866c-.9 0-1.635.735-1.635 1.634v1.636c-.9 0-1.635.734-1.635 1.634 0 .9.736 1.634 1.635 1.634V52.27c-.9 0-1.635.735-1.635 1.634 0 .9.736 1.636 1.635 1.636v1.634c0 .9.735 1.635 1.635 1.635s1.634-.735 1.634-1.635L27.765 24.5c0-.899-.734-1.634-1.63-1.634"/>
</g>
<path stroke-width="3" d="M93.756 58.266c-8.466 0-15.361 6.896-15.361 15.362 0 8.466 6.895 15.36 15.361 15.36 8.466 0 15.361-6.894 15.361-15.36s-6.895-15.362-15.361-15.362z" style="stroke: rgb(242, 72, 34);"/>
<path fill-rule="nonzero" d="M93.68 68.471h-3.06v10.313h8.39v-3.06h-5.33z" style="fill: rgb(242, 72, 34);"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -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) => (
<Page align="center">
<Paragraph align="center" color="primary" size="xxl" weight="bold">
{name}
</Paragraph>
<Block align="center" margin="lg">
<Img alt="Vault" height={90} src={vault} />
</Block>
<Block margin="lg">
<LinearProgress color="secondary" />
</Block>
<Block margin="md">
<Paragraph align="center" className={classes.page} noMargin size="xl">
Transaction submitted
</Paragraph>
<Paragraph align="center" className={classes.page} noMargin size="xl" weight="bolder">
Deploying your new Safe...
</Paragraph>
</Block>
<Block margin="md">
<Paragraph align="center" noMargin size="md" weight="light">
This process should take a couple of minutes. <br />
</Paragraph>
{tx && (
<Paragraph align="center" className={classes.follow} noMargin size="md" weight="light">
Follow progress on{' '}
<a className={classes.etherscan} href={getEtherScanLink('tx', tx)} rel="noopener noreferrer" target="_blank">
Etherscan.io
<OpenInNew className={classes.icon} />
</a>
</Paragraph>
)}
</Block>
</Page>
)
export default withStyles(styles)(Opening)

View File

@ -1,8 +1,8 @@
// @flow // @flow
import { connect } from 'react-redux' import { connect } from 'react-redux'
import Layout from '../component'
import selector from './selector' import selector from './selector'
export default connect(selector)(Layout) import Opening from './index'
export default connect(selector)(Opening)

View File

@ -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<any>,
submittedPromise: Promise<any>,
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 = (
<span>
<p>This process should take a couple of minutes.</p>
<p>
Follow the progress on{' '}
<EtherScanLink
aria-label="Show details on Etherscan"
href={getEtherScanLink('tx', safeCreationTxHash)}
rel="noopener noreferrer"
target="_blank"
>
Etherscan.io
</EtherScanLink>
.
</p>
</span>
)
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: (
<Button color="primary" disabled={continueButtonDisabled} onClick={navigateToSafe} variant="contained">
Continue
</Button>
),
},
]
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 <Loader />
}
return (
<Wrapper>
<Title tag="h2">Safe creation process</Title>
<Nav>
<Stepper activeStepIndex={stepIndex} error={error} orientation="vertical" steps={steps} />
</Nav>
<Body>
<BodyImage>
<Img alt="Vault" height={75} src={getImage()} />
</BodyImage>
<BodyDescription>
<CardTitle>{steps[stepIndex].description || steps[stepIndex].label}</CardTitle>
</BodyDescription>
<BodyLoader>{!error && stepIndex <= 4 && <Img alt="LoaderDots" src={LoaderDots} />}</BodyLoader>
<BodyInstruction>
<FullParagraph color="primary" noMargin size="md">
{error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction}
</FullParagraph>
</BodyInstruction>
<BodyFooter>
{error ? (
<>
<ButtonMargin onClick={onCancel} variant="contained">
Cancel
</ButtonMargin>
<Button color="primary" onClick={onRetryTx} variant="contained">
Retry
</Button>
</>
) : (
steps[stepIndex].footer
)}
</BodyFooter>
</Body>
</Wrapper>
)
}
export default SafeDeployment

View File

@ -1,14 +1,6 @@
// @flow // @flow
import { history } from '~/store'
export const SAFE_PARAM_ADDRESS = 'address' export const SAFE_PARAM_ADDRESS = 'address'
export const SAFELIST_ADDRESS = '/safes' export const SAFELIST_ADDRESS = '/safes'
export const OPEN_ADDRESS = '/open' export const OPEN_ADDRESS = '/open'
export const LOAD_ADDRESS = '/load' export const LOAD_ADDRESS = '/load'
export const WELCOME_ADDRESS = '/welcome' export const WELCOME_ADDRESS = '/welcome'
export const OPENING_ADDRESS = '/opening'
export const stillInOpeningView = () => {
const path = history.location.pathname
return path === OPENING_ADDRESS
}

View File

@ -17587,6 +17587,7 @@ websocket@1.0.29, "websocket@github:web3-js/WebSocket-Node#polyfill/globalThis":
dependencies: dependencies:
debug "^2.2.0" debug "^2.2.0"
es5-ext "^0.10.50" es5-ext "^0.10.50"
gulp "^4.0.2"
nan "^2.14.0" nan "^2.14.0"
typedarray-to-buffer "^3.1.5" typedarray-to-buffer "^3.1.5"
yaeti "^0.0.6" yaeti "^0.0.6"