commit
42d438fb53
56
package.json
56
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "safe-react",
|
||||
"version": "2.17.0",
|
||||
"version": "2.18.0",
|
||||
"description": "Allowing crypto users manage funds in a safer way",
|
||||
"website": "https://github.com/gnosis/safe-react#readme",
|
||||
"bugs": {
|
||||
|
@ -67,7 +67,7 @@
|
|||
},
|
||||
"productName": "Safe Multisig",
|
||||
"build": {
|
||||
"appId": "io.gnosis.safe.macos",
|
||||
"appId": "io.gnosis.safe",
|
||||
"afterSign": "scripts/notarize.js",
|
||||
"extends": null,
|
||||
"productName": "Safe Multisig",
|
||||
|
@ -95,19 +95,10 @@
|
|||
]
|
||||
},
|
||||
"files": [
|
||||
"**/*",
|
||||
"!src${/*}",
|
||||
"!config${/*}",
|
||||
"!contracts${/*}",
|
||||
"!migrations${/*}",
|
||||
"!flow-typed${/*}",
|
||||
"!apps${/*}",
|
||||
"!out${/*}",
|
||||
"!.editorconfig",
|
||||
"!.gitignore",
|
||||
"!README.md",
|
||||
"!yarn-error.log",
|
||||
"!yarn.lock"
|
||||
"build/",
|
||||
"public",
|
||||
"scripts",
|
||||
"package.json"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "public/build"
|
||||
|
@ -134,12 +125,12 @@
|
|||
"deleteAppDataOnUninstall": true
|
||||
},
|
||||
"linux": {
|
||||
"category": "Finance",
|
||||
"target": [
|
||||
"AppImage",
|
||||
"deb",
|
||||
"zip"
|
||||
],
|
||||
"icon": "./public/build/safe.png"
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
|
@ -165,21 +156,21 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@gnosis.pm/safe-apps-sdk": "1.0.0-beta.4",
|
||||
"@gnosis.pm/safe-apps-sdk": "1.0.2",
|
||||
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
|
||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#bf3a84486b7353bd25447ddff39c406f6fafecc6",
|
||||
"@gnosis.pm/util-contracts": "2.0.6",
|
||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.34.0",
|
||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.36.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@material-ui/icons": "^4.11.0",
|
||||
"@material-ui/lab": "4.0.0-alpha.56",
|
||||
"@openzeppelin/contracts": "3.1.0",
|
||||
"@sentry/react": "^5.28.0",
|
||||
"@sentry/tracing": "^5.28.0",
|
||||
"@truffle/contract": "4.2.30",
|
||||
"@truffle/contract": "^4.3.0",
|
||||
"async-sema": "^3.1.0",
|
||||
"axios": "0.21.0",
|
||||
"axios": "0.21.1",
|
||||
"bignumber.js": "9.0.1",
|
||||
"bnc-onboard": "^1.16.1",
|
||||
"classnames": "^2.2.6",
|
||||
|
@ -190,7 +181,7 @@
|
|||
"date-fns": "2.16.1",
|
||||
"detect-port": "^1.3.0",
|
||||
"electron-is-dev": "^1.2.0",
|
||||
"electron-log": "4.3.0",
|
||||
"electron-log": "^4.3.0",
|
||||
"electron-settings": "^4.0.2",
|
||||
"electron-updater": "4.3.5",
|
||||
"eth-sig-util": "^2.5.3",
|
||||
|
@ -208,9 +199,10 @@
|
|||
"lodash.memoize": "^4.1.2",
|
||||
"material-ui-search-bar": "^1.0.0",
|
||||
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
|
||||
"qrcode.react": "1.0.0",
|
||||
"query-string": "6.13.7",
|
||||
"qrcode.react": "1.0.1",
|
||||
"query-string": "6.13.8",
|
||||
"react": "16.13.1",
|
||||
"react-device-detect": "^1.15.0",
|
||||
"react-dom": "16.13.1",
|
||||
"react-final-form": "^6.5.2",
|
||||
"react-final-form-listeners": "^1.0.2",
|
||||
|
@ -225,7 +217,7 @@
|
|||
"redux-actions": "^2.6.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"semver": "7.3.2",
|
||||
"semver": "^7.3.2",
|
||||
"styled-components": "^5.2.1",
|
||||
"web3": "1.2.11",
|
||||
"web3-core": "^1.2.11",
|
||||
|
@ -240,8 +232,8 @@
|
|||
"@storybook/addons": "^5.3.19",
|
||||
"@storybook/preset-create-react-app": "^3.1.5",
|
||||
"@storybook/react": "^5.3.19",
|
||||
"@testing-library/jest-dom": "5.11.6",
|
||||
"@testing-library/react": "11.2.2",
|
||||
"@testing-library/jest-dom": "^5.11.6",
|
||||
"@testing-library/react": "^11.2.2",
|
||||
"@typechain/web3-v1": "^2.0.0",
|
||||
"@types/history": "4.6.2",
|
||||
"@types/jest": "^26.0.16",
|
||||
|
@ -254,7 +246,6 @@
|
|||
"@types/styled-components": "^5.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
||||
"@typescript-eslint/parser": "^4.6.0",
|
||||
"autoprefixer": "9.8.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
|
@ -262,13 +253,12 @@
|
|||
"electron-builder": "22.9.1",
|
||||
"electron-notarize": "1.0.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-prettier": "^6.14.0",
|
||||
"eslint-config-prettier": "^7.0.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-sort-destructure-keys": "^1.3.5",
|
||||
"ethereumjs-abi": "0.6.8",
|
||||
"husky": "^4.3.0",
|
||||
"lint-staged": "^10.5.2",
|
||||
"patch-package": "^6.2.2",
|
||||
|
@ -276,7 +266,7 @@
|
|||
"prettier": "^2.2.0",
|
||||
"sass": "^1.29.0",
|
||||
"typechain": "^4.0.0",
|
||||
"typescript": "4.0.5",
|
||||
"wait-on": "5.2.0"
|
||||
"typescript": "4.1.3",
|
||||
"wait-on": "5.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ function createWindow(port = DEFAULT_PORT) {
|
|||
allowRunningInsecureContent: true,
|
||||
nativeWindowOpen: true, // need to be set in order to display modal
|
||||
},
|
||||
icon: electron.nativeImage.createFromPath(path.join(__dirname, './build/safe.png')),
|
||||
icon: electron.nativeImage.createFromPath(path.join(__dirname, '/public/build/safe.png')),
|
||||
})
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
|
|
|
@ -11,7 +11,7 @@ import ErrorIcon from 'src/assets/icons/error.svg'
|
|||
import InfoIcon from 'src/assets/icons/info.svg'
|
||||
|
||||
import AppLayout from 'src/components/AppLayout'
|
||||
import SafeListSidebarProvider, { SafeListSidebarContext } from 'src/components/SafeListSidebar'
|
||||
import { SafeListSidebar, SafeListSidebarContext } from 'src/components/SafeListSidebar'
|
||||
import CookiesBanner from 'src/components/CookiesBanner'
|
||||
import Notifier from 'src/components/Notifier'
|
||||
import Backdrop from 'src/components/layout/Backdrop'
|
||||
|
@ -159,9 +159,9 @@ const App: React.FC = ({ children }) => {
|
|||
}
|
||||
|
||||
const WrapperAppWithSidebar: React.FC = ({ children }) => (
|
||||
<SafeListSidebarProvider>
|
||||
<SafeListSidebar>
|
||||
<App>{children}</App>
|
||||
</SafeListSidebarProvider>
|
||||
</SafeListSidebar>
|
||||
)
|
||||
|
||||
export default WrapperAppWithSidebar
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
import { Button, Text, Card, Icon } from '@gnosis.pm/safe-react-components'
|
||||
import { fade } from '@material-ui/core/styles/colorManipulator'
|
||||
import styled from 'styled-components'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { MobileView } from 'react-device-detect'
|
||||
|
||||
import Phone from 'src/components/AppLayout/MobileStart/assets/phone@2x.png'
|
||||
|
||||
const Overlay = styled.div`
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: ${({ theme }) => fade(theme.colors.overlay.color, 0.75)};
|
||||
z-index: 2147483009; /* on top of Intercom Button */
|
||||
`
|
||||
|
||||
const ModalApp = styled.div`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
height: 260px;
|
||||
background-color: ${({ theme }) => theme.colors.background};
|
||||
box-shadow: 1px 2px 10px rgba(40, 54, 61, 0.18);
|
||||
z-index: 2147483004; /* on top of Intercom Button */
|
||||
padding: 20px 16px 0 16px;
|
||||
`
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
background-color: #fdfdfd;
|
||||
/* width: 45vw; */
|
||||
min-width: 245px;
|
||||
height: 220px;
|
||||
padding: 24px 58px 24px 24px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-direction: column;
|
||||
|
||||
@media (max-width: 340px) {
|
||||
padding: 16px;
|
||||
min-width: 215px;
|
||||
}
|
||||
`
|
||||
const StyledImg = styled.img`
|
||||
margin: 24px -81px 0 -58px;
|
||||
z-index: 1;
|
||||
width: 45%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
|
||||
@media (max-width: 340px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 430px) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
width: 20%;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledCloseIcon = styled(Icon)`
|
||||
margin: 0 34px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.colors.separator};
|
||||
border-radius: 16px;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 340px) {
|
||||
margin: 8px 34px 0 16px;
|
||||
}
|
||||
`
|
||||
const StyledButton = styled(Button)`
|
||||
background-color: transparent;
|
||||
min-width: 0;
|
||||
|
||||
:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledLink = styled.a`
|
||||
text-decoration: none;
|
||||
`
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const MobileNotSupported = ({ onClose }: Props): ReactElement => {
|
||||
return (
|
||||
<MobileView>
|
||||
<Overlay>
|
||||
<ModalApp>
|
||||
<StyledCard>
|
||||
<Text size="lg">The Safe Multisig web app is not optimized for mobile.</Text>
|
||||
<Text size="lg">Get the mobile app for a better experience.</Text>
|
||||
<Button size="md" color="primary" variant="contained">
|
||||
<StyledLink target="_blank" href="https://gnosis-safe.io/#mobile" rel="noopener noreferrer">
|
||||
<Text color="white" size="xl">
|
||||
Get the App
|
||||
</Text>
|
||||
</StyledLink>
|
||||
</Button>
|
||||
</StyledCard>
|
||||
|
||||
<StyledImg src={Phone} alt="Phone" />
|
||||
<StyledButton size="md" variant="outlined" color="primary" onClick={onClose}>
|
||||
<StyledCloseIcon size="md" type="cross" />
|
||||
</StyledButton>
|
||||
</ModalApp>
|
||||
</Overlay>
|
||||
</MobileView>
|
||||
)
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
|
@ -1,10 +1,11 @@
|
|||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ListItemType } from 'src/components/List'
|
||||
|
||||
import Header from './Header'
|
||||
import Footer from './Footer'
|
||||
import Sidebar from './Sidebar'
|
||||
import { MobileNotSupported } from './MobileNotSupported'
|
||||
|
||||
const Container = styled.div`
|
||||
height: 100vh;
|
||||
|
@ -85,30 +86,38 @@ const Layout: React.FC<Props> = ({
|
|||
onNewTransactionClick,
|
||||
children,
|
||||
sidebarItems,
|
||||
}): React.ReactElement => (
|
||||
<Container>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
<BodyWrapper>
|
||||
<SidebarWrapper>
|
||||
<Sidebar
|
||||
items={sidebarItems}
|
||||
safeAddress={safeAddress}
|
||||
safeName={safeName}
|
||||
balance={balance}
|
||||
granted={granted}
|
||||
onToggleSafeList={onToggleSafeList}
|
||||
onReceiveClick={onReceiveClick}
|
||||
onNewTransactionClick={onNewTransactionClick}
|
||||
/>
|
||||
</SidebarWrapper>
|
||||
<ContentWrapper>
|
||||
<div>{children}</div>
|
||||
<Footer />
|
||||
</ContentWrapper>
|
||||
</BodyWrapper>
|
||||
</Container>
|
||||
)
|
||||
}): React.ReactElement => {
|
||||
const [mobileNotSupportedClosed, setMobileNotSupportedClosed] = useState(false)
|
||||
|
||||
const closeMobileNotSupported = () => setMobileNotSupportedClosed(true)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
<BodyWrapper>
|
||||
<SidebarWrapper>
|
||||
<Sidebar
|
||||
items={sidebarItems}
|
||||
safeAddress={safeAddress}
|
||||
safeName={safeName}
|
||||
balance={balance}
|
||||
granted={granted}
|
||||
onToggleSafeList={onToggleSafeList}
|
||||
onReceiveClick={onReceiveClick}
|
||||
onNewTransactionClick={onNewTransactionClick}
|
||||
/>
|
||||
</SidebarWrapper>
|
||||
<ContentWrapper>
|
||||
<div>{children}</div>
|
||||
<Footer />
|
||||
</ContentWrapper>
|
||||
</BodyWrapper>
|
||||
|
||||
{!mobileNotSupportedClosed && <MobileNotSupported onClose={closeMobileNotSupported} />}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
|
|
|
@ -17,7 +17,7 @@ const useStyles = makeStyles(
|
|||
position: 'absolute',
|
||||
top: '120px',
|
||||
width: '500px',
|
||||
height: '540px',
|
||||
height: '580px',
|
||||
borderRadius: sm,
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)',
|
||||
|
|
|
@ -6,9 +6,10 @@ import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
|||
import DefaultBadge from './DefaultBadge'
|
||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe'
|
||||
import { SetDefaultSafe } from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||
import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
const StyledButtonLink = styled(ButtonLink)`
|
||||
visibility: hidden;
|
||||
|
@ -46,14 +47,18 @@ const useStyles = makeStyles({
|
|||
type Props = {
|
||||
safe: SafeRecordProps
|
||||
defaultSafe: DefaultSafe
|
||||
setDefaultSafe: SetDefaultSafe
|
||||
}
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
export const AddressWrapper = (props: Props): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const { safe, defaultSafe, setDefaultSafe } = props
|
||||
const { safe, defaultSafe } = props
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const setDefaultSafeAction = (safeAddress: string) => {
|
||||
dispatch(setDefaultSafe(safeAddress))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
|
@ -68,7 +73,7 @@ export const AddressWrapper = (props: Props): React.ReactElement => {
|
|||
className="safeListMakeDefaultButton"
|
||||
textSize="sm"
|
||||
onClick={() => {
|
||||
setDefaultSafe(safe.address)
|
||||
setDefaultSafeAction(safe.address)
|
||||
}}
|
||||
color="primary"
|
||||
>
|
|
@ -6,12 +6,11 @@ import * as React from 'react'
|
|||
import styled from 'styled-components'
|
||||
import { SafeRecord } from 'src/logic/safe/store/models/safe'
|
||||
import { DefaultSafe } from 'src/routes/safe/store/reducer/types/safe'
|
||||
import { SetDefaultSafe } from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||
import Hairline from 'src/components/layout/Hairline'
|
||||
import Link from 'src/components/layout/Link'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||
import { AddressWrapper } from './AddresWrapper'
|
||||
import { AddressWrapper } from 'src/components/SafeListSidebar/SafeList/AddressWrapper'
|
||||
export const SIDEBAR_SAFELIST_ROW_TESTID = 'SIDEBAR_SAFELIST_ROW_TESTID'
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
|
@ -46,10 +45,9 @@ type Props = {
|
|||
defaultSafe: DefaultSafe
|
||||
safes: SafeRecord[]
|
||||
onSafeClick: () => void
|
||||
setDefaultSafe: SetDefaultSafe
|
||||
}
|
||||
|
||||
const SafeList = ({ currentSafe, defaultSafe, onSafeClick, safes, setDefaultSafe }: Props): React.ReactElement => {
|
||||
export const SafeList = ({ currentSafe, defaultSafe, onSafeClick, safes }: Props): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
|
@ -67,7 +65,7 @@ const SafeList = ({ currentSafe, defaultSafe, onSafeClick, safes, setDefaultSafe
|
|||
) : (
|
||||
<div className={classes.noIcon}>placeholder</div>
|
||||
)}
|
||||
<AddressWrapper safe={safe} defaultSafe={defaultSafe} setDefaultSafe={setDefaultSafe} />
|
||||
<AddressWrapper safe={safe} defaultSafe={defaultSafe} />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Hairline />
|
||||
|
@ -76,5 +74,3 @@ const SafeList = ({ currentSafe, defaultSafe, onSafeClick, safes, setDefaultSafe
|
|||
</MuiList>
|
||||
)
|
||||
}
|
||||
|
||||
export default SafeList
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useState, ReactElement } from 'react'
|
||||
import Drawer from '@material-ui/core/Drawer'
|
||||
import SearchIcon from '@material-ui/icons/Search'
|
||||
import SearchBar from 'material-ui-search-bar'
|
||||
import { connect } from 'react-redux'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import SafeList from './SafeList'
|
||||
import { SafeList } from './SafeList'
|
||||
import { sortedSafeListSelector } from './selectors'
|
||||
import useSidebarStyles from './style'
|
||||
|
||||
|
@ -15,11 +15,9 @@ import Hairline from 'src/components/layout/Hairline'
|
|||
import Link from 'src/components/layout/Link'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { WELCOME_ADDRESS } from 'src/routes/routes'
|
||||
import setDefaultSafe from 'src/logic/safe/store/actions/setDefaultSafe'
|
||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||
|
||||
import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const SafeListSidebarContext = React.createContext({
|
||||
isOpen: false,
|
||||
|
@ -34,9 +32,17 @@ const filterBy = (filter, safes) =>
|
|||
safe.name.toLowerCase().includes(filter.toLowerCase()),
|
||||
)
|
||||
|
||||
const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefaultSafeAction }) => {
|
||||
type Props = {
|
||||
children: ReactElement
|
||||
}
|
||||
|
||||
export const SafeListSidebar = ({ children }: Props): ReactElement => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [filter, setFilter] = useState('')
|
||||
const safes = useSelector(sortedSafeListSelector)
|
||||
const defaultSafe = useSelector(defaultSafeSelector)
|
||||
const currentSafe = useSelector(safeParamAddressFromStateSelector)
|
||||
|
||||
const classes = useSidebarStyles()
|
||||
const { trackEvent } = useAnalytics()
|
||||
|
||||
|
@ -118,19 +124,9 @@ const SafeListSidebar = ({ children, currentSafe, defaultSafe, safes, setDefault
|
|||
defaultSafe={defaultSafe}
|
||||
onSafeClick={toggleSidebar}
|
||||
safes={filteredSafes}
|
||||
setDefaultSafe={setDefaultSafeAction}
|
||||
/>
|
||||
</Drawer>
|
||||
{children}
|
||||
</SafeListSidebarContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: AppReduxState) => ({
|
||||
safes: sortedSafeListSelector(state),
|
||||
defaultSafe: defaultSafeSelector(state),
|
||||
currentSafe: safeParamAddressFromStateSelector(state),
|
||||
}),
|
||||
{ setDefaultSafeAction: setDefaultSafe },
|
||||
)(SafeListSidebar)
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import { createStyles, makeStyles } from '@material-ui/core'
|
||||
import { sm } from 'src/theme/variables'
|
||||
import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Img from 'src/components/layout/Img'
|
||||
import InfoIcon from 'src/assets/icons/info_red.svg'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||
|
||||
const styles = createStyles({
|
||||
executionWarningRow: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
warningIcon: {
|
||||
marginRight: sm,
|
||||
},
|
||||
})
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type TransactionFailTextProps = {
|
||||
txEstimationExecutionStatus: EstimationStatus
|
||||
isExecution: boolean
|
||||
}
|
||||
|
||||
export const TransactionFailText = ({
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
}: TransactionFailTextProps): React.ReactElement | null => {
|
||||
const classes = useStyles()
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
|
||||
if (txEstimationExecutionStatus !== EstimationStatus.FAILURE) {
|
||||
return null
|
||||
}
|
||||
|
||||
let errorMessage = 'To save gas costs, avoid creating the transaction.'
|
||||
if (isExecution) {
|
||||
errorMessage =
|
||||
threshold && threshold > 1
|
||||
? `To save gas costs, cancel this transaction`
|
||||
: `To save gas costs, avoid executing the transaction.`
|
||||
}
|
||||
|
||||
return (
|
||||
<Row align="center">
|
||||
<Paragraph color="error" className={classes.executionWarningRow}>
|
||||
<Img alt="Info Tooltip" height={16} src={InfoIcon} className={classes.warningIcon} />
|
||||
This transaction will most likely fail. {errorMessage}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react'
|
||||
import { EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { TransactionFailText } from 'src/components/TransactionFailText'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
import { WALLETS } from 'src/config/networks/network.d'
|
||||
|
||||
type TransactionFailTextProps = {
|
||||
txEstimationExecutionStatus: EstimationStatus
|
||||
gasCostFormatted: string
|
||||
isExecution: boolean
|
||||
isCreation: boolean
|
||||
isOffChainSignature: boolean
|
||||
}
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
export const TransactionFees = ({
|
||||
gasCostFormatted,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
txEstimationExecutionStatus,
|
||||
}: TransactionFailTextProps): React.ReactElement | null => {
|
||||
const providerName = useSelector(providerNameSelector)
|
||||
|
||||
let transactionAction
|
||||
if (isCreation) {
|
||||
transactionAction = 'create'
|
||||
} else if (isExecution) {
|
||||
transactionAction = 'execute'
|
||||
} else {
|
||||
transactionAction = 'approve'
|
||||
}
|
||||
|
||||
// FIXME this should be removed when estimating with WalletConnect correctly
|
||||
if (!providerName || sameString(providerName, WALLETS.WALLET_CONNECT)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph>
|
||||
You're about to {transactionAction} a transaction and will have to confirm it with your currently connected
|
||||
wallet.
|
||||
{!isOffChainSignature &&
|
||||
` Make sure you have ${gasCostFormatted} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
<TransactionFailText txEstimationExecutionStatus={txEstimationExecutionStatus} isExecution={isExecution} />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
// source: https://github.com/final-form/react-final-form/issues/369#issuecomment-439823584
|
||||
|
||||
import React from 'react'
|
||||
import { Field } from 'react-final-form'
|
||||
|
||||
import { trimSpaces } from 'src/utils/strings'
|
||||
|
||||
const DebounceValidationField = ({ debounce = 1000, validate, ...rest }: any) => {
|
||||
let clearTimeout
|
||||
|
||||
const localValidation = (value, values, fieldState) => {
|
||||
const url = trimSpaces(value)
|
||||
|
||||
if (fieldState.active) {
|
||||
return new Promise((resolve) => {
|
||||
if (clearTimeout) clearTimeout()
|
||||
const timerId = setTimeout(() => {
|
||||
resolve(validate(url, values, fieldState))
|
||||
}, debounce)
|
||||
clearTimeout = () => {
|
||||
clearTimeout(timerId)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return validate(url, values, fieldState)
|
||||
}
|
||||
}
|
||||
|
||||
return <Field {...rest} format={trimSpaces} validate={localValidation} />
|
||||
}
|
||||
|
||||
export default DebounceValidationField
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react'
|
||||
import React from 'react'
|
||||
import { Field } from 'react-final-form'
|
||||
|
||||
// $FlowFixMe
|
||||
|
|
|
@ -46,7 +46,7 @@ const generateBatchRequests = <ReturnValues>({
|
|||
return new Promise((resolve) => {
|
||||
const resolver = (error, result) => {
|
||||
if (error) {
|
||||
resolve()
|
||||
resolve(undefined)
|
||||
} else {
|
||||
resolve(result)
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ const generateBatchRequests = <ReturnValues>({
|
|||
request = web3[type][method].request(...args, resolver)
|
||||
} else {
|
||||
if (address === null) {
|
||||
resolve()
|
||||
resolve(undefined)
|
||||
return
|
||||
}
|
||||
request = contractInstance.methods[method](...args).call.request(resolver)
|
||||
|
@ -68,7 +68,7 @@ const generateBatchRequests = <ReturnValues>({
|
|||
batch ? batch.add(request) : localBatch.add(request)
|
||||
} catch (e) {
|
||||
console.warn('There was an error trying to batch request from web3.', e)
|
||||
resolve()
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -130,7 +130,11 @@ export const estimateGasForDeployingSafe = async (
|
|||
const proxyFactoryData = proxyFactoryMaster.methods
|
||||
.createProxyWithNonce(safeMaster.options.address, gnosisSafeData, safeCreationSalt)
|
||||
.encodeABI()
|
||||
const gas = await calculateGasOf(proxyFactoryData, userAccount, proxyFactoryMaster.options.address)
|
||||
const gas = await calculateGasOf({
|
||||
data: proxyFactoryData,
|
||||
from: userAccount,
|
||||
to: proxyFactoryMaster.options.address,
|
||||
})
|
||||
const gasPrice = await calculateGasPrice()
|
||||
|
||||
return gas * parseInt(gasPrice, 10)
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
estimateGasForTransactionApproval,
|
||||
estimateGasForTransactionCreation,
|
||||
estimateGasForTransactionExecution,
|
||||
} from 'src/logic/safe/transactions/gas'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { calculateGasPrice } from 'src/logic/wallets/ethTransactions'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { useSelector } from 'react-redux'
|
||||
import {
|
||||
safeCurrentVersionSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
safeThresholdSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { CALL } from 'src/logic/safe/transactions'
|
||||
import { providerSelector } from '../wallets/store/selectors'
|
||||
|
||||
import { List } from 'immutable'
|
||||
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||
import { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
import { WALLETS } from 'src/config/networks/network.d'
|
||||
|
||||
export enum EstimationStatus {
|
||||
LOADING = 'LOADING',
|
||||
FAILURE = 'FAILURE',
|
||||
SUCCESS = 'SUCCESS',
|
||||
}
|
||||
|
||||
const checkIfTxIsExecution = (
|
||||
threshold: number,
|
||||
preApprovingOwner?: string,
|
||||
txConfirmations?: number,
|
||||
txType?: string,
|
||||
): boolean =>
|
||||
txConfirmations === threshold || !!preApprovingOwner || threshold === 1 || sameString(txType, 'spendingLimit')
|
||||
|
||||
const checkIfTxIsApproveAndExecution = (threshold: number, txConfirmations: number, txType?: string): boolean =>
|
||||
txConfirmations + 1 === threshold || sameString(txType, 'spendingLimit')
|
||||
|
||||
const checkIfTxIsCreation = (txConfirmations: number, txType?: string): boolean =>
|
||||
txConfirmations === 0 && !sameString(txType, 'spendingLimit')
|
||||
|
||||
type TransactionEstimationProps = {
|
||||
txData: string
|
||||
safeAddress: string
|
||||
txRecipient: string
|
||||
txConfirmations?: List<Confirmation>
|
||||
txAmount?: string
|
||||
operation?: number
|
||||
gasPrice?: string
|
||||
gasToken?: string
|
||||
refundReceiver?: string // Address of receiver of gas payment (or 0 if tx.origin).
|
||||
safeTxGas?: number
|
||||
from?: string
|
||||
isExecution: boolean
|
||||
isCreation: boolean
|
||||
isOffChainSignature?: boolean
|
||||
approvalAndExecution?: boolean
|
||||
}
|
||||
|
||||
const estimateTransactionGas = async ({
|
||||
txData,
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txConfirmations,
|
||||
txAmount,
|
||||
operation,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
refundReceiver,
|
||||
safeTxGas,
|
||||
from,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature = false,
|
||||
approvalAndExecution,
|
||||
}: TransactionEstimationProps): Promise<number> => {
|
||||
if (isCreation) {
|
||||
return estimateGasForTransactionCreation(safeAddress, txData, txRecipient, txAmount || '0', operation || CALL)
|
||||
}
|
||||
|
||||
if (!from) {
|
||||
throw new Error('No from provided for approving or execute transaction')
|
||||
}
|
||||
|
||||
if (isExecution) {
|
||||
return estimateGasForTransactionExecution({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txConfirmations,
|
||||
txAmount: txAmount || '0',
|
||||
txData,
|
||||
operation: operation || CALL,
|
||||
from,
|
||||
gasPrice: gasPrice || '0',
|
||||
gasToken: gasToken || ZERO_ADDRESS,
|
||||
refundReceiver: refundReceiver || ZERO_ADDRESS,
|
||||
safeTxGas: safeTxGas || 0,
|
||||
approvalAndExecution,
|
||||
})
|
||||
}
|
||||
|
||||
return estimateGasForTransactionApproval({
|
||||
safeAddress,
|
||||
operation: operation || CALL,
|
||||
txData,
|
||||
txAmount: txAmount || '0',
|
||||
txRecipient,
|
||||
from,
|
||||
isOffChainSignature,
|
||||
})
|
||||
}
|
||||
|
||||
type UseEstimateTransactionGasProps = {
|
||||
txData: string
|
||||
txRecipient: string
|
||||
txConfirmations?: List<Confirmation>
|
||||
txAmount?: string
|
||||
preApprovingOwner?: string
|
||||
operation?: number
|
||||
safeTxGas?: number
|
||||
txType?: string
|
||||
}
|
||||
|
||||
type TransactionGasEstimationResult = {
|
||||
txEstimationExecutionStatus: EstimationStatus
|
||||
gasEstimation: number // Amount of gas needed for execute or approve the transaction
|
||||
gasCost: string // Cost of gas in raw format (estimatedGas * gasPrice)
|
||||
gasCostFormatted: string // Cost of gas in format '< | > 100'
|
||||
gasPrice: string // Current price of gas unit
|
||||
isExecution: boolean // Returns true if the user will execute the tx or false if it just signs it
|
||||
isCreation: boolean // Returns true if the transaction is a creation transaction
|
||||
isOffChainSignature: boolean // Returns true if offChainSignature is available
|
||||
}
|
||||
|
||||
export const useEstimateTransactionGas = ({
|
||||
txRecipient,
|
||||
txData,
|
||||
txConfirmations,
|
||||
txAmount,
|
||||
preApprovingOwner,
|
||||
operation,
|
||||
safeTxGas,
|
||||
txType,
|
||||
}: UseEstimateTransactionGasProps): TransactionGasEstimationResult => {
|
||||
const [gasEstimation, setGasEstimation] = useState<TransactionGasEstimationResult>({
|
||||
txEstimationExecutionStatus: EstimationStatus.LOADING,
|
||||
gasEstimation: 0,
|
||||
gasCost: '0',
|
||||
gasCostFormatted: '< 0.001',
|
||||
gasPrice: '0',
|
||||
isExecution: false,
|
||||
isCreation: false,
|
||||
isOffChainSignature: false,
|
||||
})
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
const safeVersion = useSelector(safeCurrentVersionSelector)
|
||||
const { account: from, smartContractWallet, name: providerName } = useSelector(providerSelector)
|
||||
|
||||
useEffect(() => {
|
||||
const estimateGas = async () => {
|
||||
if (!txData.length) {
|
||||
return
|
||||
}
|
||||
// FIXME this should be removed when estimating with WalletConnect correctly
|
||||
if (!providerName || sameString(providerName, WALLETS.WALLET_CONNECT)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isExecution = checkIfTxIsExecution(Number(threshold), preApprovingOwner, txConfirmations?.size, txType)
|
||||
const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0, txType)
|
||||
const approvalAndExecution = checkIfTxIsApproveAndExecution(Number(threshold), txConfirmations?.size || 0, txType)
|
||||
|
||||
try {
|
||||
const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)
|
||||
|
||||
const gasEstimation = await estimateTransactionGas({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txData,
|
||||
txAmount,
|
||||
txConfirmations,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
operation,
|
||||
from,
|
||||
safeTxGas,
|
||||
approvalAndExecution,
|
||||
})
|
||||
const gasPrice = await calculateGasPrice()
|
||||
const estimatedGasCosts = gasEstimation * parseInt(gasPrice, 10)
|
||||
const gasCost = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const gasCostFormatted = formatAmount(gasCost)
|
||||
|
||||
let txEstimationExecutionStatus = EstimationStatus.SUCCESS
|
||||
|
||||
if (gasEstimation <= 0) {
|
||||
txEstimationExecutionStatus = isOffChainSignature ? EstimationStatus.SUCCESS : EstimationStatus.FAILURE
|
||||
}
|
||||
|
||||
setGasEstimation({
|
||||
txEstimationExecutionStatus,
|
||||
gasEstimation,
|
||||
gasCost,
|
||||
gasCostFormatted,
|
||||
gasPrice,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(error.message)
|
||||
// We put a fixed the amount of gas to let the user try to execute the tx, but it's not accurate so it will probably fail
|
||||
const gasEstimation = 10000
|
||||
const gasCost = fromTokenUnit(gasEstimation, nativeCoin.decimals)
|
||||
const gasCostFormatted = formatAmount(gasCost)
|
||||
setGasEstimation({
|
||||
txEstimationExecutionStatus: EstimationStatus.FAILURE,
|
||||
gasEstimation,
|
||||
gasCost,
|
||||
gasCostFormatted,
|
||||
gasPrice: '1',
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
estimateGas()
|
||||
}, [
|
||||
txData,
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txConfirmations,
|
||||
txAmount,
|
||||
preApprovingOwner,
|
||||
nativeCoin.decimals,
|
||||
threshold,
|
||||
from,
|
||||
operation,
|
||||
safeVersion,
|
||||
smartContractWallet,
|
||||
safeTxGas,
|
||||
txType,
|
||||
providerName,
|
||||
])
|
||||
|
||||
return gasEstimation
|
||||
}
|
|
@ -20,7 +20,7 @@ export default handleActions(
|
|||
const { dismissAll, key } = action.payload
|
||||
|
||||
if (key) {
|
||||
return state.update(key, (prev) => prev.set('dismissed', true))
|
||||
return state.update(key, (prev) => prev?.set('dismissed', true))
|
||||
}
|
||||
if (dismissAll) {
|
||||
return state.withMutations((map) => {
|
||||
|
|
|
@ -1,31 +1,61 @@
|
|||
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
|
||||
// https://github.com/gnosis/safe-contracts/blob/master/test/gnosisSafeTeamEdition.js#L26
|
||||
export const generateSignaturesFromTxConfirmations = (confirmations, preApprovingOwner) => {
|
||||
// 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.toLowerCase()] = obj // eslint-disable-line no-param-reassign
|
||||
return map
|
||||
}, {})
|
||||
import { List } from 'immutable'
|
||||
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import semverSatisfies from 'semver/functions/satisfies'
|
||||
import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES } from './transactions/offchainSigner'
|
||||
|
||||
// Here we're checking that safe contract version is greater or equal 1.1.1, but
|
||||
// theoretically EIP712 should also work for 1.0.0 contracts
|
||||
// Also, offchain signatures are not working for ledger/trezor wallet because of a bug in their library:
|
||||
// https://github.com/LedgerHQ/ledgerjs/issues/378
|
||||
// Couldn't find an issue for trezor but the error is almost the same
|
||||
export const checkIfOffChainSignatureIsPossible = (
|
||||
isExecution: boolean,
|
||||
isSmartContractWallet: boolean,
|
||||
safeVersion?: string,
|
||||
): boolean =>
|
||||
!isExecution &&
|
||||
!isSmartContractWallet &&
|
||||
!!safeVersion &&
|
||||
semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
|
||||
|
||||
// https://docs.gnosis.io/safe/docs/contracts_signatures/#pre-validated-signatures
|
||||
export const getPreValidatedSignatures = (from: string, initialString: string = EMPTY_DATA): string => {
|
||||
return `${initialString}000000000000000000000000${from.replace(
|
||||
EMPTY_DATA,
|
||||
'',
|
||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
}
|
||||
|
||||
export const generateSignaturesFromTxConfirmations = (
|
||||
confirmations?: List<Confirmation>,
|
||||
preApprovingOwner?: string,
|
||||
): string => {
|
||||
let confirmationsMap =
|
||||
confirmations?.map((value) => {
|
||||
return {
|
||||
signature: value.signature,
|
||||
owner: value.owner.toLowerCase(),
|
||||
}
|
||||
}) || List([])
|
||||
|
||||
if (preApprovingOwner) {
|
||||
confirmationsMap[preApprovingOwner.toLowerCase()] = { owner: preApprovingOwner }
|
||||
confirmationsMap = confirmationsMap.push({ owner: preApprovingOwner, signature: null })
|
||||
}
|
||||
|
||||
// The constant parts need to be sorted so that the recovered signers are sorted ascending
|
||||
// (natural order) by address (not checksummed).
|
||||
confirmationsMap = confirmationsMap.sort((ownerA, ownerB) => ownerA.owner.localeCompare(ownerB.owner))
|
||||
|
||||
let sigs = '0x'
|
||||
Object.keys(confirmationsMap)
|
||||
.sort()
|
||||
.forEach((addr) => {
|
||||
const conf = confirmationsMap[addr]
|
||||
if (conf.signature) {
|
||||
sigs += conf.signature.slice(2)
|
||||
} else {
|
||||
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
|
||||
sigs += `000000000000000000000000${addr.replace(
|
||||
'0x',
|
||||
'',
|
||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
}
|
||||
})
|
||||
confirmationsMap.forEach(({ signature, owner }) => {
|
||||
if (signature) {
|
||||
sigs += signature.slice(2)
|
||||
} else {
|
||||
// https://docs.gnosis.io/safe/docs/contracts_signatures/#pre-validated-signatures
|
||||
sigs += getPreValidatedSignatures(owner, '')
|
||||
}
|
||||
})
|
||||
|
||||
return sigs
|
||||
}
|
||||
|
|
|
@ -3,22 +3,8 @@ import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
|||
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||
|
||||
describe('Store actions utils > getNewTxNonce', () => {
|
||||
it(`Should return passed predicted transaction nonce if it's a valid value`, async () => {
|
||||
// Given
|
||||
const txNonce = '45'
|
||||
const lastTx = { nonce: 44 } as TxServiceModel
|
||||
const safeInstance = {}
|
||||
|
||||
// When
|
||||
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe)
|
||||
|
||||
// Then
|
||||
expect(nonce).toBe('45')
|
||||
})
|
||||
|
||||
it(`Should return nonce of a last transaction + 1 if passed nonce is less than last transaction or invalid`, async () => {
|
||||
// Given
|
||||
const txNonce = ''
|
||||
const lastTx = { nonce: 44 } as TxServiceModel
|
||||
const safeInstance = {
|
||||
methods: {
|
||||
|
@ -29,7 +15,7 @@ describe('Store actions utils > getNewTxNonce', () => {
|
|||
}
|
||||
|
||||
// When
|
||||
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe)
|
||||
const nonce = await getNewTxNonce(lastTx, safeInstance as GnosisSafe)
|
||||
|
||||
// Then
|
||||
expect(nonce).toBe('45')
|
||||
|
@ -37,7 +23,6 @@ describe('Store actions utils > getNewTxNonce', () => {
|
|||
|
||||
it(`Should retrieve contract's instance nonce value as a fallback, if txNonce and lastTx are not valid`, async () => {
|
||||
// Given
|
||||
const txNonce = ''
|
||||
const lastTx = null
|
||||
const safeInstance = {
|
||||
methods: {
|
||||
|
@ -48,7 +33,7 @@ describe('Store actions utils > getNewTxNonce', () => {
|
|||
}
|
||||
|
||||
// When
|
||||
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance as GnosisSafe)
|
||||
const nonce = await getNewTxNonce(lastTx, safeInstance as GnosisSafe)
|
||||
|
||||
// Then
|
||||
expect(nonce).toBe('45')
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { push } from 'connected-react-router'
|
||||
import semverSatisfies from 'semver/functions/satisfies'
|
||||
import { ThunkAction } from 'redux-thunk'
|
||||
|
||||
import { onboardUser } from 'src/components/ConnectButton'
|
||||
|
@ -10,11 +9,10 @@ import {
|
|||
CALL,
|
||||
getApprovalTransaction,
|
||||
getExecutionTransaction,
|
||||
SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES,
|
||||
saveTxToHistory,
|
||||
tryOffchainSigning,
|
||||
} from 'src/logic/safe/transactions'
|
||||
import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas'
|
||||
import { estimateGasForTransactionCreation } from 'src/logic/safe/transactions/gas'
|
||||
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
|
@ -40,6 +38,7 @@ import { AnyAction } from 'redux'
|
|||
import { PayableTx } from 'src/types/contracts/types.d'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { Dispatch, DispatchReturn } from './types'
|
||||
import { checkIfOffChainSignatureIsPossible, getPreValidatedSignatures } from 'src/logic/safe/safeTxSigner'
|
||||
|
||||
export interface CreateTransactionArgs {
|
||||
navigateToTransactionsTab?: boolean
|
||||
|
@ -87,18 +86,18 @@ const createTransaction = (
|
|||
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nonce = await getNewTxNonce(txNonce?.toString(), lastTx, safeInstance)
|
||||
const nonce = txNonce ? txNonce.toString() : await getNewTxNonce(lastTx, safeInstance)
|
||||
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
|
||||
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
||||
const safeTxGas =
|
||||
safeTxGasArg || (await estimateSafeTxGas(safeInstance, safeAddress, txData, to, valueInWei, operation))
|
||||
|
||||
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
|
||||
const sigs = `0x000000000000000000000000${from.replace(
|
||||
'0x',
|
||||
'',
|
||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
let safeTxGas
|
||||
try {
|
||||
safeTxGas =
|
||||
safeTxGasArg || (await estimateGasForTransactionCreation(safeAddress, txData, to, valueInWei, operation))
|
||||
} catch (error) {
|
||||
safeTxGas = safeTxGasArg || 0
|
||||
}
|
||||
|
||||
const sigs = getPreValidatedSignatures(from)
|
||||
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin)
|
||||
const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
|
||||
|
||||
|
@ -123,11 +122,7 @@ const createTransaction = (
|
|||
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
|
||||
|
||||
try {
|
||||
// Here we're checking that safe contract version is greater or equal 1.1.1, but
|
||||
// theoretically EIP712 should also work for 1.0.0 contracts
|
||||
const canTryOffchainSigning =
|
||||
!isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
|
||||
if (canTryOffchainSigning) {
|
||||
if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) {
|
||||
const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
||||
|
||||
if (signature) {
|
||||
|
@ -141,9 +136,7 @@ const createTransaction = (
|
|||
}
|
||||
}
|
||||
|
||||
const tx = isExecution
|
||||
? await getExecutionTransaction(txArgs)
|
||||
: await getApprovalTransaction(safeInstance, safeTxHash)
|
||||
const tx = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, safeTxHash)
|
||||
const sendParams: PayableTx = { from, value: 0 }
|
||||
|
||||
// if not set owner management tests will fail on ganache
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { AnyAction } from 'redux'
|
||||
import { ThunkAction } from 'redux-thunk'
|
||||
import semverSatisfies from 'semver/functions/satisfies'
|
||||
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { getNotificationsFromTxType } from 'src/logic/notifications'
|
||||
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
|
||||
import {
|
||||
checkIfOffChainSignatureIsPossible,
|
||||
generateSignaturesFromTxConfirmations,
|
||||
getPreValidatedSignatures,
|
||||
} from 'src/logic/safe/safeTxSigner'
|
||||
import { getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions'
|
||||
import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES, tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner'
|
||||
import { tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner'
|
||||
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { providerSelector } from 'src/logic/wallets/store/selectors'
|
||||
|
@ -29,16 +32,18 @@ interface ProcessTransactionArgs {
|
|||
safeAddress: string
|
||||
tx: Transaction
|
||||
userAddress: string
|
||||
thresholdReached: boolean
|
||||
}
|
||||
|
||||
type ProcessTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
|
||||
|
||||
const processTransaction = ({
|
||||
export const processTransaction = ({
|
||||
approveAndExecute,
|
||||
notifiedTransaction,
|
||||
safeAddress,
|
||||
tx,
|
||||
userAddress,
|
||||
thresholdReached,
|
||||
}: ProcessTransactionArgs): ProcessTransactionAction => async (
|
||||
dispatch: Dispatch,
|
||||
getState: () => AppReduxState,
|
||||
|
@ -49,17 +54,15 @@ const processTransaction = ({
|
|||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nonce = await getNewTxNonce(undefined, lastTx, safeInstance)
|
||||
const nonce = await getNewTxNonce(lastTx, safeInstance)
|
||||
const isExecution = approveAndExecute || (await shouldExecuteTransaction(safeInstance, nonce, lastTx))
|
||||
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
||||
|
||||
let sigs = generateSignaturesFromTxConfirmations(tx.confirmations, approveAndExecute && userAddress)
|
||||
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
|
||||
const preApprovingOwner = approveAndExecute && !thresholdReached ? userAddress : undefined
|
||||
let sigs = generateSignaturesFromTxConfirmations(tx.confirmations, preApprovingOwner)
|
||||
|
||||
if (!sigs) {
|
||||
sigs = `0x000000000000000000000000${from.replace(
|
||||
'0x',
|
||||
'',
|
||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
sigs = getPreValidatedSignatures(from)
|
||||
}
|
||||
|
||||
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, tx.origin)
|
||||
|
@ -86,14 +89,7 @@ const processTransaction = ({
|
|||
}
|
||||
|
||||
try {
|
||||
// Here we're checking that safe contract version is greater or equal 1.1.1, but
|
||||
// theoretically EIP712 should also work for 1.0.0 contracts
|
||||
// Also, offchain signatures are not working for ledger/trezor wallet because of a bug in their library:
|
||||
// https://github.com/LedgerHQ/ledgerjs/issues/378
|
||||
// Couldn't find an issue for trezor but the error is almost the same
|
||||
const canTryOffchainSigning =
|
||||
!isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
|
||||
if (canTryOffchainSigning) {
|
||||
if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) {
|
||||
const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
||||
|
||||
if (signature) {
|
||||
|
@ -109,9 +105,7 @@ const processTransaction = ({
|
|||
}
|
||||
}
|
||||
|
||||
transaction = isExecution
|
||||
? await getExecutionTransaction(txArgs)
|
||||
: await getApprovalTransaction(safeInstance, tx.safeTxHash)
|
||||
transaction = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, tx.safeTxHash)
|
||||
|
||||
const sendParams: any = { from, value: 0 }
|
||||
|
||||
|
@ -196,5 +190,3 @@ const processTransaction = ({
|
|||
|
||||
return txHash
|
||||
}
|
||||
|
||||
export default processTransaction
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { Action, Dispatch } from 'redux'
|
||||
import { loadStoredSafes } from 'src/logic/safe/utils'
|
||||
import removeSafe from 'src/logic/safe/store/actions/removeSafe'
|
||||
|
||||
export const removeLocalSafe = (safeAddress: string) => async (dispatch: Dispatch): Promise<Action | void> => {
|
||||
const storedSafes = await loadStoredSafes()
|
||||
if (storedSafes) {
|
||||
delete storedSafes[safeAddress]
|
||||
}
|
||||
dispatch(removeSafe(safeAddress))
|
||||
}
|
|
@ -173,12 +173,12 @@ export const calculateTransactionStatus = (
|
|||
|
||||
if (tx.isExecuted && tx.isSuccessful) {
|
||||
txStatus = TransactionStatus.SUCCESS
|
||||
} else if (tx.creationTx) {
|
||||
txStatus = TransactionStatus.SUCCESS
|
||||
} else if (tx.cancelled || nonce > tx.nonce) {
|
||||
txStatus = TransactionStatus.CANCELLED
|
||||
} else if (tx.confirmations.size === threshold) {
|
||||
txStatus = TransactionStatus.AWAITING_EXECUTION
|
||||
} else if (tx.creationTx) {
|
||||
txStatus = TransactionStatus.SUCCESS
|
||||
} else if (!!tx.isPending) {
|
||||
txStatus = TransactionStatus.PENDING
|
||||
} else {
|
||||
|
|
|
@ -16,15 +16,7 @@ export const getLastTx = async (safeAddress: string): Promise<TxServiceModel | n
|
|||
}
|
||||
}
|
||||
|
||||
export const getNewTxNonce = async (
|
||||
txNonce: string | undefined,
|
||||
lastTx: TxServiceModel | null,
|
||||
safeInstance: GnosisSafe,
|
||||
): Promise<string> => {
|
||||
if (txNonce) {
|
||||
return txNonce
|
||||
}
|
||||
|
||||
export const getNewTxNonce = async (lastTx: TxServiceModel | null, safeInstance: GnosisSafe): Promise<string> => {
|
||||
// use current's safe nonce as fallback
|
||||
return lastTx ? `${lastTx.nonce + 1}` : (await safeInstance.methods.nonce().call()).toString()
|
||||
}
|
||||
|
|
|
@ -107,17 +107,18 @@ export default handleActions(
|
|||
|
||||
[ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
const { safe } = action.payload
|
||||
const safeAddress = safe.address
|
||||
|
||||
if (!state.hasIn(['safes', safe.address])) {
|
||||
return state.setIn(['safes', safe.address], makeSafe(safe))
|
||||
if (!state.hasIn(['safes', safeAddress])) {
|
||||
return state.setIn(['safes', safeAddress], makeSafe(safe))
|
||||
}
|
||||
|
||||
const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safe.safeAddress]))
|
||||
const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safeAddress]))
|
||||
|
||||
return shouldUpdate
|
||||
? state.updateIn(
|
||||
['safes', safe.address],
|
||||
makeSafe({ name: safe?.name || 'LOADED SAFE', address: safe.address }),
|
||||
['safes', safeAddress],
|
||||
makeSafe({ name: safe?.name || 'LOADED SAFE', address: safeAddress }),
|
||||
(prevSafe) => updateSafeProps(prevSafe, safe),
|
||||
)
|
||||
: state
|
||||
|
@ -125,7 +126,14 @@ export default handleActions(
|
|||
[REMOVE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
const safeAddress = action.payload
|
||||
|
||||
return state.deleteIn(['safes', safeAddress])
|
||||
const currentDefaultSafe = state.get('defaultSafe')
|
||||
|
||||
let newState = state.deleteIn(['safes', safeAddress])
|
||||
if (sameAddress(safeAddress, currentDefaultSafe)) {
|
||||
newState = newState.set('defaultSafe', DEFAULT_SAFE_INITIAL_STATE)
|
||||
}
|
||||
|
||||
return newState
|
||||
},
|
||||
[ADD_SAFE_OWNER]: (state: SafeReducerMap, action) => {
|
||||
const { ownerAddress, ownerName, safeAddress } = action.payload
|
||||
|
|
Binary file not shown.
|
@ -1,19 +1,15 @@
|
|||
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
import { CALL } from '.'
|
||||
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { EMPTY_DATA, calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions'
|
||||
import { getAccountFrom, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
||||
import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
|
||||
import { List } from 'immutable'
|
||||
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||
|
||||
const estimateDataGasCosts = (data: string): number => {
|
||||
// Receives the response data of the safe method requiredTxGas() and parses it to get the gas amount
|
||||
const parseRequiredTxGasResponse = (data: string): number => {
|
||||
const reducer = (accumulator, currentValue) => {
|
||||
if (currentValue === EMPTY_DATA) {
|
||||
return accumulator + 0
|
||||
|
@ -29,76 +25,38 @@ const estimateDataGasCosts = (data: string): number => {
|
|||
return data.match(/.{2}/g)?.reduce(reducer, 0)
|
||||
}
|
||||
|
||||
export const estimateTxGasCosts = async (
|
||||
safeAddress: string,
|
||||
to: string,
|
||||
data: string,
|
||||
tx?: Transaction,
|
||||
preApprovingOwner?: string,
|
||||
): Promise<number> => {
|
||||
interface ErrorDataJson extends JSON {
|
||||
originalError?: {
|
||||
data?: string
|
||||
}
|
||||
data?: string
|
||||
}
|
||||
|
||||
const getJSONOrNullFromString = (stringInput: string): ErrorDataJson | null => {
|
||||
try {
|
||||
const web3 = getWeb3()
|
||||
const from = await getAccountFrom(web3)
|
||||
|
||||
if (!from) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const safeInstance = (new web3.eth.Contract(GnosisSafeSol.abi as AbiItem[], safeAddress) as unknown) as GnosisSafe
|
||||
const nonce = await safeInstance.methods.nonce().call()
|
||||
const threshold = await safeInstance.methods.getThreshold().call()
|
||||
const isExecution = tx?.confirmations.size === Number(threshold) || !!preApprovingOwner || threshold === '1'
|
||||
|
||||
let txData
|
||||
if (isExecution) {
|
||||
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
|
||||
const signatures = tx?.confirmations
|
||||
? generateSignaturesFromTxConfirmations(tx.confirmations, preApprovingOwner)
|
||||
: `0x000000000000000000000000${from.replace(
|
||||
EMPTY_DATA,
|
||||
'',
|
||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
txData = await safeInstance.methods
|
||||
.execTransaction(
|
||||
to,
|
||||
tx?.value || 0,
|
||||
data,
|
||||
CALL,
|
||||
tx?.safeTxGas || 0,
|
||||
0,
|
||||
0,
|
||||
ZERO_ADDRESS,
|
||||
ZERO_ADDRESS,
|
||||
signatures,
|
||||
)
|
||||
.encodeABI()
|
||||
} else {
|
||||
const txHash = await safeInstance.methods
|
||||
.getTransactionHash(to, tx?.value || 0, data, CALL, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, nonce)
|
||||
.call({
|
||||
from,
|
||||
})
|
||||
txData = await safeInstance.methods.approveHash(txHash).encodeABI()
|
||||
}
|
||||
|
||||
const gas = await calculateGasOf(txData, from, safeAddress)
|
||||
const gasPrice = await calculateGasPrice()
|
||||
|
||||
return gas * parseInt(gasPrice, 10)
|
||||
} catch (err) {
|
||||
console.error('Error while estimating transaction execution gas costs:')
|
||||
console.error(err)
|
||||
|
||||
return 10000
|
||||
return JSON.parse(stringInput)
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Parses the result from the error message (GETH, OpenEthereum/Parity and Nethermind) and returns the data value
|
||||
export const getDataFromNodeErrorMessage = (errorMessage: string): string | undefined => {
|
||||
// Replace illegal characters that often comes within the error string (like <20> for example)
|
||||
// https://stackoverflow.com/questions/12754256/removing-invalid-characters-in-javascript
|
||||
const normalizedErrorString = errorMessage.replace(/\uFFFD/g, '')
|
||||
|
||||
// Extracts JSON object from the error message
|
||||
const [, ...error] = errorMessage.split('\n')
|
||||
const [, ...error] = normalizedErrorString.split('\n')
|
||||
|
||||
try {
|
||||
const errorAsJSON = JSON.parse(error.join(''))
|
||||
const errorAsString = error.join('')
|
||||
const errorAsJSON = getJSONOrNullFromString(errorAsString)
|
||||
|
||||
// Trezor wallet returns the error not as an JSON object but directly as string
|
||||
if (!errorAsJSON) {
|
||||
return errorAsString.length ? errorAsString : undefined
|
||||
}
|
||||
|
||||
// For new GETH nodes they will return the data as error in the format:
|
||||
// {
|
||||
|
@ -130,7 +88,7 @@ export const getDataFromNodeErrorMessage = (errorMessage: string): string | unde
|
|||
}
|
||||
}
|
||||
|
||||
const getGasEstimationTxResponse = async (txConfig: {
|
||||
export const getGasEstimationTxResponse = async (txConfig: {
|
||||
to: string
|
||||
from: string
|
||||
data: string
|
||||
|
@ -190,8 +148,7 @@ const calculateMinimumGasForTransaction = async (
|
|||
return 0
|
||||
}
|
||||
|
||||
export const estimateSafeTxGas = async (
|
||||
safe: GnosisSafe | undefined,
|
||||
export const estimateGasForTransactionCreation = async (
|
||||
safeAddress: string,
|
||||
data: string,
|
||||
to: string,
|
||||
|
@ -199,10 +156,7 @@ export const estimateSafeTxGas = async (
|
|||
operation: number,
|
||||
): Promise<number> => {
|
||||
try {
|
||||
let safeInstance = safe
|
||||
if (!safeInstance) {
|
||||
safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
}
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
|
||||
const estimateData = safeInstance.methods.requiredTxGas(to, valueInWei, data, operation).encodeABI()
|
||||
const gasEstimationResponse = await getGasEstimationTxResponse({
|
||||
|
@ -214,7 +168,7 @@ export const estimateSafeTxGas = async (
|
|||
const txGasEstimation = gasEstimationResponse + 10000
|
||||
|
||||
// 21000 - additional gas costs (e.g. base tx costs, transfer costs)
|
||||
const dataGasEstimation = estimateDataGasCosts(estimateData) + 21000
|
||||
const dataGasEstimation = parseRequiredTxGasResponse(estimateData) + 21000
|
||||
const additionalGasBatches = [0, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000]
|
||||
|
||||
return await calculateMinimumGasForTransaction(
|
||||
|
@ -225,7 +179,102 @@ export const estimateSafeTxGas = async (
|
|||
dataGasEstimation,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error calculating tx gas estimation', error)
|
||||
return 0
|
||||
console.info('Error calculating tx gas estimation', error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
type TransactionExecutionEstimationProps = {
|
||||
txData: string
|
||||
safeAddress: string
|
||||
txRecipient: string
|
||||
txConfirmations?: List<Confirmation>
|
||||
txAmount: string
|
||||
operation: number
|
||||
gasPrice: string
|
||||
gasToken: string
|
||||
refundReceiver: string // Address of receiver of gas payment (or 0 if tx.origin).
|
||||
safeTxGas: number
|
||||
from: string
|
||||
approvalAndExecution?: boolean
|
||||
}
|
||||
|
||||
export const estimateGasForTransactionExecution = async ({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txConfirmations,
|
||||
txAmount,
|
||||
txData,
|
||||
operation,
|
||||
gasPrice,
|
||||
gasToken,
|
||||
refundReceiver,
|
||||
safeTxGas,
|
||||
approvalAndExecution,
|
||||
}: TransactionExecutionEstimationProps): Promise<number> => {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
try {
|
||||
if (approvalAndExecution) {
|
||||
console.info(`Estimating transaction success for execution & approval...`)
|
||||
// @todo (agustin) once we solve the problem with the preApprovingOwner, we need to use the method bellow (execTransaction) with sigs = generateSignaturesFromTxConfirmations(txConfirmations,from)
|
||||
const gasEstimation = await estimateGasForTransactionCreation(
|
||||
safeAddress,
|
||||
txData,
|
||||
txRecipient,
|
||||
txAmount,
|
||||
operation,
|
||||
)
|
||||
console.info(`Gas estimation successfully finished with gas amount: ${gasEstimation}`)
|
||||
return gasEstimation
|
||||
}
|
||||
const sigs = generateSignaturesFromTxConfirmations(txConfirmations)
|
||||
console.info(`Estimating transaction success for with gas amount: ${safeTxGas}...`)
|
||||
await safeInstance.methods
|
||||
.execTransaction(txRecipient, txAmount, txData, operation, safeTxGas, 0, gasPrice, gasToken, refundReceiver, sigs)
|
||||
.call()
|
||||
|
||||
console.info(`Gas estimation successfully finished with gas amount: ${safeTxGas}`)
|
||||
return safeTxGas
|
||||
} catch (error) {
|
||||
throw new Error(`Gas estimation failed with gas amount: ${safeTxGas}`)
|
||||
}
|
||||
}
|
||||
|
||||
type TransactionApprovalEstimationProps = {
|
||||
txData: string
|
||||
safeAddress: string
|
||||
txRecipient: string
|
||||
txAmount: string
|
||||
operation: number
|
||||
from: string
|
||||
isOffChainSignature: boolean
|
||||
}
|
||||
|
||||
export const estimateGasForTransactionApproval = async ({
|
||||
safeAddress,
|
||||
txRecipient,
|
||||
txAmount,
|
||||
txData,
|
||||
operation,
|
||||
from,
|
||||
isOffChainSignature,
|
||||
}: TransactionApprovalEstimationProps): Promise<number> => {
|
||||
if (isOffChainSignature) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
|
||||
const nonce = await safeInstance.methods.nonce().call()
|
||||
const txHash = await safeInstance.methods
|
||||
.getTransactionHash(txRecipient, txAmount, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, nonce)
|
||||
.call({
|
||||
from,
|
||||
})
|
||||
const approveTransactionTxData = await safeInstance.methods.approveHash(txHash).encodeABI()
|
||||
return calculateGasOf({
|
||||
data: approveTransactionTxData,
|
||||
from,
|
||||
to: safeAddress,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -30,10 +30,7 @@ export const getTransactionHash = async ({
|
|||
return txHash
|
||||
}
|
||||
|
||||
export const getApprovalTransaction = async (
|
||||
safeInstance: GnosisSafe,
|
||||
txHash: string,
|
||||
): Promise<NonPayableTransactionObject<void>> => {
|
||||
export const getApprovalTransaction = (safeInstance: GnosisSafe, txHash: string): NonPayableTransactionObject<void> => {
|
||||
try {
|
||||
return safeInstance.methods.approveHash(txHash)
|
||||
} catch (err) {
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
SAFE_MASTER_COPY_ADDRESS,
|
||||
getGnosisSafeInstanceAt,
|
||||
} from 'src/logic/contracts/safeContracts'
|
||||
import { DELEGATE_CALL } from 'src/logic/safe/transactions'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { MultiSend } from 'src/types/contracts/MultiSend.d'
|
||||
|
||||
|
@ -49,7 +48,7 @@ export const getEncodedMultiSendCallData = (txs: MultiSendTx[], web3: Web3): str
|
|||
return encodedMultiSendCallData
|
||||
}
|
||||
|
||||
export const upgradeSafeToLatestVersion = async (safeAddress: string, createTransaction): Promise<void> => {
|
||||
export const getUpgradeSafeTransactionHash = async (safeAddress: string): Promise<string> => {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const fallbackHandlerTxData = safeInstance.methods.setFallbackHandler(DEFAULT_FALLBACK_HANDLER_ADDRESS).encodeABI()
|
||||
const updateSafeTxData = safeInstance.methods.changeMasterCopy(SAFE_MASTER_COPY_ADDRESS).encodeABI()
|
||||
|
@ -69,17 +68,5 @@ export const upgradeSafeToLatestVersion = async (safeAddress: string, createTran
|
|||
]
|
||||
|
||||
const web3 = getWeb3()
|
||||
const encodeMultiSendCallData = getEncodedMultiSendCallData(txs, web3)
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: MULTI_SEND_ADDRESS,
|
||||
valueInWei: 0,
|
||||
txData: encodeMultiSendCallData,
|
||||
notifiedTransaction: 'STANDARD_TX',
|
||||
enqueueSnackbar: () => {},
|
||||
closeSnackbar: () => {},
|
||||
operation: DELEGATE_CALL,
|
||||
})
|
||||
|
||||
return
|
||||
return getEncodedMultiSendCallData(txs, web3)
|
||||
}
|
||||
|
|
|
@ -50,10 +50,16 @@ export const calculateGasPrice = async (): Promise<string> => {
|
|||
}
|
||||
}
|
||||
|
||||
export const calculateGasOf = async (data: string, from: string, to: string): Promise<number> => {
|
||||
export const calculateGasOf = async (txConfig: {
|
||||
to: string
|
||||
from: string
|
||||
data: string
|
||||
gasPrice?: number
|
||||
gas?: number
|
||||
}): Promise<number> => {
|
||||
const web3 = getWeb3()
|
||||
try {
|
||||
const gas = await web3.eth.estimateGas({ data, from, to })
|
||||
const gas = await web3.eth.estimateGas(txConfig)
|
||||
|
||||
return gas * 2
|
||||
} catch (err) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
getOwnerAddressBy,
|
||||
getOwnerNameBy,
|
||||
} from 'src/routes/open/components/fields'
|
||||
import Welcome from 'src/routes/welcome/components/Layout'
|
||||
import { WelcomeLayout } from 'src/routes/welcome/components/index'
|
||||
import { history } from 'src/store'
|
||||
import { secondary, sm } from 'src/theme/variables'
|
||||
import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||
|
@ -138,7 +138,7 @@ export const Layout = (props: LayoutProps): React.ReactElement => {
|
|||
</Stepper>
|
||||
</Block>
|
||||
) : (
|
||||
<Welcome isOldMultisigMigration provider={provider} />
|
||||
<WelcomeLayout isOldMultisigMigration />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -32,7 +32,7 @@ import { LoadingContainer } from 'src/components/LoaderContainer/index'
|
|||
import { TIMEOUT } from 'src/utils/constants'
|
||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
import ConfirmTransactionModal from '../components/ConfirmTransactionModal'
|
||||
import { ConfirmTransactionModal } from '../components/ConfirmTransactionModal'
|
||||
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
||||
import { useLegalConsent } from '../hooks/useLegalConsent'
|
||||
import LegalDisclaimer from './LegalDisclaimer'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { Icon, Text, Title, GenericModal, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components'
|
||||
import { GenericModal, Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components'
|
||||
import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1'
|
||||
import styled from 'styled-components'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
@ -20,11 +20,13 @@ import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
|||
import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||
import { DELEGATE_CALL, TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend'
|
||||
import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas'
|
||||
|
||||
import GasEstimationInfo from './GasEstimationInfo'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { TransactionParams } from './AppFrame'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
const isTxValid = (t: Transaction): boolean => {
|
||||
if (!['string', 'number'].includes(typeof t.value)) {
|
||||
|
@ -67,6 +69,10 @@ const StyledTextBox = styled(TextBox)`
|
|||
max-width: 444px;
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 480px;
|
||||
`
|
||||
|
||||
type OwnProps = {
|
||||
isOpen: boolean
|
||||
app: SafeApp
|
||||
|
@ -82,7 +88,7 @@ type OwnProps = {
|
|||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
const ConfirmTransactionModal = ({
|
||||
export const ConfirmTransactionModal = ({
|
||||
isOpen,
|
||||
app,
|
||||
txs,
|
||||
|
@ -95,32 +101,25 @@ const ConfirmTransactionModal = ({
|
|||
onTxReject,
|
||||
}: OwnProps): React.ReactElement | null => {
|
||||
const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0)
|
||||
const [estimatingGas, setEstimatingGas] = useState(false)
|
||||
|
||||
const {
|
||||
gasEstimation,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
isExecution,
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: encodeMultiSendCall(txs),
|
||||
txRecipient: MULTI_SEND_ADDRESS,
|
||||
operation: DELEGATE_CALL,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const estimateGas = async () => {
|
||||
try {
|
||||
setEstimatingGas(true)
|
||||
const safeTxGas = await estimateSafeTxGas(
|
||||
undefined,
|
||||
safeAddress,
|
||||
encodeMultiSendCall(txs),
|
||||
MULTI_SEND_ADDRESS,
|
||||
'0',
|
||||
DELEGATE_CALL,
|
||||
)
|
||||
|
||||
setEstimatedSafeTxGas(safeTxGas)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setEstimatingGas(false)
|
||||
}
|
||||
}
|
||||
if (params?.safeTxGas) {
|
||||
estimateGas()
|
||||
setEstimatedSafeTxGas(gasEstimation)
|
||||
}
|
||||
}, [params, safeAddress, txs])
|
||||
}, [params, gasEstimation])
|
||||
|
||||
const dispatch = useDispatch()
|
||||
if (!isOpen) {
|
||||
|
@ -173,7 +172,7 @@ const ConfirmTransactionModal = ({
|
|||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Container>
|
||||
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<DividerLine withArrow />
|
||||
{txs.map((tx, index) => (
|
||||
|
@ -205,11 +204,20 @@ const ConfirmTransactionModal = ({
|
|||
<GasEstimationInfo
|
||||
appEstimation={params.safeTxGas}
|
||||
internalEstimation={estimatedSafeTxGas}
|
||||
loading={estimatingGas}
|
||||
loading={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -229,5 +237,3 @@ const ConfirmTransactionModal = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfirmTransactionModal
|
||||
|
|
|
@ -46,7 +46,6 @@ export const styles = createStyles({
|
|||
marginRight: sm,
|
||||
},
|
||||
currencyValueRow: {
|
||||
maxWidth: '125px',
|
||||
textAlign: 'right',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import AddressInfo from 'src/components/AddressInfo'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
|
@ -14,15 +14,15 @@ import Paragraph from 'src/components/layout/Paragraph'
|
|||
import Row from 'src/components/layout/Row'
|
||||
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
import Header from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { generateFormFieldKey, getValueFromTxInputs } from '../utils'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -45,41 +45,41 @@ const { nativeCoin } = getNetworkInfo()
|
|||
const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const { address: safeAddress } = useSelector(safeSelector) || {}
|
||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const [txParameters, setTxParameters] = useState<{
|
||||
txRecipient: string
|
||||
txData: string
|
||||
txAmount: string
|
||||
}>({ txData: '', txAmount: '', txRecipient: '' })
|
||||
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
} = useEstimateTransactionGas({
|
||||
txRecipient: txParameters?.txRecipient,
|
||||
txAmount: txParameters?.txAmount,
|
||||
txData: txParameters?.txData,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
const estimateGas = async (): Promise<void> => {
|
||||
const txData = tx.data ? tx.data.trim() : ''
|
||||
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.contractAddress as string, txData)
|
||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const formattedGasCosts = formatAmount(gasCosts)
|
||||
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
}
|
||||
}
|
||||
|
||||
estimateGas()
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [safeAddress, tx.contractAddress, tx.data])
|
||||
setTxParameters({
|
||||
txRecipient: tx.contractAddress as string,
|
||||
txAmount: tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0',
|
||||
txData: tx.data ? tx.data.trim() : '',
|
||||
})
|
||||
}, [tx.contractAddress, tx.value, tx.data, safeAddress])
|
||||
|
||||
const submitTx = async () => {
|
||||
const txRecipient = tx.contractAddress
|
||||
const txData = tx.data ? tx.data.trim() : ''
|
||||
const txValue = tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0'
|
||||
if (safeAddress) {
|
||||
if (safeAddress && txParameters) {
|
||||
dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: txRecipient as string,
|
||||
valueInWei: txValue,
|
||||
txData,
|
||||
to: txParameters?.txRecipient,
|
||||
valueInWei: txParameters?.txAmount,
|
||||
txData: txParameters?.txData,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -162,9 +162,13 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
|||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Block from 'src/components/layout/Block'
|
||||
|
@ -16,10 +16,8 @@ import Img from 'src/components/layout/Img'
|
|||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||
|
@ -29,6 +27,8 @@ import ArrowDown from '../../assets/arrow-down.svg'
|
|||
|
||||
import { styles } from './style'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
export type CustomTx = {
|
||||
contractAddress?: string
|
||||
|
@ -49,29 +49,19 @@ const { nativeCoin } = getNetworkInfo()
|
|||
const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const { address: safeAddress } = useSelector(safeSelector) || {}
|
||||
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
|
||||
const estimateGas = async () => {
|
||||
const txData = tx.data ? tx.data.trim() : ''
|
||||
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, tx.contractAddress as string, txData)
|
||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const formattedGasCosts = formatAmount(gasCosts)
|
||||
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
}
|
||||
}
|
||||
|
||||
estimateGas()
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [safeAddress, tx.data, tx.contractAddress])
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
} = useEstimateTransactionGas({
|
||||
txRecipient: tx.contractAddress as string,
|
||||
txData: tx.data ? tx.data.trim() : '',
|
||||
txAmount: tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0',
|
||||
})
|
||||
|
||||
const submitTx = async (): Promise<void> => {
|
||||
const txRecipient = tx.contractAddress
|
||||
|
@ -161,9 +151,13 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
|
|||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
|
|
|
@ -7,7 +7,7 @@ import GnoForm from 'src/components/forms/GnoForm'
|
|||
import Block from 'src/components/layout/Block'
|
||||
import Hairline from 'src/components/layout/Hairline'
|
||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Buttons from './Buttons'
|
||||
import ContractABI from './ContractABI'
|
||||
|
@ -53,14 +53,14 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({
|
|||
isABI,
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const { address: safeAddress = '' } = useSelector(safeSelector) || {}
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
let setCallResults
|
||||
|
||||
React.useMemo(() => {
|
||||
if (contractAddress) {
|
||||
initialValues.contractAddress = contractAddress
|
||||
}
|
||||
}, [contractAddress, initialValues.contractAddress])
|
||||
}, [contractAddress, initialValues])
|
||||
|
||||
const saveForm = async (values: CreatedTx): Promise<void> => {
|
||||
await handleSubmit(values, false)
|
||||
|
|
|
@ -3,9 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Block from 'src/components/layout/Block'
|
||||
|
@ -17,10 +15,8 @@ import Paragraph from 'src/components/layout/Paragraph'
|
|||
import Row from 'src/components/layout/Row'
|
||||
import { nftTokensSelector } from 'src/logic/collectibles/store/selectors'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||
import { sm } from 'src/theme/variables'
|
||||
|
@ -31,8 +27,8 @@ import ArrowDown from '../assets/arrow-down.svg'
|
|||
|
||||
import { styles } from './style'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -53,34 +49,38 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
|||
const classes = useStyles()
|
||||
const shortener = textShortener()
|
||||
const dispatch = useDispatch()
|
||||
const { address: safeAddress } = useSelector(safeSelector) || {}
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const nftTokens = useSelector(nftTokensSelector)
|
||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
||||
const txToken = nftTokens.find(
|
||||
({ assetAddress, tokenId }) => assetAddress === tx.assetAddress && tokenId === tx.nftTokenId,
|
||||
)
|
||||
const [data, setData] = useState('')
|
||||
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: data,
|
||||
txRecipient: tx.assetAddress,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
const estimateGas = async () => {
|
||||
const calculateERC721TransferData = async () => {
|
||||
try {
|
||||
const txData = await generateERC721TransferTxData(tx, safeAddress)
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress ?? '', tx.recipientAddress, txData)
|
||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const formattedGasCosts = formatAmount(gasCosts)
|
||||
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
setData(txData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while calculating estimated gas:', error)
|
||||
console.error('Error calculating ERC721 transfer data:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
estimateGas()
|
||||
calculateERC721TransferData()
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
|
@ -164,9 +164,13 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
|
|||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<Paragraph>
|
||||
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
|
|
|
@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import Close from '@material-ui/icons/Close'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { toTokenUnit, fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
|
@ -17,11 +17,9 @@ import Paragraph from 'src/components/layout/Paragraph'
|
|||
import Row from 'src/components/layout/Row'
|
||||
import { getSpendingLimitContract } from 'src/logic/contracts/safeContracts'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
|
||||
import { getHumanFriendlyToken } from 'src/logic/tokens/store/actions/fetchTokens'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
|
@ -35,7 +33,10 @@ import ArrowDown from '../assets/arrow-down.svg'
|
|||
|
||||
import { styles } from './style'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import { TokenProps } from 'src/logic/tokens/store/model/token'
|
||||
import { RecordOf } from 'immutable'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
@ -55,59 +56,61 @@ type ReviewTxProps = {
|
|||
tx: ReviewTxProp
|
||||
}
|
||||
|
||||
const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const { address: safeAddress } = useSelector(safeSelector) || {}
|
||||
const tokens = useSelector(extendedSafeTokensSelector)
|
||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
||||
const useTxData = (
|
||||
isSendingNativeToken: boolean,
|
||||
txAmount: string,
|
||||
recipientAddress: string,
|
||||
txToken?: RecordOf<TokenProps>,
|
||||
): string => {
|
||||
const [data, setData] = useState('')
|
||||
|
||||
const txToken = useMemo(() => tokens.find((token) => sameAddress(token.address, tx.token)), [tokens, tx.token])
|
||||
const isSendingETH = sameAddress(txToken?.address, nativeCoin.address)
|
||||
const txRecipient = isSendingETH ? tx.recipientAddress : txToken?.address
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
const estimateGas = async () => {
|
||||
const updateTxDataAsync = async () => {
|
||||
if (!txToken) {
|
||||
return
|
||||
}
|
||||
|
||||
let txData = EMPTY_DATA
|
||||
|
||||
if (!isSendingETH) {
|
||||
if (!isSendingNativeToken) {
|
||||
const StandardToken = await getHumanFriendlyToken()
|
||||
const tokenInstance = await StandardToken.at(txToken.address as string)
|
||||
const txAmount = toTokenUnit(tx.amount, txToken.decimals)
|
||||
|
||||
txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI()
|
||||
}
|
||||
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress as string, txRecipient as string, txData)
|
||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const formattedGasCosts = formatAmount(gasCosts)
|
||||
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
setData(txData)
|
||||
const erc20TransferAmount = toTokenUnit(txAmount, txToken.decimals)
|
||||
txData = tokenInstance.contract.methods.transfer(recipientAddress, erc20TransferAmount).encodeABI()
|
||||
}
|
||||
setData(txData)
|
||||
}
|
||||
|
||||
estimateGas()
|
||||
updateTxDataAsync()
|
||||
}, [isSendingNativeToken, recipientAddress, txAmount, txToken])
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [isSendingETH, safeAddress, tx.amount, tx.recipientAddress, txRecipient, txToken])
|
||||
return data
|
||||
}
|
||||
|
||||
const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const tokens = useSelector(extendedSafeTokensSelector)
|
||||
const txToken = useMemo(() => tokens.find((token) => sameAddress(token.address, tx.token)), [tokens, tx.token])
|
||||
const isSendingNativeToken = sameAddress(txToken?.address, nativeCoin.address)
|
||||
const txRecipient = isSendingNativeToken ? tx.recipientAddress : txToken?.address || ''
|
||||
const txValue = isSendingNativeToken ? toTokenUnit(tx.amount, nativeCoin.decimals) : '0'
|
||||
const data = useTxData(isSendingNativeToken, tx.amount, tx.recipientAddress, txToken)
|
||||
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: data,
|
||||
txRecipient,
|
||||
txType: tx.txType,
|
||||
})
|
||||
|
||||
const submitTx = async () => {
|
||||
const isSpendingLimit = sameString(tx.txType, 'spendingLimit')
|
||||
// txAmount should be 0 if we send tokens
|
||||
// the real value is encoded in txData and will be used by the contract
|
||||
// if txAmount > 0 it would send ETH from the Safe
|
||||
const txAmount = isSendingETH ? toTokenUnit(tx.amount, nativeCoin.decimals) : '0'
|
||||
|
||||
if (!safeAddress) {
|
||||
console.error('There was an error trying to submit the transaction, the safeAddress was not found')
|
||||
|
@ -115,11 +118,12 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement =>
|
|||
}
|
||||
|
||||
if (isSpendingLimit && txToken && tx.tokenSpendingLimit) {
|
||||
const spendingLimitTokenAddress = isSendingNativeToken ? ZERO_ADDRESS : txToken.address
|
||||
const spendingLimit = getSpendingLimitContract()
|
||||
spendingLimit.methods
|
||||
.executeAllowanceTransfer(
|
||||
safeAddress,
|
||||
sameAddress(txToken.address, nativeCoin.address) ? ZERO_ADDRESS : txToken.address,
|
||||
spendingLimitTokenAddress,
|
||||
tx.recipientAddress,
|
||||
toTokenUnit(tx.amount, txToken.decimals),
|
||||
ZERO_ADDRESS,
|
||||
|
@ -135,7 +139,7 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement =>
|
|||
createTransaction({
|
||||
safeAddress: safeAddress,
|
||||
to: txRecipient as string,
|
||||
valueInWei: txAmount,
|
||||
valueInWei: txValue,
|
||||
txData: data,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
}),
|
||||
|
@ -207,9 +211,13 @@ const ReviewTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactElement =>
|
|||
</Paragraph>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph data-testid="fee-meg-review-step">
|
||||
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
|
|
|
@ -13,15 +13,7 @@ export const BALANCE_TABLE_VALUE_ID = 'value'
|
|||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
const getTokenPriceInCurrency = (
|
||||
token: Token,
|
||||
currencySelected?: string,
|
||||
currencyValues?: BalanceCurrencyList,
|
||||
currencyRate?: number,
|
||||
): string => {
|
||||
if (!currencySelected) {
|
||||
return ''
|
||||
}
|
||||
const getTokenValue = (token: Token, currencyValues?: BalanceCurrencyList, currencyRate?: number): string => {
|
||||
const currencyValue = currencyValues?.find(({ tokenAddress }) => {
|
||||
if (token.address === nativeCoin.address && !tokenAddress) {
|
||||
return true
|
||||
|
@ -35,9 +27,16 @@ const getTokenPriceInCurrency = (
|
|||
}
|
||||
|
||||
const { balanceInBaseCurrency } = currencyValue
|
||||
const balance = new BigNumber(balanceInBaseCurrency).times(currencyRate).toFixed(2)
|
||||
const balance = new BigNumber(balanceInBaseCurrency).times(currencyRate).toString()
|
||||
|
||||
return `${formatAmountInUsFormat(balance)} ${currencySelected}`
|
||||
return balance
|
||||
}
|
||||
|
||||
const getTokenPriceInCurrency = (balance: string, currencySelected?: string): string => {
|
||||
if (!currencySelected) {
|
||||
return Number('').toFixed(2)
|
||||
}
|
||||
return `${formatAmountInUsFormat(Number(balance).toFixed(2))} ${currencySelected}`
|
||||
}
|
||||
|
||||
export interface BalanceData {
|
||||
|
@ -47,6 +46,7 @@ export interface BalanceData {
|
|||
balanceOrder: number
|
||||
fixed: boolean
|
||||
value: string
|
||||
valueOrder: number
|
||||
}
|
||||
|
||||
export const getBalanceData = (
|
||||
|
@ -56,19 +56,23 @@ export const getBalanceData = (
|
|||
currencyRate?: number,
|
||||
): List<BalanceData> => {
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
return activeTokens.map((token) => ({
|
||||
[BALANCE_TABLE_ASSET_ID]: {
|
||||
name: token.name,
|
||||
logoUri: token.logoUri,
|
||||
address: token.address,
|
||||
symbol: token.symbol,
|
||||
},
|
||||
assetOrder: token.name,
|
||||
[BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance?.toString() || '0')} ${token.symbol}`,
|
||||
balanceOrder: Number(token.balance),
|
||||
[FIXED]: token.symbol === nativeCoin.symbol,
|
||||
[BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(token, currencySelected, currencyValues, currencyRate),
|
||||
}))
|
||||
return activeTokens.map((token) => {
|
||||
const balance = getTokenValue(token, currencyValues, currencyRate)
|
||||
return {
|
||||
[BALANCE_TABLE_ASSET_ID]: {
|
||||
name: token.name,
|
||||
logoUri: token.logoUri,
|
||||
address: token.address,
|
||||
symbol: token.symbol,
|
||||
},
|
||||
assetOrder: token.name,
|
||||
[BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance?.toString() || '0')} ${token.symbol}`,
|
||||
balanceOrder: Number(token.balance),
|
||||
[FIXED]: token.symbol === nativeCoin.symbol,
|
||||
[BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(balance, currencySelected),
|
||||
valueOrder: Number(balance),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const generateColumns = (): List<TableColumn> => {
|
||||
|
@ -101,21 +105,11 @@ export const generateColumns = (): List<TableColumn> => {
|
|||
|
||||
const value: TableColumn = {
|
||||
id: BALANCE_TABLE_VALUE_ID,
|
||||
order: false,
|
||||
align: 'right',
|
||||
order: true,
|
||||
label: 'Value',
|
||||
custom: false,
|
||||
static: true,
|
||||
disablePadding: false,
|
||||
style: {
|
||||
fontSize: '11px',
|
||||
color: '#5d6d74',
|
||||
borderBottomWidth: '2px',
|
||||
width: '125px',
|
||||
fontFamily: 'Averta',
|
||||
fontWeight: 'normal',
|
||||
fontStyle: 'normal',
|
||||
textAlign: 'right',
|
||||
},
|
||||
}
|
||||
|
||||
return List([assetColumn, balanceColumn, value, actions])
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { OwnerForm } from './screens/OwnerForm'
|
||||
import ReviewAddOwner from './screens/Review'
|
||||
import { ReviewAddOwner } from './screens/Review'
|
||||
import ThresholdForm from './screens/ThresholdForm'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import classNames from 'classnames'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Block from 'src/components/layout/Block'
|
||||
|
@ -16,37 +15,61 @@ import Paragraph from 'src/components/layout/Paragraph'
|
|||
import Row from 'src/components/layout/Row'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
|
||||
import { styles } from './style'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
export const ADD_OWNER_SUBMIT_BTN_TEST_ID = 'add-owner-submit-btn'
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) => {
|
||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
||||
type ReviewAddOwnerProps = {
|
||||
onClickBack: () => void
|
||||
onClose: () => void
|
||||
onSubmit: () => void
|
||||
values: {
|
||||
ownerAddress: string
|
||||
threshold: string
|
||||
ownerName: string
|
||||
}
|
||||
}
|
||||
|
||||
export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: ReviewAddOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [data, setData] = useState('')
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: data,
|
||||
txRecipient: safeAddress,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
const estimateGas = async () => {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
|
||||
const txData = safeInstance.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI()
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData)
|
||||
const calculateAddOwnerData = async () => {
|
||||
try {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const txData = safeInstance.methods.addOwnerWithThreshold(values.ownerAddress, values.threshold).encodeABI()
|
||||
|
||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const formattedGasCosts = formatAmount(gasCosts)
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
if (isCurrent) {
|
||||
setData(txData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error calculating ERC721 transfer data:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
estimateGas()
|
||||
calculateAddOwnerData()
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
|
@ -68,7 +91,7 @@ const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) =>
|
|||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.formContainer}>
|
||||
<Block>
|
||||
<Row className={classes.root}>
|
||||
<Col layout="column" xs={4}>
|
||||
<Block className={classes.details}>
|
||||
|
@ -157,11 +180,13 @@ const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) =>
|
|||
</Block>
|
||||
<Hairline />
|
||||
<Block className={classes.gasCostsContainer}>
|
||||
<Paragraph>
|
||||
You're about to create a transaction and will have to confirm it with your currently connected wallet.
|
||||
<br />
|
||||
{`Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
|
@ -183,5 +208,3 @@ const ReviewAddOwner = ({ classes, onClickBack, onClose, onSubmit, values }) =>
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(ReviewAddOwner)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { background, border, lg, secondaryText, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
root: {
|
||||
height: '372px',
|
||||
},
|
||||
|
@ -76,7 +77,9 @@ export const styles = () => ({
|
|||
},
|
||||
},
|
||||
gasCostsContainer: {
|
||||
padding: `0 ${lg}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
|
|
|
@ -40,17 +40,18 @@ type OwnProps = {
|
|||
selectedOwnerName: string
|
||||
}
|
||||
|
||||
const EditOwnerComponent = ({ isOpen, onClose, ownerAddress, selectedOwnerName }: OwnProps): React.ReactElement => {
|
||||
export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerName }: OwnProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
||||
const handleSubmit = (values) => {
|
||||
const { ownerName } = values
|
||||
|
||||
dispatch(editSafeOwner({ safeAddress, ownerAddress, ownerName }))
|
||||
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName })))
|
||||
dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG))
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
|
||||
const handleSubmit = ({ ownerName }: { ownerName: string }): void => {
|
||||
// Update the value only if the ownerName really changed
|
||||
if (ownerName !== selectedOwnerName) {
|
||||
dispatch(editSafeOwner({ safeAddress, ownerAddress, ownerName }))
|
||||
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName })))
|
||||
dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG))
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
|
||||
|
@ -71,54 +72,56 @@ const EditOwnerComponent = ({ isOpen, onClose, ownerAddress, selectedOwnerName }
|
|||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<GnoForm onSubmit={handleSubmit}>
|
||||
{() => (
|
||||
<>
|
||||
<Block className={classes.container}>
|
||||
<Row margin="md">
|
||||
<Field
|
||||
component={TextField}
|
||||
initialValue={selectedOwnerName}
|
||||
name="ownerName"
|
||||
placeholder="Owner name*"
|
||||
testId={RENAME_OWNER_INPUT_TEST_ID}
|
||||
text="Owner name*"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
/>
|
||||
<GnoForm onSubmit={handleSubmit} subscription={{ pristine: true }}>
|
||||
{(...args) => {
|
||||
const pristine = args[2].pristine
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.container}>
|
||||
<Row margin="md">
|
||||
<Field
|
||||
component={TextField}
|
||||
initialValue={selectedOwnerName}
|
||||
name="ownerName"
|
||||
placeholder="Owner name*"
|
||||
testId={RENAME_OWNER_INPUT_TEST_ID}
|
||||
text="Owner name*"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Block justify="center">
|
||||
<Identicon address={ownerAddress} diameter={32} />
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ marginLeft: sm, marginRight: sm }}>
|
||||
{ownerAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={ownerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(ownerAddress)} />
|
||||
</Block>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
minHeight={42}
|
||||
minWidth={140}
|
||||
testId={SAVE_OWNER_CHANGES_BTN_TEST_ID}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={pristine}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Row>
|
||||
<Row>
|
||||
<Block justify="center">
|
||||
<Identicon address={ownerAddress} diameter={32} />
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ marginLeft: sm, marginRight: sm }}>
|
||||
{ownerAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={ownerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(ownerAddress)} />
|
||||
</Block>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
minHeight={42}
|
||||
minWidth={140}
|
||||
testId={SAVE_OWNER_CHANGES_BTN_TEST_ID}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</GnoForm>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditOwnerComponent
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import CheckOwner from './screens/CheckOwner'
|
||||
import ReviewRemoveOwner from './screens/Review'
|
||||
import { ReviewRemoveOwnerModal } from './screens/Review'
|
||||
import ThresholdForm from './screens/ThresholdForm'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
|
@ -69,7 +69,12 @@ type RemoveOwnerProps = {
|
|||
ownerName: string
|
||||
}
|
||||
|
||||
const RemoveOwner = ({ isOpen, onClose, ownerAddress, ownerName }: RemoveOwnerProps): React.ReactElement => {
|
||||
export const RemoveOwnerModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
}: RemoveOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||
const [values, setValues] = useState<any>({})
|
||||
|
@ -124,18 +129,16 @@ const RemoveOwner = ({ isOpen, onClose, ownerAddress, ownerName }: RemoveOwnerPr
|
|||
<ThresholdForm onClickBack={onClickBack} onClose={onClose} onSubmit={thresholdSubmitted} />
|
||||
)}
|
||||
{activeScreen === 'reviewRemoveOwner' && (
|
||||
<ReviewRemoveOwner
|
||||
<ReviewRemoveOwnerModal
|
||||
onClickBack={onClickBack}
|
||||
onClose={onClose}
|
||||
onSubmit={onRemoveOwner}
|
||||
ownerAddress={ownerAddress}
|
||||
ownerName={ownerName}
|
||||
values={values}
|
||||
threshold={threshold}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default RemoveOwner
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import classNames from 'classnames'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Block from 'src/components/layout/Block'
|
||||
|
@ -16,50 +15,84 @@ import Paragraph from 'src/components/layout/Paragraph'
|
|||
import Row from 'src/components/layout/Row'
|
||||
import { getGnosisSafeInstanceAt, SENTINEL_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||
import { safeNameSelector, safeOwnersSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
|
||||
import { styles } from './style'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { List } from 'immutable'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
export const REMOVE_OWNER_REVIEW_BTN_TEST_ID = 'remove-owner-review-btn'
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddress, ownerName, values }) => {
|
||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
||||
type ReviewRemoveOwnerProps = {
|
||||
onClickBack: () => void
|
||||
onClose: () => void
|
||||
onSubmit: () => void
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
threshold?: number
|
||||
}
|
||||
|
||||
export const ReviewRemoveOwnerModal = ({
|
||||
onClickBack,
|
||||
onClose,
|
||||
onSubmit,
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
threshold,
|
||||
}: ReviewRemoveOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [data, setData] = useState('')
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
||||
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: data,
|
||||
txRecipient: safeAddress,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
const estimateGas = async () => {
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||
const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase())
|
||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||
const txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddress, values.threshold).encodeABI()
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData)
|
||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const formattedGasCosts = formatAmount(gasCosts)
|
||||
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
}
|
||||
if (!threshold) {
|
||||
console.error("Threshold value was not define, tx can't be executed")
|
||||
return
|
||||
}
|
||||
|
||||
estimateGas()
|
||||
const calculateRemoveOwnerData = async () => {
|
||||
try {
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||
const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase())
|
||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||
const txData = gnosisSafe.methods.removeOwner(prevAddress, ownerAddress, threshold).encodeABI()
|
||||
|
||||
if (isCurrent) {
|
||||
setData(txData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error calculating ERC721 transfer data:', error.message)
|
||||
}
|
||||
}
|
||||
calculateRemoveOwnerData()
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [ownerAddress, safeAddress, values.threshold])
|
||||
}, [safeAddress, ownerAddress, threshold])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -95,7 +128,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||
Any transaction requires the confirmation of:
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
|
||||
{`${values.threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`}
|
||||
{`${threshold} out of ${owners ? owners.size - 1 : 0} owner(s)`}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Block>
|
||||
|
@ -165,11 +198,13 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||
</Block>
|
||||
<Hairline />
|
||||
<Block className={classes.gasCostsContainer}>
|
||||
<Paragraph>
|
||||
You're about to create a transaction and will have to confirm it with your currently connected wallet.
|
||||
<br />
|
||||
{`Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
|
@ -191,5 +226,3 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(ReviewRemoveOwner)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { background, border, lg, secondaryText, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
root: {
|
||||
height: '372px',
|
||||
},
|
||||
|
@ -76,7 +77,9 @@ export const styles = () => ({
|
|||
},
|
||||
},
|
||||
gasCostsContainer: {
|
||||
padding: `0 ${lg}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
|
|
|
@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'
|
|||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import OwnerForm from './screens/OwnerForm'
|
||||
import ReviewReplaceOwner from './screens/Review'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||
|
@ -16,6 +15,7 @@ import { checksumAddress } from 'src/utils/checksumAddress'
|
|||
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
import { ReviewReplaceOwnerModal } from './screens/Review'
|
||||
|
||||
const styles = createStyles({
|
||||
biggerModalWindow: {
|
||||
|
@ -28,9 +28,8 @@ const styles = createStyles({
|
|||
const useStyles = makeStyles(styles)
|
||||
|
||||
type OwnerValues = {
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
threshold: string
|
||||
newOwnerAddress: string
|
||||
newOwnerName: string
|
||||
}
|
||||
|
||||
export const sendReplaceOwner = async (
|
||||
|
@ -44,7 +43,7 @@ export const sendReplaceOwner = async (
|
|||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||
const index = safeOwners.findIndex((ownerAddress) => sameAddress(ownerAddress, ownerAddressToRemove))
|
||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddressToRemove, values.ownerAddress).encodeABI()
|
||||
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddressToRemove, values.newOwnerAddress).encodeABI()
|
||||
|
||||
const txHash = await dispatch(
|
||||
createTransaction({
|
||||
|
@ -61,8 +60,8 @@ export const sendReplaceOwner = async (
|
|||
replaceSafeOwner({
|
||||
safeAddress,
|
||||
oldOwnerAddress: ownerAddressToRemove,
|
||||
ownerAddress: values.ownerAddress,
|
||||
ownerName: values.ownerName,
|
||||
ownerAddress: values.newOwnerAddress,
|
||||
ownerName: values.newOwnerName,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
@ -75,10 +74,18 @@ type ReplaceOwnerProps = {
|
|||
ownerName: string
|
||||
}
|
||||
|
||||
const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwnerProps): React.ReactElement => {
|
||||
export const ReplaceOwnerModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
}: ReplaceOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||
const [values, setValues] = useState<any>({})
|
||||
const [values, setValues] = useState({
|
||||
newOwnerAddress: '',
|
||||
newOwnerName: '',
|
||||
})
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
|
@ -86,7 +93,10 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner
|
|||
useEffect(
|
||||
() => () => {
|
||||
setActiveScreen('checkOwner')
|
||||
setValues({})
|
||||
setValues({
|
||||
newOwnerAddress: '',
|
||||
newOwnerName: '',
|
||||
})
|
||||
},
|
||||
[isOpen],
|
||||
)
|
||||
|
@ -96,9 +106,10 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner
|
|||
const ownerSubmitted = (newValues) => {
|
||||
const { ownerAddress, ownerName } = newValues
|
||||
const checksumAddr = checksumAddress(ownerAddress)
|
||||
values.ownerName = ownerName
|
||||
values.ownerAddress = checksumAddr
|
||||
setValues(values)
|
||||
setValues({
|
||||
newOwnerAddress: checksumAddr,
|
||||
newOwnerName: ownerName,
|
||||
})
|
||||
setActiveScreen('reviewReplaceOwner')
|
||||
}
|
||||
|
||||
|
@ -108,7 +119,9 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner
|
|||
await sendReplaceOwner(values, safeAddress, ownerAddress, dispatch, threshold)
|
||||
|
||||
dispatch(
|
||||
addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: values.ownerAddress, name: values.ownerName })),
|
||||
addOrUpdateAddressBookEntry(
|
||||
makeAddressBookEntry({ address: values.newOwnerAddress, name: values.newOwnerName }),
|
||||
),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error while removing an owner', error)
|
||||
|
@ -128,7 +141,7 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner
|
|||
<OwnerForm onClose={onClose} onSubmit={ownerSubmitted} ownerAddress={ownerAddress} ownerName={ownerName} />
|
||||
)}
|
||||
{activeScreen === 'reviewReplaceOwner' && (
|
||||
<ReviewReplaceOwner
|
||||
<ReviewReplaceOwnerModal
|
||||
onClickBack={onClickBack}
|
||||
onClose={onClose}
|
||||
onSubmit={onReplaceOwner}
|
||||
|
@ -141,5 +154,3 @@ const ReplaceOwner = ({ isOpen, onClose, ownerAddress, ownerName }: ReplaceOwner
|
|||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReplaceOwner
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import classNames from 'classnames'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
@ -7,8 +7,7 @@ import { useSelector } from 'react-redux'
|
|||
import { List } from 'immutable'
|
||||
import { ExplorerButton } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
import { getExplorerInfo } from 'src/config'
|
||||
import CopyBtn from 'src/components/CopyBtn'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Block from 'src/components/layout/Block'
|
||||
|
@ -24,47 +23,75 @@ import {
|
|||
safeParamAddressFromStateSelector,
|
||||
safeThresholdSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
|
||||
import { styles } from './style'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
export const REPLACE_OWNER_SUBMIT_BTN_TEST_ID = 'replace-owner-submit-btn'
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddress, ownerName, values }) => {
|
||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
||||
type ReplaceOwnerProps = {
|
||||
onClose: () => void
|
||||
onClickBack: () => void
|
||||
onSubmit: () => void
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
values: {
|
||||
newOwnerAddress: string
|
||||
newOwnerName: string
|
||||
}
|
||||
}
|
||||
|
||||
export const ReviewReplaceOwnerModal = ({
|
||||
onClickBack,
|
||||
onClose,
|
||||
onSubmit,
|
||||
ownerAddress,
|
||||
ownerName,
|
||||
values,
|
||||
}: ReplaceOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [data, setData] = useState('')
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
|
||||
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: data,
|
||||
txRecipient: safeAddress,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
const estimateGas = async () => {
|
||||
const calculateReplaceOwnerData = async () => {
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const safeOwners = await gnosisSafe.methods.getOwners().call()
|
||||
const index = safeOwners.findIndex((owner) => owner.toLowerCase() === ownerAddress.toLowerCase())
|
||||
const prevAddress = index === 0 ? SENTINEL_ADDRESS : safeOwners[index - 1]
|
||||
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddress, values.ownerAddress).encodeABI()
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData)
|
||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const formattedGasCosts = formatAmount(gasCosts)
|
||||
const txData = gnosisSafe.methods.swapOwner(prevAddress, ownerAddress, values.newOwnerAddress).encodeABI()
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
setData(txData)
|
||||
}
|
||||
}
|
||||
|
||||
estimateGas()
|
||||
calculateReplaceOwnerData()
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [ownerAddress, safeAddress, values.ownerAddress])
|
||||
}, [ownerAddress, safeAddress, values.newOwnerAddress])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -78,7 +105,7 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.formContainer}>
|
||||
<Block>
|
||||
<Row className={classes.root}>
|
||||
<Col layout="column" xs={4}>
|
||||
<Block className={classes.details}>
|
||||
|
@ -172,19 +199,19 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||
<Hairline />
|
||||
<Row className={classes.selectedOwnerAdded}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={values.ownerAddress} diameter={32} />
|
||||
<Identicon address={values.newOwnerAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={classNames(classes.name, classes.userName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{values.ownerName}
|
||||
{values.newOwnerName}
|
||||
</Paragraph>
|
||||
<Block className={classes.user} justify="center">
|
||||
<Paragraph className={classes.address} color="disabled" noMargin size="md">
|
||||
{values.ownerAddress}
|
||||
{values.newOwnerAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={values.ownerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(values.ownerAddress)} />
|
||||
<CopyBtn content={values.newOwnerAddress} />
|
||||
<ExplorerButton explorerUrl={getExplorerInfo(values.newOwnerAddress)} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
|
@ -195,11 +222,13 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||
</Block>
|
||||
<Hairline />
|
||||
<Block className={classes.gasCostsContainer}>
|
||||
<Paragraph>
|
||||
You're about to create a transaction and will have to confirm it with your currently connected wallet.
|
||||
<br />
|
||||
{`Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
|
@ -221,5 +250,3 @@ const ReviewRemoveOwner = ({ classes, onClickBack, onClose, onSubmit, ownerAddre
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(ReviewRemoveOwner)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { background, border, lg, secondaryText, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
root: {
|
||||
height: '372px',
|
||||
},
|
||||
|
@ -81,7 +82,9 @@ export const styles = () => ({
|
|||
},
|
||||
},
|
||||
gasCostsContainer: {
|
||||
padding: `0 ${lg}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
|
|
|
@ -9,10 +9,10 @@ import { List } from 'immutable'
|
|||
import RemoveOwnerIcon from '../assets/icons/bin.svg'
|
||||
|
||||
import AddOwnerModal from './AddOwnerModal'
|
||||
import EditOwnerModal from './EditOwnerModal'
|
||||
import { EditOwnerModal } from './EditOwnerModal'
|
||||
import OwnerAddressTableCell from './OwnerAddressTableCell'
|
||||
import RemoveOwnerModal from './RemoveOwnerModal'
|
||||
import ReplaceOwnerModal from './ReplaceOwnerModal'
|
||||
import { RemoveOwnerModal } from './RemoveOwnerModal'
|
||||
import { ReplaceOwnerModal } from './ReplaceOwnerModal'
|
||||
import RenameOwnerIcon from './assets/icons/rename-owner.svg'
|
||||
import ReplaceOwnerIcon from './assets/icons/replace-owner.svg'
|
||||
import { OWNERS_TABLE_ADDRESS_ID, OWNERS_TABLE_NAME_ID, generateColumns, getOwnerData } from './dataFetcher'
|
||||
|
|
|
@ -18,9 +18,16 @@ import Hairline from 'src/components/layout/Hairline'
|
|||
import Link from 'src/components/layout/Link'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import removeSafe from 'src/logic/safe/store/actions/removeSafe'
|
||||
import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import {
|
||||
defaultSafeSelector,
|
||||
safeNameSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { md, secondary } from 'src/theme/variables'
|
||||
import { WELCOME_ADDRESS } from 'src/routes/routes'
|
||||
import { removeLocalSafe } from 'src/logic/safe/store/actions/removeLocalSafe'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { saveDefaultSafe } from 'src/logic/safe/utils'
|
||||
|
||||
const openIconStyle = {
|
||||
height: md,
|
||||
|
@ -29,14 +36,33 @@ const openIconStyle = {
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const RemoveSafeComponent = ({ isOpen, onClose }) => {
|
||||
type RemoveSafeModalProps = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const RemoveSafeModal = ({ isOpen, onClose }: RemoveSafeModalProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const safeName = useSelector(safeNameSelector)
|
||||
const defaultSafe = useSelector(defaultSafeSelector)
|
||||
const dispatch = useDispatch()
|
||||
const explorerInfo = getExplorerInfo(safeAddress)
|
||||
const { url } = explorerInfo()
|
||||
|
||||
const onRemoveSafeHandler = async () => {
|
||||
await dispatch(removeLocalSafe(safeAddress))
|
||||
if (sameAddress(safeAddress, defaultSafe)) {
|
||||
await saveDefaultSafe('')
|
||||
}
|
||||
|
||||
onClose()
|
||||
// using native redirect in order to avoid problems in several components
|
||||
// trying to access references of the removed safe.
|
||||
const relativePath = window.location.href.split('/#/')[0]
|
||||
window.location.href = `${relativePath}/#/${WELCOME_ADDRESS}`
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
description="Remove the selected Safe"
|
||||
|
@ -91,13 +117,7 @@ const RemoveSafeComponent = ({ isOpen, onClose }) => {
|
|||
<Button
|
||||
className={classes.buttonRemove}
|
||||
minWidth={140}
|
||||
onClick={() => {
|
||||
dispatch(removeSafe(safeAddress))
|
||||
onClose()
|
||||
// using native redirect in order to avoid problems in several components
|
||||
// trying to access references of the removed safe.
|
||||
window.location.href = '/app/'
|
||||
}}
|
||||
onClick={onRemoveSafeHandler}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
|
@ -107,5 +127,3 @@ const RemoveSafeComponent = ({ isOpen, onClose }) => {
|
|||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export const RemoveSafeModal = RemoveSafeComponent
|
||||
|
|
|
@ -19,7 +19,7 @@ import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackb
|
|||
import { getNotificationsFromTxType, enhanceSnackbarForAction } from 'src/logic/notifications'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import UpdateSafeModal from 'src/routes/safe/components/Settings/UpdateSafeModal'
|
||||
import { UpdateSafeModal } from 'src/routes/safe/components/Settings/UpdateSafeModal'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
||||
import Link from 'src/components/layout/Link'
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { styles } from './style'
|
||||
|
||||
import Field from 'src/components/forms/Field'
|
||||
|
@ -18,35 +16,60 @@ import Hairline from 'src/components/layout/Hairline'
|
|||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { SafeOwner } from 'src/logic/safe/store/models/safe'
|
||||
import { List } from 'immutable'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
const THRESHOLD_FIELD_NAME = 'threshold'
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddress, threshold }) => {
|
||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
||||
type ChangeThresholdModalProps = {
|
||||
onChangeThreshold: (newThreshold: number) => void
|
||||
onClose: () => void
|
||||
owners?: List<SafeOwner>
|
||||
safeAddress: string
|
||||
threshold?: number
|
||||
}
|
||||
|
||||
export const ChangeThresholdModal = ({
|
||||
onChangeThreshold,
|
||||
onClose,
|
||||
owners,
|
||||
safeAddress,
|
||||
threshold = 1,
|
||||
}: ChangeThresholdModalProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [data, setData] = useState('')
|
||||
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isCreation,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: data,
|
||||
txRecipient: safeAddress,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
const estimateGasCosts = async () => {
|
||||
const calculateChangeThresholdData = async () => {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const txData = safeInstance.methods.changeThreshold('1').encodeABI()
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, txData)
|
||||
const gasCostsAsEth = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const formattedGasCosts = formatAmount(gasCostsAsEth)
|
||||
const txData = safeInstance.methods.changeThreshold(threshold).encodeABI()
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
setData(txData)
|
||||
}
|
||||
}
|
||||
|
||||
estimateGasCosts()
|
||||
|
||||
calculateChangeThresholdData()
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [safeAddress])
|
||||
}, [safeAddress, threshold])
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
const newThreshold = values[THRESHOLD_FIELD_NAME]
|
||||
|
@ -81,7 +104,7 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr
|
|||
render={(props) => (
|
||||
<>
|
||||
<SelectField {...props} disableError>
|
||||
{[...Array(Number(owners.size))].map((x, index) => (
|
||||
{[...Array(Number(owners?.size))].map((x, index) => (
|
||||
<MenuItem key={index} value={`${index + 1}`}>
|
||||
{index + 1}
|
||||
</MenuItem>
|
||||
|
@ -99,14 +122,18 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr
|
|||
</Col>
|
||||
<Col xs={10}>
|
||||
<Paragraph className={classes.ownersText} color="primary" noMargin size="lg">
|
||||
{`out of ${owners.size} owner(s)`}
|
||||
{`out of ${owners?.size} owner(s)`}
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
|
@ -124,5 +151,3 @@ const ChangeThreshold = ({ classes, onChangeThreshold, onClose, owners, safeAddr
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(ChangeThreshold)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { lg, md, secondaryText, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
heading: {
|
||||
padding: `${sm} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
|
|
|
@ -2,7 +2,7 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import ChangeThreshold from './ChangeThreshold'
|
||||
import { ChangeThresholdModal } from './ChangeThreshold'
|
||||
import { styles } from './style'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
|
@ -30,7 +30,7 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||
const [isModalOpen, setModalOpen] = useState(false)
|
||||
const dispatch = useDispatch()
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
|
||||
|
@ -38,7 +38,7 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||
setModalOpen((prevOpen) => !prevOpen)
|
||||
}
|
||||
|
||||
const onChangeThreshold = async (newThreshold) => {
|
||||
const onChangeThreshold = async (newThreshold: number) => {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const txData = safeInstance.methods.changeThreshold(newThreshold).encodeABI()
|
||||
|
||||
|
@ -87,7 +87,7 @@ const ThresholdSettings = (): React.ReactElement => {
|
|||
open={isModalOpen}
|
||||
title="Change Required Confirmations"
|
||||
>
|
||||
<ChangeThreshold
|
||||
<ChangeThresholdModal
|
||||
onChangeThreshold={onChangeThreshold}
|
||||
onClose={toggleModal}
|
||||
owners={owners}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import { withStyles } from '@material-ui/styles'
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
|
@ -13,17 +11,61 @@ import Button from 'src/components/layout/Button'
|
|||
import Hairline from 'src/components/layout/Hairline'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { upgradeSafeToLatestVersion } from 'src/logic/safe/utils/upgradeSafe'
|
||||
import { getUpgradeSafeTransactionHash } from 'src/logic/safe/utils/upgradeSafe'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts'
|
||||
import { DELEGATE_CALL } from 'src/logic/safe/transactions'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
|
||||
const UpdateSafeModal = ({ classes, onClose, safeAddress }) => {
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
safeAddress: string
|
||||
}
|
||||
|
||||
export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const [multiSendCallData, setMultiSendCallData] = useState(EMPTY_DATA)
|
||||
|
||||
useEffect(() => {
|
||||
const calculateUpgradeSafeModal = async () => {
|
||||
const encodeMultiSendCallData = await getUpgradeSafeTransactionHash(safeAddress)
|
||||
setMultiSendCallData(encodeMultiSendCallData)
|
||||
}
|
||||
calculateUpgradeSafeModal()
|
||||
}, [safeAddress])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Call the update safe method
|
||||
await upgradeSafeToLatestVersion(safeAddress, bindActionCreators(createTransaction, dispatch))
|
||||
dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: MULTI_SEND_ADDRESS,
|
||||
valueInWei: '0',
|
||||
txData: multiSendCallData,
|
||||
notifiedTransaction: 'STANDARD_TX',
|
||||
operation: DELEGATE_CALL,
|
||||
}),
|
||||
)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: multiSendCallData,
|
||||
txRecipient: safeAddress,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
|
@ -56,6 +98,15 @@ const UpdateSafeModal = ({ classes, onClose, safeAddress }) => {
|
|||
have to confirm the update in case more than one confirmation is required for this Safe.
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
|
@ -72,5 +123,3 @@ const UpdateSafeModal = ({ classes, onClose, safeAddress }) => {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(UpdateSafeModal)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { lg, md, secondaryText, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
heading: {
|
||||
padding: `${sm} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
|
|
|
@ -3,10 +3,8 @@ import FormControlLabel from '@material-ui/core/FormControlLabel'
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
|
@ -18,13 +16,13 @@ import Hairline from 'src/components/layout/Hairline'
|
|||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||
import processTransaction from 'src/logic/safe/store/actions/processTransaction'
|
||||
import { processTransaction } from 'src/logic/safe/store/actions/processTransaction'
|
||||
|
||||
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -62,9 +60,8 @@ type Props = {
|
|||
thresholdReached: boolean
|
||||
tx: Transaction
|
||||
}
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
const ApproveTxModal = ({
|
||||
export const ApproveTxModal = ({
|
||||
canExecute,
|
||||
isCancelTx = false,
|
||||
isOpen,
|
||||
|
@ -76,37 +73,27 @@ const ApproveTxModal = ({
|
|||
const userAddress = useSelector(userAccountSelector)
|
||||
const classes = useStyles()
|
||||
const threshold = useSelector(safeThresholdSelector)
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const [approveAndExecute, setApproveAndExecute] = useState(canExecute)
|
||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
||||
const { description, title } = getModalTitleAndDescription(thresholdReached, isCancelTx)
|
||||
const oneConfirmationLeft = !thresholdReached && tx.confirmations.size + 1 === threshold
|
||||
const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
const estimateGas = async () => {
|
||||
const estimatedGasCosts = await estimateTxGasCosts(
|
||||
safeAddress,
|
||||
tx.recipient,
|
||||
tx.data as string,
|
||||
tx,
|
||||
approveAndExecute ? userAddress : undefined,
|
||||
)
|
||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const formattedGasCosts = formatAmount(gasCosts)
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
}
|
||||
}
|
||||
|
||||
estimateGas()
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [approveAndExecute, safeAddress, tx, userAddress])
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
} = useEstimateTransactionGas({
|
||||
txRecipient: tx.recipient,
|
||||
txData: tx.data || '',
|
||||
txConfirmations: tx.confirmations,
|
||||
txAmount: tx.value,
|
||||
preApprovingOwner: approveAndExecute ? userAddress : undefined,
|
||||
safeTxGas: tx.safeTxGas,
|
||||
operation: tx.operation,
|
||||
})
|
||||
|
||||
const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute)
|
||||
|
||||
|
@ -118,6 +105,7 @@ const ApproveTxModal = ({
|
|||
userAddress,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
|
||||
approveAndExecute: canExecute && approveAndExecute && isTheTxReadyToBeExecuted,
|
||||
thresholdReached,
|
||||
}),
|
||||
)
|
||||
onClose()
|
||||
|
@ -159,15 +147,13 @@ const ApproveTxModal = ({
|
|||
</>
|
||||
)}
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
{`You're about to ${
|
||||
approveAndExecute ? 'execute' : 'approve'
|
||||
} a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${
|
||||
nativeCoin.name
|
||||
} in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Block>
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minHeight={42} minWidth={140} onClick={onClose}>
|
||||
|
@ -188,5 +174,3 @@ const ApproveTxModal = ({
|
|||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApproveTxModal
|
||||
|
|
|
@ -33,7 +33,7 @@ function getOwnersConfirmations(tx: Transaction, userAddress: string): [string[]
|
|||
const ownersWhoConfirmed: string[] = []
|
||||
let currentUserAlreadyConfirmed = false
|
||||
|
||||
tx.confirmations.forEach((conf) => {
|
||||
tx.confirmations?.forEach((conf) => {
|
||||
if (conf.owner === userAddress) {
|
||||
currentUserAlreadyConfirmed = true
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
|
@ -16,13 +14,13 @@ import Hairline from 'src/components/layout/Hairline'
|
|||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gas'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -32,31 +30,21 @@ type Props = {
|
|||
tx: Transaction
|
||||
}
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElement => {
|
||||
const [gasCosts, setGasCosts] = useState('< 0.001')
|
||||
export const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElement => {
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const classes = useStyles()
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
const estimateGasCosts = async () => {
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, safeAddress, EMPTY_DATA)
|
||||
const gasCosts = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const formattedGasCosts = formatAmount(gasCosts)
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
}
|
||||
}
|
||||
|
||||
estimateGasCosts()
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [safeAddress])
|
||||
const {
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: EMPTY_DATA,
|
||||
txRecipient: safeAddress,
|
||||
})
|
||||
|
||||
const sendReplacementTransaction = () => {
|
||||
dispatch(
|
||||
|
@ -95,9 +83,13 @@ const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElement => {
|
|||
</Paragraph>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ${nativeCoin.name} in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
|
@ -118,5 +110,3 @@ const RejectTxModal = ({ isOpen, onClose, tx }: Props): React.ReactElement => {
|
|||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default RejectTxModal
|
||||
|
|
|
@ -26,7 +26,6 @@ const TransferDescription = ({
|
|||
}: TransferDescriptionProps): ReactElement | null => {
|
||||
const recipientName = useSelector((state) => getNameFromAddressBookSelector(state, recipient))
|
||||
const [sendModalOpen, setSendModalOpen] = React.useState(false)
|
||||
|
||||
const sendModalOpenHandler = () => {
|
||||
setSendModalOpen(true)
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ import React, { ReactElement, useMemo, useState } from 'react'
|
|||
import { useSelector } from 'react-redux'
|
||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import ApproveTxModal from './ApproveTxModal'
|
||||
import { ApproveTxModal } from './ApproveTxModal'
|
||||
import OwnersColumn from './OwnersColumn'
|
||||
import RejectTxModal from './RejectTxModal'
|
||||
import { RejectTxModal } from './RejectTxModal'
|
||||
import TxDescription from './TxDescription'
|
||||
import { IncomingTx } from './IncomingTx'
|
||||
import { CreationTx } from './CreationTx'
|
||||
|
@ -73,13 +73,11 @@ const ExpandedModuleTx = ({ tx }: { tx: SafeModuleTransaction }): ReactElement =
|
|||
</Block>
|
||||
<Hairline />
|
||||
<Block className={cn(classes.txDataContainer, classes.incomingTxBlock)}>
|
||||
{recipient && (
|
||||
<TransferDescription
|
||||
amountWithSymbol={amountWithSymbol}
|
||||
isTokenTransfer={!sameAddress(amountWithSymbol, NOT_AVAILABLE)}
|
||||
recipient={recipient}
|
||||
/>
|
||||
)}
|
||||
<TransferDescription
|
||||
amountWithSymbol={amountWithSymbol}
|
||||
isTokenTransfer={!sameAddress(amountWithSymbol, NOT_AVAILABLE)}
|
||||
recipient={recipient}
|
||||
/>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -107,7 +105,7 @@ const ExpandedSafeTx = ({ cancelTx, tx }: ExpandedSafeTxProps): ReactElement =>
|
|||
|
||||
const thresholdReached = !isIncomingTx && threshold <= tx.confirmations.size
|
||||
const canExecute = !isIncomingTx && nonce === tx.nonce
|
||||
const cancelThresholdReached = !!cancelTx && threshold <= cancelTx.confirmations.size
|
||||
const cancelThresholdReached = !!cancelTx && threshold <= cancelTx.confirmations?.size
|
||||
const canExecuteCancel = nonce === tx.nonce
|
||||
|
||||
const openRejectModal = () => {
|
||||
|
|
|
@ -121,12 +121,9 @@ const TxsTable = (): React.ReactElement => {
|
|||
colSpan={6}
|
||||
style={{ paddingBottom: 0, paddingTop: 0 }}
|
||||
>
|
||||
<Collapse
|
||||
component={() => <ExpandedTx row={row} />}
|
||||
in={expandedTx === rowId}
|
||||
timeout="auto"
|
||||
unmountOnExit
|
||||
/>
|
||||
<Collapse in={expandedTx === rowId} unmountOnExit>
|
||||
<ExpandedTx row={row} />
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -66,7 +66,7 @@ describe('getNewTxNonce', () => {
|
|||
const expectedResult = '2'
|
||||
|
||||
// when
|
||||
const result = await getNewTxNonce(undefined, lastTx, safeInstance)
|
||||
const result = await getNewTxNonce(lastTx, safeInstance)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
|
@ -82,7 +82,7 @@ describe('getNewTxNonce', () => {
|
|||
safeInstance.methods.nonce = mockFnNonce
|
||||
|
||||
// when
|
||||
const result = await getNewTxNonce(undefined, null, safeInstance)
|
||||
const result = await getNewTxNonce(null, safeInstance)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
|
@ -98,19 +98,7 @@ describe('getNewTxNonce', () => {
|
|||
const lastTx = getMockedTxServiceModel({ nonce: 10 })
|
||||
|
||||
// when
|
||||
const result = await getNewTxNonce(undefined, lastTx, safeInstance)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
})
|
||||
it('Given a pre-calculated nonce number should return it', async () => {
|
||||
// given
|
||||
const safeInstance = getMockedSafeInstance({})
|
||||
const expectedResult = '114'
|
||||
const nextNonce = '114'
|
||||
|
||||
// when
|
||||
const result = await getNewTxNonce(nextNonce, null, safeInstance)
|
||||
const result = await getNewTxNonce(lastTx, safeInstance)
|
||||
|
||||
// then
|
||||
expect(result).toBe(expectedResult)
|
||||
|
|
|
@ -16,6 +16,8 @@ import Link from 'src/components/layout/Link'
|
|||
import Block from 'src/components/layout/Block'
|
||||
import { LOAD_ADDRESS, OPEN_ADDRESS } from 'src/routes/routes'
|
||||
import { onConnectButtonClick } from 'src/components/ConnectButton'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
|
@ -67,10 +69,10 @@ const StyledButtonLink = styled(ButtonLink)`
|
|||
|
||||
type Props = {
|
||||
isOldMultisigMigration?: boolean
|
||||
provider: any
|
||||
}
|
||||
|
||||
const Welcome = ({ isOldMultisigMigration, provider }: Props): React.ReactElement => {
|
||||
export const WelcomeLayout = ({ isOldMultisigMigration }: Props): React.ReactElement => {
|
||||
const provider = useSelector(providerNameSelector)
|
||||
return (
|
||||
<Block>
|
||||
{/* Title */}
|
||||
|
@ -125,7 +127,7 @@ const Welcome = ({ isOldMultisigMigration, provider }: Props): React.ReactElemen
|
|||
color="primary"
|
||||
variant="contained"
|
||||
onClick={onConnectButtonClick}
|
||||
disabled={provider}
|
||||
disabled={!!provider}
|
||||
data-testid="connect-btn"
|
||||
>
|
||||
<Text size="xl" color="white">
|
||||
|
@ -187,5 +189,3 @@ const Welcome = ({ isOldMultisigMigration, provider }: Props): React.ReactElemen
|
|||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
export default Welcome
|
|
@ -1,16 +1,12 @@
|
|||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import Layout from '../components/Layout'
|
||||
|
||||
import selector from './selector'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { WelcomeLayout } from 'src/routes/welcome/components'
|
||||
|
||||
import Page from 'src/components/layout/Page'
|
||||
|
||||
const Welcome = ({ provider }) => (
|
||||
const Welcome = (): ReactElement => (
|
||||
<Page align="center">
|
||||
<Layout provider={provider} />
|
||||
<WelcomeLayout />
|
||||
</Page>
|
||||
)
|
||||
|
||||
export default connect(selector)(Welcome)
|
||||
export default Welcome
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import { createStructuredSelector } from 'reselect'
|
||||
|
||||
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
|
||||
|
||||
export default createStructuredSelector({
|
||||
provider: providerNameSelector,
|
||||
})
|
|
@ -1,8 +1,14 @@
|
|||
//
|
||||
//
|
||||
import { List } from 'immutable'
|
||||
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
|
||||
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||
import { makeConfirmation } from 'src/logic/safe/store/models/confirmation'
|
||||
|
||||
const makeMockConfirmation = (address) => ({ owner: { address } })
|
||||
const makeMockConfirmation = (address: string): Confirmation => {
|
||||
return makeConfirmation({
|
||||
owner: address
|
||||
})
|
||||
}
|
||||
|
||||
describe('Signatures Blockchain Test', () => {
|
||||
it('generates signatures in natural order even checksumed', async () => {
|
||||
|
|
Loading…
Reference in New Issue