Merge branch 'development' of github.com:gnosis/safe-react into 1013-undefined-error
This commit is contained in:
commit
ca51768b04
|
@ -18,7 +18,6 @@ module.exports = {
|
||||||
'react-hooks/rules-of-hooks': 'error',
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
'react-hooks/exhaustive-deps': 'warn',
|
'react-hooks/exhaustive-deps': 'warn',
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
||||||
'@typescript-eslint/camelcase': 'off',
|
'@typescript-eslint/camelcase': 'off',
|
||||||
'@typescript-eslint/no-var-requires': 'off',
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
'@typescript-eslint/no-empty-function': 'off',
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "safe-react",
|
"name": "safe-react",
|
||||||
"version": "2.1.1",
|
"version": "2.3.1",
|
||||||
"description": "Allowing crypto users manage funds in a safer way",
|
"description": "Allowing crypto users manage funds in a safer way",
|
||||||
"website": "https://github.com/gnosis/safe-react#readme",
|
"website": "https://github.com/gnosis/safe-react#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
@ -144,6 +144,7 @@ const CookiesBanner = () => {
|
||||||
onKeyDown={closeCookiesBannerHandler}
|
onKeyDown={closeCookiesBannerHandler}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
data-testid="accept-preferences"
|
||||||
>
|
>
|
||||||
Accept preferences >
|
Accept preferences >
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -61,7 +61,7 @@ const Layout = openHoc(({ classes, clickAway, open, providerDetails, providerInf
|
||||||
<Row className={classes.summary}>
|
<Row className={classes.summary}>
|
||||||
<Col className={classes.logo} middle="xs" start="xs">
|
<Col className={classes.logo} middle="xs" start="xs">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<Img alt="Gnosis Team Safe" height={32} src={logo} />
|
<Img alt="Gnosis Team Safe" height={32} src={logo} testId="heading-gnosis-logo" />
|
||||||
</Link>
|
</Link>
|
||||||
</Col>
|
</Col>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
|
@ -45,7 +45,7 @@ const ConnectDetails = ({ classes }) => (
|
||||||
<CircleDot center circleSize={75} dotRight={25} dotSize={25} dotTop={50} keySize={32} mode="error" />
|
<CircleDot center circleSize={75} dotRight={25} dotSize={25} dotTop={50} keySize={32} mode="error" />
|
||||||
</Row>
|
</Row>
|
||||||
<Block className={classes.connect}>
|
<Block className={classes.connect}>
|
||||||
<ConnectButton />
|
<ConnectButton data-testid="heading-connect-btn" />
|
||||||
</Block>
|
</Block>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -163,7 +163,14 @@ const UserDetails = ({ classes, connected, network, onDisconnect, openDashboard,
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
<Row className={classes.disconnect}>
|
<Row className={classes.disconnect}>
|
||||||
<Button color="primary" fullWidth onClick={onDisconnect} size="medium" variant="contained">
|
<Button
|
||||||
|
color="primary"
|
||||||
|
fullWidth
|
||||||
|
onClick={onDisconnect}
|
||||||
|
size="medium"
|
||||||
|
variant="contained"
|
||||||
|
data-testid="disconnect-btn"
|
||||||
|
>
|
||||||
<Paragraph className={classes.disconnectText} color="white" noMargin size="md">
|
<Paragraph className={classes.disconnectText} color="white" noMargin size="md">
|
||||||
Disconnect
|
Disconnect
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
|
@ -62,7 +62,14 @@ const ProviderInfo = ({ classes, connected, network, provider, userAddress }) =>
|
||||||
)}
|
)}
|
||||||
{!connected && <CircleDot circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={14} mode="warning" />}
|
{!connected && <CircleDot circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={14} mode="warning" />}
|
||||||
<Col className={classes.account} layout="column" start="sm">
|
<Col className={classes.account} layout="column" start="sm">
|
||||||
<Paragraph className={classes.network} noMargin size="xs" transform="capitalize" weight="bolder">
|
<Paragraph
|
||||||
|
className={classes.network}
|
||||||
|
noMargin
|
||||||
|
size="xs"
|
||||||
|
transform="capitalize"
|
||||||
|
weight="bolder"
|
||||||
|
data-testid="connected-wallet"
|
||||||
|
>
|
||||||
{providerText}
|
{providerText}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph className={classes.address} color={color} noMargin size="xs">
|
<Paragraph className={classes.address} color={color} noMargin size="xs">
|
||||||
|
|
|
@ -29,7 +29,14 @@ const ProviderDisconnected = ({ classes }) => (
|
||||||
<>
|
<>
|
||||||
<CircleDot circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={17} mode="error" />
|
<CircleDot circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={17} mode="error" />
|
||||||
<Col className={classes.account} end="sm" layout="column" middle="xs">
|
<Col className={classes.account} end="sm" layout="column" middle="xs">
|
||||||
<Paragraph className={classes.network} noMargin size="sm" transform="capitalize" weight="bold">
|
<Paragraph
|
||||||
|
className={classes.network}
|
||||||
|
noMargin
|
||||||
|
size="sm"
|
||||||
|
transform="capitalize"
|
||||||
|
weight="bold"
|
||||||
|
data-testid="not-connected-wallet"
|
||||||
|
>
|
||||||
Not Connected
|
Not Connected
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph className={classes.connect} color="fancy" noMargin size="sm">
|
<Paragraph className={classes.connect} color="fancy" noMargin size="sm">
|
||||||
|
|
|
@ -44,7 +44,7 @@ const SafeListHeader = ({ safesCount }) => {
|
||||||
return (
|
return (
|
||||||
<Col className={classes.container} middle="xs" start="xs">
|
<Col className={classes.container} middle="xs" start="xs">
|
||||||
Safes
|
Safes
|
||||||
<Paragraph className={classes.counter} size="xs">
|
<Paragraph className={classes.counter} size="xs" data-testid="safe-counter-heading">
|
||||||
{safesCount}
|
{safesCount}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
@ -39,6 +39,7 @@ export const ScanQRWrapper = (props) => {
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
src={QRIcon}
|
src={QRIcon}
|
||||||
|
testId="qr-icon"
|
||||||
/>
|
/>
|
||||||
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={onScanFinished} />}
|
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={onScanFinished} />}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -58,14 +58,18 @@ export const minValue = (min: number | string) => (value: string) => {
|
||||||
return `Should be at least ${min}`
|
return `Should be at least ${min}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const maxValue = (max: number | string) => (value: string) => {
|
export const maxValueCheck = (max: number | string, value: string): string | undefined => {
|
||||||
if (Number.isNaN(Number(value)) || parseFloat(value) <= parseFloat(max.toString())) {
|
if (!max || Number.isNaN(Number(value)) || parseFloat(value) <= parseFloat(max.toString())) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return `Maximum value is ${max}`
|
return `Maximum value is ${max}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const maxValue = (max: number | string) => (value: string) => {
|
||||||
|
return maxValueCheck(max, value)
|
||||||
|
}
|
||||||
|
|
||||||
export const ok = () => undefined
|
export const ok = () => undefined
|
||||||
|
|
||||||
export const mustBeEthereumAddress = simpleMemoize((address: string) => {
|
export const mustBeEthereumAddress = simpleMemoize((address: string) => {
|
||||||
|
|
|
@ -100,6 +100,7 @@ const Details = ({ classes, errors, form }) => {
|
||||||
text="Safe name"
|
text="Safe name"
|
||||||
type="text"
|
type="text"
|
||||||
validate={required}
|
validate={required}
|
||||||
|
testId="load-safe-name-field"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -114,7 +115,7 @@ const Details = ({ classes, errors, form }) => {
|
||||||
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
|
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<CheckCircle className={classes.check} />
|
<CheckCircle className={classes.check} data-testid="valid-address" />
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -123,6 +124,7 @@ const Details = ({ classes, errors, form }) => {
|
||||||
placeholder="Safe Address*"
|
placeholder="Safe Address*"
|
||||||
text="Safe Address"
|
text="Safe Address"
|
||||||
type="text"
|
type="text"
|
||||||
|
testId="load-safe-address-field"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col center="xs" className={classes} middle="xs" xs={1}>
|
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||||
|
|
|
@ -109,7 +109,7 @@ const OwnerListComponent = (props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Block className={classes.title}>
|
<Block className={classes.title}>
|
||||||
<Paragraph color="primary" noMargin size="md">
|
<Paragraph color="primary" noMargin size="md" data-testid="load-safe-step-two">
|
||||||
{`This Safe has ${owners.length} owners. Optional: Provide a name for each owner.`}
|
{`This Safe has ${owners.length} owners. Optional: Provide a name for each owner.`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -122,7 +122,7 @@ const OwnerListComponent = (props) => {
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Block margin="md" padding="md">
|
<Block margin="md" padding="md">
|
||||||
{owners.map((address, index) => (
|
{owners.map((address, index) => (
|
||||||
<Row className={classes.owner} key={address}>
|
<Row className={classes.owner} key={address} data-testid="owner-row">
|
||||||
<Col className={classes.ownerName} xs={4}>
|
<Col className={classes.ownerName} xs={4}>
|
||||||
<Field
|
<Field
|
||||||
className={classes.name}
|
className={classes.name}
|
||||||
|
@ -133,6 +133,7 @@ const OwnerListComponent = (props) => {
|
||||||
text="Owner Name"
|
text="Owner Name"
|
||||||
type="text"
|
type="text"
|
||||||
validate={required}
|
validate={required}
|
||||||
|
testId={`load-safe-owner-name-${index}`}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={8}>
|
<Col xs={8}>
|
||||||
|
|
|
@ -109,7 +109,7 @@ class ReviewComponent extends React.PureComponent<any> {
|
||||||
<Col className={classes.detailsColumn} layout="column" xs={4}>
|
<Col className={classes.detailsColumn} layout="column" xs={4}>
|
||||||
<Block className={classes.details}>
|
<Block className={classes.details}>
|
||||||
<Block margin="lg">
|
<Block margin="lg">
|
||||||
<Paragraph color="primary" noMargin size="lg">
|
<Paragraph color="primary" noMargin size="lg" data-testid="load-safe-step-three">
|
||||||
Review details
|
Review details
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -117,7 +117,14 @@ class ReviewComponent extends React.PureComponent<any> {
|
||||||
<Paragraph color="disabled" noMargin size="sm">
|
<Paragraph color="disabled" noMargin size="sm">
|
||||||
Name of the Safe
|
Name of the Safe
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
<Paragraph
|
||||||
|
className={classes.name}
|
||||||
|
color="primary"
|
||||||
|
noMargin
|
||||||
|
size="lg"
|
||||||
|
weight="bolder"
|
||||||
|
data-testid="load-form-review-safe-name"
|
||||||
|
>
|
||||||
{values[FIELD_LOAD_NAME]}
|
{values[FIELD_LOAD_NAME]}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -168,7 +175,7 @@ class ReviewComponent extends React.PureComponent<any> {
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={11}>
|
<Col xs={11}>
|
||||||
<Block className={classNames(classes.name, classes.userName)}>
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
<Paragraph noMargin size="lg">
|
<Paragraph noMargin size="lg" data-testid="load-safe-review-owner-name">
|
||||||
{values[getOwnerNameBy(index)]}
|
{values[getOwnerNameBy(index)]}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Block className={classes.user} justify="center">
|
<Block className={classes.user} justify="center">
|
||||||
|
|
|
@ -87,7 +87,9 @@ const Layout = (props) => {
|
||||||
<IconButton disableRipple onClick={back} style={iconStyle}>
|
<IconButton disableRipple onClick={back} style={iconStyle}>
|
||||||
<ChevronLeft />
|
<ChevronLeft />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Heading tag="h2">Create New Safe</Heading>
|
<Heading tag="h2" testId="create-safe-form-title">
|
||||||
|
Create New Safe
|
||||||
|
</Heading>
|
||||||
</Row>
|
</Row>
|
||||||
<Stepper
|
<Stepper
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
|
|
|
@ -112,7 +112,7 @@ const ReviewComponent = ({ classes, userAccount, values }: any) => {
|
||||||
<Col className={classes.detailsColumn} layout="column" xs={4}>
|
<Col className={classes.detailsColumn} layout="column" xs={4}>
|
||||||
<Block className={classes.details}>
|
<Block className={classes.details}>
|
||||||
<Block margin="lg">
|
<Block margin="lg">
|
||||||
<Paragraph color="primary" noMargin size="lg">
|
<Paragraph color="primary" noMargin size="lg" data-testid="create-safe-step-three">
|
||||||
Details
|
Details
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -120,7 +120,14 @@ const ReviewComponent = ({ classes, userAccount, values }: any) => {
|
||||||
<Paragraph color="disabled" noMargin size="sm">
|
<Paragraph color="disabled" noMargin size="sm">
|
||||||
Name of new Safe
|
Name of new Safe
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
<Paragraph
|
||||||
|
className={classes.name}
|
||||||
|
color="primary"
|
||||||
|
noMargin
|
||||||
|
size="lg"
|
||||||
|
weight="bolder"
|
||||||
|
data-testid="create-safe-review-name"
|
||||||
|
>
|
||||||
{values[FIELD_NAME]}
|
{values[FIELD_NAME]}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -128,7 +135,13 @@ const ReviewComponent = ({ classes, userAccount, values }: any) => {
|
||||||
<Paragraph color="disabled" noMargin size="sm">
|
<Paragraph color="disabled" noMargin size="sm">
|
||||||
Any transaction requires the confirmation of:
|
Any transaction requires the confirmation of:
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph color="primary" noMargin size="lg" weight="bolder">
|
<Paragraph
|
||||||
|
color="primary"
|
||||||
|
noMargin
|
||||||
|
size="lg"
|
||||||
|
weight="bolder"
|
||||||
|
data-testid={`create-safe-review-req-owners-${values[FIELD_CONFIRMATIONS]}`}
|
||||||
|
>
|
||||||
{`${values[FIELD_CONFIRMATIONS]} out of ${numOwners} owners`}
|
{`${values[FIELD_CONFIRMATIONS]} out of ${numOwners} owners`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -150,11 +163,16 @@ const ReviewComponent = ({ classes, userAccount, values }: any) => {
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={11}>
|
<Col xs={11}>
|
||||||
<Block className={classNames(classes.name, classes.userName)}>
|
<Block className={classNames(classes.name, classes.userName)}>
|
||||||
<Paragraph noMargin size="lg">
|
<Paragraph noMargin size="lg" data-testid={`create-safe-owner-name-${index}`}>
|
||||||
{name}
|
{name}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Block className={classes.user} justify="center">
|
<Block className={classes.user} justify="center">
|
||||||
<Paragraph color="disabled" noMargin size="md">
|
<Paragraph
|
||||||
|
color="disabled"
|
||||||
|
noMargin
|
||||||
|
size="md"
|
||||||
|
data-testid={`create-safe-owner-address-${index}`}
|
||||||
|
>
|
||||||
{addresses[index]}
|
{addresses[index]}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<CopyBtn content={addresses[index]} />
|
<CopyBtn content={addresses[index]} />
|
||||||
|
|
|
@ -45,6 +45,7 @@ const SafeName = ({ classes, safeName }) => (
|
||||||
text="Safe name"
|
text="Safe name"
|
||||||
type="text"
|
type="text"
|
||||||
validate={required}
|
validate={required}
|
||||||
|
testId="create-safe-name-field"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
<Block margin="lg">
|
<Block margin="lg">
|
||||||
|
|
|
@ -99,7 +99,7 @@ const SafeOwners = (props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Block className={classes.title}>
|
<Block className={classes.title}>
|
||||||
<Paragraph color="primary" noMargin size="md">
|
<Paragraph color="primary" noMargin size="md" data-testid="create-safe-step-two">
|
||||||
Your Safe will have one or more owners. We have prefilled the first owner with your connected wallet details,
|
Your Safe will have one or more owners. We have prefilled the first owner with your connected wallet details,
|
||||||
but you are free to change this to a different owner.
|
but you are free to change this to a different owner.
|
||||||
<br />
|
<br />
|
||||||
|
@ -120,7 +120,7 @@ const SafeOwners = (props) => {
|
||||||
const addressName = getOwnerAddressBy(index)
|
const addressName = getOwnerAddressBy(index)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={classes.owner} key={`owner${index}`}>
|
<Row className={classes.owner} key={`owner${index}`} data-testid={`create-safe-owner-row`}>
|
||||||
<Col className={classes.ownerName} xs={4}>
|
<Col className={classes.ownerName} xs={4}>
|
||||||
<Field
|
<Field
|
||||||
className={classes.name}
|
className={classes.name}
|
||||||
|
@ -130,6 +130,7 @@ const SafeOwners = (props) => {
|
||||||
text="Owner Name"
|
text="Owner Name"
|
||||||
type="text"
|
type="text"
|
||||||
validate={required}
|
validate={required}
|
||||||
|
testId={`create-safe-owner-name-field-${index}`}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className={classes.ownerAddress} xs={6}>
|
<Col className={classes.ownerAddress} xs={6}>
|
||||||
|
@ -142,7 +143,7 @@ const SafeOwners = (props) => {
|
||||||
noErrorsOn(addressName, errors) && {
|
noErrorsOn(addressName, errors) && {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<CheckCircle className={classes.check} />
|
<CheckCircle className={classes.check} data-testid={`valid-address-${index}`} />
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -152,6 +153,7 @@ const SafeOwners = (props) => {
|
||||||
text="Owner Address"
|
text="Owner Address"
|
||||||
type="text"
|
type="text"
|
||||||
validators={[getAddressValidator(otherAccounts, index)]}
|
validators={[getAddressValidator(otherAccounts, index)]}
|
||||||
|
testId={`create-safe-address-field-${index}`}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col center="xs" className={classes.remove} middle="xs" xs={1}>
|
<Col center="xs" className={classes.remove} middle="xs" xs={1}>
|
||||||
|
@ -191,14 +193,20 @@ const SafeOwners = (props) => {
|
||||||
validate={composeValidators(required, mustBeInteger, minValue(1))}
|
validate={composeValidators(required, mustBeInteger, minValue(1))}
|
||||||
>
|
>
|
||||||
{[...Array(Number(validOwners))].map((x, index) => (
|
{[...Array(Number(validOwners))].map((x, index) => (
|
||||||
<MenuItem key={`selectOwner${index}`} value={`${index + 1}`}>
|
<MenuItem key={`selectOwner${index}`} value={`${index + 1}`} data-testid={`input-${index + 1}`}>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Field>
|
</Field>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className={classes.ownersAmountItem} xs={10}>
|
<Col className={classes.ownersAmountItem} xs={10}>
|
||||||
<Paragraph className={classes.owners} color="primary" noMargin size="lg">
|
<Paragraph
|
||||||
|
className={classes.owners}
|
||||||
|
color="primary"
|
||||||
|
noMargin
|
||||||
|
size="lg"
|
||||||
|
data-testid={`create-safe-req-conf-${validOwners}`}
|
||||||
|
>
|
||||||
out of {validOwners} owner(s)
|
out of {validOwners} owner(s)
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -38,7 +38,13 @@ export const ContinueFooter = ({
|
||||||
continueButtonDisabled: boolean
|
continueButtonDisabled: boolean
|
||||||
onContinue: (event: SyntheticEvent) => void
|
onContinue: (event: SyntheticEvent) => void
|
||||||
}) => (
|
}) => (
|
||||||
<Button color="primary" disabled={continueButtonDisabled} onClick={onContinue} variant="contained">
|
<Button
|
||||||
|
color="primary"
|
||||||
|
disabled={continueButtonDisabled}
|
||||||
|
onClick={onContinue}
|
||||||
|
variant="contained"
|
||||||
|
data-testid="continue-btn"
|
||||||
|
>
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
|
@ -302,7 +302,9 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Title tag="h2">Safe creation process</Title>
|
<Title tag="h2" testId="safe-creation-process-title">
|
||||||
|
Safe creation process
|
||||||
|
</Title>
|
||||||
<Nav>
|
<Nav>
|
||||||
<Stepper activeStepIndex={stepIndex} error={error} orientation="vertical" steps={steps} />
|
<Stepper activeStepIndex={stepIndex} error={error} orientation="vertical" steps={steps} />
|
||||||
</Nav>
|
</Nav>
|
||||||
|
@ -336,7 +338,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
|
||||||
) : null}
|
) : null}
|
||||||
</BodyFooter>
|
</BodyFooter>
|
||||||
</Body>
|
</Body>
|
||||||
<BackButton color="primary" minWidth={140} onClick={onCancel}>
|
<BackButton color="primary" minWidth={140} onClick={onCancel} data-testid="safe-creation-back-btn">
|
||||||
Back
|
Back
|
||||||
</BackButton>
|
</BackButton>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { BigNumber } from 'bignumber.js'
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
|
||||||
import AddressInfo from 'src/components/AddressInfo'
|
import AddressInfo from 'src/components/AddressInfo'
|
||||||
import DividerLine from 'src/components/DividerLine'
|
import DividerLine from 'src/components/DividerLine'
|
||||||
import Collapse from 'src/components/Collapse'
|
import Collapse from 'src/components/Collapse'
|
||||||
|
@ -55,17 +54,12 @@ const IconText = styled.div`
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const isTxValid = (t): boolean => {
|
const isTxValid = (t: SafeAppTx): boolean => {
|
||||||
try {
|
if (!['string', 'number'].includes(typeof t.value)) {
|
||||||
if (!['string', 'number'].includes(typeof t.value)) {
|
return false
|
||||||
return false
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof t.value === 'string') {
|
if (typeof t.value === 'string' && !/^\d+$/.test(t.value)) {
|
||||||
const web3 = getWeb3()
|
|
||||||
web3.eth.abi.decodeParameter('uint256', t.value)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,14 @@ const removeLastTrailingSlash = (url) => {
|
||||||
|
|
||||||
const gnosisAppsUrl = removeLastTrailingSlash(getGnosisSafeAppsUrl())
|
const gnosisAppsUrl = removeLastTrailingSlash(getGnosisSafeAppsUrl())
|
||||||
export const staticAppsList: Array<{ url: string; disabled: boolean }> = [
|
export const staticAppsList: Array<{ url: string; disabled: boolean }> = [
|
||||||
|
// Sablier
|
||||||
|
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmabPEk7g4zaytFefp6fE4nz8f85QMJoWmRQQZypvJViNG`, disabled: false },
|
||||||
|
// request
|
||||||
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQapdJP6zERqpDKKPECNeMDDgwmGUqbKk1PjHpYj8gfDJ`, disabled: false },
|
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQapdJP6zERqpDKKPECNeMDDgwmGUqbKk1PjHpYj8gfDJ`, disabled: false },
|
||||||
|
// Aave
|
||||||
|
// { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUfgEqdJ5kVjWTQofnDmvxdhDLBAaejiHkhQhfw6aYvBg`, disabled: false },
|
||||||
{ url: `${gnosisAppsUrl}/compound`, disabled: false },
|
{ url: `${gnosisAppsUrl}/compound`, disabled: false },
|
||||||
{ url: `${gnosisAppsUrl}/tx-builder`, disabled: false },
|
{ url: `${gnosisAppsUrl}/tx-builder`, disabled: false },
|
||||||
{ url: `${gnosisAppsUrl}/aave`, disabled: false },
|
|
||||||
{ url: `${gnosisAppsUrl}/pool-together`, disabled: false },
|
{ url: `${gnosisAppsUrl}/pool-together`, disabled: false },
|
||||||
{ url: `${gnosisAppsUrl}/open-zeppelin`, disabled: false },
|
{ url: `${gnosisAppsUrl}/open-zeppelin`, disabled: false },
|
||||||
{ url: `${gnosisAppsUrl}/synthetix`, disabled: false },
|
{ url: `${gnosisAppsUrl}/synthetix`, disabled: false },
|
||||||
|
|
|
@ -19,6 +19,10 @@ const ContractInteraction = React.lazy(() => import('./screens/ContractInteracti
|
||||||
|
|
||||||
const ContractInteractionReview: any = React.lazy(() => import('./screens/ContractInteraction/Review'))
|
const ContractInteractionReview: any = React.lazy(() => import('./screens/ContractInteraction/Review'))
|
||||||
|
|
||||||
|
const SendCustomTx = React.lazy(() => import('./screens/ContractInteraction/SendCustomTx'))
|
||||||
|
|
||||||
|
const ReviewCustomTx = React.lazy(() => import('./screens/ContractInteraction/ReviewCustomTx'))
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
scalableModalWindow: {
|
scalableModalWindow: {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
|
@ -40,9 +44,11 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType')
|
const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType')
|
||||||
const [tx, setTx] = useState({})
|
const [tx, setTx] = useState({})
|
||||||
|
const [isABI, setIsABI] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveScreen(activeScreenType || 'chooseTxType')
|
setActiveScreen(activeScreenType || 'chooseTxType')
|
||||||
|
setIsABI(true)
|
||||||
setTx({})
|
setTx({})
|
||||||
}, [activeScreenType, isOpen])
|
}, [activeScreenType, isOpen])
|
||||||
|
|
||||||
|
@ -53,9 +59,14 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
|
||||||
setTx(txInfo)
|
setTx(txInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleContractInteractionCreation = (contractInteractionInfo) => {
|
const handleContractInteractionCreation = (contractInteractionInfo: any, submit: boolean): void => {
|
||||||
setTx(contractInteractionInfo)
|
setTx(contractInteractionInfo)
|
||||||
setActiveScreen('contractInteractionReview')
|
if (submit) setActiveScreen('contractInteractionReview')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCustomTxCreation = (customTxInfo: any, submit: boolean): void => {
|
||||||
|
setTx(customTxInfo)
|
||||||
|
if (submit) setActiveScreen('reviewCustomTx')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSendCollectible = (txInfo) => {
|
const handleSendCollectible = (txInfo) => {
|
||||||
|
@ -63,6 +74,10 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
|
||||||
setTx(txInfo)
|
setTx(txInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSwitchMethod = (): void => {
|
||||||
|
setIsABI(!isABI)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
description="Send Tokens Form"
|
description="Send Tokens Form"
|
||||||
|
@ -93,17 +108,32 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
|
||||||
{activeScreen === 'reviewTx' && (
|
{activeScreen === 'reviewTx' && (
|
||||||
<ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx} />
|
<ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx} />
|
||||||
)}
|
)}
|
||||||
{activeScreen === 'contractInteraction' && (
|
{activeScreen === 'contractInteraction' && isABI && (
|
||||||
<ContractInteraction
|
<ContractInteraction
|
||||||
|
isABI={isABI}
|
||||||
|
switchMethod={handleSwitchMethod}
|
||||||
contractAddress={recipientAddress}
|
contractAddress={recipientAddress}
|
||||||
initialValues={tx}
|
initialValues={tx}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onNext={handleContractInteractionCreation}
|
onNext={handleContractInteractionCreation}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeScreen === 'contractInteractionReview' && tx && (
|
{activeScreen === 'contractInteractionReview' && isABI && tx && (
|
||||||
<ContractInteractionReview onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} />
|
<ContractInteractionReview onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} />
|
||||||
)}
|
)}
|
||||||
|
{activeScreen === 'contractInteraction' && !isABI && (
|
||||||
|
<SendCustomTx
|
||||||
|
initialValues={tx}
|
||||||
|
isABI={isABI}
|
||||||
|
switchMethod={handleSwitchMethod}
|
||||||
|
onClose={onClose}
|
||||||
|
onNext={handleCustomTxCreation}
|
||||||
|
contractAddress={recipientAddress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeScreen === 'reviewCustomTx' && (
|
||||||
|
<ReviewCustomTx onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} />
|
||||||
|
)}
|
||||||
{activeScreen === 'sendCollectible' && (
|
{activeScreen === 'sendCollectible' && (
|
||||||
<SendCollectible
|
<SendCollectible
|
||||||
initialValues={tx}
|
initialValues={tx}
|
||||||
|
|
|
@ -88,6 +88,9 @@ const AddressBookInput = ({
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
setADBKList(filteredADBK)
|
setADBKList(filteredADBK)
|
||||||
|
if (!isValidText) {
|
||||||
|
setSelectedEntry({ address: addressValue })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setIsValidForm(isValidText === undefined)
|
setIsValidForm(isValidText === undefined)
|
||||||
setValidationText(isValidText)
|
setValidationText(isValidText)
|
||||||
|
|
|
@ -50,7 +50,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isCurrent = true
|
let isCurrent = true
|
||||||
|
|
||||||
const estimateGas = async () => {
|
const estimateGas = async (): Promise<void> => {
|
||||||
const { fromWei, toBN } = getWeb3().utils
|
const { fromWei, toBN } = getWeb3().utils
|
||||||
const txData = tx.data ? tx.data.trim() : ''
|
const txData = tx.data ? tx.data.trim() : ''
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import { useSnackbar } from 'notistack'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import ArrowDown from '../../assets/arrow-down.svg'
|
||||||
|
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
import CopyBtn from 'src/components/CopyBtn'
|
||||||
|
import EtherscanBtn from 'src/components/EtherscanBtn'
|
||||||
|
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'
|
||||||
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
|
import Row from 'src/components/layout/Row'
|
||||||
|
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||||
|
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gasNew'
|
||||||
|
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||||
|
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
||||||
|
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
|
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
|
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||||
|
import createTransaction from 'src/routes/safe/store/actions/createTransaction'
|
||||||
|
import { safeSelector } from 'src/routes/safe/store/selectors'
|
||||||
|
import { sm } from 'src/theme/variables'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void
|
||||||
|
onPrev: () => void
|
||||||
|
tx: { contractAddress?: string; data?: string; value?: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const ReviewCustomTx = ({ onClose, onPrev, tx }: Props) => {
|
||||||
|
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
|
||||||
|
const classes = useStyles()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const { address: safeAddress } = useSelector(safeSelector)
|
||||||
|
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isCurrent = true
|
||||||
|
|
||||||
|
const estimateGas = async () => {
|
||||||
|
const { fromWei, toBN } = getWeb3().utils
|
||||||
|
const txData = tx.data ? tx.data.trim() : ''
|
||||||
|
|
||||||
|
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.contractAddress, txData)
|
||||||
|
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
|
||||||
|
const formattedGasCosts = formatAmount(gasCostsAsEth)
|
||||||
|
|
||||||
|
if (isCurrent) {
|
||||||
|
setGasCosts(formattedGasCosts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
estimateGas()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCurrent = false
|
||||||
|
}
|
||||||
|
}, [safeAddress, tx.data, tx.contractAddress])
|
||||||
|
|
||||||
|
const submitTx = async (): Promise<void> => {
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const txRecipient = tx.contractAddress
|
||||||
|
const txData = tx.data ? tx.data.trim() : ''
|
||||||
|
const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : '0'
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
createTransaction({
|
||||||
|
safeAddress,
|
||||||
|
to: txRecipient,
|
||||||
|
valueInWei: txValue,
|
||||||
|
txData,
|
||||||
|
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||||
|
enqueueSnackbar,
|
||||||
|
closeSnackbar,
|
||||||
|
} as any),
|
||||||
|
)
|
||||||
|
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
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} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col layout="column" xs={11}>
|
||||||
|
<Block justify="left">
|
||||||
|
<Paragraph noMargin weight="bolder">
|
||||||
|
{tx.contractAddress}
|
||||||
|
</Paragraph>
|
||||||
|
<CopyBtn content={tx.contractAddress} />
|
||||||
|
<EtherscanBtn type="address" value={tx.contractAddress} />
|
||||||
|
</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}
|
||||||
|
{' ETH'}
|
||||||
|
</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>
|
||||||
|
<Row>
|
||||||
|
<Paragraph>
|
||||||
|
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ETH in this wallet to fund this confirmation.`}
|
||||||
|
</Paragraph>
|
||||||
|
</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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReviewCustomTx
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { border, lg, md, secondaryText, sm } from 'src/theme/variables'
|
||||||
|
import { createStyles } from '@material-ui/core'
|
||||||
|
|
||||||
|
export const styles = createStyles({
|
||||||
|
heading: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
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}`,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
marginLeft: sm,
|
||||||
|
},
|
||||||
|
outerData: {
|
||||||
|
borderRadius: '5px',
|
||||||
|
border: `1px solid ${border}`,
|
||||||
|
padding: '11px',
|
||||||
|
minHeight: '21px',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
overflow: 'auto',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'Averta',
|
||||||
|
maxHeight: '100px',
|
||||||
|
letterSpacing: 'normal',
|
||||||
|
fontStretch: 'normal',
|
||||||
|
lineHeight: '1.43',
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
'& > button': {
|
||||||
|
fontFamily: 'Averta',
|
||||||
|
fontSize: md,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submitButton: {
|
||||||
|
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||||
|
marginLeft: '15px',
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,278 @@
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||||
|
import Switch from '@material-ui/core/Switch'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import ArrowDown from '../../assets/arrow-down.svg'
|
||||||
|
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
import QRIcon from 'src/assets/icons/qrcode.svg'
|
||||||
|
import CopyBtn from 'src/components/CopyBtn'
|
||||||
|
import EtherscanBtn from 'src/components/EtherscanBtn'
|
||||||
|
import Identicon from 'src/components/Identicon'
|
||||||
|
import ScanQRModal from 'src/components/ScanQRModal'
|
||||||
|
import Field from 'src/components/forms/Field'
|
||||||
|
import GnoForm from 'src/components/forms/GnoForm'
|
||||||
|
import TextField from 'src/components/forms/TextField'
|
||||||
|
import TextareaField from 'src/components/forms/TextareaField'
|
||||||
|
import { composeValidators, maxValue, mustBeFloat, greaterThan } from 'src/components/forms/validator'
|
||||||
|
import Block from 'src/components/layout/Block'
|
||||||
|
import Button from 'src/components/layout/Button'
|
||||||
|
import ButtonLink from 'src/components/layout/ButtonLink'
|
||||||
|
import Col from 'src/components/layout/Col'
|
||||||
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
|
import Img from 'src/components/layout/Img'
|
||||||
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
|
import Row from 'src/components/layout/Row'
|
||||||
|
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
|
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||||
|
import { safeSelector } from 'src/routes/safe/store/selectors'
|
||||||
|
import { sm } from 'src/theme/variables'
|
||||||
|
|
||||||
|
export interface CreatedTx {
|
||||||
|
contractAddress: string
|
||||||
|
data: string
|
||||||
|
value: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialValues: { contractAddress?: string }
|
||||||
|
onClose: () => void
|
||||||
|
onNext: (tx: CreatedTx, submit: boolean) => void
|
||||||
|
isABI: boolean
|
||||||
|
switchMethod: () => void
|
||||||
|
contractAddress: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contractAddress, switchMethod, isABI }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const { ethBalance } = useSelector(safeSelector)
|
||||||
|
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
|
||||||
|
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({
|
||||||
|
address: contractAddress || initialValues.contractAddress,
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
|
const [isValidAddress, setIsValidAddress] = useState<boolean>(true)
|
||||||
|
|
||||||
|
const saveForm = async (values) => {
|
||||||
|
await handleSubmit(values, false)
|
||||||
|
switchMethod()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (values: any, submit = true) => {
|
||||||
|
if (values.data || values.value) {
|
||||||
|
onNext(values, submit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openQrModal = () => {
|
||||||
|
setQrModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeQrModal = () => {
|
||||||
|
setQrModalOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formMutators = {
|
||||||
|
setMax: (args, state, utils) => {
|
||||||
|
utils.changeValue(state, 'value', () => ethBalance)
|
||||||
|
},
|
||||||
|
setRecipient: (args, state, utils) => {
|
||||||
|
utils.changeValue(state, 'contractAddress', () => args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row align="center" className={classes.heading} grow>
|
||||||
|
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||||
|
Send custom transactions
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>1 of 2</Paragraph>
|
||||||
|
<IconButton disableRipple onClick={onClose}>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<GnoForm
|
||||||
|
formMutators={formMutators}
|
||||||
|
initialValues={initialValues}
|
||||||
|
subscription={{ submitting: true, pristine: true, values: true }}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
{(...args) => {
|
||||||
|
const mutators = args[3]
|
||||||
|
const pristine = args[2].pristine
|
||||||
|
let shouldDisableSubmitButton = !isValidAddress
|
||||||
|
if (selectedEntry) {
|
||||||
|
shouldDisableSubmitButton = !selectedEntry.address
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScan = (value) => {
|
||||||
|
let scannedAddress = value
|
||||||
|
|
||||||
|
if (scannedAddress.startsWith('ethereum:')) {
|
||||||
|
scannedAddress = scannedAddress.replace('ethereum:', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
mutators.setRecipient(scannedAddress)
|
||||||
|
closeQrModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Block className={classes.formContainer}>
|
||||||
|
<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>
|
||||||
|
{selectedEntry && selectedEntry.address ? (
|
||||||
|
<div
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.keyCode !== 9) {
|
||||||
|
setSelectedEntry(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="listbox"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<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={selectedEntry.address} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col layout="column" xs={11}>
|
||||||
|
<Block justify="left">
|
||||||
|
<Block>
|
||||||
|
<Paragraph
|
||||||
|
className={classes.selectAddress}
|
||||||
|
noMargin
|
||||||
|
onClick={() => setSelectedEntry(null)}
|
||||||
|
weight="bolder"
|
||||||
|
>
|
||||||
|
{selectedEntry.name}
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph
|
||||||
|
className={classes.selectAddress}
|
||||||
|
noMargin
|
||||||
|
onClick={() => setSelectedEntry(null)}
|
||||||
|
weight="bolder"
|
||||||
|
>
|
||||||
|
{selectedEntry.address}
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<CopyBtn content={selectedEntry.address} />
|
||||||
|
<EtherscanBtn type="address" value={selectedEntry.address} />
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={11}>
|
||||||
|
<AddressBookInput
|
||||||
|
fieldMutator={mutators.setRecipient}
|
||||||
|
isCustomTx
|
||||||
|
pristine={pristine}
|
||||||
|
setIsValidAddress={setIsValidAddress}
|
||||||
|
setSelectedEntry={setSelectedEntry}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||||
|
<Img
|
||||||
|
alt="Scan QR"
|
||||||
|
className={classes.qrCodeBtn}
|
||||||
|
height={20}
|
||||||
|
onClick={() => {
|
||||||
|
openQrModal()
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
src={QRIcon}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Row margin="xs">
|
||||||
|
<Col between="lg">
|
||||||
|
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||||
|
Value
|
||||||
|
</Paragraph>
|
||||||
|
<ButtonLink onClick={mutators.setMax} weight="bold">
|
||||||
|
Send max
|
||||||
|
</ButtonLink>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col>
|
||||||
|
<Field
|
||||||
|
component={TextField}
|
||||||
|
inputAdornment={{
|
||||||
|
endAdornment: <InputAdornment position="end">ETH</InputAdornment>,
|
||||||
|
}}
|
||||||
|
name="value"
|
||||||
|
placeholder="Value*"
|
||||||
|
text="Value*"
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(mustBeFloat, maxValue(ethBalance), greaterThan(0))}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="sm">
|
||||||
|
<Col>
|
||||||
|
<TextareaField
|
||||||
|
name="data"
|
||||||
|
placeholder="Data (hex encoded)*"
|
||||||
|
text="Data (hex encoded)*"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||||
|
Use custom data (hex encoded)
|
||||||
|
<Switch onChange={() => saveForm(args[2].values)} checked={!isABI} />
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button minWidth={140} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={classes.submitButton}
|
||||||
|
color="primary"
|
||||||
|
data-testid="review-tx-btn"
|
||||||
|
disabled={shouldDisableSubmitButton}
|
||||||
|
minWidth={140}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Review
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</GnoForm>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SendCustomTx
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { lg, md } from 'src/theme/variables'
|
||||||
|
import { createStyles } from '@material-ui/core'
|
||||||
|
|
||||||
|
export const styles = createStyles({
|
||||||
|
heading: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '75px',
|
||||||
|
},
|
||||||
|
annotation: {
|
||||||
|
letterSpacing: '-1px',
|
||||||
|
color: '#a2a8ba',
|
||||||
|
marginRight: 'auto',
|
||||||
|
marginLeft: '20px',
|
||||||
|
},
|
||||||
|
manage: {
|
||||||
|
fontSize: lg,
|
||||||
|
},
|
||||||
|
closeIcon: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
qrCodeBtn: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
formContainer: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
'& > button': {
|
||||||
|
fontFamily: 'Averta',
|
||||||
|
fontSize: md,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submitButton: {
|
||||||
|
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||||
|
marginLeft: '15px',
|
||||||
|
},
|
||||||
|
dataInput: {
|
||||||
|
'& TextField-root-294': {
|
||||||
|
lineHeight: 'auto',
|
||||||
|
border: 'green',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectAddress: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,13 +1,14 @@
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
import Switch from '@material-ui/core/Switch'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
import GnoForm from 'src/components/forms/GnoForm'
|
import GnoForm from 'src/components/forms/GnoForm'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
import { safeSelector } from 'src/routes/safe/store/selectors'
|
import { safeSelector } from 'src/routes/safe/store/selectors'
|
||||||
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Buttons from './Buttons'
|
import Buttons from './Buttons'
|
||||||
import ContractABI from './ContractABI'
|
import ContractABI from './ContractABI'
|
||||||
import EthAddressInput from './EthAddressInput'
|
import EthAddressInput from './EthAddressInput'
|
||||||
|
@ -33,11 +34,20 @@ export interface CreatedTx {
|
||||||
export interface ContractInteractionProps {
|
export interface ContractInteractionProps {
|
||||||
contractAddress: string
|
contractAddress: string
|
||||||
initialValues: { contractAddress?: string }
|
initialValues: { contractAddress?: string }
|
||||||
|
isABI: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onNext: (tx: CreatedTx) => void
|
switchMethod: () => void
|
||||||
|
onNext: (tx: CreatedTx, submit: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }: ContractInteractionProps) => {
|
const ContractInteraction: React.FC<ContractInteractionProps> = ({
|
||||||
|
contractAddress,
|
||||||
|
initialValues,
|
||||||
|
onClose,
|
||||||
|
onNext,
|
||||||
|
switchMethod,
|
||||||
|
isABI,
|
||||||
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const { address: safeAddress = '' } = useSelector(safeSelector)
|
const { address: safeAddress = '' } = useSelector(safeSelector)
|
||||||
let setCallResults
|
let setCallResults
|
||||||
|
@ -48,13 +58,21 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
||||||
}
|
}
|
||||||
}, [contractAddress, initialValues.contractAddress])
|
}, [contractAddress, initialValues.contractAddress])
|
||||||
|
|
||||||
const handleSubmit = async ({ contractAddress, selectedMethod, value, ...values }) => {
|
const saveForm = async (values: CreatedTx): Promise<void> => {
|
||||||
|
await handleSubmit(values, false)
|
||||||
|
switchMethod()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (
|
||||||
|
{ contractAddress, selectedMethod, value, ...values },
|
||||||
|
submit = true,
|
||||||
|
): Promise<void | any> => {
|
||||||
if (value || (contractAddress && selectedMethod)) {
|
if (value || (contractAddress && selectedMethod)) {
|
||||||
try {
|
try {
|
||||||
const txObject = createTxObject(selectedMethod, contractAddress, values)
|
const txObject = createTxObject(selectedMethod, contractAddress, values)
|
||||||
const data = txObject.encodeABI()
|
const data = txObject.encodeABI()
|
||||||
|
|
||||||
if (isReadMethod(selectedMethod)) {
|
if (isReadMethod(selectedMethod) && submit) {
|
||||||
const result = await txObject.call({ from: safeAddress })
|
const result = await txObject.call({ from: safeAddress })
|
||||||
setCallResults(result)
|
setCallResults(result)
|
||||||
|
|
||||||
|
@ -62,7 +80,7 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
onNext({ ...values, contractAddress, data, selectedMethod, value })
|
onNext({ ...values, contractAddress, data, selectedMethod, value }, submit)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleSubmitError(error, values)
|
return handleSubmitError(error, values)
|
||||||
}
|
}
|
||||||
|
@ -78,7 +96,7 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
||||||
formMutators={formMutators}
|
formMutators={formMutators}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
subscription={{ submitting: true, pristine: true }}
|
subscription={{ submitting: true, pristine: true, values: true }}
|
||||||
>
|
>
|
||||||
{(submitting, validating, rest, mutators) => {
|
{(submitting, validating, rest, mutators) => {
|
||||||
setCallResults = mutators.setCallResults
|
setCallResults = mutators.setCallResults
|
||||||
|
@ -99,6 +117,10 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
||||||
<RenderInputParams />
|
<RenderInputParams />
|
||||||
<RenderOutputParams />
|
<RenderOutputParams />
|
||||||
<FormErrorMessage />
|
<FormErrorMessage />
|
||||||
|
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||||
|
Use custom data (hex encoded)
|
||||||
|
<Switch checked={!isABI} onChange={() => saveForm(rest.values)} />
|
||||||
|
</Paragraph>
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Buttons onClose={onClose} />
|
<Buttons onClose={onClose} />
|
||||||
|
|
|
@ -17,7 +17,14 @@ import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
import GnoForm from 'src/components/forms/GnoForm'
|
import GnoForm from 'src/components/forms/GnoForm'
|
||||||
import TextField from 'src/components/forms/TextField'
|
import TextField from 'src/components/forms/TextField'
|
||||||
import { composeValidators, greaterThan, maxValue, mustBeFloat, required } from 'src/components/forms/validator'
|
import {
|
||||||
|
composeValidators,
|
||||||
|
greaterThan,
|
||||||
|
maxValue,
|
||||||
|
maxValueCheck,
|
||||||
|
mustBeFloat,
|
||||||
|
required,
|
||||||
|
} from 'src/components/forms/validator'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Button from 'src/components/layout/Button'
|
import Button from 'src/components/layout/Button'
|
||||||
import ButtonLink from 'src/components/layout/ButtonLink'
|
import ButtonLink from 'src/components/layout/ButtonLink'
|
||||||
|
@ -39,7 +46,7 @@ const formMutators = {
|
||||||
utils.changeValue(state, 'amount', () => args[0])
|
utils.changeValue(state, 'amount', () => args[0])
|
||||||
},
|
},
|
||||||
onTokenChange: (args, state, utils) => {
|
onTokenChange: (args, state, utils) => {
|
||||||
utils.changeValue(state, 'amount', () => '')
|
utils.changeValue(state, 'amount', () => state.formState.values.amount)
|
||||||
},
|
},
|
||||||
setRecipient: (args, state, utils) => {
|
setRecipient: (args, state, utils) => {
|
||||||
utils.changeValue(state, 'recipientAddress', () => args[0])
|
utils.changeValue(state, 'recipientAddress', () => args[0])
|
||||||
|
@ -56,6 +63,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
|
||||||
address: recipientAddress || initialValues.recipientAddress,
|
address: recipientAddress || initialValues.recipientAddress,
|
||||||
name: '',
|
name: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const [pristine, setPristine] = useState(true)
|
const [pristine, setPristine] = useState(true)
|
||||||
const [isValidAddress, setIsValidAddress] = useState(true)
|
const [isValidAddress, setIsValidAddress] = useState(true)
|
||||||
|
|
||||||
|
@ -86,7 +94,18 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<GnoForm formMutators={formMutators} initialValues={initialValues} onSubmit={handleSubmit}>
|
<GnoForm
|
||||||
|
formMutators={formMutators}
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
validation={(values) => {
|
||||||
|
const selectedTokenRecord = tokens.find((token) => token.address === values?.token)
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount: maxValueCheck(selectedTokenRecord?.balance, values.amount),
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
{(...args) => {
|
{(...args) => {
|
||||||
const formState = args[2]
|
const formState = args[2]
|
||||||
const mutators = args[3]
|
const mutators = args[3]
|
||||||
|
@ -224,11 +243,15 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
|
||||||
required,
|
required,
|
||||||
mustBeFloat,
|
mustBeFloat,
|
||||||
greaterThan(0),
|
greaterThan(0),
|
||||||
maxValue(selectedTokenRecord && selectedTokenRecord.balance),
|
maxValue(selectedTokenRecord?.balance),
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<OnChange name="token">
|
<OnChange name="token">
|
||||||
{() => {
|
{() => {
|
||||||
|
setSelectedEntry({
|
||||||
|
name: selectedEntry?.name,
|
||||||
|
address: selectedEntry?.address,
|
||||||
|
})
|
||||||
mutators.onTokenChange()
|
mutators.onTokenChange()
|
||||||
}}
|
}}
|
||||||
</OnChange>
|
</OnChange>
|
||||||
|
|
|
@ -38,7 +38,13 @@ const LayoutHeader = (props) => {
|
||||||
{!granted && <Block className={classes.readonly}>Read Only</Block>}
|
{!granted && <Block className={classes.readonly}>Read Only</Block>}
|
||||||
</Row>
|
</Row>
|
||||||
<Block className={classes.user} justify="center">
|
<Block className={classes.user} justify="center">
|
||||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
<Paragraph
|
||||||
|
className={classes.address}
|
||||||
|
color="disabled"
|
||||||
|
noMargin
|
||||||
|
size="md"
|
||||||
|
data-testid="safe-address-heading"
|
||||||
|
>
|
||||||
{address}
|
{address}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<CopyBtn content={address} />
|
<CopyBtn content={address} />
|
||||||
|
@ -54,6 +60,7 @@ const LayoutHeader = (props) => {
|
||||||
onClick={() => showSendFunds('')}
|
onClick={() => showSendFunds('')}
|
||||||
size="small"
|
size="small"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
testId="main-send-btn"
|
||||||
>
|
>
|
||||||
<CallMade
|
<CallMade
|
||||||
alt="Send Transaction"
|
alt="Send Transaction"
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
ADDRESS_BOOK_TAB_BTN_TEST_ID,
|
ADDRESS_BOOK_TAB_BTN_TEST_ID,
|
||||||
BALANCES_TAB_BTN_TEST_ID,
|
BALANCES_TAB_BTN_TEST_ID,
|
||||||
SETTINGS_TAB_BTN_TEST_ID,
|
SETTINGS_TAB_BTN_TEST_ID,
|
||||||
|
APPS_TAB_BTN_TEST_ID,
|
||||||
TRANSACTIONS_TAB_BTN_TEST_ID,
|
TRANSACTIONS_TAB_BTN_TEST_ID,
|
||||||
} from 'src/routes/safe/components/Layout'
|
} from 'src/routes/safe/components/Layout'
|
||||||
import SettingsTab from 'src/routes/safe/components/Layout/Tabs/SettingsTab'
|
import SettingsTab from 'src/routes/safe/components/Layout/Tabs/SettingsTab'
|
||||||
|
@ -105,7 +106,7 @@ const TabsComponent = (props: Props) => {
|
||||||
selected: classes.tabWrapperSelected,
|
selected: classes.tabWrapperSelected,
|
||||||
wrapper: classes.tabWrapper,
|
wrapper: classes.tabWrapper,
|
||||||
}}
|
}}
|
||||||
data-testid={TRANSACTIONS_TAB_BTN_TEST_ID}
|
data-testid={APPS_TAB_BTN_TEST_ID}
|
||||||
label={AppsLabel}
|
label={AppsLabel}
|
||||||
value={`${match.url}/apps`}
|
value={`${match.url}/apps`}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { wrapInSuspense } from 'src/utils/wrapInSuspense'
|
||||||
|
|
||||||
export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn'
|
export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn'
|
||||||
export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn'
|
export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn'
|
||||||
|
export const APPS_TAB_BTN_TEST_ID = 'apps-tab-btn'
|
||||||
export const TRANSACTIONS_TAB_BTN_TEST_ID = 'transactions-tab-btn'
|
export const TRANSACTIONS_TAB_BTN_TEST_ID = 'transactions-tab-btn'
|
||||||
export const ADDRESS_BOOK_TAB_BTN_TEST_ID = 'address-book-tab-btn'
|
export const ADDRESS_BOOK_TAB_BTN_TEST_ID = 'address-book-tab-btn'
|
||||||
export const SAFE_VIEW_NAME_HEADING_TEST_ID = 'safe-name-heading'
|
export const SAFE_VIEW_NAME_HEADING_TEST_ID = 'safe-name-heading'
|
||||||
|
|
|
@ -18,15 +18,13 @@ export const getTxData = (tx) => {
|
||||||
const { to } = tx.decodedParams.transfer
|
const { to } = tx.decodedParams.transfer
|
||||||
txData.recipient = to
|
txData.recipient = to
|
||||||
txData.isTokenTransfer = true
|
txData.isTokenTransfer = true
|
||||||
}
|
} else if (tx.isCollectibleTransfer) {
|
||||||
if (tx.isCollectibleTransfer) {
|
|
||||||
const { safeTransferFrom, transfer, transferFrom } = tx.decodedParams
|
const { safeTransferFrom, transfer, transferFrom } = tx.decodedParams
|
||||||
const { to, value } = safeTransferFrom || transferFrom || transfer
|
const { to, value } = safeTransferFrom || transferFrom || transfer
|
||||||
txData.recipient = to
|
txData.recipient = to
|
||||||
txData.tokenId = value
|
txData.tokenId = value
|
||||||
txData.isCollectibleTransfer = true
|
txData.isCollectibleTransfer = true
|
||||||
}
|
} else if (tx.modifySettingsTx) {
|
||||||
if (tx.modifySettingsTx) {
|
|
||||||
txData.recipient = tx.recipient
|
txData.recipient = tx.recipient
|
||||||
txData.modifySettingsTx = true
|
txData.modifySettingsTx = true
|
||||||
|
|
||||||
|
@ -50,11 +48,12 @@ export const getTxData = (tx) => {
|
||||||
txData.removedOwner = oldOwner
|
txData.removedOwner = oldOwner
|
||||||
txData.addedOwner = newOwner
|
txData.addedOwner = newOwner
|
||||||
}
|
}
|
||||||
}
|
} else if (tx.multiSendTx) {
|
||||||
if (tx.multiSendTx) {
|
|
||||||
txData.recipient = tx.recipient
|
txData.recipient = tx.recipient
|
||||||
txData.data = tx.data
|
txData.data = tx.data
|
||||||
txData.customTx = true
|
txData.customTx = true
|
||||||
|
} else {
|
||||||
|
txData.recipient = tx.recipient
|
||||||
}
|
}
|
||||||
} else if (tx.customTx) {
|
} else if (tx.customTx) {
|
||||||
txData.recipient = tx.recipient
|
txData.recipient = tx.recipient
|
||||||
|
|
|
@ -35,6 +35,7 @@ export const CreateSafe = ({ provider, size }: any) => (
|
||||||
size={size || 'medium'}
|
size={size || 'medium'}
|
||||||
to={OPEN_ADDRESS}
|
to={OPEN_ADDRESS}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
testId="create-new-safe-btn"
|
||||||
>
|
>
|
||||||
<Img alt="Safe" height={14} src={plus} />
|
<Img alt="Safe" height={14} src={plus} />
|
||||||
<div style={buttonStyle}>Create new Safe</div>
|
<div style={buttonStyle}>Create new Safe</div>
|
||||||
|
@ -50,6 +51,7 @@ export const LoadSafe = ({ provider, size }) => (
|
||||||
size={size || 'medium'}
|
size={size || 'medium'}
|
||||||
to={LOAD_ADDRESS}
|
to={LOAD_ADDRESS}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
testId="load-existing-safe-btn"
|
||||||
>
|
>
|
||||||
<Img alt="Safe" height={14} src={safe} />
|
<Img alt="Safe" height={14} src={safe} />
|
||||||
<div style={buttonStyle}>Load existing Safe</div>
|
<div style={buttonStyle}>Load existing Safe</div>
|
||||||
|
@ -108,7 +110,7 @@ const Welcome = ({ isOldMultisigMigration, provider }: any) => {
|
||||||
<Heading align="center" margin="md" tag="h3">
|
<Heading align="center" margin="md" tag="h3">
|
||||||
Get Started by Connecting a Wallet
|
Get Started by Connecting a Wallet
|
||||||
</Heading>
|
</Heading>
|
||||||
<ConnectButton minHeight={42} minWidth={240} />
|
<ConnectButton minHeight={42} minWidth={240} data-testid="connect-btn" />
|
||||||
</Block>
|
</Block>
|
||||||
)}
|
)}
|
||||||
</Block>
|
</Block>
|
||||||
|
|
Loading…
Reference in New Issue