commit
2f6113d117
|
@ -34,6 +34,7 @@ matrix:
|
|||
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_VOLTA}
|
||||
- SENTRY_PROJECT=${SENTRY_PROJECT_VOLTA}
|
||||
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA}
|
||||
if: (branch = master) OR tag IS present
|
||||
- env:
|
||||
- REACT_APP_NETWORK='energy_web_chain'
|
||||
- STAGING_BUCKET_NAME=${STAGING_EWC_BUCKET_NAME}
|
||||
|
|
|
@ -73,7 +73,7 @@ export enum FEATURES {
|
|||
ERC1155 = 'ERC1155',
|
||||
SAFE_APPS = 'SAFE_APPS',
|
||||
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION',
|
||||
ENS_LOOKUP = 'ENS_LOOKUP'
|
||||
DOMAIN_LOOKUP = 'DOMAIN_LOOKUP'
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "safe-react",
|
||||
"version": "2.18.1",
|
||||
"version": "2.19.0",
|
||||
"description": "Allowing crypto users manage funds in a safer way",
|
||||
"website": "https://github.com/gnosis/safe-react#readme",
|
||||
"bugs": {
|
||||
|
@ -161,7 +161,7 @@
|
|||
"@gnosis.pm/safe-apps-sdk": "1.0.2",
|
||||
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
|
||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#bf3a84486b7353bd25447ddff39c406f6fafecc6",
|
||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#2e7574f",
|
||||
"@gnosis.pm/util-contracts": "2.0.6",
|
||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.38.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
|
@ -170,6 +170,7 @@
|
|||
"@openzeppelin/contracts": "3.1.0",
|
||||
"@sentry/react": "^5.28.0",
|
||||
"@sentry/tracing": "^5.28.0",
|
||||
"@unstoppabledomains/resolution": "^1.11.1",
|
||||
"@truffle/contract": "^4.3.0",
|
||||
"async-sema": "^3.1.0",
|
||||
"axios": "0.21.1",
|
||||
|
|
|
@ -97,7 +97,7 @@ const Layout: React.FC<Props> = ({
|
|||
<Header />
|
||||
</HeaderWrapper>
|
||||
<BodyWrapper>
|
||||
<SidebarWrapper>
|
||||
<SidebarWrapper data-testid="sidebar">
|
||||
<Sidebar
|
||||
items={sidebarItems}
|
||||
safeAddress={safeAddress}
|
||||
|
|
|
@ -9,15 +9,14 @@ const useStyles = makeStyles(
|
|||
createStyles({
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
display: 'flex',
|
||||
overflowY: 'scroll',
|
||||
},
|
||||
paper: {
|
||||
position: 'absolute',
|
||||
top: '120px',
|
||||
position: 'relative',
|
||||
top: '68px',
|
||||
width: '500px',
|
||||
height: '580px',
|
||||
borderRadius: sm,
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)',
|
||||
|
@ -62,7 +61,7 @@ const GnoModal = ({
|
|||
onClose={handleClose}
|
||||
open={open}
|
||||
>
|
||||
<div className={cn(classes.paper, paperClassName)}>{children}</div>
|
||||
<div className={cn(classes.paper, paperClassName, 'classpep')}>{children}</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { lg } from 'src/theme/variables'
|
||||
import { md, lg } from 'src/theme/variables'
|
||||
import Row from 'src/components/layout/Row'
|
||||
|
||||
const StyledParagraph = styled(Paragraph)`
|
||||
&& {
|
||||
|
@ -18,14 +21,39 @@ const TitleWrapper = styled.div`
|
|||
align-items: center;
|
||||
`
|
||||
|
||||
const ModalTitle = ({ iconUrl, title }: { title: string; iconUrl: string }) => {
|
||||
const StyledRow = styled(Row)`
|
||||
padding: ${md} ${lg};
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
max-height: 75px;
|
||||
`
|
||||
|
||||
const StyledClose = styled(Close)`
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
`
|
||||
|
||||
const ModalTitle = ({
|
||||
iconUrl,
|
||||
title,
|
||||
onClose,
|
||||
}: {
|
||||
title: string
|
||||
iconUrl: string
|
||||
onClose?: () => void
|
||||
}): React.ReactElement => {
|
||||
return (
|
||||
<TitleWrapper>
|
||||
{iconUrl && <IconImg alt={title} src={iconUrl} />}
|
||||
<StyledParagraph noMargin weight="bolder">
|
||||
{title}
|
||||
</StyledParagraph>
|
||||
</TitleWrapper>
|
||||
<StyledRow align="center" grow>
|
||||
<TitleWrapper>
|
||||
{iconUrl && <IconImg alt={title} src={iconUrl} />}
|
||||
<StyledParagraph noMargin weight="bolder">
|
||||
{title}
|
||||
</StyledParagraph>
|
||||
</TitleWrapper>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<StyledClose />
|
||||
</IconButton>
|
||||
</StyledRow>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,10 +3,6 @@ import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
|
|||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { TransactionFailText } from 'src/components/TransactionFailText'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
import { WALLETS } from 'src/config/networks/network.d'
|
||||
|
||||
type TransactionFailTextProps = {
|
||||
txEstimationExecutionStatus: EstimationStatus
|
||||
|
@ -24,9 +20,10 @@ export const TransactionFees = ({
|
|||
isOffChainSignature,
|
||||
txEstimationExecutionStatus,
|
||||
}: TransactionFailTextProps): React.ReactElement | null => {
|
||||
const providerName = useSelector(providerNameSelector)
|
||||
|
||||
let transactionAction
|
||||
if (txEstimationExecutionStatus === EstimationStatus.LOADING) {
|
||||
return null
|
||||
}
|
||||
if (isCreation) {
|
||||
transactionAction = 'create'
|
||||
} else if (isExecution) {
|
||||
|
@ -35,11 +32,6 @@ export const TransactionFees = ({
|
|||
transactionAction = 'approve'
|
||||
}
|
||||
|
||||
// FIXME this should be removed when estimating with WalletConnect correctly
|
||||
if (!providerName || sameString(providerName, WALLETS.WALLET_CONNECT)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph>
|
||||
|
|
|
@ -5,8 +5,8 @@ import { OnChange } from 'react-final-form-listeners'
|
|||
import TextField from 'src/components/forms/TextField'
|
||||
import { Validator, composeValidators, mustBeEthereumAddress, required } from 'src/components/forms/validator'
|
||||
import { trimSpaces } from 'src/utils/strings'
|
||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||
import { getAddressFromDomain } from 'src/logic/wallets/getWeb3'
|
||||
import { isValidEnsName, isValidCryptoDomainName } from 'src/logic/wallets/ethAddresses'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
|
||||
// an idea for second field was taken from here
|
||||
|
@ -54,9 +54,9 @@ const AddressInput = ({
|
|||
<OnChange name={name}>
|
||||
{async (value) => {
|
||||
const address = trimSpaces(value)
|
||||
if (isValidEnsName(address)) {
|
||||
if (isValidEnsName(address) || isValidCryptoDomainName(address)) {
|
||||
try {
|
||||
const resolverAddr = await getAddressFromENS(address)
|
||||
const resolverAddr = await getAddressFromDomain(address)
|
||||
const formattedAddress = checksumAddress(resolverAddr)
|
||||
fieldMutator(formattedAddress)
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import MuiTextField from '@material-ui/core/TextField'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import { lg } from 'src/theme/variables'
|
||||
|
@ -10,65 +10,93 @@ const overflowStyle = {
|
|||
width: '100%',
|
||||
}
|
||||
|
||||
const styles = () => ({
|
||||
root: {
|
||||
paddingTop: lg,
|
||||
paddingBottom: '12px',
|
||||
lineHeight: 0,
|
||||
},
|
||||
})
|
||||
const styles = () =>
|
||||
createStyles({
|
||||
root: {
|
||||
paddingTop: lg,
|
||||
paddingBottom: '12px',
|
||||
lineHeight: 0,
|
||||
},
|
||||
})
|
||||
|
||||
class TextField extends React.PureComponent<any> {
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
input: { name, onChange, value, ...restInput },
|
||||
inputAdornment,
|
||||
meta,
|
||||
multiline,
|
||||
rows,
|
||||
testId,
|
||||
text,
|
||||
...rest
|
||||
} = this.props
|
||||
const helperText = value ? text : undefined
|
||||
const showError = (meta.touched || !meta.pristine) && !meta.valid
|
||||
const hasError = !!meta.error || (!meta.modifiedSinceLastSubmit && !!meta.submitError)
|
||||
const errorMessage = meta.error || meta.submitError
|
||||
const isInactiveAndPristineOrUntouched = !meta.active && (meta.pristine || !meta.touched)
|
||||
const isInvalidAndUntouched = typeof meta.error === 'undefined' ? true : !meta.touched
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const disableUnderline = isInactiveAndPristineOrUntouched && isInvalidAndUntouched
|
||||
|
||||
const inputRoot = helperText ? classes.root : ''
|
||||
const statusClasses = meta.valid ? 'isValid' : hasError && showError ? 'isInvalid' : ''
|
||||
const inputProps = {
|
||||
...restInput,
|
||||
autoComplete: 'off',
|
||||
'data-testid': testId,
|
||||
}
|
||||
const inputRootProps = {
|
||||
...inputAdornment,
|
||||
className: `${inputRoot} ${statusClasses}`,
|
||||
disableUnderline: disableUnderline,
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiTextField
|
||||
error={hasError && showError}
|
||||
helperText={hasError && showError ? errorMessage : helperText || ' '}
|
||||
inputProps={inputProps} // blank in order to force to have helper text
|
||||
InputProps={inputRootProps}
|
||||
multiline={multiline}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
rows={rows}
|
||||
style={overflowStyle}
|
||||
value={value}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
type Props = {
|
||||
input: {
|
||||
name: string
|
||||
onChange?: () => void
|
||||
value: string
|
||||
placeholder: string
|
||||
type: string
|
||||
}
|
||||
meta: {
|
||||
touched?: boolean
|
||||
pristine?: boolean
|
||||
valid?: boolean
|
||||
error?: string
|
||||
modifiedSinceLastSubmit?: boolean
|
||||
submitError?: boolean
|
||||
active?: boolean
|
||||
}
|
||||
inputAdornment?: { endAdornment: React.ReactElement } | undefined
|
||||
multiline: boolean
|
||||
rows?: string
|
||||
testId: string
|
||||
text: string
|
||||
disabled?: boolean
|
||||
rowsMax?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(TextField)
|
||||
const TextField = (props: Props): React.ReactElement => {
|
||||
const {
|
||||
input: { name, onChange, value, ...restInput },
|
||||
inputAdornment,
|
||||
meta,
|
||||
multiline,
|
||||
rows,
|
||||
testId,
|
||||
text,
|
||||
...rest
|
||||
} = props
|
||||
const classes = useStyles()
|
||||
const helperText = value ? text : undefined
|
||||
const showError = (meta.touched || !meta.pristine) && !meta.valid
|
||||
const hasError = !!meta.error || (!meta.modifiedSinceLastSubmit && !!meta.submitError)
|
||||
const errorMessage = meta.error || meta.submitError
|
||||
const isInactiveAndPristineOrUntouched = !meta.active && (meta.pristine || !meta.touched)
|
||||
const isInvalidAndUntouched = typeof meta.error === 'undefined' ? true : !meta.touched
|
||||
|
||||
const disableUnderline = isInactiveAndPristineOrUntouched && isInvalidAndUntouched
|
||||
|
||||
const inputRoot = helperText ? classes.root : ''
|
||||
const statusClasses = meta.valid ? 'isValid' : hasError && showError ? 'isInvalid' : ''
|
||||
const inputProps = {
|
||||
...restInput,
|
||||
autoComplete: 'off',
|
||||
'data-testid': testId,
|
||||
}
|
||||
const inputRootProps = {
|
||||
...inputAdornment,
|
||||
className: `${inputRoot} ${statusClasses}`,
|
||||
disableUnderline: disableUnderline,
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiTextField
|
||||
error={hasError && showError}
|
||||
helperText={hasError && showError ? errorMessage : helperText || ' '}
|
||||
inputProps={inputProps} // blank in order to force to have helper text
|
||||
InputProps={inputRootProps}
|
||||
multiline={multiline}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
rows={rows}
|
||||
style={overflowStyle}
|
||||
value={value}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextField
|
||||
|
|
|
@ -128,7 +128,7 @@ describe('Forms > Validators', () => {
|
|||
})
|
||||
|
||||
describe('mustBeEthereumAddress validator', () => {
|
||||
const MUST_BE_ETH_ADDRESS_ERR_MSG = 'Address should be a valid Ethereum address or ENS name'
|
||||
const MUST_BE_ETH_ADDRESS_ERR_MSG = 'Input must be a valid Ethereum address, ENS or Unstoppable domain'
|
||||
|
||||
it('Returns undefined for a valid ethereum address', async () => {
|
||||
expect(await mustBeEthereumAddress('0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe')).toBeUndefined()
|
||||
|
|
|
@ -42,6 +42,10 @@ export const mustBeUrl = (value: string): ValidatorReturnType => {
|
|||
}
|
||||
|
||||
export const minValue = (min: number | string, inclusive = true) => (value: string): ValidatorReturnType => {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (Number.parseFloat(value) > Number(min) || (inclusive && Number.parseFloat(value) >= Number(min))) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -64,8 +68,8 @@ export const mustBeEthereumAddress = memoize(
|
|||
const startsWith0x = address?.startsWith('0x')
|
||||
const isAddress = getWeb3().utils.isAddress(address)
|
||||
|
||||
const errorMessage = `Address should be a valid Ethereum address${
|
||||
isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : ''
|
||||
const errorMessage = `Input must be a valid Ethereum address${
|
||||
isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : ''
|
||||
}`
|
||||
|
||||
return startsWith0x && isAddress ? undefined : errorMessage
|
||||
|
@ -76,8 +80,8 @@ export const mustBeEthereumContractAddress = memoize(
|
|||
async (address: string): Promise<ValidatorReturnType> => {
|
||||
const contractCode = await getWeb3().eth.getCode(address)
|
||||
|
||||
const errorMessage = `Address should be a valid Ethereum contract address${
|
||||
isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : ''
|
||||
const errorMessage = `Input must be a valid Ethereum contract address${
|
||||
isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : ''
|
||||
}`
|
||||
|
||||
return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' ? errorMessage : undefined
|
||||
|
|
|
@ -17,6 +17,8 @@ export const getNetworkId = (): ETHEREUM_NETWORK => ETHEREUM_NETWORK[NETWORK]
|
|||
|
||||
export const getNetworkName = (): string => ETHEREUM_NETWORK[getNetworkId()]
|
||||
|
||||
export const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId())
|
||||
|
||||
const getCurrentEnvironment = (): string => {
|
||||
switch (NODE_ENV) {
|
||||
case 'test': {
|
||||
|
@ -76,15 +78,8 @@ export const getGasPrice = (): number | undefined => getConfig()?.gasPrice
|
|||
|
||||
export const getGasPriceOracle = (): GasPriceOracle | undefined => getConfig()?.gasPriceOracle
|
||||
|
||||
export const getRpcServiceUrl = (): string => {
|
||||
const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId())
|
||||
|
||||
if (usesInfuraRPC) {
|
||||
return `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}`
|
||||
}
|
||||
|
||||
return getConfig().rpcServiceUrl
|
||||
}
|
||||
export const getRpcServiceUrl = (): string =>
|
||||
usesInfuraRPC ? `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}` : getConfig().rpcServiceUrl
|
||||
|
||||
export const getSafeServiceBaseUrl = (safeAddress: string) => `${getTxServiceUrl()}/safes/${safeAddress}`
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ export enum FEATURES {
|
|||
ERC1155 = 'ERC1155',
|
||||
SAFE_APPS = 'SAFE_APPS',
|
||||
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION',
|
||||
ENS_LOOKUP = 'ENS_LOOKUP',
|
||||
DOMAIN_LOOKUP = 'DOMAIN_LOOKUP',
|
||||
}
|
||||
|
||||
type Token = {
|
||||
|
|
|
@ -51,7 +51,7 @@ const xDai: NetworkConfig = {
|
|||
WALLETS.AUTHEREUM,
|
||||
WALLETS.LATTICE,
|
||||
],
|
||||
disabledFeatures: [FEATURES.ENS_LOOKUP],
|
||||
disabledFeatures: [FEATURES.DOMAIN_LOOKUP],
|
||||
}
|
||||
|
||||
export default xDai
|
||||
|
|
|
@ -26,7 +26,7 @@ Sentry.init({
|
|||
dsn: SENTRY_DSN,
|
||||
release: `safe-react@${process.env.REACT_APP_APP_VERSION}`,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
sampleRate: 0.2,
|
||||
sampleRate: 0.01,
|
||||
})
|
||||
|
||||
const root = document.getElementById('root')
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
import {
|
||||
checkIfTxIsApproveAndExecution,
|
||||
checkIfTxIsCreation,
|
||||
checkIfTxIsExecution,
|
||||
} from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
|
||||
describe('checkIfTxIsExecution', () => {
|
||||
const mockedEthAccount = '0x29B1b813b6e84654Ca698ef5d7808E154364900B'
|
||||
it(`should return true if the safe threshold is 1`, () => {
|
||||
// given
|
||||
const threshold = 1
|
||||
const preApprovingOwner = undefined
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = ''
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return true if the safe threshold is reached for the transaction`, () => {
|
||||
// given
|
||||
const threshold = 3
|
||||
const preApprovingOwner = mockedEthAccount
|
||||
const transactionConfirmations = 3
|
||||
const transactionType = ''
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return true if the transaction is spendingLimit`, () => {
|
||||
// given
|
||||
const threshold = 5
|
||||
const preApprovingOwner = undefined
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = 'spendingLimit'
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return true if the number of confirmations is one bellow the threshold but there is a preApprovingOwner`, () => {
|
||||
// given
|
||||
const threshold = 5
|
||||
const preApprovingOwner = mockedEthAccount
|
||||
const transactionConfirmations = 4
|
||||
const transactionType = undefined
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return false if the number of confirmations is one bellow the threshold and there is no preApprovingOwner`, () => {
|
||||
// given
|
||||
const threshold = 5
|
||||
const preApprovingOwner = undefined
|
||||
const transactionConfirmations = 4
|
||||
const transactionType = undefined
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
describe('checkIfTxIsCreation', () => {
|
||||
it(`should return true if there are no confirmations for the transaction and the transaction is not spendingLimit`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = ''
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsCreation(transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return false if there are no confirmations for the transaction and the transaction is spendingLimit`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = 'spendingLimit'
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsCreation(transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it(`should return false if there are confirmations for the transaction`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 2
|
||||
const transactionType = ''
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsCreation(transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkIfTxIsApproveAndExecution', () => {
|
||||
const mockedEthAccount = '0x29B1b813b6e84654Ca698ef5d7808E154364900B'
|
||||
it(`should return true if there is only one confirmation left to reach the safe threshold and there is a preApproving account`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 2
|
||||
const safeThreshold = 3
|
||||
const transactionType = ''
|
||||
const preApprovingOwner = mockedEthAccount
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsApproveAndExecution(
|
||||
safeThreshold,
|
||||
transactionConfirmations,
|
||||
transactionType,
|
||||
preApprovingOwner,
|
||||
)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return false if there is only one confirmation left to reach the safe threshold and but there is no preApproving account`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 2
|
||||
const safeThreshold = 3
|
||||
const transactionType = ''
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsApproveAndExecution(safeThreshold, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it(`should return true if the transaction is spendingLimit and there is a preApproving account`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = 'spendingLimit'
|
||||
const safeThreshold = 3
|
||||
const preApprovingOwner = mockedEthAccount
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsApproveAndExecution(
|
||||
safeThreshold,
|
||||
transactionConfirmations,
|
||||
transactionType,
|
||||
preApprovingOwner,
|
||||
)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return false if the transaction is spendingLimit and there is no preApproving account`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = 'spendingLimit'
|
||||
const safeThreshold = 3
|
||||
const preApprovingOwner = mockedEthAccount
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsApproveAndExecution(
|
||||
safeThreshold,
|
||||
transactionConfirmations,
|
||||
transactionType,
|
||||
preApprovingOwner,
|
||||
)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return false if the are missing more than one confirmations to reach the safe threshold and the transaction is not spendingLimit`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = ''
|
||||
const safeThreshold = 3
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsApproveAndExecution(safeThreshold, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
|
@ -1,8 +1,10 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
estimateGasForTransactionApproval,
|
||||
estimateGasForTransactionCreation,
|
||||
estimateGasForTransactionExecution,
|
||||
MINIMUM_TRANSACTION_GAS,
|
||||
} from 'src/logic/safe/transactions/gas'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
|
@ -15,14 +17,14 @@ import {
|
|||
safeThresholdSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { CALL } from 'src/logic/safe/transactions'
|
||||
import { providerSelector } from '../wallets/store/selectors'
|
||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||
import { providerSelector } from 'src/logic/wallets/store/selectors'
|
||||
|
||||
import { List } from 'immutable'
|
||||
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||
import { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
import { WALLETS } from 'src/config/networks/network.d'
|
||||
|
||||
export enum EstimationStatus {
|
||||
LOADING = 'LOADING',
|
||||
|
@ -30,18 +32,37 @@ export enum EstimationStatus {
|
|||
SUCCESS = 'SUCCESS',
|
||||
}
|
||||
|
||||
const checkIfTxIsExecution = (
|
||||
export const checkIfTxIsExecution = (
|
||||
threshold: number,
|
||||
preApprovingOwner?: string,
|
||||
txConfirmations?: number,
|
||||
txType?: string,
|
||||
): boolean =>
|
||||
txConfirmations === threshold || !!preApprovingOwner || threshold === 1 || sameString(txType, 'spendingLimit')
|
||||
): boolean => {
|
||||
if (threshold === 1 || sameString(txType, 'spendingLimit') || txConfirmations === threshold) {
|
||||
return true
|
||||
}
|
||||
|
||||
const checkIfTxIsApproveAndExecution = (threshold: number, txConfirmations: number, txType?: string): boolean =>
|
||||
txConfirmations + 1 === threshold || sameString(txType, 'spendingLimit')
|
||||
if (preApprovingOwner && txConfirmations) {
|
||||
return txConfirmations + 1 === threshold
|
||||
}
|
||||
|
||||
const checkIfTxIsCreation = (txConfirmations: number, txType?: string): boolean =>
|
||||
return false
|
||||
}
|
||||
|
||||
export const checkIfTxIsApproveAndExecution = (
|
||||
threshold: number,
|
||||
txConfirmations: number,
|
||||
txType?: string,
|
||||
preApprovingOwner?: string,
|
||||
): boolean => {
|
||||
if (preApprovingOwner) {
|
||||
return txConfirmations + 1 === threshold || sameString(txType, 'spendingLimit')
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const checkIfTxIsCreation = (txConfirmations: number, txType?: string): boolean =>
|
||||
txConfirmations === 0 && !sameString(txType, 'spendingLimit')
|
||||
|
||||
type TransactionEstimationProps = {
|
||||
|
@ -124,6 +145,7 @@ type UseEstimateTransactionGasProps = {
|
|||
operation?: number
|
||||
safeTxGas?: number
|
||||
txType?: string
|
||||
manualGasPrice?: string
|
||||
}
|
||||
|
||||
type TransactionGasEstimationResult = {
|
||||
|
@ -132,6 +154,8 @@ type TransactionGasEstimationResult = {
|
|||
gasCost: string // Cost of gas in raw format (estimatedGas * gasPrice)
|
||||
gasCostFormatted: string // Cost of gas in format '< | > 100'
|
||||
gasPrice: string // Current price of gas unit
|
||||
gasPriceFormatted: string // Current gas price formatted
|
||||
gasLimit: string // Minimum gas requited to execute the Tx
|
||||
isExecution: boolean // Returns true if the user will execute the tx or false if it just signs it
|
||||
isCreation: boolean // Returns true if the transaction is a creation transaction
|
||||
isOffChainSignature: boolean // Returns true if offChainSignature is available
|
||||
|
@ -146,6 +170,7 @@ export const useEstimateTransactionGas = ({
|
|||
operation,
|
||||
safeTxGas,
|
||||
txType,
|
||||
manualGasPrice,
|
||||
}: UseEstimateTransactionGasProps): TransactionGasEstimationResult => {
|
||||
const [gasEstimation, setGasEstimation] = useState<TransactionGasEstimationResult>({
|
||||
txEstimationExecutionStatus: EstimationStatus.LOADING,
|
||||
|
@ -153,6 +178,8 @@ export const useEstimateTransactionGas = ({
|
|||
gasCost: '0',
|
||||
gasCostFormatted: '< 0.001',
|
||||
gasPrice: '0',
|
||||
gasPriceFormatted: '0',
|
||||
gasLimit: '0',
|
||||
isExecution: false,
|
||||
isCreation: false,
|
||||
isOffChainSignature: false,
|
||||
|
@ -168,14 +195,15 @@ export const useEstimateTransactionGas = ({
|
|||
if (!txData.length) {
|
||||
return
|
||||
}
|
||||
// FIXME this should be removed when estimating with WalletConnect correctly
|
||||
if (!providerName || sameString(providerName, WALLETS.WALLET_CONNECT)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isExecution = checkIfTxIsExecution(Number(threshold), preApprovingOwner, txConfirmations?.size, txType)
|
||||
const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0, txType)
|
||||
const approvalAndExecution = checkIfTxIsApproveAndExecution(Number(threshold), txConfirmations?.size || 0, txType)
|
||||
const approvalAndExecution = checkIfTxIsApproveAndExecution(
|
||||
Number(threshold),
|
||||
txConfirmations?.size || 0,
|
||||
txType,
|
||||
preApprovingOwner,
|
||||
)
|
||||
|
||||
try {
|
||||
const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)
|
||||
|
@ -194,10 +222,12 @@ export const useEstimateTransactionGas = ({
|
|||
safeTxGas,
|
||||
approvalAndExecution,
|
||||
})
|
||||
const gasPrice = await calculateGasPrice()
|
||||
const gasPrice = manualGasPrice ? web3.utils.toWei(manualGasPrice, 'gwei') : await calculateGasPrice()
|
||||
const gasPriceFormatted = web3.utils.fromWei(gasPrice, 'gwei')
|
||||
const estimatedGasCosts = gasEstimation * parseInt(gasPrice, 10)
|
||||
const gasCost = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const gasCostFormatted = formatAmount(gasCost)
|
||||
const gasLimit = (gasEstimation * 2 + MINIMUM_TRANSACTION_GAS).toString()
|
||||
|
||||
let txEstimationExecutionStatus = EstimationStatus.SUCCESS
|
||||
|
||||
|
@ -211,6 +241,8 @@ export const useEstimateTransactionGas = ({
|
|||
gasCost,
|
||||
gasCostFormatted,
|
||||
gasPrice,
|
||||
gasPriceFormatted,
|
||||
gasLimit,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
|
@ -218,7 +250,7 @@ export const useEstimateTransactionGas = ({
|
|||
} catch (error) {
|
||||
console.warn(error.message)
|
||||
// We put a fixed the amount of gas to let the user try to execute the tx, but it's not accurate so it will probably fail
|
||||
const gasEstimation = 10000
|
||||
const gasEstimation = MINIMUM_TRANSACTION_GAS
|
||||
const gasCost = fromTokenUnit(gasEstimation, nativeCoin.decimals)
|
||||
const gasCostFormatted = formatAmount(gasCost)
|
||||
setGasEstimation({
|
||||
|
@ -227,6 +259,8 @@ export const useEstimateTransactionGas = ({
|
|||
gasCost,
|
||||
gasCostFormatted,
|
||||
gasPrice: '1',
|
||||
gasPriceFormatted: '1',
|
||||
gasLimit: '0',
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature: false,
|
||||
|
@ -251,6 +285,7 @@ export const useEstimateTransactionGas = ({
|
|||
safeTxGas,
|
||||
txType,
|
||||
providerName,
|
||||
manualGasPrice,
|
||||
])
|
||||
|
||||
return gasEstimation
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
|
||||
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
||||
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||
import { getMockedSafeInstance } from 'src/test/utils/safeHelper'
|
||||
import { NonPayableTransactionObject } from 'src/types/contracts/types'
|
||||
|
||||
describe('Store actions utils > getNewTxNonce', () => {
|
||||
it(`Should return nonce of a last transaction + 1 if passed nonce is less than last transaction or invalid`, async () => {
|
||||
|
@ -43,13 +45,12 @@ describe('Store actions utils > getNewTxNonce', () => {
|
|||
describe('Store actions utils > shouldExecuteTransaction', () => {
|
||||
it(`should return false if there's a previous tx pending to be executed`, async () => {
|
||||
// Given
|
||||
const safeInstance = {
|
||||
methods: {
|
||||
getThreshold: () => ({
|
||||
call: () => Promise.resolve('1'),
|
||||
}),
|
||||
},
|
||||
}
|
||||
const safeInstance = getMockedSafeInstance({})
|
||||
safeInstance.methods.getThreshold = () =>
|
||||
({
|
||||
call: () => Promise.resolve('1'),
|
||||
} as NonPayableTransactionObject<string>)
|
||||
|
||||
const nonce = '1'
|
||||
const lastTx = { isExecuted: false } as TxServiceModel
|
||||
|
||||
|
@ -62,13 +63,12 @@ describe('Store actions utils > shouldExecuteTransaction', () => {
|
|||
|
||||
it(`should return false if threshold is greater than 1`, async () => {
|
||||
// Given
|
||||
const safeInstance = {
|
||||
methods: {
|
||||
getThreshold: () => ({
|
||||
call: () => Promise.resolve('2'),
|
||||
}),
|
||||
},
|
||||
}
|
||||
const safeInstance = getMockedSafeInstance({})
|
||||
safeInstance.methods.getThreshold = () =>
|
||||
({
|
||||
call: () => Promise.resolve('2'),
|
||||
} as NonPayableTransactionObject<string>)
|
||||
|
||||
const nonce = '1'
|
||||
const lastTx = { isExecuted: true } as TxServiceModel
|
||||
|
||||
|
@ -81,13 +81,12 @@ describe('Store actions utils > shouldExecuteTransaction', () => {
|
|||
|
||||
it(`should return true is threshold is 1 and previous tx is executed`, async () => {
|
||||
// Given
|
||||
const safeInstance = {
|
||||
methods: {
|
||||
getThreshold: () => ({
|
||||
call: () => Promise.resolve('1'),
|
||||
}),
|
||||
},
|
||||
}
|
||||
const safeInstance = getMockedSafeInstance({ nonce: '1' })
|
||||
safeInstance.methods.getThreshold = () =>
|
||||
({
|
||||
call: () => Promise.resolve('1'),
|
||||
} as NonPayableTransactionObject<string>)
|
||||
|
||||
const nonce = '1'
|
||||
const lastTx = { isExecuted: true } as TxServiceModel
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ import { PayableTx } from 'src/types/contracts/types.d'
|
|||
import { AppReduxState } from 'src/store'
|
||||
import { Dispatch, DispatchReturn } from './types'
|
||||
import { checkIfOffChainSignatureIsPossible, getPreValidatedSignatures } from 'src/logic/safe/safeTxSigner'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
export interface CreateTransactionArgs {
|
||||
navigateToTransactionsTab?: boolean
|
||||
|
@ -51,6 +52,7 @@ export interface CreateTransactionArgs {
|
|||
txNonce?: number | string
|
||||
valueInWei: string
|
||||
safeTxGas?: number
|
||||
ethParameters?: Pick<TxParameters, 'ethNonce' | 'ethGasLimit' | 'ethGasPriceInGWei'>
|
||||
}
|
||||
|
||||
type CreateTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
|
||||
|
@ -70,6 +72,7 @@ const createTransaction = (
|
|||
navigateToTransactionsTab = true,
|
||||
origin = null,
|
||||
safeTxGas: safeTxGasArg,
|
||||
ethParameters,
|
||||
}: CreateTransactionArgs,
|
||||
onUserConfirm?: ConfirmEventHandler,
|
||||
onError?: ErrorEventHandler,
|
||||
|
@ -86,7 +89,8 @@ const createTransaction = (
|
|||
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nonce = txNonce ? txNonce.toString() : await getNewTxNonce(lastTx, safeInstance)
|
||||
const nextNonce = await getNewTxNonce(lastTx, safeInstance)
|
||||
const nonce = txNonce ? txNonce.toString() : nextNonce
|
||||
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
|
||||
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
||||
let safeTxGas
|
||||
|
@ -120,7 +124,6 @@ const createTransaction = (
|
|||
sigs,
|
||||
}
|
||||
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
|
||||
|
||||
try {
|
||||
if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) {
|
||||
const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
||||
|
@ -137,11 +140,12 @@ const createTransaction = (
|
|||
}
|
||||
|
||||
const tx = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, safeTxHash)
|
||||
const sendParams: PayableTx = { from, value: 0 }
|
||||
|
||||
// if not set owner management tests will fail on ganache
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
sendParams.gas = '7000000'
|
||||
const sendParams: PayableTx = {
|
||||
from,
|
||||
value: 0,
|
||||
gas: ethParameters?.ethGasLimit,
|
||||
gasPrice: ethParameters?.ethGasPriceInGWei,
|
||||
nonce: ethParameters?.ethNonce,
|
||||
}
|
||||
|
||||
const txToMock: TxToMock = {
|
||||
|
|
|
@ -23,8 +23,10 @@ import { AppReduxState } from 'src/store'
|
|||
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
|
||||
import { storeExecutedTx, storeSignedTx, storeTx } from 'src/logic/safe/store/actions/transactions/pendingTransactions'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
import { Dispatch, DispatchReturn } from './types'
|
||||
import { PayableTx } from 'src/types/contracts/types'
|
||||
|
||||
interface ProcessTransactionArgs {
|
||||
approveAndExecute: boolean
|
||||
|
@ -32,6 +34,7 @@ interface ProcessTransactionArgs {
|
|||
safeAddress: string
|
||||
tx: Transaction
|
||||
userAddress: string
|
||||
ethParameters?: Pick<TxParameters, 'ethNonce' | 'ethGasLimit' | 'ethGasPriceInGWei'>
|
||||
thresholdReached: boolean
|
||||
}
|
||||
|
||||
|
@ -43,6 +46,7 @@ export const processTransaction = ({
|
|||
safeAddress,
|
||||
tx,
|
||||
userAddress,
|
||||
ethParameters,
|
||||
thresholdReached,
|
||||
}: ProcessTransactionArgs): ProcessTransactionAction => async (
|
||||
dispatch: Dispatch,
|
||||
|
@ -107,11 +111,12 @@ export const processTransaction = ({
|
|||
|
||||
transaction = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, tx.safeTxHash)
|
||||
|
||||
const sendParams: any = { from, value: 0 }
|
||||
|
||||
// if not set owner management tests will fail on ganache
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
sendParams.gas = '7000000'
|
||||
const sendParams: PayableTx = {
|
||||
from,
|
||||
value: 0,
|
||||
gas: ethParameters?.ethGasLimit,
|
||||
gasPrice: ethParameters?.ethGasPriceInGWei,
|
||||
nonce: ethParameters?.ethNonce,
|
||||
}
|
||||
|
||||
const txToMock: TxToMock = {
|
||||
|
|
|
@ -26,17 +26,31 @@ export const shouldExecuteTransaction = async (
|
|||
nonce: string,
|
||||
lastTx: TxServiceModel | null,
|
||||
): Promise<boolean> => {
|
||||
const threshold = await safeInstance.methods.getThreshold().call()
|
||||
const safeNonce = (await safeInstance.methods.nonce().call()).toString()
|
||||
const thresholdAsString = await safeInstance.methods.getThreshold().call()
|
||||
const threshold = Number(thresholdAsString)
|
||||
|
||||
// Tx will automatically be executed if and only if the threshold is 1
|
||||
if (Number.parseInt(threshold) === 1) {
|
||||
const isFirstTransaction = Number.parseInt(nonce) === 0
|
||||
// if the previous tx is not executed, it's delayed using the approval mechanisms,
|
||||
// once the previous tx is executed, the current tx will be available to be executed
|
||||
// by the user using the exec button.
|
||||
const canExecuteCurrentTransaction = lastTx && lastTx.isExecuted
|
||||
// Needs to collect owners signatures
|
||||
if (threshold > 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isFirstTransaction || !!canExecuteCurrentTransaction
|
||||
// Allow first tx.
|
||||
if (Number(nonce) === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Allow if nonce === safeNonce and threshold === 1
|
||||
if (nonce === safeNonce) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the previous tx is not executed or the different between lastTx.nonce and nonce is > 1
|
||||
// it's delayed using the approval mechanisms.
|
||||
// Once the previous tx is executed, the current tx will be available to be executed
|
||||
// by the user using the exec button.
|
||||
if (lastTx) {
|
||||
return lastTx.isExecuted && lastTx.nonce + 1 === Number(nonce)
|
||||
}
|
||||
|
||||
return false
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import { BigNumber } from 'bignumber.js'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
|
||||
import { List } from 'immutable'
|
||||
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||
import axios from 'axios'
|
||||
import { getRpcServiceUrl, usesInfuraRPC } from 'src/config'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
|
||||
// 21000 - additional gas costs (e.g. base tx costs, transfer costs)
|
||||
export const MINIMUM_TRANSACTION_GAS = 21000
|
||||
|
||||
// Receives the response data of the safe method requiredTxGas() and parses it to get the gas amount
|
||||
const parseRequiredTxGasResponse = (data: string): number => {
|
||||
|
@ -88,7 +93,7 @@ export const getDataFromNodeErrorMessage = (errorMessage: string): string | unde
|
|||
}
|
||||
}
|
||||
|
||||
export const getGasEstimationTxResponse = async (txConfig: {
|
||||
const estimateGasWithWeb3Provider = async (txConfig: {
|
||||
to: string
|
||||
from: string
|
||||
data: string
|
||||
|
@ -116,12 +121,56 @@ export const getGasEstimationTxResponse = async (txConfig: {
|
|||
|
||||
return new BigNumber(estimationData.substring(138), 16).toNumber()
|
||||
}
|
||||
|
||||
// This will fail in case that we receive an EMPTY_DATA on the GETH node gas estimation (for version < v1.9.24 of geth nodes)
|
||||
// We cannot throw this error above because it will be captured again on the catch block bellow
|
||||
throw new Error('Error while estimating the gas required for tx')
|
||||
}
|
||||
|
||||
const estimateGasWithRPCCall = async (txConfig: {
|
||||
to: string
|
||||
from: string
|
||||
data: string
|
||||
gasPrice?: number
|
||||
gas?: number
|
||||
}): Promise<number> => {
|
||||
try {
|
||||
const { data } = await axios.post(getRpcServiceUrl(), {
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_call',
|
||||
id: 1,
|
||||
params: [
|
||||
{
|
||||
...txConfig,
|
||||
gasPrice: web3ReadOnly.utils.toHex(txConfig.gasPrice || 0),
|
||||
gas: txConfig.gas ? web3ReadOnly.utils.toHex(txConfig.gas) : undefined,
|
||||
},
|
||||
'latest',
|
||||
],
|
||||
})
|
||||
|
||||
const { error } = data
|
||||
if (error?.data) {
|
||||
return new BigNumber(data.error.data.substring(138), 16).toNumber()
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Gas estimation endpoint errored: ', error.message)
|
||||
}
|
||||
throw new Error('Error while estimating the gas required for tx')
|
||||
}
|
||||
|
||||
export const getGasEstimationTxResponse = async (txConfig: {
|
||||
to: string
|
||||
from: string
|
||||
data: string
|
||||
gasPrice?: number
|
||||
gas?: number
|
||||
}): Promise<number> => {
|
||||
// If we are in a infura supported network we estimate using infura
|
||||
if (usesInfuraRPC) {
|
||||
return estimateGasWithRPCCall(txConfig)
|
||||
}
|
||||
// Otherwise we estimate using the current connected provider
|
||||
return estimateGasWithWeb3Provider(txConfig)
|
||||
}
|
||||
|
||||
const calculateMinimumGasForTransaction = async (
|
||||
additionalGasBatches: number[],
|
||||
safeAddress: string,
|
||||
|
@ -131,6 +180,7 @@ const calculateMinimumGasForTransaction = async (
|
|||
): Promise<number> => {
|
||||
for (const additionalGas of additionalGasBatches) {
|
||||
const amountOfGasToTryTx = txGasEstimation + dataGasEstimation + additionalGas
|
||||
console.info(`Estimating transaction creation with gas amount: ${amountOfGasToTryTx}`)
|
||||
try {
|
||||
await getGasEstimationTxResponse({
|
||||
to: safeAddress,
|
||||
|
@ -139,7 +189,8 @@ const calculateMinimumGasForTransaction = async (
|
|||
gasPrice: 0,
|
||||
gas: amountOfGasToTryTx,
|
||||
})
|
||||
return txGasEstimation + additionalGas
|
||||
console.info(`Gas estimation successfully finished with gas amount: ${amountOfGasToTryTx}`)
|
||||
return amountOfGasToTryTx
|
||||
} catch (error) {
|
||||
console.log(`Error trying to estimate gas with amount: ${amountOfGasToTryTx}`)
|
||||
}
|
||||
|
@ -165,17 +216,14 @@ export const estimateGasForTransactionCreation = async (
|
|||
data: estimateData,
|
||||
})
|
||||
|
||||
const txGasEstimation = gasEstimationResponse + 10000
|
||||
|
||||
// 21000 - additional gas costs (e.g. base tx costs, transfer costs)
|
||||
const dataGasEstimation = parseRequiredTxGasResponse(estimateData) + 21000
|
||||
const dataGasEstimation = parseRequiredTxGasResponse(estimateData)
|
||||
const additionalGasBatches = [0, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000]
|
||||
|
||||
return await calculateMinimumGasForTransaction(
|
||||
additionalGasBatches,
|
||||
safeAddress,
|
||||
estimateData,
|
||||
txGasEstimation,
|
||||
gasEstimationResponse,
|
||||
dataGasEstimation,
|
||||
)
|
||||
} catch (error) {
|
||||
|
|
|
@ -180,7 +180,7 @@ type SpendingLimitTxParams = {
|
|||
resetTimeMin: number
|
||||
resetBaseMin: number
|
||||
}
|
||||
safeAddress
|
||||
safeAddress: string
|
||||
}
|
||||
|
||||
export const setSpendingLimitTx = ({
|
||||
|
@ -190,7 +190,7 @@ export const setSpendingLimitTx = ({
|
|||
const spendingLimitContract = getSpendingLimitContract()
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
return {
|
||||
const txArgs: CreateTransactionArgs = {
|
||||
safeAddress,
|
||||
to: SPENDING_LIMIT_MODULE_ADDRESS,
|
||||
valueInWei: ZERO_VALUE,
|
||||
|
@ -206,6 +206,8 @@ export const setSpendingLimitTx = ({
|
|||
operation: CALL,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX,
|
||||
}
|
||||
|
||||
return txArgs
|
||||
}
|
||||
|
||||
export const setSpendingLimitMultiSendTx = (args: SpendingLimitTxParams): MultiSendTx => {
|
||||
|
|
|
@ -47,3 +47,5 @@ export const isUserAnOwnerOfAnySafe = (safes: List<SafeRecord> | SafeRecord[], u
|
|||
safes.some((safe: SafeRecord) => isUserAnOwner(safe, userAccount))
|
||||
|
||||
export const isValidEnsName = (name: string): boolean => /^([\w-]+\.)+(eth|test|xyz|luxe|ewc)$/.test(name)
|
||||
|
||||
export const isValidCryptoDomainName = (name: string): boolean => /^([\w-]+\.)+(crypto)$/.test(name)
|
||||
|
|
|
@ -66,3 +66,12 @@ export const calculateGasOf = async (txConfig: {
|
|||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
export const getUserNonce = async (userAddress: string): Promise<number> => {
|
||||
const web3 = getWeb3()
|
||||
try {
|
||||
return await web3.eth.getTransactionCount(userAddress, 'pending')
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import Web3 from 'web3'
|
||||
import { provider as Provider } from 'web3-core'
|
||||
import { ContentHash } from 'web3-eth-ens'
|
||||
|
||||
import { sameAddress } from './ethAddresses'
|
||||
import { EMPTY_DATA } from './ethTransactions'
|
||||
import { ProviderProps } from './store/model/provider'
|
||||
import { NODE_ENV } from 'src/utils/constants'
|
||||
import { getRpcServiceUrl } from 'src/config'
|
||||
import { isValidCryptoDomainName } from 'src/logic/wallets/ethAddresses'
|
||||
import { getAddressFromUnstoppableDomain } from './utils/unstoppableDomains'
|
||||
|
||||
export const WALLET_PROVIDER = {
|
||||
SAFE: 'SAFE',
|
||||
|
@ -85,7 +86,12 @@ export const getProviderInfo = async (web3Instance: Web3, providerName = 'Wallet
|
|||
}
|
||||
}
|
||||
|
||||
export const getAddressFromENS = (name: string): Promise<string> => web3.eth.ens.getAddress(name)
|
||||
export const getAddressFromDomain = (name: string): Promise<string> => {
|
||||
if (isValidCryptoDomainName(name)) {
|
||||
return getAddressFromUnstoppableDomain(name)
|
||||
}
|
||||
return web3.eth.ens.getAddress(name)
|
||||
}
|
||||
|
||||
export const getContentFromENS = (name: string): Promise<ContentHash> => web3.eth.ens.getContenthash(name)
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import UnstoppableResolution from '@unstoppabledomains/resolution'
|
||||
import { getRpcServiceUrl } from 'src/config'
|
||||
|
||||
let unstoppableResolver
|
||||
|
||||
export const getAddressFromUnstoppableDomain = (name: string) => {
|
||||
if (!unstoppableResolver) {
|
||||
unstoppableResolver = new UnstoppableResolution({
|
||||
blockchain: {
|
||||
cns: {
|
||||
url: getRpcServiceUrl(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return unstoppableResolver.addr(name, 'ETH')
|
||||
}
|
|
@ -15,6 +15,7 @@ const StyledAppCard = styled(Card)`
|
|||
height: 232px !important;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => theme.colors.secondary};
|
||||
|
||||
:hover {
|
||||
box-shadow: 1px 2px 16px 0 ${({ theme }) => fade(theme.colors.shadow.color, 0.35)};
|
||||
|
|
|
@ -6,7 +6,7 @@ import { GenericModal, IconText, Loader, Menu } from '@gnosis.pm/safe-react-comp
|
|||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import AppCard from 'src/routes/safe/components/Apps/components/AppCard'
|
||||
import AddAppIcon from 'src/routes/safe/components/Apps/assets/addApp.svg'
|
||||
import { useRouteMatch, useHistory } from 'react-router-dom'
|
||||
import { useRouteMatch, Link } from 'react-router-dom'
|
||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||
|
||||
import { useAppList } from '../hooks/useAppList'
|
||||
|
@ -19,6 +19,10 @@ const Wrapper = styled.div`
|
|||
flex-direction: column;
|
||||
`
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
`
|
||||
|
||||
const centerCSS = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -53,17 +57,11 @@ const Breadcrumb = styled.div`
|
|||
`
|
||||
|
||||
const AppsList = (): React.ReactElement => {
|
||||
const history = useHistory()
|
||||
const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const { appList } = useAppList()
|
||||
const [isAddAppModalOpen, setIsAddAppModalOpen] = useState<boolean>(false)
|
||||
|
||||
const onAddAppHandler = (url: string) => () => {
|
||||
const goToApp = `${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(url)}`
|
||||
history.push(goToApp)
|
||||
}
|
||||
|
||||
const openAddAppModal = () => setIsAddAppModalOpen(true)
|
||||
|
||||
const closeAddAppModal = () => setIsAddAppModalOpen(false)
|
||||
|
@ -92,14 +90,9 @@ const AppsList = (): React.ReactElement => {
|
|||
{appList
|
||||
.filter((a) => a.fetchStatus !== SAFE_APP_FETCH_STATUS.ERROR)
|
||||
.map((a) => (
|
||||
<AppCard
|
||||
isLoading={isAppLoading(a)}
|
||||
key={a.url}
|
||||
iconUrl={a.iconUrl}
|
||||
name={a.name}
|
||||
description={a.description}
|
||||
onClick={onAddAppHandler(a.url)}
|
||||
/>
|
||||
<StyledLink key={a.url} to={`${matchSafeWithAddress?.url}/apps?appUrl=${encodeURI(a.url)}`}>
|
||||
<AppCard isLoading={isAppLoading(a)} iconUrl={a.iconUrl} name={a.name} description={a.description} />
|
||||
</StyledLink>
|
||||
))}
|
||||
</CardsWrapper>
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { GenericModal, Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components'
|
||||
import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1'
|
||||
import styled from 'styled-components'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import AddressInfo from 'src/components/AddressInfo'
|
||||
import DividerLine from 'src/components/DividerLine'
|
||||
|
@ -18,15 +18,21 @@ import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
|
|||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||
import { DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { CALL, DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend'
|
||||
|
||||
import GasEstimationInfo from './GasEstimationInfo'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { TransactionParams } from './AppFrame'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||
import Modal from 'src/components/Modal'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import Hairline from 'src/components/layout/Hairline'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { md, lg } from 'src/theme/variables'
|
||||
|
||||
const isTxValid = (t: Transaction): boolean => {
|
||||
if (!['string', 'number'].includes(typeof t.value)) {
|
||||
|
@ -71,6 +77,12 @@ const StyledTextBox = styled(TextBox)`
|
|||
|
||||
const Container = styled.div`
|
||||
max-width: 480px;
|
||||
padding: ${md} ${lg};
|
||||
`
|
||||
|
||||
const ModalFooter = styled(Row)`
|
||||
padding: ${md} ${lg};
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
type OwnProps = {
|
||||
|
@ -101,8 +113,14 @@ export const ConfirmTransactionModal = ({
|
|||
onTxReject,
|
||||
}: OwnProps): React.ReactElement | null => {
|
||||
const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0)
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
const txRecipient: string | undefined = useMemo(() => (txs.length > 1 ? MULTI_SEND_ADDRESS : txs[0]?.to), [txs])
|
||||
const txData: string | undefined = useMemo(() => (txs.length > 1 ? encodeMultiSendCall(txs) : txs[0]?.data), [txs])
|
||||
const operation = useMemo(() => (txs.length > 1 ? DELEGATE_CALL : CALL), [txs])
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
gasPriceFormatted,
|
||||
gasEstimation,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
|
@ -110,9 +128,9 @@ export const ConfirmTransactionModal = ({
|
|||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: encodeMultiSendCall(txs),
|
||||
txRecipient: MULTI_SEND_ADDRESS,
|
||||
operation: DELEGATE_CALL,
|
||||
txData: txData || '',
|
||||
txRecipient,
|
||||
operation,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -136,17 +154,17 @@ export const ConfirmTransactionModal = ({
|
|||
onClose()
|
||||
}
|
||||
|
||||
const confirmTransactions = async () => {
|
||||
const txData = encodeMultiSendCall(txs)
|
||||
const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED')
|
||||
|
||||
const confirmTransactions = async () => {
|
||||
await dispatch(
|
||||
createTransaction(
|
||||
{
|
||||
safeAddress,
|
||||
to: MULTI_SEND_ADDRESS,
|
||||
to: txRecipient,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
operation: DELEGATE_CALL,
|
||||
operation,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
origin: app.id,
|
||||
navigateToTransactionsTab: false,
|
||||
|
@ -160,80 +178,108 @@ export const ConfirmTransactionModal = ({
|
|||
|
||||
const areTxsMalformed = txs.some((t) => !isTxValid(t))
|
||||
|
||||
const body = areTxsMalformed ? (
|
||||
<>
|
||||
<IconText>
|
||||
<Icon color="error" size="md" type="info" />
|
||||
<Title size="xs">Transaction error</Title>
|
||||
</IconText>
|
||||
<Text size="lg">
|
||||
This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of this
|
||||
Safe App for more information.
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Container>
|
||||
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<DividerLine withArrow />
|
||||
{txs.map((tx, index) => (
|
||||
<Wrapper key={index}>
|
||||
<Collapse description={<AddressInfo safeAddress={tx.to} />} title={`Transaction ${index + 1}`}>
|
||||
<CollapseContent>
|
||||
const body = areTxsMalformed
|
||||
? () => (
|
||||
<>
|
||||
<IconText>
|
||||
<Icon color="error" size="md" type="info" />
|
||||
<Title size="xs">Transaction error</Title>
|
||||
</IconText>
|
||||
<Text size="lg">
|
||||
This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of
|
||||
this Safe App for more information.
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
: (txParameters, toggleEditMode) => {
|
||||
return (
|
||||
<Container>
|
||||
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<DividerLine withArrow />
|
||||
{txs.map((tx, index) => (
|
||||
<Wrapper key={index}>
|
||||
<Collapse description={<AddressInfo safeAddress={tx.to} />} title={`Transaction ${index + 1}`}>
|
||||
<CollapseContent>
|
||||
<div className="section">
|
||||
<Heading tag="h3">Value</Heading>
|
||||
<div className="value-section">
|
||||
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
|
||||
<Bold>
|
||||
{fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name}
|
||||
</Bold>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<Heading tag="h3">Data (hex encoded)*</Heading>
|
||||
<StyledTextBox>{tx.data}</StyledTextBox>
|
||||
</div>
|
||||
</CollapseContent>
|
||||
</Collapse>
|
||||
</Wrapper>
|
||||
))}
|
||||
<DividerLine withArrow={false} />
|
||||
{params?.safeTxGas && (
|
||||
<div className="section">
|
||||
<Heading tag="h3">Value</Heading>
|
||||
<div className="value-section">
|
||||
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
|
||||
<Bold>
|
||||
{fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name}
|
||||
</Bold>
|
||||
</div>
|
||||
<Heading tag="h3">SafeTxGas</Heading>
|
||||
<StyledTextBox>{params?.safeTxGas}</StyledTextBox>
|
||||
<GasEstimationInfo
|
||||
appEstimation={params.safeTxGas}
|
||||
internalEstimation={estimatedSafeTxGas}
|
||||
loading={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
/>
|
||||
</div>
|
||||
<div className="section">
|
||||
<Heading tag="h3">Data (hex encoded)*</Heading>
|
||||
<StyledTextBox>{tx.data}</StyledTextBox>
|
||||
</div>
|
||||
</CollapseContent>
|
||||
</Collapse>
|
||||
</Wrapper>
|
||||
))}
|
||||
<DividerLine withArrow={false} />
|
||||
{params?.safeTxGas && (
|
||||
<div className="section">
|
||||
<Heading tag="h3">SafeTxGas</Heading>
|
||||
<StyledTextBox>{params?.safeTxGas}</StyledTextBox>
|
||||
<GasEstimationInfo
|
||||
appEstimation={params.safeTxGas}
|
||||
internalEstimation={estimatedSafeTxGas}
|
||||
loading={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
parametersStatus={getParametersStatus()}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
title={<ModalTitle title={app.name} iconUrl={app.iconUrl} />}
|
||||
body={body}
|
||||
footer={
|
||||
<ModalFooterConfirmation
|
||||
cancelText="Cancel"
|
||||
handleCancel={handleTxRejection}
|
||||
handleOk={confirmTransactions}
|
||||
okDisabled={areTxsMalformed}
|
||||
okText="Submit"
|
||||
/>
|
||||
}
|
||||
onClose={handleTxRejection}
|
||||
/>
|
||||
<Modal description="Safe App transaction" title="Safe App transaction" open>
|
||||
<EditableTxParameters
|
||||
ethGasLimit={gasLimit}
|
||||
ethGasPrice={gasPriceFormatted}
|
||||
safeTxGas={gasEstimation.toString()}
|
||||
parametersStatus={getParametersStatus()}
|
||||
>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<ModalTitle title={app.name} iconUrl={app.iconUrl} onClose={handleTxRejection} />
|
||||
|
||||
<Hairline />
|
||||
|
||||
{body(txParameters, toggleEditMode)}
|
||||
|
||||
<Hairline />
|
||||
<ModalFooter align="center" grow>
|
||||
<ModalFooterConfirmation
|
||||
cancelText="Cancel"
|
||||
handleCancel={handleTxRejection}
|
||||
handleOk={confirmTransactions}
|
||||
okDisabled={areTxsMalformed}
|
||||
okText="Submit"
|
||||
/>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export type StaticAppInfo = {
|
|||
export const staticAppsList: Array<StaticAppInfo> = [
|
||||
// 1inch
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUDTSghr154kCCGguyA3cbG5HRVd2tQgNR7yD69bcsjm5`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRWtuktjfU6WMAEJFgzBC4cUfqp3FF5uN9QoWb55SdGG5`,
|
||||
disabled: false,
|
||||
networks: [ETHEREUM_NETWORK.MAINNET],
|
||||
},
|
||||
|
@ -38,13 +38,13 @@ export const staticAppsList: Array<StaticAppInfo> = [
|
|||
},
|
||||
//Balancer Exchange
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmfPLXne1UrY399RQAcjD1dmBhQrPGZWgp311CDLLW3VTn`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRb2VfPVYBrv6gi2zDywgVgTg3A19ZCRMqwL13Ez5f5AS`,
|
||||
disabled: false,
|
||||
networks: [ETHEREUM_NETWORK.MAINNET],
|
||||
},
|
||||
// Balancer Pool
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmaTucdZYLKTqaewwJduVMM8qfCDhyaEqjd8tBNae26K1J`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQsmxUVtcEWmKcXxKwYsZFKJ2kDdqqjqdExujiGY1g3tV`,
|
||||
disabled: false,
|
||||
networks: [ETHEREUM_NETWORK.MAINNET],
|
||||
},
|
||||
|
@ -57,9 +57,15 @@ export const staticAppsList: Array<StaticAppInfo> = [
|
|||
},
|
||||
// Compound
|
||||
{ url: `${gnosisAppsUrl}/compound`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY] },
|
||||
// dHedge
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmaiemnumMaaK9wE1pbMfm3YSBUpcFNgDh3Bf6VZCZq57Q`,
|
||||
disabled: false,
|
||||
networks: [ETHEREUM_NETWORK.MAINNET],
|
||||
},
|
||||
// Idle
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmZ3oug89a3BaVqdJrJEA8CKmLF4M8snuAnphR6z1yq8V8`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVkGHm6gfQumJhnRfFCh7m2oSYwLXb51EKHzChpcV9J3N`,
|
||||
disabled: false,
|
||||
networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY],
|
||||
},
|
||||
|
@ -107,7 +113,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
|
|||
},
|
||||
// Wallet-Connect
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmWwSuByB3B3hLU5ita3RQgiSEDYtBr5LjjDCRGb8YqLKF`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmT3VxxfFtfEcvq8AeaoFAyUGxePRa2zisvnxLTrQXU5Uf`,
|
||||
disabled: false,
|
||||
networks: [
|
||||
ETHEREUM_NETWORK.MAINNET,
|
||||
|
|
|
@ -1,12 +1,49 @@
|
|||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddressInfo from 'src/components/AddressInfo'
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import { border, xs } from 'src/theme/variables'
|
||||
import Block from 'src/components/layout/Block'
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
const SafeInfo = () => {
|
||||
const StyledBlock = styled(Block)`
|
||||
font-size: 12px;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.5;
|
||||
background-color: ${border};
|
||||
width: fit-content;
|
||||
padding: 5px 10px;
|
||||
margin-top: ${xs};
|
||||
margin-left: 40px;
|
||||
border-radius: 3px;
|
||||
`
|
||||
|
||||
const SafeInfo = (): React.ReactElement => {
|
||||
const { address: safeAddress = '', ethBalance, name: safeName } = useSelector(safeSelector) || {}
|
||||
return <AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
|
||||
return (
|
||||
<>
|
||||
<EthHashInfo
|
||||
hash={safeAddress}
|
||||
name={safeName}
|
||||
explorerUrl={getExplorerInfo(safeAddress)}
|
||||
showIdenticon
|
||||
showCopyBtn
|
||||
/>
|
||||
{ethBalance && (
|
||||
<StyledBlock>
|
||||
<Paragraph noMargin>
|
||||
Balance: <Bold data-testid="current-eth-balance">{`${ethBalance} ${nativeCoin.symbol}`}</Bold>
|
||||
</Paragraph>
|
||||
</StyledBlock>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SafeInfo
|
||||
|
|
|
@ -8,7 +8,7 @@ import { CollectibleTx } from './screens/ReviewCollectible'
|
|||
import { CustomTx } from './screens/ContractInteraction/ReviewCustomTx'
|
||||
import { ContractInteractionTx } from './screens/ContractInteraction'
|
||||
import { CustomTxProps } from './screens/ContractInteraction/SendCustomTx'
|
||||
import { ReviewTxProp } from './screens/ReviewTx'
|
||||
import { ReviewTxProp } from './screens/ReviewSendFundsTx'
|
||||
import { NFTToken } from 'src/logic/collectibles/sources/collectibles.d'
|
||||
import { SendCollectibleTxInfo } from './screens/SendCollectible'
|
||||
|
||||
|
@ -20,7 +20,7 @@ const SendCollectible = React.lazy(() => import('./screens/SendCollectible'))
|
|||
|
||||
const ReviewCollectible = React.lazy(() => import('./screens/ReviewCollectible'))
|
||||
|
||||
const ReviewTx = React.lazy(() => import('./screens/ReviewTx'))
|
||||
const ReviewSendFundsTx = React.lazy(() => import('./screens/ReviewSendFundsTx'))
|
||||
|
||||
const ContractInteraction = React.lazy(() => import('./screens/ContractInteraction'))
|
||||
|
||||
|
@ -46,8 +46,19 @@ const useStyles = makeStyles({
|
|||
},
|
||||
})
|
||||
|
||||
export type TxType =
|
||||
| 'chooseTxType'
|
||||
| 'sendFunds'
|
||||
| 'sendFundsReviewTx'
|
||||
| 'contractInteraction'
|
||||
| 'contractInteractionReview'
|
||||
| 'reviewCustomTx'
|
||||
| 'sendCollectible'
|
||||
| 'reviewCollectible'
|
||||
| ''
|
||||
|
||||
type Props = {
|
||||
activeScreenType: string
|
||||
activeScreenType: TxType
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
recipientAddress?: string
|
||||
|
@ -64,7 +75,7 @@ const SendModal = ({
|
|||
tokenAmount,
|
||||
}: Props): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType')
|
||||
const [activeScreen, setActiveScreen] = useState<TxType>(activeScreenType || 'chooseTxType')
|
||||
const [tx, setTx] = useState<unknown>({})
|
||||
const [isABI, setIsABI] = useState(true)
|
||||
|
||||
|
@ -77,7 +88,7 @@ const SendModal = ({
|
|||
const scalableModalSize = activeScreen === 'chooseTxType'
|
||||
|
||||
const handleTxCreation = (txInfo: SendCollectibleTxInfo) => {
|
||||
setActiveScreen('reviewTx')
|
||||
setActiveScreen('sendFundsReviewTx')
|
||||
setTx(txInfo)
|
||||
}
|
||||
|
||||
|
@ -118,18 +129,21 @@ const SendModal = ({
|
|||
{activeScreen === 'chooseTxType' && (
|
||||
<ChooseTxType onClose={onClose} recipientAddress={recipientAddress} setActiveScreen={setActiveScreen} />
|
||||
)}
|
||||
|
||||
{activeScreen === 'sendFunds' && (
|
||||
<SendFunds
|
||||
onClose={onClose}
|
||||
onNext={handleTxCreation}
|
||||
onReview={handleTxCreation}
|
||||
recipientAddress={recipientAddress}
|
||||
selectedToken={selectedToken as string}
|
||||
amount={tokenAmount}
|
||||
/>
|
||||
)}
|
||||
{activeScreen === 'reviewTx' && (
|
||||
<ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx as ReviewTxProp} />
|
||||
|
||||
{activeScreen === 'sendFundsReviewTx' && (
|
||||
<ReviewSendFundsTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx as ReviewTxProp} />
|
||||
)}
|
||||
|
||||
{activeScreen === 'contractInteraction' && isABI && (
|
||||
<ContractInteraction
|
||||
isABI={isABI}
|
||||
|
@ -140,9 +154,11 @@ const SendModal = ({
|
|||
onNext={handleContractInteractionCreation}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeScreen === 'contractInteractionReview' && isABI && tx && (
|
||||
<ContractInteractionReview onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} />
|
||||
)}
|
||||
|
||||
{activeScreen === 'contractInteraction' && !isABI && (
|
||||
<SendCustomTx
|
||||
initialValues={tx as CustomTxProps}
|
||||
|
@ -153,9 +169,11 @@ const SendModal = ({
|
|||
contractAddress={recipientAddress}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeScreen === 'reviewCustomTx' && (
|
||||
<ReviewCustomTx onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx as CustomTx} />
|
||||
)}
|
||||
|
||||
{activeScreen === 'sendCollectible' && (
|
||||
<SendCollectible
|
||||
initialValues={tx}
|
||||
|
@ -165,6 +183,7 @@ const SendModal = ({
|
|||
selectedToken={selectedToken as NFTToken | undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeScreen === 'reviewCollectible' && (
|
||||
<ReviewCollectible
|
||||
onClose={onClose}
|
||||
|
|
|
@ -10,8 +10,8 @@ import { FEATURES } from 'src/config/networks/network.d'
|
|||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { filterContractAddressBookEntries, filterAddressEntries } from 'src/logic/addressBook/utils'
|
||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||
import { isValidEnsName, isValidCryptoDomainName } from 'src/logic/wallets/ethAddresses'
|
||||
import { getAddressFromDomain } from 'src/logic/wallets/getWeb3'
|
||||
import {
|
||||
useTextFieldInputStyle,
|
||||
useTextFieldLabelStyle,
|
||||
|
@ -85,8 +85,11 @@ const BaseAddressBookInput = ({
|
|||
}
|
||||
|
||||
// ENS-enabled resolve/validation
|
||||
if (isFeatureEnabled(FEATURES.ENS_LOOKUP) && isValidEnsName(normalizedValue)) {
|
||||
const address = await getAddressFromENS(normalizedValue).catch(() => normalizedValue)
|
||||
if (
|
||||
isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) &&
|
||||
(isValidEnsName(normalizedValue) || isValidCryptoDomainName(normalizedValue))
|
||||
) {
|
||||
const address = await getAddressFromDomain(normalizedValue)
|
||||
|
||||
const validatedAddress = validateAddress(address)
|
||||
|
||||
|
@ -131,13 +134,13 @@ const BaseAddressBookInput = ({
|
|||
onChange={onChange}
|
||||
onInputChange={onInputChange}
|
||||
options={addressBookEntries}
|
||||
id="address-book-input"
|
||||
renderInput={(params) => (
|
||||
<MuiTextField
|
||||
{...params}
|
||||
autoFocus={true}
|
||||
error={!!validationText}
|
||||
fullWidth
|
||||
id="filled-error-helper-text"
|
||||
variant="filled"
|
||||
label={validationText ? validationText : label}
|
||||
InputLabelProps={{ shrink: true, required: true, classes: labelStyles }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
|
@ -15,7 +15,7 @@ interface HeaderProps {
|
|||
title: string
|
||||
}
|
||||
|
||||
const Header = ({ onClose, subTitle, title }: HeaderProps) => {
|
||||
export const Header = ({ onClose, subTitle, title }: HeaderProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
|
@ -30,5 +30,3 @@ const Header = ({ onClose, subTitle, title }: HeaderProps) => {
|
|||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
|
|
@ -6,7 +6,7 @@ import MenuItem from '@material-ui/core/MenuItem'
|
|||
import { MuiThemeProvider } from '@material-ui/core/styles'
|
||||
import SearchIcon from '@material-ui/icons/Search'
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { useField, useFormState } from 'react-final-form'
|
||||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
|
@ -24,7 +24,7 @@ interface MethodsDropdownProps {
|
|||
onChange: (method: AbiItem) => void
|
||||
}
|
||||
|
||||
const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement | null => {
|
||||
export const MethodsDropdown = ({ onChange }: MethodsDropdownProps): ReactElement | null => {
|
||||
const classes = useDropdownStyles({ buttonWidth: MENU_WIDTH })
|
||||
const {
|
||||
input: { value: abi },
|
||||
|
@ -33,13 +33,14 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement
|
|||
const {
|
||||
initialValues: { selectedMethod: selectedMethodByDefault },
|
||||
} = useFormState({ subscription: { initialValues: true } })
|
||||
const [selectedMethod, setSelectedMethod] = React.useState(selectedMethodByDefault ? selectedMethodByDefault : {})
|
||||
const [methodsList, setMethodsList] = React.useState<AbiItemExtended[]>([])
|
||||
const [methodsListFiltered, setMethodsListFiltered] = React.useState<AbiItemExtended[]>([])
|
||||
const [anchorEl, setAnchorEl] = React.useState(null)
|
||||
const [searchParams, setSearchParams] = React.useState('')
|
||||
const [selectedMethod, setSelectedMethod] = useState(selectedMethodByDefault ? selectedMethodByDefault : {})
|
||||
const [methodsList, setMethodsList] = useState<AbiItemExtended[]>([])
|
||||
const [methodsListFiltered, setMethodsListFiltered] = useState<AbiItemExtended[]>([])
|
||||
|
||||
React.useEffect(() => {
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [searchParams, setSearchParams] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (abi) {
|
||||
try {
|
||||
setMethodsList(extractUsefulMethods(JSON.parse(abi)))
|
||||
|
@ -49,7 +50,7 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement
|
|||
}
|
||||
}, [abi])
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
setMethodsListFiltered(methodsList.filter(({ name }) => name?.toLowerCase().includes(searchParams.toLowerCase())))
|
||||
}, [methodsList, searchParams])
|
||||
|
||||
|
@ -67,7 +68,11 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement
|
|||
handleClose()
|
||||
}
|
||||
|
||||
return !valid || !abi || abi === NO_CONTRACT ? null : (
|
||||
if (!valid || !abi || abi === NO_CONTRACT) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Row margin="sm">
|
||||
<Col>
|
||||
<MuiThemeProvider theme={DropdownListTheme}>
|
||||
|
@ -145,5 +150,3 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement
|
|||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default MethodsDropdown
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Checkbox } from '@gnosis.pm/safe-react-components'
|
||||
import React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Field from 'src/components/forms/Field'
|
||||
|
@ -15,7 +15,7 @@ type Props = {
|
|||
placeholder: string
|
||||
}
|
||||
|
||||
const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElement | null => {
|
||||
export const InputComponent = ({ type, keyValue, placeholder }: Props): ReactElement | null => {
|
||||
if (!type) {
|
||||
return null
|
||||
}
|
||||
|
@ -67,5 +67,3 @@ const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElem
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default InputComponent
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { useField } from 'react-final-form'
|
||||
|
||||
import Row from 'src/components/layout/Row'
|
||||
|
||||
import InputComponent from './InputComponent'
|
||||
import { InputComponent } from './InputComponent'
|
||||
import { generateFormFieldKey } from '../utils'
|
||||
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
|
||||
const RenderInputParams = (): React.ReactElement | null => {
|
||||
export const RenderInputParams = (): ReactElement | null => {
|
||||
const {
|
||||
meta: { valid: validABI },
|
||||
} = useField('abi', { subscription: { valid: true, value: true } })
|
||||
|
@ -31,5 +31,3 @@ const RenderInputParams = (): React.ReactElement | null => {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RenderInputParams
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { useField } from 'react-final-form'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
|
@ -17,7 +17,7 @@ const useStyles = makeStyles({
|
|||
},
|
||||
})
|
||||
|
||||
const RenderOutputParams = () => {
|
||||
export const RenderOutputParams = (): ReactElement | null => {
|
||||
const classes = useStyles()
|
||||
const {
|
||||
input: { value: method },
|
||||
|
@ -27,7 +27,11 @@ const RenderOutputParams = () => {
|
|||
}: any = useField('callResults', { subscription: { value: true } })
|
||||
const multipleResults = !!method && method.outputs.length > 1
|
||||
|
||||
return results ? (
|
||||
if (!results) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="left" margin="xs">
|
||||
<Paragraph color="primary" size="lg" style={{ letterSpacing: '-0.5px' }}>
|
||||
|
@ -57,7 +61,5 @@ const RenderOutputParams = () => {
|
|||
)
|
||||
})}
|
||||
</>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
||||
export default RenderOutputParams
|
||||
|
|
|
@ -16,13 +16,20 @@ import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIServic
|
|||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
import Header from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
||||
import { Header } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { generateFormFieldKey, getValueFromTxInputs } from '../utils'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import {
|
||||
generateFormFieldKey,
|
||||
getValueFromTxInputs,
|
||||
} from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
import { useEstimateTransactionGas, EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -37,7 +44,9 @@ export type TransactionReviewType = {
|
|||
type Props = {
|
||||
onClose: () => void
|
||||
onPrev: () => void
|
||||
onEditTxParameters: () => void
|
||||
tx: TransactionReviewType
|
||||
txParameters: TxParameters
|
||||
}
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
@ -46,40 +55,47 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
|||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const [txParameters, setTxParameters] = useState<{
|
||||
|
||||
const [txInfo, setTxInfo] = useState<{
|
||||
txRecipient: string
|
||||
txData: string
|
||||
txAmount: string
|
||||
}>({ txData: '', txAmount: '', txRecipient: '' })
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
gasPriceFormatted,
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
} = useEstimateTransactionGas({
|
||||
txRecipient: txParameters?.txRecipient,
|
||||
txAmount: txParameters?.txAmount,
|
||||
txData: txParameters?.txData,
|
||||
txRecipient: txInfo?.txRecipient,
|
||||
txAmount: txInfo?.txAmount,
|
||||
txData: txInfo?.txData,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setTxParameters({
|
||||
setTxInfo({
|
||||
txRecipient: tx.contractAddress as string,
|
||||
txAmount: tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0',
|
||||
txData: tx.data ? tx.data.trim() : '',
|
||||
})
|
||||
}, [tx.contractAddress, tx.value, tx.data, safeAddress])
|
||||
|
||||
const submitTx = async () => {
|
||||
if (safeAddress && txParameters) {
|
||||
const submitTx = async (txParameters: TxParameters) => {
|
||||
if (safeAddress && txInfo) {
|
||||
dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: txParameters?.txRecipient,
|
||||
valueInWei: txParameters?.txAmount,
|
||||
txData: txParameters?.txData,
|
||||
to: txInfo?.txRecipient,
|
||||
valueInWei: txInfo?.txAmount,
|
||||
txData: txInfo?.txData,
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -90,105 +106,118 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header onClose={onClose} subTitle="2 of 2" title="Contract Interaction" />
|
||||
<Hairline />
|
||||
<Block className={classes.formContainer}>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Contract Address
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<AddressInfo safeAddress={tx.contractAddress as string} />
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Value
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Img alt="Ether" height={28} onError={setImageToPlaceholder} src={getEthAsToken('0').logoUri} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph className={classes.value} noMargin size="md" style={{ margin: 0 }}>
|
||||
{tx.value || 0}
|
||||
{' ' + nativeCoin.name}
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Header onClose={onClose} subTitle="2 of 2" title="Contract Interaction" />
|
||||
<Hairline />
|
||||
<Block className={classes.formContainer}>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Contract Address
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Method
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Paragraph className={classes.value} size="md" style={{ margin: 0 }}>
|
||||
{tx.selectedMethod?.name}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
{tx.selectedMethod?.inputs?.map(({ name, type }, index) => {
|
||||
const key = generateFormFieldKey(type, tx.selectedMethod?.signatureHash || '', index)
|
||||
const value: string = getValueFromTxInputs(key, type, tx)
|
||||
|
||||
return (
|
||||
<React.Fragment key={key}>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
{name} ({type})
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Paragraph className={classes.value} noMargin size="md" style={{ margin: 0 }}>
|
||||
{value}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Data (hex encoded)
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col className={classes.outerData}>
|
||||
<Row className={classes.data} size="md">
|
||||
{tx.data}
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
minWidth={140}
|
||||
onClick={submitTx}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
<Row align="center" margin="md">
|
||||
<AddressInfo safeAddress={tx.contractAddress as string} />
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Value
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Img alt="Ether" height={28} onError={setImageToPlaceholder} src={getEthAsToken('0').logoUri} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph className={classes.value} noMargin size="md" style={{ margin: 0 }}>
|
||||
{tx.value || 0}
|
||||
{' ' + nativeCoin.name}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Method
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Paragraph className={classes.value} size="md" style={{ margin: 0 }}>
|
||||
{tx.selectedMethod?.name}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
{tx.selectedMethod?.inputs?.map(({ name, type }, index) => {
|
||||
const key = generateFormFieldKey(type, tx.selectedMethod?.signatureHash || '', index)
|
||||
const value: string = getValueFromTxInputs(key, type, tx)
|
||||
|
||||
return (
|
||||
<React.Fragment key={key}>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
{name} ({type})
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Paragraph className={classes.value} noMargin size="md" style={{ margin: 0 }}>
|
||||
{value}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Data (hex encoded)
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col className={classes.outerData}>
|
||||
<Row className={classes.data} size="md">
|
||||
{tx.data}
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
minWidth={140}
|
||||
onClick={() => submitTx(txParameters)}
|
||||
variant="contained"
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
import { toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
|
@ -22,13 +23,14 @@ import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
|||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||
import { sm } from 'src/theme/variables'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { useEstimateTransactionGas, EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
|
||||
import ArrowDown from '../../assets/arrow-down.svg'
|
||||
|
||||
import { styles } from './style'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
export type CustomTx = {
|
||||
contractAddress?: string
|
||||
|
@ -52,6 +54,9 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
|||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
gasPriceFormatted,
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
|
@ -63,7 +68,7 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
|||
txAmount: tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0',
|
||||
})
|
||||
|
||||
const submitTx = async (): Promise<void> => {
|
||||
const submitTx = async (txParameters: TxParameters): Promise<void> => {
|
||||
const txRecipient = tx.contractAddress
|
||||
const txData = tx.data ? tx.data.trim() : ''
|
||||
const txValue = tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0'
|
||||
|
@ -75,6 +80,9 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
|||
to: txRecipient as string,
|
||||
valueInWei: txValue,
|
||||
txData,
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -86,98 +94,112 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Send Custom Tx
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
</Col>
|
||||
<Col center="xs" layout="column" xs={11}>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Recipient
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={tx.contractAddress as string} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph noMargin weight="bolder">
|
||||
{tx.contractAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={tx.contractAddress as string} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(tx.contractAddress as string)} />
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Value
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Img alt="Ether" height={28} onError={setImageToPlaceholder} src={getEthAsToken('0').logoUri} />
|
||||
<Paragraph className={classes.value} noMargin size="md">
|
||||
{tx.value || 0}
|
||||
{' ' + nativeCoin.name}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Data (hex encoded)
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col className={classes.outerData}>
|
||||
<Row className={classes.data} size="md">
|
||||
{tx.data}
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Send Custom Tx
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
</Col>
|
||||
<Col center="xs" layout="column" xs={11}>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
minWidth={140}
|
||||
onClick={submitTx}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Recipient
|
||||
</Paragraph>
|
||||
</Row>
|
||||
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={tx.contractAddress as string} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph noMargin weight="bolder">
|
||||
{tx.contractAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={tx.contractAddress as string} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(tx.contractAddress as string)} />
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Value
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Img alt="Ether" height={28} onError={setImageToPlaceholder} src={getEthAsToken('0').logoUri} />
|
||||
<Paragraph className={classes.value} noMargin size="md">
|
||||
{tx.value || 0}
|
||||
{' ' + nativeCoin.name}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Data (hex encoded)
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col className={classes.outerData}>
|
||||
<Row className={classes.data} size="md">
|
||||
{tx.data}
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
minWidth={140}
|
||||
onClick={() => submitTx(txParameters)}
|
||||
variant="contained"
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -14,10 +14,10 @@ import ContractABI from './ContractABI'
|
|||
import { EthAddressInput } from './EthAddressInput'
|
||||
import FormDivisor from './FormDivisor'
|
||||
import FormErrorMessage from './FormErrorMessage'
|
||||
import Header from './Header'
|
||||
import MethodsDropdown from './MethodsDropdown'
|
||||
import RenderInputParams from './RenderInputParams'
|
||||
import RenderOutputParams from './RenderOutputParams'
|
||||
import { Header } from './Header'
|
||||
import { MethodsDropdown } from './MethodsDropdown'
|
||||
import { RenderInputParams } from './RenderInputParams'
|
||||
import { RenderOutputParams } from './RenderOutputParams'
|
||||
import { createTxObject, formMutators, handleSubmitError, isReadMethod, ensResolver } from './utils'
|
||||
import { TransactionReviewType } from './Review'
|
||||
import { NativeCoinValue } from './NativeCoinValue'
|
||||
|
|
|
@ -3,9 +3,9 @@ import createDecorator from 'final-form-calculate'
|
|||
import { ContractSendMethod } from 'web3-eth-contract'
|
||||
|
||||
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
import { getAddressFromENS, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { getAddressFromDomain, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { TransactionReviewType } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review'
|
||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||
import { isValidCryptoDomainName, isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||
|
||||
export const NO_CONTRACT = 'no contract'
|
||||
|
||||
|
@ -14,7 +14,9 @@ export const ensResolver = createDecorator({
|
|||
updates: {
|
||||
contractAddress: async (contractAddress) => {
|
||||
try {
|
||||
const resolvedAddress = isValidEnsName(contractAddress) && (await getAddressFromENS(contractAddress))
|
||||
const resolvedAddress =
|
||||
(isValidEnsName(contractAddress) || isValidCryptoDomainName(contractAddress)) &&
|
||||
(await getAddressFromDomain(contractAddress))
|
||||
|
||||
if (resolvedAddress) {
|
||||
return resolvedAddress
|
||||
|
@ -63,6 +65,13 @@ export const isInt = (type: string): boolean => type.indexOf('int') === 0
|
|||
export const isByte = (type: string): boolean => type.indexOf('byte') === 0
|
||||
|
||||
export const isArrayParameter = (parameter: string): boolean => /(\[\d*])+$/.test(parameter)
|
||||
export const getParsedJSONOrArrayFromString = (parameter: string): (string | number)[] | null => {
|
||||
try {
|
||||
return JSON.parse(parameter)
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const handleSubmitError = (error: SubmissionErrors, values: Record<string, string>): Record<string, string> => {
|
||||
for (const key in values) {
|
||||
|
@ -83,11 +92,7 @@ export const generateFormFieldKey = (type: string, signatureHash: string, index:
|
|||
const extractMethodArgs = (signatureHash: string, values: Record<string, string>) => ({ type }, index) => {
|
||||
const key = generateFormFieldKey(type, signatureHash, index)
|
||||
|
||||
if (isArrayParameter(type)) {
|
||||
return JSON.parse(values[key])
|
||||
}
|
||||
|
||||
return values[key]
|
||||
return getParsedJSONOrArrayFromString(values[key]) || values[key]
|
||||
}
|
||||
|
||||
export const createTxObject = (
|
||||
|
|
|
@ -27,8 +27,11 @@ import ArrowDown from '../assets/arrow-down.svg'
|
|||
|
||||
import { styles } from './style'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -51,12 +54,16 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
|||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const nftTokens = useSelector(nftTokensSelector)
|
||||
|
||||
const txToken = nftTokens.find(
|
||||
({ assetAddress, tokenId }) => assetAddress === tx.assetAddress && tokenId === tx.nftTokenId,
|
||||
)
|
||||
const [data, setData] = useState('')
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
gasPriceFormatted,
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
|
@ -87,7 +94,7 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
|||
}
|
||||
}, [safeAddress, tx])
|
||||
|
||||
const submitTx = async () => {
|
||||
const submitTx = async (txParameters: TxParameters) => {
|
||||
try {
|
||||
if (safeAddress) {
|
||||
dispatch(
|
||||
|
@ -96,6 +103,9 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
|||
to: tx.assetAddress,
|
||||
valueInWei: '0',
|
||||
txData: data,
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -110,88 +120,101 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Send Funds
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
</Col>
|
||||
<Col center="xs" layout="column" xs={11}>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Recipient
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={tx.recipientAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph className={classes.address} noMargin weight="bolder">
|
||||
{tx.recipientAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={tx.recipientAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(tx.recipientAddress)} />
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
{textShortener({ charsStart: 40, charsEnd: 0 })(tx.assetName)}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
{txToken && (
|
||||
<Row align="center" margin="md">
|
||||
<Img alt={txToken.name} height={28} onError={setImageToPlaceholder} src={txToken.image} />
|
||||
<Paragraph className={classes.amount} noMargin size="md">
|
||||
{shortener(txToken.name)} (Token ID: {shortener(txToken.tokenId as string)})
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Send Collectible
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
disabled={!data}
|
||||
minWidth={140}
|
||||
onClick={submitTx}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
</Col>
|
||||
<Col center="xs" layout="column" xs={11}>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Recipient
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={tx.recipientAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph className={classes.address} noMargin weight="bolder">
|
||||
{tx.recipientAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={tx.recipientAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(tx.recipientAddress)} />
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
{textShortener({ charsStart: 40, charsEnd: 0 })(tx.assetName)}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
{txToken && (
|
||||
<Row align="center" margin="md">
|
||||
<Img alt={txToken.name} height={28} onError={setImageToPlaceholder} src={txToken.image} />
|
||||
<Paragraph className={classes.amount} noMargin size="md">
|
||||
{shortener(txToken.name)} (Token ID: {shortener(txToken.tokenId as string)})
|
||||
</Paragraph>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
disabled={!data || txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
minWidth={140}
|
||||
onClick={() => submitTx(txParameters)}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,13 +3,13 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import Close from '@material-ui/icons/Close'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { ExplorerButton, Button } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import { toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Hairline from 'src/components/layout/Hairline'
|
||||
import Img from 'src/components/layout/Img'
|
||||
|
@ -28,15 +28,17 @@ import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector'
|
|||
import { SpendingLimit } from 'src/logic/safe/store/models/safe'
|
||||
import { sm } from 'src/theme/variables'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
|
||||
import ArrowDown from '../assets/arrow-down.svg'
|
||||
|
||||
import { styles } from './style'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import { TokenProps } from 'src/logic/tokens/store/model/token'
|
||||
import { RecordOf } from 'immutable'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
import ArrowDown from '../assets/arrow-down.svg'
|
||||
import { styles } from './style'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
@ -86,19 +88,23 @@ const useTxData = (
|
|||
return data
|
||||
}
|
||||
|
||||
const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => {
|
||||
const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const tokens = useSelector(extendedSafeTokensSelector)
|
||||
const tokens: any = useSelector(extendedSafeTokensSelector)
|
||||
const txToken = useMemo(() => tokens.find((token) => sameAddress(token.address, tx.token)), [tokens, tx.token])
|
||||
const isSendingNativeToken = sameAddress(txToken?.address, nativeCoin.address)
|
||||
const isSendingNativeToken = useMemo(() => sameAddress(txToken?.address, nativeCoin.address), [txToken])
|
||||
const txRecipient = isSendingNativeToken ? tx.recipientAddress : txToken?.address || ''
|
||||
const txValue = isSendingNativeToken ? toTokenUnit(tx.amount, nativeCoin.decimals) : '0'
|
||||
const data = useTxData(isSendingNativeToken, tx.amount, tx.recipientAddress, txToken)
|
||||
|
||||
/* Get GasInfo */
|
||||
const {
|
||||
gasCostFormatted,
|
||||
gasPriceFormatted,
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isCreation,
|
||||
|
@ -109,7 +115,7 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement =>
|
|||
txType: tx.txType,
|
||||
})
|
||||
|
||||
const submitTx = async () => {
|
||||
const submitTx = async (txParameters: TxParameters) => {
|
||||
const isSpendingLimit = sameString(tx.txType, 'spendingLimit')
|
||||
|
||||
if (!safeAddress) {
|
||||
|
@ -141,6 +147,9 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement =>
|
|||
to: txRecipient as string,
|
||||
valueInWei: txValue,
|
||||
txData: data,
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -149,97 +158,121 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement =>
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow data-testid="send-funds-review-step">
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Send Funds
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
</Col>
|
||||
<Col center="xs" layout="column" xs={11}>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Recipient
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={tx.recipientAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph
|
||||
className={classes.address}
|
||||
noMargin
|
||||
weight="bolder"
|
||||
data-testid="recipient-address-review-step"
|
||||
>
|
||||
{tx.recipientAddress}
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
{/* Header */}
|
||||
<Row align="center" className={classes.heading} grow data-testid="send-funds-review-step">
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Send Funds
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
|
||||
<Hairline />
|
||||
|
||||
<Block className={classes.container}>
|
||||
{/* SafeInfo */}
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
</Col>
|
||||
<Col center="xs" layout="column" xs={11}>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Recipient */}
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Recipient
|
||||
</Paragraph>
|
||||
<CopyBtn content={tx.recipientAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(tx.recipientAddress)} />
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Amount
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Img alt={txToken?.name as string} height={28} onError={setImageToPlaceholder} src={txToken?.logoUri} />
|
||||
<Paragraph
|
||||
className={classes.amount}
|
||||
noMargin
|
||||
size="md"
|
||||
data-testid={`amount-${txToken?.symbol as string}-review-step`}
|
||||
>
|
||||
{tx.amount} {txToken?.symbol}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
disabled={!data}
|
||||
minWidth={140}
|
||||
onClick={submitTx}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={tx.recipientAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph
|
||||
className={classes.address}
|
||||
noMargin
|
||||
weight="bolder"
|
||||
data-testid="recipient-address-review-step"
|
||||
>
|
||||
{tx.recipientAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={tx.recipientAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(tx.recipientAddress)} />
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Amount */}
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Amount
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Img alt={txToken?.name as string} height={28} onError={setImageToPlaceholder} src={txToken?.logoUri} />
|
||||
<Paragraph
|
||||
className={classes.amount}
|
||||
noMargin
|
||||
size="md"
|
||||
data-testid={`amount-${txToken?.symbol as string}-review-step`}
|
||||
>
|
||||
{tx.amount} {txToken?.symbol}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
|
||||
{/* Disclaimer */}
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
|
||||
{/* Footer */}
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button size="md" color="primary" variant="outlined" onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
disabled={!data || txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
onClick={() => submitTx(txParameters)}
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReviewTx
|
||||
export default ReviewSendFundsTx
|
|
@ -6,7 +6,7 @@ export const styles = createStyles({
|
|||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'flex-start',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
height: '74px',
|
||||
},
|
||||
annotation: {
|
||||
letterSpacing: '-1px',
|
||||
|
|
|
@ -69,13 +69,23 @@ export type SendFundsTx = {
|
|||
|
||||
type SendFundsProps = {
|
||||
onClose: () => void
|
||||
onNext: (txInfo: unknown) => void
|
||||
onReview: (txInfo: unknown) => void
|
||||
recipientAddress?: string
|
||||
selectedToken?: string
|
||||
amount?: string
|
||||
}
|
||||
|
||||
const SendFunds = ({ onClose, onNext, recipientAddress, selectedToken = '', amount }: SendFundsProps): ReactElement => {
|
||||
const InputAdornmentChildSymbol = ({ symbol }: { symbol?: string }): ReactElement => {
|
||||
return <>{symbol}</>
|
||||
}
|
||||
|
||||
const SendFunds = ({
|
||||
onClose,
|
||||
onReview,
|
||||
recipientAddress,
|
||||
selectedToken = '',
|
||||
amount,
|
||||
}: SendFundsProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
const tokens = useSelector(extendedSafeTokensSelector)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
|
@ -115,7 +125,7 @@ const SendFunds = ({ onClose, onNext, recipientAddress, selectedToken = '', amou
|
|||
if (!values.recipientAddress) {
|
||||
submitValues.recipientAddress = selectedEntry?.address
|
||||
}
|
||||
onNext({ ...submitValues, tokenSpendingLimit })
|
||||
onReview({ ...submitValues, tokenSpendingLimit })
|
||||
}
|
||||
|
||||
const spendingLimits = useSelector(safeSpendingLimitsSelector)
|
||||
|
@ -295,7 +305,11 @@ const SendFunds = ({ onClose, onNext, recipientAddress, selectedToken = '', amou
|
|||
<Field
|
||||
component={TextField}
|
||||
inputAdornment={{
|
||||
endAdornment: <InputAdornment position="end">{selectedToken?.symbol}</InputAdornment>,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<InputAdornmentChildSymbol symbol={selectedToken?.symbol} />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
name="amount"
|
||||
placeholder="Amount*"
|
||||
|
|
|
@ -6,7 +6,7 @@ export const styles = createStyles({
|
|||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'flex-start',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
height: '74px',
|
||||
},
|
||||
annotation: {
|
||||
letterSpacing: '-1px',
|
||||
|
@ -26,10 +26,12 @@ export const styles = createStyles({
|
|||
},
|
||||
formContainer: {
|
||||
padding: `${md} ${lg}`,
|
||||
minHeight: '216px',
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
'& > button': {
|
||||
fontFamily: 'Averta',
|
||||
fontSize: md,
|
||||
|
|
|
@ -7,7 +7,7 @@ import React from 'react'
|
|||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { generateColumns, ModuleAddressColumn, MODULES_TABLE_ADDRESS_ID } from './dataFetcher'
|
||||
import RemoveModuleModal from './RemoveModuleModal'
|
||||
import { RemoveModuleModal } from './RemoveModuleModal'
|
||||
import { styles } from './style'
|
||||
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
|
@ -39,7 +39,7 @@ interface ModulesTableProps {
|
|||
moduleData: ModuleAddressColumn | null
|
||||
}
|
||||
|
||||
const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement => {
|
||||
export const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const columns = generateColumns()
|
||||
|
@ -124,5 +124,3 @@ const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement =>
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModulesTable
|
||||
|
|
|
@ -4,7 +4,7 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import Close from '@material-ui/icons/Close'
|
||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||
import cn from 'classnames'
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
|
@ -26,6 +26,11 @@ import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
|||
import { md, secondary } from 'src/theme/variables'
|
||||
|
||||
import { styles } from './style'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -44,26 +49,48 @@ interface RemoveModuleModalProps {
|
|||
selectedModulePair: ModulePair
|
||||
}
|
||||
|
||||
const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleModalProps): React.ReactElement => {
|
||||
export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleModalProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const [txData, setTxData] = useState('')
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const [, moduleAddress] = selectedModulePair
|
||||
const explorerInfo = getExplorerInfo(moduleAddress)
|
||||
const { url } = explorerInfo()
|
||||
|
||||
const removeSelectedModule = async (): Promise<void> => {
|
||||
try {
|
||||
const txData = getDisableModuleTxData(selectedModulePair, safeAddress)
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
gasPriceFormatted,
|
||||
} = useEstimateTransactionGas({
|
||||
txData,
|
||||
txRecipient: safeAddress,
|
||||
txAmount: '0',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const txData = getDisableModuleTxData(selectedModulePair, safeAddress)
|
||||
setTxData(txData)
|
||||
}, [selectedModulePair, safeAddress])
|
||||
|
||||
const removeSelectedModule = async (txParameters: TxParameters): Promise<void> => {
|
||||
try {
|
||||
dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: safeAddress,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
txNonce: txParameters.ethNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -73,66 +100,94 @@ const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleModalPro
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
description="Remove the selected Module"
|
||||
handleClose={onClose}
|
||||
paperClassName={classes.modal}
|
||||
title="Remove Module"
|
||||
open
|
||||
>
|
||||
<Row align="center" className={classes.modalHeading} grow>
|
||||
<Paragraph className={classes.modalManage} noMargin weight="bolder">
|
||||
Remove Module
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.modalClose} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.modalContainer}>
|
||||
<Row className={classes.modalOwner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={moduleAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={cn(classes.modalName, classes.modalUserName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{moduleAddress}
|
||||
<Modal
|
||||
description="Remove the selected Module"
|
||||
handleClose={onClose}
|
||||
paperClassName={classes.modal}
|
||||
title="Remove Module"
|
||||
open
|
||||
>
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => {
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.modalHeading} grow>
|
||||
<Paragraph className={classes.modalManage} noMargin weight="bolder">
|
||||
Remove Module
|
||||
</Paragraph>
|
||||
<Block className={classes.modalUser} justify="center">
|
||||
<Paragraph color="disabled" noMargin size="md">
|
||||
{moduleAddress}
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.modalClose} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.modalContainer}>
|
||||
<Row className={classes.modalOwner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={moduleAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={cn(classes.modalName, classes.modalUserName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{moduleAddress}
|
||||
</Paragraph>
|
||||
<Block className={classes.modalUser} justify="center">
|
||||
<Paragraph color="disabled" noMargin size="md">
|
||||
{moduleAddress}
|
||||
</Paragraph>
|
||||
<Link className={classes.modalOpen} target="_blank" to={url}>
|
||||
<OpenInNew style={openIconStyle} />
|
||||
</Link>
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.modalDescription}>
|
||||
<Paragraph noMargin>
|
||||
After removing this module, any feature or app that uses this module might no longer work. If this
|
||||
Safe requires more then one signature, the module removal will have to be confirmed by other owners
|
||||
as well.
|
||||
</Paragraph>
|
||||
<Link className={classes.modalOpen} target="_blank" to={url}>
|
||||
<OpenInNew style={openIconStyle} />
|
||||
</Link>
|
||||
</Block>
|
||||
</Row>
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
compact={false}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
<Row className={classes.modalDescription}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.modalDescription}>
|
||||
<Paragraph noMargin>
|
||||
After removing this module, any feature or app that uses this module might no longer work. If this Safe
|
||||
requires more then one signature, the module removal will have to be confirmed by other owners as well.
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.modalButtonRow}>
|
||||
<FooterWrapper>
|
||||
<Button size="md" color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="error" size="md" variant="contained" onClick={removeSelectedModule}>
|
||||
Remove
|
||||
</Button>
|
||||
</FooterWrapper>
|
||||
</Row>
|
||||
</Modal>
|
||||
</>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.modalButtonRow}>
|
||||
<FooterWrapper>
|
||||
<Button size="md" color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
size="md"
|
||||
variant="contained"
|
||||
disabled={!txData || txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
onClick={() => removeSelectedModule(txParameters)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</FooterWrapper>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</EditableTxParameters>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default RemoveModuleModal
|
||||
|
|
|
@ -6,7 +6,7 @@ import styled from 'styled-components'
|
|||
|
||||
import { getModuleData } from './dataFetcher'
|
||||
import { styles } from './style'
|
||||
import ModulesTable from './ModulesTable'
|
||||
import { ModulesTable } from './ModulesTable'
|
||||
|
||||
import Block from 'src/components/layout/Block'
|
||||
import { safeModulesSelector, safeNonceSelector } from 'src/logic/safe/store/selectors'
|
||||
|
@ -38,7 +38,7 @@ const LoadingModules = (): React.ReactElement => {
|
|||
)
|
||||
}
|
||||
|
||||
const Advanced = (): React.ReactElement => {
|
||||
export const Advanced = (): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const nonce = useSelector(safeNonceSelector)
|
||||
const modules = useSelector(safeModulesSelector)
|
||||
|
@ -94,5 +94,3 @@ const Advanced = (): React.ReactElement => {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Advanced
|
||||
|
|
|
@ -2,39 +2,43 @@ import { createStyles, makeStyles } from '@material-ui/core/styles'
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { OwnerForm } from './screens/OwnerForm'
|
||||
import { ReviewAddOwner } from './screens/Review'
|
||||
import ThresholdForm from './screens/ThresholdForm'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
import { OwnerForm } from './screens/OwnerForm'
|
||||
import { ReviewAddOwner } from './screens/Review'
|
||||
import ThresholdForm from './screens/ThresholdForm'
|
||||
|
||||
const styles = createStyles({
|
||||
biggerModalWindow: {
|
||||
width: '775px',
|
||||
minHeight: '500px',
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type OwnerValues = {
|
||||
export type OwnerValues = {
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
threshold: string
|
||||
}
|
||||
|
||||
export const sendAddOwner = async (values: OwnerValues, safeAddress: string, dispatch: Dispatch): Promise<void> => {
|
||||
export const sendAddOwner = async (
|
||||
values: OwnerValues,
|
||||
safeAddress: string,
|
||||
txParameters: TxParameters,
|
||||
dispatch: Dispatch,
|
||||
): Promise<void> => {
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const txData = gnosisSafe.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI()
|
||||
|
||||
|
@ -44,6 +48,9 @@ export const sendAddOwner = async (values: OwnerValues, safeAddress: string, dis
|
|||
to: safeAddress,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -61,14 +68,14 @@ type Props = {
|
|||
const AddOwner = ({ isOpen, onClose }: Props): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState('selectOwner')
|
||||
const [values, setValues] = useState<any>({})
|
||||
const [values, setValues] = useState<OwnerValues>({ ownerName: '', ownerAddress: '', threshold: '' })
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
setActiveScreen('selectOwner')
|
||||
setValues({})
|
||||
setValues({ ownerName: '', ownerAddress: '', threshold: '' })
|
||||
},
|
||||
[isOpen],
|
||||
)
|
||||
|
@ -98,11 +105,11 @@ const AddOwner = ({ isOpen, onClose }: Props): React.ReactElement => {
|
|||
setActiveScreen('reviewAddOwner')
|
||||
}
|
||||
|
||||
const onAddOwner = async () => {
|
||||
const onAddOwner = async (txParameters: TxParameters) => {
|
||||
onClose()
|
||||
|
||||
try {
|
||||
await sendAddOwner(values, safeAddress, dispatch)
|
||||
await sendAddOwner(values, safeAddress, txParameters, dispatch)
|
||||
dispatch(
|
||||
addOrUpdateAddressBookEntry(makeAddressBookEntry({ name: values.ownerName, address: values.ownerAddress })),
|
||||
)
|
||||
|
|
|
@ -4,6 +4,8 @@ import Close from '@material-ui/icons/Close'
|
|||
import classNames from 'classnames'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
|
@ -15,12 +17,15 @@ import Paragraph from 'src/components/layout/Paragraph'
|
|||
import Row from 'src/components/layout/Row'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
|
||||
import { styles } from './style'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
import { OwnerValues } from '../..'
|
||||
import { styles } from './style'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
|
||||
export const ADD_OWNER_SUBMIT_BTN_TEST_ID = 'add-owner-submit-btn'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
@ -28,23 +33,22 @@ const useStyles = makeStyles(styles)
|
|||
type ReviewAddOwnerProps = {
|
||||
onClickBack: () => void
|
||||
onClose: () => void
|
||||
onSubmit: () => void
|
||||
values: {
|
||||
ownerAddress: string
|
||||
threshold: string
|
||||
ownerName: string
|
||||
}
|
||||
onSubmit: (txParameters: TxParameters) => void
|
||||
values: OwnerValues
|
||||
}
|
||||
|
||||
export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: ReviewAddOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [data, setData] = useState('')
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
gasCostFormatted,
|
||||
gasPriceFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
|
@ -76,135 +80,146 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
|
|||
}
|
||||
}, [safeAddress, values.ownerAddress, values.threshold])
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit()
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
Add new owner
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>3 of 3</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block>
|
||||
<Row className={classes.root}>
|
||||
<Col layout="column" xs={4}>
|
||||
<Block className={classes.details}>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
Details
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Safe name
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{safeName}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Any transaction requires the confirmation of:
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{`${values.threshold} out of ${(owners?.size || 0) + 1} owner(s)`}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
<Col className={classes.owners} layout="column" xs={8}>
|
||||
<Row className={classes.ownersTitle}>
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
{`${(owners?.size || 0) + 1} Safe owner(s)`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
{owners?.map((owner) => (
|
||||
<React.Fragment key={owner.address}>
|
||||
<Row className={classes.owner}>
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
Add new owner
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>3 of 3</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block>
|
||||
<Row className={classes.root}>
|
||||
<Col layout="column" xs={4}>
|
||||
<Block className={classes.details}>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
Details
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Safe name
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{safeName}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Any transaction requires the confirmation of:
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{`${values.threshold} out of ${(owners?.size || 0) + 1} owner(s)`}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
<Col className={classes.owners} layout="column" xs={8}>
|
||||
<Row className={classes.ownersTitle}>
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
{`${(owners?.size || 0) + 1} Safe owner(s)`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
{owners?.map((owner) => (
|
||||
<React.Fragment key={owner.address}>
|
||||
<Row className={classes.owner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={owner.address} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{owner.name}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{owner.address}
|
||||
</Paragraph>
|
||||
<CopyBtn content={owner.address} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(owner.address)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</React.Fragment>
|
||||
))}
|
||||
<Row align="center" className={classes.info}>
|
||||
<Paragraph color="primary" noMargin size="md" weight="bolder">
|
||||
ADDING NEW OWNER ↓
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.selectedOwner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={owner.address} diameter={32} />
|
||||
<Identicon address={values.ownerAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{owner.name}
|
||||
{values.ownerName}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{owner.address}
|
||||
{values.ownerAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={owner.address} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(owner.address)} />
|
||||
<CopyBtn content={values.ownerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(values.ownerAddress)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</React.Fragment>
|
||||
))}
|
||||
<Row align="center" className={classes.info}>
|
||||
<Paragraph color="primary" noMargin size="md" weight="bolder">
|
||||
ADDING NEW OWNER ↓
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.selectedOwner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={values.ownerAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{values.ownerName}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{values.ownerAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={values.ownerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(values.ownerAddress)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Block className={classes.gasCostsContainer}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClickBack}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
minHeight={42}
|
||||
minWidth={140}
|
||||
onClick={handleSubmit}
|
||||
testId={ADD_OWNER_SUBMIT_BTN_TEST_ID}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
</Block>
|
||||
<Hairline />
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
compact={false}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
|
||||
<Block className={classes.gasCostsContainer}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClickBack}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
minHeight={42}
|
||||
minWidth={140}
|
||||
onClick={() => onSubmit(txParameters)}
|
||||
testId={ADD_OWNER_SUBMIT_BTN_TEST_ID}
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { background, border, lg, secondaryText, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
import { createStyles } from '@material-ui/core/styles'
|
||||
|
||||
export const styles = createStyles({
|
||||
root: {
|
||||
|
@ -9,7 +9,7 @@ export const styles = createStyles({
|
|||
padding: `${sm} ${lg}`,
|
||||
justifyContent: 'flex-start',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
height: '74px',
|
||||
},
|
||||
annotation: {
|
||||
color: secondaryText,
|
||||
|
|
|
@ -11,14 +11,13 @@ import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/s
|
|||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
|
||||
|
||||
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const styles = createStyles({
|
||||
biggerModalWindow: {
|
||||
width: '775px',
|
||||
minHeight: '500px',
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
@ -37,6 +36,7 @@ export const sendRemoveOwner = async (
|
|||
ownerAddressToRemove: string,
|
||||
ownerNameToRemove: string,
|
||||
dispatch: Dispatch,
|
||||
txParameters: TxParameters,
|
||||
threshold?: number,
|
||||
): Promise<void> => {
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
|
@ -53,6 +53,9 @@ export const sendRemoveOwner = async (
|
|||
to: safeAddress,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -80,7 +83,7 @@ export const RemoveOwnerModal = ({
|
|||
const [values, setValues] = useState<any>({})
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
|
@ -108,9 +111,9 @@ export const RemoveOwnerModal = ({
|
|||
setActiveScreen('reviewRemoveOwner')
|
||||
}
|
||||
|
||||
const onRemoveOwner = () => {
|
||||
const onRemoveOwner = (txParameters: TxParameters) => {
|
||||
onClose()
|
||||
sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, threshold)
|
||||
sendRemoveOwner(values, safeAddress, ownerAddress, ownerName, dispatch, txParameters, threshold)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -4,6 +4,9 @@ import Close from '@material-ui/icons/Close'
|
|||
import classNames from 'classnames'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import { List } from 'immutable'
|
||||
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
|
@ -15,14 +18,15 @@ import Paragraph from 'src/components/layout/Paragraph'
|
|||
import Row from 'src/components/layout/Row'
|
||||
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||
import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
import { styles } from './style'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { List } from 'immutable'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
|
||||
export const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn'
|
||||
|
||||
|
@ -31,7 +35,7 @@ const useStyles = makeStyles(styles)
|
|||
type ReviewRemoveOwnerProps = {
|
||||
onClickBack: () => void
|
||||
onClose: () => void
|
||||
onSubmit: () => void
|
||||
onSubmit: (txParameters: TxParameters) => void
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
threshold?: number
|
||||
|
@ -43,7 +47,7 @@ export const ReviewRemoveOwnerModal = ({
|
|||
onSubmit,
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
threshold,
|
||||
threshold = 1,
|
||||
}: ReviewRemoveOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [data, setData] = useState('')
|
||||
|
@ -54,6 +58,9 @@ export const ReviewRemoveOwnerModal = ({
|
|||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
gasPriceFormatted,
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
|
@ -95,134 +102,151 @@ export const ReviewRemoveOwnerModal = ({
|
|||
}, [safeAddress, ownerAddress, threshold])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
Remove owner
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>3 of 3</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block>
|
||||
<Row className={classes.root}>
|
||||
<Col layout="column" xs={4}>
|
||||
<Block className={classes.details}>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
Details
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Safe name
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{safeName}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Any transaction requires the confirmation of:
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{`${threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
<Col className={classes.owners} layout="column" xs={8}>
|
||||
<Row className={classes.ownersTitle}>
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
{`${owners ? owners.size - 1 : 0} Safe owner(s)`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
{ownersWithAddressBookName?.map(
|
||||
(owner) =>
|
||||
owner.address !== ownerAddress && (
|
||||
<React.Fragment key={owner.address}>
|
||||
<Row className={classes.owner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={owner.address} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{owner.name}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{owner.address}
|
||||
</Paragraph>
|
||||
<CopyBtn content={owner.address} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(owner.address)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</React.Fragment>
|
||||
),
|
||||
)}
|
||||
<Row align="center" className={classes.info}>
|
||||
<Paragraph color="primary" noMargin size="md" weight="bolder">
|
||||
REMOVING OWNER ↓
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.selectedOwner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={ownerAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{ownerName}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{ownerAddress}
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
Remove owner
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>3 of 3</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block>
|
||||
<Row className={classes.root}>
|
||||
<Col layout="column" xs={4}>
|
||||
<Block className={classes.details}>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
Details
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Safe name
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{safeName}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Any transaction requires the confirmation of:
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{`${threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`}
|
||||
</Paragraph>
|
||||
<CopyBtn content={ownerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(ownerAddress)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
<Col className={classes.owners} layout="column" xs={8}>
|
||||
<Row className={classes.ownersTitle}>
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
{`${owners ? owners.size - 1 : 0} Safe owner(s)`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
{ownersWithAddressBookName?.map(
|
||||
(owner) =>
|
||||
owner.address !== ownerAddress && (
|
||||
<React.Fragment key={owner.address}>
|
||||
<Row className={classes.owner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={owner.address} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{owner.name}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{owner.address}
|
||||
</Paragraph>
|
||||
<CopyBtn content={owner.address} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(owner.address)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</React.Fragment>
|
||||
),
|
||||
)}
|
||||
<Row align="center" className={classes.info}>
|
||||
<Paragraph color="primary" noMargin size="md" weight="bolder">
|
||||
REMOVING OWNER ↓
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.selectedOwner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={ownerAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{ownerName}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{ownerAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={ownerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(ownerAddress)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Block className={classes.gasCostsContainer}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClickBack}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
minHeight={42}
|
||||
minWidth={140}
|
||||
onClick={onSubmit}
|
||||
testId={REMOVE_OWNER_REVIEW_BTN_TEST_ID}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
</Block>
|
||||
<Hairline />
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
compact={false}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
|
||||
{txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (
|
||||
<Block className={classes.gasCostsContainer}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
<Hairline />
|
||||
</Block>
|
||||
)}
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClickBack}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
minHeight={42}
|
||||
minWidth={140}
|
||||
onClick={() => onSubmit(txParameters)}
|
||||
testId={REMOVE_OWNER_REVIEW_BTN_TEST_ID}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ import { createStyles, makeStyles } from '@material-ui/core/styles'
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import OwnerForm from './screens/OwnerForm'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||
import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
|
@ -15,12 +13,14 @@ import { checksumAddress } from 'src/utils/checksumAddress'
|
|||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
|
||||
import OwnerForm from './screens/OwnerForm'
|
||||
import { ReviewReplaceOwnerModal } from './screens/Review'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const styles = createStyles({
|
||||
biggerModalWindow: {
|
||||
width: '775px',
|
||||
minHeight: '500px',
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
@ -37,6 +37,7 @@ export const sendReplaceOwner = async (
|
|||
safeAddress: string,
|
||||
ownerAddressToRemove: string,
|
||||
dispatch: Dispatch,
|
||||
txParameters: TxParameters,
|
||||
threshold?: number,
|
||||
): Promise<void> => {
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
|
@ -51,6 +52,9 @@ export const sendReplaceOwner = async (
|
|||
to: safeAddress,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -88,7 +92,7 @@ export const ReplaceOwnerModal = ({
|
|||
})
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
|
@ -113,10 +117,10 @@ export const ReplaceOwnerModal = ({
|
|||
setActiveScreen('reviewReplaceOwner')
|
||||
}
|
||||
|
||||
const onReplaceOwner = async () => {
|
||||
const onReplaceOwner = async (txParameters: TxParameters) => {
|
||||
onClose()
|
||||
try {
|
||||
await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, threshold)
|
||||
await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, txParameters, threshold)
|
||||
|
||||
dispatch(
|
||||
addOrUpdateAddressBookEntry(
|
||||
|
|
|
@ -25,10 +25,13 @@ import {
|
|||
} from 'src/logic/safe/store/selectors'
|
||||
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
|
||||
import { styles } from './style'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
export const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn'
|
||||
|
||||
|
@ -37,7 +40,7 @@ const useStyles = makeStyles(styles)
|
|||
type ReplaceOwnerProps = {
|
||||
onClose: () => void
|
||||
onClickBack: () => void
|
||||
onSubmit: () => void
|
||||
onSubmit: (txParameters: TxParameters) => void
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
values: {
|
||||
|
@ -59,11 +62,14 @@ export const ReviewReplaceOwnerModal = ({
|
|||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
gasPriceFormatted,
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
|
@ -94,159 +100,174 @@ export const ReviewReplaceOwnerModal = ({
|
|||
}, [ownerAddress, safeAddress, values.newOwnerAddress])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
Replace owner
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block>
|
||||
<Row className={classes.root}>
|
||||
<Col layout="column" xs={4}>
|
||||
<Block className={classes.details}>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
Details
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Safe name
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{safeName}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Any transaction requires the confirmation of:
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{`${threshold} out of ${owners?.size || 0} owner(s)`}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
<Col className={classes.owners} layout="column" xs={8}>
|
||||
<Row className={classes.ownersTitle}>
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
{`${owners?.size || 0} Safe owner(s)`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
{ownersWithAddressBookName?.map(
|
||||
(owner) =>
|
||||
owner.address !== ownerAddress && (
|
||||
<React.Fragment key={owner.address}>
|
||||
<Row className={classes.owner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={owner.address} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{owner.name}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{owner.address}
|
||||
</Paragraph>
|
||||
<CopyBtn content={owner.address} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(owner.address)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</React.Fragment>
|
||||
),
|
||||
)}
|
||||
<Row align="center" className={classes.info}>
|
||||
<Paragraph color="primary" noMargin size="md" weight="bolder">
|
||||
REMOVING OWNER ↓
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.selectedOwnerRemoved}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={ownerAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{ownerName}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{ownerAddress}
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
Replace owner
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block>
|
||||
<Row className={classes.root}>
|
||||
<Col layout="column" xs={4}>
|
||||
<Block className={classes.details}>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
Details
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Safe name
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{safeName}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Block margin="lg">
|
||||
<Paragraph color="disabled" noMargin size="sm">
|
||||
Any transaction requires the confirmation of:
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{`${threshold} out of ${owners?.size || 0} owner(s)`}
|
||||
</Paragraph>
|
||||
<CopyBtn content={ownerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(ownerAddress)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="center" className={classes.info}>
|
||||
<Paragraph color="primary" noMargin size="md" weight="bolder">
|
||||
ADDING NEW OWNER ↓
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.selectedOwnerAdded}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={values.newOwnerAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{values.newOwnerName}
|
||||
<Col className={classes.owners} layout="column" xs={8}>
|
||||
<Row className={classes.ownersTitle}>
|
||||
<Paragraph color="primary" noMargin size="lg">
|
||||
{`${owners?.size || 0} Safe owner(s)`}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{values.newOwnerAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={values.newOwnerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(values.newOwnerAddress)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Row>
|
||||
<Hairline />
|
||||
{ownersWithAddressBookName?.map(
|
||||
(owner) =>
|
||||
owner.address !== ownerAddress && (
|
||||
<React.Fragment key={owner.address}>
|
||||
<Row className={classes.owner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={owner.address} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{owner.name}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{owner.address}
|
||||
</Paragraph>
|
||||
<CopyBtn content={owner.address} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(owner.address)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</React.Fragment>
|
||||
),
|
||||
)}
|
||||
<Row align="center" className={classes.info}>
|
||||
<Paragraph color="primary" noMargin size="md" weight="bolder">
|
||||
REMOVING OWNER ↓
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.selectedOwnerRemoved}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={ownerAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{ownerName}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{ownerAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={ownerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(ownerAddress)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="center" className={classes.info}>
|
||||
<Paragraph color="primary" noMargin size="md" weight="bolder">
|
||||
ADDING NEW OWNER ↓
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.selectedOwnerAdded}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={values.newOwnerAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{values.newOwnerName}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{values.newOwnerAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={values.newOwnerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(values.newOwnerAddress)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Block className={classes.gasCostsContainer}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClickBack}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
minHeight={42}
|
||||
minWidth={140}
|
||||
onClick={onSubmit}
|
||||
testId={REPLACE_OWNER_SUBMIT_BTN_TEST_ID}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
</Block>
|
||||
<Hairline />
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
compact={false}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
|
||||
<Block className={classes.gasCostsContainer}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClickBack}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
minHeight={42}
|
||||
minWidth={140}
|
||||
onClick={() => onSubmit(txParameters)}
|
||||
testId={REPLACE_OWNER_SUBMIT_BTN_TEST_ID}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ interface SpentVsAmountProps {
|
|||
tokenAddress: string
|
||||
}
|
||||
|
||||
const SpentVsAmount = ({ amount, spent, tokenAddress }: SpentVsAmountProps): ReactElement | null => {
|
||||
export const SpentVsAmount = ({ amount, spent, tokenAddress }: SpentVsAmountProps): ReactElement | null => {
|
||||
const { width } = useWindowDimensions()
|
||||
const showIcon = useMemo(() => width > 1024, [width])
|
||||
|
||||
|
@ -55,5 +55,3 @@ const SpentVsAmount = ({ amount, spent, tokenAddress }: SpentVsAmountProps): Rea
|
|||
</StyledImageName>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default SpentVsAmount
|
||||
|
|
|
@ -9,7 +9,7 @@ import Row from 'src/components/layout/Row'
|
|||
import { TableCell, TableRow } from 'src/components/layout/Table'
|
||||
import Table from 'src/components/Table'
|
||||
import { AddressInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay'
|
||||
import RemoveLimitModal from 'src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal'
|
||||
import { RemoveLimitModal } from 'src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal'
|
||||
import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
|
||||
|
@ -20,7 +20,7 @@ import {
|
|||
SPENDING_LIMIT_TABLE_SPENT_ID,
|
||||
SpendingLimitTable,
|
||||
} from './dataFetcher'
|
||||
import SpentVsAmount from './SpentVsAmount'
|
||||
import { SpentVsAmount } from './SpentVsAmount'
|
||||
|
||||
const TableActionButton = styled(Button)`
|
||||
background-color: transparent;
|
||||
|
@ -35,7 +35,7 @@ interface SpendingLimitTableProps {
|
|||
data?: SpendingLimitTable[]
|
||||
}
|
||||
|
||||
const LimitsTable = ({ data }: SpendingLimitTableProps): ReactElement => {
|
||||
export const LimitsTable = ({ data }: SpendingLimitTableProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
const granted = useSelector(grantedSelector)
|
||||
|
||||
|
@ -106,5 +106,3 @@ const LimitsTable = ({ data }: SpendingLimitTableProps): ReactElement => {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LimitsTable
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Button, Text } from '@gnosis.pm/safe-react-components'
|
||||
import React, { ReactElement, useMemo } from 'react'
|
||||
import React, { ReactElement, useEffect, useMemo, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import createTransaction, { CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { SafeRecordProps, SpendingLimit } from 'src/logic/safe/store/models/safe'
|
||||
import {
|
||||
addSpendingLimitBeneficiaryMultiSendTx,
|
||||
|
@ -26,8 +26,13 @@ import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/component
|
|||
import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal'
|
||||
import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style'
|
||||
import { safeParamAddressFromStateSelector, safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
|
||||
import { ActionCallback, CREATE } from '.'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
|
@ -63,6 +68,76 @@ const useExistentSpendingLimit = ({
|
|||
}, [spendingLimits, txToken.decimals, values.beneficiary, values.token])
|
||||
}
|
||||
|
||||
const calculateSpendingLimitsTxData = (
|
||||
safeAddress: string,
|
||||
spendingLimits: SpendingLimit[] | null | undefined,
|
||||
existentSpendingLimit: SpendingLimit | null,
|
||||
txToken: Token,
|
||||
values: Record<string, string>,
|
||||
txParameters?: TxParameters,
|
||||
): {
|
||||
spendingLimitTxData: CreateTransactionArgs
|
||||
transactions: MultiSendTx[]
|
||||
spendingLimitArgs: {
|
||||
beneficiary: string
|
||||
token: string
|
||||
spendingLimitInWei: string
|
||||
resetTimeMin: number
|
||||
resetBaseMin: number
|
||||
}
|
||||
} => {
|
||||
const isSpendingLimitEnabled = spendingLimits !== null
|
||||
const transactions: MultiSendTx[] = []
|
||||
|
||||
// is spendingLimit module enabled? -> if not, create the tx to enable it, and encode it
|
||||
if (!isSpendingLimitEnabled && safeAddress) {
|
||||
transactions.push(enableSpendingLimitModuleMultiSendTx(safeAddress))
|
||||
}
|
||||
|
||||
// does `delegate` already exist? (`getDelegates`, previously queried to build the table with allowances (??))
|
||||
// ^ - shall we rely on this or query the list of delegates once again?
|
||||
const isDelegateAlreadyAdded =
|
||||
spendingLimits?.some(({ delegate }) => sameAddress(delegate, values?.beneficiary)) ?? false
|
||||
|
||||
// if `delegate` does not exist, add it by calling `addDelegate(beneficiary)`
|
||||
if (!isDelegateAlreadyAdded && values?.beneficiary) {
|
||||
transactions.push(addSpendingLimitBeneficiaryMultiSendTx(values.beneficiary))
|
||||
}
|
||||
|
||||
// prepare the setAllowance tx
|
||||
const startTime = currentMinutes() - 30
|
||||
const spendingLimitArgs = {
|
||||
beneficiary: values.beneficiary,
|
||||
token: values.token,
|
||||
spendingLimitInWei: toTokenUnit(values.amount, txToken.decimals),
|
||||
resetTimeMin: values.withResetTime ? +values.resetTime * 60 * 24 : 0,
|
||||
resetBaseMin: values.withResetTime ? startTime : 0,
|
||||
}
|
||||
|
||||
let spendingLimitTxData
|
||||
if (safeAddress) {
|
||||
// if there's no tx for enable module or adding a delegate, then we avoid using multiSend Tx
|
||||
if (transactions.length === 0) {
|
||||
spendingLimitTxData = setSpendingLimitTx({ spendingLimitArgs, safeAddress })
|
||||
} else {
|
||||
const encodedTxForMultisend = setSpendingLimitMultiSendTx({ spendingLimitArgs, safeAddress })
|
||||
transactions.push(encodedTxForMultisend)
|
||||
spendingLimitTxData = spendingLimitMultiSendTx({ transactions, safeAddress })
|
||||
}
|
||||
|
||||
if (txParameters) {
|
||||
spendingLimitTxData.txNonce = txParameters.safeNonce
|
||||
spendingLimitTxData.safeTxGas = txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined
|
||||
spendingLimitTxData.ethParameters = txParameters
|
||||
}
|
||||
}
|
||||
return {
|
||||
spendingLimitTxData,
|
||||
transactions,
|
||||
spendingLimitArgs,
|
||||
}
|
||||
}
|
||||
|
||||
interface ReviewSpendingLimitProps {
|
||||
onBack: ActionCallback
|
||||
onClose: () => void
|
||||
|
@ -71,7 +146,7 @@ interface ReviewSpendingLimitProps {
|
|||
existentSpendingLimit?: SpendingLimitRow
|
||||
}
|
||||
|
||||
const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): ReactElement => {
|
||||
export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
@ -79,44 +154,57 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps):
|
|||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const spendingLimits = useSelector(safeSpendingLimitsSelector)
|
||||
const existentSpendingLimit = useExistentSpendingLimit({ spendingLimits, txToken, values })
|
||||
const [estimateGasArgs, setEstimateGasArgs] = useState<Partial<CreateTransactionArgs>>({
|
||||
to: '',
|
||||
txData: '',
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
const isSpendingLimitEnabled = spendingLimits !== null
|
||||
const transactions: MultiSendTx[] = []
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
gasPrice,
|
||||
gasPriceFormatted,
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: estimateGasArgs.txData as string,
|
||||
txRecipient: estimateGasArgs.to as string,
|
||||
operation: estimateGasArgs.operation,
|
||||
})
|
||||
|
||||
// is spendingLimit module enabled? -> if not, create the tx to enable it, and encode it
|
||||
if (!isSpendingLimitEnabled && safeAddress) {
|
||||
transactions.push(enableSpendingLimitModuleMultiSendTx(safeAddress))
|
||||
useEffect(() => {
|
||||
const { spendingLimitTxData } = calculateSpendingLimitsTxData(
|
||||
safeAddress,
|
||||
spendingLimits,
|
||||
existentSpendingLimit,
|
||||
txToken,
|
||||
values,
|
||||
)
|
||||
setEstimateGasArgs(spendingLimitTxData)
|
||||
}, [safeAddress, spendingLimits, existentSpendingLimit, txToken, values])
|
||||
|
||||
const handleSubmit = (txParameters: TxParameters): void => {
|
||||
const { ethGasPrice, ethGasLimit, ethGasPriceInGWei } = txParameters
|
||||
const advancedOptionsTxParameters = {
|
||||
...txParameters,
|
||||
ethGasPrice: ethGasPrice || gasPrice,
|
||||
ethGasPriceInGWei: ethGasPriceInGWei || gasPriceFormatted,
|
||||
ethGasLimit: ethGasLimit || gasLimit,
|
||||
}
|
||||
|
||||
// does `delegate` already exist? (`getDelegates`, previously queried to build the table with allowances (??))
|
||||
// ^ - shall we rely on this or query the list of delegates once again?
|
||||
const isDelegateAlreadyAdded =
|
||||
spendingLimits?.some(({ delegate }) => sameAddress(delegate, values?.beneficiary)) ?? false
|
||||
|
||||
// if `delegate` does not exist, add it by calling `addDelegate(beneficiary)`
|
||||
if (!isDelegateAlreadyAdded && values?.beneficiary) {
|
||||
transactions.push(addSpendingLimitBeneficiaryMultiSendTx(values.beneficiary))
|
||||
}
|
||||
|
||||
// prepare the setAllowance tx
|
||||
const startTime = currentMinutes() - 30
|
||||
const spendingLimitArgs = {
|
||||
beneficiary: values.beneficiary,
|
||||
token: values.token,
|
||||
spendingLimitInWei: toTokenUnit(values.amount, txToken.decimals),
|
||||
resetTimeMin: values.withResetTime ? +values.resetTime * 60 * 24 : 0,
|
||||
resetBaseMin: values.withResetTime ? startTime : 0,
|
||||
}
|
||||
|
||||
if (safeAddress) {
|
||||
// if there's no tx for enable module or adding a delegate, then we avoid using multiSend Tx
|
||||
if (transactions.length === 0) {
|
||||
dispatch(createTransaction(setSpendingLimitTx({ spendingLimitArgs, safeAddress })))
|
||||
} else {
|
||||
transactions.push(setSpendingLimitMultiSendTx({ spendingLimitArgs, safeAddress }))
|
||||
dispatch(createTransaction(spendingLimitMultiSendTx({ transactions, safeAddress })))
|
||||
}
|
||||
const { spendingLimitTxData } = calculateSpendingLimitsTxData(
|
||||
safeAddress,
|
||||
spendingLimits,
|
||||
existentSpendingLimit,
|
||||
txToken,
|
||||
values,
|
||||
advancedOptionsTxParameters,
|
||||
)
|
||||
|
||||
dispatch(createTransaction(spendingLimitTxData))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,60 +218,82 @@ const Review = ({ onBack, onClose, txToken, values }: ReviewSpendingLimitProps):
|
|||
?.label ?? 'One-time spending limit'
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal.TopBar title="New Spending Limit" titleNote="2 of 2" onClose={onClose} />
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Modal.TopBar title="New Spending Limit" titleNote="2 of 2" onClose={onClose} />
|
||||
|
||||
<Block className={classes.container}>
|
||||
<Col margin="lg">
|
||||
<AddressInfo address={values.beneficiary} title="Beneficiary" />
|
||||
</Col>
|
||||
<Col margin="lg">
|
||||
<TokenInfo
|
||||
amount={fromTokenUnit(toTokenUnit(values.amount, txToken.decimals), txToken.decimals)}
|
||||
title="Amount"
|
||||
token={txToken}
|
||||
/>
|
||||
{existentSpendingLimit && (
|
||||
<Text size="lg" color="error">
|
||||
Previous Amount: {existentSpendingLimit.amount}
|
||||
</Text>
|
||||
)}
|
||||
</Col>
|
||||
<Col margin="lg">
|
||||
<ResetTimeInfo title="Reset Time" label={resetTimeLabel} />
|
||||
{existentSpendingLimit && (
|
||||
<Row align="center" margin="md">
|
||||
<Text size="lg" color="error">
|
||||
Previous Reset Time: {previousResetTime(existentSpendingLimit)}
|
||||
<Block className={classes.container}>
|
||||
<Col margin="lg">
|
||||
<AddressInfo address={values.beneficiary} title="Beneficiary" />
|
||||
</Col>
|
||||
<Col margin="lg">
|
||||
<TokenInfo
|
||||
amount={fromTokenUnit(toTokenUnit(values.amount, txToken.decimals), txToken.decimals)}
|
||||
title="Amount"
|
||||
token={txToken}
|
||||
/>
|
||||
{existentSpendingLimit && (
|
||||
<Text size="lg" color="error">
|
||||
Previous Amount: {existentSpendingLimit.amount}
|
||||
</Text>
|
||||
)}
|
||||
</Col>
|
||||
<Col margin="lg">
|
||||
<ResetTimeInfo title="Reset Time" label={resetTimeLabel} />
|
||||
{existentSpendingLimit && (
|
||||
<Row align="center" margin="md">
|
||||
<Text size="lg" color="error">
|
||||
Previous Reset Time: {previousResetTime(existentSpendingLimit)}
|
||||
</Text>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{existentSpendingLimit && (
|
||||
<Text size="xl" color="error" center strong>
|
||||
You are about to replace an existent spending limit
|
||||
</Text>
|
||||
)}
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</Block>
|
||||
|
||||
{existentSpendingLimit && (
|
||||
<Text size="xl" color="error" center strong>
|
||||
You are about to replace an existent spending limit
|
||||
</Text>
|
||||
)}
|
||||
</Block>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
color="primary"
|
||||
size="md"
|
||||
onClick={() => onBack({ values: {}, txToken: makeToken(), step: CREATE })}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button color="primary" size="md" onClick={() => onBack({ values: {}, txToken: makeToken(), step: CREATE })}>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
size="md"
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
disabled={existentSpendingLimit === undefined}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</>
|
||||
<Button
|
||||
color="primary"
|
||||
size="md"
|
||||
variant="contained"
|
||||
onClick={() => handleSubmit(txParameters)}
|
||||
disabled={existentSpendingLimit === undefined || txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
)
|
||||
}
|
||||
|
||||
export default Review
|
||||
|
|
|
@ -8,7 +8,7 @@ import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector'
|
|||
import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal'
|
||||
|
||||
import Create from './Create'
|
||||
import Review from './Review'
|
||||
import { ReviewSpendingLimits } from './Review'
|
||||
|
||||
export const CREATE = 'CREATE' as const
|
||||
export const REVIEW = 'REVIEW' as const
|
||||
|
@ -81,7 +81,7 @@ interface SpendingLimitModalProps {
|
|||
open: boolean
|
||||
}
|
||||
|
||||
const NewLimitModal = ({ close, open }: SpendingLimitModalProps): ReactElement => {
|
||||
export const NewLimitModal = ({ close, open }: SpendingLimitModalProps): ReactElement => {
|
||||
// state and dispatch
|
||||
const [{ step, txToken, values }, { create, review }] = useNewLimitModal(CREATE)
|
||||
|
||||
|
@ -98,9 +98,7 @@ const NewLimitModal = ({ close, open }: SpendingLimitModalProps): ReactElement =
|
|||
description="set rules for specific beneficiaries to access funds from this Safe without having to collect all signatures"
|
||||
>
|
||||
{step === CREATE && <Create initialValues={values} onCancel={close} onReview={handleReview} />}
|
||||
{step === REVIEW && <Review onBack={create} onClose={close} txToken={txToken} values={values} />}
|
||||
{step === REVIEW && <ReviewSpendingLimits onBack={create} onClose={close} txToken={txToken} values={values} />}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewLimitModal
|
||||
|
|
|
@ -28,7 +28,7 @@ const StepsLine = styled.div`
|
|||
margin: 46px 0;
|
||||
`
|
||||
|
||||
const NewLimitSteps = (): ReactElement => (
|
||||
export const NewLimitSteps = (): ReactElement => (
|
||||
<StepWrapper>
|
||||
<Step>
|
||||
<Img alt="Select Beneficiary" title="Beneficiary" height={96} src={Beneficiary} />
|
||||
|
@ -75,5 +75,3 @@ const NewLimitSteps = (): ReactElement => (
|
|||
</Step>
|
||||
</StepWrapper>
|
||||
)
|
||||
|
||||
export default NewLimitSteps
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Button } from '@gnosis.pm/safe-react-components'
|
||||
import React, { ReactElement } from 'react'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import Block from 'src/components/layout/Block'
|
||||
|
@ -17,6 +17,12 @@ import { AddressInfo, ResetTimeInfo, TokenInfo } from './InfoDisplay'
|
|||
import { SpendingLimitTable } from './LimitsTable/dataFetcher'
|
||||
import Modal from './Modal'
|
||||
import { useStyles } from './style'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
interface RemoveSpendingLimitModalProps {
|
||||
onClose: () => void
|
||||
|
@ -24,28 +30,50 @@ interface RemoveSpendingLimitModalProps {
|
|||
open: boolean
|
||||
}
|
||||
|
||||
const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitModalProps): ReactElement => {
|
||||
export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitModalProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const tokenInfo = useTokenInfo(spendingLimit.spent.tokenAddress)
|
||||
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const [txData, setTxData] = useState('')
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const removeSelectedSpendingLimit = async (): Promise<void> => {
|
||||
try {
|
||||
const {
|
||||
beneficiary,
|
||||
spent: { tokenAddress },
|
||||
} = spendingLimit
|
||||
const txData = getDeleteAllowanceTxData({ beneficiary, tokenAddress })
|
||||
useEffect(() => {
|
||||
const {
|
||||
beneficiary,
|
||||
spent: { tokenAddress },
|
||||
} = spendingLimit
|
||||
const txData = getDeleteAllowanceTxData({ beneficiary, tokenAddress })
|
||||
setTxData(txData)
|
||||
}, [spendingLimit])
|
||||
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
gasPriceFormatted,
|
||||
} = useEstimateTransactionGas({
|
||||
txData,
|
||||
txRecipient: SPENDING_LIMIT_MODULE_ADDRESS,
|
||||
txAmount: '0',
|
||||
})
|
||||
|
||||
const removeSelectedSpendingLimit = async (txParameters: TxParameters): Promise<void> => {
|
||||
try {
|
||||
dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: SPENDING_LIMIT_MODULE_ADDRESS,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.REMOVE_SPENDING_LIMIT_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -67,36 +95,66 @@ const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendingLimitM
|
|||
title="Remove Spending Limit"
|
||||
description="Remove the selected Spending Limit"
|
||||
>
|
||||
<Modal.TopBar title="Remove Spending Limit" onClose={onClose} />
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => {
|
||||
return (
|
||||
<>
|
||||
<Modal.TopBar title="Remove Spending Limit" onClose={onClose} />
|
||||
|
||||
<Block className={classes.container}>
|
||||
<Col margin="lg">
|
||||
<AddressInfo title="Beneficiary" address={spendingLimit.beneficiary} />
|
||||
</Col>
|
||||
<Col margin="lg">
|
||||
{tokenInfo && (
|
||||
<TokenInfo
|
||||
amount={fromTokenUnit(spendingLimit.spent.amount, tokenInfo.decimals)}
|
||||
title="Amount"
|
||||
token={tokenInfo}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col margin="lg">
|
||||
<ResetTimeInfo title="Reset Time" label={resetTimeLabel} />
|
||||
</Col>
|
||||
</Block>
|
||||
<Block className={classes.container}>
|
||||
<Col margin="lg">
|
||||
<AddressInfo title="Beneficiary" address={spendingLimit.beneficiary} />
|
||||
</Col>
|
||||
<Col margin="lg">
|
||||
{tokenInfo && (
|
||||
<TokenInfo
|
||||
amount={fromTokenUnit(spendingLimit.spent.amount, tokenInfo.decimals)}
|
||||
title="Amount"
|
||||
token={tokenInfo}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col margin="lg">
|
||||
<ResetTimeInfo title="Reset Time" label={resetTimeLabel} />
|
||||
</Col>
|
||||
</Block>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button size="md" color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="error" size="md" variant="contained" onClick={removeSelectedSpendingLimit}>
|
||||
Remove
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
compact={false}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
<Row className={classes.modalDescription}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button size="md" color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
size="md"
|
||||
variant="contained"
|
||||
onClick={() => removeSelectedSpendingLimit(txParameters)}
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</EditableTxParameters>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default RemoveLimitModal
|
||||
|
|
|
@ -9,17 +9,17 @@ import Row from 'src/components/layout/Row'
|
|||
import { safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
|
||||
import LimitsTable from './LimitsTable'
|
||||
import { LimitsTable } from './LimitsTable'
|
||||
import { getSpendingLimitData } from './LimitsTable/dataFetcher'
|
||||
import NewLimitModal from './NewLimitModal'
|
||||
import NewLimitSteps from './NewLimitSteps'
|
||||
import { NewLimitModal } from './NewLimitModal'
|
||||
import { NewLimitSteps } from './NewLimitSteps'
|
||||
import { useStyles } from './style'
|
||||
|
||||
const InfoText = styled(Text)`
|
||||
margin-top: 16px;
|
||||
`
|
||||
|
||||
const SpendingLimitSettings = (): ReactElement => {
|
||||
export const SpendingLimitSettings = (): ReactElement => {
|
||||
const classes = useStyles()
|
||||
const granted = useSelector(grantedSelector)
|
||||
const allowances = useSelector(safeSpendingLimitsSelector)
|
||||
|
@ -68,5 +68,3 @@ const SpendingLimitSettings = (): ReactElement => {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpendingLimitSettings
|
||||
|
|
|
@ -73,7 +73,7 @@ export const useStyles = makeStyles(
|
|||
modalHeading: {
|
||||
boxSizing: 'border-box',
|
||||
justifyContent: 'space-between',
|
||||
maxHeight: '75px',
|
||||
height: '74px',
|
||||
padding: `${sm} ${lg}`,
|
||||
},
|
||||
modalContainer: {
|
||||
|
@ -123,7 +123,6 @@ export const useStyles = makeStyles(
|
|||
modal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
amountInput: {
|
||||
width: '100% !important',
|
||||
|
|
|
@ -3,7 +3,7 @@ import MenuItem from '@material-ui/core/MenuItem'
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { styles } from './style'
|
||||
import { List } from 'immutable'
|
||||
|
||||
import Field from 'src/components/forms/Field'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
|
@ -17,10 +17,12 @@ import Paragraph from 'src/components/layout/Paragraph'
|
|||
import Row from 'src/components/layout/Row'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
||||
import { List } from 'immutable'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
|
||||
import { styles } from './style'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
|
||||
const THRESHOLD_FIELD_NAME = 'threshold'
|
||||
|
||||
|
@ -50,6 +52,9 @@ export const ChangeThresholdModal = ({
|
|||
isCreation,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
gasLimit,
|
||||
gasPriceFormatted,
|
||||
gasEstimation,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: data,
|
||||
txRecipient: safeAddress,
|
||||
|
@ -71,6 +76,8 @@ export const ChangeThresholdModal = ({
|
|||
}
|
||||
}, [safeAddress, threshold])
|
||||
|
||||
const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED')
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
const newThreshold = values[THRESHOLD_FIELD_NAME]
|
||||
|
||||
|
@ -79,75 +86,98 @@ export const ChangeThresholdModal = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Change required confirmations
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.close} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<GnoForm initialValues={{ threshold: threshold.toString() }} onSubmit={handleSubmit}>
|
||||
{() => (
|
||||
<>
|
||||
<Block className={classes.modalContent}>
|
||||
<Row>
|
||||
<Paragraph weight="bolder">Any transaction requires the confirmation of:</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" className={classes.inputRow} margin="xl">
|
||||
<Col xs={2}>
|
||||
<Field
|
||||
data-testid="threshold-select-input"
|
||||
name={THRESHOLD_FIELD_NAME}
|
||||
render={(props) => (
|
||||
<>
|
||||
<SelectField {...props} disableError>
|
||||
{[...Array(Number(owners?.size))].map((x, index) => (
|
||||
<MenuItem key={index} value={`${index + 1}`}>
|
||||
{index + 1}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SelectField>
|
||||
{props.meta.error && props.meta.touched && (
|
||||
<Paragraph className={classes.errorText} color="error" noMargin>
|
||||
{props.meta.error}
|
||||
</Paragraph>
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Change required confirmations
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.close} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<GnoForm initialValues={{ threshold: threshold.toString() }} onSubmit={handleSubmit}>
|
||||
{() => (
|
||||
<>
|
||||
<Block className={classes.modalContent}>
|
||||
<Row>
|
||||
<Paragraph weight="bolder">Any transaction requires the confirmation of:</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" className={classes.inputRow} margin="xl">
|
||||
<Col xs={2}>
|
||||
<Field
|
||||
data-testid="threshold-select-input"
|
||||
name={THRESHOLD_FIELD_NAME}
|
||||
render={(props) => (
|
||||
<>
|
||||
<SelectField {...props} disableError>
|
||||
{[...Array(Number(owners?.size))].map((x, index) => (
|
||||
<MenuItem key={index} value={`${index + 1}`}>
|
||||
{index + 1}
|
||||
</MenuItem>
|
||||
))}
|
||||
</SelectField>
|
||||
{props.meta.error && props.meta.touched && (
|
||||
<Paragraph className={classes.errorText} color="error" noMargin>
|
||||
{props.meta.error}
|
||||
</Paragraph>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
validate={composeValidators(required, mustBeInteger, minValue(1), differentFrom(threshold))}
|
||||
validate={composeValidators(required, mustBeInteger, minValue(1), differentFrom(threshold))}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={10}>
|
||||
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
||||
{`out of ${owners?.size} owner(s)`}
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
compact={true}
|
||||
parametersStatus={getParametersStatus()}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={10}>
|
||||
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
||||
{`out of ${owners?.size} owner(s)`}
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onClose}>
|
||||
Back
|
||||
</Button>
|
||||
<Button color="primary" minWidth={140} type="submit" variant="contained">
|
||||
Change
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</GnoForm>
|
||||
</>
|
||||
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onClose}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
minWidth={140}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</GnoForm>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ export const styles = createStyles({
|
|||
padding: `${sm} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
height: '74px',
|
||||
},
|
||||
annotation: {
|
||||
letterSpacing: '-1px',
|
||||
|
@ -30,7 +30,7 @@ export const styles = createStyles({
|
|||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
position: 'relative',
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
},
|
||||
|
|
|
@ -2,9 +2,6 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { ChangeThresholdModal } from './ChangeThreshold'
|
||||
import { styles } from './style'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
|
@ -22,6 +19,11 @@ import {
|
|||
safeThresholdSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||
import { useTransactionParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { EditTxParametersForm } from 'src/routes/safe/components/Transactions/helpers/EditTxParametersForm'
|
||||
|
||||
import { ChangeThresholdModal } from './ChangeThreshold'
|
||||
import { styles } from './style'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -29,10 +31,12 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||
const classes = useStyles()
|
||||
const [isModalOpen, setModalOpen] = useState(false)
|
||||
const dispatch = useDispatch()
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
const txParameters = useTransactionParameters()
|
||||
const [activeScreen, setActiveScreen] = useState('form')
|
||||
|
||||
const toggleModal = () => {
|
||||
setModalOpen((prevOpen) => !prevOpen)
|
||||
|
@ -48,6 +52,9 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||
to: safeAddress,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -59,6 +66,10 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Settings', label: 'Owners' })
|
||||
}, [trackEvent])
|
||||
|
||||
const closeEditTxParameters = () => setActiveScreen('form')
|
||||
|
||||
const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED')
|
||||
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.container}>
|
||||
|
@ -87,13 +98,22 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||
open={isModalOpen}
|
||||
title="Change Required Confirmations"
|
||||
>
|
||||
<ChangeThresholdModal
|
||||
onChangeThreshold={onChangeThreshold}
|
||||
onClose={toggleModal}
|
||||
owners={owners}
|
||||
safeAddress={safeAddress}
|
||||
threshold={threshold}
|
||||
/>
|
||||
{activeScreen === 'form' && (
|
||||
<ChangeThresholdModal
|
||||
onChangeThreshold={onChangeThreshold}
|
||||
onClose={toggleModal}
|
||||
owners={owners}
|
||||
safeAddress={safeAddress}
|
||||
threshold={threshold}
|
||||
/>
|
||||
)}
|
||||
{activeScreen === 'editTxParameters' && (
|
||||
<EditTxParametersForm
|
||||
txParameters={txParameters}
|
||||
onClose={closeEditTxParameters}
|
||||
parametersStatus={getParametersStatus()}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -15,10 +15,13 @@ import { getUpgradeSafeTransactionHash } from 'src/logic/safe/utils/upgradeSafe'
|
|||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||
import { DELEGATE_CALL } from 'src/logic/safe/transactions'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -40,7 +43,7 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle
|
|||
calculateUpgradeSafeModal()
|
||||
}, [safeAddress])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const handleSubmit = async (txParameters: TxParameters) => {
|
||||
// Call the update safe method
|
||||
dispatch(
|
||||
createTransaction({
|
||||
|
@ -48,6 +51,9 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle
|
|||
to: MULTI_SEND_ADDRESS,
|
||||
valueInWei: '0',
|
||||
txData: multiSendCallData,
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: 'STANDARD_TX',
|
||||
operation: DELEGATE_CALL,
|
||||
}),
|
||||
|
@ -61,65 +67,86 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle
|
|||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
gasPriceFormatted,
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: multiSendCallData,
|
||||
txRecipient: safeAddress,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Update to new Safe version
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.close} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<GnoForm onSubmit={handleSubmit}>
|
||||
{() => (
|
||||
<>
|
||||
<Block className={classes.modalContent}>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
Update now to take advantage of new features and the highest security standards available.
|
||||
</Paragraph>
|
||||
<Block>
|
||||
This update includes:
|
||||
<ul>
|
||||
<li>Compatibility with new asset types (ERC-721 / ERC-1155)</li>
|
||||
<li>Improved interoperability with modules</li>
|
||||
<li>Minor security improvements</li>
|
||||
</ul>
|
||||
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Update to new Safe version
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.close} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<GnoForm onSubmit={() => handleSubmit(txParameters)}>
|
||||
{() => (
|
||||
<>
|
||||
<Block className={classes.modalContent}>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
Update now to take advantage of new features and the highest security standards available.
|
||||
</Paragraph>
|
||||
<Block>
|
||||
This update includes:
|
||||
<ul>
|
||||
<li>Compatibility with new asset types (ERC-721 / ERC-1155)</li>
|
||||
<li>Improved interoperability with modules</li>
|
||||
<li>Minor security improvements</li>
|
||||
</ul>
|
||||
</Block>
|
||||
<Paragraph>
|
||||
You will need to confirm this update just like any other transaction. This means other owners will
|
||||
have to confirm the update in case more than one confirmation is required for this Safe.
|
||||
</Paragraph>
|
||||
</Row>
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
compact={false}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Paragraph>
|
||||
You will need to confirm this update just like any other transaction. This means other owners will
|
||||
have to confirm the update in case more than one confirmation is required for this Safe.
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onClose}>
|
||||
Back
|
||||
</Button>
|
||||
<Button color="primary" minWidth={140} type="submit" variant="contained">
|
||||
Update Safe
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</GnoForm>
|
||||
</>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onClose}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
minWidth={140}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={!multiSendCallData || txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Update Safe
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</GnoForm>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -30,9 +30,6 @@ export const styles = createStyles({
|
|||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
},
|
||||
inputRow: {
|
||||
position: 'relative',
|
||||
|
|
|
@ -6,8 +6,8 @@ import * as React from 'react'
|
|||
import { useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import Advanced from './Advanced'
|
||||
import SpendingLimitSettings from './SpendingLimit'
|
||||
import { Advanced } from './Advanced'
|
||||
import { SpendingLimitSettings } from './SpendingLimit'
|
||||
import ManageOwners from './ManageOwners'
|
||||
import { RemoveSafeModal } from './RemoveSafeModal'
|
||||
import SafeDetails from './SafeDetails'
|
||||
|
|
|
@ -21,8 +21,11 @@ import { processTransaction } from 'src/logic/safe/store/actions/processTransact
|
|||
|
||||
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -53,33 +56,37 @@ const getModalTitleAndDescription = (thresholdReached, isCancelTx) => {
|
|||
}
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
canExecute: boolean
|
||||
isCancelTx?: boolean
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
thresholdReached: boolean
|
||||
tx: Transaction
|
||||
txParameters: TxParameters
|
||||
}
|
||||
|
||||
export const ApproveTxModal = ({
|
||||
onClose,
|
||||
canExecute,
|
||||
isCancelTx = false,
|
||||
isOpen,
|
||||
onClose,
|
||||
thresholdReached,
|
||||
tx,
|
||||
}: Props): React.ReactElement => {
|
||||
const dispatch = useDispatch()
|
||||
const userAddress = useSelector(userAccountSelector)
|
||||
const classes = useStyles()
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const [approveAndExecute, setApproveAndExecute] = useState(canExecute)
|
||||
const { description, title } = getModalTitleAndDescription(thresholdReached, isCancelTx)
|
||||
const oneConfirmationLeft = !thresholdReached && tx.confirmations.size + 1 === threshold
|
||||
const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
gasPriceFormatted,
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
|
@ -93,11 +100,12 @@ export const ApproveTxModal = ({
|
|||
preApprovingOwner: approveAndExecute ? userAddress : undefined,
|
||||
safeTxGas: tx.safeTxGas,
|
||||
operation: tx.operation,
|
||||
manualGasPrice: manualGasPrice,
|
||||
})
|
||||
|
||||
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
|
||||
|
||||
const approveTx = () => {
|
||||
const approveTx = (txParameters: TxParameters) => {
|
||||
dispatch(
|
||||
processTransaction({
|
||||
safeAddress,
|
||||
|
@ -105,72 +113,128 @@ export const ApproveTxModal = ({
|
|||
userAddress,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
|
||||
approveAndExecute: canExecute && approveAndExecute && isTheTxReadyToBeExecuted,
|
||||
ethParameters: txParameters,
|
||||
thresholdReached,
|
||||
}),
|
||||
)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const getParametersStatus = () => {
|
||||
if (canExecute || approveAndExecute) {
|
||||
return 'SAFE_DISABLED'
|
||||
}
|
||||
|
||||
return 'DISABLED'
|
||||
}
|
||||
|
||||
const closeEditModalCallback = (txParameters: TxParameters) => {
|
||||
const oldGasPrice = Number(gasPriceFormatted)
|
||||
const newGasPrice = Number(txParameters.ethGasPrice)
|
||||
|
||||
if (newGasPrice && oldGasPrice !== newGasPrice) {
|
||||
setManualGasPrice(newGasPrice.toString())
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal description={description} handleClose={onClose} open={isOpen} title={title}>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
{title}
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<Row style={{ flexDirection: 'column' }}>
|
||||
<Paragraph>{description}</Paragraph>
|
||||
<Paragraph color="medium" size="sm">
|
||||
Transaction nonce:
|
||||
<br />
|
||||
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
|
||||
</Paragraph>
|
||||
{oneConfirmationLeft && canExecute && (
|
||||
<EditableTxParameters
|
||||
parametersStatus={getParametersStatus()}
|
||||
ethGasLimit={gasLimit}
|
||||
ethGasPrice={gasPriceFormatted}
|
||||
safeNonce={tx.nonce.toString()}
|
||||
safeTxGas={tx.safeTxGas.toString()}
|
||||
closeEditModalCallback={closeEditModalCallback}
|
||||
>
|
||||
{(txParameters, toggleEditMode) => {
|
||||
return (
|
||||
<>
|
||||
<Paragraph color="error">
|
||||
Approving this transaction executes it right away.
|
||||
{!isCancelTx &&
|
||||
' If you want approve but execute the transaction manually later, click on the checkbox below.'}
|
||||
</Paragraph>
|
||||
{!isCancelTx && (
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={approveAndExecute} color="primary" onChange={handleExecuteCheckbox} />}
|
||||
label="Execute transaction"
|
||||
data-testid="execute-checkbox"
|
||||
{/* Header */}
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
{title}
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
|
||||
<Hairline />
|
||||
|
||||
{/* Tx info */}
|
||||
<Block className={classes.container}>
|
||||
<Row style={{ flexDirection: 'column' }}>
|
||||
<Paragraph>{description}</Paragraph>
|
||||
<Paragraph color="medium" size="sm">
|
||||
Transaction nonce:
|
||||
<br />
|
||||
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
|
||||
</Paragraph>
|
||||
|
||||
{oneConfirmationLeft && canExecute && (
|
||||
<>
|
||||
<Paragraph color="error">
|
||||
Approving this transaction executes it right away.
|
||||
{!isCancelTx &&
|
||||
' If you want approve but execute the transaction manually later, click on the checkbox below.'}
|
||||
</Paragraph>
|
||||
|
||||
{!isCancelTx && (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox checked={approveAndExecute} color="primary" onChange={handleExecuteCheckbox} />
|
||||
}
|
||||
label="Execute transaction"
|
||||
data-testid="execute-checkbox"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Tx Parameters */}
|
||||
{approveAndExecute && (
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
parametersStatus={getParametersStatus()}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
|
||||
{/* Footer */}
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClose}>
|
||||
Exit
|
||||
</Button>
|
||||
<Button
|
||||
color={isCancelTx ? 'secondary' : 'primary'}
|
||||
minHeight={42}
|
||||
minWidth={214}
|
||||
onClick={() => approveTx(txParameters)}
|
||||
testId={isCancelTx ? REJECT_TX_MODAL_SUBMIT_BTN_TEST_ID : APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Block>
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClose}>
|
||||
Exit
|
||||
</Button>
|
||||
<Button
|
||||
color={isCancelTx ? 'secondary' : 'primary'}
|
||||
minHeight={42}
|
||||
minWidth={214}
|
||||
onClick={approveTx}
|
||||
testId={isCancelTx ? REJECT_TX_MODAL_SUBMIT_BTN_TEST_ID : APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
</Row>
|
||||
)
|
||||
}}
|
||||
</EditableTxParameters>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ export const styles = createStyles({
|
|||
padding: `${sm} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
height: '74px',
|
||||
},
|
||||
headingText: {
|
||||
fontSize: lg,
|
||||
|
@ -21,7 +21,7 @@ export const styles = createStyles({
|
|||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
position: 'relative',
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: `1px solid ${border}`,
|
||||
|
|
|
@ -19,8 +19,12 @@ import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
|||
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { ParametersStatus } from 'src/routes/safe/components/Transactions/helpers/utils'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -41,72 +45,106 @@ export const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElemen
|
|||
isExecution,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
gasPriceFormatted,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: EMPTY_DATA,
|
||||
txRecipient: safeAddress,
|
||||
})
|
||||
|
||||
const sendReplacementTransaction = () => {
|
||||
const sendReplacementTransaction = (txParameters: TxParameters) => {
|
||||
dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: safeAddress,
|
||||
valueInWei: '0',
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX,
|
||||
txNonce: tx.nonce,
|
||||
origin: tx.origin,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX,
|
||||
}),
|
||||
)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const getParametersStatus = (): ParametersStatus => {
|
||||
return 'CANCEL_TRANSACTION'
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal description="Reject Transaction" handleClose={onClose} open={isOpen} title="Reject Transaction">
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Reject transaction
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
This action will cancel this transaction. A separate transaction will be performed to submit the rejection.
|
||||
</Paragraph>
|
||||
<Paragraph color="medium" size="sm">
|
||||
Transaction nonce:
|
||||
<br />
|
||||
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClose}>
|
||||
Exit
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
minHeight={42}
|
||||
minWidth={214}
|
||||
onClick={sendReplacementTransaction}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Reject Transaction
|
||||
</Button>
|
||||
</Row>
|
||||
<EditableTxParameters
|
||||
ethGasLimit={gasLimit}
|
||||
ethGasPrice={gasPriceFormatted}
|
||||
safeTxGas={gasEstimation.toString()}
|
||||
safeNonce={tx.nonce.toString()}
|
||||
parametersStatus={getParametersStatus()}
|
||||
>
|
||||
{(txParameters, toggleEditMode) => {
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Reject transaction
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
This action will cancel this transaction. A separate transaction will be performed to submit the
|
||||
rejection.
|
||||
</Paragraph>
|
||||
<Paragraph color="medium" size="sm">
|
||||
Transaction nonce:
|
||||
<br />
|
||||
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
|
||||
</Paragraph>
|
||||
</Row>
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
compact={false}
|
||||
parametersStatus={getParametersStatus()}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClose}>
|
||||
Exit
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
minHeight={42}
|
||||
minWidth={214}
|
||||
onClick={() => sendReplacementTransaction(txParameters)}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Reject Transaction
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</EditableTxParameters>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ export const styles = createStyles({
|
|||
padding: `${sm} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
height: '74px',
|
||||
},
|
||||
headingText: {
|
||||
fontSize: lg,
|
||||
|
@ -21,7 +21,7 @@ export const styles = createStyles({
|
|||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
position: 'relative',
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: `1px solid ${border}`,
|
||||
|
|
|
@ -34,6 +34,7 @@ import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
|||
import TransferDescription from './TxDescription/TransferDescription'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { safeNonceSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||
import { useTransactionParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const ExpandedModuleTx = ({ tx }: { tx: SafeModuleTransaction }): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
@ -92,22 +93,27 @@ interface ExpandedSafeTxProps {
|
|||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
type ScreenType = 'approveTx' | 'executeRejectTx' | 'rejectTx'
|
||||
|
||||
const ExpandedSafeTx = ({ cancelTx, tx }: ExpandedSafeTxProps): ReactElement => {
|
||||
const { fromWei, toBN } = getWeb3().utils
|
||||
const classes = useStyles()
|
||||
const nonce = useSelector(safeNonceSelector)
|
||||
const threshold = useSelector(safeThresholdSelector) as number
|
||||
const [openModal, setOpenModal] = useState<'approveTx' | 'executeRejectTx' | 'rejectTx'>()
|
||||
const openApproveModal = () => setOpenModal('approveTx')
|
||||
const closeModal = () => setOpenModal(undefined)
|
||||
const [openModal, setOpenModal] = useState<ScreenType>()
|
||||
const txParameters = useTransactionParameters()
|
||||
|
||||
const isIncomingTx = !!INCOMING_TX_TYPES[tx.type]
|
||||
const isCreationTx = tx.type === TransactionTypes.CREATION
|
||||
|
||||
const thresholdReached = !isIncomingTx && threshold <= tx.confirmations.size
|
||||
const canExecute = !isIncomingTx && nonce === tx.nonce
|
||||
const cancelThresholdReached = !!cancelTx && threshold <= cancelTx.confirmations?.size
|
||||
const canExecuteCancel = nonce === tx.nonce
|
||||
|
||||
const openApproveModal = () => setOpenModal('approveTx')
|
||||
|
||||
const closeModal = () => setOpenModal(undefined)
|
||||
|
||||
const openRejectModal = () => {
|
||||
if (!!cancelTx && nonce === cancelTx.nonce) {
|
||||
setOpenModal('executeRejectTx')
|
||||
|
@ -168,6 +174,8 @@ const ExpandedSafeTx = ({ cancelTx, tx }: ExpandedSafeTxProps): ReactElement =>
|
|||
)}
|
||||
</Row>
|
||||
</Block>
|
||||
|
||||
{/* Approve TX */}
|
||||
{openModal === 'approveTx' && (
|
||||
<ApproveTxModal
|
||||
canExecute={canExecute}
|
||||
|
@ -175,17 +183,23 @@ const ExpandedSafeTx = ({ cancelTx, tx }: ExpandedSafeTxProps): ReactElement =>
|
|||
onClose={closeModal}
|
||||
thresholdReached={thresholdReached}
|
||||
tx={tx}
|
||||
txParameters={txParameters}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Reject TX */}
|
||||
{openModal === 'rejectTx' && <RejectTxModal isOpen onClose={closeModal} tx={tx} />}
|
||||
|
||||
{/* Execute the rejection TX */}
|
||||
{openModal === 'executeRejectTx' && cancelTx && (
|
||||
<ApproveTxModal
|
||||
canExecute={canExecuteCancel}
|
||||
onClose={closeModal}
|
||||
isCancelTx
|
||||
isOpen
|
||||
onClose={closeModal}
|
||||
canExecute={canExecuteCancel}
|
||||
thresholdReached={cancelThresholdReached}
|
||||
tx={cancelTx}
|
||||
txParameters={txParameters}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
import React from 'react'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { Title, Text, Divider, Link, Icon } from '@gnosis.pm/safe-react-components'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Field from 'src/components/forms/Field'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { styles } from './style'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { composeValidators, minValue } from 'src/components/forms/validator'
|
||||
|
||||
import { ParametersStatus, areSafeParamsEnabled, areEthereumParamsEnabled } from '../utils'
|
||||
|
||||
const StyledDivider = styled(Divider)`
|
||||
margin: 0px;
|
||||
`
|
||||
|
||||
const SafeOptions = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
const EthereumOptions = styled.div`
|
||||
display: flex;
|
||||
/* justify-content: space-between; */
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 20px;
|
||||
|
||||
div {
|
||||
width: 216px !important;
|
||||
}
|
||||
`
|
||||
const StyledLink = styled(Link)`
|
||||
margin: 16px 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
> :first-of-type {
|
||||
margin-right: 5px;
|
||||
}
|
||||
`
|
||||
const StyledIconButton = styled(IconButton)`
|
||||
margin: 10px 0 0 0;
|
||||
`
|
||||
const StyledText = styled(Text)`
|
||||
margin: 0 0 4px 0;
|
||||
`
|
||||
const StyledTextMt = styled(Text)`
|
||||
margin: 16px 0 4px 0;
|
||||
`
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
interface Props {
|
||||
txParameters: TxParameters
|
||||
onClose: (txParameters?: TxParameters) => void
|
||||
parametersStatus: ParametersStatus
|
||||
}
|
||||
|
||||
const formValidation = (values) => {
|
||||
const { ethGasLimit, ethGasPrice, ethNonce, safeNonce, safeTxGas } = values ?? {}
|
||||
|
||||
const ethGasLimitValidation = minValue(0, true)(ethGasLimit)
|
||||
|
||||
const ethGasPriceValidation = minValue(0, true)(ethGasPrice)
|
||||
|
||||
const ethNonceValidation = minValue(0, true)(ethNonce)
|
||||
|
||||
const safeNonceValidation = minValue(0, true)(safeNonce)
|
||||
|
||||
const safeTxGasValidation = composeValidators(minValue(0, true), (value: string) => {
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Number(value) > Number(ethGasLimit)) {
|
||||
return 'Bigger than Ethereum gas limit.'
|
||||
}
|
||||
})(safeTxGas)
|
||||
|
||||
return {
|
||||
ethGasLimit: ethGasLimitValidation,
|
||||
ethGasPrice: ethGasPriceValidation,
|
||||
ethNonce: ethNonceValidation,
|
||||
safeNonce: safeNonceValidation,
|
||||
safeTxGas: safeTxGasValidation,
|
||||
}
|
||||
}
|
||||
|
||||
export const EditTxParametersForm = ({
|
||||
onClose,
|
||||
txParameters,
|
||||
parametersStatus = 'ENABLED',
|
||||
}: Props): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const { safeNonce, safeTxGas, ethNonce, ethGasLimit, ethGasPrice } = txParameters
|
||||
|
||||
const onSubmit = (values: TxParameters) => {
|
||||
onClose(values)
|
||||
}
|
||||
|
||||
const onCloseFormHandler = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Header */}
|
||||
<Row align="center" className={classes.heading} grow data-testid="send-funds-review-step">
|
||||
<Title size="sm" withoutMargin>
|
||||
Advanced options
|
||||
</Title>
|
||||
<StyledIconButton disableRipple onClick={onCloseFormHandler}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</StyledIconButton>
|
||||
</Row>
|
||||
|
||||
<StyledDivider />
|
||||
|
||||
<Block className={classes.container}>
|
||||
<GnoForm
|
||||
initialValues={{
|
||||
safeNonce: safeNonce || 0,
|
||||
safeTxGas: safeTxGas || '',
|
||||
ethNonce: ethNonce || '',
|
||||
ethGasLimit: ethGasLimit || '',
|
||||
ethGasPrice: ethGasPrice || '',
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
validation={formValidation}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<StyledText size="xl" strong>
|
||||
Safe transactions parameters
|
||||
</StyledText>
|
||||
|
||||
<SafeOptions>
|
||||
<Field
|
||||
name="safeNonce"
|
||||
defaultValue={safeNonce}
|
||||
placeholder="Safe nonce"
|
||||
text="Safe nonce"
|
||||
type="number"
|
||||
min="0"
|
||||
component={TextField}
|
||||
disabled={!areSafeParamsEnabled(parametersStatus)}
|
||||
/>
|
||||
<Field
|
||||
name="safeTxGas"
|
||||
defaultValue={safeTxGas}
|
||||
placeholder="SafeTxGas"
|
||||
text="SafeTxGas"
|
||||
type="number"
|
||||
min="0"
|
||||
component={TextField}
|
||||
disabled={!areSafeParamsEnabled(parametersStatus)}
|
||||
/>
|
||||
</SafeOptions>
|
||||
|
||||
<StyledTextMt size="xl" strong>
|
||||
Ethereum transactions parameters
|
||||
</StyledTextMt>
|
||||
|
||||
<EthereumOptions>
|
||||
<Field
|
||||
name="ethNonce"
|
||||
defaultValue={ethNonce}
|
||||
placeholder="Ethereum nonce"
|
||||
text="Ethereum nonce"
|
||||
type="number"
|
||||
component={TextField}
|
||||
disabled={!areEthereumParamsEnabled(parametersStatus)}
|
||||
/>
|
||||
<Field
|
||||
name="ethGasLimit"
|
||||
defaultValue={ethGasLimit}
|
||||
placeholder="Ethereum gas limit"
|
||||
text="Ethereum gas limit"
|
||||
type="number"
|
||||
component={TextField}
|
||||
disabled={
|
||||
parametersStatus === 'CANCEL_TRANSACTION' ? false : !areEthereumParamsEnabled(parametersStatus)
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
name="ethGasPrice"
|
||||
defaultValue={ethGasPrice}
|
||||
type="number"
|
||||
placeholder="Ethereum gas price (GWEI)"
|
||||
text="Ethereum gas price (GWEI)"
|
||||
component={TextField}
|
||||
disabled={!areEthereumParamsEnabled(parametersStatus)}
|
||||
/>
|
||||
</EthereumOptions>
|
||||
|
||||
<StyledLink
|
||||
href="https://docs.gnosis.io/safe/docs/contracts_tx_execution/#safe-transaction-gas-limit-estimation"
|
||||
target="_blank"
|
||||
>
|
||||
<Text size="xl" color="primary">
|
||||
How can I configure the gas price manually?
|
||||
</Text>
|
||||
<Icon size="sm" type="externalLink" color="primary" />
|
||||
</StyledLink>
|
||||
|
||||
<StyledDivider />
|
||||
|
||||
{/* Footer */}
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onCloseFormHandler}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
/* disabled={!data} */
|
||||
minWidth={140}
|
||||
/* onClick={submitTx} */
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</GnoForm>
|
||||
</Block>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { lg, md, secondaryText, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = createStyles({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
},
|
||||
annotation: {
|
||||
letterSpacing: '-1px',
|
||||
color: secondaryText,
|
||||
marginRight: 'auto',
|
||||
marginLeft: '20px',
|
||||
},
|
||||
headingText: {
|
||||
fontSize: lg,
|
||||
},
|
||||
closeIcon: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
container: {
|
||||
padding: `${md} ${lg}`,
|
||||
},
|
||||
amount: {
|
||||
marginLeft: sm,
|
||||
},
|
||||
address: {
|
||||
marginRight: sm,
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
'& > button': {
|
||||
fontFamily: 'Averta',
|
||||
fontSize: md,
|
||||
},
|
||||
},
|
||||
submitButton: {
|
||||
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||
marginLeft: '15px',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,81 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { TxParameters, useTransactionParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { EditTxParametersForm } from 'src/routes/safe/components/Transactions/helpers/EditTxParametersForm'
|
||||
import { ParametersStatus } from './utils'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||
|
||||
type Props = {
|
||||
children: (txParameters: TxParameters, toggleStatus: (txParameters?: TxParameters) => void) => any
|
||||
parametersStatus?: ParametersStatus
|
||||
ethGasLimit?: TxParameters['ethGasLimit']
|
||||
ethGasPrice?: TxParameters['ethGasPrice']
|
||||
safeNonce?: TxParameters['safeNonce']
|
||||
safeTxGas?: TxParameters['safeTxGas']
|
||||
closeEditModalCallback?: (txParameters: TxParameters) => void
|
||||
}
|
||||
|
||||
export const EditableTxParameters = ({
|
||||
children,
|
||||
parametersStatus,
|
||||
ethGasLimit,
|
||||
ethGasPrice,
|
||||
safeNonce,
|
||||
safeTxGas,
|
||||
closeEditModalCallback,
|
||||
}: Props): React.ReactElement => {
|
||||
const [isEditMode, toggleEditMode] = useState(false)
|
||||
const [useManualValues, setUseManualValues] = useState(false)
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
const defaultParameterStatus = threshold > 1 ? 'ETH_DISABLED' : 'ENABLED'
|
||||
const txParameters = useTransactionParameters({
|
||||
parameterStatus: parametersStatus || defaultParameterStatus,
|
||||
initialEthGasLimit: ethGasLimit,
|
||||
initialEthGasPrice: ethGasPrice,
|
||||
initialSafeNonce: safeNonce,
|
||||
initialSafeTxGas: safeTxGas,
|
||||
})
|
||||
const { setEthGasPrice, setEthGasLimit, setSafeNonce, setSafeTxGas, setEthNonce } = txParameters
|
||||
|
||||
// Update TxParameters
|
||||
useEffect(() => {
|
||||
if (!useManualValues) {
|
||||
setEthGasLimit(ethGasLimit)
|
||||
setEthGasPrice(ethGasPrice)
|
||||
setSafeTxGas(safeTxGas)
|
||||
}
|
||||
}, [ethGasLimit, setEthGasLimit, ethGasPrice, setEthGasPrice, useManualValues, safeTxGas, setSafeTxGas])
|
||||
|
||||
const toggleStatus = () => {
|
||||
toggleEditMode((prev) => !prev)
|
||||
}
|
||||
|
||||
// Sends a callback with the last values of txParameters
|
||||
useEffect(() => {
|
||||
if (!isEditMode && closeEditModalCallback) {
|
||||
closeEditModalCallback(txParameters)
|
||||
}
|
||||
}, [isEditMode, closeEditModalCallback, txParameters])
|
||||
|
||||
const closeEditFormHandler = (txParameters?: TxParameters) => {
|
||||
if (txParameters) {
|
||||
setUseManualValues(true)
|
||||
setSafeNonce(txParameters.safeNonce)
|
||||
setSafeTxGas(txParameters.safeTxGas)
|
||||
setEthGasLimit(txParameters.ethGasLimit)
|
||||
setEthGasPrice(txParameters.ethGasPrice)
|
||||
setEthNonce(txParameters.ethNonce)
|
||||
}
|
||||
toggleStatus()
|
||||
}
|
||||
|
||||
return isEditMode ? (
|
||||
<EditTxParametersForm
|
||||
txParameters={txParameters}
|
||||
onClose={closeEditFormHandler}
|
||||
parametersStatus={parametersStatus ? parametersStatus : defaultParameterStatus}
|
||||
/>
|
||||
) : (
|
||||
children(txParameters, toggleStatus)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Text, ButtonLink, Accordion, AccordionSummary, AccordionDetails } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { ParametersStatus, areEthereumParamsEnabled, areSafeParamsEnabled } from '../utils'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||
|
||||
const TxParameterWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const AccordionDetailsWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
const StyledText = styled(Text)`
|
||||
margin: 8px 0 0 0;
|
||||
`
|
||||
|
||||
const StyledButtonLink = styled(ButtonLink)`
|
||||
padding-left: 0;
|
||||
margin: 8px 0 0 0;
|
||||
|
||||
> p {
|
||||
margin-left: 0;
|
||||
}
|
||||
`
|
||||
|
||||
type Props = {
|
||||
txParameters: TxParameters
|
||||
onEdit: () => void
|
||||
compact?: boolean
|
||||
parametersStatus?: ParametersStatus
|
||||
isTransactionExecution: boolean
|
||||
isTransactionCreation: boolean
|
||||
}
|
||||
|
||||
export const TxParametersDetail = ({
|
||||
onEdit,
|
||||
txParameters,
|
||||
compact = true,
|
||||
parametersStatus,
|
||||
isTransactionCreation,
|
||||
isTransactionExecution,
|
||||
}: Props): ReactElement | null => {
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
const defaultParameterStatus = threshold > 1 ? 'ETH_DISABLED' : 'ENABLED'
|
||||
|
||||
if (!isTransactionExecution && !isTransactionCreation) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion {...compact}>
|
||||
<AccordionSummary>
|
||||
<Text size="lg">Advanced options</Text>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<AccordionDetailsWrapper>
|
||||
<StyledText size="md" color="placeHolder">
|
||||
Safe transactions parameters
|
||||
</StyledText>
|
||||
|
||||
<TxParameterWrapper>
|
||||
<Text
|
||||
size="lg"
|
||||
color={areSafeParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
|
||||
>
|
||||
Safe nonce
|
||||
</Text>
|
||||
<Text
|
||||
size="lg"
|
||||
color={areSafeParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
|
||||
>
|
||||
{txParameters.safeNonce}
|
||||
</Text>
|
||||
</TxParameterWrapper>
|
||||
|
||||
<TxParameterWrapper>
|
||||
<Text
|
||||
size="lg"
|
||||
color={areSafeParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
|
||||
>
|
||||
SafeTxGas
|
||||
</Text>
|
||||
<Text
|
||||
size="lg"
|
||||
color={areSafeParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
|
||||
>
|
||||
{txParameters.safeTxGas}
|
||||
</Text>
|
||||
</TxParameterWrapper>
|
||||
|
||||
<TxParameterWrapper>
|
||||
<StyledText size="md" color="placeHolder">
|
||||
Ethereum transaction parameters
|
||||
</StyledText>
|
||||
</TxParameterWrapper>
|
||||
|
||||
<TxParameterWrapper>
|
||||
<Text
|
||||
size="lg"
|
||||
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
|
||||
>
|
||||
Ethereum nonce
|
||||
</Text>
|
||||
<Text
|
||||
size="lg"
|
||||
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
|
||||
>
|
||||
{txParameters.ethNonce}
|
||||
</Text>
|
||||
</TxParameterWrapper>
|
||||
|
||||
<TxParameterWrapper>
|
||||
<Text
|
||||
size="lg"
|
||||
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
|
||||
>
|
||||
Ethereum gas limit
|
||||
</Text>
|
||||
<Text
|
||||
size="lg"
|
||||
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
|
||||
>
|
||||
{txParameters.ethGasLimit}
|
||||
</Text>
|
||||
</TxParameterWrapper>
|
||||
|
||||
<TxParameterWrapper>
|
||||
<Text
|
||||
size="lg"
|
||||
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
|
||||
>
|
||||
Ethereum gas price
|
||||
</Text>
|
||||
<Text
|
||||
size="lg"
|
||||
color={areEthereumParamsEnabled(parametersStatus || defaultParameterStatus) ? 'text' : 'secondaryLight'}
|
||||
>
|
||||
{txParameters.ethGasPrice}
|
||||
</Text>
|
||||
</TxParameterWrapper>
|
||||
|
||||
<StyledButtonLink color="primary" textSize="xl" onClick={onEdit}>
|
||||
Edit
|
||||
</StyledButtonLink>
|
||||
</AccordionDetailsWrapper>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
export type ParametersStatus = 'ENABLED' | 'DISABLED' | 'SAFE_DISABLED' | 'ETH_DISABLED' | 'CANCEL_TRANSACTION'
|
||||
|
||||
export const areEthereumParamsEnabled = (parametersStatus: ParametersStatus): boolean => {
|
||||
return (
|
||||
parametersStatus === 'ENABLED' || (parametersStatus !== 'ETH_DISABLED' && parametersStatus !== 'CANCEL_TRANSACTION')
|
||||
)
|
||||
}
|
||||
|
||||
export const areSafeParamsEnabled = (parametersStatus: ParametersStatus): boolean => {
|
||||
return (
|
||||
parametersStatus === 'ENABLED' ||
|
||||
(parametersStatus !== 'SAFE_DISABLED' && parametersStatus !== 'CANCEL_TRANSACTION')
|
||||
)
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { getUserNonce } from 'src/logic/wallets/ethTransactions'
|
||||
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { getLastTx, getNewTxNonce } from 'src/logic/safe/store/actions/utils'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||
import { ParametersStatus } from 'src/routes/safe/components/Transactions/helpers/utils'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
|
||||
export type TxParameters = {
|
||||
safeNonce: string | undefined
|
||||
setSafeNonce: (safeNonce: string | undefined) => void
|
||||
safeTxGas: string | undefined
|
||||
setSafeTxGas: (gas: string | undefined) => void
|
||||
ethNonce: string | undefined
|
||||
setEthNonce: (ethNonce: string | undefined) => void
|
||||
ethGasLimit: string | undefined
|
||||
setEthGasLimit: (ethGasLimit: string | undefined) => void
|
||||
ethGasPrice: string | undefined
|
||||
setEthGasPrice: (ethGasPrice: string | undefined) => void
|
||||
ethGasPriceInGWei: string | undefined
|
||||
}
|
||||
|
||||
type Props = {
|
||||
parameterStatus?: ParametersStatus
|
||||
initialSafeNonce?: string
|
||||
initialSafeTxGas?: string
|
||||
initialEthGasLimit?: string
|
||||
initialEthGasPrice?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* This hooks is used to store tx parameter
|
||||
* It needs to be initialized calling setGasEstimation.
|
||||
*/
|
||||
export const useTransactionParameters = (props?: Props): TxParameters => {
|
||||
const isCancelTransaction = sameString(props?.parameterStatus || 'ENABLED', 'CANCEL_TRANSACTION')
|
||||
const connectedWalletAddress = useSelector(userAccountSelector)
|
||||
const { address: safeAddress } = useSelector(safeSelector) || {}
|
||||
|
||||
// Safe Params
|
||||
const [safeNonce, setSafeNonce] = useState<string | undefined>(props?.initialSafeNonce)
|
||||
// SafeTxGas: for a new Tx call requiredTxGas, for an existing tx get it from the backend.
|
||||
const [safeTxGas, setSafeTxGas] = useState<string | undefined>(isCancelTransaction ? '0' : props?.initialSafeTxGas)
|
||||
|
||||
// ETH Params
|
||||
const [ethNonce, setEthNonce] = useState<string | undefined>() // we delegate it to the wallet
|
||||
const [ethGasLimit, setEthGasLimit] = useState<string | undefined>(props?.initialEthGasLimit) // call execTx until it returns a number > 0
|
||||
const [ethGasPrice, setEthGasPrice] = useState<string | undefined>(props?.initialEthGasPrice) // get fast gas price
|
||||
const [ethGasPriceInGWei, setEthGasPriceInGWei] = useState<string | undefined>() // get fast gas price
|
||||
|
||||
// Get nonce for connected wallet
|
||||
useEffect(() => {
|
||||
const getNonce = async () => {
|
||||
const res = await getUserNonce(connectedWalletAddress)
|
||||
setEthNonce(res.toString())
|
||||
}
|
||||
|
||||
if (connectedWalletAddress) {
|
||||
getNonce()
|
||||
}
|
||||
}, [connectedWalletAddress])
|
||||
|
||||
// Get ETH gas price
|
||||
useEffect(() => {
|
||||
if (!ethGasPrice) {
|
||||
setEthGasPriceInGWei(undefined)
|
||||
return
|
||||
}
|
||||
if (isCancelTransaction) {
|
||||
setEthGasPrice('0')
|
||||
return
|
||||
}
|
||||
setEthGasPriceInGWei(web3.utils.toWei(ethGasPrice, 'Gwei'))
|
||||
}, [ethGasPrice, isCancelTransaction])
|
||||
|
||||
// Calc safe nonce
|
||||
useEffect(() => {
|
||||
const getSafeNonce = async () => {
|
||||
if (safeAddress) {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nonce = await getNewTxNonce(lastTx, safeInstance)
|
||||
setSafeNonce(nonce)
|
||||
}
|
||||
}
|
||||
|
||||
const safeNonce = Number(props?.initialSafeNonce || 0)
|
||||
if (!safeNonce) {
|
||||
getSafeNonce()
|
||||
}
|
||||
}, [safeAddress, props?.initialSafeNonce])
|
||||
|
||||
return {
|
||||
safeNonce,
|
||||
setSafeNonce,
|
||||
safeTxGas,
|
||||
setSafeTxGas,
|
||||
ethNonce,
|
||||
setEthNonce,
|
||||
ethGasLimit,
|
||||
setEthGasLimit,
|
||||
ethGasPrice,
|
||||
setEthGasPrice,
|
||||
ethGasPriceInGWei,
|
||||
}
|
||||
}
|
|
@ -19,9 +19,9 @@ describe('shouldExecuteTransaction', () => {
|
|||
})
|
||||
it('It should return true if given a safe with a threshold === 1 and the previous transaction is already executed', async () => {
|
||||
// given
|
||||
const nonce = '0'
|
||||
const nonce = '1'
|
||||
const threshold = '1'
|
||||
const safeInstance = getMockedSafeInstance({ threshold })
|
||||
const safeInstance = getMockedSafeInstance({ threshold, nonce })
|
||||
const lastTx = getMockedTxServiceModel({})
|
||||
|
||||
// when
|
||||
|
@ -34,7 +34,7 @@ describe('shouldExecuteTransaction', () => {
|
|||
// given
|
||||
const nonce = '10'
|
||||
const threshold = '1'
|
||||
const safeInstance = getMockedSafeInstance({ threshold })
|
||||
const safeInstance = getMockedSafeInstance({ threshold, nonce })
|
||||
const lastTx = getMockedTxServiceModel({ isExecuted: true })
|
||||
|
||||
// when
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
import 'styled-components'
|
||||
import { theme } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
type Theme = typeof theme
|
||||
|
||||
export {}
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -10,3 +15,7 @@ declare global {
|
|||
}
|
||||
declare module '@openzeppelin/contracts/build/contracts/ERC721'
|
||||
declare module 'currency-flags/dist/currency-flags.min.css'
|
||||
|
||||
declare module 'styled-components' {
|
||||
export interface DefaultTheme extends Theme {} // eslint-disable-line
|
||||
}
|
||||
|
|
396
yarn.lock
396
yarn.lock
|
@ -1299,6 +1299,15 @@
|
|||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
|
||||
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
||||
|
||||
"@ensdomains/address-encoder@0.1.8":
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@ensdomains/address-encoder/-/address-encoder-0.1.8.tgz#be022a706cfd39327caaca2fa7f59d19ea2787d9"
|
||||
integrity sha512-0YwXk/cX7G/qM7YHrOybF9Ht7cwR0/XH37yh0cdPdIOPJ6Y839yymyAWGVC7Sl2ESAhqj4hfoHpb2+QllB7KAQ==
|
||||
dependencies:
|
||||
bech32 "^1.1.3"
|
||||
crypto-addr-codec "^0.1.7"
|
||||
eztz-lib "^0.1.2"
|
||||
|
||||
"@eslint/eslintrc@^0.2.2":
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.2.tgz#d01fc791e2fc33e88a29d6f3dc7e93d0cd784b76"
|
||||
|
@ -1345,6 +1354,21 @@
|
|||
"@ethersproject/properties" "^5.0.3"
|
||||
"@ethersproject/strings" "^5.0.4"
|
||||
|
||||
"@ethersproject/abi@^5.0.1":
|
||||
version "5.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.9.tgz#738c1c557e56d8f395a5a27caef9b0449bc85a10"
|
||||
integrity sha512-ily2OufA2DTrxkiHQw5GqbkMSnNKuwZBqKsajtT0ERhZy1r9w2CpW1bmtRMIGzaqQxCdn/GEoFogexk72cBBZQ==
|
||||
dependencies:
|
||||
"@ethersproject/address" "^5.0.4"
|
||||
"@ethersproject/bignumber" "^5.0.7"
|
||||
"@ethersproject/bytes" "^5.0.4"
|
||||
"@ethersproject/constants" "^5.0.4"
|
||||
"@ethersproject/hash" "^5.0.4"
|
||||
"@ethersproject/keccak256" "^5.0.3"
|
||||
"@ethersproject/logger" "^5.0.5"
|
||||
"@ethersproject/properties" "^5.0.3"
|
||||
"@ethersproject/strings" "^5.0.4"
|
||||
|
||||
"@ethersproject/abstract-provider@^5.0.8":
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.0.8.tgz#880793c29bfed33dff4c2b2be7ecb9ba966d52c0"
|
||||
|
@ -1529,9 +1553,9 @@
|
|||
solc "0.5.14"
|
||||
truffle "^5.1.21"
|
||||
|
||||
"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#bf3a84486b7353bd25447ddff39c406f6fafecc6":
|
||||
"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#2e7574f":
|
||||
version "0.4.0"
|
||||
resolved "https://github.com/gnosis/safe-react-components.git#bf3a84486b7353bd25447ddff39c406f6fafecc6"
|
||||
resolved "https://github.com/gnosis/safe-react-components.git#2e7574fa0ea4ec798aac35249f532be86a1c5cb8"
|
||||
dependencies:
|
||||
classnames "^2.2.6"
|
||||
polished "^3.6.7"
|
||||
|
@ -1791,11 +1815,26 @@
|
|||
"@ledgerhq/logs" "^5.38.0"
|
||||
rxjs "^6.6.3"
|
||||
|
||||
"@ledgerhq/devices@^5.41.0":
|
||||
version "5.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-5.41.0.tgz#e69d6aa4379a30f60cc8109f9855d902eb0d2f27"
|
||||
integrity sha512-3rZzjJ8JEX2dBU+Nq8+FygxFoMZJ/XucjwNBXRaZQfqIFfYGywUH2RueQTY8d0JpzeSS5XYALAXzwLCRZmYrAg==
|
||||
dependencies:
|
||||
"@ledgerhq/errors" "^5.41.0"
|
||||
"@ledgerhq/logs" "^5.41.0"
|
||||
rxjs "^6.6.3"
|
||||
semver "^7.3.4"
|
||||
|
||||
"@ledgerhq/errors@^5.34.0", "@ledgerhq/errors@^5.38.0":
|
||||
version "5.38.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-5.38.0.tgz#1642b87de47cbabc7b75ca93005a690895920a72"
|
||||
integrity sha512-d4gQzbOLNBoGIwDtEGFNSb0w0aYN10T5Y749e+vqiJoS3dWrB+5BCSQ/U/ALet0wi/UMIyFY/xmgd1gPaPB3Hw==
|
||||
|
||||
"@ledgerhq/errors@^5.36.0", "@ledgerhq/errors@^5.41.0":
|
||||
version "5.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-5.41.0.tgz#246668c6a6ee2e0e9fbb87f00b354fdd386308b7"
|
||||
integrity sha512-Di6Uq9l9c/W9V1jZAQjsVPbvSOSRipF+zXd/Z6SVKB1QxIHwXZu7KYSUnDLV6jqKZ9fxXoLjTYWq2Gl/EW87Kw==
|
||||
|
||||
"@ledgerhq/hw-app-eth@^5.21.0":
|
||||
version "5.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-5.37.0.tgz#bd01465c78add275f6769e025a2ce4d142f1a10c"
|
||||
|
@ -1851,11 +1890,25 @@
|
|||
"@ledgerhq/errors" "^5.38.0"
|
||||
events "^3.2.0"
|
||||
|
||||
"@ledgerhq/hw-transport@^5.36.0":
|
||||
version "5.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-5.41.0.tgz#b4d222fdef304582efaadf098d1916f5edb9caa3"
|
||||
integrity sha512-4zmTW1XwxvO5Ei+LoV11qXPd97UI8XxwuCeb794g/r6dkqlAL8bh35aVCHpbiHRXnoCt51a74ZciQ1yYteR/8Q==
|
||||
dependencies:
|
||||
"@ledgerhq/devices" "^5.41.0"
|
||||
"@ledgerhq/errors" "^5.41.0"
|
||||
events "^3.2.0"
|
||||
|
||||
"@ledgerhq/logs@^5.30.0", "@ledgerhq/logs@^5.38.0":
|
||||
version "5.38.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-5.38.0.tgz#3c5dbd1f62c0bf5580477b218fb57c67b575643a"
|
||||
integrity sha512-i87Yn89Cq2D9Y0KmrEzCm62XHzI2edeOTBENKH6vAyzESGzyF+SBoqtZNwrjJcKup3/9dNn/zHjpicY7ev94Vw==
|
||||
|
||||
"@ledgerhq/logs@^5.41.0":
|
||||
version "5.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-5.41.0.tgz#377b77aa29c33c63c6b0b7e1916fa300bd52ce17"
|
||||
integrity sha512-pY3nIxOWISAreVTiKRDdKgjQvKWkzz3lov9LsJLyaTn0Q1PISHPjcuMpchueLI50BMA6v1M8ENzXgpqqw+KQDw==
|
||||
|
||||
"@material-ui/core@^4.11.0":
|
||||
version "4.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.11.2.tgz#f8276dfa40d88304e6ceb98962af73803d27d42d"
|
||||
|
@ -3660,6 +3713,24 @@
|
|||
"@restless/sanitizers" "^0.2.5"
|
||||
reactive-properties "^0.1.11"
|
||||
|
||||
"@unstoppabledomains/resolution@^1.11.1":
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@unstoppabledomains/resolution/-/resolution-1.11.1.tgz#88ac10b195104a396f4700287b9bbbc1a202b07b"
|
||||
integrity sha512-BLLKRCYg3ouDbG9HlzJS3cn1W1OY5rdCMiFChqLjWUBvRjsNO7KdREa6SqXsWwm7zQZ35HDjX1aRdiypQ50WUA==
|
||||
dependencies:
|
||||
"@ensdomains/address-encoder" "0.1.8"
|
||||
"@ethersproject/abi" "^5.0.1"
|
||||
bip44-constants "^8.0.5"
|
||||
bn.js "^4.4.0"
|
||||
commander "^4.1.1"
|
||||
content-hash "^2.5.2"
|
||||
ethereum-ens-network-map "^1.0.2"
|
||||
hash.js "^1.1.7"
|
||||
js-sha3 "^0.8.0"
|
||||
node-fetch "^2.6.0"
|
||||
optionalDependencies:
|
||||
dotenv "^8.2.0"
|
||||
|
||||
"@walletconnect/client@^1.3.2":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/client/-/client-1.3.2.tgz#8f67ac53bc3f7dfa0cff7a86ea6e43b11178d4f4"
|
||||
|
@ -4246,6 +4317,11 @@ argparse@^1.0.7:
|
|||
dependencies:
|
||||
sprintf-js "~1.0.2"
|
||||
|
||||
argv@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/argv/-/argv-0.0.2.tgz#ecbd16f8949b157183711b1bda334f37840185ab"
|
||||
integrity sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=
|
||||
|
||||
aria-query@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
|
||||
|
@ -4423,6 +4499,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
||||
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
|
||||
|
||||
assert-plus@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
|
||||
integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ=
|
||||
|
||||
assert@^1.1.1:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb"
|
||||
|
@ -4562,12 +4643,17 @@ await-semaphore@^0.1.3:
|
|||
resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3"
|
||||
integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==
|
||||
|
||||
aws-sign2@~0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
|
||||
integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8=
|
||||
|
||||
aws-sign2@~0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||
integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
|
||||
|
||||
aws4@^1.8.0:
|
||||
aws4@^1.2.1, aws4@^1.8.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
|
||||
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
|
||||
|
@ -5565,6 +5651,11 @@ bcrypt-pbkdf@^1.0.0:
|
|||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
bech32@^1.1.3:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
|
||||
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
|
||||
|
||||
bfj@^7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2"
|
||||
|
@ -5575,6 +5666,11 @@ bfj@^7.0.2:
|
|||
hoopy "^0.1.4"
|
||||
tryer "^1.0.1"
|
||||
|
||||
big-integer@1.6.36:
|
||||
version "1.6.36"
|
||||
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.36.tgz#78631076265d4ae3555c04f85e7d9d2f3a071a36"
|
||||
integrity sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg==
|
||||
|
||||
big.js@^5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
|
@ -5612,6 +5708,22 @@ bindings@^1.2.1, bindings@^1.3.0, bindings@^1.5.0:
|
|||
dependencies:
|
||||
file-uri-to-path "1.0.0"
|
||||
|
||||
bip39@^2.5.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/bip39/-/bip39-2.6.0.tgz#9e3a720b42ec8b3fbe4038f1e445317b6a99321c"
|
||||
integrity sha512-RrnQRG2EgEoqO24ea+Q/fftuPUZLmrEM3qNhhGsA3PbaXaCW791LTzPuVyx/VprXQcTbPJ3K3UeTna8ZnVl2sg==
|
||||
dependencies:
|
||||
create-hash "^1.1.0"
|
||||
pbkdf2 "^3.0.9"
|
||||
randombytes "^2.0.1"
|
||||
safe-buffer "^5.0.1"
|
||||
unorm "^1.3.3"
|
||||
|
||||
bip44-constants@^8.0.5:
|
||||
version "8.0.84"
|
||||
resolved "https://registry.yarnpkg.com/bip44-constants/-/bip44-constants-8.0.84.tgz#d9576385714fdafee44e83e41a6bd256f58fdc80"
|
||||
integrity sha512-GihFb+wM+fZmi+UEh4tYdNbw3gsZZ22aKoaCxadhmyzDjXQAZ5x/FZiEw5cTs1y6vNZ94JnrTJOIvDsMSGw+1A==
|
||||
|
||||
bip66@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22"
|
||||
|
@ -5747,6 +5859,13 @@ boolean@^3.0.1:
|
|||
resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.0.2.tgz#df1baa18b6a2b0e70840475e1d93ec8fe75b2570"
|
||||
integrity sha512-RwywHlpCRc3/Wh81MiCKun4ydaIFyW5Ea6JbL6sRCVx5q5irDw7pMXBUFYF/jArQ6YrG36q0kpovc9P/Kd3I4g==
|
||||
|
||||
boom@2.x.x:
|
||||
version "2.10.1"
|
||||
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
|
||||
integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=
|
||||
dependencies:
|
||||
hoek "2.x.x"
|
||||
|
||||
bowser@^2.10.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
|
||||
|
@ -5927,7 +6046,7 @@ bs58@^4.0.0, bs58@^4.0.1:
|
|||
dependencies:
|
||||
base-x "^3.0.2"
|
||||
|
||||
bs58check@^2.1.2:
|
||||
bs58check@^2.1.1, bs58check@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc"
|
||||
integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==
|
||||
|
@ -5991,6 +6110,14 @@ buffer-xor@^1.0.3:
|
|||
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
|
||||
integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
|
||||
|
||||
buffer@5.6.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
|
||||
integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
|
||||
dependencies:
|
||||
base64-js "^1.0.2"
|
||||
ieee754 "^1.1.4"
|
||||
|
||||
buffer@^4.3.0:
|
||||
version "4.9.2"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
|
||||
|
@ -6000,7 +6127,7 @@ buffer@^4.3.0:
|
|||
ieee754 "^1.1.4"
|
||||
isarray "^1.0.0"
|
||||
|
||||
buffer@^5.0.5, buffer@^5.2.1, buffer@^5.4.2, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0:
|
||||
buffer@^5.0.5, buffer@^5.1.0, buffer@^5.2.1, buffer@^5.4.2, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||
|
@ -6265,6 +6392,11 @@ case-sensitive-paths-webpack-plugin@2.3.0, case-sensitive-paths-webpack-plugin@^
|
|||
resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7"
|
||||
integrity sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ==
|
||||
|
||||
caseless@~0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
|
||||
integrity sha1-cVuW6phBWTzDMGeSP17GDr2k99c=
|
||||
|
||||
caseless@~0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||
|
@ -6287,7 +6419,7 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2:
|
|||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chalk@^1.1.3:
|
||||
chalk@^1.1.1, chalk@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
|
||||
integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
|
||||
|
@ -6637,6 +6769,15 @@ code-point-at@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
|
||||
|
||||
codecov@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/codecov/-/codecov-2.3.1.tgz#7dda945cd58a1f6081025b5b03ee01a2ef20f86e"
|
||||
integrity sha1-fdqUXNWKH2CBAltbA+4Bou8g+G4=
|
||||
dependencies:
|
||||
argv "0.0.2"
|
||||
request "2.77.0"
|
||||
urlgrey "0.4.4"
|
||||
|
||||
collect-v8-coverage@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
|
||||
|
@ -6700,7 +6841,7 @@ colors@^1.1.2:
|
|||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
|
||||
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||
combined-stream@^1.0.5, combined-stream@^1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
|
@ -6731,7 +6872,7 @@ commander@3.0.2:
|
|||
resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e"
|
||||
integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==
|
||||
|
||||
commander@^2.19.0, commander@^2.20.0:
|
||||
commander@^2.19.0, commander@^2.20.0, commander@^2.9.0:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
@ -7121,6 +7262,26 @@ cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2:
|
|||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
cryptiles@2.x.x:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
|
||||
integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=
|
||||
dependencies:
|
||||
boom "2.x.x"
|
||||
|
||||
crypto-addr-codec@^0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/crypto-addr-codec/-/crypto-addr-codec-0.1.7.tgz#e16cea892730178fe25a38f6d15b680cab3124ae"
|
||||
integrity sha512-X4hzfBzNhy4mAc3UpiXEC/L0jo5E8wAa9unsnA8nNXYzXjCcGk83hfC5avJWCSGT8V91xMnAS9AKMHmjw5+XCg==
|
||||
dependencies:
|
||||
base-x "^3.0.8"
|
||||
big-integer "1.6.36"
|
||||
blakejs "^1.1.0"
|
||||
bs58 "^4.0.1"
|
||||
ripemd160-min "0.0.6"
|
||||
safe-buffer "^5.2.0"
|
||||
sha3 "^2.1.1"
|
||||
|
||||
crypto-browserify@3.12.0, crypto-browserify@^3.11.0:
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
|
||||
|
@ -9043,6 +9204,11 @@ ethereum-cryptography@^0.1.3:
|
|||
secp256k1 "^4.0.1"
|
||||
setimmediate "^1.0.5"
|
||||
|
||||
ethereum-ens-network-map@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.2.tgz#4e27bad18dae7bd95d84edbcac2c9e739fc959b9"
|
||||
integrity sha512-5qwJ5n3YhjSpE6O/WEBXCAb2nagUgyagJ6C0lGUBWC4LjKp/rRzD+pwtDJ6KCiITFEAoX4eIrWOjRy0Sylq5Hg==
|
||||
|
||||
ethereum-ens@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/ethereum-ens/-/ethereum-ens-0.8.0.tgz#6d0f79acaa61fdbc87d2821779c4e550243d4c57"
|
||||
|
@ -9483,7 +9649,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
|
|||
assign-symbols "^1.0.0"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
extend@^3.0.0, extend@~3.0.2:
|
||||
extend@^3.0.0, extend@~3.0.0, extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
@ -9531,6 +9697,21 @@ extsprintf@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
|
||||
|
||||
eztz-lib@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eztz-lib/-/eztz-lib-0.1.2.tgz#6e00037f85862f06f9997108302cfa673fe2e703"
|
||||
integrity sha512-77pr673PMEPvbcHylbRuHwd5P5D3Myc3VC54AC2ovaKTdIVbW/vc6HIrf8D44/a5q9eZzlUpunfKrkBug02OBw==
|
||||
dependencies:
|
||||
bignumber.js "^7.2.1"
|
||||
bip39 "^2.5.0"
|
||||
bs58check "^2.1.1"
|
||||
buffer "^5.1.0"
|
||||
codecov "^2.3.1"
|
||||
libsodium-wrappers "^0.5.4"
|
||||
pbkdf2 "^3.0.14"
|
||||
xhr2 "^0.1.4"
|
||||
xmlhttprequest "^1.8.0"
|
||||
|
||||
fake-merkle-patricia-tree@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz#4b8c3acfb520afadf9860b1f14cd8ce3402cddd3"
|
||||
|
@ -9959,6 +10140,15 @@ form-data@^2.3.1:
|
|||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@~2.1.1:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
|
||||
integrity sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.5"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
||||
|
@ -10152,6 +10342,20 @@ gauge@~2.7.3:
|
|||
strip-ansi "^3.0.1"
|
||||
wide-align "^1.1.0"
|
||||
|
||||
generate-function@^2.0.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
|
||||
integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
|
||||
dependencies:
|
||||
is-property "^1.0.2"
|
||||
|
||||
generate-object-property@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
|
||||
integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=
|
||||
dependencies:
|
||||
is-property "^1.0.0"
|
||||
|
||||
gensync@^1.0.0-beta.1:
|
||||
version "1.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
|
@ -10499,6 +10703,16 @@ har-schema@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||
integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
|
||||
|
||||
har-validator@~2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
|
||||
integrity sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=
|
||||
dependencies:
|
||||
chalk "^1.1.1"
|
||||
commander "^2.9.0"
|
||||
is-my-json-valid "^2.12.4"
|
||||
pinkie-promise "^2.0.0"
|
||||
|
||||
har-validator@~5.1.3:
|
||||
version "5.1.5"
|
||||
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
|
||||
|
@ -10634,6 +10848,16 @@ hastscript@^5.0.0:
|
|||
property-information "^5.0.0"
|
||||
space-separated-tokens "^1.0.0"
|
||||
|
||||
hawk@~3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
|
||||
integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=
|
||||
dependencies:
|
||||
boom "2.x.x"
|
||||
cryptiles "2.x.x"
|
||||
hoek "2.x.x"
|
||||
sntp "1.x.x"
|
||||
|
||||
hdkey@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-2.0.1.tgz#0a211d0c510bfc44fa3ec9d44b13b634641cad74"
|
||||
|
@ -10689,6 +10913,11 @@ hmac-drbg@^1.0.0:
|
|||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
hoek@2.x.x:
|
||||
version "2.16.3"
|
||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
|
||||
integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=
|
||||
|
||||
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
|
@ -10884,6 +11113,15 @@ http-proxy@^1.17.0:
|
|||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
http-signature@~1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
|
||||
integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=
|
||||
dependencies:
|
||||
assert-plus "^0.2.0"
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
http-signature@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||
|
@ -11534,6 +11772,22 @@ is-module@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
|
||||
integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=
|
||||
|
||||
is-my-ip-valid@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
|
||||
integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==
|
||||
|
||||
is-my-json-valid@^2.12.4:
|
||||
version "2.20.5"
|
||||
resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.20.5.tgz#5eca6a8232a687f68869b7361be1612e7512e5df"
|
||||
integrity sha512-VTPuvvGQtxvCeghwspQu1rBgjYUT6FGxPlvFKbYuFtgc4ADsX3U5ihZOYN0qyU6u+d4X9xXb0IT5O6QpXKt87A==
|
||||
dependencies:
|
||||
generate-function "^2.0.0"
|
||||
generate-object-property "^1.1.0"
|
||||
is-my-ip-valid "^1.0.0"
|
||||
jsonpointer "^4.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
is-negative-zero@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
|
||||
|
@ -11617,6 +11871,11 @@ is-potential-custom-element-name@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
|
||||
integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
|
||||
|
||||
is-property@^1.0.0, is-property@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
|
||||
integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=
|
||||
|
||||
is-regex@^1.0.4, is-regex@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
|
||||
|
@ -12553,6 +12812,11 @@ jsonify@~0.0.0:
|
|||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
|
||||
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
|
||||
|
||||
jsonpointer@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc"
|
||||
integrity sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==
|
||||
|
||||
jsprim@^1.2.2:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
|
||||
|
@ -12874,6 +13138,18 @@ levn@~0.3.0:
|
|||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
libsodium-wrappers@^0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.5.4.tgz#0059bf219c3a37b228f823342399b1607a4c5ba4"
|
||||
integrity sha512-dAYsfQgh6XwR4I65y7T5qDgb2XNKDpzXEXz229sDplaJfnAuIBTHBYlQ44jL5DIS4cCUspE3+g0kF4/Xe8286A==
|
||||
dependencies:
|
||||
libsodium "0.5.4"
|
||||
|
||||
libsodium@0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.5.4.tgz#874413435ee40c512dd59ea6db9b75970f8aec02"
|
||||
integrity sha512-s1TQ2V23JvGby1gnCQEQncTNTGck/rtJPPA8c0TiBo9z9TpT4eUk5zThte8H1TkdoKQznneqZqyoqdrwu2btWw==
|
||||
|
||||
lines-and-columns@^1.1.6:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
||||
|
@ -13453,7 +13729,7 @@ mime-db@1.45.0, "mime-db@>= 1.43.0 < 2":
|
|||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
|
||||
integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
|
||||
|
||||
mime-types@^2.1.12, mime-types@^2.1.16, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
|
||||
mime-types@^2.1.12, mime-types@^2.1.16, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.7:
|
||||
version "2.1.28"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd"
|
||||
integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==
|
||||
|
@ -13997,6 +14273,11 @@ node-releases@^1.1.29, node-releases@^1.1.3, node-releases@^1.1.61, node-release
|
|||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12"
|
||||
integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==
|
||||
|
||||
node-uuid@~1.4.7:
|
||||
version "1.4.8"
|
||||
resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907"
|
||||
integrity sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=
|
||||
|
||||
nofilter@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-1.0.4.tgz#78d6f4b6a613e7ced8b015cec534625f7667006e"
|
||||
|
@ -14139,6 +14420,11 @@ nwsapi@^2.2.0:
|
|||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
|
||||
integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
|
||||
|
||||
oauth-sign@~0.8.1:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
|
||||
integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=
|
||||
|
||||
oauth-sign@~0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
||||
|
@ -14766,7 +15052,7 @@ path-type@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||
|
||||
pbkdf2@^3.0.17, pbkdf2@^3.0.3:
|
||||
pbkdf2@^3.0.14, pbkdf2@^3.0.17, pbkdf2@^3.0.3, pbkdf2@^3.0.9:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94"
|
||||
integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==
|
||||
|
@ -15925,7 +16211,7 @@ punycode@2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
|
||||
integrity sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=
|
||||
|
||||
punycode@^1.2.4:
|
||||
punycode@^1.2.4, punycode@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
||||
|
@ -15984,6 +16270,11 @@ qs@^6.5.1, qs@^6.6.0:
|
|||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
|
||||
integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
|
||||
|
||||
qs@~6.3.0:
|
||||
version "6.3.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
|
||||
integrity sha1-51vV9uJoEioqDgvaYwslUMFmUCw=
|
||||
|
||||
qs@~6.5.2:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
|
@ -16995,6 +17286,32 @@ request-promise-native@^1.0.8:
|
|||
stealthy-require "^1.1.1"
|
||||
tough-cookie "^2.3.3"
|
||||
|
||||
request@2.77.0:
|
||||
version "2.77.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.77.0.tgz#2b00d82030ededcc97089ffa5d8810a9c2aa314b"
|
||||
integrity sha1-KwDYIDDt7cyXCJ/6XYgQqcKqMUs=
|
||||
dependencies:
|
||||
aws-sign2 "~0.6.0"
|
||||
aws4 "^1.2.1"
|
||||
caseless "~0.11.0"
|
||||
combined-stream "~1.0.5"
|
||||
extend "~3.0.0"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.1.1"
|
||||
har-validator "~2.0.6"
|
||||
hawk "~3.1.3"
|
||||
http-signature "~1.1.0"
|
||||
is-typedarray "~1.0.0"
|
||||
isstream "~0.1.2"
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.7"
|
||||
node-uuid "~1.4.7"
|
||||
oauth-sign "~0.8.1"
|
||||
qs "~6.3.0"
|
||||
stringstream "~0.0.4"
|
||||
tough-cookie "~2.3.0"
|
||||
tunnel-agent "~0.4.1"
|
||||
|
||||
request@^2.79.0, request@^2.85.0, request@^2.88.2:
|
||||
version "2.88.2"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||
|
@ -17211,6 +17528,11 @@ rimraf@^3.0.0, rimraf@^3.0.2:
|
|||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
ripemd160-min@0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/ripemd160-min/-/ripemd160-min-0.0.6.tgz#a904b77658114474d02503e819dcc55853b67e62"
|
||||
integrity sha512-+GcJgQivhs6S9qvLogusiTcS9kQUfgR75whKuy5jIhuiOfQuJ8fjqxV6EGD5duH1Y/FawFUMtMhyeq3Fbnib8A==
|
||||
|
||||
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
|
||||
|
@ -17561,7 +17883,7 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0:
|
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@^7.2.1, semver@^7.3.2:
|
||||
semver@^7.2.1, semver@^7.3.2, semver@^7.3.4:
|
||||
version "7.3.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
|
||||
integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
|
||||
|
@ -17706,6 +18028,13 @@ sha.js@^2.4.0, sha.js@^2.4.8:
|
|||
inherits "^2.0.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
sha3@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/sha3/-/sha3-2.1.3.tgz#ab05b841b2bce347765db31f57fe2a3134b9fb48"
|
||||
integrity sha512-Io53D4o9qOmf3Ow9p/DoGLQiQHhtuR0ulbyambvRSG+OX5yXExk2yYfvjHtb7AtOyk6K6+sPeK/qaowWc/E/GA==
|
||||
dependencies:
|
||||
buffer "5.6.0"
|
||||
|
||||
shallow-clone@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060"
|
||||
|
@ -17911,6 +18240,13 @@ snapdragon@^0.8.1:
|
|||
source-map-resolve "^0.5.0"
|
||||
use "^3.1.0"
|
||||
|
||||
sntp@1.x.x:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
|
||||
integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=
|
||||
dependencies:
|
||||
hoek "2.x.x"
|
||||
|
||||
sockjs-client@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177"
|
||||
|
@ -18417,6 +18753,11 @@ stringify-object@^3.3.0:
|
|||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
stringstream@~0.0.4:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72"
|
||||
integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==
|
||||
|
||||
strip-ansi@5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f"
|
||||
|
@ -19077,6 +19418,13 @@ tough-cookie@^3.0.1:
|
|||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tough-cookie@~2.3.0:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
|
||||
integrity sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==
|
||||
dependencies:
|
||||
punycode "^1.4.1"
|
||||
|
||||
tr46@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479"
|
||||
|
@ -19203,6 +19551,11 @@ tunnel-agent@^0.6.0:
|
|||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
tunnel-agent@~0.4.1:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
|
||||
integrity sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=
|
||||
|
||||
tunnel@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
|
||||
|
@ -19441,6 +19794,11 @@ universalify@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
|
||||
unorm@^1.3.3:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/unorm/-/unorm-1.6.0.tgz#029b289661fba714f1a9af439eb51d9b16c205af"
|
||||
integrity sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
|
@ -19553,6 +19911,11 @@ url@^0.11.0:
|
|||
punycode "1.3.2"
|
||||
querystring "0.2.0"
|
||||
|
||||
urlgrey@0.4.4:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.yarnpkg.com/urlgrey/-/urlgrey-0.4.4.tgz#892fe95960805e85519f1cd4389f2cb4cbb7652f"
|
||||
integrity sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=
|
||||
|
||||
usb-detection@^4.10.0:
|
||||
version "4.10.0"
|
||||
resolved "https://registry.yarnpkg.com/usb-detection/-/usb-detection-4.10.0.tgz#0f8a3b8965a5e4e7fbee1667971ca97e455ed11f"
|
||||
|
@ -21191,6 +21554,11 @@ xhr2-cookies@1.1.0:
|
|||
dependencies:
|
||||
cookiejar "^2.1.1"
|
||||
|
||||
xhr2@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"
|
||||
integrity sha1-f4dliEdxbbUCYyOBL4GMras4el8=
|
||||
|
||||
xhr@^2.0.4, xhr@^2.2.0, xhr@^2.3.3:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.6.0.tgz#b69d4395e792b4173d6b7df077f0fc5e4e2b249d"
|
||||
|
@ -21211,7 +21579,7 @@ xmlchars@^2.2.0:
|
|||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||
|
||||
xmlhttprequest@1.8.0:
|
||||
xmlhttprequest@1.8.0, xmlhttprequest@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
|
||||
integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=
|
||||
|
|
Loading…
Reference in New Issue