Merge pull request #1828 from gnosis/release/v2.19.0

Release v2.19.0
This commit is contained in:
Daniel Sanchez 2021-02-02 12:26:59 +01:00 committed by GitHub
commit 2f6113d117
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 3775 additions and 1667 deletions

View File

@ -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}

View File

@ -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'
}
```

View File

@ -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",

View File

@ -97,7 +97,7 @@ const Layout: React.FC<Props> = ({
<Header />
</HeaderWrapper>
<BodyWrapper>
<SidebarWrapper>
<SidebarWrapper data-testid="sidebar">
<Sidebar
items={sidebarItems}
safeAddress={safeAddress}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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) {

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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}`

View File

@ -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 = {

View File

@ -51,7 +51,7 @@ const xDai: NetworkConfig = {
WALLETS.AUTHEREUM,
WALLETS.LATTICE,
],
disabledFeatures: [FEATURES.ENS_LOOKUP],
disabledFeatures: [FEATURES.DOMAIN_LOOKUP],
}
export default xDai

View File

@ -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')

View File

@ -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)
})
})

View File

@ -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

View File

@ -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

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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

View File

@ -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) {

View File

@ -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 => {

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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')
}

View File

@ -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)};

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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,

View File

@ -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

View File

@ -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}

View File

@ -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 }}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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'

View File

@ -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 = (

View File

@ -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>
)
}

View File

@ -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

View File

@ -6,7 +6,7 @@ export const styles = createStyles({
padding: `${md} ${lg}`,
justifyContent: 'flex-start',
boxSizing: 'border-box',
maxHeight: '75px',
height: '74px',
},
annotation: {
letterSpacing: '-1px',

View File

@ -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*"

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 })),
)

View File

@ -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 &darr;
</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 &darr;
</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>
)
}

View File

@ -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,

View File

@ -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 (

View File

@ -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 &darr;
</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 &darr;
</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>
)
}

View File

@ -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(

View File

@ -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 &darr;
</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 &darr;
</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 &darr;
</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 &darr;
</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>
)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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>
)
}

View File

@ -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%',
},

View File

@ -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>
</>
)

View File

@ -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>
)
}

View File

@ -30,9 +30,6 @@ export const styles = createStyles({
buttonRow: {
height: '84px',
justifyContent: 'center',
position: 'absolute',
bottom: 0,
width: '100%',
},
inputRow: {
position: 'relative',

View File

@ -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'

View File

@ -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>
)
}

View File

@ -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}`,

View File

@ -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>
)
}

View File

@ -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}`,

View File

@ -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}
/>
)}
</>

View File

@ -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>
</>
)
}

View File

@ -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',
},
})

View File

@ -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)
)
}

View File

@ -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>
)
}

View File

@ -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')
)
}

View File

@ -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,
}
}

View File

@ -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

View File

@ -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
View File

@ -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=