Fix trezor connect
This commit is contained in:
commit
51f6c9a3e8
|
@ -18,15 +18,14 @@ REACT_APP_SQUARELINK_ID=
|
|||
REACT_APP_FORTMATIC_KEY=
|
||||
REACT_APP_OPENSEA_API_KEY=
|
||||
REACT_APP_COLLECTIBLES_SOURCE=
|
||||
REACT_APP_ETHERSCAN_API_KEY=
|
||||
|
||||
# Versions
|
||||
REACT_APP_LATEST_SAFE_VERSION=
|
||||
|
||||
# Leave it untouched, version will set using dotenv-expand
|
||||
REACT_APP_APP_VERSION=$npm_package_version
|
||||
|
||||
# all environments
|
||||
REACT_APP_INFURA_TOKEN=
|
||||
|
||||
# For Apps
|
||||
# For Apps
|
||||
REACT_APP_GNOSIS_APPS_URL=https://safe-apps.staging.gnosisdev.com
|
||||
REACT_APP_APPS_DISABLED=false
|
||||
|
|
|
@ -3,10 +3,9 @@ name: Build/Release Desktop app
|
|||
# this will help you specify where to run
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# this will run on the specified branch
|
||||
- master
|
||||
- development
|
||||
branches-ignore:
|
||||
# Temporary disable action
|
||||
- '**'
|
||||
|
||||
env:
|
||||
REACT_APP_BLOCKNATIVE_KEY: ${{ secrets.REACT_APP_BLOCKNATIVE_KEY }}
|
||||
|
|
60
package.json
60
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "safe-react",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.1",
|
||||
"description": "Allowing crypto users manage funds in a safer way",
|
||||
"homepage": "https://github.com/gnosis/safe-react#readme",
|
||||
"bugs": {
|
||||
|
@ -25,7 +25,7 @@
|
|||
"electron-dev": "concurrently \"BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electron .\"",
|
||||
"preelectron-pack": "yarn build",
|
||||
"build-mainnet": "cross-env REACT_APP_NETWORK=mainnet yarn build",
|
||||
"build-desktop": "cross-env REACT_APP_BUILD_FOR_DESKTOP=true yarn build-mainnet",
|
||||
"build-desktop": "cross-env REACT_APP_BUILD_FOR_DESKTOP=true REACT_APP_ENV=production yarn build-mainnet",
|
||||
"flow": "flow",
|
||||
"format:staged": "lint-staged",
|
||||
"lint:check": "eslint './src/**/*.{js,jsx}'",
|
||||
|
@ -136,42 +136,44 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
||||
"@gnosis.pm/safe-react-components": "^0.1.0",
|
||||
"@gnosis.pm/util-contracts": "2.0.6",
|
||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#a057248",
|
||||
"@ledgerhq/hw-transport-node-hid": "5.12.0",
|
||||
"@material-ui/core": "4.9.10",
|
||||
"@ledgerhq/hw-transport-node-hid": "5.15.0",
|
||||
"@material-ui/core": "4.9.14",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.39",
|
||||
"@openzeppelin/contracts": "^2.5.0",
|
||||
"@testing-library/jest-dom": "5.5.0",
|
||||
"@welldone-software/why-did-you-render": "4.0.8",
|
||||
"@openzeppelin/contracts": "3.0.1",
|
||||
"@testing-library/jest-dom": "5.7.0",
|
||||
"@welldone-software/why-did-you-render": "4.2.1",
|
||||
"async-sema": "^3.1.0",
|
||||
"axios": "0.19.2",
|
||||
"bignumber.js": "9.0.0",
|
||||
"bnc-onboard": "1.9.0",
|
||||
"connected-react-router": "6.8.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"currency-flags": "^2.1.1",
|
||||
"date-fns": "2.12.0",
|
||||
"currency-flags": "2.1.2",
|
||||
"date-fns": "2.13.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"electron-is-dev": "^1.1.0",
|
||||
"electron-log": "^4.1.1",
|
||||
"electron-updater": "4.2.0",
|
||||
"electron-log": "4.1.2",
|
||||
"electron-updater": "4.3.1",
|
||||
"ethereum-ens": "0.8.0",
|
||||
"express": "^4.17.1",
|
||||
"final-form": "4.19.1",
|
||||
"final-form-calculate": "^1.3.1",
|
||||
"history": "4.10.1",
|
||||
"immortal-db": "^1.0.2",
|
||||
"immutable": "^4.0.0-rc.9",
|
||||
"install": "^0.13.0",
|
||||
"js-cookie": "^2.2.1",
|
||||
"lint-staged": "10.1.3",
|
||||
"lint-staged": "10.2.2",
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"material-ui-search-bar": "^1.0.0-beta.13",
|
||||
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
|
||||
"npm": "^6.14.4",
|
||||
"npm": "6.14.5",
|
||||
"open": "^7.0.3",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||
"polished": "3.5.1",
|
||||
"polished": "3.6.3",
|
||||
"qrcode.react": "1.0.0",
|
||||
"query-string": "6.12.1",
|
||||
"react": "16.13.1",
|
||||
|
@ -180,10 +182,10 @@
|
|||
"react-final-form": "6.4.0",
|
||||
"react-final-form-listeners": "^1.0.2",
|
||||
"react-ga": "^2.7.0",
|
||||
"react-hot-loader": "4.12.20",
|
||||
"react-hot-loader": "4.12.21",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-redux": "7.2.0",
|
||||
"react-router-dom": "5.1.2",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-window": "^1.8.5",
|
||||
"recompose": "^0.30.0",
|
||||
"redux": "4.0.5",
|
||||
|
@ -192,8 +194,8 @@
|
|||
"reselect": "^4.0.0",
|
||||
"semver": "7.3.2",
|
||||
"styled-components": "^5.0.1",
|
||||
"wait-on": "^4.0.1",
|
||||
"web3": "1.2.6"
|
||||
"wait-on": "5.0.0",
|
||||
"web3": "1.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.8.4",
|
||||
|
@ -220,13 +222,13 @@
|
|||
"@babel/preset-env": "7.9.5",
|
||||
"@babel/preset-flow": "7.9.0",
|
||||
"@babel/preset-react": "7.9.4",
|
||||
"@testing-library/react": "10.0.2",
|
||||
"@testing-library/react": "10.0.3",
|
||||
"autoprefixer": "9.7.6",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-jest": "25.3.0",
|
||||
"babel-jest": "25.4.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.0",
|
||||
"babel-plugin-dynamic-import-node": "2.3.3",
|
||||
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
|
||||
"babel-plugin-transform-es3-property-literals": "^6.22.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
|
@ -239,37 +241,37 @@
|
|||
"electron-builder": "22.2.0",
|
||||
"electron-notarize": "^0.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "6.10.1",
|
||||
"eslint-config-prettier": "6.11.0",
|
||||
"eslint-plugin-flowtype": "4.7.0",
|
||||
"eslint-plugin-import": "2.20.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-react": "^7.18.3",
|
||||
"eslint-plugin-sort-destructure-keys": "^1.3.3",
|
||||
"eslint-plugin-sort-destructure-keys": "1.3.4",
|
||||
"ethereumjs-abi": "0.6.8",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"file-loader": "6.0.0",
|
||||
"flow-bin": "0.122.0",
|
||||
"flow-bin": "0.123.0",
|
||||
"fs-extra": "9.0.0",
|
||||
"html-loader": "1.1.0",
|
||||
"html-webpack-plugin": "4.2.0",
|
||||
"husky": "^4.2.2",
|
||||
"jest": "25.3.0",
|
||||
"jest": "25.4.0",
|
||||
"jest-dom": "4.0.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"mini-css-extract-plugin": "0.9.0",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-mixins": "6.2.3",
|
||||
"postcss-simple-vars": "^5.0.2",
|
||||
"prettier": "2.0.4",
|
||||
"prettier": "2.0.5",
|
||||
"run-with-testrpc": "0.3.1",
|
||||
"style-loader": "1.1.4",
|
||||
"terser-webpack-plugin": "2.3.5",
|
||||
"truffle": "5.1.21",
|
||||
"truffle": "5.1.23",
|
||||
"truffle-contract": "4.0.31",
|
||||
"truffle-solidity-loader": "0.1.32",
|
||||
"url-loader": "4.1.0",
|
||||
"webpack": "4.42.1",
|
||||
"webpack": "4.43.0",
|
||||
"webpack-bundle-analyzer": "3.7.0",
|
||||
"webpack-cli": "3.3.11",
|
||||
"webpack-dev-server": "3.10.3",
|
||||
|
|
|
@ -24,7 +24,7 @@ function init(mainWindow) {
|
|||
initialized = true;
|
||||
|
||||
autoUpdater.on('error', (error) => {
|
||||
dialog.showErrorBox('Error: ', error == null ? "unknown" : (error.stack || error).toString());
|
||||
log.error(error == null ? "unknown" : (error.stack || error).toString());
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="21" viewBox="0 0 13 21">
|
||||
<path fill="#B2B5B2" fill-rule="evenodd" d="M8.7 11.266V0H4.27v11.266H0l6.484 9.172 6.493-9.172z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 195 B |
|
@ -1,31 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ArrowDown from './arrow-down.svg'
|
||||
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import { md, sm } from '~/theme/variables'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: ${md} 0;
|
||||
|
||||
img {
|
||||
margin: 0 ${sm};
|
||||
}
|
||||
`
|
||||
|
||||
type Props = {
|
||||
withArrow: boolean,
|
||||
}
|
||||
|
||||
const DividerLine = ({ withArrow }: Props) => (
|
||||
<Wrapper>
|
||||
{withArrow && <img alt="Arrow Down" src={ArrowDown} />}
|
||||
<Hairline />
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
export default DividerLine
|
|
@ -1,21 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { border } from '~/theme/variables'
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
}
|
||||
|
||||
const Box = styled.p`
|
||||
padding: 10px;
|
||||
word-wrap: break-word;
|
||||
border: solid 2px ${border};
|
||||
`
|
||||
|
||||
const TextBox = ({ children }: Props) => {
|
||||
return <Box>{children}</Box>
|
||||
}
|
||||
|
||||
export default TextBox
|
|
@ -1,4 +0,0 @@
|
|||
// @flow
|
||||
export { default as DividerLine } from './DividerLine'
|
||||
export { default as TextBox } from './TextBox'
|
||||
export { default as IconText } from './IconText'
|
|
@ -1,24 +0,0 @@
|
|||
// @flow
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-content: ${({ centered }) => (centered ? 'center' : 'start')};
|
||||
align-items: center;
|
||||
`
|
||||
type Props = {
|
||||
size?: number,
|
||||
centered: boolean,
|
||||
}
|
||||
|
||||
const Loader = ({ centered = true, size }: Props) => (
|
||||
<Wrapper centered={centered}>
|
||||
<CircularProgress size={size || 60} />
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
export default Loader
|
|
@ -1,2 +0,0 @@
|
|||
// @flow
|
||||
export { default as Loader } from './Loader'
|
|
@ -1,8 +0,0 @@
|
|||
// @flow
|
||||
export * from './dataDisplay'
|
||||
export * from './feedback'
|
||||
export * from './layouts'
|
||||
export * from './navigation'
|
||||
export * from './safeUtils'
|
||||
export * from './surfaces'
|
||||
export * from './utils'
|
|
@ -1,2 +0,0 @@
|
|||
// @flow
|
||||
export { default as ListContentLayout } from './ListContentLayout'
|
|
@ -1,48 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { IconText } from '~/components-v2'
|
||||
import CheckIcon from '~/components/layout/PageFrame/assets/check.svg'
|
||||
import {
|
||||
background as backgroundColor,
|
||||
secondaryText as disabledColor,
|
||||
error as errorColor,
|
||||
secondary,
|
||||
} from '~/theme/variables'
|
||||
|
||||
const Circle = styled.div`
|
||||
background-color: ${({ disabled, error }) => {
|
||||
if (error) {
|
||||
return errorColor
|
||||
}
|
||||
if (disabled) {
|
||||
return disabledColor
|
||||
}
|
||||
|
||||
return secondary
|
||||
}};
|
||||
color: ${backgroundColor};
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
type Props = {
|
||||
dotIndex: number,
|
||||
currentIndex: number,
|
||||
error?: boolean,
|
||||
}
|
||||
const DotStep = ({ currentIndex, dotIndex, error }: Props) => {
|
||||
return (
|
||||
<Circle disabled={dotIndex > currentIndex} error={error}>
|
||||
{dotIndex < currentIndex ? <IconText iconUrl={CheckIcon} /> : dotIndex + 1}
|
||||
</Circle>
|
||||
)
|
||||
}
|
||||
|
||||
export default DotStep
|
|
@ -1,69 +0,0 @@
|
|||
// @flow
|
||||
import StepMUI from '@material-ui/core/Step'
|
||||
import StepLabelMUI from '@material-ui/core/StepLabel'
|
||||
import StepperMUI from '@material-ui/core/Stepper'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import DotStep from './DotStep'
|
||||
|
||||
import { secondaryText as disabled, error as errorColor, primary, secondary } from '~/theme/variables'
|
||||
|
||||
const StyledStepper = styled(StepperMUI)`
|
||||
background-color: transparent;
|
||||
`
|
||||
|
||||
const StyledStepLabel = styled.p`
|
||||
&& {
|
||||
color: ${({ activeStepIndex, error, index }) => {
|
||||
if (error) {
|
||||
return errorColor
|
||||
}
|
||||
|
||||
if (index === activeStepIndex) {
|
||||
return secondary
|
||||
}
|
||||
|
||||
if (index < activeStepIndex) {
|
||||
return disabled
|
||||
}
|
||||
|
||||
return primary
|
||||
}};
|
||||
}
|
||||
`
|
||||
|
||||
type Props = {
|
||||
steps: Array<{ id: string | number, label: string }>,
|
||||
activeStepIndex: number,
|
||||
error?: boolean,
|
||||
orientation: 'vertical' | 'horizontal',
|
||||
}
|
||||
|
||||
const Stepper = ({ activeStepIndex, error, orientation, steps }: Props) => {
|
||||
return (
|
||||
<StyledStepper activeStep={activeStepIndex} orientation={orientation}>
|
||||
{steps.map((s, index) => {
|
||||
return (
|
||||
<StepMUI key={s.id}>
|
||||
<StepLabelMUI
|
||||
icon={
|
||||
<DotStep currentIndex={activeStepIndex} dotIndex={index} error={index === activeStepIndex && error} />
|
||||
}
|
||||
>
|
||||
<StyledStepLabel
|
||||
activeStepIndex={activeStepIndex}
|
||||
error={index === activeStepIndex && error}
|
||||
index={index}
|
||||
>
|
||||
{s.label}
|
||||
</StyledStepLabel>
|
||||
</StepLabelMUI>
|
||||
</StepMUI>
|
||||
)
|
||||
})}
|
||||
</StyledStepper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Stepper
|
|
@ -1,2 +0,0 @@
|
|||
// @flow
|
||||
export { default as Stepper } from './Stepper'
|
|
@ -1,2 +0,0 @@
|
|||
// @flow
|
||||
export { default as AddressInfo } from './AddressInfo'
|
|
@ -1,48 +0,0 @@
|
|||
// @flow
|
||||
import CollapseMUI from '@material-ui/core/Collapse'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import ExpandLess from '@material-ui/icons/ExpandLess'
|
||||
import ExpandMore from '@material-ui/icons/ExpandMore'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const Wrapper = styled.div``
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Title = styled.div``
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
children: React.Node,
|
||||
description: React.Node,
|
||||
}
|
||||
|
||||
const Collapse = ({ children, description, title }: Props) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open)
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Title>{title}</Title>
|
||||
<Header>
|
||||
<IconButton disableRipple onClick={handleClick} size="small">
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
{description}
|
||||
</Header>
|
||||
|
||||
<CollapseMUI in={open} timeout="auto" unmountOnExit>
|
||||
{children}
|
||||
</CollapseMUI>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Collapse
|
|
@ -1,2 +0,0 @@
|
|||
// @flow
|
||||
export { default as Collapse } from './Collapse'
|
|
@ -1,2 +0,0 @@
|
|||
// @flow
|
||||
export * from './modals'
|
|
@ -1,73 +0,0 @@
|
|||
// @flow
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React, { type Node } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Modal from '~/components/Modal'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
|
||||
const TitleSection = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 10px 20px;
|
||||
`
|
||||
const BodySection = styled.div`
|
||||
padding: 10px 20px;
|
||||
max-height: 460px;
|
||||
overflow-y: auto;
|
||||
`
|
||||
const FooterSection = styled.div`
|
||||
margin: 10px 20px;
|
||||
`
|
||||
|
||||
const StyledClose = styled(Close)`
|
||||
&& {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
}
|
||||
`
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
paper: {
|
||||
height: 'auto',
|
||||
position: 'static',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
body: Node,
|
||||
footer: Node,
|
||||
onClose: () => void,
|
||||
}
|
||||
|
||||
const GenericModal = ({ body, footer, onClose, title }: Props) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Modal description="GenericModal" handleClose={onClose} open paperClassName={classes.paper} title="GenericModal">
|
||||
<TitleSection>
|
||||
{title}
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<StyledClose />
|
||||
</IconButton>
|
||||
</TitleSection>
|
||||
|
||||
<Hairline />
|
||||
<BodySection>{body}</BodySection>
|
||||
|
||||
{footer && (
|
||||
<>
|
||||
<Hairline />
|
||||
<FooterSection>{footer}</FooterSection>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default GenericModal
|
|
@ -1,3 +0,0 @@
|
|||
// @flow
|
||||
export { default as GenericModal } from './GenericModal'
|
||||
export * from './utils'
|
|
@ -1,60 +0,0 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Button from '~/components/layout/Button'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import { lg } from '~/theme/variables'
|
||||
|
||||
const StyledParagraph = styled(Paragraph)`
|
||||
&& {
|
||||
font-size: ${lg};
|
||||
}
|
||||
`
|
||||
const IconImg = styled.img`
|
||||
width: 20px;
|
||||
margin-right: 10px;
|
||||
`
|
||||
const TitleWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export const ModalTitle = ({ iconUrl, title }: { title: string, iconUrl: string }) => {
|
||||
return (
|
||||
<TitleWrapper>
|
||||
{iconUrl && <IconImg alt={title} src={iconUrl} />}
|
||||
<StyledParagraph noMargin weight="bolder">
|
||||
{title}
|
||||
</StyledParagraph>
|
||||
</TitleWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const FooterWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
`
|
||||
|
||||
export const ModalFooterConfirmation = ({
|
||||
cancelText,
|
||||
handleCancel,
|
||||
handleOk,
|
||||
okText,
|
||||
}: {
|
||||
okText: string,
|
||||
cancelText: string,
|
||||
handleOk: () => void,
|
||||
handleCancel: () => void,
|
||||
}) => {
|
||||
return (
|
||||
<FooterWrapper>
|
||||
<Button minWidth={130} onClick={handleCancel}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button color="primary" minWidth={130} onClick={handleOk} variant="contained">
|
||||
{okText}
|
||||
</Button>
|
||||
</FooterWrapper>
|
||||
)
|
||||
}
|
|
@ -38,9 +38,9 @@ const StyledBlock = styled(Block)`
|
|||
border-radius: 3px;
|
||||
`
|
||||
type Props = {
|
||||
safeName: string,
|
||||
safeName?: string,
|
||||
safeAddress: string,
|
||||
ethBalance: string,
|
||||
ethBalance?: string,
|
||||
}
|
||||
|
||||
const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props) => {
|
|
@ -15,11 +15,11 @@ const Text = styled.span`
|
|||
height: 17px;
|
||||
`
|
||||
|
||||
const IconText = ({ iconUrl, text }: { iconUrl: string, text?: string }) => (
|
||||
const CustomIconText = ({ iconUrl, text }: { iconUrl: string, text?: string }) => (
|
||||
<Wrapper>
|
||||
<Icon alt={text} src={iconUrl} />
|
||||
{text && <Text>{text}</Text>}
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
export default IconText
|
||||
export default CustomIconText
|
|
@ -6,11 +6,10 @@ export const Wrapper = styled.div`
|
|||
grid-template-columns: 245px auto;
|
||||
grid-template-rows: 514px;
|
||||
min-height: 525px;
|
||||
|
||||
.background {
|
||||
box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59);
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
`
|
||||
export const Nav = styled.div`
|
||||
grid-column: 1/3;
|
|
@ -34,23 +34,21 @@ type Props = {
|
|||
classes: Object,
|
||||
}
|
||||
|
||||
const List = ({ activeItem, classes, items, onItemClick }: Props) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
{items.map((i) => (
|
||||
<Item
|
||||
className={cn(classes.menuOption, activeItem === i.id && classes.active)}
|
||||
key={i.id}
|
||||
onClick={() => onItemClick(i.id)}
|
||||
>
|
||||
<div className="container">
|
||||
{i.iconUrl && <IconImg alt={i.name} src={i.iconUrl} />}
|
||||
<span>{i.name}</span>
|
||||
</div>
|
||||
</Item>
|
||||
))}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
const List = ({ activeItem, classes, items, onItemClick }: Props) => (
|
||||
<Wrapper>
|
||||
{items.map((i) => (
|
||||
<Item
|
||||
className={cn(classes.menuOption, activeItem === i.id && classes.active)}
|
||||
key={i.id}
|
||||
onClick={() => onItemClick(i.id)}
|
||||
>
|
||||
<div className="container">
|
||||
{i.iconUrl && <IconImg alt={i.name} src={i.iconUrl} />}
|
||||
<span>{i.name}</span>
|
||||
</div>
|
||||
</Item>
|
||||
))}
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
export default withStyles(styles)(List)
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { type FormApi } from 'final-form'
|
||||
import { type Decorator, type FormApi } from 'final-form'
|
||||
import * as React from 'react'
|
||||
import { Form } from 'react-final-form'
|
||||
|
||||
|
@ -10,13 +10,15 @@ export type OnSubmit = (
|
|||
) => ?Object | Promise<?Object> | void
|
||||
|
||||
type Props = {
|
||||
onSubmit: OnSubmit,
|
||||
children: Function,
|
||||
padding?: number,
|
||||
validation?: (values: Object) => Object | Promise<Object>,
|
||||
initialValues?: Object,
|
||||
decorators?: Decorator<{ [string]: any }>[],
|
||||
formMutators?: Object,
|
||||
initialValues?: Object,
|
||||
onSubmit: OnSubmit,
|
||||
subscription?: Object,
|
||||
padding?: number,
|
||||
testId?: string,
|
||||
validation?: (values: Object) => Object | Promise<Object>,
|
||||
}
|
||||
|
||||
const stylesBasedOn = (padding: number): $Shape<CSSStyleDeclaration> => ({
|
||||
|
@ -25,8 +27,19 @@ const stylesBasedOn = (padding: number): $Shape<CSSStyleDeclaration> => ({
|
|||
flex: '1 0 auto',
|
||||
})
|
||||
|
||||
const GnoForm = ({ children, formMutators, initialValues, onSubmit, padding = 0, testId = '', validation }: Props) => (
|
||||
const GnoForm = ({
|
||||
children,
|
||||
decorators,
|
||||
formMutators,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
padding = 0,
|
||||
subscription,
|
||||
testId = '',
|
||||
validation,
|
||||
}: Props) => (
|
||||
<Form
|
||||
decorators={decorators}
|
||||
initialValues={initialValues}
|
||||
mutators={formMutators}
|
||||
onSubmit={onSubmit}
|
||||
|
@ -35,6 +48,7 @@ const GnoForm = ({ children, formMutators, initialValues, onSubmit, padding = 0,
|
|||
{children(rest.submitting, rest.validating, rest, rest.form.mutators)}
|
||||
</form>
|
||||
)}
|
||||
subscription={subscription}
|
||||
validate={validation}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -12,6 +12,7 @@ import { REMOVE_ENTRY } from '~/logic/addressBook/store/actions/removeAddressBoo
|
|||
import { UPDATE_ENTRY } from '~/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { getAddressesListFromAdbk } from '~/logic/addressBook/utils'
|
||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||
import { checksumAddress } from '~/utils/checksumAddress'
|
||||
|
||||
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
|
||||
|
||||
|
@ -20,7 +21,7 @@ export type State = Map<string, Map<string, AddressBookEntry>>
|
|||
export const buildAddressBook = (storedAdbk: AddressBook): AddressBookProps => {
|
||||
let addressBookBuilt = Map([])
|
||||
Object.entries(storedAdbk).forEach((adbkProps: Array<string, AddressBookEntry[]>) => {
|
||||
const safeAddress = adbkProps[0]
|
||||
const safeAddress = checksumAddress(adbkProps[0])
|
||||
const adbkRecords = adbkProps[1].map(makeAddressBookEntry)
|
||||
const adbkSafeEntries = List(adbkRecords)
|
||||
addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries)
|
||||
|
|
|
@ -34,3 +34,16 @@ export const getAddressBookListSelector: Selector<GlobalState, {}, List<AddressB
|
|||
return result
|
||||
},
|
||||
)
|
||||
|
||||
export const getNameFromAddressBook = createSelector(
|
||||
getAddressBookListSelector,
|
||||
(_, address) => address,
|
||||
(addressBook: Map<string, AddressBook>, address: string) => {
|
||||
const adbkEntry = addressBook.find((addressBookItem) => addressBookItem.address === address)
|
||||
if (adbkEntry) {
|
||||
return adbkEntry.name
|
||||
}
|
||||
|
||||
return 'UNKNOWN'
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
// @flow
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import type { AddressBook, AddressBookProps } from '~/logic/addressBook/model/addressBook'
|
||||
import { getAddressBook } from '~/logic/addressBook/store/selectors'
|
||||
import type { Owner } from '~/routes/safe/store/models/owner'
|
||||
import { loadFromStorage, saveToStorage } from '~/utils/storage'
|
||||
|
||||
|
@ -33,14 +30,6 @@ export const getNameFromAdbk = (addressBook: AddressBook, userAddress: string):
|
|||
return null
|
||||
}
|
||||
|
||||
export const getNameFromAddressBook = (userAddress: string): string | null => {
|
||||
if (!userAddress) {
|
||||
return null
|
||||
}
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
return addressBook ? getNameFromAdbk(addressBook, userAddress) : null
|
||||
}
|
||||
|
||||
export const getOwnersWithNameFromAddressBook = (addressBook: AddressBook, ownerList: List<Owner>) => {
|
||||
if (!ownerList) {
|
||||
return []
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
// @flow
|
||||
import type Web3 from 'web3'
|
||||
|
||||
import type {
|
||||
ABI,
|
||||
ContractInterface,
|
||||
ExtendedABI,
|
||||
ExtendedContractInterface,
|
||||
} from '~/logic/contractInteraction/sources/types'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
|
||||
class ABIService {
|
||||
static extractUsefulMethods(abi: ABI): ExtendedABI {
|
||||
return abi
|
||||
.filter(({ constant, name, type }) => type === 'function' && !!name && typeof constant === 'boolean')
|
||||
.map((method) => ({
|
||||
action: method.constant ? 'read' : 'write',
|
||||
...ABIService.getMethodSignatureAndSignatureHash(method),
|
||||
...method,
|
||||
}))
|
||||
.sort(({ name: a }, { name: b }) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1))
|
||||
}
|
||||
|
||||
static getMethodHash(method: ContractInterface): string {
|
||||
const signature = ABIService.getMethodSignature(method)
|
||||
return ABIService.getSignatureHash(signature)
|
||||
}
|
||||
|
||||
static getMethodSignatureAndSignatureHash(
|
||||
method: ContractInterface,
|
||||
): {|
|
||||
signature: string,
|
||||
signatureHash: string,
|
||||
|} {
|
||||
const signature = ABIService.getMethodSignature(method)
|
||||
const signatureHash = ABIService.getSignatureHash(signature)
|
||||
return { signature, signatureHash }
|
||||
}
|
||||
|
||||
static getMethodSignature({ inputs, name }: ContractInterface): string {
|
||||
const params = inputs.map((x) => x.type).join(',')
|
||||
return `${name}(${params})`
|
||||
}
|
||||
|
||||
static getSignatureHash(signature: string): string {
|
||||
const web3: Web3 = getWeb3()
|
||||
return web3.utils.keccak256(signature).toString(2, 10)
|
||||
}
|
||||
|
||||
static isPayable(method: ContractInterface | ExtendedContractInterface): boolean {
|
||||
return method.payable
|
||||
}
|
||||
}
|
||||
|
||||
export default ABIService
|
|
@ -0,0 +1,63 @@
|
|||
// @flow
|
||||
import { RateLimit } from 'async-sema'
|
||||
import memoize from 'lodash.memoize'
|
||||
|
||||
import ABIService from '~/logic/contractInteraction/sources/ABIService'
|
||||
import { ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3'
|
||||
import { ETHERSCAN_API_KEY } from '~/utils/constants'
|
||||
|
||||
class EtherscanService extends ABIService {
|
||||
_rateLimit = async () => {}
|
||||
|
||||
_endpointsUrls: { [key: string]: string } = {
|
||||
[ETHEREUM_NETWORK.MAINNET]: 'https://api.etherscan.io/api',
|
||||
[ETHEREUM_NETWORK.RINKEBY]: 'https://api-rinkeby.etherscan.io/api',
|
||||
}
|
||||
|
||||
_fetch = memoize(
|
||||
async (url: string, contractAddress: string) => {
|
||||
let params = {
|
||||
module: 'contract',
|
||||
action: 'getAbi',
|
||||
address: contractAddress,
|
||||
}
|
||||
|
||||
if (ETHERSCAN_API_KEY) {
|
||||
const apiKey = ETHERSCAN_API_KEY
|
||||
params = { ...params, apiKey }
|
||||
}
|
||||
|
||||
const response = await fetch(`${url}?${new URLSearchParams(params)}`)
|
||||
|
||||
if (!response.ok) {
|
||||
return { status: 0, result: [] }
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
(url, contractAddress) => `${url}_${contractAddress}`,
|
||||
)
|
||||
|
||||
constructor(options: { rps: number }) {
|
||||
super()
|
||||
this._rateLimit = RateLimit(options.rps)
|
||||
}
|
||||
|
||||
async getContractABI(contractAddress: string, network: string) {
|
||||
const etherscanUrl = this._endpointsUrls[network]
|
||||
try {
|
||||
const { result, status } = await this._fetch(etherscanUrl, contractAddress)
|
||||
|
||||
if (status === '0') {
|
||||
return []
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error('Failed to retrieve ABI', e)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default EtherscanService
|
|
@ -0,0 +1,8 @@
|
|||
// @flow
|
||||
import EtherscanService from '~/logic/contractInteraction/sources/EtherscanService'
|
||||
|
||||
const sources = {
|
||||
etherscan: new EtherscanService({ rps: 4 }),
|
||||
}
|
||||
|
||||
export const getConfiguredSource = () => sources['etherscan']
|
|
@ -0,0 +1,27 @@
|
|||
// @flow
|
||||
export type InterfaceParams = {
|
||||
internalType: string,
|
||||
name: string,
|
||||
type: string,
|
||||
}
|
||||
|
||||
export type ContractInterface = {|
|
||||
constant: boolean,
|
||||
inputs: InterfaceParams[],
|
||||
name: string,
|
||||
outputs: InterfaceParams[],
|
||||
payable: boolean,
|
||||
stateMutability: string,
|
||||
type: string,
|
||||
|}
|
||||
|
||||
export type ExtendedContractInterface = {|
|
||||
...ContractInterface,
|
||||
action: string,
|
||||
signature: string,
|
||||
signatureHash: string,
|
||||
|}
|
||||
|
||||
export type ABI = ContractInterface[]
|
||||
|
||||
export type ExtendedABI = ExtendedContractInterface[]
|
|
@ -5,8 +5,8 @@ import { getExchangeRatesUrl } from '~/config'
|
|||
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
|
||||
|
||||
const fetchCurrenciesRates = async (
|
||||
baseCurrency: AVAILABLE_CURRENCIES,
|
||||
targetCurrencyValue: AVAILABLE_CURRENCIES,
|
||||
baseCurrency: $Keys<typeof AVAILABLE_CURRENCIES>,
|
||||
targetCurrencyValue: $Keys<typeof AVAILABLE_CURRENCIES>,
|
||||
): Promise<number> => {
|
||||
let rate = 0
|
||||
const url = `${getExchangeRatesUrl()}?base=${baseCurrency}&symbols=${targetCurrencyValue}`
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { Dispatch as ReduxDispatch } from 'redux'
|
||||
import { type Dispatch as ReduxDispatch } from 'redux'
|
||||
|
||||
import fetchCurrenciesRates from '~/logic/currencyValues/api/fetchCurrenciesRates'
|
||||
import { setCurrencyRate } from '~/logic/currencyValues/store/actions/setCurrencyRate'
|
||||
|
|
|
@ -3,13 +3,11 @@ import { List } from 'immutable'
|
|||
import { batch } from 'react-redux'
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
|
||||
import fetchCurrencyRate from '~/logic/currencyValues/store/actions/fetchCurrencyRate'
|
||||
import { setCurrencyBalances } from '~/logic/currencyValues/store/actions/setCurrencyBalances'
|
||||
import { setCurrencyRate } from '~/logic/currencyValues/store/actions/setCurrencyRate'
|
||||
import { setSelectedCurrency } from '~/logic/currencyValues/store/actions/setSelectedCurrency'
|
||||
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
|
||||
import { loadCurrencyValues } from '~/logic/currencyValues/store/utils/currencyValuesStorage'
|
||||
import fetchSafeTokens from '~/logic/tokens/store/actions/fetchSafeTokens'
|
||||
import type { GlobalState } from '~/store'
|
||||
|
||||
export const fetchCurrencyValues = (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
|
@ -27,12 +25,11 @@ export const fetchCurrencyValues = (safeAddress: string) => async (dispatch: Red
|
|||
Object.entries(storedCurrencies).forEach((kv) => {
|
||||
const safeAddr = kv[0]
|
||||
const value = kv[1]
|
||||
|
||||
const { currencyRate, selectedCurrency } = value
|
||||
batch(() => {
|
||||
dispatch(setSelectedCurrency(safeAddr, selectedCurrency))
|
||||
dispatch(setSelectedCurrency(safeAddr, selectedCurrency || AVAILABLE_CURRENCIES.USD))
|
||||
dispatch(setCurrencyRate(safeAddr, currencyRate))
|
||||
dispatch(fetchCurrencyRate(safeAddr, selectedCurrency))
|
||||
dispatch(fetchSafeTokens(safeAddress))
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
|
|
|
@ -12,7 +12,7 @@ export const generateSignaturesFromTxConfirmations = (
|
|||
// The constant parts need to be sorted so that the recovered signers are sorted ascending
|
||||
// (natural order) by address (not checksummed).
|
||||
const confirmationsMap = confirmations.reduce((map, obj) => {
|
||||
map[obj.owner.address.toLowerCase()] = obj // eslint-disable-line no-param-reassign
|
||||
map[obj.owner.toLowerCase()] = obj // eslint-disable-line no-param-reassign
|
||||
return map
|
||||
}, {})
|
||||
|
||||
|
|
|
@ -27,9 +27,7 @@ export const getAwaitingTransactions = (
|
|||
if (!transaction.executionTxHash && !isTransactionCancelled) {
|
||||
// Then we check if the waiting confirmations are not from the current user, otherwise, filters this
|
||||
// transaction
|
||||
const transactionWaitingUser = transaction.confirmations.filter(
|
||||
(confirmation) => confirmation.owner && confirmation.owner.address !== userAccount,
|
||||
)
|
||||
const transactionWaitingUser = transaction.confirmations.filter(({ owner }) => owner !== userAccount)
|
||||
|
||||
return transactionWaitingUser.size > 0
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// @flow
|
||||
import { getIncomingTxServiceUriTo, getTxServiceHost } from '~/config'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { checksumAddress } from '~/utils/checksumAddress'
|
||||
|
||||
export const buildIncomingTxServiceUrl = (safeAddress: string) => {
|
||||
const host = getTxServiceHost()
|
||||
const address = getWeb3().utils.toChecksumAddress(safeAddress)
|
||||
const address = checksumAddress(safeAddress)
|
||||
const base = getIncomingTxServiceUriTo(address)
|
||||
|
||||
return `${host}${base}`
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import axios from 'axios'
|
||||
|
||||
import { getTxServiceHost, getTxServiceUriFrom } from '~/config'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { checksumAddress } from '~/utils/checksumAddress'
|
||||
|
||||
export type TxServiceType = 'confirmation' | 'execution' | 'initialised'
|
||||
export type Operation = 0 | 1 | 2
|
||||
|
@ -38,7 +38,7 @@ const calculateBodyFrom = async (
|
|||
)
|
||||
|
||||
return {
|
||||
to: getWeb3().utils.toChecksumAddress(to),
|
||||
to: checksumAddress(to),
|
||||
value: valueInWei,
|
||||
data,
|
||||
operation,
|
||||
|
@ -50,7 +50,7 @@ const calculateBodyFrom = async (
|
|||
refundReceiver,
|
||||
contractTransactionHash,
|
||||
transactionHash,
|
||||
sender: getWeb3().utils.toChecksumAddress(sender),
|
||||
sender: checksumAddress(sender),
|
||||
origin,
|
||||
signature,
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ const calculateBodyFrom = async (
|
|||
|
||||
export const buildTxServiceUrl = (safeAddress: string) => {
|
||||
const host = getTxServiceHost()
|
||||
const address = getWeb3().utils.toChecksumAddress(safeAddress)
|
||||
const address = checksumAddress(safeAddress)
|
||||
const base = getTxServiceUriFrom(address)
|
||||
return `${host}${base}`
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ const wallets = [
|
|||
appUrl: 'gnosis-safe.io',
|
||||
preferred: true,
|
||||
email: 'safe@gnosis.io',
|
||||
desktop: true,
|
||||
desktop: false,
|
||||
rpcUrl: infuraUrl,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -29,10 +29,10 @@ const Routes = ({ location }: RoutesProps) => {
|
|||
const { trackPage } = useAnalytics()
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname !== '/') {
|
||||
if (isInitialLoad && location.pathname !== '/') {
|
||||
setInitialLoad(false)
|
||||
}
|
||||
}, [])
|
||||
}, [location.pathname, isInitialLoad])
|
||||
|
||||
useEffect(() => {
|
||||
const page = location.pathname + location.search
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// @flow
|
||||
import { Loader } from '@gnosis.pm/safe-react-components'
|
||||
import queryString from 'query-string'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
|
||||
|
@ -10,7 +12,6 @@ import Layout from '../components/Layout'
|
|||
import actions, { type Actions } from './actions'
|
||||
import selector from './selector'
|
||||
|
||||
import { Loader } from '~/components-v2'
|
||||
import Page from '~/components/layout/Page'
|
||||
import { getSafeDeploymentTransaction } from '~/logic/contracts/safeContracts'
|
||||
import { checkReceiptStatus } from '~/logic/wallets/ethTransactions'
|
||||
|
@ -120,7 +121,7 @@ const Open = ({ addSafe, network, provider, userAccount }: Props) => {
|
|||
threshold,
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
// check if there is a safe being created
|
||||
useEffect(() => {
|
||||
|
@ -163,6 +164,12 @@ const Open = ({ addSafe, network, provider, userAccount }: Props) => {
|
|||
const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses)
|
||||
addSafe(safeProps)
|
||||
|
||||
ReactGA.event({
|
||||
category: 'User',
|
||||
action: 'Created a safe',
|
||||
value: safeAddress,
|
||||
})
|
||||
|
||||
removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
|
||||
const url = {
|
||||
pathname: `${SAFELIST_ADDRESS}/${safeProps.address}/balances`,
|
||||
|
@ -191,7 +198,7 @@ const Open = ({ addSafe, network, provider, userAccount }: Props) => {
|
|||
}
|
||||
|
||||
if (loading || showProgress === undefined) {
|
||||
return <Loader />
|
||||
return <Loader size="md" />
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1,56 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Button from '~/components/layout/Button'
|
||||
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
||||
import { connected } from '~/theme/variables'
|
||||
|
||||
const EtherScanLink = styled.a`
|
||||
color: ${connected};
|
||||
`
|
||||
|
||||
const ButtonWithMargin = styled(Button)`
|
||||
margin-right: 16px;
|
||||
`
|
||||
|
||||
export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: string }) => (
|
||||
<span>
|
||||
<p>This process should take a couple of minutes.</p>
|
||||
<p>
|
||||
Follow the progress on{' '}
|
||||
<EtherScanLink
|
||||
aria-label="Show details on Etherscan"
|
||||
href={getEtherScanLink('tx', safeCreationTxHash)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Etherscan.io
|
||||
</EtherScanLink>
|
||||
.
|
||||
</p>
|
||||
</span>
|
||||
)
|
||||
|
||||
export const ContinueFooter = ({
|
||||
continueButtonDisabled,
|
||||
onContinue,
|
||||
}: {
|
||||
continueButtonDisabled: boolean,
|
||||
onContinue: Function,
|
||||
}) => (
|
||||
<Button color="primary" disabled={continueButtonDisabled} onClick={onContinue} variant="contained">
|
||||
Continue
|
||||
</Button>
|
||||
)
|
||||
|
||||
export const ErrorFooter = ({ onCancel, onRetry }: { onCancel: Function, onRetry: Function }) => (
|
||||
<>
|
||||
<ButtonWithMargin onClick={onCancel} variant="contained">
|
||||
Cancel
|
||||
</ButtonWithMargin>
|
||||
<Button color="primary" onClick={onRetry} variant="contained">
|
||||
Retry
|
||||
</Button>
|
||||
</>
|
||||
)
|
|
@ -1,18 +1,21 @@
|
|||
// @flow
|
||||
import { Loader, Stepper } from '@gnosis.pm/safe-react-components'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { Loader, Stepper } from '~/components-v2'
|
||||
import LoaderDots from '~/components-v2/feedback/Loader-dots/assets/loader-dots.svg'
|
||||
import { ErrorFooter } from './components/Footer'
|
||||
import { isConfirmationStep, steps } from './steps'
|
||||
|
||||
import Button from '~/components/layout/Button'
|
||||
import Heading from '~/components/layout/Heading'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import { initContracts } from '~/logic/contracts/safeContracts'
|
||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
import { getEtherScanLink, getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { background, connected } from '~/theme/variables'
|
||||
|
||||
const loaderDotsSvg = require('./assets/loader-dots.svg')
|
||||
const successSvg = require('./assets/success.svg')
|
||||
const vaultErrorSvg = require('./assets/vault-error.svg')
|
||||
const vaultSvg = require('./assets/vault.svg')
|
||||
|
@ -47,21 +50,18 @@ const Body = styled.div`
|
|||
display: grid;
|
||||
grid-template-rows: 100px 50px 70px 60px 100px;
|
||||
`
|
||||
const EtherScanLink = styled.a`
|
||||
color: ${connected};
|
||||
`
|
||||
|
||||
const CardTitle = styled.div`
|
||||
font-size: 20px;
|
||||
`
|
||||
const FullParagraph = styled(Paragraph)`
|
||||
background-color: ${background};
|
||||
background-color: ${(p) => (p.inverseColors ? connected : background)};
|
||||
color: ${(p) => (p.inverseColors ? background : connected)};
|
||||
padding: 24px;
|
||||
font-size: 16px;
|
||||
margin-bottom: 16px;
|
||||
`
|
||||
const ButtonMargin = styled(Button)`
|
||||
margin-right: 16px;
|
||||
|
||||
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||
`
|
||||
|
||||
const BodyImage = styled.div`
|
||||
|
@ -88,6 +88,11 @@ const BodyFooter = styled.div`
|
|||
align-items: flex-end;
|
||||
`
|
||||
|
||||
const BackButton = styled(Button)`
|
||||
grid-column: 2;
|
||||
margin: 20px auto 0;
|
||||
`
|
||||
|
||||
type Props = {
|
||||
provider: string,
|
||||
creationTxHash: Promise<any>,
|
||||
|
@ -108,78 +113,13 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
|
|||
const [waitingSafeDeployed, setWaitingSafeDeployed] = useState(false)
|
||||
const [continueButtonDisabled, setContinueButtonDisabled] = useState(false)
|
||||
|
||||
const genericFooter = (
|
||||
<span>
|
||||
<p>This process should take a couple of minutes.</p>
|
||||
<p>
|
||||
Follow the progress on{' '}
|
||||
<EtherScanLink
|
||||
aria-label="Show details on Etherscan"
|
||||
href={getEtherScanLink('tx', safeCreationTxHash)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Etherscan.io
|
||||
</EtherScanLink>
|
||||
.
|
||||
</p>
|
||||
</span>
|
||||
)
|
||||
const confirmationStep = isConfirmationStep(stepIndex)
|
||||
|
||||
const navigateToSafe = () => {
|
||||
setContinueButtonDisabled(true)
|
||||
onSuccess(createdSafeAddress)
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Waiting for transaction confirmation',
|
||||
description: undefined,
|
||||
instruction: 'Please confirm the Safe creation in your wallet',
|
||||
footer: null,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Transaction submitted',
|
||||
description: undefined,
|
||||
instruction: 'Please do not leave the page',
|
||||
footer: genericFooter,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'Validating transaction',
|
||||
description: undefined,
|
||||
instruction: 'Please do not leave the page',
|
||||
footer: genericFooter,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
label: 'Deploying smart contract',
|
||||
description: undefined,
|
||||
instruction: 'Please do not leave the page',
|
||||
footer: genericFooter,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
label: 'Generating your Safe',
|
||||
description: undefined,
|
||||
instruction: 'Please do not leave the page',
|
||||
footer: genericFooter,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
label: 'Success',
|
||||
description: 'Your Safe was created successfully',
|
||||
instruction: 'Click below to get started',
|
||||
footer: (
|
||||
<Button color="primary" disabled={continueButtonDisabled} onClick={navigateToSafe} variant="contained">
|
||||
Continue
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const onError = (error) => {
|
||||
setIntervalStarted(false)
|
||||
setWaitingSafeDeployed(false)
|
||||
|
@ -354,6 +294,13 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
|
|||
return <Loader />
|
||||
}
|
||||
|
||||
let FooterComponent = null
|
||||
if (error) {
|
||||
FooterComponent = ErrorFooter
|
||||
} else if (steps[stepIndex].footerComponent) {
|
||||
FooterComponent = steps[stepIndex].footerComponent
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Title tag="h2">Safe creation process</Title>
|
||||
|
@ -369,29 +316,30 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, provider
|
|||
<CardTitle>{steps[stepIndex].description || steps[stepIndex].label}</CardTitle>
|
||||
</BodyDescription>
|
||||
|
||||
<BodyLoader>{!error && stepIndex <= 4 && <Img alt="LoaderDots" src={LoaderDots} />}</BodyLoader>
|
||||
<BodyLoader>{!error && stepIndex <= 4 && <Img alt="Loader dots" src={loaderDotsSvg} />}</BodyLoader>
|
||||
|
||||
<BodyInstruction>
|
||||
<FullParagraph color="primary" noMargin size="md">
|
||||
<FullParagraph color="primary" inverseColors={confirmationStep} noMargin size="md">
|
||||
{error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction}
|
||||
</FullParagraph>
|
||||
</BodyInstruction>
|
||||
|
||||
<BodyFooter>
|
||||
{error ? (
|
||||
<>
|
||||
<ButtonMargin onClick={onCancel} variant="contained">
|
||||
Cancel
|
||||
</ButtonMargin>
|
||||
<Button color="primary" onClick={onRetryTx} variant="contained">
|
||||
Retry
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
steps[stepIndex].footer
|
||||
)}
|
||||
{FooterComponent ? (
|
||||
<FooterComponent
|
||||
continueButtonDisabled={continueButtonDisabled}
|
||||
onCancel={onCancel}
|
||||
onClick={onRetryTx}
|
||||
onContinue={navigateToSafe}
|
||||
onRetry={onRetryTx}
|
||||
safeCreationTxHash={safeCreationTxHash}
|
||||
/>
|
||||
) : null}
|
||||
</BodyFooter>
|
||||
</Body>
|
||||
<BackButton color="primary" minWidth={140} onClick={onCancel}>
|
||||
Back
|
||||
</BackButton>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
// @flow
|
||||
import { ContinueFooter, GenericFooter } from './components/Footer'
|
||||
|
||||
export const isConfirmationStep = (stepIndex?: number) => stepIndex === 0
|
||||
|
||||
export const steps = [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Waiting for transaction confirmation',
|
||||
description: undefined,
|
||||
instruction: 'Please confirm the Safe creation in your wallet',
|
||||
footerComponent: null,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Transaction submitted',
|
||||
description: undefined,
|
||||
instruction: 'Please do not leave the page',
|
||||
footerComponent: GenericFooter,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'Validating transaction',
|
||||
description: undefined,
|
||||
instruction: 'Please do not leave the page',
|
||||
footerComponent: GenericFooter,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
label: 'Deploying smart contract',
|
||||
description: undefined,
|
||||
instruction: 'Please do not leave the page',
|
||||
footerComponent: GenericFooter,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
label: 'Generating your Safe',
|
||||
description: undefined,
|
||||
instruction: 'Please do not leave the page',
|
||||
footerComponent: GenericFooter,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
label: 'Success',
|
||||
description: 'Your Safe was created successfully',
|
||||
instruction: 'Click below to get started',
|
||||
footerComponent: ContinueFooter,
|
||||
},
|
||||
]
|
|
@ -44,6 +44,7 @@ import type { OwnerRow } from '~/routes/safe/components/Settings/ManageOwners/da
|
|||
import RemoveOwnerIcon from '~/routes/safe/components/Settings/assets/icons/bin.svg'
|
||||
import RemoveOwnerIconDisabled from '~/routes/safe/components/Settings/assets/icons/disabled-bin.svg'
|
||||
import { addressBookQueryParamsSelector, safesListSelector } from '~/routes/safe/store/selectors'
|
||||
import { checksumAddress } from '~/utils/checksumAddress'
|
||||
|
||||
type Props = {
|
||||
classes: Object,
|
||||
|
@ -69,7 +70,8 @@ const AddressBookTable = ({ classes }: Props) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (entryAddressToEditOrCreateNew) {
|
||||
const key = addressBook.findKey((entry) => entry.address === entryAddressToEditOrCreateNew)
|
||||
const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew)
|
||||
const key = addressBook.findKey((entry) => entry.address === checksumEntryAdd)
|
||||
if (key >= 0) {
|
||||
// Edit old entry
|
||||
const value = addressBook.get(key)
|
||||
|
@ -79,7 +81,7 @@ const AddressBookTable = ({ classes }: Props) => {
|
|||
setSelectedEntry({
|
||||
entry: {
|
||||
name: '',
|
||||
address: entryAddressToEditOrCreateNew,
|
||||
address: checksumEntryAdd,
|
||||
isNew: true,
|
||||
},
|
||||
})
|
||||
|
@ -89,17 +91,25 @@ const AddressBookTable = ({ classes }: Props) => {
|
|||
|
||||
const newEntryModalHandler = (entry: AddressBookEntry) => {
|
||||
setEditCreateEntryModalOpen(false)
|
||||
dispatch(addAddressBookEntry(makeAddressBookEntry(entry)))
|
||||
const checksumEntries = {
|
||||
...entry,
|
||||
address: checksumAddress(entry.address),
|
||||
}
|
||||
dispatch(addAddressBookEntry(makeAddressBookEntry(checksumEntries)))
|
||||
}
|
||||
|
||||
const editEntryModalHandler = (entry: AddressBookEntry) => {
|
||||
setSelectedEntry(null)
|
||||
setEditCreateEntryModalOpen(false)
|
||||
dispatch(updateAddressBookEntry(makeAddressBookEntry(entry)))
|
||||
const checksumEntries = {
|
||||
...entry,
|
||||
address: checksumAddress(entry.address),
|
||||
}
|
||||
dispatch(updateAddressBookEntry(makeAddressBookEntry(checksumEntries)))
|
||||
}
|
||||
|
||||
const deleteEntryModalHandler = () => {
|
||||
const entryAddress = selectedEntry.entry.address
|
||||
const entryAddress = checksumAddress(selectedEntry.entry.address)
|
||||
setSelectedEntry(null)
|
||||
setDeleteEntryModalOpen(false)
|
||||
dispatch(removeAddressBookEntry(entryAddress))
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
// @flow
|
||||
import {
|
||||
Collapse,
|
||||
DividerLine,
|
||||
Icon,
|
||||
ModalFooterConfirmation,
|
||||
ModalTitle,
|
||||
Text,
|
||||
TextBox,
|
||||
Title,
|
||||
} from '@gnosis.pm/safe-react-components'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { AddressInfo, Collapse, DividerLine, ModalFooterConfirmation, ModalTitle, TextBox } from '~/components-v2'
|
||||
import AddressInfo from '~/components/AddressInfo'
|
||||
import { mustBeEthereumAddress } from '~/components/forms/validator'
|
||||
import Bold from '~/components/layout/Bold'
|
||||
import Heading from '~/components/layout/Heading'
|
||||
import Img from '~/components/layout/Img'
|
||||
|
@ -27,6 +38,19 @@ const CollapseContent = styled.div`
|
|||
}
|
||||
`
|
||||
|
||||
const IconText = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
margin-right: 4px;
|
||||
}
|
||||
`
|
||||
const isTxValid = (t) => {
|
||||
const isAddressValid = mustBeEthereumAddress(t.to) === undefined
|
||||
return isAddressValid && t.value !== undefined && typeof t.value === 'number' && t.data && typeof t.data === 'string'
|
||||
}
|
||||
|
||||
const confirmTransactions = (
|
||||
safeAddress: string,
|
||||
safeName: string,
|
||||
|
@ -38,9 +62,22 @@ const confirmTransactions = (
|
|||
closeModal: () => void,
|
||||
onConfirm: () => void,
|
||||
) => {
|
||||
const areTxsMalformed = txs.some((t) => !isTxValid(t))
|
||||
|
||||
const title = <ModalTitle iconUrl={iconApp} title={nameApp} />
|
||||
|
||||
const body = (
|
||||
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>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<DividerLine withArrow />
|
||||
|
@ -67,8 +104,15 @@ const confirmTransactions = (
|
|||
})}
|
||||
</>
|
||||
)
|
||||
|
||||
const footer = (
|
||||
<ModalFooterConfirmation cancelText="Cancel" handleCancel={closeModal} handleOk={onConfirm} okText="Submit" />
|
||||
<ModalFooterConfirmation
|
||||
cancelText="Cancel"
|
||||
handleCancel={closeModal}
|
||||
handleOk={onConfirm}
|
||||
okDisabled={areTxsMalformed}
|
||||
okText="Submit"
|
||||
/>
|
||||
)
|
||||
|
||||
openModal({
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { Card, FixedDialog, FixedIcon, IconText, Menu, Text, Title } from '@gnosis.pm/safe-react-components'
|
||||
import { Card, FixedDialog, FixedIcon, IconText, Loader, Menu, Text, Title } from '@gnosis.pm/safe-react-components'
|
||||
import { withSnackbar } from 'notistack'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
@ -11,7 +11,7 @@ import confirmTransactions from './confirmTransactions'
|
|||
import sendTransactions from './sendTransactions'
|
||||
import { getAppInfoFromUrl, staticAppsList } from './utils'
|
||||
|
||||
import { ListContentLayout as LCL, Loader } from '~/components-v2'
|
||||
import LCL from '~/components/ListContentLayout'
|
||||
import { networkSelector } from '~/logic/wallets/store/selectors'
|
||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||
import { grantedSelector } from '~/routes/safe/container/selector'
|
||||
|
|
|
@ -27,28 +27,26 @@ const sendTransactions = (
|
|||
const web3 = getWeb3()
|
||||
const multiSend = new web3.eth.Contract(multiSendAbi, multiSendAddress)
|
||||
|
||||
const encodeMultiSendCalldata = multiSend.methods
|
||||
.multiSend(
|
||||
`0x${txs
|
||||
.map((tx) =>
|
||||
[
|
||||
web3.eth.abi.encodeParameter('uint8', 0).slice(-2),
|
||||
web3.eth.abi.encodeParameter('address', tx.to).slice(-40),
|
||||
web3.eth.abi.encodeParameter('uint256', tx.value).slice(-64),
|
||||
web3.eth.abi.encodeParameter('uint256', web3.utils.hexToBytes(tx.data).length).slice(-64),
|
||||
tx.data.replace(/^0x/, ''),
|
||||
].join(''),
|
||||
)
|
||||
.join('')}`,
|
||||
const joinedTxs = txs
|
||||
.map((tx) =>
|
||||
[
|
||||
web3.eth.abi.encodeParameter('uint8', 0).slice(-2),
|
||||
web3.eth.abi.encodeParameter('address', tx.to).slice(-40),
|
||||
web3.eth.abi.encodeParameter('uint256', tx.value).slice(-64),
|
||||
web3.eth.abi.encodeParameter('uint256', web3.utils.hexToBytes(tx.data).length).slice(-64),
|
||||
tx.data.replace(/^0x/, ''),
|
||||
].join(''),
|
||||
)
|
||||
.encodeABI()
|
||||
.join('')
|
||||
|
||||
const encodeMultiSendCallData = multiSend.methods.multiSend(`0x${joinedTxs}`).encodeABI()
|
||||
|
||||
return dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: multiSendAddress,
|
||||
valueInWei: 0,
|
||||
txData: encodeMultiSendCalldata,
|
||||
txData: encodeMultiSendCallData,
|
||||
notifiedTransaction: 'STANDARD_TX',
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
|
|
|
@ -1,67 +1,13 @@
|
|||
// @flow
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import CopyBtn from '~/components/CopyBtn'
|
||||
import EtherscanBtn from '~/components/EtherscanBtn'
|
||||
import Identicon from '~/components/Identicon'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Bold from '~/components/layout/Bold'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { border, xs } from '~/theme/variables'
|
||||
import AddressInfo from '~/components/AddressInfo'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
balanceContainer: {
|
||||
fontSize: '12px',
|
||||
lineHeight: 1.08,
|
||||
letterSpacing: -0.5,
|
||||
backgroundColor: border,
|
||||
width: 'fit-content',
|
||||
padding: '5px 10px',
|
||||
marginTop: xs,
|
||||
borderRadius: '3px',
|
||||
},
|
||||
address: {
|
||||
marginRight: xs,
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
safeAddress: string,
|
||||
safeName: string,
|
||||
ethBalance: string,
|
||||
}
|
||||
|
||||
const SafeInfo = (props: Props) => {
|
||||
const { ethBalance, safeAddress, safeName } = props
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={safeAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Paragraph noMargin style={{ lineHeight: 1 }} weight="bolder">
|
||||
{safeName}
|
||||
</Paragraph>
|
||||
<Block justify="left">
|
||||
<Paragraph className={classes.address} noMargin weight="bolder">
|
||||
{safeAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={safeAddress} />
|
||||
<EtherscanBtn type="address" value={safeAddress} />
|
||||
</Block>
|
||||
<Block className={classes.balanceContainer}>
|
||||
<Paragraph noMargin>
|
||||
Balance: <Bold>{`${ethBalance} ETH`}</Bold>
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
const SafeInfo = () => {
|
||||
const { address: safeAddress = '', ethBalance, name: safeName } = useSelector(safeSelector)
|
||||
return <AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
}
|
||||
|
||||
export default SafeInfo
|
||||
|
|
|
@ -18,16 +18,16 @@ const ReviewCollectible = React.lazy(() => import('./screens/ReviewCollectible')
|
|||
|
||||
const ReviewTx = React.lazy(() => import('./screens/ReviewTx'))
|
||||
|
||||
const SendCustomTx = React.lazy(() => import('./screens/SendCustomTx'))
|
||||
const ContractInteraction = React.lazy(() => import('./screens/ContractInteraction'))
|
||||
|
||||
const ReviewCustomTx = React.lazy(() => import('./screens/ReviewCustomTx'))
|
||||
const ContractInteractionReview = React.lazy(() => import('./screens/ContractInteraction/Review'))
|
||||
|
||||
type ActiveScreen =
|
||||
| 'chooseTxType'
|
||||
| 'sendFunds'
|
||||
| 'reviewTx'
|
||||
| 'sendCustomTx'
|
||||
| 'reviewCustomTx'
|
||||
| 'contractInteraction'
|
||||
| 'contractInteractionReview'
|
||||
| 'sendCollectible'
|
||||
| 'reviewCollectible'
|
||||
|
||||
|
@ -82,9 +82,9 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
|
|||
setTx(txInfo)
|
||||
}
|
||||
|
||||
const handleCustomTxCreation = (customTxInfo) => {
|
||||
setActiveScreen('reviewCustomTx')
|
||||
setTx(customTxInfo)
|
||||
const handleContractInteractionCreation = (contractInteractionInfo) => {
|
||||
setTx(contractInteractionInfo)
|
||||
setActiveScreen('contractInteractionReview')
|
||||
}
|
||||
|
||||
const handleSendCollectible = (txInfo) => {
|
||||
|
@ -122,16 +122,16 @@ const SendModal = ({ activeScreenType, isOpen, onClose, recipientAddress, select
|
|||
{activeScreen === 'reviewTx' && (
|
||||
<ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx} />
|
||||
)}
|
||||
{activeScreen === 'sendCustomTx' && (
|
||||
<SendCustomTx
|
||||
{activeScreen === 'contractInteraction' && (
|
||||
<ContractInteraction
|
||||
contractAddress={recipientAddress}
|
||||
initialValues={tx}
|
||||
onClose={onClose}
|
||||
onNext={handleCustomTxCreation}
|
||||
recipientAddress={recipientAddress}
|
||||
onNext={handleContractInteractionCreation}
|
||||
/>
|
||||
)}
|
||||
{activeScreen === 'reviewCustomTx' && (
|
||||
<ReviewCustomTx onClose={onClose} onPrev={() => setActiveScreen('sendCustomTx')} tx={tx} />
|
||||
{activeScreen === 'contractInteractionReview' && tx && (
|
||||
<ContractInteractionReview onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} />
|
||||
)}
|
||||
{activeScreen === 'sendCollectible' && (
|
||||
<SendCollectible
|
||||
|
|
|
@ -6,7 +6,6 @@ import classNames from 'classnames/bind'
|
|||
import * as React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import Code from '../assets/code.svg'
|
||||
import Collectible from '../assets/collectibles.svg'
|
||||
import Token from '../assets/token.svg'
|
||||
|
||||
|
@ -17,6 +16,7 @@ import Hairline from '~/components/layout/Hairline'
|
|||
import Img from '~/components/layout/Img'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import ContractInteractionIcon from '~/routes/safe/components/Transactions/TxsTable/TxType/assets/custom.svg'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
import { lg, md, sm } from '~/theme/variables'
|
||||
|
||||
|
@ -71,13 +71,13 @@ const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }: Props) =>
|
|||
const classes = useStyles()
|
||||
const { featuresEnabled } = useSelector(safeSelector)
|
||||
const erc721Enabled = featuresEnabled.includes('ERC721')
|
||||
const [disableCustomTx, setDisableCustomTx] = React.useState(!!recipientAddress)
|
||||
const [disableContractInteraction, setDisableContractInteraction] = React.useState(!!recipientAddress)
|
||||
|
||||
React.useEffect(() => {
|
||||
let isCurrent = true
|
||||
const isContract = async () => {
|
||||
if (recipientAddress && isCurrent) {
|
||||
setDisableCustomTx(!!(await mustBeEthereumContractAddress(recipientAddress)))
|
||||
setDisableContractInteraction(!!(await mustBeEthereumContractAddress(recipientAddress)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,14 +140,18 @@ const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }: Props) =>
|
|||
)}
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={disableCustomTx}
|
||||
disabled={disableContractInteraction}
|
||||
minHeight={52}
|
||||
minWidth={260}
|
||||
onClick={() => setActiveScreen('sendCustomTx')}
|
||||
onClick={() => setActiveScreen('contractInteraction')}
|
||||
variant="outlined"
|
||||
>
|
||||
<Img alt="Send custom transaction" className={classNames(classes.leftIcon, classes.iconSmall)} src={Code} />
|
||||
Send custom transaction
|
||||
<Img
|
||||
alt="Contract Interaction"
|
||||
className={classNames(classes.leftIcon, classes.iconSmall)}
|
||||
src={ContractInteractionIcon}
|
||||
/>
|
||||
Contract Interaction
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
// @flow
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import { useField, useFormState } from 'react-final-form'
|
||||
|
||||
import Button from '~/components/layout/Button'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { styles } from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
import { createTxObject } from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Buttons = ({ onCallSubmit, onClose }: { onCallSubmit: (string) => void, onClose: () => void }) => {
|
||||
const classes = useStyles()
|
||||
const {
|
||||
input: { value: method },
|
||||
} = useField('selectedMethod', { value: true })
|
||||
const {
|
||||
input: { value: contractAddress },
|
||||
} = useField('contractAddress', { valid: true })
|
||||
const { submitting, valid, validating, values } = useFormState({
|
||||
subscription: { submitting: true, valid: true, values: true, validating: true },
|
||||
})
|
||||
|
||||
const handleCallSubmit = async () => {
|
||||
const results = await createTxObject(method, contractAddress, values).call()
|
||||
onCallSubmit(results)
|
||||
}
|
||||
|
||||
return (
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
{method && method.action === 'read' ? (
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="review-tx-btn"
|
||||
disabled={validating || !valid}
|
||||
minWidth={140}
|
||||
onClick={handleCallSubmit}
|
||||
variant="contained"
|
||||
>
|
||||
Call
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="review-tx-btn"
|
||||
disabled={submitting || validating || !valid || !method || method.action === 'read'}
|
||||
minWidth={140}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Review
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default Buttons
|
|
@ -0,0 +1,31 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
|
||||
import TextareaField from '~/components/forms/TextareaField'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Row from '~/components/layout/Row'
|
||||
import EtherscanService from '~/logic/contractInteraction/sources/EtherscanService'
|
||||
|
||||
export const NO_DATA = 'no data'
|
||||
|
||||
const mustBeValidABI = (abi: string) => {
|
||||
try {
|
||||
const parsedABI = EtherscanService.extractUsefulMethods(JSON.parse(abi))
|
||||
|
||||
if (parsedABI.length === 0) {
|
||||
return NO_DATA
|
||||
}
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const ContractABI = () => (
|
||||
<Row margin="sm">
|
||||
<Col>
|
||||
<TextareaField name="abi" placeholder="ABI*" text="ABI*" type="text" validate={mustBeValidABI} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
|
||||
export default ContractABI
|
|
@ -0,0 +1,66 @@
|
|||
// @flow
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
|
||||
import Field from '~/components/forms/Field'
|
||||
import TextField from '~/components/forms/TextField'
|
||||
import {
|
||||
composeValidators,
|
||||
mustBeEthereumAddress,
|
||||
mustBeEthereumContractAddress,
|
||||
required,
|
||||
} from '~/components/forms/validator'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { styles } from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type Props = {
|
||||
isContract?: boolean,
|
||||
isRequired?: boolean,
|
||||
name: string,
|
||||
onScannedValue: (string) => void,
|
||||
text: string,
|
||||
}
|
||||
|
||||
const EthAddressInput = ({ isContract = true, isRequired = true, name, onScannedValue, text }: Props) => {
|
||||
const classes = useStyles()
|
||||
const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress]
|
||||
const validate = composeValidators(...validatorsList.filter((_) => _))
|
||||
|
||||
const handleScan = (value, closeQrModal) => {
|
||||
let scannedAddress = value
|
||||
|
||||
if (scannedAddress.startsWith('ethereum:')) {
|
||||
scannedAddress = scannedAddress.replace('ethereum:', '')
|
||||
}
|
||||
|
||||
onScannedValue(scannedAddress)
|
||||
closeQrModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<Field
|
||||
component={TextField}
|
||||
name={name}
|
||||
placeholder={text}
|
||||
testId={name}
|
||||
text={text}
|
||||
type="text"
|
||||
validate={validate}
|
||||
/>
|
||||
</Col>
|
||||
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||
<ScanQRWrapper handleScan={handleScan} />
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EthAddressInput
|
|
@ -0,0 +1,65 @@
|
|||
// @flow
|
||||
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import { useField } from 'react-final-form'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import Field from '~/components/forms/Field'
|
||||
import TextField from '~/components/forms/TextField'
|
||||
import { composeValidators, maxValue, mustBeFloat } from '~/components/forms/validator'
|
||||
import ButtonLink from '~/components/layout/ButtonLink'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import ABIService from '~/logic/contractInteraction/sources/ABIService'
|
||||
import { styles } from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const EthValue = ({ onSetMax }: { onSetMax: (string) => void }) => {
|
||||
const classes = useStyles()
|
||||
const { ethBalance } = useSelector(safeSelector)
|
||||
const {
|
||||
input: { value: method },
|
||||
} = useField('selectedMethod', { value: true })
|
||||
const disabled = !ABIService.isPayable(method)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row className={classes.fullWidth} margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Value
|
||||
</Paragraph>
|
||||
<ButtonLink
|
||||
color={disabled ? 'disabled' : 'secondary'}
|
||||
onClick={() => !disabled && onSetMax(ethBalance)}
|
||||
weight="bold"
|
||||
>
|
||||
Send max
|
||||
</ButtonLink>
|
||||
</Row>
|
||||
<Row margin="md">
|
||||
<Col>
|
||||
<Field
|
||||
className={classes.addressInput}
|
||||
component={TextField}
|
||||
disabled={disabled}
|
||||
inputAdornment={{
|
||||
endAdornment: <InputAdornment position="end">ETH</InputAdornment>,
|
||||
disabled,
|
||||
}}
|
||||
name="value"
|
||||
placeholder="Value"
|
||||
text="Value"
|
||||
type="text"
|
||||
validate={!disabled && composeValidators(mustBeFloat, maxValue(ethBalance))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EthValue
|
|
@ -0,0 +1,21 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
|
||||
import Col from '~/components/layout/Col'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Row from '~/components/layout/Row'
|
||||
import ArrowDown from '~/routes/safe/components/Balances/SendModal/screens/assets/arrow-down.svg'
|
||||
import { sm } from '~/theme/variables'
|
||||
|
||||
const FormDivisor = () => (
|
||||
<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>
|
||||
)
|
||||
|
||||
export default FormDivisor
|
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
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 Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { styles } from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Header = ({ onClose, subTitle, title }: { onClose: () => void, title: string, subTitle: string }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
{title}
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>{subTitle}</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
|
@ -0,0 +1,146 @@
|
|||
// @flow
|
||||
import InputBase from '@material-ui/core/InputBase'
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import Menu from '@material-ui/core/Menu'
|
||||
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 { useField, useFormState } from 'react-final-form'
|
||||
|
||||
import Col from '~/components/layout/Col'
|
||||
import Row from '~/components/layout/Row'
|
||||
import EtherscanService from '~/logic/contractInteraction/sources/EtherscanService'
|
||||
import { NO_CONTRACT } from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
import CheckIcon from '~/routes/safe/components/CurrencyDropdown/img/check.svg'
|
||||
import { useDropdownStyles } from '~/routes/safe/components/CurrencyDropdown/style'
|
||||
import { DropdownListTheme } from '~/theme/mui'
|
||||
|
||||
const MENU_WIDTH = '452px'
|
||||
|
||||
const MethodsDropdown = ({ onChange }: { onChange: (any) => void }) => {
|
||||
const classes = useDropdownStyles({ buttonWidth: MENU_WIDTH })
|
||||
const {
|
||||
input: { value: abi },
|
||||
meta: { valid },
|
||||
} = useField('abi', { value: true, valid: true })
|
||||
const {
|
||||
initialValues: { selectedMethod: selectedMethodByDefault },
|
||||
} = useFormState({ subscription: { initialValues: true } })
|
||||
const [selectedMethod, setSelectedMethod] = React.useState(selectedMethodByDefault ? selectedMethodByDefault : {})
|
||||
const [methodsList, setMethodsList] = React.useState([])
|
||||
const [methodsListFiltered, setMethodsListFiltered] = React.useState([])
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
|
||||
const [searchParams, setSearchParams] = React.useState('')
|
||||
|
||||
React.useEffect(() => {
|
||||
if (abi) {
|
||||
try {
|
||||
setMethodsList(EtherscanService.extractUsefulMethods(JSON.parse(abi)))
|
||||
} catch (e) {
|
||||
setMethodsList([])
|
||||
}
|
||||
}
|
||||
}, [abi])
|
||||
|
||||
React.useMemo(() => {
|
||||
setMethodsListFiltered(methodsList.filter(({ name }) => name.toLowerCase().includes(searchParams.toLowerCase())))
|
||||
}, [methodsList, searchParams])
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const onMethodSelectedChanged = (chosenMethod) => {
|
||||
setSelectedMethod(chosenMethod)
|
||||
onChange(chosenMethod)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
return !valid || !abi || abi === NO_CONTRACT ? null : (
|
||||
<Row margin="sm">
|
||||
<Col>
|
||||
<MuiThemeProvider theme={DropdownListTheme}>
|
||||
<>
|
||||
<button className={classes.button} onClick={handleClick} type="button">
|
||||
<span className={classNames(classes.buttonInner, anchorEl && classes.openMenuButton)}>
|
||||
{selectedMethod.name}
|
||||
</span>
|
||||
</button>
|
||||
<Menu
|
||||
PaperProps={{ style: { width: MENU_WIDTH } }}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
horizontal: 'center',
|
||||
vertical: 'bottom',
|
||||
}}
|
||||
elevation={0}
|
||||
getContentAnchorEl={null}
|
||||
id="customizedMenu"
|
||||
keepMounted
|
||||
onClose={handleClose}
|
||||
open={!!anchorEl}
|
||||
rounded={0}
|
||||
transformOrigin={{
|
||||
horizontal: 'center',
|
||||
vertical: 'top',
|
||||
}}
|
||||
>
|
||||
<MenuItem className={classes.listItemSearch} key="0">
|
||||
<div className={classes.search}>
|
||||
<div className={classes.searchIcon}>
|
||||
<SearchIcon />
|
||||
</div>
|
||||
<InputBase
|
||||
classes={{
|
||||
root: classes.inputRoot,
|
||||
input: classes.inputInput,
|
||||
}}
|
||||
inputProps={{ 'aria-label': 'search' }}
|
||||
onChange={(event) => setSearchParams(event.target.value)}
|
||||
placeholder="Search…"
|
||||
value={searchParams}
|
||||
/>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<div className={classes.dropdownItemsScrollWrapper}>
|
||||
{methodsListFiltered.map((method) => {
|
||||
const { action, name, signatureHash } = method
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className={classes.listItem}
|
||||
key={signatureHash}
|
||||
onClick={() => onMethodSelectedChanged(method)}
|
||||
value={signatureHash}
|
||||
>
|
||||
<ListItemText primary={name} />
|
||||
<ListItemIcon className={classes.iconRight}>
|
||||
{signatureHash === selectedMethod.signatureHash ? (
|
||||
<img alt="checked" src={CheckIcon} />
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemIcon className={classes.iconRight}>
|
||||
<div>{action}</div>
|
||||
</ListItemIcon>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Menu>
|
||||
</>
|
||||
</MuiThemeProvider>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default MethodsDropdown
|
|
@ -0,0 +1,45 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import { useField } from 'react-final-form'
|
||||
|
||||
import Field from '~/components/forms/Field'
|
||||
import TextField from '~/components/forms/TextField'
|
||||
import { composeValidators, mustBeEthereumAddress, required } from '~/components/forms/validator'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Row from '~/components/layout/Row'
|
||||
|
||||
const RenderInputParams = () => {
|
||||
const {
|
||||
meta: { valid: validABI },
|
||||
} = useField('abi', { valid: true })
|
||||
const {
|
||||
input: { value: method },
|
||||
} = useField('selectedMethod', { value: true })
|
||||
const renderInputs = validABI && !!method && method.inputs.length
|
||||
|
||||
return !renderInputs
|
||||
? null
|
||||
: method.inputs.map(({ name, type }, index) => {
|
||||
const placeholder = name ? `${name} (${type})` : type
|
||||
const key = `methodInput-${method.name}_${index}_${type}`
|
||||
const validate = type === 'address' ? composeValidators(required, mustBeEthereumAddress) : required
|
||||
|
||||
return (
|
||||
<Row key={key} margin="sm">
|
||||
<Col>
|
||||
<Field
|
||||
component={TextField}
|
||||
name={key}
|
||||
placeholder={placeholder}
|
||||
testId={key}
|
||||
text={placeholder}
|
||||
type="text"
|
||||
validate={validate}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default RenderInputParams
|
|
@ -0,0 +1,41 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import { useField } from 'react-final-form'
|
||||
|
||||
import TextField from '~/components/forms/TextField'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Row from '~/components/layout/Row'
|
||||
|
||||
const RenderOutputParams = () => {
|
||||
const {
|
||||
input: { value: method },
|
||||
} = useField('selectedMethod', { value: true })
|
||||
const {
|
||||
input: { value: results },
|
||||
} = useField('callResults', { value: true })
|
||||
const multipleResults = !!method && method.outputs.length > 1
|
||||
|
||||
return results
|
||||
? method.outputs.map(({ name, type }, index) => {
|
||||
const placeholder = name ? `${name} (${type})` : type
|
||||
const key = `methodCallResult-${method.name}_${index}_${type}`
|
||||
const value = multipleResults ? results[index] : results
|
||||
|
||||
return (
|
||||
<Row key={key} margin="sm">
|
||||
<Col>
|
||||
<TextField
|
||||
disabled
|
||||
input={{ name: key, value, placeholder, type: 'text' }}
|
||||
meta={{ valid: true }}
|
||||
testId={key}
|
||||
text={placeholder}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
: null
|
||||
}
|
||||
|
||||
export default RenderOutputParams
|
|
@ -1,18 +1,12 @@
|
|||
// @flow
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import { withSnackbar } 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 '~/components/CopyBtn'
|
||||
import EtherscanBtn from '~/components/EtherscanBtn'
|
||||
import Identicon from '~/components/Identicon'
|
||||
import AddressInfo from '~/components/AddressInfo'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Col from '~/components/layout/Col'
|
||||
|
@ -25,11 +19,10 @@ import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew'
|
|||
import { formatAmount } from '~/logic/tokens/utils/formatAmount'
|
||||
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import Header from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
||||
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
import { sm } from '~/theme/variables'
|
||||
|
||||
type Props = {
|
||||
closeSnackbar: () => void,
|
||||
|
@ -41,10 +34,10 @@ type Props = {
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ReviewCustomTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }: Props) => {
|
||||
const ContractInteractionReview = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }: Props) => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
|
||||
const { address: safeAddress } = useSelector(safeSelector)
|
||||
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -54,7 +47,7 @@ const ReviewCustomTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }:
|
|||
const { fromWei, toBN } = getWeb3().utils
|
||||
const txData = tx.data ? tx.data.trim() : ''
|
||||
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.recipientAddress, txData)
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.contractAddress, txData)
|
||||
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
|
||||
const formattedGasCosts = formatAmount(gasCostsAsEth)
|
||||
|
||||
|
@ -72,7 +65,7 @@ const ReviewCustomTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }:
|
|||
|
||||
const submitTx = async () => {
|
||||
const web3 = getWeb3()
|
||||
const txRecipient = tx.recipientAddress
|
||||
const txRecipient = tx.contractAddress
|
||||
const txData = tx.data ? tx.data.trim() : ''
|
||||
const txValue = tx.value ? web3.utils.toWei(tx.value, 'ether') : '0'
|
||||
|
||||
|
@ -93,44 +86,16 @@ const ReviewCustomTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }:
|
|||
|
||||
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>
|
||||
<Header onClose={onClose} subTitle="2 of 2" title="Contract Interaction" />
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<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
|
||||
Contract Address
|
||||
</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} />
|
||||
<EtherscanBtn type="address" value={tx.recipientAddress} />
|
||||
</Block>
|
||||
</Col>
|
||||
<AddressInfo safeAddress={tx.contractAddress} />
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
|
@ -138,12 +103,46 @@ const ReviewCustomTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }:
|
|||
</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'}
|
||||
<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}
|
||||
{' ETH'}
|
||||
</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 = `methodInput-${tx.selectedMethod.name}_${index}_${type}`
|
||||
|
||||
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 }}>
|
||||
{tx[key]}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Data (hex encoded)
|
||||
|
@ -183,4 +182,4 @@ const ReviewCustomTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }:
|
|||
)
|
||||
}
|
||||
|
||||
export default withSnackbar(ReviewCustomTx)
|
||||
export default withSnackbar(ContractInteractionReview)
|
|
@ -0,0 +1,89 @@
|
|||
// @flow
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import GnoForm from '~/components/forms/GnoForm'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import Buttons from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Buttons'
|
||||
import ContractABI from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ContractABI'
|
||||
import EthAddressInput from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput'
|
||||
import EthValue from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthValue'
|
||||
import FormDivisor from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/FormDivisor'
|
||||
import Header from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
||||
import MethodsDropdown from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/MethodsDropdown'
|
||||
import RenderInputParams from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams'
|
||||
import RenderOutputParams from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderOutputParams'
|
||||
import {
|
||||
abiExtractor,
|
||||
createTxObject,
|
||||
formMutators,
|
||||
} from '~/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
|
||||
type Props = {
|
||||
initialValues: Object,
|
||||
onClose: () => void,
|
||||
onNext: (any) => void,
|
||||
contractAddress?: string,
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }: Props) => {
|
||||
const classes = useStyles()
|
||||
|
||||
React.useMemo(() => {
|
||||
if (contractAddress) {
|
||||
initialValues.contractAddress = contractAddress
|
||||
}
|
||||
}, [contractAddress])
|
||||
|
||||
const handleSubmit = async ({ contractAddress, selectedMethod, value, ...values }: {}) => {
|
||||
if (value || (contractAddress && selectedMethod)) {
|
||||
const data = await createTxObject(selectedMethod, contractAddress, values).encodeABI()
|
||||
onNext({ contractAddress, data, selectedMethod, value, ...values })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header onClose={onClose} subTitle="1 of 2" title="Contract Interaction" />
|
||||
<Hairline />
|
||||
<GnoForm
|
||||
decorators={[abiExtractor]}
|
||||
formMutators={formMutators}
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
subscription={{ submitting: true, pristine: true }}
|
||||
>
|
||||
{(submitting, validating, rest, mutators) => {
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.formContainer}>
|
||||
<SafeInfo />
|
||||
<FormDivisor />
|
||||
<EthAddressInput
|
||||
name="contractAddress"
|
||||
onScannedValue={mutators.setContractAddress}
|
||||
text="Contract Address*"
|
||||
/>
|
||||
<EthValue onSetMax={mutators.setMax} />
|
||||
<ContractABI />
|
||||
<MethodsDropdown onChange={mutators.setSelectedMethod} />
|
||||
<RenderInputParams />
|
||||
<RenderOutputParams />
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Buttons onCallSubmit={mutators.setCallResults} onClose={onClose} />
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</GnoForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContractInteraction
|
|
@ -48,4 +48,7 @@ export const styles = () => ({
|
|||
selectAddress: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
fullWidth: {
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,59 @@
|
|||
// @flow
|
||||
import createDecorator from 'final-form-calculate'
|
||||
|
||||
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from '~/components/forms/validator'
|
||||
import { getNetwork } from '~/config'
|
||||
import { getConfiguredSource } from '~/logic/contractInteraction/sources'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
|
||||
export const NO_CONTRACT = 'no contract'
|
||||
|
||||
export const abiExtractor = createDecorator({
|
||||
field: 'contractAddress',
|
||||
updates: {
|
||||
abi: async (contractAddress) => {
|
||||
if (
|
||||
!contractAddress ||
|
||||
mustBeEthereumAddress(contractAddress) ||
|
||||
(await mustBeEthereumContractAddress(contractAddress))
|
||||
) {
|
||||
return NO_CONTRACT
|
||||
}
|
||||
const network = getNetwork()
|
||||
const source = getConfiguredSource()
|
||||
return source.getContractABI(contractAddress, network)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const formMutators = {
|
||||
setMax: (args, state, utils) => {
|
||||
utils.changeValue(state, 'value', () => args[0])
|
||||
},
|
||||
setContractAddress: (args, state, utils) => {
|
||||
utils.changeValue(state, 'contractAddress', () => args[0])
|
||||
},
|
||||
setSelectedMethod: (args, state, utils) => {
|
||||
const modified =
|
||||
state.lastFormState.values.selectedMethod && state.lastFormState.values.selectedMethod.name !== args[0].name
|
||||
|
||||
if (modified) {
|
||||
utils.changeValue(state, 'callResults', () => '')
|
||||
utils.changeValue(state, 'value', () => '')
|
||||
}
|
||||
|
||||
utils.changeValue(state, 'selectedMethod', () => args[0])
|
||||
},
|
||||
setCallResults: (args, state, utils) => {
|
||||
utils.changeValue(state, 'callResults', () => args[0])
|
||||
},
|
||||
}
|
||||
|
||||
export const createTxObject = (method, contractAddress, values) => {
|
||||
const web3 = getWeb3()
|
||||
const contract = new web3.eth.Contract([method], contractAddress)
|
||||
const { inputs, name } = method
|
||||
const args = inputs.map(({ type }, index) => values[`methodInput-${name}_${index}_${type}`])
|
||||
|
||||
return contract.methods[name](...args)
|
||||
}
|
|
@ -53,7 +53,7 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx
|
|||
const classes = useStyles()
|
||||
const shortener = textShortener()
|
||||
const dispatch = useDispatch()
|
||||
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
|
||||
const { address: safeAddress } = useSelector(safeSelector)
|
||||
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
||||
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
|
||||
const txToken = nftTokens.find(
|
||||
|
@ -121,7 +121,7 @@ const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx
|
|||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
|
|
|
@ -48,7 +48,7 @@ const useStyles = makeStyles(styles)
|
|||
const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }: Props) => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
|
||||
const { address: safeAddress } = useSelector(safeSelector)
|
||||
const tokens = useSelector(extendedSafeTokensSelector)
|
||||
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
|
||||
const [data, setData] = useState('')
|
||||
|
@ -125,7 +125,7 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }: Props
|
|||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
|
|
|
@ -31,7 +31,6 @@ import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
|||
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import CollectibleSelectField from '~/routes/safe/components/Balances/SendModal/screens/SendCollectible/CollectibleSelectField'
|
||||
import TokenSelectField from '~/routes/safe/components/Balances/SendModal/screens/SendCollectible/TokenSelectField'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
import { sm } from '~/theme/variables'
|
||||
|
||||
type Props = {
|
||||
|
@ -58,7 +57,6 @@ const useStyles = makeStyles(styles)
|
|||
|
||||
const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = {} }: Props) => {
|
||||
const classes = useStyles()
|
||||
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
|
||||
const nftAssets: NFTAssetsState = useSelector(safeActiveSelectorMap)
|
||||
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
||||
const addressBook: AddressBook = useSelector(getAddressBook)
|
||||
|
@ -130,7 +128,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
|
|||
<>
|
||||
<WhenFieldChanges field="assetAddress" set="nftTokenId" to={''} />
|
||||
<Block className={classes.formContainer}>
|
||||
<SafeInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
|
|
|
@ -1,251 +1 @@
|
|||
// @flow
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||
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 CopyBtn from '~/components/CopyBtn'
|
||||
import EtherscanBtn from '~/components/EtherscanBtn'
|
||||
import Identicon from '~/components/Identicon'
|
||||
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
|
||||
import Field from '~/components/forms/Field'
|
||||
import GnoForm from '~/components/forms/GnoForm'
|
||||
import TextField from '~/components/forms/TextField'
|
||||
import TextareaField from '~/components/forms/TextareaField'
|
||||
import { composeValidators, maxValue, mustBeFloat } from '~/components/forms/validator'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Button from '~/components/layout/Button'
|
||||
import ButtonLink from '~/components/layout/ButtonLink'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
|
||||
import { getAddressBook } from '~/logic/addressBook/store/selectors'
|
||||
import { getNameFromAdbk } from '~/logic/addressBook/utils'
|
||||
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
import { sm } from '~/theme/variables'
|
||||
|
||||
type Props = {
|
||||
initialValues: Object,
|
||||
onClose: () => void,
|
||||
onNext: (any) => void,
|
||||
recipientAddress: string,
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Props) => {
|
||||
const classes = useStyles()
|
||||
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
|
||||
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
|
||||
address: recipientAddress || initialValues.recipientAddress,
|
||||
name: '',
|
||||
})
|
||||
const [pristine, setPristine] = useState<boolean>(true)
|
||||
const [isValidAddress, setIsValidAddress] = useState<boolean>(true)
|
||||
const addressBook: AddressBook = useSelector(getAddressBook)
|
||||
|
||||
React.useMemo(() => {
|
||||
if (selectedEntry === null && pristine) {
|
||||
setPristine(false)
|
||||
}
|
||||
}, [selectedEntry, pristine])
|
||||
|
||||
const handleSubmit = (values: Object) => {
|
||||
if (values.data || values.value) {
|
||||
onNext(values)
|
||||
}
|
||||
}
|
||||
|
||||
const formMutators = {
|
||||
setMax: (args, state, utils) => {
|
||||
utils.changeValue(state, 'value', () => ethBalance)
|
||||
},
|
||||
setRecipient: (args, state, utils) => {
|
||||
utils.changeValue(state, 'recipientAddress', () => 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} onSubmit={handleSubmit}>
|
||||
{(...args) => {
|
||||
const mutators = args[3]
|
||||
|
||||
let shouldDisableSubmitButton = !isValidAddress
|
||||
if (selectedEntry) {
|
||||
shouldDisableSubmitButton = !selectedEntry.address
|
||||
}
|
||||
|
||||
const handleScan = (value, closeQrModal) => {
|
||||
let scannedAddress = value
|
||||
|
||||
if (scannedAddress.startsWith('ethereum:')) {
|
||||
scannedAddress = scannedAddress.replace('ethereum:', '')
|
||||
}
|
||||
const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : ''
|
||||
mutators.setRecipient(scannedAddress)
|
||||
setSelectedEntry({
|
||||
name: scannedName,
|
||||
address: scannedAddress,
|
||||
})
|
||||
closeQrModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.formContainer}>
|
||||
<SafeInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<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}>
|
||||
<ScanQRWrapper handleScan={handleScan} />
|
||||
</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
|
||||
className={classes.addressInput}
|
||||
component={TextField}
|
||||
inputAdornment={{
|
||||
endAdornment: <InputAdornment position="end">ETH</InputAdornment>,
|
||||
}}
|
||||
name="value"
|
||||
placeholder="Value*"
|
||||
text="Value*"
|
||||
type="text"
|
||||
validate={composeValidators(mustBeFloat, maxValue(ethBalance))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="sm">
|
||||
<Col>
|
||||
<TextareaField
|
||||
name="data"
|
||||
placeholder="Data (hex encoded)*"
|
||||
text="Data (hex encoded)*"
|
||||
type="text"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</GnoForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendCustomTx
|
||||
|
|
|
@ -34,7 +34,6 @@ import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
|||
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import TokenSelectField from '~/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField'
|
||||
import { extendedSafeTokensSelector } from '~/routes/safe/container/selector'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
import { sm } from '~/theme/variables'
|
||||
|
||||
type Props = {
|
||||
|
@ -61,7 +60,6 @@ const useStyles = makeStyles(styles)
|
|||
|
||||
const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = '' }: Props) => {
|
||||
const classes = useStyles()
|
||||
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
|
||||
const tokens: Token = useSelector(extendedSafeTokensSelector)
|
||||
const addressBook: AddressBook = useSelector(getAddressBook)
|
||||
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
|
||||
|
@ -128,7 +126,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
|
|||
return (
|
||||
<>
|
||||
<Block className={classes.formContainer}>
|
||||
<SafeInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<SafeInfo />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
|
|
|
@ -21,8 +21,8 @@ import Img from '~/components/layout/Img'
|
|||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { type Token, type TokenProps } from '~/logic/tokens/store/model/token'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import TokenPlaceholder from '~/routes/safe/components/Balances/assets/token_placeholder.svg'
|
||||
import { checksumAddress } from '~/utils/checksumAddress'
|
||||
|
||||
export const ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID = 'add-custom-token-address-input'
|
||||
export const ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID = 'add-custom-token-symbols-input'
|
||||
|
@ -65,7 +65,7 @@ const AddCustomToken = (props: Props) => {
|
|||
const [formValues, setFormValues] = useState(INITIAL_FORM_STATE)
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
const address = getWeb3().utils.toChecksumAddress(values.address)
|
||||
const address = checksumAddress(values.address)
|
||||
const token = {
|
||||
address,
|
||||
decimals: values.decimals,
|
||||
|
|
|
@ -15,7 +15,7 @@ import Link from '~/components/layout/Link'
|
|||
import Row from '~/components/layout/Row'
|
||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||
import SendModal from '~/routes/safe/components/Balances/SendModal'
|
||||
import DropdownCurrency from '~/routes/safe/components/DropdownCurrency'
|
||||
import CurrencyDropdown from '~/routes/safe/components/CurrencyDropdown'
|
||||
import { useFetchTokens } from '~/routes/safe/container/Hooks/useFetchTokens'
|
||||
import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors'
|
||||
import { history } from '~/store'
|
||||
|
@ -170,7 +170,7 @@ const Balances = (props: Props) => {
|
|||
))}
|
||||
</Col>
|
||||
<Col className={tokenControls} end="sm" sm={6} xs={12}>
|
||||
{showCoins && <DropdownCurrency />}
|
||||
{showCoins && <CurrencyDropdown />}
|
||||
<ButtonLink
|
||||
className={manageTokensButton}
|
||||
onClick={erc721Enabled && showCollectibles ? () => onShow('ManageCollectibleModal') : () => onShow('Token')}
|
||||
|
|
Before Width: | Height: | Size: 188 B After Width: | Height: | Size: 188 B |
|
@ -16,11 +16,11 @@ import CheckIcon from './img/check.svg'
|
|||
import { setSelectedCurrency } from '~/logic/currencyValues/store/actions/setSelectedCurrency'
|
||||
import { AVAILABLE_CURRENCIES } from '~/logic/currencyValues/store/model/currencyValues'
|
||||
import { currentCurrencySelector } from '~/logic/currencyValues/store/selectors'
|
||||
import { useDropdownStyles } from '~/routes/safe/components/DropdownCurrency/style'
|
||||
import { useDropdownStyles } from '~/routes/safe/components/CurrencyDropdown/style'
|
||||
import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors'
|
||||
import { DropdownListTheme } from '~/theme/mui'
|
||||
|
||||
const DropdownCurrency = () => {
|
||||
const CurrencyDropdown = () => {
|
||||
const currenciesList = Object.values(AVAILABLE_CURRENCIES)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const dispatch = useDispatch()
|
||||
|
@ -122,4 +122,4 @@ const DropdownCurrency = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export default DropdownCurrency
|
||||
export default CurrencyDropdown
|
|
@ -4,11 +4,11 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
const buttonWidth = '140px'
|
||||
export const useDropdownStyles = makeStyles({
|
||||
listItem: {
|
||||
maxWidth: buttonWidth,
|
||||
maxWidth: (props) => (props.buttonWidth ? props.buttonWidth : buttonWidth),
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
listItemSearch: {
|
||||
maxWidth: buttonWidth,
|
||||
maxWidth: (props) => (props.buttonWidth ? props.buttonWidth : buttonWidth),
|
||||
padding: '0',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
|
@ -37,7 +37,7 @@ export const useDropdownStyles = makeStyles({
|
|||
height: '24px',
|
||||
lineHeight: '1.33',
|
||||
marginRight: '20px',
|
||||
minWidth: buttonWidth,
|
||||
minWidth: (props) => (props.buttonWidth ? props.buttonWidth : buttonWidth),
|
||||
outline: 'none',
|
||||
padding: '0',
|
||||
textAlign: 'left',
|
|
@ -26,6 +26,34 @@ type Props = {
|
|||
location: Object,
|
||||
}
|
||||
|
||||
const BalancesLabel = (
|
||||
<>
|
||||
<BalancesIcon />
|
||||
Assets
|
||||
</>
|
||||
)
|
||||
|
||||
const AddressBookLabel = (
|
||||
<>
|
||||
<AddressBookIcon />
|
||||
Address Book
|
||||
</>
|
||||
)
|
||||
|
||||
const AppsLabel = (
|
||||
<>
|
||||
<AppsIcon />
|
||||
Apps
|
||||
</>
|
||||
)
|
||||
|
||||
const TransactionsLabel = (
|
||||
<>
|
||||
<TransactionsIcon />
|
||||
Transactions
|
||||
</>
|
||||
)
|
||||
|
||||
const TabsComponent = (props: Props) => {
|
||||
const { classes, location, match } = props
|
||||
|
||||
|
@ -47,33 +75,6 @@ const TabsComponent = (props: Props) => {
|
|||
return pathname
|
||||
}
|
||||
|
||||
const labelBalances = (
|
||||
<>
|
||||
<BalancesIcon />
|
||||
Assets
|
||||
</>
|
||||
)
|
||||
|
||||
const labelAddressBook = (
|
||||
<>
|
||||
<AddressBookIcon />
|
||||
Address Book
|
||||
</>
|
||||
)
|
||||
|
||||
const labelApps = (
|
||||
<>
|
||||
<AppsIcon />
|
||||
Apps
|
||||
</>
|
||||
)
|
||||
|
||||
const labelTransactions = (
|
||||
<>
|
||||
<TransactionsIcon />
|
||||
Transactions
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<Tabs
|
||||
indicatorColor="secondary"
|
||||
|
@ -88,7 +89,7 @@ const TabsComponent = (props: Props) => {
|
|||
wrapper: classes.tabWrapper,
|
||||
}}
|
||||
data-testid={BALANCES_TAB_BTN_TEST_ID}
|
||||
label={labelBalances}
|
||||
label={BalancesLabel}
|
||||
value={`${match.url}/balances`}
|
||||
/>
|
||||
<Tab
|
||||
|
@ -97,7 +98,7 @@ const TabsComponent = (props: Props) => {
|
|||
wrapper: classes.tabWrapper,
|
||||
}}
|
||||
data-testid={TRANSACTIONS_TAB_BTN_TEST_ID}
|
||||
label={labelTransactions}
|
||||
label={TransactionsLabel}
|
||||
value={`${match.url}/transactions`}
|
||||
/>
|
||||
{process.env.REACT_APP_APPS_DISABLED !== 'true' && (
|
||||
|
@ -107,7 +108,7 @@ const TabsComponent = (props: Props) => {
|
|||
wrapper: classes.tabWrapper,
|
||||
}}
|
||||
data-testid={TRANSACTIONS_TAB_BTN_TEST_ID}
|
||||
label={labelApps}
|
||||
label={AppsLabel}
|
||||
value={`${match.url}/apps`}
|
||||
/>
|
||||
)}
|
||||
|
@ -117,7 +118,7 @@ const TabsComponent = (props: Props) => {
|
|||
wrapper: classes.tabWrapper,
|
||||
}}
|
||||
data-testid={ADDRESS_BOOK_TAB_BTN_TEST_ID}
|
||||
label={labelAddressBook}
|
||||
label={AddressBookLabel}
|
||||
value={`${match.url}/address-book`}
|
||||
/>
|
||||
<Tab
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// @flow
|
||||
import { GenericModal } from '@gnosis.pm/safe-react-components'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
@ -8,7 +9,6 @@ import Receive from '../Balances/Receive'
|
|||
|
||||
import { styles } from './style'
|
||||
|
||||
import { GenericModal } from '~/components-v2'
|
||||
import Modal from '~/components/Modal'
|
||||
import NoSafe from '~/components/NoSafe'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
|
@ -33,7 +33,6 @@ const TxsTable = React.lazy(() => import('~/routes/safe/components/Transactions/
|
|||
const AddressBookTable = React.lazy(() => import('~/routes/safe/components/AddressBook'))
|
||||
|
||||
type Props = {
|
||||
classes: Object,
|
||||
sendFunds: Object,
|
||||
showReceive: boolean,
|
||||
onShow: Function,
|
||||
|
|
|
@ -17,6 +17,7 @@ import addSafeOwner from '~/routes/safe/store/actions/addSafeOwner'
|
|||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||
import { safeOwnersSelector, safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors'
|
||||
import { checksumAddress } from '~/utils/checksumAddress'
|
||||
|
||||
const styles = () => ({
|
||||
biggerModalWindow: {
|
||||
|
@ -91,7 +92,7 @@ const AddOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose }:
|
|||
setValues((stateValues) => ({
|
||||
...stateValues,
|
||||
ownerName: newValues.ownerName,
|
||||
ownerAddress: newValues.ownerAddress,
|
||||
ownerAddress: checksumAddress(newValues.ownerAddress),
|
||||
}))
|
||||
setActiveScreen('selectThreshold')
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
|
|||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||
import replaceSafeOwner from '~/routes/safe/store/actions/replaceSafeOwner'
|
||||
import { safeParamAddressFromStateSelector, safeThresholdSelector } from '~/routes/safe/store/selectors'
|
||||
import { checksumAddress } from '~/utils/checksumAddress'
|
||||
|
||||
const styles = () => ({
|
||||
biggerModalWindow: {
|
||||
|
@ -96,8 +97,10 @@ const ReplaceOwner = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClose
|
|||
const onClickBack = () => setActiveScreen('checkOwner')
|
||||
|
||||
const ownerSubmitted = (newValues: Object) => {
|
||||
values.ownerName = newValues.ownerName
|
||||
values.ownerAddress = newValues.ownerAddress
|
||||
const { ownerAddress, ownerName } = newValues
|
||||
const checksumAddr = checksumAddress(ownerAddress)
|
||||
values.ownerName = ownerName
|
||||
values.ownerAddress = checksumAddr
|
||||
setValues(values)
|
||||
setActiveScreen('reviewReplaceOwner')
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@ class ManageOwners extends React.Component<Props, State> {
|
|||
{autoColumns.map((column: Column) => (
|
||||
<TableCell align={column.align} component="td" key={column.id} style={cellWidth(column.width)}>
|
||||
{column.id === OWNERS_TABLE_ADDRESS_ID ? (
|
||||
<OwnerAddressTableCell address={row[column.id]} />
|
||||
<OwnerAddressTableCell address={row[column.id]} showLinks />
|
||||
) : (
|
||||
row[column.id]
|
||||
)}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
// @flow
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import EtherscanLink from '~/components/EtherscanLink'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Bold from '~/components/layout/Bold'
|
||||
import { getNameFromAddressBook } from '~/logic/addressBook/utils'
|
||||
import { getNameFromAddressBook } from '~/logic/addressBook/store/selectors'
|
||||
import OwnerAddressTableCell from '~/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
import { getIncomingTxAmount } from '~/routes/safe/components/Transactions/TxsTable/columns'
|
||||
import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||
|
@ -46,7 +47,7 @@ const TransferDescription = ({ from, txFromName, value = '' }: TransferDescProps
|
|||
|
||||
const IncomingTxDescription = ({ tx }: Props) => {
|
||||
const classes = useStyles()
|
||||
const txFromName = getNameFromAddressBook(tx.from)
|
||||
const txFromName = useSelector((state) => getNameFromAddressBook(state, tx.from))
|
||||
return (
|
||||
<Block className={classes.txDataContainer}>
|
||||
<TransferDescription from={tx.from} txFromName={txFromName} value={getIncomingTxAmount(tx, false)} />
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { withStyles } from '@material-ui/core/styles'
|
||||
import cn from 'classnames'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import CancelSmallFilledCircle from './assets/cancel-small-filled.svg'
|
||||
import ConfirmSmallFilledCircle from './assets/confirm-small-filled.svg'
|
||||
|
@ -16,8 +17,7 @@ import Block from '~/components/layout/Block'
|
|||
import Button from '~/components/layout/Button'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import { getNameFromAddressBook } from '~/logic/addressBook/utils'
|
||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||
import { getNameFromAddressBook } from '~/logic/addressBook/store/selectors'
|
||||
|
||||
export const CONFIRM_TX_BTN_TEST_ID = 'confirm-btn'
|
||||
export const EXECUTE_TX_BTN_TEST_ID = 'execute-btn'
|
||||
|
@ -32,7 +32,7 @@ type OwnerProps = {
|
|||
onTxReject?: Function,
|
||||
onTxConfirm: Function,
|
||||
onTxExecute: Function,
|
||||
owner: Owner,
|
||||
owner: string,
|
||||
showRejectBtn: boolean,
|
||||
showExecuteRejectBtn: boolean,
|
||||
showConfirmBtn: boolean,
|
||||
|
@ -57,8 +57,7 @@ const OwnerComponent = ({
|
|||
thresholdReached,
|
||||
userAddress,
|
||||
}: OwnerProps) => {
|
||||
const nameInAdbk = getNameFromAddressBook(owner.address)
|
||||
const ownerName = nameInAdbk || owner.name
|
||||
const nameInAdbk = useSelector((state) => getNameFromAddressBook(state, owner))
|
||||
const [imgCircle, setImgCircle] = React.useState(ConfirmSmallGreyCircle)
|
||||
|
||||
React.useMemo(() => {
|
||||
|
@ -77,15 +76,15 @@ const OwnerComponent = ({
|
|||
<div className={classes.circleState}>
|
||||
<Img alt="" src={imgCircle} />
|
||||
</div>
|
||||
<Identicon address={owner.address} className={classes.icon} diameter={32} />
|
||||
<Identicon address={owner} className={classes.icon} diameter={32} />
|
||||
<Block>
|
||||
<Paragraph className={classes.name} noMargin>
|
||||
{ownerName}
|
||||
{nameInAdbk}
|
||||
</Paragraph>
|
||||
<EtherscanLink className={classes.address} cut={4} type="address" value={owner.address} />
|
||||
<EtherscanLink className={classes.address} cut={4} type="address" value={owner} />
|
||||
</Block>
|
||||
<Block className={classes.spacer} />
|
||||
{owner.address === userAddress && (
|
||||
{owner === userAddress && (
|
||||
<Block>
|
||||
{isCancelTx ? (
|
||||
<>
|
||||
|
@ -140,7 +139,7 @@ const OwnerComponent = ({
|
|||
)}
|
||||
</Block>
|
||||
)}
|
||||
{owner.address === executor && <Block className={classes.executor}>Executor</Block>}
|
||||
{owner === executor && <Block className={classes.executor}>Executor</Block>}
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ const OwnersList = ({
|
|||
confirmed
|
||||
executor={executor}
|
||||
isCancelTx={isCancelTx}
|
||||
key={owner.address}
|
||||
key={owner}
|
||||
onTxExecute={onTxExecute}
|
||||
onTxReject={onTxReject}
|
||||
owner={owner}
|
||||
|
@ -64,7 +64,7 @@ const OwnersList = ({
|
|||
classes={classes}
|
||||
executor={executor}
|
||||
isCancelTx={isCancelTx}
|
||||
key={owner.address}
|
||||
key={owner}
|
||||
onTxConfirm={onTxConfirm}
|
||||
onTxExecute={onTxExecute}
|
||||
onTxReject={onTxReject}
|
||||
|
|
|
@ -39,7 +39,7 @@ function getOwnersConfirmations(tx, userAddress) {
|
|||
let currentUserAlreadyConfirmed = false
|
||||
|
||||
tx.confirmations.forEach((conf) => {
|
||||
if (conf.owner.address === userAddress) {
|
||||
if (conf.owner === userAddress) {
|
||||
currentUserAlreadyConfirmed = true
|
||||
}
|
||||
|
||||
|
@ -52,18 +52,20 @@ function getOwnersConfirmations(tx, userAddress) {
|
|||
}
|
||||
|
||||
function getPendingOwnersConfirmations(owners, tx, userAddress) {
|
||||
const ownersUnconfirmed = owners.filter(
|
||||
(owner) => tx.confirmations.findIndex((conf) => conf.owner.address === owner.address) === -1,
|
||||
)
|
||||
const ownersNotConfirmed = []
|
||||
let currentUserNotConfirmed = true
|
||||
|
||||
let userIsUnconfirmedOwner = false
|
||||
|
||||
ownersUnconfirmed.some((owner) => {
|
||||
userIsUnconfirmedOwner = owner.address === userAddress
|
||||
return userIsUnconfirmedOwner
|
||||
owners.forEach((owner) => {
|
||||
const confirmationsEntry = tx.confirmations.find((conf) => conf.owner === owner.address)
|
||||
if (!confirmationsEntry) {
|
||||
ownersNotConfirmed.push(owner.address)
|
||||
}
|
||||
if (confirmationsEntry && confirmationsEntry.owner === userAddress) {
|
||||
currentUserNotConfirmed = false
|
||||
}
|
||||
})
|
||||
|
||||
return [ownersUnconfirmed, userIsUnconfirmedOwner]
|
||||
return [ownersNotConfirmed, currentUserNotConfirmed]
|
||||
}
|
||||
|
||||
const OwnersColumn = ({
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import React, { useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { getTxData } from './utils'
|
||||
|
||||
|
@ -9,7 +10,7 @@ import Block from '~/components/layout/Block'
|
|||
import Bold from '~/components/layout/Bold'
|
||||
import LinkWithRef from '~/components/layout/Link'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import { getNameFromAddressBook } from '~/logic/addressBook/utils'
|
||||
import { getNameFromAddressBook } from '~/logic/addressBook/store/selectors'
|
||||
import { SAFE_METHODS_NAMES } from '~/logic/contracts/methodIds'
|
||||
import { shortVersionOf } from '~/logic/wallets/ethAddresses'
|
||||
import OwnerAddressTableCell from '~/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
|
@ -68,7 +69,7 @@ type CustomDescProps = {
|
|||
}
|
||||
|
||||
const TransferDescription = ({ amount = '', recipient }: TransferDescProps) => {
|
||||
const recipientName = getNameFromAddressBook(recipient)
|
||||
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
|
||||
<Bold>Send {amount} to:</Bold>
|
||||
|
@ -82,7 +83,7 @@ const TransferDescription = ({ amount = '', recipient }: TransferDescProps) => {
|
|||
}
|
||||
|
||||
const RemovedOwner = ({ removedOwner }: { removedOwner: string }) => {
|
||||
const ownerChangedName = getNameFromAddressBook(removedOwner)
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, removedOwner))
|
||||
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID}>
|
||||
|
@ -97,7 +98,7 @@ const RemovedOwner = ({ removedOwner }: { removedOwner: string }) => {
|
|||
}
|
||||
|
||||
const AddedOwner = ({ addedOwner }: { addedOwner: string }) => {
|
||||
const ownerChangedName = getNameFromAddressBook(addedOwner)
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, addedOwner))
|
||||
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_ADD_OWNER_TEST_ID}>
|
||||
|
@ -161,7 +162,7 @@ const SettingsDescription = ({ action, addedOwner, newThreshold, removedOwner }:
|
|||
|
||||
const CustomDescription = ({ amount = 0, classes, data, recipient }: CustomDescProps) => {
|
||||
const [showTxData, setShowTxData] = useState(false)
|
||||
const recipientName = getNameFromAddressBook(recipient)
|
||||
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
|
||||
return (
|
||||
<>
|
||||
<Block data-testid={TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID}>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// @flow
|
||||
import { IconText, Loader } from '@gnosis.pm/safe-react-components'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import CustomTxIcon from './assets/custom.svg'
|
||||
|
@ -6,7 +7,7 @@ import IncomingTxIcon from './assets/incoming.svg'
|
|||
import OutgoingTxIcon from './assets/outgoing.svg'
|
||||
import SettingsTxIcon from './assets/settings.svg'
|
||||
|
||||
import { IconText, Loader } from '~/components-v2'
|
||||
import CustomIconText from '~/components/CustomIconText'
|
||||
import { getAppInfoFromOrigin, getAppInfoFromUrl } from '~/routes/safe/components/Apps/utils'
|
||||
import { type TransactionType } from '~/routes/safe/store/models/transaction'
|
||||
|
||||
|
@ -23,7 +24,7 @@ const typeToIcon = {
|
|||
const typeToLabel = {
|
||||
outgoing: 'Outgoing transfer',
|
||||
incoming: 'Incoming transfer',
|
||||
custom: 'Custom transaction',
|
||||
custom: 'Contract Interaction',
|
||||
settings: 'Modify settings',
|
||||
creation: 'Safe created',
|
||||
cancellation: 'Cancellation transaction',
|
||||
|
@ -56,9 +57,9 @@ const TxType = ({ origin, txType }: { txType: TransactionType, origin: string |
|
|||
}, [origin, txType])
|
||||
|
||||
if (forceCustom || !origin) {
|
||||
return <IconText iconUrl={typeToIcon[txType]} text={typeToLabel[txType]} />
|
||||
return <CustomIconText iconUrl={typeToIcon[txType]} text={typeToLabel[txType]} />
|
||||
}
|
||||
|
||||
return loading ? <Loader centered={false} size={20} /> : <IconText iconUrl={appInfo.iconUrl} text={appInfo.name} />
|
||||
return loading ? <Loader size="md" /> : <IconText iconUrl={appInfo.iconUrl} text={appInfo.name} />
|
||||
}
|
||||
export default TxType
|
||||
|
|
|
@ -34,7 +34,7 @@ const getTxStatus = (tx: Transaction, userAddress: string, safe: Safe): Transact
|
|||
} else if (!tx.confirmations.size) {
|
||||
txStatus = 'pending'
|
||||
} else {
|
||||
const userConfirmed = tx.confirmations.filter((conf) => conf.owner.address === userAddress).size === 1
|
||||
const userConfirmed = tx.confirmations.filter((conf) => conf.owner === userAddress).size === 1
|
||||
const userIsSafeOwner = safe.owners.filter((owner) => owner.address === userAddress).size === 1
|
||||
txStatus = !userConfirmed && userIsSafeOwner ? 'awaiting_your_confirmation' : 'awaiting_confirmations'
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import generateBatchRequests from '~/logic/contracts/generateBatchRequests'
|
|||
import { getLocalSafe, getSafeName } from '~/logic/safe/utils'
|
||||
import { enabledFeatures, safeNeedsUpdate } from '~/logic/safe/utils/safeVersion'
|
||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||
import { getBalanceInEtherOf, getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { getBalanceInEtherOf } from '~/logic/wallets/getWeb3'
|
||||
import addSafe from '~/routes/safe/store/actions/addSafe'
|
||||
import addSafeOwner from '~/routes/safe/store/actions/addSafeOwner'
|
||||
import removeSafeOwner from '~/routes/safe/store/actions/removeSafeOwner'
|
||||
|
@ -15,29 +15,31 @@ import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
|||
import { makeOwner } from '~/routes/safe/store/models/owner'
|
||||
import type { SafeProps } from '~/routes/safe/store/models/safe'
|
||||
import { type GlobalState } from '~/store'
|
||||
import { checksumAddress } from '~/utils/checksumAddress'
|
||||
|
||||
const buildOwnersFrom = (
|
||||
safeOwners: string[],
|
||||
localSafe: SafeProps | {}, // eslint-disable-next-line
|
||||
) =>
|
||||
safeOwners.map((ownerAddress: string) => {
|
||||
const convertedAdd = checksumAddress(ownerAddress)
|
||||
if (!localSafe) {
|
||||
return makeOwner({ name: 'UNKNOWN', address: ownerAddress })
|
||||
return makeOwner({ name: 'UNKNOWN', address: convertedAdd })
|
||||
}
|
||||
|
||||
const storedOwner = localSafe.owners.find(({ address }) => sameAddress(address, ownerAddress))
|
||||
const storedOwner = localSafe.owners.find(({ address }) => sameAddress(address, convertedAdd))
|
||||
if (!storedOwner) {
|
||||
return makeOwner({ name: 'UNKNOWN', address: ownerAddress })
|
||||
return makeOwner({ name: 'UNKNOWN', address: convertedAdd })
|
||||
}
|
||||
|
||||
return makeOwner({
|
||||
name: storedOwner.name || 'UNKNOWN',
|
||||
address: ownerAddress,
|
||||
address: convertedAdd,
|
||||
})
|
||||
})
|
||||
|
||||
export const buildSafe = async (safeAdd: string, safeName: string, latestMasterContractVersion: string) => {
|
||||
const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd)
|
||||
const safeAddress = checksumAddress(safeAdd)
|
||||
|
||||
const safeParams = ['getThreshold', 'nonce', 'VERSION', 'getOwners']
|
||||
const [[thresholdStr, nonceStr, currentVersion, remoteOwners], localSafe, ethBalance] = await Promise.all([
|
||||
|
@ -72,7 +74,7 @@ export const buildSafe = async (safeAdd: string, safeName: string, latestMasterC
|
|||
}
|
||||
|
||||
export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: ReduxDispatch<*>) => {
|
||||
const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd)
|
||||
const safeAddress = checksumAddress(safeAdd)
|
||||
// Check if the owner's safe did change and update them
|
||||
const safeParams = ['getThreshold', 'nonce', 'getOwners']
|
||||
const [[remoteThreshold, remoteNonce, remoteOwners], localSafe] = await Promise.all([
|
||||
|
@ -125,7 +127,7 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: ReduxDis
|
|||
// eslint-disable-next-line consistent-return
|
||||
export default (safeAdd: string) => async (dispatch: ReduxDispatch<GlobalState>, getState: () => GlobalState) => {
|
||||
try {
|
||||
const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd)
|
||||
const safeAddress = checksumAddress(safeAdd)
|
||||
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'
|
||||
const latestMasterContractVersion = getState().safes.get('latestMasterContractVersion')
|
||||
const safeProps: SafeProps = await buildSafe(safeAddress, safeName, latestMasterContractVersion)
|
||||
|
|
|
@ -13,7 +13,6 @@ import generateBatchRequests from '~/logic/contracts/generateBatchRequests'
|
|||
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds'
|
||||
import { buildIncomingTxServiceUrl } from '~/logic/safe/transactions/incomingTxHistory'
|
||||
import { type TxServiceType, buildTxServiceUrl } from '~/logic/safe/transactions/txHistory'
|
||||
import { getLocalSafe } from '~/logic/safe/utils'
|
||||
import { TOKEN_REDUCER_ID } from '~/logic/tokens/store/reducer/tokens'
|
||||
import { ALTERNATIVE_TOKEN_ABI } from '~/logic/tokens/utils/alternativeAbi'
|
||||
import {
|
||||
|
@ -28,7 +27,6 @@ import { getWeb3 } from '~/logic/wallets/getWeb3'
|
|||
import { addCancellationTransactions } from '~/routes/safe/store/actions/addCancellationTransactions'
|
||||
import { makeConfirmation } from '~/routes/safe/store/models/confirmation'
|
||||
import { type IncomingTransaction, makeIncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||
import { makeOwner } from '~/routes/safe/store/models/owner'
|
||||
import type { TransactionProps } from '~/routes/safe/store/models/transaction'
|
||||
import { type Transaction, makeTransaction } from '~/routes/safe/store/models/transaction'
|
||||
import { type GlobalState } from '~/store'
|
||||
|
@ -83,27 +81,15 @@ export const buildTransactionFrom = async (
|
|||
txTokenName,
|
||||
txTokenSymbol,
|
||||
): Promise<Transaction> => {
|
||||
const localSafe = await getLocalSafe(safeAddress)
|
||||
|
||||
const confirmations = List(
|
||||
tx.confirmations.map((conf: ConfirmationServiceModel) => {
|
||||
let ownerName = 'UNKNOWN'
|
||||
|
||||
if (localSafe && localSafe.owners) {
|
||||
const storedOwner = localSafe.owners.find((owner) => sameAddress(conf.owner, owner.address))
|
||||
|
||||
if (storedOwner) {
|
||||
ownerName = storedOwner.name
|
||||
}
|
||||
}
|
||||
|
||||
return makeConfirmation({
|
||||
owner: makeOwner({ address: conf.owner, name: ownerName }),
|
||||
tx.confirmations.map((conf: ConfirmationServiceModel) =>
|
||||
makeConfirmation({
|
||||
owner: conf.owner,
|
||||
type: ((conf.confirmationType.toLowerCase(): any): TxServiceType),
|
||||
hash: conf.transactionHash,
|
||||
signature: conf.signature,
|
||||
})
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
const modifySettingsTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !!tx.data
|
||||
const cancellationTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !tx.data
|
||||
|
|
|
@ -4,14 +4,13 @@ import type { Dispatch as ReduxDispatch } from 'redux'
|
|||
import setDefaultSafe from './setDefaultSafe'
|
||||
|
||||
import { getDefaultSafe } from '~/logic/safe/utils'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
import { checksumAddress } from '~/utils/checksumAddress'
|
||||
|
||||
const loadDefaultSafe = () => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
try {
|
||||
const defaultSafe: string = await getDefaultSafe()
|
||||
const checksumed =
|
||||
defaultSafe && defaultSafe.length > 0 ? getWeb3().utils.toChecksumAddress(defaultSafe) : defaultSafe
|
||||
const checksumed = defaultSafe && defaultSafe.length > 0 ? checksumAddress(defaultSafe) : defaultSafe
|
||||
dispatch(setDefaultSafe(checksumed))
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
|
|
|
@ -3,17 +3,16 @@ import { Record } from 'immutable'
|
|||
import type { RecordFactory, RecordOf } from 'immutable'
|
||||
|
||||
import { type TxServiceType } from '~/logic/safe/transactions/txHistory'
|
||||
import { type Owner, makeOwner } from '~/routes/safe/store/models/owner'
|
||||
|
||||
export type ConfirmationProps = {
|
||||
owner: Owner,
|
||||
owner: string,
|
||||
type: TxServiceType,
|
||||
hash: string,
|
||||
signature?: string,
|
||||
}
|
||||
|
||||
export const makeConfirmation: RecordFactory<ConfirmationProps> = Record({
|
||||
owner: makeOwner(),
|
||||
owner: '',
|
||||
type: 'initialised',
|
||||
hash: '',
|
||||
signature: null,
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { Map, Set } from 'immutable'
|
||||
import { type ActionType, handleActions } from 'redux-actions'
|
||||
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from '~/routes/safe/store/actions/activateTokenForAllSafes'
|
||||
import { ADD_SAFE, buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
|
||||
import { ADD_SAFE_OWNER } from '~/routes/safe/store/actions/addSafeOwner'
|
||||
|
@ -15,6 +14,7 @@ import { SET_LATEST_MASTER_CONTRACT_VERSION } from '~/routes/safe/store/actions/
|
|||
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
|
||||
import { makeOwner } from '~/routes/safe/store/models/owner'
|
||||
import SafeRecord, { type SafeProps } from '~/routes/safe/store/models/safe'
|
||||
import { checksumAddress } from '~/utils/checksumAddress'
|
||||
|
||||
export const SAFE_REDUCER_ID = 'safes'
|
||||
|
||||
|
@ -22,7 +22,7 @@ export type SafeReducerState = Map<string, *>
|
|||
|
||||
export const buildSafe = (storedSafe: SafeProps) => {
|
||||
const names = storedSafe.owners.map((owner) => owner.name)
|
||||
const addresses = storedSafe.owners.map((owner) => getWeb3().utils.toChecksumAddress(owner.address))
|
||||
const addresses = storedSafe.owners.map((owner) => checksumAddress(owner.address))
|
||||
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
||||
const activeTokens = Set(storedSafe.activeTokens)
|
||||
const activeAssets = Set(storedSafe.activeAssets)
|
||||
|
|
|
@ -5,7 +5,6 @@ import { type OutputSelector, createSelector } from 'reselect'
|
|||
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from '~/routes/routes'
|
||||
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
|
||||
import type { IncomingTransaction } from '~/routes/safe/store/models/incomingTransaction'
|
||||
import { type Safe } from '~/routes/safe/store/models/safe'
|
||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
|
@ -20,13 +19,14 @@ import {
|
|||
import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe'
|
||||
import { TRANSACTIONS_REDUCER_ID, type State as TransactionsState } from '~/routes/safe/store/reducer/transactions'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
import { checksumAddress } from '~/utils/checksumAddress'
|
||||
|
||||
export type RouterProps = {
|
||||
match: Match,
|
||||
}
|
||||
|
||||
type TransactionProps = {
|
||||
transaction: Transaction,
|
||||
export type SafeProps = {
|
||||
safeAddress: string,
|
||||
}
|
||||
|
||||
const safesStateSelector = (state: GlobalState): Map<string, *> => state[SAFE_REDUCER_ID]
|
||||
|
@ -64,8 +64,6 @@ const cancellationTransactionsSelector = (state: GlobalState): CancelTransaction
|
|||
const incomingTransactionsSelector = (state: GlobalState): IncomingTransactionsState =>
|
||||
state[INCOMING_TRANSACTIONS_REDUCER_ID]
|
||||
|
||||
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
|
||||
|
||||
export const safeParamAddressFromStateSelector = (state: GlobalState): string | null => {
|
||||
const match = matchPath(state.router.location.pathname, { path: `${SAFELIST_ADDRESS}/:safeAddress` })
|
||||
|
||||
|
@ -79,7 +77,7 @@ export const safeParamAddressFromStateSelector = (state: GlobalState): string |
|
|||
|
||||
export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => {
|
||||
const urlAdd = props.match.params[SAFE_PARAM_ADDRESS]
|
||||
return urlAdd ? getWeb3().utils.toChecksumAddress(urlAdd) : ''
|
||||
return urlAdd ? checksumAddress(urlAdd) : ''
|
||||
}
|
||||
|
||||
type TxSelectorType = OutputSelector<GlobalState, RouterProps, List<Transaction>>
|
||||
|
@ -144,22 +142,6 @@ export const safeIncomingTransactionsSelector: IncomingTxSelectorType = createSe
|
|||
},
|
||||
)
|
||||
|
||||
export const confirmationsTransactionSelector: OutputSelector<GlobalState, TransactionProps, number> = createSelector(
|
||||
oneTransactionSelector,
|
||||
(tx: Transaction) => {
|
||||
if (!tx) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const confirmations: List<Confirmation> = tx.get('confirmations')
|
||||
if (!confirmations) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return confirmations.filter((confirmation: Confirmation) => confirmation.get('type') === 'confirmation').count()
|
||||
},
|
||||
)
|
||||
|
||||
export type SafeSelectorProps = Safe | typeof undefined
|
||||
|
||||
export const safeSelector: OutputSelector<GlobalState, RouterProps, SafeSelectorProps> = createSelector(
|
||||
|
@ -169,7 +151,7 @@ export const safeSelector: OutputSelector<GlobalState, RouterProps, SafeSelector
|
|||
if (!address) {
|
||||
return undefined
|
||||
}
|
||||
const checksumed = getWeb3().utils.toChecksumAddress(address)
|
||||
const checksumed = checksumAddress(address)
|
||||
const safe = safes.get(checksumed)
|
||||
|
||||
return safe
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
// @flow
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
|
||||
export const checksumAddress = (address: string) => {
|
||||
if (!address) return null
|
||||
return getWeb3().utils.toChecksumAddress(address)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue