Merge branch 'development' of github.com:gnosis/safe-react into development
This commit is contained in:
commit
0bac3a5aab
|
@ -34,6 +34,7 @@ matrix:
|
|||
- REACT_APP_SENTRY_DSN=${SENTRY_DSN_VOLTA}
|
||||
- SENTRY_PROJECT=${SENTRY_PROJECT_VOLTA}
|
||||
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_VOLTA}
|
||||
if: (branch = master) OR tag IS present
|
||||
- env:
|
||||
- REACT_APP_NETWORK='energy_web_chain'
|
||||
- STAGING_BUCKET_NAME=${STAGING_EWC_BUCKET_NAME}
|
||||
|
@ -43,7 +44,7 @@ matrix:
|
|||
if: (branch = master AND NOT type = pull_request) OR tag IS present
|
||||
cache:
|
||||
npm: false
|
||||
yarn: false
|
||||
yarn: true
|
||||
before_script:
|
||||
- if [[ -n "$TRAVIS_TAG" ]]; then export REACT_APP_ENV='production'; fi;
|
||||
- if [ $TRAVIS_PULL_REQUEST != "false" ]; then export PUBLIC_URL="/${REACT_APP_NETWORK}/app"; fi;
|
||||
|
@ -53,7 +54,6 @@ before_install:
|
|||
- sudo apt-get -y install python3-pip python3-dev libusb-1.0-0-dev libudev-dev
|
||||
- pip install awscli --upgrade --user
|
||||
script:
|
||||
- yarn lint:check
|
||||
- yarn prettier:check
|
||||
- yarn test:coverage
|
||||
- yarn build
|
||||
|
|
|
@ -73,7 +73,7 @@ export enum FEATURES {
|
|||
ERC1155 = 'ERC1155',
|
||||
SAFE_APPS = 'SAFE_APPS',
|
||||
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION',
|
||||
ENS_LOOKUP = 'ENS_LOOKUP'
|
||||
DOMAIN_LOOKUP = 'DOMAIN_LOOKUP'
|
||||
}
|
||||
```
|
||||
|
||||
|
|
36
package.json
36
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "safe-react",
|
||||
"version": "2.18.1",
|
||||
"version": "2.19.2",
|
||||
"description": "Allowing crypto users manage funds in a safer way",
|
||||
"website": "https://github.com/gnosis/safe-react#readme",
|
||||
"bugs": {
|
||||
|
@ -94,7 +94,7 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"files": [
|
||||
"build",
|
||||
"patches",
|
||||
"public",
|
||||
|
@ -158,19 +158,20 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@gnosis.pm/safe-apps-sdk": "1.0.2",
|
||||
"@gnosis.pm/safe-apps-sdk": "1.0.3",
|
||||
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
|
||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#bf3a84486b7353bd25447ddff39c406f6fafecc6",
|
||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#8dea3a6",
|
||||
"@gnosis.pm/util-contracts": "2.0.6",
|
||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.38.0",
|
||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.41.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.11.0",
|
||||
"@material-ui/lab": "4.0.0-alpha.56",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"@openzeppelin/contracts": "3.1.0",
|
||||
"@sentry/react": "^5.28.0",
|
||||
"@sentry/tracing": "^5.28.0",
|
||||
"@sentry/react": "^5.30.0",
|
||||
"@sentry/tracing": "^5.30.0",
|
||||
"@truffle/contract": "^4.3.0",
|
||||
"@unstoppabledomains/resolution": "^1.17.0",
|
||||
"async-sema": "^3.1.0",
|
||||
"axios": "0.21.1",
|
||||
"bignumber.js": "9.0.1",
|
||||
|
@ -198,9 +199,13 @@
|
|||
"immutable": "^4.0.0-rc.12",
|
||||
"js-cookie": "^2.2.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.memoize": "^4.1.2",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"material-ui-search-bar": "^1.0.0",
|
||||
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
|
||||
"object-hash": "^2.1.1",
|
||||
"qrcode.react": "1.0.1",
|
||||
"query-string": "6.13.8",
|
||||
"react": "16.13.1",
|
||||
|
@ -210,6 +215,7 @@
|
|||
"react-final-form-listeners": "^1.0.2",
|
||||
"react-ga": "3.3.0",
|
||||
"react-hot-loader": "4.13.0",
|
||||
"react-infinite-scroll-component": "^5.1.0",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-redux": "7.2.2",
|
||||
"react-router-dom": "5.2.0",
|
||||
|
@ -228,7 +234,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@rescripts/cli": "^0.0.15",
|
||||
"@sentry/cli": "^1.59.0",
|
||||
"@sentry/cli": "^1.62.0",
|
||||
"@storybook/addon-actions": "^5.3.19",
|
||||
"@storybook/addon-links": "^5.3.19",
|
||||
"@storybook/addons": "^5.3.19",
|
||||
|
@ -239,23 +245,25 @@
|
|||
"@typechain/web3-v1": "^2.0.0",
|
||||
"@types/history": "4.6.2",
|
||||
"@types/jest": "^26.0.16",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/lodash.memoize": "^4.1.6",
|
||||
"@types/node": "^14.14.10",
|
||||
"@types/react": "^16.9.55",
|
||||
"@types/react-dom": "^16.9.9",
|
||||
"@types/react-redux": "^7.1.11",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"@types/redux-actions": "^2.6.1",
|
||||
"@types/styled-components": "^5.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
||||
"@typescript-eslint/parser": "^4.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.0",
|
||||
"@typescript-eslint/parser": "^4.14.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
"electron": "^9.4.0",
|
||||
"electron-builder": "22.9.1",
|
||||
"electron-notarize": "1.0.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-prettier": "^7.0.0",
|
||||
"eslint": "^7.17.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
|
@ -266,7 +274,7 @@
|
|||
"patch-package": "^6.2.2",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.2.0",
|
||||
"sass": "^1.29.0",
|
||||
"sass": "^1.32.0",
|
||||
"typechain": "^4.0.0",
|
||||
"typescript": "4.1.3",
|
||||
"wait-on": "5.2.1"
|
||||
|
|
|
@ -50,7 +50,7 @@ const Footer = (): React.ReactElement => {
|
|||
const dispatch = useDispatch()
|
||||
|
||||
const openCookiesHandler = () => {
|
||||
dispatch(openCookieBanner(true))
|
||||
dispatch(openCookieBanner({ cookieBannerOpen: true }))
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M0 0H16V16H0z" transform="translate(-260 -550) translate(260 550)"/>
|
||||
<path fill="#F02525" d="M8 5.996c.553 0 1 .447 1 1v.998c0 .553-.447 1-1 1-.552 0-1-.447-1-1v-.998c0-.553.448-1 1-1M8 9.895c.607 0 1.101.492 1.101 1.1 0 .607-.494 1.1-1.1 1.1-.608 0-1.1-.493-1.1-1.1 0-.608.492-1.1 1.1-1.1" transform="translate(-260 -550) translate(260 550)"/>
|
||||
<path fill="#F02525" d="M8 0c-.383 0-.766.193-.975.581L.133 13.373c-.396.734.138 1.624.974 1.624h13.786c.836 0 1.37-.89.974-1.624L8.974.581C8.766.193 8.384 0 8 0m0 3l5.386 9.997H2.613L8 3" transform="translate(-260 -550) translate(260 550)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 843 B |
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
|
@ -1,10 +1,8 @@
|
|||
import Checkbox from '@material-ui/core/Checkbox'
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import cn from 'classnames'
|
||||
import React, { useEffect, useState, useCallback } from 'react'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import Button from 'src/components/layout/Button'
|
||||
import Link from 'src/components/layout/Link'
|
||||
import { COOKIES_KEY } from 'src/logic/cookies/model/cookie'
|
||||
|
@ -13,7 +11,9 @@ import { cookieBannerOpen } from 'src/logic/cookies/store/selectors'
|
|||
import { loadFromCookie, saveCookie } from 'src/logic/cookies/utils'
|
||||
import { mainFontFamily, md, primary, screenSm } from 'src/theme/variables'
|
||||
import { loadGoogleAnalytics } from 'src/utils/googleAnalytics'
|
||||
import { loadIntercom } from 'src/utils/intercom'
|
||||
import { closeIntercom, isIntercomLoaded, loadIntercom } from 'src/utils/intercom'
|
||||
import AlertRedIcon from './assets/alert-red.svg'
|
||||
import IntercomIcon from './assets/intercom.png'
|
||||
|
||||
const isDesktop = process.env.REACT_APP_BUILD_FOR_DESKTOP
|
||||
|
||||
|
@ -27,14 +27,13 @@ const useStyles = makeStyles({
|
|||
justifyContent: 'center',
|
||||
left: '0',
|
||||
minHeight: '200px',
|
||||
padding: '27px 15px',
|
||||
padding: '30px 15px 45px',
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
zIndex: '15',
|
||||
zIndex: '999',
|
||||
},
|
||||
content: {
|
||||
maxWidth: '100%',
|
||||
width: '830px',
|
||||
},
|
||||
text: {
|
||||
color: primary,
|
||||
|
@ -42,19 +41,21 @@ const useStyles = makeStyles({
|
|||
fontSize: md,
|
||||
fontWeight: 'normal',
|
||||
lineHeight: '1.38',
|
||||
margin: '0 0 25px',
|
||||
margin: '0 auto 35px',
|
||||
textAlign: 'center',
|
||||
maxWidth: '810px',
|
||||
},
|
||||
form: {
|
||||
columnGap: '10px',
|
||||
columnGap: '20px',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr',
|
||||
paddingBottom: '30px',
|
||||
rowGap: '10px',
|
||||
|
||||
paddingBottom: '50px',
|
||||
rowGap: '15px',
|
||||
margin: '0 auto',
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr',
|
||||
paddingBottom: '0',
|
||||
rowGap: '5px',
|
||||
},
|
||||
},
|
||||
formItem: {
|
||||
|
@ -68,139 +69,199 @@ const useStyles = makeStyles({
|
|||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
acceptPreferences: {
|
||||
bottom: '-20px',
|
||||
intercomAlert: {
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
padding: '0 0 13px 0',
|
||||
svg: {
|
||||
marginRight: '5px',
|
||||
},
|
||||
},
|
||||
intercomImage: {
|
||||
position: 'fixed',
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
right: '20px',
|
||||
textDecoration: 'underline',
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
bottom: '-10px',
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
height: '80px',
|
||||
width: '80px',
|
||||
bottom: '8px',
|
||||
right: '10px',
|
||||
zIndex: '1000',
|
||||
boxShadow: '1px 2px 10px 0 var(rgba(40, 54, 61, 0.18))',
|
||||
},
|
||||
} as any)
|
||||
|
||||
const CookiesBanner = () => {
|
||||
interface CookiesBannerFormProps {
|
||||
alertMessage: boolean
|
||||
}
|
||||
|
||||
const CookiesBanner = (): ReactElement => {
|
||||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const [showAnalytics, setShowAnalytics] = useState(false)
|
||||
const [showIntercom, setShowIntercom] = useState(false)
|
||||
const [localNecessary, setLocalNecessary] = useState(true)
|
||||
const [localAnalytics, setLocalAnalytics] = useState(false)
|
||||
const showBanner = useSelector(cookieBannerOpen)
|
||||
const [localIntercom, setLocalIntercom] = useState(false)
|
||||
|
||||
const acceptCookiesHandler = useCallback(async () => {
|
||||
const newState = {
|
||||
acceptedNecessary: true,
|
||||
acceptedAnalytics: !isDesktop,
|
||||
}
|
||||
await saveCookie(COOKIES_KEY, newState, 365)
|
||||
dispatch(openCookieBanner(false))
|
||||
setShowAnalytics(!isDesktop)
|
||||
}, [dispatch])
|
||||
const showBanner = useSelector(cookieBannerOpen)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchCookiesFromStorage() {
|
||||
const cookiesState = await loadFromCookie(COOKIES_KEY)
|
||||
if (cookiesState) {
|
||||
const { acceptedAnalytics, acceptedNecessary } = cookiesState
|
||||
if (!cookiesState) {
|
||||
dispatch(openCookieBanner({ cookieBannerOpen: true }))
|
||||
} else {
|
||||
const { acceptedIntercom, acceptedAnalytics, acceptedNecessary } = cookiesState
|
||||
if (acceptedIntercom === undefined) {
|
||||
const newState = {
|
||||
acceptedNecessary,
|
||||
acceptedAnalytics,
|
||||
acceptedIntercom: acceptedAnalytics,
|
||||
}
|
||||
const expDays = acceptedAnalytics ? 365 : 7
|
||||
await saveCookie(COOKIES_KEY, newState, expDays)
|
||||
setLocalIntercom(newState.acceptedIntercom)
|
||||
setShowIntercom(newState.acceptedIntercom)
|
||||
} else {
|
||||
setLocalIntercom(acceptedIntercom)
|
||||
setShowIntercom(acceptedIntercom)
|
||||
}
|
||||
setLocalAnalytics(acceptedAnalytics)
|
||||
setLocalNecessary(acceptedNecessary)
|
||||
const openBanner = acceptedNecessary === false || showBanner
|
||||
dispatch(openCookieBanner(openBanner))
|
||||
setShowAnalytics(acceptedAnalytics)
|
||||
} else {
|
||||
dispatch(openCookieBanner(true))
|
||||
}
|
||||
}
|
||||
fetchCookiesFromStorage()
|
||||
}, [dispatch, showBanner])
|
||||
}, [showAnalytics, showIntercom])
|
||||
|
||||
useEffect(() => {
|
||||
if (isDesktop && showBanner) acceptCookiesHandler()
|
||||
}, [acceptCookiesHandler, showBanner])
|
||||
const acceptCookiesHandler = async () => {
|
||||
const newState = {
|
||||
acceptedNecessary: true,
|
||||
acceptedAnalytics: !isDesktop,
|
||||
acceptedIntercom: true,
|
||||
}
|
||||
await saveCookie(COOKIES_KEY, newState, 365)
|
||||
setShowAnalytics(!isDesktop)
|
||||
setShowIntercom(true)
|
||||
dispatch(openCookieBanner({ cookieBannerOpen: false }))
|
||||
}
|
||||
|
||||
const closeCookiesBannerHandler = async () => {
|
||||
const newState = {
|
||||
acceptedNecessary: true,
|
||||
acceptedAnalytics: localAnalytics,
|
||||
acceptedIntercom: localIntercom,
|
||||
}
|
||||
const expDays = localAnalytics ? 365 : 7
|
||||
await saveCookie(COOKIES_KEY, newState, expDays)
|
||||
setShowAnalytics(localAnalytics)
|
||||
dispatch(openCookieBanner(false))
|
||||
setShowIntercom(localIntercom)
|
||||
if (!localIntercom && isIntercomLoaded()) {
|
||||
closeIntercom()
|
||||
}
|
||||
dispatch(openCookieBanner({ cookieBannerOpen: false }))
|
||||
}
|
||||
|
||||
const cookieBannerContent = (
|
||||
<div className={classes.container}>
|
||||
<span
|
||||
className={cn(classes.acceptPreferences, classes.text)}
|
||||
onClick={closeCookiesBannerHandler}
|
||||
onKeyDown={closeCookiesBannerHandler}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
data-testid="accept-preferences"
|
||||
>
|
||||
Accept preferences >
|
||||
</span>
|
||||
<div className={classes.content}>
|
||||
<p className={classes.text}>
|
||||
We use cookies to give you the best experience and to help improve our website. Please read our{' '}
|
||||
<Link className={classes.link} to="https://gnosis-safe.io/cookie">
|
||||
Cookie Policy
|
||||
</Link>{' '}
|
||||
for more information. By clicking "Accept all", you agree to the storing of cookies on your device
|
||||
to enhance site navigation, analyze site usage and provide customer support.
|
||||
</p>
|
||||
<div className={classes.form}>
|
||||
<div className={classes.formItem}>
|
||||
<FormControlLabel
|
||||
checked={localNecessary}
|
||||
control={<Checkbox disabled />}
|
||||
disabled
|
||||
label="Necessary"
|
||||
name="Necessary"
|
||||
onChange={() => setLocalNecessary((prev) => !prev)}
|
||||
value={localNecessary}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formItem}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={localAnalytics} />}
|
||||
label="Analytics"
|
||||
name="Analytics"
|
||||
onChange={() => setLocalAnalytics((prev) => !prev)}
|
||||
value={localAnalytics}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formItem}>
|
||||
<Button
|
||||
color="primary"
|
||||
component={Link}
|
||||
minWidth={180}
|
||||
onClick={() => acceptCookiesHandler()}
|
||||
variant="outlined"
|
||||
>
|
||||
Accept All
|
||||
</Button>
|
||||
if (showAnalytics && !isDesktop) {
|
||||
loadGoogleAnalytics()
|
||||
}
|
||||
|
||||
if (showIntercom) {
|
||||
loadIntercom()
|
||||
}
|
||||
|
||||
const CookiesBannerForm = (props: CookiesBannerFormProps) => {
|
||||
const { alertMessage } = props
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<div className={classes.content}>
|
||||
{alertMessage && (
|
||||
<div className={classes.intercomAlert}>
|
||||
<img src={AlertRedIcon} />
|
||||
You attempted to open the customer support chat. Please accept the customer support cookie.
|
||||
</div>
|
||||
)}
|
||||
<p className={classes.text}>
|
||||
We use cookies to provide you with the best experience and to help improve our website and application.
|
||||
Please read our{' '}
|
||||
<Link className={classes.link} to="https://gnosis-safe.io/cookie">
|
||||
Cookie Policy
|
||||
</Link>{' '}
|
||||
for more information. By clicking "Accept all", you agree to the storing of cookies on your device
|
||||
to enhance site navigation, analyze site usage and provide customer support.
|
||||
</p>
|
||||
<div className={classes.form}>
|
||||
<div className={classes.formItem}>
|
||||
<FormControlLabel
|
||||
checked={localNecessary}
|
||||
control={<Checkbox disabled />}
|
||||
disabled
|
||||
label="Necessary"
|
||||
name="Necessary"
|
||||
onChange={() => setLocalNecessary((prev) => !prev)}
|
||||
value={localNecessary}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formItem}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={localIntercom} />}
|
||||
label="Customer support"
|
||||
name="Customer support"
|
||||
onChange={() => setLocalIntercom((prev) => !prev)}
|
||||
value={localIntercom}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formItem}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={localAnalytics} />}
|
||||
label="Analytics"
|
||||
name="Analytics"
|
||||
onChange={() => setLocalAnalytics((prev) => !prev)}
|
||||
value={localAnalytics}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.formItem}>
|
||||
<Button
|
||||
color="primary"
|
||||
component={Link}
|
||||
minWidth={180}
|
||||
onClick={() => closeCookiesBannerHandler()}
|
||||
variant="outlined"
|
||||
>
|
||||
Accept selection
|
||||
</Button>
|
||||
</div>
|
||||
<div className={classes.formItem}>
|
||||
<Button
|
||||
color="primary"
|
||||
component={Link}
|
||||
minWidth={180}
|
||||
onClick={() => acceptCookiesHandler()}
|
||||
variant="contained"
|
||||
>
|
||||
Accept all
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (showAnalytics) {
|
||||
loadIntercom()
|
||||
loadGoogleAnalytics()
|
||||
)
|
||||
}
|
||||
if (isDesktop) loadIntercom()
|
||||
|
||||
return showBanner && !isDesktop ? cookieBannerContent : null
|
||||
return (
|
||||
<>
|
||||
{!isDesktop && !showIntercom && (
|
||||
<img
|
||||
className={classes.intercomImage}
|
||||
src={IntercomIcon}
|
||||
onClick={() => dispatch(openCookieBanner({ cookieBannerOpen: true, intercomAlertDisplayed: true }))}
|
||||
/>
|
||||
)}
|
||||
{!isDesktop && showBanner?.cookieBannerOpen && (
|
||||
<CookiesBannerForm alertMessage={showBanner?.intercomAlertDisplayed} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CookiesBanner
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
|
@ -8,16 +9,13 @@ const Wrapper = styled.div`
|
|||
const Icon = styled.img`
|
||||
max-width: 15px;
|
||||
max-height: 15px;
|
||||
`
|
||||
const Text = styled.span`
|
||||
margin-left: 5px;
|
||||
height: 17px;
|
||||
margin-right: 9px;
|
||||
`
|
||||
|
||||
const CustomIconText = ({ iconUrl, text }: { iconUrl: string; text?: string }) => (
|
||||
<Wrapper>
|
||||
<Icon alt={text} src={iconUrl} />
|
||||
{text && <Text>{text}</Text>}
|
||||
{text && <Text size="xl">{text}</Text>}
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { Loader } from '@gnosis.pm/safe-react-components'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { default as ReactInfiniteScroll, Props as ReactInfiniteScrollProps } from 'react-infinite-scroll-component'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { Overwrite } from 'src/types/helpers'
|
||||
|
||||
export const Centered = styled.div<{ padding?: number }>`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: ${({ padding }) => `${padding}px`};
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export const SCROLLABLE_TARGET_ID = 'scrollableDiv'
|
||||
|
||||
type InfiniteScrollProps = Overwrite<ReactInfiniteScrollProps, { loader?: ReactInfiniteScrollProps['loader'] }>
|
||||
|
||||
export const InfiniteScroll = ({ dataLength, next, hasMore, ...props }: InfiniteScrollProps): ReactElement => {
|
||||
return (
|
||||
<ReactInfiniteScroll
|
||||
style={{ overflow: 'hidden' }}
|
||||
dataLength={dataLength}
|
||||
next={next}
|
||||
hasMore={hasMore}
|
||||
loader={
|
||||
<Centered>
|
||||
<Loader size="md" />
|
||||
</Centered>
|
||||
}
|
||||
scrollThreshold="120px"
|
||||
scrollableTarget={SCROLLABLE_TARGET_ID}
|
||||
>
|
||||
{props.children}
|
||||
</ReactInfiniteScroll>
|
||||
)
|
||||
}
|
|
@ -9,15 +9,14 @@ const useStyles = makeStyles(
|
|||
createStyles({
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
display: 'flex',
|
||||
overflowY: 'scroll',
|
||||
},
|
||||
paper: {
|
||||
position: 'absolute',
|
||||
top: '120px',
|
||||
position: 'relative',
|
||||
top: '68px',
|
||||
width: '500px',
|
||||
height: '580px',
|
||||
borderRadius: sm,
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)',
|
||||
|
@ -62,7 +61,7 @@ const GnoModal = ({
|
|||
onClose={handleClose}
|
||||
open={open}
|
||||
>
|
||||
<div className={cn(classes.paper, paperClassName)}>{children}</div>
|
||||
<div className={cn(classes.paper, paperClassName, 'classpep')}>{children}</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { lg } from 'src/theme/variables'
|
||||
import { md, lg } from 'src/theme/variables'
|
||||
import Row from 'src/components/layout/Row'
|
||||
|
||||
const StyledParagraph = styled(Paragraph)`
|
||||
&& {
|
||||
|
@ -18,14 +21,39 @@ const TitleWrapper = styled.div`
|
|||
align-items: center;
|
||||
`
|
||||
|
||||
const ModalTitle = ({ iconUrl, title }: { title: string; iconUrl: string }) => {
|
||||
const StyledRow = styled(Row)`
|
||||
padding: ${md} ${lg};
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
max-height: 75px;
|
||||
`
|
||||
|
||||
const StyledClose = styled(Close)`
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
`
|
||||
|
||||
const ModalTitle = ({
|
||||
iconUrl,
|
||||
title,
|
||||
onClose,
|
||||
}: {
|
||||
title: string
|
||||
iconUrl: string
|
||||
onClose?: () => void
|
||||
}): React.ReactElement => {
|
||||
return (
|
||||
<TitleWrapper>
|
||||
{iconUrl && <IconImg alt={title} src={iconUrl} />}
|
||||
<StyledParagraph noMargin weight="bolder">
|
||||
{title}
|
||||
</StyledParagraph>
|
||||
</TitleWrapper>
|
||||
<StyledRow align="center" grow>
|
||||
<TitleWrapper>
|
||||
{iconUrl && <IconImg alt={title} src={iconUrl} />}
|
||||
<StyledParagraph noMargin weight="bolder">
|
||||
{title}
|
||||
</StyledParagraph>
|
||||
</TitleWrapper>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<StyledClose />
|
||||
</IconButton>
|
||||
</StyledRow>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,10 +3,7 @@ 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'
|
||||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
|
||||
type TransactionFailTextProps = {
|
||||
txEstimationExecutionStatus: EstimationStatus
|
||||
|
@ -24,9 +21,10 @@ export const TransactionFees = ({
|
|||
isOffChainSignature,
|
||||
txEstimationExecutionStatus,
|
||||
}: TransactionFailTextProps): React.ReactElement | null => {
|
||||
const providerName = useSelector(providerNameSelector)
|
||||
|
||||
let transactionAction
|
||||
if (txEstimationExecutionStatus === EstimationStatus.LOADING) {
|
||||
return null
|
||||
}
|
||||
if (isCreation) {
|
||||
transactionAction = 'create'
|
||||
} else if (isExecution) {
|
||||
|
@ -35,18 +33,20 @@ export const TransactionFees = ({
|
|||
transactionAction = 'approve'
|
||||
}
|
||||
|
||||
// FIXME this should be removed when estimating with WalletConnect correctly
|
||||
if (!providerName || sameString(providerName, WALLETS.WALLET_CONNECT)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph>
|
||||
<Paragraph size="lg" align="center">
|
||||
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.`}
|
||||
wallet.{' '}
|
||||
{!isOffChainSignature && (
|
||||
<>
|
||||
Make sure you have{' '}
|
||||
<Text size="lg" as="span" color="text" strong>
|
||||
{gasCostFormatted}
|
||||
</Text>{' '}
|
||||
(fee price) {nativeCoin.name} in this wallet to fund this confirmation.
|
||||
</>
|
||||
)}
|
||||
</Paragraph>
|
||||
<TransactionFailText txEstimationExecutionStatus={txEstimationExecutionStatus} isExecution={isExecution} />
|
||||
</>
|
||||
|
|
|
@ -5,8 +5,8 @@ import { OnChange } from 'react-final-form-listeners'
|
|||
import TextField from 'src/components/forms/TextField'
|
||||
import { Validator, composeValidators, mustBeEthereumAddress, required } from 'src/components/forms/validator'
|
||||
import { trimSpaces } from 'src/utils/strings'
|
||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||
import { getAddressFromDomain } from 'src/logic/wallets/getWeb3'
|
||||
import { isValidEnsName, isValidCryptoDomainName } from 'src/logic/wallets/ethAddresses'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
|
||||
// an idea for second field was taken from here
|
||||
|
@ -54,9 +54,9 @@ const AddressInput = ({
|
|||
<OnChange name={name}>
|
||||
{async (value) => {
|
||||
const address = trimSpaces(value)
|
||||
if (isValidEnsName(address)) {
|
||||
if (isValidEnsName(address) || isValidCryptoDomainName(address)) {
|
||||
try {
|
||||
const resolverAddr = await getAddressFromENS(address)
|
||||
const resolverAddr = await getAddressFromDomain(address)
|
||||
const formattedAddress = checksumAddress(resolverAddr)
|
||||
fieldMutator(formattedAddress)
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import MuiTextField from '@material-ui/core/TextField'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import { lg } from 'src/theme/variables'
|
||||
|
@ -10,65 +10,93 @@ const overflowStyle = {
|
|||
width: '100%',
|
||||
}
|
||||
|
||||
const styles = () => ({
|
||||
root: {
|
||||
paddingTop: lg,
|
||||
paddingBottom: '12px',
|
||||
lineHeight: 0,
|
||||
},
|
||||
})
|
||||
const styles = () =>
|
||||
createStyles({
|
||||
root: {
|
||||
paddingTop: lg,
|
||||
paddingBottom: '12px',
|
||||
lineHeight: 0,
|
||||
},
|
||||
})
|
||||
|
||||
class TextField extends React.PureComponent<any> {
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
input: { name, onChange, value, ...restInput },
|
||||
inputAdornment,
|
||||
meta,
|
||||
multiline,
|
||||
rows,
|
||||
testId,
|
||||
text,
|
||||
...rest
|
||||
} = this.props
|
||||
const helperText = value ? text : undefined
|
||||
const showError = (meta.touched || !meta.pristine) && !meta.valid
|
||||
const hasError = !!meta.error || (!meta.modifiedSinceLastSubmit && !!meta.submitError)
|
||||
const errorMessage = meta.error || meta.submitError
|
||||
const isInactiveAndPristineOrUntouched = !meta.active && (meta.pristine || !meta.touched)
|
||||
const isInvalidAndUntouched = typeof meta.error === 'undefined' ? true : !meta.touched
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const disableUnderline = isInactiveAndPristineOrUntouched && isInvalidAndUntouched
|
||||
|
||||
const inputRoot = helperText ? classes.root : ''
|
||||
const statusClasses = meta.valid ? 'isValid' : hasError && showError ? 'isInvalid' : ''
|
||||
const inputProps = {
|
||||
...restInput,
|
||||
autoComplete: 'off',
|
||||
'data-testid': testId,
|
||||
}
|
||||
const inputRootProps = {
|
||||
...inputAdornment,
|
||||
className: `${inputRoot} ${statusClasses}`,
|
||||
disableUnderline: disableUnderline,
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiTextField
|
||||
error={hasError && showError}
|
||||
helperText={hasError && showError ? errorMessage : helperText || ' '}
|
||||
inputProps={inputProps} // blank in order to force to have helper text
|
||||
InputProps={inputRootProps}
|
||||
multiline={multiline}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
rows={rows}
|
||||
style={overflowStyle}
|
||||
value={value}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
type Props = {
|
||||
input: {
|
||||
name: string
|
||||
onChange?: () => void
|
||||
value: string
|
||||
placeholder: string
|
||||
type: string
|
||||
}
|
||||
meta: {
|
||||
touched?: boolean
|
||||
pristine?: boolean
|
||||
valid?: boolean
|
||||
error?: string
|
||||
modifiedSinceLastSubmit?: boolean
|
||||
submitError?: boolean
|
||||
active?: boolean
|
||||
}
|
||||
inputAdornment?: { endAdornment: React.ReactElement } | undefined
|
||||
multiline: boolean
|
||||
rows?: string
|
||||
testId: string
|
||||
text: string
|
||||
disabled?: boolean
|
||||
rowsMax?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(TextField)
|
||||
const TextField = (props: Props): React.ReactElement => {
|
||||
const {
|
||||
input: { name, onChange, value, ...restInput },
|
||||
inputAdornment,
|
||||
meta,
|
||||
multiline,
|
||||
rows,
|
||||
testId,
|
||||
text,
|
||||
...rest
|
||||
} = props
|
||||
const classes = useStyles()
|
||||
const helperText = value ? text : undefined
|
||||
const showError = (meta.touched || !meta.pristine) && !meta.valid
|
||||
const hasError = !!meta.error || (!meta.modifiedSinceLastSubmit && !!meta.submitError)
|
||||
const errorMessage = meta.error || meta.submitError
|
||||
const isInactiveAndPristineOrUntouched = !meta.active && (meta.pristine || !meta.touched)
|
||||
const isInvalidAndUntouched = typeof meta.error === 'undefined' ? true : !meta.touched
|
||||
|
||||
const disableUnderline = isInactiveAndPristineOrUntouched && isInvalidAndUntouched
|
||||
|
||||
const inputRoot = helperText ? classes.root : ''
|
||||
const statusClasses = meta.valid ? 'isValid' : hasError && showError ? 'isInvalid' : ''
|
||||
const inputProps = {
|
||||
...restInput,
|
||||
autoComplete: 'off',
|
||||
'data-testid': testId,
|
||||
}
|
||||
const inputRootProps = {
|
||||
...inputAdornment,
|
||||
className: `${inputRoot} ${statusClasses}`,
|
||||
disableUnderline: disableUnderline,
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiTextField
|
||||
error={hasError && showError}
|
||||
helperText={hasError && showError ? errorMessage : helperText || ' '}
|
||||
inputProps={inputProps} // blank in order to force to have helper text
|
||||
InputProps={inputRootProps}
|
||||
multiline={multiline}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
rows={rows}
|
||||
style={overflowStyle}
|
||||
value={value}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextField
|
||||
|
|
|
@ -128,7 +128,7 @@ describe('Forms > Validators', () => {
|
|||
})
|
||||
|
||||
describe('mustBeEthereumAddress validator', () => {
|
||||
const MUST_BE_ETH_ADDRESS_ERR_MSG = 'Address should be a valid Ethereum address or ENS name'
|
||||
const MUST_BE_ETH_ADDRESS_ERR_MSG = 'Input must be a valid Ethereum address, ENS or Unstoppable domain'
|
||||
|
||||
it('Returns undefined for a valid ethereum address', async () => {
|
||||
expect(await mustBeEthereumAddress('0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe')).toBeUndefined()
|
||||
|
|
|
@ -42,6 +42,10 @@ export const mustBeUrl = (value: string): ValidatorReturnType => {
|
|||
}
|
||||
|
||||
export const minValue = (min: number | string, inclusive = true) => (value: string): ValidatorReturnType => {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (Number.parseFloat(value) > Number(min) || (inclusive && Number.parseFloat(value) >= Number(min))) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -64,8 +68,8 @@ export const mustBeEthereumAddress = memoize(
|
|||
const startsWith0x = address?.startsWith('0x')
|
||||
const isAddress = getWeb3().utils.isAddress(address)
|
||||
|
||||
const errorMessage = `Address should be a valid Ethereum address${
|
||||
isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : ''
|
||||
const errorMessage = `Input must be a valid Ethereum address${
|
||||
isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : ''
|
||||
}`
|
||||
|
||||
return startsWith0x && isAddress ? undefined : errorMessage
|
||||
|
@ -76,8 +80,8 @@ export const mustBeEthereumContractAddress = memoize(
|
|||
async (address: string): Promise<ValidatorReturnType> => {
|
||||
const contractCode = await getWeb3().eth.getCode(address)
|
||||
|
||||
const errorMessage = `Address should be a valid Ethereum contract address${
|
||||
isFeatureEnabled(FEATURES.ENS_LOOKUP) ? ' or ENS name' : ''
|
||||
const errorMessage = `Input must be a valid Ethereum contract address${
|
||||
isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : ''
|
||||
}`
|
||||
|
||||
return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' ? errorMessage : undefined
|
||||
|
|
|
@ -1,23 +1,34 @@
|
|||
import classNames from 'classnames/bind'
|
||||
import * as React from 'react'
|
||||
import React, { MouseEventHandler, CSSProperties, ReactElement, ReactNode } from 'react'
|
||||
|
||||
import styles from './index.module.scss'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
class Paragraph extends React.PureComponent<any> {
|
||||
render() {
|
||||
const { align, children, className, color, dot, noMargin, size, transform, weight, ...props } = this.props
|
||||
interface Props {
|
||||
align?: string
|
||||
children: ReactNode
|
||||
className?: string
|
||||
color?: string
|
||||
dot?: string
|
||||
noMargin?: boolean
|
||||
size?: string
|
||||
transform?: string
|
||||
weight?: string
|
||||
onClick?: MouseEventHandler<HTMLParagraphElement>
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
className={cx(styles.paragraph, className, weight, { noMargin, dot }, size, color, transform, align)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
const Paragraph = (props: Props): ReactElement => {
|
||||
const { align, children, className, color, dot, noMargin, size, transform, weight, ...restProps } = props
|
||||
return (
|
||||
<p
|
||||
className={cx(styles.paragraph, className, weight, { noMargin, dot }, size, color, transform, align)}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export default Paragraph
|
||||
|
|
|
@ -17,6 +17,8 @@ export const getNetworkId = (): ETHEREUM_NETWORK => ETHEREUM_NETWORK[NETWORK]
|
|||
|
||||
export const getNetworkName = (): string => ETHEREUM_NETWORK[getNetworkId()]
|
||||
|
||||
export const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId())
|
||||
|
||||
const getCurrentEnvironment = (): string => {
|
||||
switch (NODE_ENV) {
|
||||
case 'test': {
|
||||
|
@ -66,6 +68,8 @@ const configuration = (): NetworkSpecificConfiguration => {
|
|||
|
||||
const getConfig: () => NetworkSpecificConfiguration = ensureOnce(configuration)
|
||||
|
||||
export const getClientGatewayUrl = (): string => getConfig().clientGatewayUrl
|
||||
|
||||
export const getTxServiceUrl = (): string => getConfig().txServiceUrl
|
||||
|
||||
export const getRelayUrl = (): string | undefined => getConfig().relayApiUrl
|
||||
|
@ -76,15 +80,13 @@ export const getGasPrice = (): number | undefined => getConfig()?.gasPrice
|
|||
|
||||
export const getGasPriceOracle = (): GasPriceOracle | undefined => getConfig()?.gasPriceOracle
|
||||
|
||||
export const getRpcServiceUrl = (): string => {
|
||||
const usesInfuraRPC = [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY].includes(getNetworkId())
|
||||
export const getRpcServiceUrl = (): string =>
|
||||
usesInfuraRPC ? `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}` : getConfig().rpcServiceUrl
|
||||
|
||||
if (usesInfuraRPC) {
|
||||
return `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}`
|
||||
}
|
||||
export const getSafeClientGatewayBaseUrl = (safeAddress: string) => `${getClientGatewayUrl()}/safes/${safeAddress}`
|
||||
|
||||
return getConfig().rpcServiceUrl
|
||||
}
|
||||
export const getTxDetailsUrl = (clientGatewayTxId: string) =>
|
||||
`${getClientGatewayUrl()}/transactions/${clientGatewayTxId}`
|
||||
|
||||
export const getSafeServiceBaseUrl = (safeAddress: string) => `${getTxServiceUrl()}/safes/${safeAddress}`
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 's
|
|||
// @todo (agustin) we need to use fixed gasPrice because the oracle is not working right now and it's returning 0
|
||||
// once the oracle is fixed we need to remove the fixed value
|
||||
const baseConfig: EnvironmentSettings = {
|
||||
clientGatewayUrl: 'https://safe-client.ewc.gnosis.io/v1',
|
||||
txServiceUrl: 'https://safe-transaction.ewc.gnosis.io/api/v1',
|
||||
safeAppsUrl: 'https://safe-apps-ewc.staging.gnosisdev.com',
|
||||
gasPriceOracle: {
|
||||
|
|
|
@ -2,6 +2,7 @@ import EtherLogo from 'src/config/assets/token_eth.svg'
|
|||
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
|
||||
|
||||
const baseConfig: EnvironmentSettings = {
|
||||
clientGatewayUrl: 'http://localhost:8001/v1',
|
||||
txServiceUrl: 'http://localhost:8000/api/v1',
|
||||
relayApiUrl: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
||||
safeAppsUrl: 'http://localhost:3002',
|
||||
|
|
|
@ -2,6 +2,7 @@ import EtherLogo from 'src/config/assets/token_eth.svg'
|
|||
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
|
||||
|
||||
const baseConfig: EnvironmentSettings = {
|
||||
clientGatewayUrl: 'https://safe-client.mainnet.staging.gnosisdev.com/v1',
|
||||
txServiceUrl: 'https://safe-transaction.mainnet.staging.gnosisdev.com/api/v1',
|
||||
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
|
||||
gasPriceOracle: {
|
||||
|
@ -25,6 +26,7 @@ const mainnet: NetworkConfig = {
|
|||
},
|
||||
production: {
|
||||
...baseConfig,
|
||||
clientGatewayUrl: 'https://safe-client.mainnet.gnosis.io/v1',
|
||||
txServiceUrl: 'https://safe-transaction.mainnet.gnosis.io/api/v1',
|
||||
safeAppsUrl: 'https://apps.gnosis-safe.io',
|
||||
},
|
||||
|
|
|
@ -24,7 +24,7 @@ export enum FEATURES {
|
|||
ERC1155 = 'ERC1155',
|
||||
SAFE_APPS = 'SAFE_APPS',
|
||||
CONTRACT_INTERACTION = 'CONTRACT_INTERACTION',
|
||||
ENS_LOOKUP = 'ENS_LOOKUP',
|
||||
DOMAIN_LOOKUP = 'DOMAIN_LOOKUP',
|
||||
}
|
||||
|
||||
type Token = {
|
||||
|
@ -85,8 +85,9 @@ type GasPrice =
|
|||
}
|
||||
|
||||
export type EnvironmentSettings = GasPrice & {
|
||||
clientGatewayUrl: string
|
||||
txServiceUrl: string
|
||||
// Shall we keep a reference to the relay?
|
||||
// TODO: Shall we keep a reference to the relay?
|
||||
relayApiUrl?: string
|
||||
safeAppsUrl: string
|
||||
rpcServiceUrl: string
|
||||
|
|
|
@ -2,6 +2,7 @@ import EtherLogo from 'src/config/assets/token_eth.svg'
|
|||
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig } from 'src/config/networks/network.d'
|
||||
|
||||
const baseConfig: EnvironmentSettings = {
|
||||
clientGatewayUrl: 'https://safe-client.rinkeby.staging.gnosisdev.com/v1',
|
||||
txServiceUrl: 'https://safe-transaction.staging.gnosisdev.com/api/v1',
|
||||
safeAppsUrl: 'https://safe-apps.dev.gnosisdev.com',
|
||||
gasPriceOracle: {
|
||||
|
@ -25,6 +26,7 @@ const rinkeby: NetworkConfig = {
|
|||
},
|
||||
production: {
|
||||
...baseConfig,
|
||||
clientGatewayUrl: 'https://safe-client.rinkeby.gnosis.io/v1',
|
||||
txServiceUrl: 'https://safe-transaction.rinkeby.gnosis.io/api/v1',
|
||||
safeAppsUrl: 'https://apps.gnosis-safe.io',
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@ import EwcLogo from 'src/config/assets/token_ewc.svg'
|
|||
import { EnvironmentSettings, ETHEREUM_NETWORK, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
|
||||
|
||||
const baseConfig: EnvironmentSettings = {
|
||||
clientGatewayUrl: 'https://safe-client.volta.gnosis.io/v1',
|
||||
txServiceUrl: 'https://safe-transaction.volta.gnosis.io/api/v1',
|
||||
safeAppsUrl: 'https://safe-apps-volta.staging.gnosisdev.com',
|
||||
gasPriceOracle: {
|
||||
|
|
|
@ -2,6 +2,7 @@ import xDaiLogo from 'src/config/assets/token_xdai.svg'
|
|||
import { EnvironmentSettings, ETHEREUM_NETWORK, FEATURES, NetworkConfig, WALLETS } from 'src/config/networks/network.d'
|
||||
|
||||
const baseConfig: EnvironmentSettings = {
|
||||
clientGatewayUrl: 'https://safe-client.xdai.gnosis.io/v1',
|
||||
txServiceUrl: 'https://safe-transaction.xdai.gnosis.io/api/v1',
|
||||
safeAppsUrl: 'https://safe-apps-xdai.staging.gnosisdev.com',
|
||||
gasPrice: 1e9,
|
||||
|
@ -51,7 +52,7 @@ const xDai: NetworkConfig = {
|
|||
WALLETS.AUTHEREUM,
|
||||
WALLETS.LATTICE,
|
||||
],
|
||||
disabledFeatures: [FEATURES.ENS_LOOKUP],
|
||||
disabledFeatures: [FEATURES.DOMAIN_LOOKUP],
|
||||
}
|
||||
|
||||
export default xDai
|
||||
|
|
|
@ -26,7 +26,7 @@ Sentry.init({
|
|||
dsn: SENTRY_DSN,
|
||||
release: `safe-react@${process.env.REACT_APP_APP_VERSION}`,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
sampleRate: 0.2,
|
||||
sampleRate: 0.01,
|
||||
})
|
||||
|
||||
const root = document.getElementById('root')
|
||||
|
|
|
@ -9,7 +9,7 @@ type addAddressBookEntryOptions = {
|
|||
|
||||
export const addAddressBookEntry = createAction(
|
||||
ADD_ENTRY,
|
||||
(entry: AddressBookEntry, options: addAddressBookEntryOptions) => {
|
||||
(entry: AddressBookEntry, options?: addAddressBookEntryOptions) => {
|
||||
let notifyEntryUpdate = true
|
||||
if (options) {
|
||||
notifyEntryUpdate = options.notifyEntryUpdate
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { handleActions } from 'redux-actions'
|
||||
import { Action, handleActions } from 'redux-actions'
|
||||
|
||||
import { AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
|
||||
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
|
||||
import { LOAD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/loadAddressBook'
|
||||
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
|
||||
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
|
||||
|
||||
|
@ -18,13 +19,19 @@ export const buildAddressBook = (storedAddressBook: AddressBookState): AddressBo
|
|||
})
|
||||
}
|
||||
|
||||
export default handleActions(
|
||||
type AddressBookPayload = { addressBook: AddressBookState }
|
||||
type EntryPayload = { entry: AddressBookEntry }
|
||||
type RemoveEntryPayload = { entryAddress: string }
|
||||
|
||||
type Payloads = AddressBookPayload | EntryPayload | RemoveEntryPayload
|
||||
|
||||
export default handleActions<AppReduxState['addressBook'], Payloads>(
|
||||
{
|
||||
[LOAD_ADDRESS_BOOK]: (state, action) => {
|
||||
[LOAD_ADDRESS_BOOK]: (state, action: Action<AddressBookPayload>) => {
|
||||
const { addressBook } = action.payload
|
||||
return addressBook
|
||||
},
|
||||
[ADD_ENTRY]: (state, action) => {
|
||||
[ADD_ENTRY]: (state, action: Action<EntryPayload>) => {
|
||||
const { entry } = action.payload
|
||||
|
||||
const entryFound = state.find((oldEntry) => oldEntry.address === entry.address)
|
||||
|
@ -34,7 +41,7 @@ export default handleActions(
|
|||
}
|
||||
return state
|
||||
},
|
||||
[UPDATE_ENTRY]: (state, action) => {
|
||||
[UPDATE_ENTRY]: (state, action: Action<EntryPayload>) => {
|
||||
const { entry } = action.payload
|
||||
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address)
|
||||
if (entryIndex >= 0) {
|
||||
|
@ -42,13 +49,13 @@ export default handleActions(
|
|||
}
|
||||
return state
|
||||
},
|
||||
[REMOVE_ENTRY]: (state, action) => {
|
||||
[REMOVE_ENTRY]: (state, action: Action<RemoveEntryPayload>) => {
|
||||
const { entryAddress } = action.payload
|
||||
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress)
|
||||
state.splice(entryIndex, 1)
|
||||
return state
|
||||
},
|
||||
[ADD_OR_UPDATE_ENTRY]: (state, action) => {
|
||||
[ADD_OR_UPDATE_ENTRY]: (state, action: Action<EntryPayload>) => {
|
||||
const { entry } = action.payload
|
||||
|
||||
// Only updates entries with valid names
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { ADD_NFT_ASSETS, ADD_NFT_TOKENS } from 'src/logic/collectibles/store/actions/addCollectibles'
|
||||
import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const NFT_ASSETS_REDUCER_ID = 'nftAssets'
|
||||
export const NFT_TOKENS_REDUCER_ID = 'nftTokens'
|
||||
|
||||
export const nftAssetReducer = handleActions(
|
||||
type NFTAssetsPayload = { nftAssets: NFTAssets }
|
||||
|
||||
export const nftAssetReducer = handleActions<AppReduxState['nftAssets'], NFTAssetsPayload>(
|
||||
{
|
||||
[ADD_NFT_ASSETS]: (state, action) => {
|
||||
const { nftAssets } = action.payload
|
||||
|
@ -16,7 +20,9 @@ export const nftAssetReducer = handleActions(
|
|||
{},
|
||||
)
|
||||
|
||||
export const nftTokensReducer = handleActions(
|
||||
type NFTTokensPayload = { nftTokens: NFTTokens }
|
||||
|
||||
export const nftTokensReducer = handleActions<AppReduxState['nftTokens'], NFTTokensPayload>(
|
||||
{
|
||||
[ADD_NFT_TOKENS]: (state, action) => {
|
||||
const { nftTokens } = action.payload
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getNetworkId, getNetworkInfo } from 'src/config'
|
||||
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
|
||||
import { nftAssetsListAddressesSelector } from 'src/logic/collectibles/store/selectors'
|
||||
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||
import { BuildTx, ServiceTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { TOKEN_TRANSFER_METHODS_NAMES } from 'src/logic/safe/store/models/types/transactions.d'
|
||||
import { getERC721TokenContract, getStandardTokenContract } from 'src/logic/tokens/store/actions/fetchTokens'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
|
@ -31,10 +31,10 @@ export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
|
|||
|
||||
/**
|
||||
* Verifies that a tx received by the transaction service is an ERC721 token-related transaction
|
||||
* @param {TxServiceModel} tx
|
||||
* @param {BuildTx['tx']} tx
|
||||
* @returns boolean
|
||||
*/
|
||||
export const isSendERC721Transaction = (tx: TxServiceModel): boolean => {
|
||||
export const isSendERC721Transaction = (tx: BuildTx['tx']): boolean => {
|
||||
let hasERC721Transfer = false
|
||||
|
||||
if (tx.dataDecoded && sameString(tx.dataDecoded.method, TOKEN_TRANSFER_METHODS_NAMES.SAFE_TRANSFER_FROM)) {
|
||||
|
@ -44,7 +44,7 @@ export const isSendERC721Transaction = (tx: TxServiceModel): boolean => {
|
|||
// Note: this is only valid with our current case (client rendering), if we move to server side rendering we need to refactor this
|
||||
const state = store.getState()
|
||||
const knownAssets = nftAssetsListAddressesSelector(state)
|
||||
return knownAssets.includes(tx.to) || hasERC721Transfer
|
||||
return knownAssets.includes((tx as ServiceTx).to) || hasERC721Transfer
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,7 +25,7 @@ const decodeInfo = ({ paramsHash, params }: DecodeInfoProps): DataDecoded['param
|
|||
}))
|
||||
}
|
||||
|
||||
export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => {
|
||||
export const decodeParamsFromSafeMethod = (data: string): DataDecoded | undefined => {
|
||||
const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)]
|
||||
const method = SAFE_METHODS_NAMES[methodId]
|
||||
|
||||
|
@ -99,7 +99,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
|
|||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,7 +113,7 @@ export const isDeleteAllowanceMethod = (data: string): boolean => {
|
|||
return sameString(SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId], SPENDING_LIMIT_METHODS_NAMES.DELETE_ALLOWANCE)
|
||||
}
|
||||
|
||||
export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | null => {
|
||||
export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | undefined => {
|
||||
const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)]
|
||||
const method = SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId]
|
||||
|
||||
|
@ -171,7 +171,7 @@ export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | null
|
|||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,9 +183,9 @@ const isSpendingLimitMethod = (methodId: string): boolean => {
|
|||
return !!SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId]
|
||||
}
|
||||
|
||||
export const decodeMethods = (data: string | null): DataDecoded | null => {
|
||||
export const decodeMethods = (data: string | null): DataDecoded | undefined => {
|
||||
if (!data?.length) {
|
||||
return null
|
||||
return
|
||||
}
|
||||
|
||||
const [methodId, paramsHash] = [data.slice(0, 10), data.slice(10)]
|
||||
|
@ -226,6 +226,6 @@ export const decodeMethods = (data: string | null): DataDecoded | null => {
|
|||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
import { OpenCookieBannerPayload } from 'src/logic/cookies/store/reducer/cookies'
|
||||
|
||||
export const OPEN_COOKIE_BANNER = 'OPEN_COOKIE_BANNER'
|
||||
|
||||
export const openCookieBanner = createAction(OPEN_COOKIE_BANNER, (cookieBannerOpen) => ({
|
||||
cookieBannerOpen,
|
||||
}))
|
||||
export const openCookieBanner = createAction<OpenCookieBannerPayload>(OPEN_COOKIE_BANNER)
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import { Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { OPEN_COOKIE_BANNER } from 'src/logic/cookies/store/actions/openCookieBanner'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const COOKIES_REDUCER_ID = 'cookies'
|
||||
|
||||
export default handleActions(
|
||||
export type OpenCookieBannerPayload = { cookieBannerOpen: boolean; intercomAlertDisplayed?: boolean }
|
||||
|
||||
export default handleActions<AppReduxState['cookies'], OpenCookieBannerPayload>(
|
||||
{
|
||||
[OPEN_COOKIE_BANNER]: (state, action) => {
|
||||
const { cookieBannerOpen } = action.payload
|
||||
|
||||
return state.set('cookieBannerOpen', cookieBannerOpen)
|
||||
const { intercomAlertDisplayed = false, cookieBannerOpen } = action.payload
|
||||
return state.set('cookieBannerOpen', { intercomAlertDisplayed, cookieBannerOpen })
|
||||
},
|
||||
},
|
||||
Map(),
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import { Action } from 'redux-actions'
|
||||
import { ThunkDispatch } from 'redux-thunk'
|
||||
|
||||
import fetchCurrenciesRates from 'src/logic/currencyValues/api/fetchCurrenciesRates'
|
||||
import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
|
||||
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { Dispatch } from 'redux'
|
||||
import { CurrencyRatePayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
const fetchCurrencyRate = (safeAddress: string, selectedCurrency: string) => async (
|
||||
dispatch: Dispatch<typeof setCurrencyRate>,
|
||||
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrencyRatePayload>>,
|
||||
): Promise<void> => {
|
||||
if (AVAILABLE_CURRENCIES.USD === selectedCurrency) {
|
||||
return dispatch(setCurrencyRate(safeAddress, 1))
|
||||
dispatch(setCurrencyRate(safeAddress, 1))
|
||||
return
|
||||
}
|
||||
|
||||
const selectedCurrencyRateInBaseCurrency: number = await fetchCurrenciesRates(
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
|
||||
import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
|
||||
import { Action } from 'redux-actions'
|
||||
import { ThunkDispatch } from 'redux-thunk'
|
||||
|
||||
import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
|
||||
import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { CurrentCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||
import { loadSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage'
|
||||
import { Dispatch } from 'redux'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const fetchSelectedCurrency = (safeAddress: string) => async (
|
||||
dispatch: Dispatch<typeof setCurrencyBalances | typeof setSelectedCurrency | typeof setCurrencyRate>,
|
||||
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrentCurrencyPayload>>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const storedSelectedCurrency = await loadSelectedCurrency()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
import { Action, createAction } from 'redux-actions'
|
||||
import { ThunkDispatch } from 'redux-thunk'
|
||||
import { AnyAction } from 'redux'
|
||||
|
||||
import { CurrencyPayloads } from 'src/logic/currencyValues/store/reducer/currencyValues'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate'
|
||||
|
||||
|
@ -12,7 +13,7 @@ const setCurrentCurrency = createAction(SET_CURRENT_CURRENCY, (safeAddress: stri
|
|||
}))
|
||||
|
||||
export const setSelectedCurrency = (safeAddress: string, selectedCurrency: string) => (
|
||||
dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>,
|
||||
dispatch: ThunkDispatch<AppReduxState, undefined, Action<CurrencyPayloads>>,
|
||||
): void => {
|
||||
dispatch(setCurrentCurrency(safeAddress, selectedCurrency))
|
||||
dispatch(fetchCurrencyRate(safeAddress, selectedCurrency))
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
import { Action, handleActions } from 'redux-actions'
|
||||
|
||||
import { SET_CURRENCY_BALANCES } from 'src/logic/currencyValues/store/actions/setCurrencyBalances'
|
||||
import { SET_CURRENCY_RATE } from 'src/logic/currencyValues/store/actions/setCurrencyRate'
|
||||
import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency'
|
||||
import { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
import { BalanceCurrencyList, CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues'
|
||||
|
||||
export const CURRENCY_VALUES_KEY = 'currencyValues'
|
||||
|
||||
|
@ -15,19 +15,26 @@ export interface CurrencyReducerMap extends Map<string, any> {
|
|||
|
||||
export type CurrencyValuesState = Map<string, CurrencyReducerMap>
|
||||
|
||||
export default handleActions(
|
||||
type CurrencyBasePayload = { safeAddress: string }
|
||||
export type CurrencyRatePayload = CurrencyBasePayload & { currencyRate: number }
|
||||
export type CurrencyBalancesPayload = CurrencyBasePayload & { currencyBalances: BalanceCurrencyList }
|
||||
export type CurrentCurrencyPayload = CurrencyBasePayload & { selectedCurrency: string }
|
||||
|
||||
export type CurrencyPayloads = CurrencyRatePayload | CurrencyBalancesPayload | CurrentCurrencyPayload
|
||||
|
||||
export default handleActions<CurrencyReducerMap, CurrencyPayloads>(
|
||||
{
|
||||
[SET_CURRENCY_RATE]: (state: CurrencyReducerMap, action) => {
|
||||
[SET_CURRENCY_RATE]: (state, action: Action<CurrencyRatePayload>) => {
|
||||
const { currencyRate, safeAddress } = action.payload
|
||||
|
||||
return state.setIn([safeAddress, 'currencyRate'], currencyRate)
|
||||
},
|
||||
[SET_CURRENCY_BALANCES]: (state: CurrencyReducerMap, action) => {
|
||||
const { currencyBalances, safeAddress } = action.payload
|
||||
[SET_CURRENCY_BALANCES]: (state, action: Action<CurrencyBalancesPayload>) => {
|
||||
const { safeAddress, currencyBalances } = action.payload
|
||||
|
||||
return state.setIn([safeAddress, 'currencyBalances'], currencyBalances)
|
||||
},
|
||||
[SET_CURRENT_CURRENCY]: (state: CurrencyReducerMap, action) => {
|
||||
[SET_CURRENT_CURRENCY]: (state, action: Action<CurrentCurrencyPayload>) => {
|
||||
const { safeAddress, selectedCurrency } = action.payload
|
||||
|
||||
return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { handleActions } from 'redux-actions'
|
||||
import { Action, handleActions } from 'redux-actions'
|
||||
|
||||
import { LOAD_CURRENT_SESSION } from 'src/logic/currentSession/store/actions/loadCurrentSession'
|
||||
import { UPDATE_VIEWED_SAFES } from 'src/logic/currentSession/store/actions/updateViewedSafes'
|
||||
import { saveCurrentSessionToStorage } from 'src/logic/currentSession/utils'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const CURRENT_SESSION_REDUCER_ID = 'currentSession'
|
||||
|
||||
|
@ -14,13 +15,15 @@ export const initialState = {
|
|||
viewedSafes: [],
|
||||
}
|
||||
|
||||
export default handleActions(
|
||||
type CurrentSessionPayloads = CurrentSessionState | string
|
||||
|
||||
export default handleActions<AppReduxState['currentSession'], CurrentSessionPayloads>(
|
||||
{
|
||||
[LOAD_CURRENT_SESSION]: (state = initialState, action) => ({
|
||||
[LOAD_CURRENT_SESSION]: (state = initialState, action: Action<CurrentSessionState>) => ({
|
||||
...state,
|
||||
...action.payload,
|
||||
}),
|
||||
[UPDATE_VIEWED_SAFES]: (state, action) => {
|
||||
[UPDATE_VIEWED_SAFES]: (state, action: Action<string>) => {
|
||||
const safeAddress = action.payload
|
||||
const viewedSafes = state.viewedSafes
|
||||
const newState = {
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
import {
|
||||
checkIfTxIsApproveAndExecution,
|
||||
checkIfTxIsCreation,
|
||||
checkIfTxIsExecution,
|
||||
} from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
|
||||
describe('checkIfTxIsExecution', () => {
|
||||
const mockedEthAccount = '0x29B1b813b6e84654Ca698ef5d7808E154364900B'
|
||||
it(`should return true if the safe threshold is 1`, () => {
|
||||
// given
|
||||
const threshold = 1
|
||||
const preApprovingOwner = undefined
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = ''
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return true if the safe threshold is reached for the transaction`, () => {
|
||||
// given
|
||||
const threshold = 3
|
||||
const preApprovingOwner = mockedEthAccount
|
||||
const transactionConfirmations = 3
|
||||
const transactionType = ''
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return true if the transaction is spendingLimit`, () => {
|
||||
// given
|
||||
const threshold = 5
|
||||
const preApprovingOwner = undefined
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = 'spendingLimit'
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return true if the number of confirmations is one bellow the threshold but there is a preApprovingOwner`, () => {
|
||||
// given
|
||||
const threshold = 5
|
||||
const preApprovingOwner = mockedEthAccount
|
||||
const transactionConfirmations = 4
|
||||
const transactionType = undefined
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return false if the number of confirmations is one bellow the threshold and there is no preApprovingOwner`, () => {
|
||||
// given
|
||||
const threshold = 5
|
||||
const preApprovingOwner = undefined
|
||||
const transactionConfirmations = 4
|
||||
const transactionType = undefined
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsExecution(threshold, preApprovingOwner, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
describe('checkIfTxIsCreation', () => {
|
||||
it(`should return true if there are no confirmations for the transaction and the transaction is not spendingLimit`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = ''
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsCreation(transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return false if there are no confirmations for the transaction and the transaction is spendingLimit`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = 'spendingLimit'
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsCreation(transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it(`should return false if there are confirmations for the transaction`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 2
|
||||
const transactionType = ''
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsCreation(transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkIfTxIsApproveAndExecution', () => {
|
||||
const mockedEthAccount = '0x29B1b813b6e84654Ca698ef5d7808E154364900B'
|
||||
it(`should return true if there is only one confirmation left to reach the safe threshold and there is a preApproving account`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 2
|
||||
const safeThreshold = 3
|
||||
const transactionType = ''
|
||||
const preApprovingOwner = mockedEthAccount
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsApproveAndExecution(
|
||||
safeThreshold,
|
||||
transactionConfirmations,
|
||||
transactionType,
|
||||
preApprovingOwner,
|
||||
)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return false if there is only one confirmation left to reach the safe threshold and but there is no preApproving account`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 2
|
||||
const safeThreshold = 3
|
||||
const transactionType = ''
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsApproveAndExecution(safeThreshold, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it(`should return true if the transaction is spendingLimit and there is a preApproving account`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = 'spendingLimit'
|
||||
const safeThreshold = 3
|
||||
const preApprovingOwner = mockedEthAccount
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsApproveAndExecution(
|
||||
safeThreshold,
|
||||
transactionConfirmations,
|
||||
transactionType,
|
||||
preApprovingOwner,
|
||||
)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return false if the transaction is spendingLimit and there is no preApproving account`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = 'spendingLimit'
|
||||
const safeThreshold = 3
|
||||
const preApprovingOwner = mockedEthAccount
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsApproveAndExecution(
|
||||
safeThreshold,
|
||||
transactionConfirmations,
|
||||
transactionType,
|
||||
preApprovingOwner,
|
||||
)
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
it(`should return false if the are missing more than one confirmations to reach the safe threshold and the transaction is not spendingLimit`, () => {
|
||||
// given
|
||||
const transactionConfirmations = 0
|
||||
const transactionType = ''
|
||||
const safeThreshold = 3
|
||||
|
||||
// when
|
||||
const result = checkIfTxIsApproveAndExecution(safeThreshold, transactionConfirmations, transactionType)
|
||||
|
||||
// then
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
|
@ -1,8 +1,10 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
estimateGasForTransactionApproval,
|
||||
estimateGasForTransactionCreation,
|
||||
estimateGasForTransactionExecution,
|
||||
MINIMUM_TRANSACTION_GAS,
|
||||
} from 'src/logic/safe/transactions/gas'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
|
@ -15,14 +17,14 @@ import {
|
|||
safeThresholdSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { CALL } from 'src/logic/safe/transactions'
|
||||
import { providerSelector } from '../wallets/store/selectors'
|
||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||
import { providerSelector } from 'src/logic/wallets/store/selectors'
|
||||
|
||||
import { List } from 'immutable'
|
||||
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||
import { checkIfOffChainSignatureIsPossible } from 'src/logic/safe/safeTxSigner'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
import { WALLETS } from 'src/config/networks/network.d'
|
||||
|
||||
export enum EstimationStatus {
|
||||
LOADING = 'LOADING',
|
||||
|
@ -30,18 +32,37 @@ export enum EstimationStatus {
|
|||
SUCCESS = 'SUCCESS',
|
||||
}
|
||||
|
||||
const checkIfTxIsExecution = (
|
||||
export const checkIfTxIsExecution = (
|
||||
threshold: number,
|
||||
preApprovingOwner?: string,
|
||||
txConfirmations?: number,
|
||||
txType?: string,
|
||||
): boolean =>
|
||||
txConfirmations === threshold || !!preApprovingOwner || threshold === 1 || sameString(txType, 'spendingLimit')
|
||||
): boolean => {
|
||||
if (threshold === 1 || sameString(txType, 'spendingLimit') || txConfirmations === threshold) {
|
||||
return true
|
||||
}
|
||||
|
||||
const checkIfTxIsApproveAndExecution = (threshold: number, txConfirmations: number, txType?: string): boolean =>
|
||||
txConfirmations + 1 === threshold || sameString(txType, 'spendingLimit')
|
||||
if (preApprovingOwner && txConfirmations) {
|
||||
return txConfirmations + 1 === threshold
|
||||
}
|
||||
|
||||
const checkIfTxIsCreation = (txConfirmations: number, txType?: string): boolean =>
|
||||
return false
|
||||
}
|
||||
|
||||
export const checkIfTxIsApproveAndExecution = (
|
||||
threshold: number,
|
||||
txConfirmations: number,
|
||||
txType?: string,
|
||||
preApprovingOwner?: string,
|
||||
): boolean => {
|
||||
if (preApprovingOwner) {
|
||||
return txConfirmations + 1 === threshold || sameString(txType, 'spendingLimit')
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const checkIfTxIsCreation = (txConfirmations: number, txType?: string): boolean =>
|
||||
txConfirmations === 0 && !sameString(txType, 'spendingLimit')
|
||||
|
||||
type TransactionEstimationProps = {
|
||||
|
@ -80,7 +101,14 @@ const estimateTransactionGas = async ({
|
|||
approvalAndExecution,
|
||||
}: TransactionEstimationProps): Promise<number> => {
|
||||
if (isCreation) {
|
||||
return estimateGasForTransactionCreation(safeAddress, txData, txRecipient, txAmount || '0', operation || CALL)
|
||||
return estimateGasForTransactionCreation(
|
||||
safeAddress,
|
||||
txData,
|
||||
txRecipient,
|
||||
txAmount || '0',
|
||||
operation || CALL,
|
||||
safeTxGas,
|
||||
)
|
||||
}
|
||||
|
||||
if (!from) {
|
||||
|
@ -124,14 +152,17 @@ type UseEstimateTransactionGasProps = {
|
|||
operation?: number
|
||||
safeTxGas?: number
|
||||
txType?: string
|
||||
manualGasPrice?: string
|
||||
}
|
||||
|
||||
type TransactionGasEstimationResult = {
|
||||
export 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
|
||||
gasPriceFormatted: string // Current gas price formatted
|
||||
gasLimit: string // Minimum gas requited to execute the Tx
|
||||
isExecution: boolean // Returns true if the user will execute the tx or false if it just signs it
|
||||
isCreation: boolean // Returns true if the transaction is a creation transaction
|
||||
isOffChainSignature: boolean // Returns true if offChainSignature is available
|
||||
|
@ -146,6 +177,7 @@ export const useEstimateTransactionGas = ({
|
|||
operation,
|
||||
safeTxGas,
|
||||
txType,
|
||||
manualGasPrice,
|
||||
}: UseEstimateTransactionGasProps): TransactionGasEstimationResult => {
|
||||
const [gasEstimation, setGasEstimation] = useState<TransactionGasEstimationResult>({
|
||||
txEstimationExecutionStatus: EstimationStatus.LOADING,
|
||||
|
@ -153,6 +185,8 @@ export const useEstimateTransactionGas = ({
|
|||
gasCost: '0',
|
||||
gasCostFormatted: '< 0.001',
|
||||
gasPrice: '0',
|
||||
gasPriceFormatted: '0',
|
||||
gasLimit: '0',
|
||||
isExecution: false,
|
||||
isCreation: false,
|
||||
isOffChainSignature: false,
|
||||
|
@ -168,14 +202,15 @@ export const useEstimateTransactionGas = ({
|
|||
if (!txData.length) {
|
||||
return
|
||||
}
|
||||
// FIXME this should be removed when estimating with WalletConnect correctly
|
||||
if (!providerName || sameString(providerName, WALLETS.WALLET_CONNECT)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isExecution = checkIfTxIsExecution(Number(threshold), preApprovingOwner, txConfirmations?.size, txType)
|
||||
const isCreation = checkIfTxIsCreation(txConfirmations?.size || 0, txType)
|
||||
const approvalAndExecution = checkIfTxIsApproveAndExecution(Number(threshold), txConfirmations?.size || 0, txType)
|
||||
const approvalAndExecution = checkIfTxIsApproveAndExecution(
|
||||
Number(threshold),
|
||||
txConfirmations?.size || 0,
|
||||
txType,
|
||||
preApprovingOwner,
|
||||
)
|
||||
|
||||
try {
|
||||
const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)
|
||||
|
@ -194,10 +229,12 @@ export const useEstimateTransactionGas = ({
|
|||
safeTxGas,
|
||||
approvalAndExecution,
|
||||
})
|
||||
const gasPrice = await calculateGasPrice()
|
||||
const gasPrice = manualGasPrice ? web3.utils.toWei(manualGasPrice, 'gwei') : await calculateGasPrice()
|
||||
const gasPriceFormatted = web3.utils.fromWei(gasPrice, 'gwei')
|
||||
const estimatedGasCosts = gasEstimation * parseInt(gasPrice, 10)
|
||||
const gasCost = fromTokenUnit(estimatedGasCosts, nativeCoin.decimals)
|
||||
const gasCostFormatted = formatAmount(gasCost)
|
||||
const gasLimit = (gasEstimation * 2 + MINIMUM_TRANSACTION_GAS).toString()
|
||||
|
||||
let txEstimationExecutionStatus = EstimationStatus.SUCCESS
|
||||
|
||||
|
@ -211,6 +248,8 @@ export const useEstimateTransactionGas = ({
|
|||
gasCost,
|
||||
gasCostFormatted,
|
||||
gasPrice,
|
||||
gasPriceFormatted,
|
||||
gasLimit,
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature,
|
||||
|
@ -218,7 +257,7 @@ export const useEstimateTransactionGas = ({
|
|||
} catch (error) {
|
||||
console.warn(error.message)
|
||||
// We put a fixed the amount of gas to let the user try to execute the tx, but it's not accurate so it will probably fail
|
||||
const gasEstimation = 10000
|
||||
const gasEstimation = MINIMUM_TRANSACTION_GAS
|
||||
const gasCost = fromTokenUnit(gasEstimation, nativeCoin.decimals)
|
||||
const gasCostFormatted = formatAmount(gasCost)
|
||||
setGasEstimation({
|
||||
|
@ -227,6 +266,8 @@ export const useEstimateTransactionGas = ({
|
|||
gasCost,
|
||||
gasCostFormatted,
|
||||
gasPrice: '1',
|
||||
gasPriceFormatted: '1',
|
||||
gasLimit: '0',
|
||||
isExecution,
|
||||
isCreation,
|
||||
isOffChainSignature: false,
|
||||
|
@ -251,6 +292,7 @@ export const useEstimateTransactionGas = ({
|
|||
safeTxGas,
|
||||
txType,
|
||||
providerName,
|
||||
manualGasPrice,
|
||||
])
|
||||
|
||||
return gasEstimation
|
||||
|
|
|
@ -22,18 +22,15 @@ const getStandardTxNotificationsQueue = (
|
|||
origin: string,
|
||||
): Record<string, Record<string, Notification> | Notification> => ({
|
||||
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
|
||||
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin),
|
||||
afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin),
|
||||
afterExecution: {
|
||||
noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin),
|
||||
moreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG, origin),
|
||||
},
|
||||
afterExecutionError: setNotificationOrigin(NOTIFICATIONS.TX_FAILED_MSG, origin),
|
||||
})
|
||||
|
||||
const waitingTransactionNotificationsQueue = {
|
||||
beforeExecution: null,
|
||||
pendingExecution: null,
|
||||
afterRejection: null,
|
||||
waitingConfirmation: NOTIFICATIONS.TX_WAITING_MSG,
|
||||
afterExecution: null,
|
||||
|
@ -43,7 +40,6 @@ const waitingTransactionNotificationsQueue = {
|
|||
const getConfirmationTxNotificationsQueue = (origin: string) => {
|
||||
return {
|
||||
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
|
||||
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_CONFIRMATION_PENDING_MSG, origin),
|
||||
afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin),
|
||||
afterExecution: {
|
||||
noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin),
|
||||
|
@ -56,7 +52,6 @@ const getConfirmationTxNotificationsQueue = (origin: string) => {
|
|||
const getCancellationTxNotificationsQueue = (origin: string) => {
|
||||
return {
|
||||
beforeExecution: setNotificationOrigin(NOTIFICATIONS.SIGN_TX_MSG, origin),
|
||||
pendingExecution: setNotificationOrigin(NOTIFICATIONS.TX_PENDING_MSG, origin),
|
||||
afterRejection: setNotificationOrigin(NOTIFICATIONS.TX_REJECTED_MSG, origin),
|
||||
afterExecution: {
|
||||
noMoreConfirmationsNeeded: setNotificationOrigin(NOTIFICATIONS.TX_EXECUTED_MSG, origin),
|
||||
|
@ -68,7 +63,6 @@ const getCancellationTxNotificationsQueue = (origin: string) => {
|
|||
|
||||
const safeNameChangeNotificationsQueue = {
|
||||
beforeExecution: null,
|
||||
pendingExecution: null,
|
||||
afterRejection: null,
|
||||
afterExecution: {
|
||||
noMoreConfirmationsNeeded: NOTIFICATIONS.SAFE_NAME_CHANGED_MSG,
|
||||
|
@ -79,7 +73,6 @@ const safeNameChangeNotificationsQueue = {
|
|||
|
||||
const ownerNameChangeNotificationsQueue = {
|
||||
beforeExecution: null,
|
||||
pendingExecution: null,
|
||||
afterRejection: null,
|
||||
afterExecution: {
|
||||
noMoreConfirmationsNeeded: NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG,
|
||||
|
@ -90,7 +83,6 @@ const ownerNameChangeNotificationsQueue = {
|
|||
|
||||
const settingsChangeTxNotificationsQueue = {
|
||||
beforeExecution: NOTIFICATIONS.SIGN_SETTINGS_CHANGE_MSG,
|
||||
pendingExecution: NOTIFICATIONS.SETTINGS_CHANGE_PENDING_MSG,
|
||||
afterRejection: NOTIFICATIONS.SETTINGS_CHANGE_REJECTED_MSG,
|
||||
afterExecution: {
|
||||
noMoreConfirmationsNeeded: NOTIFICATIONS.SETTINGS_CHANGE_EXECUTED_MSG,
|
||||
|
@ -101,7 +93,6 @@ const settingsChangeTxNotificationsQueue = {
|
|||
|
||||
const newSpendingLimitTxNotificationsQueue = {
|
||||
beforeExecution: NOTIFICATIONS.SIGN_NEW_SPENDING_LIMIT_MSG,
|
||||
pendingExecution: NOTIFICATIONS.NEW_SPENDING_LIMIT_PENDING_MSG,
|
||||
afterRejection: NOTIFICATIONS.NEW_SPENDING_LIMIT_REJECTED_MSG,
|
||||
afterExecution: {
|
||||
noMoreConfirmationsNeeded: NOTIFICATIONS.NEW_SPENDING_LIMIT_EXECUTED_MSG,
|
||||
|
@ -112,7 +103,6 @@ const newSpendingLimitTxNotificationsQueue = {
|
|||
|
||||
const removeSpendingLimitTxNotificationsQueue = {
|
||||
beforeExecution: NOTIFICATIONS.SIGN_REMOVE_SPENDING_LIMIT_MSG,
|
||||
pendingExecution: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_PENDING_MSG,
|
||||
afterRejection: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_REJECTED_MSG,
|
||||
afterExecution: {
|
||||
noMoreConfirmationsNeeded: NOTIFICATIONS.REMOVE_SPENDING_LIMIT_EXECUTED_MSG,
|
||||
|
@ -123,18 +113,15 @@ const removeSpendingLimitTxNotificationsQueue = {
|
|||
|
||||
const defaultNotificationsQueue = {
|
||||
beforeExecution: NOTIFICATIONS.SIGN_TX_MSG,
|
||||
pendingExecution: NOTIFICATIONS.TX_PENDING_MSG,
|
||||
afterRejection: NOTIFICATIONS.TX_REJECTED_MSG,
|
||||
afterExecution: {
|
||||
noMoreConfirmationsNeeded: NOTIFICATIONS.TX_EXECUTED_MSG,
|
||||
moreConfirmationsNeeded: NOTIFICATIONS.TX_EXECUTED_MORE_CONFIRMATIONS_MSG,
|
||||
},
|
||||
afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG,
|
||||
}
|
||||
|
||||
const addressBookNewEntry = {
|
||||
beforeExecution: null,
|
||||
pendingExecution: null,
|
||||
afterRejection: null,
|
||||
waitingConfirmation: null,
|
||||
afterExecution: {
|
||||
|
@ -146,7 +133,6 @@ const addressBookNewEntry = {
|
|||
|
||||
const addressBookEditEntry = {
|
||||
beforeExecution: null,
|
||||
pendingExecution: null,
|
||||
afterRejection: null,
|
||||
waitingConfirmation: null,
|
||||
afterExecution: {
|
||||
|
@ -158,7 +144,6 @@ const addressBookEditEntry = {
|
|||
|
||||
const addressBookDeleteEntry = {
|
||||
beforeExecution: null,
|
||||
pendingExecution: null,
|
||||
afterRejection: null,
|
||||
waitingConfirmation: null,
|
||||
afterExecution: {
|
||||
|
@ -231,7 +216,7 @@ export const getNotificationsFromTxType: any = (txType, origin) => {
|
|||
|
||||
export const enhanceSnackbarForAction = (
|
||||
notification: Notification,
|
||||
key?: number | string,
|
||||
key?: string,
|
||||
onClick?: () => void,
|
||||
): Notification => ({
|
||||
...notification,
|
||||
|
|
|
@ -15,45 +15,35 @@ export type NotificationId = keyof typeof NOTIFICATION_IDS
|
|||
export type Notification = {
|
||||
message: string
|
||||
options: OptionsObject
|
||||
key?: number | string
|
||||
key?: string
|
||||
dismissed?: boolean
|
||||
}
|
||||
|
||||
const NOTIFICATION_IDS = {
|
||||
CONNECT_WALLET_MSG: 'CONNECT_WALLET_MSG',
|
||||
CONNECT_WALLET_READ_MODE_MSG: 'CONNECT_WALLET_READ_MODE_MSG',
|
||||
WALLET_CONNECTED_MSG: 'WALLET_CONNECTED_MSG',
|
||||
WALLET_DISCONNECTED_MSG: 'WALLET_DISCONNECTED_MSG',
|
||||
UNLOCK_WALLET_MSG: 'UNLOCK_WALLET_MSG',
|
||||
CONNECT_WALLET_ERROR_MSG: 'CONNECT_WALLET_ERROR_MSG',
|
||||
SIGN_TX_MSG: 'SIGN_TX_MSG',
|
||||
TX_PENDING_MSG: 'TX_PENDING_MSG',
|
||||
TX_REJECTED_MSG: 'TX_REJECTED_MSG',
|
||||
TX_EXECUTED_MSG: 'TX_EXECUTED_MSG',
|
||||
TX_CANCELLATION_EXECUTED_MSG: 'TX_CANCELLATION_EXECUTED_MSG',
|
||||
TX_FAILED_MSG: 'TX_FAILED_MSG',
|
||||
TX_EXECUTED_MORE_CONFIRMATIONS_MSG: 'TX_EXECUTED_MORE_CONFIRMATIONS_MSG',
|
||||
TX_WAITING_MSG: 'TX_WAITING_MSG',
|
||||
TX_INCOMING_MSG: 'TX_INCOMING_MSG',
|
||||
TX_CONFIRMATION_PENDING_MSG: 'TX_CONFIRMATION_PENDING_MSG',
|
||||
TX_CONFIRMATION_EXECUTED_MSG: 'TX_CONFIRMATION_EXECUTED_MSG',
|
||||
TX_CONFIRMATION_FAILED_MSG: 'TX_CONFIRMATION_FAILED_MSG',
|
||||
SAFE_NAME_CHANGED_MSG: 'SAFE_NAME_CHANGED_MSG',
|
||||
OWNER_NAME_CHANGE_EXECUTED_MSG: 'OWNER_NAME_CHANGE_EXECUTED_MSG',
|
||||
SIGN_SETTINGS_CHANGE_MSG: 'SIGN_SETTINGS_CHANGE_MSG',
|
||||
SETTINGS_CHANGE_PENDING_MSG: 'SETTINGS_CHANGE_PENDING_MSG',
|
||||
SETTINGS_CHANGE_REJECTED_MSG: 'SETTINGS_CHANGE_REJECTED_MSG',
|
||||
SETTINGS_CHANGE_EXECUTED_MSG: 'SETTINGS_CHANGE_EXECUTED_MSG',
|
||||
SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: 'SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG',
|
||||
SETTINGS_CHANGE_FAILED_MSG: 'SETTINGS_CHANGE_FAILED_MSG',
|
||||
TESTNET_VERSION_MSG: 'TESTNET_VERSION_MSG',
|
||||
SIGN_NEW_SPENDING_LIMIT_MSG: 'SIGN_NEW_SPENDING_LIMIT_MSG',
|
||||
NEW_SPENDING_LIMIT_PENDING_MSG: 'NEW_SPENDING_LIMIT_PENDING_MSG',
|
||||
NEW_SPENDING_LIMIT_REJECTED_MSG: 'NEW_SPENDING_LIMIT_REJECTED_MSG',
|
||||
NEW_SPENDING_LIMIT_EXECUTED_MSG: 'NEW_SPENDING_LIMIT_EXECUTED_MSG',
|
||||
NEW_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: 'NEW_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG',
|
||||
NEW_SPENDING_LIMIT_FAILED_MSG: 'NEW_SPENDING_LIMIT_FAILED_MSG',
|
||||
SIGN_REMOVE_SPENDING_LIMIT_MSG: 'SIGN_REMOVE_SPENDING_LIMIT_MSG',
|
||||
REMOVE_SPENDING_LIMIT_PENDING_MSG: 'REMOVE_SPENDING_LIMIT_PENDING_MSG',
|
||||
REMOVE_SPENDING_LIMIT_REJECTED_MSG: 'REMOVE_SPENDING_LIMIT_REJECTED_MSG',
|
||||
REMOVE_SPENDING_LIMIT_EXECUTED_MSG: 'REMOVE_SPENDING_LIMIT_EXECUTED_MSG',
|
||||
REMOVE_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG: 'REMOVE_SPENDING_LIMIT_EXECUTED_MORE_CONFIRMATIONS_MSG',
|
||||
|
@ -67,32 +57,6 @@ const NOTIFICATION_IDS = {
|
|||
|
||||
export const NOTIFICATIONS: Record<NotificationId, Notification> = {
|
||||
// Wallet Connection
|
||||
CONNECT_WALLET_MSG: {
|
||||
message: 'Please connect wallet to continue',
|
||||
options: { variant: WARNING, persist: true, preventDuplicate: true },
|
||||
},
|
||||
CONNECT_WALLET_READ_MODE_MSG: {
|
||||
message: 'You are in read-only mode: Please connect wallet',
|
||||
options: { variant: WARNING, persist: true, preventDuplicate: true },
|
||||
},
|
||||
WALLET_CONNECTED_MSG: {
|
||||
message: 'Wallet connected',
|
||||
options: {
|
||||
variant: SUCCESS,
|
||||
persist: false,
|
||||
autoHideDuration: shortDuration,
|
||||
},
|
||||
},
|
||||
WALLET_DISCONNECTED_MSG: {
|
||||
message: 'Wallet disconnected',
|
||||
key: 'WALLET_DISCONNECTED_MSG',
|
||||
options: {
|
||||
variant: SUCCESS,
|
||||
persist: false,
|
||||
autoHideDuration: shortDuration,
|
||||
preventDuplicate: true,
|
||||
},
|
||||
},
|
||||
UNLOCK_WALLET_MSG: {
|
||||
message: 'Unlock your wallet to connect',
|
||||
options: { variant: WARNING, persist: true, preventDuplicate: true },
|
||||
|
@ -107,62 +71,40 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
|
|||
message: 'Please sign the transaction',
|
||||
options: { variant: INFO, persist: true },
|
||||
},
|
||||
TX_PENDING_MSG: {
|
||||
message: 'Transaction pending',
|
||||
options: { variant: INFO, persist: true },
|
||||
},
|
||||
TX_REJECTED_MSG: {
|
||||
message: 'Transaction rejected',
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: shortDuration },
|
||||
},
|
||||
TX_EXECUTED_MSG: {
|
||||
message: 'Transaction successfully executed',
|
||||
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
|
||||
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
|
||||
},
|
||||
TX_CANCELLATION_EXECUTED_MSG: {
|
||||
message: 'Rejection successfully submitted',
|
||||
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
|
||||
},
|
||||
TX_EXECUTED_MORE_CONFIRMATIONS_MSG: {
|
||||
message: 'Transaction successfully created. More confirmations needed to execute',
|
||||
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
|
||||
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
|
||||
},
|
||||
TX_FAILED_MSG: {
|
||||
message: 'Transaction failed',
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: shortDuration },
|
||||
},
|
||||
TX_WAITING_MSG: {
|
||||
message: 'A pending transaction requires your confirmation!',
|
||||
message: 'A transaction requires your confirmation',
|
||||
key: 'TX_WAITING_MSG',
|
||||
options: {
|
||||
variant: WARNING,
|
||||
persist: true,
|
||||
preventDuplicate: true,
|
||||
},
|
||||
},
|
||||
TX_INCOMING_MSG: {
|
||||
message: 'Incoming transfer: ',
|
||||
key: 'TX_INCOMING_MSG',
|
||||
options: {
|
||||
variant: SUCCESS,
|
||||
persist: false,
|
||||
autoHideDuration: longDuration,
|
||||
autoHideDuration: shortDuration,
|
||||
preventDuplicate: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Approval Transactions
|
||||
TX_CONFIRMATION_PENDING_MSG: {
|
||||
message: 'Confirmation transaction pending',
|
||||
options: { variant: INFO, persist: true },
|
||||
},
|
||||
TX_CONFIRMATION_EXECUTED_MSG: {
|
||||
message: 'Confirmation transaction was successful',
|
||||
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
|
||||
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
|
||||
},
|
||||
TX_CONFIRMATION_FAILED_MSG: {
|
||||
message: 'Confirmation transaction failed',
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: shortDuration },
|
||||
},
|
||||
|
||||
// Safe Name
|
||||
|
@ -182,17 +124,13 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
|
|||
message: 'Please sign the settings change',
|
||||
options: { variant: INFO, persist: true },
|
||||
},
|
||||
SETTINGS_CHANGE_PENDING_MSG: {
|
||||
message: 'Settings change pending',
|
||||
options: { variant: INFO, persist: true },
|
||||
},
|
||||
SETTINGS_CHANGE_REJECTED_MSG: {
|
||||
message: 'Settings change rejected',
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: shortDuration },
|
||||
},
|
||||
SETTINGS_CHANGE_EXECUTED_MSG: {
|
||||
message: 'Settings change successfully executed',
|
||||
options: { variant: SUCCESS, persist: false, autoHideDuration: longDuration },
|
||||
options: { variant: SUCCESS, persist: false, autoHideDuration: shortDuration },
|
||||
},
|
||||
SETTINGS_CHANGE_EXECUTED_MORE_CONFIRMATIONS_MSG: {
|
||||
message: 'Settings change successfully created. More confirmations needed to execute',
|
||||
|
@ -200,7 +138,7 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
|
|||
},
|
||||
SETTINGS_CHANGE_FAILED_MSG: {
|
||||
message: 'Settings change failed',
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: shortDuration },
|
||||
},
|
||||
|
||||
// Spending Limit
|
||||
|
@ -208,10 +146,6 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
|
|||
message: 'Please sign the new Spending Limit',
|
||||
options: { variant: INFO, persist: true },
|
||||
},
|
||||
NEW_SPENDING_LIMIT_PENDING_MSG: {
|
||||
message: 'New Spending Limit pending',
|
||||
options: { variant: INFO, persist: true },
|
||||
},
|
||||
NEW_SPENDING_LIMIT_REJECTED_MSG: {
|
||||
message: 'New Spending Limit rejected',
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
|
||||
|
@ -232,10 +166,6 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
|
|||
message: 'Please sign the remove Spending Limit',
|
||||
options: { variant: INFO, persist: true },
|
||||
},
|
||||
REMOVE_SPENDING_LIMIT_PENDING_MSG: {
|
||||
message: 'Remove Spending Limit pending',
|
||||
options: { variant: INFO, persist: true },
|
||||
},
|
||||
REMOVE_SPENDING_LIMIT_REJECTED_MSG: {
|
||||
message: 'Remove Spending Limit rejected',
|
||||
options: { variant: ERROR, persist: false, autoHideDuration: longDuration },
|
||||
|
@ -256,7 +186,7 @@ export const NOTIFICATIONS: Record<NotificationId, Notification> = {
|
|||
// Network
|
||||
TESTNET_VERSION_MSG: {
|
||||
message: "Testnet Version: Don't send production assets to this Safe",
|
||||
options: { variant: WARNING, persist: true, preventDuplicate: true },
|
||||
options: { variant: WARNING, persist: false, preventDuplicate: true, autoHideDuration: longDuration },
|
||||
},
|
||||
WRONG_NETWORK_MSG: {
|
||||
message: `Wrong network: Please use ${getNetworkName()}`,
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { Record } from 'immutable'
|
||||
|
||||
export const makeNotification = Record({
|
||||
key: 0,
|
||||
message: '',
|
||||
options: {},
|
||||
dismissed: false,
|
||||
})
|
|
@ -1,38 +1,55 @@
|
|||
import { Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
import { Action, handleActions } from 'redux-actions'
|
||||
|
||||
import { Notification } from 'src/logic/notifications/notificationTypes'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { CLOSE_SNACKBAR } from '../actions/closeSnackbar'
|
||||
import { ENQUEUE_SNACKBAR } from '../actions/enqueueSnackbar'
|
||||
import { REMOVE_SNACKBAR } from '../actions/removeSnackbar'
|
||||
|
||||
import { makeNotification } from 'src/logic/notifications/store/models/notification'
|
||||
|
||||
export const NOTIFICATIONS_REDUCER_ID = 'notifications'
|
||||
|
||||
export default handleActions(
|
||||
type CloseSnackBarPayload = { key: string; dismissAll: boolean }
|
||||
type Payloads = Notification | CloseSnackBarPayload | string
|
||||
|
||||
export default handleActions<AppReduxState['notifications'], Payloads>(
|
||||
{
|
||||
[ENQUEUE_SNACKBAR]: (state, action) => {
|
||||
[ENQUEUE_SNACKBAR]: (state, action: Action<Notification>) => {
|
||||
const notification = action.payload
|
||||
|
||||
return state.set(notification.key, makeNotification(notification))
|
||||
if (!notification.key) {
|
||||
return state
|
||||
}
|
||||
|
||||
return state.set(notification.key, notification)
|
||||
},
|
||||
[CLOSE_SNACKBAR]: (state, action) => {
|
||||
[CLOSE_SNACKBAR]: (state, action: Action<CloseSnackBarPayload>) => {
|
||||
const { dismissAll, key } = action.payload
|
||||
|
||||
if (key) {
|
||||
return state.update(key, (prev) => prev?.set('dismissed', true))
|
||||
if (key && state.get(key)) {
|
||||
return state.update(key, (notification) => {
|
||||
if (notification) {
|
||||
return {
|
||||
...notification,
|
||||
dismissed: true,
|
||||
}
|
||||
}
|
||||
|
||||
return notification
|
||||
})
|
||||
}
|
||||
|
||||
if (dismissAll) {
|
||||
return state.withMutations((map) => {
|
||||
map.forEach((notification, notificationKey) => {
|
||||
map.set(notificationKey, notification.set('dismissed', true))
|
||||
map.set(notificationKey, { ...notification, dismissed: true })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return state
|
||||
},
|
||||
[REMOVE_SNACKBAR]: (state, action) => {
|
||||
[REMOVE_SNACKBAR]: (state, action: Action<string>) => {
|
||||
const key = action.payload
|
||||
|
||||
return state.delete(key)
|
||||
|
|
|
@ -90,7 +90,7 @@ describe('isInnerTransaction', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('isCancelTransaction', () => {
|
||||
describe.skip('isCancelTransaction', () => {
|
||||
const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
|
||||
const mockedETHAccount = '0xd76e0B566e218a80F4c96458FE09a322EBAa9aF2'
|
||||
it('It should return false if given a inner transaction with empty data', () => {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
|
||||
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
||||
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||
import { getMockedSafeInstance } from 'src/test/utils/safeHelper'
|
||||
import { NonPayableTransactionObject } from 'src/types/contracts/types'
|
||||
|
||||
describe('Store actions utils > getNewTxNonce', () => {
|
||||
it(`Should return nonce of a last transaction + 1 if passed nonce is less than last transaction or invalid`, async () => {
|
||||
|
@ -43,13 +45,12 @@ describe('Store actions utils > getNewTxNonce', () => {
|
|||
describe('Store actions utils > shouldExecuteTransaction', () => {
|
||||
it(`should return false if there's a previous tx pending to be executed`, async () => {
|
||||
// Given
|
||||
const safeInstance = {
|
||||
methods: {
|
||||
getThreshold: () => ({
|
||||
call: () => Promise.resolve('1'),
|
||||
}),
|
||||
},
|
||||
}
|
||||
const safeInstance = getMockedSafeInstance({})
|
||||
safeInstance.methods.getThreshold = () =>
|
||||
({
|
||||
call: () => Promise.resolve('1'),
|
||||
} as NonPayableTransactionObject<string>)
|
||||
|
||||
const nonce = '1'
|
||||
const lastTx = { isExecuted: false } as TxServiceModel
|
||||
|
||||
|
@ -62,13 +63,12 @@ describe('Store actions utils > shouldExecuteTransaction', () => {
|
|||
|
||||
it(`should return false if threshold is greater than 1`, async () => {
|
||||
// Given
|
||||
const safeInstance = {
|
||||
methods: {
|
||||
getThreshold: () => ({
|
||||
call: () => Promise.resolve('2'),
|
||||
}),
|
||||
},
|
||||
}
|
||||
const safeInstance = getMockedSafeInstance({})
|
||||
safeInstance.methods.getThreshold = () =>
|
||||
({
|
||||
call: () => Promise.resolve('2'),
|
||||
} as NonPayableTransactionObject<string>)
|
||||
|
||||
const nonce = '1'
|
||||
const lastTx = { isExecuted: true } as TxServiceModel
|
||||
|
||||
|
@ -81,13 +81,12 @@ describe('Store actions utils > shouldExecuteTransaction', () => {
|
|||
|
||||
it(`should return true is threshold is 1 and previous tx is executed`, async () => {
|
||||
// Given
|
||||
const safeInstance = {
|
||||
methods: {
|
||||
getThreshold: () => ({
|
||||
call: () => Promise.resolve('1'),
|
||||
}),
|
||||
},
|
||||
}
|
||||
const safeInstance = getMockedSafeInstance({ nonce: '1' })
|
||||
safeInstance.methods.getThreshold = () =>
|
||||
({
|
||||
call: () => Promise.resolve('1'),
|
||||
} as NonPayableTransactionObject<string>)
|
||||
|
||||
const nonce = '1'
|
||||
const lastTx = { isExecuted: true } as TxServiceModel
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ export const buildOwnersFrom = (names: string[], addresses: string[]): List<Safe
|
|||
return List(owners)
|
||||
}
|
||||
|
||||
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps, loadedFromStorage = false) => ({
|
||||
export const addOrUpdateSafe = createAction(ADD_OR_UPDATE_SAFE, (safe: SafeRecordProps) => ({
|
||||
safe,
|
||||
loadedFromStorage,
|
||||
}))
|
||||
|
|
|
@ -2,7 +2,6 @@ import { push } from 'connected-react-router'
|
|||
import { ThunkAction } from 'redux-thunk'
|
||||
|
||||
import { onboardUser } from 'src/components/ConnectButton'
|
||||
import { decodeMethods } from 'src/logic/contracts/methodIds'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { getNotificationsFromTxType } from 'src/logic/notifications'
|
||||
import {
|
||||
|
@ -20,16 +19,7 @@ import { providerSelector } from 'src/logic/wallets/store/selectors'
|
|||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
|
||||
import {
|
||||
removeTxFromStore,
|
||||
storeSignedTx,
|
||||
storeExecutedTx,
|
||||
} from 'src/logic/safe/store/actions/transactions/pendingTransactions'
|
||||
import {
|
||||
generateSafeTxHash,
|
||||
mockTransaction,
|
||||
TxToMock,
|
||||
} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { generateSafeTxHash } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
|
||||
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
|
||||
import fetchTransactions from './transactions/fetchTransactions'
|
||||
|
@ -39,6 +29,7 @@ import { PayableTx } from 'src/types/contracts/types.d'
|
|||
import { AppReduxState } from 'src/store'
|
||||
import { Dispatch, DispatchReturn } from './types'
|
||||
import { checkIfOffChainSignatureIsPossible, getPreValidatedSignatures } from 'src/logic/safe/safeTxSigner'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
export interface CreateTransactionArgs {
|
||||
navigateToTransactionsTab?: boolean
|
||||
|
@ -51,6 +42,7 @@ export interface CreateTransactionArgs {
|
|||
txNonce?: number | string
|
||||
valueInWei: string
|
||||
safeTxGas?: number
|
||||
ethParameters?: Pick<TxParameters, 'ethNonce' | 'ethGasLimit' | 'ethGasPriceInGWei'>
|
||||
}
|
||||
|
||||
type CreateTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
|
||||
|
@ -58,7 +50,7 @@ type ConfirmEventHandler = (safeTxHash: string) => void
|
|||
type ErrorEventHandler = () => void
|
||||
export const METAMASK_REJECT_CONFIRM_TX_ERROR_CODE = 4001
|
||||
|
||||
const createTransaction = (
|
||||
export const createTransaction = (
|
||||
{
|
||||
safeAddress,
|
||||
to,
|
||||
|
@ -70,6 +62,7 @@ const createTransaction = (
|
|||
navigateToTransactionsTab = true,
|
||||
origin = null,
|
||||
safeTxGas: safeTxGasArg,
|
||||
ethParameters,
|
||||
}: CreateTransactionArgs,
|
||||
onUserConfirm?: ConfirmEventHandler,
|
||||
onError?: ErrorEventHandler,
|
||||
|
@ -86,7 +79,8 @@ const createTransaction = (
|
|||
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastTx = await getLastTx(safeAddress)
|
||||
const nonce = txNonce ? txNonce.toString() : await getNewTxNonce(lastTx, safeInstance)
|
||||
const nextNonce = await getNewTxNonce(lastTx, safeInstance)
|
||||
const nonce = txNonce ? txNonce.toString() : nextNonce
|
||||
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
|
||||
const safeVersion = await getCurrentSafeVersion(safeInstance)
|
||||
let safeTxGas
|
||||
|
@ -101,8 +95,6 @@ const createTransaction = (
|
|||
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin)
|
||||
const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
|
||||
|
||||
let pendingExecutionKey
|
||||
|
||||
let txHash
|
||||
const txArgs: TxArgs = {
|
||||
safeInstance,
|
||||
|
@ -127,7 +119,6 @@ const createTransaction = (
|
|||
|
||||
if (signature) {
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
|
||||
await saveTxToHistory({ ...txArgs, signature, origin })
|
||||
|
@ -137,64 +128,36 @@ const createTransaction = (
|
|||
}
|
||||
|
||||
const tx = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, safeTxHash)
|
||||
const sendParams: PayableTx = { from, value: 0 }
|
||||
|
||||
// if not set owner management tests will fail on ganache
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
sendParams.gas = '7000000'
|
||||
const sendParams: PayableTx = {
|
||||
from,
|
||||
value: 0,
|
||||
gas: ethParameters?.ethGasLimit,
|
||||
gasPrice: ethParameters?.ethGasPriceInGWei,
|
||||
nonce: ethParameters?.ethNonce,
|
||||
}
|
||||
|
||||
const txToMock: TxToMock = {
|
||||
...txArgs,
|
||||
confirmations: [], // this is used to determine if a tx is pending or not. See `calculateTransactionStatus` helper
|
||||
value: txArgs.valueInWei,
|
||||
safeTxHash,
|
||||
dataDecoded: decodeMethods(txArgs.data),
|
||||
submissionDate: new Date().toISOString(),
|
||||
}
|
||||
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
|
||||
|
||||
await tx
|
||||
.send(sendParams)
|
||||
.once('transactionHash', async (hash) => {
|
||||
onUserConfirm?.(safeTxHash)
|
||||
try {
|
||||
txHash = hash
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
|
||||
txHash = hash
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
await Promise.all([
|
||||
saveTxToHistory({ ...txArgs, txHash, origin }),
|
||||
storeSignedTx({ transaction: mockedTx, from, isExecution, safeAddress, dispatch, state }),
|
||||
])
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
} catch (e) {
|
||||
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
|
||||
}
|
||||
await saveTxToHistory({ ...txArgs, txHash, origin })
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
})
|
||||
.on('error', (error) => {
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
removeTxFromStore(mockedTx, safeAddress, dispatch, state)
|
||||
console.error('Tx error: ', error)
|
||||
|
||||
onError?.()
|
||||
})
|
||||
.then(async (receipt) => {
|
||||
if (pendingExecutionKey) {
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
if (isExecution) {
|
||||
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded))
|
||||
}
|
||||
|
||||
dispatch(
|
||||
enqueueSnackbar(
|
||||
isExecution
|
||||
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
|
||||
: notificationsQueue.afterExecution.moreConfirmationsNeeded,
|
||||
),
|
||||
)
|
||||
|
||||
await storeExecutedTx({ transaction: mockedTx, from, safeAddress, isExecution, receipt, dispatch, state })
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
|
||||
return receipt.transactionHash
|
||||
|
@ -206,10 +169,6 @@ const createTransaction = (
|
|||
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
if (pendingExecutionKey) {
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
}
|
||||
|
||||
dispatch(enqueueSnackbar({ key: err.code, message: errorMsg, options: { persist: true, variant: 'error' } }))
|
||||
|
||||
if (err.code !== METAMASK_REJECT_CONFIRM_TX_ERROR_CODE) {
|
||||
|
@ -223,5 +182,3 @@ const createTransaction = (
|
|||
|
||||
return txHash
|
||||
}
|
||||
|
||||
export default createTransaction
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import axios, { AxiosResponse } from 'axios'
|
||||
import { createAction } from 'redux-actions'
|
||||
|
||||
import { getTxDetailsUrl } from 'src/config'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types'
|
||||
import { ExpandedTxDetails, Transaction, TxLocation } from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import { TransactionDetailsPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { getTransactionDetails } from 'src/logic/safe/store/selectors/gatewayTransactions'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const UPDATE_TRANSACTION_DETAILS = 'UPDATE_TRANSACTION_DETAILS'
|
||||
const updateTransactionDetails = createAction<TransactionDetailsPayload>(UPDATE_TRANSACTION_DETAILS)
|
||||
|
||||
export const fetchTransactionDetails = ({
|
||||
transactionId,
|
||||
txLocation,
|
||||
}: {
|
||||
transactionId: Transaction['id']
|
||||
txLocation: TxLocation
|
||||
}) => async (dispatch: Dispatch, getState: () => AppReduxState): Promise<Transaction['txDetails']> => {
|
||||
const txDetails = getTransactionDetails(getState())({
|
||||
attributeValue: transactionId,
|
||||
attributeName: 'id',
|
||||
txLocation,
|
||||
})
|
||||
const safeAddress = safeParamAddressFromStateSelector(getState())
|
||||
|
||||
if (txDetails) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const url = getTxDetailsUrl(transactionId)
|
||||
const { data: transactionDetails } = await axios.get<ExpandedTxDetails, AxiosResponse<ExpandedTxDetails>>(url)
|
||||
|
||||
dispatch(updateTransactionDetails({ transactionId, txLocation, safeAddress, value: transactionDetails }))
|
||||
} catch (error) {
|
||||
console.error(`Failed to retrieve transaction ${transactionId} details`, error.message)
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> =>
|
|||
|
||||
if (safes) {
|
||||
Object.values(safes).forEach((safeProps) => {
|
||||
dispatch(addOrUpdateSafe(buildSafe(safeProps), true))
|
||||
dispatch(addOrUpdateSafe(buildSafe(safeProps)))
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { List } from 'immutable'
|
||||
import { AnyAction } from 'redux'
|
||||
import { ThunkAction } from 'redux-thunk'
|
||||
|
||||
|
@ -17,21 +18,40 @@ import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackb
|
|||
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
|
||||
import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
|
||||
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
|
||||
import { mockTransaction, TxToMock } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
|
||||
import { storeExecutedTx, storeSignedTx, storeTx } from 'src/logic/safe/store/actions/transactions/pendingTransactions'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
import { Dispatch, DispatchReturn } from './types'
|
||||
import { PayableTx } from 'src/types/contracts/types'
|
||||
|
||||
import { updateTransactionStatus } from 'src/logic/safe/store/actions/updateTransactionStatus'
|
||||
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||
import { Operation } from 'src/logic/safe/store/models/types/gateway.d'
|
||||
|
||||
interface ProcessTransactionArgs {
|
||||
approveAndExecute: boolean
|
||||
notifiedTransaction: string
|
||||
safeAddress: string
|
||||
tx: Transaction
|
||||
tx: {
|
||||
id: string
|
||||
confirmations: List<Confirmation>
|
||||
origin: string // json.stringified url, name
|
||||
to: string
|
||||
value: string
|
||||
data: string
|
||||
operation: Operation
|
||||
nonce: number
|
||||
safeTxGas: number
|
||||
safeTxHash: string
|
||||
baseGas: number
|
||||
gasPrice: string
|
||||
gasToken: string
|
||||
refundReceiver: string
|
||||
}
|
||||
userAddress: string
|
||||
ethParameters?: Pick<TxParameters, 'ethNonce' | 'ethGasLimit' | 'ethGasPriceInGWei'>
|
||||
thresholdReached: boolean
|
||||
}
|
||||
|
||||
|
@ -43,6 +63,7 @@ export const processTransaction = ({
|
|||
safeAddress,
|
||||
tx,
|
||||
userAddress,
|
||||
ethParameters,
|
||||
thresholdReached,
|
||||
}: ProcessTransactionArgs): ProcessTransactionAction => async (
|
||||
dispatch: Dispatch,
|
||||
|
@ -67,14 +88,13 @@ export const processTransaction = ({
|
|||
|
||||
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, tx.origin)
|
||||
const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
|
||||
let pendingExecutionKey
|
||||
|
||||
let txHash
|
||||
let transaction
|
||||
const txArgs = {
|
||||
...tx.toJS(), // merge the previous tx with new data
|
||||
...tx, // merge the previous tx with new data
|
||||
safeInstance,
|
||||
to: tx.recipient,
|
||||
to: tx.to,
|
||||
valueInWei: tx.value,
|
||||
data: tx.data ?? EMPTY_DATA,
|
||||
operation: tx.operation,
|
||||
|
@ -95,10 +115,10 @@ export const processTransaction = ({
|
|||
if (signature) {
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
dispatch(updateTransactionStatus({ txStatus: 'PENDING', safeAddress, nonce: tx.nonce, id: tx.id }))
|
||||
await saveTxToHistory({ ...txArgs, signature })
|
||||
// TODO: while we wait for the tx to be stored in the service and later update the tx info
|
||||
// we should update the tx status in the store to disable owners' action buttons
|
||||
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
return
|
||||
|
@ -107,59 +127,55 @@ export const processTransaction = ({
|
|||
|
||||
transaction = isExecution ? getExecutionTransaction(txArgs) : getApprovalTransaction(safeInstance, tx.safeTxHash)
|
||||
|
||||
const sendParams: any = { from, value: 0 }
|
||||
|
||||
// if not set owner management tests will fail on ganache
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
sendParams.gas = '7000000'
|
||||
const sendParams: PayableTx = {
|
||||
from,
|
||||
value: 0,
|
||||
gas: ethParameters?.ethGasLimit,
|
||||
gasPrice: ethParameters?.ethGasPriceInGWei,
|
||||
nonce: ethParameters?.ethNonce,
|
||||
}
|
||||
|
||||
const txToMock: TxToMock = {
|
||||
...txArgs,
|
||||
value: txArgs.valueInWei,
|
||||
}
|
||||
const mockedTx = await mockTransaction(txToMock, safeAddress, state)
|
||||
|
||||
await transaction
|
||||
.send(sendParams)
|
||||
.once('transactionHash', async (hash: string) => {
|
||||
txHash = hash
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
|
||||
dispatch(
|
||||
updateTransactionStatus({
|
||||
txStatus: 'PENDING',
|
||||
safeAddress,
|
||||
nonce: tx.nonce,
|
||||
// if we provide the tx ID that sole tx will have the _pending_ status.
|
||||
// if not, all the txs that share the same nonce will have the _pending_ status.
|
||||
id: tx.id,
|
||||
}),
|
||||
)
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
saveTxToHistory({ ...txArgs, txHash }),
|
||||
storeSignedTx({ transaction: mockedTx, from, isExecution, safeAddress, dispatch, state }),
|
||||
])
|
||||
await saveTxToHistory({ ...txArgs, txHash })
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
} catch (e) {
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
await storeTx({ transaction: tx, safeAddress, dispatch, state })
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
.on('error', (error) => {
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
storeTx({ transaction: tx, safeAddress, dispatch, state })
|
||||
dispatch(
|
||||
updateTransactionStatus({
|
||||
txStatus: 'PENDING_FAILED',
|
||||
safeAddress,
|
||||
nonce: tx.nonce,
|
||||
id: tx.id,
|
||||
}),
|
||||
)
|
||||
|
||||
console.error('Processing transaction error: ', error)
|
||||
})
|
||||
.then(async (receipt) => {
|
||||
if (pendingExecutionKey) {
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
if (isExecution) {
|
||||
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded))
|
||||
}
|
||||
|
||||
dispatch(
|
||||
enqueueSnackbar(
|
||||
isExecution
|
||||
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
|
||||
: notificationsQueue.afterExecution.moreConfirmationsNeeded,
|
||||
),
|
||||
)
|
||||
|
||||
await storeExecutedTx({ transaction: mockedTx, from, safeAddress, isExecution, receipt, dispatch, state })
|
||||
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
|
||||
if (isExecution) {
|
||||
|
@ -175,10 +191,14 @@ export const processTransaction = ({
|
|||
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
if (pendingExecutionKey) {
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
}
|
||||
|
||||
dispatch(
|
||||
updateTransactionStatus({
|
||||
txStatus: 'PENDING_FAILED',
|
||||
safeAddress,
|
||||
nonce: tx.nonce,
|
||||
id: tx.id,
|
||||
}),
|
||||
)
|
||||
dispatch(enqueueSnackbar({ key: err.code, message: errorMsg, options: { persist: true, variant: 'error' } }))
|
||||
|
||||
if (txHash) {
|
||||
|
|
|
@ -1,58 +1,35 @@
|
|||
import { batch } from 'react-redux'
|
||||
import { ThunkAction, ThunkDispatch } from 'redux-thunk'
|
||||
import { ThunkDispatch } from 'redux-thunk'
|
||||
import { AnyAction } from 'redux'
|
||||
import { backOff } from 'exponential-backoff'
|
||||
|
||||
import { addIncomingTransactions } from 'src/logic/safe/store/actions/addIncomingTransactions'
|
||||
import { addModuleTransactions } from 'src/logic/safe/store/actions/addModuleTransactions'
|
||||
|
||||
import { loadIncomingTransactions } from './loadIncomingTransactions'
|
||||
import { loadModuleTransactions } from './loadModuleTransactions'
|
||||
import { loadOutgoingTransactions } from './loadOutgoingTransactions'
|
||||
|
||||
import { addOrUpdateCancellationTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
|
||||
import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
|
||||
import {
|
||||
addHistoryTransactions,
|
||||
addQueuedTransactions,
|
||||
} from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
|
||||
import { loadHistoryTransactions, loadQueuedTransactions } from './loadGatewayTransactions'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
const noFunc = () => {}
|
||||
|
||||
export default (safeAddress: string): ThunkAction<Promise<void>, AppReduxState, undefined, AnyAction> => async (
|
||||
export default (safeAddress: string) => async (
|
||||
dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const transactions = await backOff(() => loadOutgoingTransactions(safeAddress))
|
||||
const [history, queued] = await Promise.allSettled([
|
||||
loadHistoryTransactions(safeAddress),
|
||||
loadQueuedTransactions(safeAddress),
|
||||
])
|
||||
|
||||
if (transactions) {
|
||||
const { cancel, outgoing } = transactions
|
||||
const updateCancellationTxs = cancel.size
|
||||
? addOrUpdateCancellationTransactions({ safeAddress, transactions: cancel })
|
||||
: noFunc
|
||||
const updateOutgoingTxs = outgoing.size
|
||||
? addOrUpdateTransactions({
|
||||
safeAddress,
|
||||
transactions: outgoing,
|
||||
})
|
||||
: noFunc
|
||||
if (history.status === 'fulfilled') {
|
||||
const values = history.value
|
||||
|
||||
batch(() => {
|
||||
dispatch(updateCancellationTxs)
|
||||
dispatch(updateOutgoingTxs)
|
||||
})
|
||||
if (values.length) {
|
||||
dispatch(addHistoryTransactions({ safeAddress, values }))
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to load history transactions', history.reason)
|
||||
}
|
||||
|
||||
const incomingTransactions = await loadIncomingTransactions(safeAddress)
|
||||
const safeIncomingTxs = incomingTransactions.get(safeAddress)
|
||||
|
||||
if (safeIncomingTxs?.size) {
|
||||
dispatch(addIncomingTransactions(incomingTransactions))
|
||||
}
|
||||
|
||||
const moduleTransactions = await loadModuleTransactions(safeAddress)
|
||||
|
||||
if (moduleTransactions.length) {
|
||||
dispatch(addModuleTransactions({ modules: moduleTransactions, safeAddress }))
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error fetching transactions:', error)
|
||||
if (queued.status === 'fulfilled') {
|
||||
const values = queued.value
|
||||
dispatch(addQueuedTransactions({ safeAddress, values }))
|
||||
} else {
|
||||
console.error('Failed to load queued transactions', queued.reason)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import axios, { AxiosResponse } from 'axios'
|
||||
|
||||
import { getSafeClientGatewayBaseUrl } from 'src/config'
|
||||
import { HistoryGatewayResponse, QueuedGatewayResponse } from 'src/logic/safe/store/models/types/gateway'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
|
||||
/*************/
|
||||
/* HISTORY */
|
||||
/*************/
|
||||
const getHistoryTransactionsUrl = (safeAddress: string): string => {
|
||||
const address = checksumAddress(safeAddress)
|
||||
return `${getSafeClientGatewayBaseUrl(address)}/transactions/history/`
|
||||
}
|
||||
|
||||
const historyPointers: { [safeAddress: string]: { next: string | null; previous: string | null } } = {}
|
||||
|
||||
/**
|
||||
* Fetch next page if there is a next pointer for the safeAddress.
|
||||
* If the fetch was success, updates the pointers.
|
||||
* @param {string} safeAddress
|
||||
*/
|
||||
export const loadPagedHistoryTransactions = async (
|
||||
safeAddress: string,
|
||||
): Promise<{ values: HistoryGatewayResponse['results']; next: string | null } | undefined> => {
|
||||
// if `historyPointers[safeAddress] is `undefined` it means `loadHistoryTransactions` wasn't called
|
||||
// if `historyPointers[safeAddress].next is `null`, it means it reached the last page in gateway-client
|
||||
if (!historyPointers[safeAddress]?.next) {
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
data: { results, ...pointers },
|
||||
} = await axios.get<HistoryGatewayResponse, AxiosResponse<HistoryGatewayResponse>>(
|
||||
historyPointers[safeAddress].next as string,
|
||||
)
|
||||
|
||||
historyPointers[safeAddress] = pointers
|
||||
|
||||
return { values: results, next: historyPointers[safeAddress].next }
|
||||
}
|
||||
|
||||
export const loadHistoryTransactions = async (safeAddress: string): Promise<HistoryGatewayResponse['results']> => {
|
||||
const historyTransactionsUrl = getHistoryTransactionsUrl(safeAddress)
|
||||
|
||||
const {
|
||||
data: { results, ...pointers },
|
||||
} = await axios.get<HistoryGatewayResponse, AxiosResponse<HistoryGatewayResponse>>(historyTransactionsUrl)
|
||||
|
||||
if (!historyPointers[safeAddress]) {
|
||||
historyPointers[safeAddress] = pointers
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/************/
|
||||
/* QUEUED */
|
||||
/************/
|
||||
const getQueuedTransactionsUrl = (safeAddress: string): string => {
|
||||
const address = checksumAddress(safeAddress)
|
||||
return `${getSafeClientGatewayBaseUrl(address)}/transactions/queued/`
|
||||
}
|
||||
|
||||
const queuedPointers: { [safeAddress: string]: { next: string | null; previous: string | null } } = {}
|
||||
|
||||
/**
|
||||
* Fetch next page if there is a next pointer for the safeAddress.
|
||||
* If the fetch was success, updates the pointers.
|
||||
* @param {string} safeAddress
|
||||
*/
|
||||
export const loadPagedQueuedTransactions = async (
|
||||
safeAddress: string,
|
||||
): Promise<{ values: QueuedGatewayResponse['results']; next: string | null } | undefined> => {
|
||||
// if `queuedPointers[safeAddress] is `undefined` it means `loadHistoryTransactions` wasn't called
|
||||
// if `queuedPointers[safeAddress].next is `null`, it means it reached the last page in gateway-client
|
||||
if (!queuedPointers[safeAddress]?.next) {
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
data: { results, ...pointers },
|
||||
} = await axios.get<QueuedGatewayResponse, AxiosResponse<QueuedGatewayResponse>>(
|
||||
queuedPointers[safeAddress].next as string,
|
||||
)
|
||||
|
||||
queuedPointers[safeAddress] = pointers
|
||||
|
||||
return { values: results, next: queuedPointers[safeAddress].next }
|
||||
}
|
||||
|
||||
export const loadQueuedTransactions = async (safeAddress: string): Promise<QueuedGatewayResponse['results']> => {
|
||||
const queuedTransactionsUrl = getQueuedTransactionsUrl(safeAddress)
|
||||
|
||||
const {
|
||||
data: { results, ...pointers },
|
||||
} = await axios.get<QueuedGatewayResponse, AxiosResponse<QueuedGatewayResponse>>(queuedTransactionsUrl)
|
||||
|
||||
if (!queuedPointers[safeAddress] || queuedPointers[safeAddress].next === null) {
|
||||
queuedPointers[safeAddress] = pointers
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { fromJS, List, Map } from 'immutable'
|
||||
|
||||
import generateBatchRequests from 'src/logic/contracts/generateBatchRequests'
|
||||
import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions'
|
||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
|
||||
import { buildTx, isCancelTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
|
@ -57,8 +58,8 @@ export type SafeTransactionsType = {
|
|||
}
|
||||
|
||||
export type OutgoingTxs = {
|
||||
cancellationTxs: Record<number, TxServiceModel>
|
||||
outgoingTxs: TxServiceModel[]
|
||||
cancellationTxs: Record<number, TxServiceModel> | CancellationTransactions
|
||||
outgoingTxs: TxServiceModel[] | List<Transaction>
|
||||
}
|
||||
|
||||
export type BatchProcessTxsProps = OutgoingTxs & {
|
||||
|
@ -94,21 +95,25 @@ const extractCancelAndOutgoingTxs = (safeAddress: string, outgoingTxs: TxService
|
|||
)
|
||||
}
|
||||
|
||||
type BatchRequestReturnValues = [TxServiceModel, string | undefined]
|
||||
type BatchRequestReturnValues = [TxServiceModel | Transaction, string | undefined]
|
||||
|
||||
/**
|
||||
* Requests Contract's code for all the Contracts the Safe has interacted with
|
||||
* @param transactions
|
||||
* @returns {Promise<[Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>, Promise<*[]>]>}
|
||||
*/
|
||||
const batchRequestContractCode = (transactions: TxServiceModel[]): Promise<BatchRequestReturnValues[]> => {
|
||||
const batchRequestContractCode = (
|
||||
transactions: (TxServiceModel | Transaction)[],
|
||||
): Promise<BatchRequestReturnValues[]> => {
|
||||
if (!transactions || !Array.isArray(transactions)) {
|
||||
throw new Error('`transactions` must be provided in order to lookup information')
|
||||
}
|
||||
|
||||
const batch = new web3ReadOnly.BatchRequest()
|
||||
|
||||
const whenTxsValues = transactions.map((tx) => {
|
||||
// this will no longer be used when txs-list-v2 feature is finished
|
||||
// that's why I'm doing this to move forward
|
||||
const whenTxsValues = (transactions as any[]).map((tx) => {
|
||||
return generateBatchRequests<BatchRequestReturnValues>({
|
||||
abi: [],
|
||||
address: tx.to,
|
||||
|
@ -142,8 +147,8 @@ const batchProcessOutgoingTransactions = async ({
|
|||
outgoing: Transaction[]
|
||||
}> => {
|
||||
// cancellation transactions
|
||||
const cancelTxsValues = Object.values(cancellationTxs)
|
||||
const cancellationTxsWithData = cancelTxsValues.length ? await batchRequestContractCode(cancelTxsValues) : []
|
||||
const cancelTxsValues = List(Object.values(cancellationTxs))
|
||||
const cancellationTxsWithData = cancelTxsValues.size ? await batchRequestContractCode(cancelTxsValues.toArray()) : []
|
||||
|
||||
const cancel = {}
|
||||
for (const [tx] of cancellationTxsWithData) {
|
||||
|
@ -157,7 +162,11 @@ const batchProcessOutgoingTransactions = async ({
|
|||
}
|
||||
|
||||
// outgoing transactions
|
||||
const outgoingTxsWithData = outgoingTxs.length ? await batchRequestContractCode(outgoingTxs) : []
|
||||
const outgoingTxsList: List<Transaction | TxServiceModel> =
|
||||
(outgoingTxs as TxServiceModel[]).length !== undefined
|
||||
? List(outgoingTxs as TxServiceModel[])
|
||||
: (outgoingTxs as List<Transaction>)
|
||||
const outgoingTxsWithData = outgoingTxsList.size ? await batchRequestContractCode(outgoingTxsList.toArray()) : []
|
||||
|
||||
const outgoing: Transaction[] = []
|
||||
for (const [tx] of outgoingTxsWithData) {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
import { HistoryPayload, QueuedPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'
|
||||
|
||||
export const ADD_HISTORY_TRANSACTIONS = 'ADD_HISTORY_TRANSACTIONS'
|
||||
export const addHistoryTransactions = createAction<HistoryPayload>(ADD_HISTORY_TRANSACTIONS)
|
||||
|
||||
export const ADD_QUEUED_TRANSACTIONS = 'ADD_QUEUED_TRANSACTIONS'
|
||||
export const addQueuedTransactions = createAction<QueuedPayload>(ADD_QUEUED_TRANSACTIONS)
|
|
@ -15,6 +15,7 @@ import {
|
|||
TransactionTypeValues,
|
||||
TxArgs,
|
||||
RefundParams,
|
||||
isStoredTransaction,
|
||||
} from 'src/logic/safe/store/models/types/transaction'
|
||||
import { AppReduxState, store } from 'src/store'
|
||||
import {
|
||||
|
@ -30,14 +31,14 @@ import {
|
|||
import { TypedDataUtils } from 'eth-sig-util'
|
||||
import { ProviderRecord } from 'src/logic/wallets/store/model/provider'
|
||||
import { SafeRecord } from 'src/logic/safe/store/models/safe'
|
||||
import { DataDecoded, DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
import { CALL } from 'src/logic/safe/transactions'
|
||||
|
||||
export const isEmptyData = (data?: string | null): boolean => {
|
||||
return !data || data === EMPTY_DATA
|
||||
}
|
||||
|
||||
export const isInnerTransaction = (tx: TxServiceModel | Transaction, safeAddress: string): boolean => {
|
||||
export const isInnerTransaction = (tx: BuildTx['tx'] | Transaction, safeAddress: string): boolean => {
|
||||
let isSameAddress = false
|
||||
|
||||
if ((tx as TxServiceModel).to !== undefined) {
|
||||
|
@ -49,9 +50,15 @@ export const isInnerTransaction = (tx: TxServiceModel | Transaction, safeAddress
|
|||
return isSameAddress && Number(tx.value) === 0
|
||||
}
|
||||
|
||||
export const isCancelTransaction = (tx: TxServiceModel, safeAddress: string): boolean => {
|
||||
if (!sameAddress(tx.to, safeAddress)) {
|
||||
return false
|
||||
export const isCancelTransaction = (tx: BuildTx['tx'], safeAddress: string): boolean => {
|
||||
if (isStoredTransaction(tx)) {
|
||||
if (!sameAddress(tx.recipient, safeAddress)) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if (!sameAddress(tx.to, safeAddress)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (Number(tx.value)) {
|
||||
|
@ -89,15 +96,15 @@ export const isPendingTransaction = (tx: Transaction, cancelTx: Transaction): bo
|
|||
return (!!cancelTx && cancelTx.status === 'pending') || tx.status === 'pending'
|
||||
}
|
||||
|
||||
export const isModifySettingsTransaction = (tx: TxServiceModel, safeAddress: string): boolean => {
|
||||
export const isModifySettingsTransaction = (tx: BuildTx['tx'], safeAddress: string): boolean => {
|
||||
return isInnerTransaction(tx, safeAddress) && !isEmptyData(tx.data)
|
||||
}
|
||||
|
||||
export const isMultiSendTransaction = (tx: TxServiceModel): boolean => {
|
||||
export const isMultiSendTransaction = (tx: BuildTx['tx']): boolean => {
|
||||
return !isEmptyData(tx.data) && tx.data?.substring(0, 10) === '0x8d80ff0a' && Number(tx.value) === 0
|
||||
}
|
||||
|
||||
export const isUpgradeTransaction = (tx: TxServiceModel): boolean => {
|
||||
export const isUpgradeTransaction = (tx: BuildTx['tx']): boolean => {
|
||||
return (
|
||||
!isEmptyData(tx.data) &&
|
||||
isMultiSendTransaction(tx) &&
|
||||
|
@ -106,11 +113,11 @@ export const isUpgradeTransaction = (tx: TxServiceModel): boolean => {
|
|||
)
|
||||
}
|
||||
|
||||
export const isOutgoingTransaction = (tx: TxServiceModel, safeAddress: string): boolean => {
|
||||
return !sameAddress(tx.to, safeAddress) && !isEmptyData(tx.data)
|
||||
export const isOutgoingTransaction = (tx: BuildTx['tx'], safeAddress?: string): boolean => {
|
||||
return !sameAddress((tx as ServiceTx).to, safeAddress) && !isEmptyData(tx.data)
|
||||
}
|
||||
|
||||
export const isCustomTransaction = async (tx: TxServiceModel, safeAddress: string): Promise<boolean> => {
|
||||
export const isCustomTransaction = async (tx: BuildTx['tx'], safeAddress?: string): Promise<boolean> => {
|
||||
const isOutgoing = isOutgoingTransaction(tx, safeAddress)
|
||||
const isErc20 = await isSendERC20Transaction(tx)
|
||||
const isUpgrade = isUpgradeTransaction(tx)
|
||||
|
@ -120,7 +127,7 @@ export const isCustomTransaction = async (tx: TxServiceModel, safeAddress: strin
|
|||
}
|
||||
|
||||
export const getRefundParams = async (
|
||||
tx: TxServiceModel,
|
||||
tx: BuildTx['tx'],
|
||||
tokenInfo: (string) => Promise<{ decimals: number; symbol: string } | null>,
|
||||
): Promise<RefundParams | null> => {
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
@ -155,7 +162,7 @@ export const getRefundParams = async (
|
|||
return refundParams
|
||||
}
|
||||
|
||||
export const getDecodedParams = (tx: TxServiceModel): DecodedParams | null => {
|
||||
export const getDecodedParams = (tx: BuildTx['tx']): DecodedParams | null => {
|
||||
if (tx.dataDecoded) {
|
||||
return {
|
||||
[tx.dataDecoded.method]: tx.dataDecoded.parameters.reduce(
|
||||
|
@ -170,22 +177,22 @@ export const getDecodedParams = (tx: TxServiceModel): DecodedParams | null => {
|
|||
return null
|
||||
}
|
||||
|
||||
export const getConfirmations = (tx: TxServiceModel): List<Confirmation> => {
|
||||
export const getConfirmations = (tx: BuildTx['tx']): List<Confirmation> => {
|
||||
return List(
|
||||
tx.confirmations.map((conf) =>
|
||||
(tx.confirmations as ServiceTx['confirmations'])?.map((conf) =>
|
||||
makeConfirmation({
|
||||
owner: conf.owner,
|
||||
hash: conf.transactionHash,
|
||||
signature: conf.signature,
|
||||
}),
|
||||
),
|
||||
) ?? [],
|
||||
)
|
||||
}
|
||||
|
||||
export const isTransactionCancelled = (
|
||||
tx: TxServiceModel,
|
||||
outgoingTxs: Array<TxServiceModel>,
|
||||
cancellationTxs: Record<string, TxServiceModel>,
|
||||
tx: BuildTx['tx'],
|
||||
outgoingTxs: BuildTx['outgoingTxs'],
|
||||
cancellationTxs: BuildTx['cancellationTxs'],
|
||||
): boolean => {
|
||||
return (
|
||||
// not executed
|
||||
|
@ -252,8 +259,10 @@ export const calculateTransactionType = (tx: Transaction): TransactionTypeValues
|
|||
return txType
|
||||
}
|
||||
|
||||
export type ServiceTx = TxServiceModel | TxToMock
|
||||
|
||||
export type BuildTx = BatchProcessTxsProps & {
|
||||
tx: TxServiceModel
|
||||
tx: ServiceTx | Transaction
|
||||
}
|
||||
|
||||
export const buildTx = async ({
|
||||
|
@ -281,19 +290,19 @@ export const buildTx = async ({
|
|||
let tokenSymbol = nativeCoin.symbol
|
||||
try {
|
||||
if (isSendERC20Tx) {
|
||||
const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.to)
|
||||
const { decimals, symbol } = await getERC20DecimalsAndSymbol((tx as ServiceTx).to)
|
||||
tokenDecimals = decimals
|
||||
tokenSymbol = symbol
|
||||
} else if (isSendERC721Tx) {
|
||||
tokenSymbol = await getERC721Symbol(tx.to)
|
||||
tokenSymbol = await getERC721Symbol((tx as ServiceTx).to)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Failed to retrieve token data from ${tx.to}`)
|
||||
console.log(`Failed to retrieve token data from ${(tx as ServiceTx).to}`)
|
||||
}
|
||||
|
||||
const txToStore = makeTransaction({
|
||||
baseGas: tx.baseGas,
|
||||
blockNumber: tx.blockNumber,
|
||||
blockNumber: (tx as ServiceTx).blockNumber,
|
||||
cancelled: isTxCancelled,
|
||||
confirmations,
|
||||
customTx: isCustomTx,
|
||||
|
@ -301,23 +310,23 @@ export const buildTx = async ({
|
|||
dataDecoded: tx.dataDecoded,
|
||||
decimals: tokenDecimals,
|
||||
decodedParams,
|
||||
executionDate: tx.executionDate,
|
||||
executionTxHash: tx.transactionHash,
|
||||
executor: tx.executor,
|
||||
fee: tx.fee,
|
||||
executionDate: (tx as ServiceTx).executionDate,
|
||||
executionTxHash: (tx as ServiceTx).transactionHash,
|
||||
executor: (tx as ServiceTx).executor,
|
||||
fee: (tx as ServiceTx).fee,
|
||||
gasPrice: tx.gasPrice,
|
||||
gasToken: tx.gasToken || ZERO_ADDRESS,
|
||||
isCancellationTx,
|
||||
isCollectibleTransfer: isSendERC721Tx,
|
||||
isExecuted: tx.isExecuted,
|
||||
isSuccessful: tx.isSuccessful,
|
||||
isSuccessful: (tx as ServiceTx).isSuccessful,
|
||||
isTokenTransfer: isSendERC20Tx,
|
||||
modifySettingsTx: isModifySettingsTx,
|
||||
multiSendTx: isMultiSendTx,
|
||||
nonce: tx.nonce,
|
||||
operation: tx.operation,
|
||||
origin: tx.origin,
|
||||
recipient: tx.to,
|
||||
origin: (tx as ServiceTx).origin,
|
||||
recipient: (tx as ServiceTx).to,
|
||||
refundParams,
|
||||
refundReceiver: tx.refundReceiver || ZERO_ADDRESS,
|
||||
safeTxGas: tx.safeTxGas,
|
||||
|
@ -325,7 +334,7 @@ export const buildTx = async ({
|
|||
submissionDate: tx.submissionDate,
|
||||
symbol: tokenSymbol,
|
||||
upgradeTx: isUpgradeTx,
|
||||
value: tx.value.toString(),
|
||||
value: tx.value?.toString(),
|
||||
})
|
||||
|
||||
return txToStore
|
||||
|
@ -333,13 +342,7 @@ export const buildTx = async ({
|
|||
.set('type', calculateTransactionType(txToStore))
|
||||
}
|
||||
|
||||
export type TxToMock = TxArgs & {
|
||||
confirmations: []
|
||||
safeTxHash: string
|
||||
value: string
|
||||
submissionDate: string
|
||||
dataDecoded: DataDecoded | null
|
||||
}
|
||||
export type TxToMock = TxArgs & Partial<TxServiceModel>
|
||||
|
||||
export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise<Transaction> => {
|
||||
const safe = safeSelector(state)
|
||||
|
@ -355,7 +358,7 @@ export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppRed
|
|||
currentUser: undefined,
|
||||
outgoingTxs,
|
||||
safe,
|
||||
tx: (tx as unknown) as TxServiceModel,
|
||||
tx,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -369,7 +372,7 @@ export const updateStoredTransactionsStatus = (dispatch: (any) => void, walletRe
|
|||
dispatch(
|
||||
addOrUpdateTransactions({
|
||||
safeAddress,
|
||||
transactions: transactions.withMutations((list: any[]) =>
|
||||
transactions: transactions.withMutations((list) =>
|
||||
list.map((tx) => tx.set('status', calculateTransactionStatus(tx, safe, walletRecord.account))),
|
||||
),
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
import { TransactionStatusPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'
|
||||
|
||||
export const UPDATE_TRANSACTION_STATUS = 'UPDATE_TRANSACTION_STATUS'
|
||||
export const updateTransactionStatus = createAction<TransactionStatusPayload>(UPDATE_TRANSACTION_STATUS)
|
|
@ -26,17 +26,31 @@ export const shouldExecuteTransaction = async (
|
|||
nonce: string,
|
||||
lastTx: TxServiceModel | null,
|
||||
): Promise<boolean> => {
|
||||
const threshold = await safeInstance.methods.getThreshold().call()
|
||||
const safeNonce = (await safeInstance.methods.nonce().call()).toString()
|
||||
const thresholdAsString = await safeInstance.methods.getThreshold().call()
|
||||
const threshold = Number(thresholdAsString)
|
||||
|
||||
// Tx will automatically be executed if and only if the threshold is 1
|
||||
if (Number.parseInt(threshold) === 1) {
|
||||
const isFirstTransaction = Number.parseInt(nonce) === 0
|
||||
// if the previous tx is not executed, it's delayed using the approval mechanisms,
|
||||
// once the previous tx is executed, the current tx will be available to be executed
|
||||
// by the user using the exec button.
|
||||
const canExecuteCurrentTransaction = lastTx && lastTx.isExecuted
|
||||
// Needs to collect owners signatures
|
||||
if (threshold > 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isFirstTransaction || !!canExecuteCurrentTransaction
|
||||
// Allow first tx.
|
||||
if (Number(nonce) === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Allow if nonce === safeNonce and threshold === 1
|
||||
if (nonce === safeNonce) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the previous tx is not executed or the different between lastTx.nonce and nonce is > 1
|
||||
// it's delayed using the approval mechanisms.
|
||||
// Once the previous tx is executed, the current tx will be available to be executed
|
||||
// by the user using the exec button.
|
||||
if (lastTx) {
|
||||
return lastTx.isExecuted && lastTx.nonce + 1 === Number(nonce)
|
||||
}
|
||||
|
||||
return false
|
||||
|
|
|
@ -3,14 +3,17 @@ import { push } from 'connected-react-router'
|
|||
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
|
||||
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
|
||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||
import { getAwaitingTransactions } from 'src/logic/safe/transactions/awaitingTransactions'
|
||||
import {
|
||||
getAwaitingTransactions,
|
||||
getAwaitingGatewayTransactions,
|
||||
} from 'src/logic/safe/transactions/awaitingTransactions'
|
||||
import { getSafeVersionInfo } from 'src/logic/safe/utils/safeVersion'
|
||||
import { isUserAnOwner } from 'src/logic/wallets/ethAddresses'
|
||||
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { getIncomingTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions'
|
||||
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
|
||||
import { ADD_QUEUED_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
|
||||
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
||||
import {
|
||||
safeParamAddressFromStateSelector,
|
||||
|
@ -18,10 +21,16 @@ import {
|
|||
safeCancellationTransactionsSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
|
||||
import { isTransactionSummary } from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { ADD_OR_UPDATE_SAFE } from '../actions/addOrUpdateSafe'
|
||||
|
||||
const watchedActions = [ADD_OR_UPDATE_TRANSACTIONS, ADD_INCOMING_TRANSACTIONS, ADD_OR_UPDATE_SAFE]
|
||||
const watchedActions = [
|
||||
ADD_OR_UPDATE_TRANSACTIONS,
|
||||
ADD_INCOMING_TRANSACTIONS,
|
||||
ADD_OR_UPDATE_SAFE,
|
||||
ADD_QUEUED_TRANSACTIONS,
|
||||
]
|
||||
|
||||
const sendAwaitingTransactionNotification = async (
|
||||
dispatch,
|
||||
|
@ -34,7 +43,7 @@ const sendAwaitingTransactionNotification = async (
|
|||
if (!dispatch || !safeAddress || !awaitingTxsSubmissionDateList || !notificationKey) {
|
||||
return
|
||||
}
|
||||
if (awaitingTxsSubmissionDateList.size === 0) {
|
||||
if (awaitingTxsSubmissionDateList.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -48,7 +57,7 @@ const sendAwaitingTransactionNotification = async (
|
|||
return lastTimeUserLoggedIn ? new Date(submissionDate) > new Date(lastTimeUserLoggedIn) : true
|
||||
})
|
||||
|
||||
if (filteredDuplicatedAwaitingTxList.size === 0) {
|
||||
if (filteredDuplicatedAwaitingTxList.length === 0) {
|
||||
return
|
||||
}
|
||||
dispatch(
|
||||
|
@ -101,40 +110,39 @@ const notificationsMiddleware = (store) => (next) => async (action) => {
|
|||
|
||||
break
|
||||
}
|
||||
case ADD_QUEUED_TRANSACTIONS: {
|
||||
const { safeAddress, values } = action.payload
|
||||
const transactions = values.filter((tx) => isTransactionSummary(tx)).map((item) => item.transaction)
|
||||
const userAddress: string = userAccountSelector(state)
|
||||
const awaitingTransactions = getAwaitingGatewayTransactions(transactions, userAddress)
|
||||
|
||||
const awaitingTxsSubmissionDateList = awaitingTransactions.map((tx) => tx.timestamp)
|
||||
|
||||
const safes = safesMapSelector(state)
|
||||
const currentSafe = safes.get(safeAddress)
|
||||
|
||||
if (!currentSafe || !isUserAnOwner(currentSafe, userAddress) || awaitingTransactions.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const notificationKey = `${safeAddress}-awaiting`
|
||||
|
||||
await sendAwaitingTransactionNotification(
|
||||
dispatch,
|
||||
safeAddress,
|
||||
awaitingTxsSubmissionDateList,
|
||||
notificationKey,
|
||||
onNotificationClicked(dispatch, notificationKey, safeAddress),
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
case ADD_INCOMING_TRANSACTIONS: {
|
||||
action.payload.forEach((incomingTransactions, safeAddress) => {
|
||||
const { latestIncomingTxBlock } = state.safes.get('safes').get(safeAddress, {})
|
||||
const viewedSafes = state.currentSession['viewedSafes']
|
||||
const recurringUser = viewedSafes?.includes(safeAddress)
|
||||
|
||||
const newIncomingTransactions = incomingTransactions.filter((tx) => tx.blockNumber > latestIncomingTxBlock)
|
||||
|
||||
const { message, ...TX_INCOMING_MSG } = NOTIFICATIONS.TX_INCOMING_MSG
|
||||
|
||||
if (recurringUser) {
|
||||
if (newIncomingTransactions.size > 3) {
|
||||
dispatch(
|
||||
enqueueSnackbar(
|
||||
enhanceSnackbarForAction({
|
||||
...TX_INCOMING_MSG,
|
||||
message: 'Multiple incoming transfers',
|
||||
}),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
newIncomingTransactions.forEach((tx) => {
|
||||
dispatch(
|
||||
enqueueSnackbar(
|
||||
enhanceSnackbarForAction({
|
||||
...TX_INCOMING_MSG,
|
||||
message: `${message}${getIncomingTxAmount(tx)}`,
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(
|
||||
updateSafe({
|
||||
address: safeAddress,
|
||||
|
|
|
@ -0,0 +1,384 @@
|
|||
type TransferDirection = 'INCOMING' | 'OUTGOING'
|
||||
|
||||
type Erc20Transfer = {
|
||||
type: 'ERC20'
|
||||
tokenAddress: string
|
||||
tokenName: string | null
|
||||
tokenSymbol: string | null
|
||||
logoUri: string | null
|
||||
decimals: number | null
|
||||
value: string
|
||||
}
|
||||
|
||||
type Erc721Transfer = {
|
||||
type: 'ERC721'
|
||||
tokenAddress: string
|
||||
tokenId: string
|
||||
tokenName: string | null
|
||||
tokenSymbol: string | null
|
||||
logoUri: string | null
|
||||
decimals: number | null
|
||||
value: string
|
||||
}
|
||||
|
||||
type NativeTransfer = {
|
||||
type: 'ETHER'
|
||||
value: string
|
||||
tokenSymbol: string | null
|
||||
decimals: number | null
|
||||
}
|
||||
|
||||
type TransferInfo = Erc20Transfer | Erc721Transfer | NativeTransfer
|
||||
|
||||
type Transfer = {
|
||||
type: 'Transfer'
|
||||
sender: string
|
||||
recipient: string
|
||||
direction?: TransferDirection
|
||||
transferInfo: TransferInfo // Polymorphic: Erc20, Erc721, Ether
|
||||
}
|
||||
|
||||
export enum Operation {
|
||||
CALL,
|
||||
DELEGATE,
|
||||
}
|
||||
|
||||
type InternalTransaction = {
|
||||
operation: Operation
|
||||
to: string
|
||||
value: number | null
|
||||
data: string | null
|
||||
dataDecoded: DataDecoded | null
|
||||
}
|
||||
|
||||
type Parameter = {
|
||||
name: string
|
||||
type: string
|
||||
value: string
|
||||
valueDecoded: InternalTransaction[] | null
|
||||
}
|
||||
|
||||
type DataDecoded = {
|
||||
method: string
|
||||
parameters: Parameter[] | null
|
||||
}
|
||||
|
||||
type SetFallbackHandler = {
|
||||
type: 'SET_FALLBACK_HANDLER'
|
||||
handler: string
|
||||
}
|
||||
|
||||
type AddOwner = {
|
||||
type: 'ADD_OWNER'
|
||||
owner: string
|
||||
threshold: number
|
||||
}
|
||||
|
||||
type RemoveOwner = {
|
||||
type: 'REMOVE_OWNER'
|
||||
owner: string
|
||||
threshold: number
|
||||
}
|
||||
|
||||
type SwapOwner = {
|
||||
type: 'SWAP_OWNER'
|
||||
oldOwner: string
|
||||
newOwner: string
|
||||
}
|
||||
|
||||
type ChangeThreshold = {
|
||||
type: 'CHANGE_THRESHOLD'
|
||||
threshold: number
|
||||
}
|
||||
|
||||
type ChangeImplementation = {
|
||||
type: 'CHANGE_IMPLEMENTATION'
|
||||
implementation: string
|
||||
}
|
||||
|
||||
type EnableModule = {
|
||||
type: 'ENABLE_MODULE'
|
||||
module: string
|
||||
}
|
||||
|
||||
type DisableModule = {
|
||||
type: 'DISABLE_MODULE'
|
||||
module: string
|
||||
}
|
||||
|
||||
type SettingsInfo =
|
||||
| SetFallbackHandler
|
||||
| AddOwner
|
||||
| RemoveOwner
|
||||
| SwapOwner
|
||||
| ChangeThreshold
|
||||
| ChangeImplementation
|
||||
| EnableModule
|
||||
| DisableModule
|
||||
|
||||
type SettingsChange = {
|
||||
type: 'SettingsChange'
|
||||
dataDecoded: DataDecoded
|
||||
settingsInfo: SettingsInfo | null
|
||||
}
|
||||
|
||||
type AddressInfo = {
|
||||
name: string
|
||||
logoUri: string | null
|
||||
}
|
||||
|
||||
type BaseCustom = {
|
||||
type: 'Custom'
|
||||
to: string
|
||||
dataSize: string
|
||||
value: string
|
||||
isCancellation: boolean
|
||||
toInfo: AddressInfo
|
||||
}
|
||||
|
||||
type Custom = BaseCustom & {
|
||||
methodName: string | null
|
||||
}
|
||||
|
||||
type MultiSend = BaseCustom & {
|
||||
methodName: 'multiSend'
|
||||
actionCount: number
|
||||
}
|
||||
|
||||
type Creation = {
|
||||
type: 'Creation'
|
||||
creator: string
|
||||
transactionHash: string
|
||||
implementation: string | null
|
||||
factory: string | null
|
||||
}
|
||||
|
||||
type TransactionStatus =
|
||||
| 'AWAITING_CONFIRMATIONS'
|
||||
| 'AWAITING_EXECUTION'
|
||||
| 'CANCELLED'
|
||||
| 'FAILED'
|
||||
| 'SUCCESS'
|
||||
| 'PENDING'
|
||||
| 'PENDING_FAILED'
|
||||
| 'WILL_BE_REPLACED'
|
||||
|
||||
type TransactionInfo = Transfer | SettingsChange | Custom | MultiSend | Creation
|
||||
|
||||
type ExecutionInfo = {
|
||||
nonce: number
|
||||
confirmationsRequired: number
|
||||
confirmationsSubmitted: number
|
||||
missingSigners?: string[]
|
||||
}
|
||||
|
||||
type SafeAppInfo = {
|
||||
name: string
|
||||
url: string
|
||||
logoUrl: string
|
||||
}
|
||||
|
||||
type TransactionSummary = {
|
||||
id: string
|
||||
timestamp: number
|
||||
txStatus: TransactionStatus
|
||||
txInfo: TransactionInfo // Polymorphic: Transfer, SettingsChange, Custom, Creation
|
||||
executionInfo: ExecutionInfo | null
|
||||
safeAppInfo: SafeAppInfo | null
|
||||
}
|
||||
|
||||
type TransactionData = {
|
||||
hexData: string | null
|
||||
dataDecoded: DataDecoded | null
|
||||
to: string
|
||||
value: string | null
|
||||
operation: Operation
|
||||
}
|
||||
|
||||
type ModuleExecutionDetails = {
|
||||
type: 'MODULE'
|
||||
address: string
|
||||
}
|
||||
|
||||
type MultiSigConfirmations = {
|
||||
signer: string
|
||||
signature: string | null
|
||||
}
|
||||
|
||||
type TokenType = 'ERC721' | 'ERC20' | 'ETHER'
|
||||
|
||||
type TokenInfo = {
|
||||
tokenType: TokenType
|
||||
address: string
|
||||
decimals: number | null
|
||||
symbol: string
|
||||
name: string
|
||||
logoUri: string | null
|
||||
}
|
||||
|
||||
type MultiSigExecutionDetails = {
|
||||
type: 'MULTISIG'
|
||||
submittedAt: number
|
||||
nonce: number
|
||||
safeTxGas: number
|
||||
baseGas: number
|
||||
gasPrice: string
|
||||
gasToken: string
|
||||
refundReceiver: string
|
||||
safeTxHash: string
|
||||
executor: string | null
|
||||
signers: string[]
|
||||
confirmationsRequired: number
|
||||
confirmations: MultiSigConfirmations[]
|
||||
gasTokenInfo: TokenInfo | null
|
||||
}
|
||||
|
||||
type DetailedExecutionInfo = ModuleExecutionDetails | MultiSigExecutionDetails
|
||||
|
||||
type ExpandedTxDetails = {
|
||||
executedAt: number
|
||||
txStatus: TransactionStatus
|
||||
txInfo: TransactionInfo
|
||||
txData: TransactionData | null
|
||||
detailedExecutionInfo: DetailedExecutionInfo | null
|
||||
txHash: string | null
|
||||
}
|
||||
|
||||
type Transaction = TransactionSummary & {
|
||||
txDetails?: ExpandedTxDetails
|
||||
}
|
||||
|
||||
type StoreStructure = {
|
||||
queued: {
|
||||
next: { [nonce: number]: Transaction[] } // 1 Transaction element
|
||||
queued: { [nonce: number]: Transaction[] } // n Transaction elements
|
||||
}
|
||||
history: { [timestamp: number]: Transaction[] } // n Transaction elements
|
||||
}
|
||||
|
||||
type TxQueuedLocation = 'queued.next' | 'queued.queued'
|
||||
|
||||
type TxHistoryLocation = 'history'
|
||||
|
||||
type TxLocation = TxHistoryLocation | TxQueuedLocation
|
||||
|
||||
type Label = {
|
||||
type: 'LABEL'
|
||||
label: 'Next' | 'Queued'
|
||||
}
|
||||
|
||||
type DateLabel = {
|
||||
type: 'DATE_LABEL'
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
type ConflictHeader = {
|
||||
type: 'CONFLICT_HEADER'
|
||||
nonce: number
|
||||
}
|
||||
|
||||
type TransactionGatewayResult = {
|
||||
type: 'TRANSACTION'
|
||||
transaction: TransactionSummary
|
||||
conflictType: 'HasNext' | 'End' | 'None'
|
||||
}
|
||||
|
||||
type GatewayResponse = {
|
||||
next: string | null
|
||||
previous: string | null
|
||||
}
|
||||
|
||||
type HistoryGatewayResult = DateLabel | TransactionGatewayResult
|
||||
|
||||
type HistoryGatewayResponse = GatewayResponse & {
|
||||
results: HistoryGatewayResult[]
|
||||
}
|
||||
|
||||
type QueuedGatewayResult = Label | ConflictHeader | TransactionGatewayResult
|
||||
|
||||
type QueuedGatewayResponse = GatewayResponse & {
|
||||
results: QueuedGatewayResult[]
|
||||
}
|
||||
|
||||
export type TransactionDetails = {
|
||||
count: number
|
||||
transactions: Array<[nonce: string, transactions: Transaction[]]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper functions
|
||||
*/
|
||||
|
||||
export const isDateLabel = (value: HistoryGatewayResult): value is DateLabel => {
|
||||
return value.type === 'DATE_LABEL'
|
||||
}
|
||||
|
||||
export const isLabel = (value: QueuedGatewayResult): value is Label => {
|
||||
return value.type === 'LABEL'
|
||||
}
|
||||
|
||||
export const isConflictHeader = (value: QueuedGatewayResult): value is ConflictHeader => {
|
||||
return value.type === 'CONFLICT_HEADER'
|
||||
}
|
||||
|
||||
export const isTransactionSummary = (
|
||||
value: HistoryGatewayResult | QueuedGatewayResult,
|
||||
): value is TransactionGatewayResult => {
|
||||
return value.type === 'TRANSACTION'
|
||||
}
|
||||
|
||||
export const isTransferTxInfo = (value: TransactionInfo): value is Transfer => {
|
||||
return value.type === 'Transfer'
|
||||
}
|
||||
|
||||
export const isSettingsChangeTxInfo = (value: TransactionInfo): value is SettingsChange => {
|
||||
return value.type === 'SettingsChange'
|
||||
}
|
||||
|
||||
export const isCustomTxInfo = (value: TransactionInfo): value is Custom => {
|
||||
return value.type === 'Custom'
|
||||
}
|
||||
|
||||
export const isMultiSendTxInfo = (value: TransactionInfo): value is MultiSend => {
|
||||
return isCustomTxInfo(value) && value.methodName === 'multiSend'
|
||||
}
|
||||
|
||||
export const isCreationTxInfo = (value: TransactionInfo): value is Creation => {
|
||||
return value.type === 'Creation'
|
||||
}
|
||||
|
||||
export const isStatusSuccess = (value: Transaction['txStatus']): value is 'SUCCESS' => {
|
||||
return value === 'SUCCESS'
|
||||
}
|
||||
|
||||
export const isStatusFailed = (value: Transaction['txStatus']): value is 'FAILED' => {
|
||||
return value === 'FAILED'
|
||||
}
|
||||
|
||||
export const isStatusCancelled = (value: Transaction['txStatus']): value is 'CANCELLED' => {
|
||||
return value === 'CANCELLED'
|
||||
}
|
||||
|
||||
export const isStatusPending = (value: Transaction['txStatus']): value is 'PENDING' => {
|
||||
return value === 'PENDING'
|
||||
}
|
||||
|
||||
export const isStatusAwaitingConfirmation = (value: Transaction['txStatus']): value is 'AWAITING_CONFIRMATIONS' => {
|
||||
return value === 'AWAITING_CONFIRMATIONS'
|
||||
}
|
||||
|
||||
export const isStatusWillBeReplaced = (value: Transaction['txStatus']): value is 'WILL_BE_REPLACED' => {
|
||||
return value === 'WILL_BE_REPLACED'
|
||||
}
|
||||
|
||||
export const isMultiSigExecutionDetails = (
|
||||
value: ExpandedTxDetails['detailedExecutionInfo'],
|
||||
): value is MultiSigExecutionDetails => {
|
||||
return value?.type === 'MULTISIG'
|
||||
}
|
||||
|
||||
export const isModuleExecutionDetails = (
|
||||
value: ExpandedTxDetails['detailedExecutionInfo'],
|
||||
): value is ModuleExecutionDetails => {
|
||||
return value?.type === 'MODULE'
|
||||
}
|
|
@ -6,6 +6,7 @@ import { Confirmation } from './confirmation'
|
|||
import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d'
|
||||
import { DataDecoded, Transfer } from './transactions'
|
||||
import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d'
|
||||
import { BuildTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
|
||||
export enum TransactionTypes {
|
||||
INCOMING = 'incoming',
|
||||
|
@ -89,7 +90,11 @@ export type TransactionProps = {
|
|||
value: string
|
||||
}
|
||||
|
||||
export type Transaction = RecordOf<TransactionProps>
|
||||
export type Transaction = RecordOf<TransactionProps> & Readonly<TransactionProps>
|
||||
|
||||
export const isStoredTransaction = (tx: BuildTx['tx']): tx is Transaction => {
|
||||
return typeof (tx as Transaction).recipient !== 'undefined'
|
||||
}
|
||||
|
||||
export type TxArgs = {
|
||||
baseGas: number
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
import { Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
import { Action, handleActions } from 'redux-actions'
|
||||
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
|
||||
import { REMOVE_CANCELLATION_TRANSACTION } from 'src/logic/safe/store/actions/transactions/removeCancellationTransaction'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const CANCELLATION_TRANSACTIONS_REDUCER_ID = 'cancellationTransactions'
|
||||
|
||||
export type CancellationTransactions = Map<string, Transaction>
|
||||
export type CancellationTxState = Map<string, CancellationTransactions>
|
||||
|
||||
export default handleActions(
|
||||
type CancellationTransactionsPayload = { safeAddress: string; transactions: CancellationTransactions }
|
||||
type CancellationTransactionPayload = { safeAddress: string; transaction: Transaction }
|
||||
|
||||
export default handleActions<
|
||||
AppReduxState['cancellationTransactions'],
|
||||
CancellationTransactionsPayload | CancellationTransactionPayload
|
||||
>(
|
||||
{
|
||||
[ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS]: (state, action) => {
|
||||
[ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS]: (state, action: Action<CancellationTransactionsPayload>) => {
|
||||
const { safeAddress, transactions } = action.payload
|
||||
|
||||
if (!safeAddress || !transactions || !transactions.size) {
|
||||
|
@ -41,7 +48,7 @@ export default handleActions(
|
|||
}
|
||||
})
|
||||
},
|
||||
[REMOVE_CANCELLATION_TRANSACTION]: (state, action) => {
|
||||
[REMOVE_CANCELLATION_TRANSACTION]: (state, action: Action<CancellationTransactionPayload>) => {
|
||||
const { safeAddress, transaction } = action.payload
|
||||
|
||||
if (!safeAddress || !transaction) {
|
||||
|
|
|
@ -0,0 +1,368 @@
|
|||
import get from 'lodash.get'
|
||||
import merge from 'lodash.merge'
|
||||
import { Action, handleActions } from 'redux-actions'
|
||||
|
||||
import {
|
||||
ADD_HISTORY_TRANSACTIONS,
|
||||
ADD_QUEUED_TRANSACTIONS,
|
||||
} from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
|
||||
import { UPDATE_TRANSACTION_STATUS } from 'src/logic/safe/store/actions/updateTransactionStatus'
|
||||
import {
|
||||
HistoryGatewayResponse,
|
||||
isConflictHeader,
|
||||
isDateLabel,
|
||||
isLabel,
|
||||
isTransactionSummary,
|
||||
QueuedGatewayResponse,
|
||||
StoreStructure,
|
||||
Transaction,
|
||||
TransactionStatus,
|
||||
TxLocation,
|
||||
} from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import { UPDATE_TRANSACTION_DETAILS } from 'src/logic/safe/store/actions/fetchTransactionDetails'
|
||||
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { getUTCStartOfDate } from 'src/utils/date'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
import { sortObject } from 'src/utils/objects'
|
||||
|
||||
export const GATEWAY_TRANSACTIONS_ID = 'gatewayTransactions'
|
||||
|
||||
type BasePayload = { safeAddress: string; isTail?: boolean }
|
||||
export type HistoryPayload = BasePayload & { values: HistoryGatewayResponse['results'] }
|
||||
export type QueuedPayload = BasePayload & { values: QueuedGatewayResponse['results'] }
|
||||
export type TransactionDetailsPayload = {
|
||||
safeAddress: string
|
||||
txLocation: TxLocation
|
||||
transactionId: string
|
||||
value: Transaction['txDetails']
|
||||
}
|
||||
export type TransactionStatusPayload = {
|
||||
safeAddress: string
|
||||
nonce: number
|
||||
id?: string
|
||||
txStatus: TransactionStatus
|
||||
}
|
||||
|
||||
type Payload = HistoryPayload | QueuedPayload | TransactionDetailsPayload | TransactionStatusPayload
|
||||
|
||||
const findTransactionLocation = (
|
||||
transactionsGroup: { [p: number]: Transaction[] },
|
||||
transactionId: string,
|
||||
): { key: string; index: number } => {
|
||||
let key
|
||||
let index
|
||||
let transactions
|
||||
|
||||
for ([key, transactions] of Object.entries(transactionsGroup)) {
|
||||
index = transactions.findIndex(({ id }) => sameString(id, transactionId))
|
||||
|
||||
if (index !== -1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return { key, index }
|
||||
}
|
||||
|
||||
export const gatewayTransactions = handleActions<AppReduxState['gatewayTransactions'], Payload>(
|
||||
{
|
||||
[ADD_HISTORY_TRANSACTIONS]: (state, action: Action<HistoryPayload>) => {
|
||||
const { safeAddress, values, isTail = false } = action.payload
|
||||
const history: StoreStructure['history'] = Object.assign({}, state[safeAddress]?.history)
|
||||
|
||||
values.forEach((value) => {
|
||||
if (isDateLabel(value)) {
|
||||
// DATE_LABEL is discarded as it's not needed for the current implementation
|
||||
return
|
||||
}
|
||||
|
||||
if (isTransactionSummary(value)) {
|
||||
const startOfDate = getUTCStartOfDate(value.transaction.timestamp)
|
||||
|
||||
if (typeof history[startOfDate] === 'undefined') {
|
||||
history[startOfDate] = []
|
||||
}
|
||||
|
||||
const txExist = history[startOfDate].some(({ id }) => sameString(id, value.transaction.id))
|
||||
|
||||
if (!txExist) {
|
||||
history[startOfDate].push(value.transaction)
|
||||
// pushing a newer transaction to the existing list messes the transactions order
|
||||
// this happens when most recent transactions are added to the existing txs in the store
|
||||
history[startOfDate] = history[startOfDate].sort((a, b) => b.timestamp - a.timestamp)
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// all the safes with their respective states
|
||||
...state,
|
||||
// current safe
|
||||
[safeAddress]: {
|
||||
// keep queued list
|
||||
...state[safeAddress],
|
||||
// extend history list
|
||||
history: isTail ? history : sortObject(history, 'desc'),
|
||||
},
|
||||
}
|
||||
},
|
||||
[ADD_QUEUED_TRANSACTIONS]: (state, action: Action<QueuedPayload>) => {
|
||||
// we're assuming that `next` and `queued` labels will be provided in the first page
|
||||
// as for usage experience there were no more than 5 transactions competing for the same nonce.
|
||||
// Thus, given the client-gateway page size of 20, we have plenty of "room" to be provided with
|
||||
// `next` and `queued` transactions in the first page.
|
||||
const { safeAddress, values } = action.payload
|
||||
let next = Object.assign({}, state[safeAddress]?.queued?.next)
|
||||
const queued = Object.assign({}, state[safeAddress]?.queued?.queued)
|
||||
|
||||
let label: 'next' | 'queued' | undefined
|
||||
values.forEach((value) => {
|
||||
if (isLabel(value)) {
|
||||
// we're assuming that the first page will always provide `next` and `queued` labels
|
||||
label = value.label.toLowerCase() as 'next' | 'queued'
|
||||
return
|
||||
}
|
||||
|
||||
if (isConflictHeader(value)) {
|
||||
// conflict header is discarded as it's not needed for the current implementation
|
||||
return
|
||||
}
|
||||
|
||||
if (isTransactionSummary(value)) {
|
||||
const txNonce = value.transaction.executionInfo?.nonce
|
||||
|
||||
if (typeof txNonce === 'undefined') {
|
||||
console.warn('A transaction without nonce was provided by client-gateway:', JSON.stringify(value))
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof label === 'undefined') {
|
||||
label = next[txNonce] ? 'next' : 'queued'
|
||||
}
|
||||
|
||||
switch (label) {
|
||||
case 'next': {
|
||||
if (next[txNonce]) {
|
||||
const txIndex = next[txNonce].findIndex(({ id }) => sameString(id, value.transaction.id))
|
||||
|
||||
if (txIndex !== -1) {
|
||||
const storedTransaction = next[txNonce][txIndex]
|
||||
const updateFromService =
|
||||
storedTransaction.executionInfo?.confirmationsSubmitted !==
|
||||
value.transaction.executionInfo?.confirmationsSubmitted
|
||||
|
||||
if (storedTransaction.txStatus === 'PENDING' && !updateFromService) {
|
||||
// we're waiting for a tx resolution. Thus, we'll prioritize 'PENDING' status
|
||||
value.transaction.txStatus = 'PENDING'
|
||||
}
|
||||
|
||||
next[txNonce][txIndex] = updateFromService
|
||||
? // by replacing the current transaction with the one returned by the service
|
||||
// we remove the `txDetails`, so this will force a re-request of the data
|
||||
value.transaction
|
||||
: // we merge, to keep the current unchanged information
|
||||
merge(storedTransaction, value.transaction)
|
||||
break
|
||||
}
|
||||
|
||||
// we add the transaction returned by the service to the list of transactions
|
||||
next[txNonce] = [...next[txNonce], value.transaction]
|
||||
break
|
||||
}
|
||||
|
||||
// a new tx has arrived to the `next` queue
|
||||
// we re-create the `next` object with the new transaction
|
||||
next = { [txNonce]: [value.transaction] }
|
||||
|
||||
// we remove the new `next` transaction from the `queue` list, if it exist
|
||||
queued[txNonce] && delete queued[txNonce]
|
||||
|
||||
break
|
||||
}
|
||||
case 'queued': {
|
||||
if (queued[txNonce]) {
|
||||
const txIndex = queued[txNonce].findIndex(({ id }) => sameString(id, value.transaction.id))
|
||||
|
||||
if (txIndex !== -1) {
|
||||
const storedTransaction = queued[txNonce][txIndex]
|
||||
const updateFromService =
|
||||
storedTransaction.executionInfo?.confirmationsSubmitted !==
|
||||
value.transaction.executionInfo?.confirmationsSubmitted
|
||||
|
||||
if (storedTransaction.txStatus === 'PENDING' && !updateFromService) {
|
||||
// we're waiting for a tx resolution. Thus, we'll prioritize 'PENDING' status
|
||||
value.transaction.txStatus = 'PENDING'
|
||||
}
|
||||
|
||||
queued[txNonce][txIndex] = updateFromService
|
||||
? // by replacing the current transaction with the one returned by the service
|
||||
// we remove the `txDetails`, so this will force a re-request of the data
|
||||
value.transaction
|
||||
: // we merge, to keep the current unchanged information
|
||||
merge(storedTransaction, value.transaction)
|
||||
break
|
||||
}
|
||||
|
||||
// we add the transaction returned by the service to the list of transactions
|
||||
queued[txNonce] = [...queued[txNonce], value.transaction]
|
||||
break
|
||||
}
|
||||
|
||||
queued[txNonce] = [value.transaction]
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// no new transactions
|
||||
if (!values.length) {
|
||||
// queued list already empty
|
||||
if (!Object.keys(queued).length) {
|
||||
// there was an existing next transaction
|
||||
if (Object.keys(next).length === 1) {
|
||||
// we cleanup the next queue
|
||||
next = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// all the safes with their respective states
|
||||
...state,
|
||||
// current safe
|
||||
[safeAddress]: {
|
||||
// keep history list
|
||||
...state[safeAddress],
|
||||
// overwrites queued lists
|
||||
queued: {
|
||||
next,
|
||||
queued,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
[UPDATE_TRANSACTION_DETAILS]: (state, action: Action<TransactionDetailsPayload>) => {
|
||||
const { safeAddress, transactionId, txLocation, value } = action.payload
|
||||
const storedTransactions = Object.assign({}, state[safeAddress])
|
||||
const { queued } = storedTransactions
|
||||
let { history } = storedTransactions
|
||||
|
||||
// get the tx group (it will be `queued.next`, `queued.queued` or `history`)
|
||||
const txGroup: StoreStructure['queued']['next' | 'queued'] | StoreStructure['history'] = get(
|
||||
storedTransactions,
|
||||
txLocation,
|
||||
)
|
||||
|
||||
// find the transaction location
|
||||
const { key, index } = findTransactionLocation(txGroup, transactionId)
|
||||
// add details to tx object
|
||||
txGroup[key][index]['txDetails'] = value
|
||||
|
||||
// replace the updated group in its corresponding location
|
||||
switch (txLocation) {
|
||||
case 'history':
|
||||
history = txGroup
|
||||
break
|
||||
case 'queued.next':
|
||||
queued['next'] = txGroup
|
||||
break
|
||||
case 'queued.queued':
|
||||
queued['queued'] = txGroup
|
||||
break
|
||||
}
|
||||
|
||||
// update state
|
||||
return {
|
||||
// all the safes with their respective states
|
||||
...state,
|
||||
// current safe
|
||||
[safeAddress]: {
|
||||
history,
|
||||
queued,
|
||||
},
|
||||
}
|
||||
},
|
||||
[UPDATE_TRANSACTION_STATUS]: (state, action: Action<TransactionStatusPayload>) => {
|
||||
// if we provide the tx ID that sole tx will have the _pending_ status.
|
||||
// if not, all the txs that share the same nonce will have the _pending_ status.
|
||||
const { nonce, id, safeAddress, txStatus } = action.payload
|
||||
const storedTransactions = Object.assign({}, state[safeAddress])
|
||||
const { queued } = storedTransactions
|
||||
const { history } = storedTransactions
|
||||
|
||||
let txLocation: TxLocation | undefined
|
||||
let historyLocation: string | undefined
|
||||
|
||||
if (queued.next[nonce]) {
|
||||
txLocation = 'queued.next'
|
||||
} else if (queued.queued[nonce]) {
|
||||
txLocation = 'queued.queued'
|
||||
} else {
|
||||
Object.entries(history).forEach(([timestamp, transactions]) => {
|
||||
const txIndex = transactions.findIndex((transaction) => Number(transaction.executionInfo?.nonce) === nonce)
|
||||
|
||||
if (txIndex !== -1) {
|
||||
txLocation = 'history'
|
||||
historyLocation = `${timestamp}[${txIndex}]`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!txLocation) {
|
||||
return state
|
||||
}
|
||||
|
||||
switch (txLocation) {
|
||||
case 'history': {
|
||||
if (historyLocation) {
|
||||
const txToUpdate = get(history, historyLocation)
|
||||
txToUpdate.txStatus = txStatus
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'queued.next': {
|
||||
queued.next[nonce] = queued.next[nonce].map((txToUpdate) => {
|
||||
if (typeof id !== 'undefined') {
|
||||
if (sameString(txToUpdate.id, id)) {
|
||||
txToUpdate.txStatus = txStatus
|
||||
}
|
||||
} else {
|
||||
txToUpdate.txStatus = txStatus
|
||||
}
|
||||
return txToUpdate
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'queued.queued': {
|
||||
queued.queued[nonce] = queued.queued[nonce].map((txToUpdate) => {
|
||||
if (typeof id !== 'undefined') {
|
||||
if (sameString(txToUpdate.id, id)) {
|
||||
txToUpdate.txStatus = txStatus
|
||||
}
|
||||
} else {
|
||||
txToUpdate.txStatus = txStatus
|
||||
}
|
||||
return txToUpdate
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// update state
|
||||
return {
|
||||
// all the safes with their respective states
|
||||
...state,
|
||||
// current safe
|
||||
[safeAddress]: {
|
||||
history,
|
||||
queued,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{},
|
||||
)
|
|
@ -2,10 +2,11 @@ import { Map } from 'immutable'
|
|||
import { handleActions } from 'redux-actions'
|
||||
|
||||
import { ADD_INCOMING_TRANSACTIONS } from 'src/logic/safe/store/actions/addIncomingTransactions'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const INCOMING_TRANSACTIONS_REDUCER_ID = 'incomingTransactions'
|
||||
|
||||
export default handleActions(
|
||||
export default handleActions<AppReduxState['incomingTransactions']>(
|
||||
{
|
||||
[ADD_INCOMING_TRANSACTIONS]: (state, action) => action.payload,
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Map, Set, List } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
import { Action, handleActions } from 'redux-actions'
|
||||
|
||||
import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes'
|
||||
import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner'
|
||||
|
@ -13,9 +13,9 @@ import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
|
|||
import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList'
|
||||
import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList'
|
||||
import { makeOwner } from 'src/logic/safe/store/models/owner'
|
||||
import makeSafe, { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
|
||||
import { ADD_OR_UPDATE_SAFE, buildOwnersFrom } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { shouldSafeStoreBeUpdated } from 'src/logic/safe/utils/shouldSafeStoreBeUpdated'
|
||||
|
@ -73,9 +73,22 @@ const updateSafeProps = (prevSafe, safe) => {
|
|||
})
|
||||
}
|
||||
|
||||
export default handleActions(
|
||||
export type SafePayload = { safe: SafeRecord }
|
||||
type SafePayloads = SafeRecord | SafePayload | string
|
||||
|
||||
type BaseOwnerPayload = { safeAddress: string; ownerAddress: string }
|
||||
type FullOwnerPayload = BaseOwnerPayload & { ownerName: string }
|
||||
type ReplaceOwnerPayload = FullOwnerPayload & { oldOwnerAddress: string }
|
||||
|
||||
type OwnerPayloads = BaseOwnerPayload | FullOwnerPayload | ReplaceOwnerPayload
|
||||
|
||||
type SafeWithAddressPayload = SafeRecord & { safeAddress: string }
|
||||
|
||||
type Payloads = SafePayloads | OwnerPayloads | SafeWithAddressPayload
|
||||
|
||||
export default handleActions<AppReduxState['safes'], Payloads>(
|
||||
{
|
||||
[UPDATE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
[UPDATE_SAFE]: (state, action: Action<SafeRecord>) => {
|
||||
const safe = action.payload
|
||||
const safeAddress = safe.address
|
||||
|
||||
|
@ -89,7 +102,7 @@ export default handleActions(
|
|||
)
|
||||
: state
|
||||
},
|
||||
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerMap, action) => {
|
||||
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state, action: Action<SafeRecord>) => {
|
||||
const tokenAddress = action.payload
|
||||
|
||||
return state.withMutations((map) => {
|
||||
|
@ -104,8 +117,7 @@ export default handleActions(
|
|||
})
|
||||
})
|
||||
},
|
||||
|
||||
[ADD_OR_UPDATE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
[ADD_OR_UPDATE_SAFE]: (state, action: Action<SafePayload>) => {
|
||||
const { safe } = action.payload
|
||||
const safeAddress = safe.address
|
||||
|
||||
|
@ -123,7 +135,7 @@ export default handleActions(
|
|||
)
|
||||
: state
|
||||
},
|
||||
[REMOVE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
[REMOVE_SAFE]: (state, action: Action<string>) => {
|
||||
const safeAddress = action.payload
|
||||
|
||||
const currentDefaultSafe = state.get('defaultSafe')
|
||||
|
@ -135,7 +147,7 @@ export default handleActions(
|
|||
|
||||
return newState
|
||||
},
|
||||
[ADD_SAFE_OWNER]: (state: SafeReducerMap, action) => {
|
||||
[ADD_SAFE_OWNER]: (state, action: Action<FullOwnerPayload>) => {
|
||||
const { ownerAddress, ownerName, safeAddress } = action.payload
|
||||
|
||||
const addressFound = state
|
||||
|
@ -152,7 +164,7 @@ export default handleActions(
|
|||
}),
|
||||
)
|
||||
},
|
||||
[REMOVE_SAFE_OWNER]: (state: SafeReducerMap, action) => {
|
||||
[REMOVE_SAFE_OWNER]: (state, action: Action<BaseOwnerPayload>) => {
|
||||
const { ownerAddress, safeAddress } = action.payload
|
||||
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) =>
|
||||
|
@ -161,7 +173,7 @@ export default handleActions(
|
|||
}),
|
||||
)
|
||||
},
|
||||
[REPLACE_SAFE_OWNER]: (state: SafeReducerMap, action) => {
|
||||
[REPLACE_SAFE_OWNER]: (state, action: Action<ReplaceOwnerPayload>) => {
|
||||
const { oldOwnerAddress, ownerAddress, ownerName, safeAddress } = action.payload
|
||||
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) =>
|
||||
|
@ -172,7 +184,7 @@ export default handleActions(
|
|||
}),
|
||||
)
|
||||
},
|
||||
[EDIT_SAFE_OWNER]: (state: SafeReducerMap, action) => {
|
||||
[EDIT_SAFE_OWNER]: (state, action: Action<FullOwnerPayload>) => {
|
||||
const { ownerAddress, ownerName, safeAddress } = action.payload
|
||||
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) => {
|
||||
|
@ -183,7 +195,7 @@ export default handleActions(
|
|||
return prevSafe.merge({ owners: updatedOwners })
|
||||
})
|
||||
},
|
||||
[UPDATE_TOKENS_LIST]: (state: SafeReducerMap, action) => {
|
||||
[UPDATE_TOKENS_LIST]: (state, action: Action<SafeWithAddressPayload>) => {
|
||||
// Only activeTokens or blackListedTokens is required
|
||||
const { safeAddress, activeTokens, blacklistedTokens } = action.payload
|
||||
|
||||
|
@ -192,7 +204,7 @@ export default handleActions(
|
|||
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
|
||||
},
|
||||
[UPDATE_ASSETS_LIST]: (state: SafeReducerMap, action) => {
|
||||
[UPDATE_ASSETS_LIST]: (state, action: Action<SafeWithAddressPayload>) => {
|
||||
// Only activeAssets or blackListedAssets is required
|
||||
const { safeAddress, activeAssets, blacklistedAssets } = action.payload
|
||||
|
||||
|
@ -201,13 +213,13 @@ export default handleActions(
|
|||
|
||||
return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list))
|
||||
},
|
||||
[SET_DEFAULT_SAFE]: (state: SafeReducerMap, action) => state.set('defaultSafe', action.payload),
|
||||
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state: SafeReducerMap, action) =>
|
||||
[SET_DEFAULT_SAFE]: (state, action: Action<SafeRecord>) => state.set('defaultSafe', action.payload),
|
||||
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action<SafeRecord>) =>
|
||||
state.set('latestMasterContractVersion', action.payload),
|
||||
},
|
||||
Map({
|
||||
defaultSafe: DEFAULT_SAFE_INITIAL_STATE,
|
||||
safes: Map(),
|
||||
latestMasterContractVersion: '',
|
||||
}),
|
||||
}) as AppReduxState['safes'],
|
||||
)
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
import { Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
import { List, Map } from 'immutable'
|
||||
import { Action, handleActions } from 'redux-actions'
|
||||
|
||||
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
|
||||
import { REMOVE_TRANSACTION } from 'src/logic/safe/store/actions/transactions/removeTransaction'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const TRANSACTIONS_REDUCER_ID = 'transactions'
|
||||
|
||||
export default handleActions(
|
||||
type TransactionBasePayload = { safeAddress: string }
|
||||
type TransactionsPayload = TransactionBasePayload & { transactions: List<Transaction> }
|
||||
type TransactionPayload = TransactionBasePayload & { transaction: Transaction }
|
||||
|
||||
type Payload = TransactionsPayload | TransactionPayload
|
||||
|
||||
export default handleActions<AppReduxState['transactions'], Payload>(
|
||||
{
|
||||
[ADD_OR_UPDATE_TRANSACTIONS]: (state, action) => {
|
||||
[ADD_OR_UPDATE_TRANSACTIONS]: (state, action: Action<TransactionsPayload>) => {
|
||||
const { safeAddress, transactions } = action.payload
|
||||
|
||||
if (!safeAddress || !transactions || !transactions.size) {
|
||||
|
@ -46,7 +54,7 @@ export default handleActions(
|
|||
}
|
||||
})
|
||||
},
|
||||
[REMOVE_TRANSACTION]: (state, action) => {
|
||||
[REMOVE_TRANSACTION]: (state, action: Action<TransactionPayload>) => {
|
||||
const { safeAddress, transaction } = action.payload
|
||||
|
||||
if (!safeAddress || !transaction) {
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
import get from 'lodash.get'
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import { StoreStructure, Transaction, TxLocation } from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import { GATEWAY_TRANSACTIONS_ID } from 'src/logic/safe/store/reducer/gatewayTransactions'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { createHashBasedSelector } from 'src/logic/safe/store/selectors/utils'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const gatewayTransactions = (state: AppReduxState): AppReduxState['gatewayTransactions'] => {
|
||||
return state[GATEWAY_TRANSACTIONS_ID]
|
||||
}
|
||||
|
||||
export const historyTransactions = createSelector(
|
||||
gatewayTransactions,
|
||||
safeParamAddressFromStateSelector,
|
||||
(gatewayTransactions, safeAddress): StoreStructure['history'] | undefined => {
|
||||
return gatewayTransactions[safeAddress]?.history
|
||||
},
|
||||
)
|
||||
|
||||
export const pendingTransactions = createSelector(
|
||||
gatewayTransactions,
|
||||
safeParamAddressFromStateSelector,
|
||||
(gatewayTransactions, safeAddress): StoreStructure['queued'] | undefined => {
|
||||
return gatewayTransactions[safeAddress]?.queued
|
||||
},
|
||||
)
|
||||
|
||||
export const nextTransactions = createSelector(pendingTransactions, (pendingTransactions):
|
||||
| StoreStructure['queued']['next']
|
||||
| undefined => {
|
||||
return pendingTransactions?.next
|
||||
})
|
||||
|
||||
export const queuedTransactions = createSelector(pendingTransactions, (pendingTransactions):
|
||||
| StoreStructure['queued']['queued']
|
||||
| undefined => {
|
||||
return pendingTransactions?.queued
|
||||
})
|
||||
|
||||
type TxByLocationAttr = { attributeName: string; attributeValue: string | number; txLocation: TxLocation }
|
||||
|
||||
type TxByLocation = {
|
||||
attributeName: string
|
||||
attributeValue: string | number
|
||||
transactions: StoreStructure['history'] | StoreStructure['queued']['queued' | 'next']
|
||||
}
|
||||
|
||||
const getTransactionsByLocation = createHashBasedSelector(
|
||||
gatewayTransactions,
|
||||
safeParamAddressFromStateSelector,
|
||||
(gatewayTransactions, safeAddress) => (rest: TxByLocationAttr): TxByLocation => ({
|
||||
attributeName: rest.attributeName,
|
||||
attributeValue: rest.attributeValue,
|
||||
transactions: get(gatewayTransactions[safeAddress], rest.txLocation),
|
||||
}),
|
||||
)
|
||||
|
||||
export const getTransactionByAttribute = createSelector(
|
||||
getTransactionsByLocation,
|
||||
(fn: (r: TxByLocationAttr) => TxByLocation) => (rest: TxByLocationAttr): Transaction | undefined => {
|
||||
const { attributeName, attributeValue, transactions } = fn(rest)
|
||||
|
||||
if (transactions && attributeValue) {
|
||||
for (const [, txs] of Object.entries(transactions)) {
|
||||
const foundTx = txs.find((transaction) => transaction[attributeName] === attributeValue)
|
||||
|
||||
if (foundTx) {
|
||||
return foundTx
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const getTransactionDetails = createSelector(
|
||||
getTransactionByAttribute,
|
||||
(fn: (rest: TxByLocationAttr) => Transaction | undefined) => (
|
||||
rest: TxByLocationAttr,
|
||||
): Transaction['txDetails'] | undefined => {
|
||||
const transaction = fn(rest)
|
||||
return transaction?.txDetails
|
||||
},
|
||||
)
|
||||
|
||||
export const getQueuedTransactionsByNonce = createSelector(
|
||||
getTransactionsByLocation,
|
||||
(fn: (r: TxByLocationAttr) => TxByLocation) => (rest: TxByLocationAttr): Transaction[] => {
|
||||
const { attributeValue, attributeName, transactions } = fn(rest)
|
||||
|
||||
if (attributeName === 'nonce') {
|
||||
return transactions?.[attributeValue] ?? []
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
)
|
|
@ -93,7 +93,7 @@ export const safeCancellationTransactionsSelector = createSelector(
|
|||
return Map()
|
||||
}
|
||||
|
||||
return cancellationTransactions.get(address, Map({}))
|
||||
return cancellationTransactions.get(address, Map())
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -109,7 +109,7 @@ export const safeIncomingTransactionsSelector = createSelector(
|
|||
return List([])
|
||||
}
|
||||
|
||||
return incomingTransactions.get(address, List([]))
|
||||
return incomingTransactions.get(address, List())
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import { List } from 'immutable'
|
||||
import { createSelector } from 'reselect'
|
||||
// import { List } from 'immutable'
|
||||
// import { createSelector } from 'reselect'
|
||||
//
|
||||
// import { safeIncomingTransactionsSelector, safeTransactionsSelector } from 'src/logic/safe/store/selectors'
|
||||
// import { Transaction, SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
// import { safeModuleTransactionsSelector } from 'src/routes/safe/container/selector'
|
||||
|
||||
import { safeIncomingTransactionsSelector, safeTransactionsSelector } from 'src/logic/safe/store/selectors'
|
||||
import { Transaction, SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { safeModuleTransactionsSelector } from 'src/routes/safe/container/selector'
|
||||
// export const extendedTransactionsSelector = createSelector(
|
||||
// safeTransactionsSelector,
|
||||
// safeIncomingTransactionsSelector,
|
||||
// safeModuleTransactionsSelector,
|
||||
// (transactions, incomingTransactions, moduleTransactions): List<Transaction | SafeModuleTransaction> =>
|
||||
// List().withMutations((list) => {
|
||||
// list.concat(transactions).concat(incomingTransactions).concat(moduleTransactions)
|
||||
// }),
|
||||
// )
|
||||
|
||||
export const extendedTransactionsSelector = createSelector(
|
||||
safeTransactionsSelector,
|
||||
safeIncomingTransactionsSelector,
|
||||
safeModuleTransactionsSelector,
|
||||
(transactions, incomingTransactions, moduleTransactions): List<Transaction | SafeModuleTransaction> =>
|
||||
List([...transactions, ...incomingTransactions, ...moduleTransactions]),
|
||||
)
|
||||
export {}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import hash from 'object-hash'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import memoize from 'lodash.memoize'
|
||||
import { createSelectorCreator, defaultMemoize } from 'reselect'
|
||||
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const createIsEqualSelector = createSelectorCreator(defaultMemoize, isEqual)
|
||||
|
||||
const hashFn = (gatewayTransactions: AppReduxState['gatewayTransactions'], safeAddress: string): string =>
|
||||
hash(gatewayTransactions[safeAddress])
|
||||
|
||||
export const createHashBasedSelector = createSelectorCreator(memoize as any, hashFn)
|
|
@ -1,18 +1,22 @@
|
|||
import { List } from 'immutable'
|
||||
|
||||
import { isPendingTransaction } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { isStatusAwaitingConfirmation } from 'src/logic/safe/store/models/types/gateway.d'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
|
||||
import { Transaction as GatewayTransaction } from 'src/logic/safe/store/models/types/gateway'
|
||||
import { addressInList } from 'src/routes/safe/components/Transactions/GatewayTransactions/utils'
|
||||
import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions'
|
||||
|
||||
export const getAwaitingTransactions = (
|
||||
allTransactions: List<Transaction>,
|
||||
cancellationTxs,
|
||||
cancellationTxs: CancellationTransactions,
|
||||
userAccount: string,
|
||||
): List<Transaction> => {
|
||||
return allTransactions.filter((tx) => {
|
||||
const cancelTx = !!tx.nonce && !isNaN(Number(tx.nonce)) ? cancellationTxs.get(`${tx.nonce}`) : null
|
||||
|
||||
// The transaction is not executed and is not cancelled, nor pending, so it's still waiting confirmations
|
||||
if (!tx.executionTxHash && !tx.cancelled && !isPendingTransaction(tx, cancelTx)) {
|
||||
if (!tx.executionTxHash && !tx.cancelled && cancelTx && !isPendingTransaction(tx, cancelTx)) {
|
||||
// Then we check if the waiting confirmations are not from the current user, otherwise, filters this transaction
|
||||
const transactionWaitingUser = tx.confirmations.filter(({ owner }) => owner !== userAccount)
|
||||
return transactionWaitingUser.size > 0
|
||||
|
@ -21,3 +25,18 @@ export const getAwaitingTransactions = (
|
|||
return false
|
||||
})
|
||||
}
|
||||
|
||||
export const getAwaitingGatewayTransactions = (
|
||||
allTransactions: GatewayTransaction[],
|
||||
userAccount: string,
|
||||
): GatewayTransaction[] => {
|
||||
return allTransactions.filter((tx) => {
|
||||
// The transaction is not executed and is not cancelled, nor pending, so it's still waiting confirmations
|
||||
if (isStatusAwaitingConfirmation(tx.txStatus)) {
|
||||
// Then we check if the waiting confirmations are not from the current user, otherwise, filters this transaction
|
||||
return addressInList(tx.executionInfo?.missingSigners)(userAccount)
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import { BigNumber } from 'bignumber.js'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { calculateGasOf, EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSigner'
|
||||
import { List } from 'immutable'
|
||||
import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
|
||||
import axios from 'axios'
|
||||
import { getRpcServiceUrl, usesInfuraRPC } from 'src/config'
|
||||
import { sameString } from 'src/utils/strings'
|
||||
|
||||
// 21000 - additional gas costs (e.g. base tx costs, transfer costs)
|
||||
export const MINIMUM_TRANSACTION_GAS = 21000
|
||||
|
||||
// Receives the response data of the safe method requiredTxGas() and parses it to get the gas amount
|
||||
const parseRequiredTxGasResponse = (data: string): number => {
|
||||
|
@ -88,7 +93,7 @@ export const getDataFromNodeErrorMessage = (errorMessage: string): string | unde
|
|||
}
|
||||
}
|
||||
|
||||
export const getGasEstimationTxResponse = async (txConfig: {
|
||||
const estimateGasWithWeb3Provider = async (txConfig: {
|
||||
to: string
|
||||
from: string
|
||||
data: string
|
||||
|
@ -116,12 +121,56 @@ export const getGasEstimationTxResponse = async (txConfig: {
|
|||
|
||||
return new BigNumber(estimationData.substring(138), 16).toNumber()
|
||||
}
|
||||
|
||||
// This will fail in case that we receive an EMPTY_DATA on the GETH node gas estimation (for version < v1.9.24 of geth nodes)
|
||||
// We cannot throw this error above because it will be captured again on the catch block bellow
|
||||
throw new Error('Error while estimating the gas required for tx')
|
||||
}
|
||||
|
||||
const estimateGasWithRPCCall = async (txConfig: {
|
||||
to: string
|
||||
from: string
|
||||
data: string
|
||||
gasPrice?: number
|
||||
gas?: number
|
||||
}): Promise<number> => {
|
||||
try {
|
||||
const { data } = await axios.post(getRpcServiceUrl(), {
|
||||
jsonrpc: '2.0',
|
||||
method: 'eth_call',
|
||||
id: 1,
|
||||
params: [
|
||||
{
|
||||
...txConfig,
|
||||
gasPrice: web3ReadOnly.utils.toHex(txConfig.gasPrice || 0),
|
||||
gas: txConfig.gas ? web3ReadOnly.utils.toHex(txConfig.gas) : undefined,
|
||||
},
|
||||
'latest',
|
||||
],
|
||||
})
|
||||
|
||||
const { error } = data
|
||||
if (error?.data) {
|
||||
return new BigNumber(data.error.data.substring(138), 16).toNumber()
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Gas estimation endpoint errored: ', error.message)
|
||||
}
|
||||
throw new Error('Error while estimating the gas required for tx')
|
||||
}
|
||||
|
||||
export const getGasEstimationTxResponse = async (txConfig: {
|
||||
to: string
|
||||
from: string
|
||||
data: string
|
||||
gasPrice?: number
|
||||
gas?: number
|
||||
}): Promise<number> => {
|
||||
// If we are in a infura supported network we estimate using infura
|
||||
if (usesInfuraRPC) {
|
||||
return estimateGasWithRPCCall(txConfig)
|
||||
}
|
||||
// Otherwise we estimate using the current connected provider
|
||||
return estimateGasWithWeb3Provider(txConfig)
|
||||
}
|
||||
|
||||
const calculateMinimumGasForTransaction = async (
|
||||
additionalGasBatches: number[],
|
||||
safeAddress: string,
|
||||
|
@ -131,15 +180,19 @@ const calculateMinimumGasForTransaction = async (
|
|||
): Promise<number> => {
|
||||
for (const additionalGas of additionalGasBatches) {
|
||||
const amountOfGasToTryTx = txGasEstimation + dataGasEstimation + additionalGas
|
||||
console.info(`Estimating transaction creation with gas amount: ${amountOfGasToTryTx}`)
|
||||
try {
|
||||
await getGasEstimationTxResponse({
|
||||
const estimation = await getGasEstimationTxResponse({
|
||||
to: safeAddress,
|
||||
from: safeAddress,
|
||||
data: estimateData,
|
||||
gasPrice: 0,
|
||||
gas: amountOfGasToTryTx,
|
||||
})
|
||||
return txGasEstimation + additionalGas
|
||||
if (estimation > 0) {
|
||||
console.info(`Gas estimation successfully finished with gas amount: ${amountOfGasToTryTx}`)
|
||||
return amountOfGasToTryTx
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error trying to estimate gas with amount: ${amountOfGasToTryTx}`)
|
||||
}
|
||||
|
@ -154,6 +207,7 @@ export const estimateGasForTransactionCreation = async (
|
|||
to: string,
|
||||
valueInWei: string,
|
||||
operation: number,
|
||||
safeTxGas?: number,
|
||||
): Promise<number> => {
|
||||
try {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
|
@ -163,19 +217,21 @@ export const estimateGasForTransactionCreation = async (
|
|||
to: safeAddress,
|
||||
from: safeAddress,
|
||||
data: estimateData,
|
||||
gas: safeTxGas ? safeTxGas : undefined,
|
||||
})
|
||||
|
||||
const txGasEstimation = gasEstimationResponse + 10000
|
||||
if (safeTxGas) {
|
||||
return gasEstimationResponse
|
||||
}
|
||||
|
||||
// 21000 - additional gas costs (e.g. base tx costs, transfer costs)
|
||||
const dataGasEstimation = parseRequiredTxGasResponse(estimateData) + 21000
|
||||
const dataGasEstimation = parseRequiredTxGasResponse(estimateData)
|
||||
const additionalGasBatches = [0, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000]
|
||||
|
||||
return await calculateMinimumGasForTransaction(
|
||||
additionalGasBatches,
|
||||
safeAddress,
|
||||
estimateData,
|
||||
txGasEstimation,
|
||||
gasEstimationResponse,
|
||||
dataGasEstimation,
|
||||
)
|
||||
} catch (error) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Map } from 'immutable'
|
||||
import isEqual from 'lodash.isequal'
|
||||
|
||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
|
||||
|
@ -6,9 +6,9 @@ import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
|||
const isStateSubset = (superObj, subObj) => {
|
||||
return Object.keys(subObj).every((key) => {
|
||||
if (subObj[key] && typeof subObj[key] == 'object') {
|
||||
if (Map.isMap(subObj[key]) || subObj[key].size >= 0) {
|
||||
if (typeof subObj[key] === 'object' || subObj[key].length >= 0) {
|
||||
// If type is Immutable Map, List or Object we use Immutable equals
|
||||
return superObj[key].equals(subObj[key])
|
||||
return isEqual(superObj[key], subObj[key])
|
||||
}
|
||||
return isStateSubset(superObj[key], subObj[key])
|
||||
}
|
||||
|
|
|
@ -180,7 +180,7 @@ type SpendingLimitTxParams = {
|
|||
resetTimeMin: number
|
||||
resetBaseMin: number
|
||||
}
|
||||
safeAddress
|
||||
safeAddress: string
|
||||
}
|
||||
|
||||
export const setSpendingLimitTx = ({
|
||||
|
@ -190,7 +190,7 @@ export const setSpendingLimitTx = ({
|
|||
const spendingLimitContract = getSpendingLimitContract()
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
return {
|
||||
const txArgs: CreateTransactionArgs = {
|
||||
safeAddress,
|
||||
to: SPENDING_LIMIT_MODULE_ADDRESS,
|
||||
valueInWei: ZERO_VALUE,
|
||||
|
@ -206,6 +206,8 @@ export const setSpendingLimitTx = ({
|
|||
operation: CALL,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX,
|
||||
}
|
||||
|
||||
return txArgs
|
||||
}
|
||||
|
||||
export const setSpendingLimitMultiSendTx = (args: SpendingLimitTxParams): MultiSendTx => {
|
||||
|
|
|
@ -15,21 +15,8 @@ import { TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
|||
import updateSafe from 'src/logic/safe/store/actions/updateSafe'
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import {
|
||||
safeActiveTokensSelector,
|
||||
safeBalancesSelector,
|
||||
safeBlacklistedTokensSelector,
|
||||
safeEthBalanceSelector,
|
||||
safeSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { safeActiveTokensSelector, safeBlacklistedTokensSelector, safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import { tokensSelector } from 'src/logic/tokens/store/selectors'
|
||||
import { currencyValuesSelector } from 'src/logic/currencyValues/store/selectors'
|
||||
|
||||
const noFunc = (): void => {}
|
||||
|
||||
const updateSafeValue = (address: string) => (valueToUpdate: Partial<SafeRecordProps>) =>
|
||||
updateSafe({ address, ...valueToUpdate })
|
||||
|
||||
interface ExtractedData {
|
||||
balances: Map<string, string>
|
||||
|
@ -78,11 +65,8 @@ const fetchSafeTokens = (safeAddress: string) => async (
|
|||
}
|
||||
|
||||
const tokenCurrenciesBalances = await backOff(() => fetchTokenCurrenciesBalances(safeAddress))
|
||||
const currentEthBalance = safeEthBalanceSelector(state)
|
||||
const safeBalances = safeBalancesSelector(state)
|
||||
const alreadyActiveTokens = safeActiveTokensSelector(state)
|
||||
const blacklistedTokens = safeBlacklistedTokensSelector(state)
|
||||
const currencyValues = currencyValuesSelector(state)
|
||||
|
||||
const { balances, currencyList, ethBalance, tokens } = tokenCurrenciesBalances.reduce<ExtractedData>(
|
||||
extractDataFromResult(currentTokens),
|
||||
|
@ -100,24 +84,10 @@ const fetchSafeTokens = (safeAddress: string) => async (
|
|||
balances.keySeq().toSet().subtract(blacklistedTokens),
|
||||
)
|
||||
|
||||
const update = updateSafeValue(safeAddress)
|
||||
const updateActiveTokens = activeTokens.equals(alreadyActiveTokens) ? noFunc : update({ activeTokens })
|
||||
const updateBalances = balances.equals(safeBalances) ? noFunc : update({ balances })
|
||||
const updateEthBalance = ethBalance === currentEthBalance ? noFunc : update({ ethBalance })
|
||||
const storedCurrencyBalances = currencyValues?.get(safeAddress)?.get('currencyBalances')
|
||||
|
||||
const updateCurrencies = currencyList.equals(storedCurrencyBalances)
|
||||
? noFunc
|
||||
: setCurrencyBalances(safeAddress, currencyList)
|
||||
|
||||
const updateTokens = tokens.size === 0 ? noFunc : addTokens(tokens)
|
||||
|
||||
batch(() => {
|
||||
dispatch(updateActiveTokens)
|
||||
dispatch(updateBalances)
|
||||
dispatch(updateEthBalance)
|
||||
dispatch(updateCurrencies)
|
||||
dispatch(updateTokens)
|
||||
dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance }))
|
||||
dispatch(setCurrencyBalances(safeAddress, currencyList))
|
||||
dispatch(addTokens(tokens))
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error fetching active token list', err)
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import { Map } from 'immutable'
|
||||
import { handleActions } from 'redux-actions'
|
||||
import { List, Map } from 'immutable'
|
||||
import { Action, handleActions } from 'redux-actions'
|
||||
|
||||
import { ADD_TOKEN } from 'src/logic/tokens/store/actions/addToken'
|
||||
import { ADD_TOKENS } from 'src/logic/tokens/store/actions/saveTokens'
|
||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const TOKEN_REDUCER_ID = 'tokens'
|
||||
|
||||
export type TokenState = Map<string, Token>
|
||||
|
||||
export default handleActions(
|
||||
type TokensPayload = { tokens: List<Token> }
|
||||
type TokenPayload = { token: Token }
|
||||
type Payloads = TokensPayload | TokenPayload
|
||||
|
||||
export default handleActions<AppReduxState['tokens'], Payloads>(
|
||||
{
|
||||
[ADD_TOKENS]: (state: TokenState, action) => {
|
||||
[ADD_TOKENS]: (state: TokenState, action: Action<TokensPayload>) => {
|
||||
const { tokens } = action.payload
|
||||
|
||||
return state.withMutations((map) => {
|
||||
|
@ -20,7 +25,7 @@ export default handleActions(
|
|||
})
|
||||
})
|
||||
},
|
||||
[ADD_TOKEN]: (state: TokenState, action) => {
|
||||
[ADD_TOKEN]: (state: TokenState, action: Action<TokenPayload>) => {
|
||||
const { token } = action.payload
|
||||
const { address: tokenAddress } = token
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@ import { isSendERC721Transaction } from 'src/logic/collectibles/utils'
|
|||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||
import { ALTERNATIVE_TOKEN_ABI } from 'src/logic/tokens/utils/alternativeAbi'
|
||||
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||
import { isEmptyData } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
|
||||
import { BuildTx, isEmptyData, ServiceTx } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { CALL } from 'src/logic/safe/transactions'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
|
||||
|
@ -35,7 +34,7 @@ export const isAddressAToken = async (tokenAddress: string): Promise<boolean> =>
|
|||
return call !== '0x'
|
||||
}
|
||||
|
||||
export const isTokenTransfer = (tx: TxServiceModel): boolean => {
|
||||
export const isTokenTransfer = (tx: BuildTx['tx']): boolean => {
|
||||
return (
|
||||
!isEmptyData(tx.data) &&
|
||||
// Check if contains 'transfer' method code
|
||||
|
@ -70,11 +69,11 @@ export const getERC20DecimalsAndSymbol = async (
|
|||
return tokenInfo
|
||||
}
|
||||
|
||||
export const isSendERC20Transaction = async (tx: TxServiceModel): Promise<boolean> => {
|
||||
export const isSendERC20Transaction = async (tx: BuildTx['tx']): Promise<boolean> => {
|
||||
let isSendTokenTx = !isSendERC721Transaction(tx) && isTokenTransfer(tx)
|
||||
|
||||
if (isSendTokenTx) {
|
||||
const { decimals, symbol } = await getERC20DecimalsAndSymbol(tx.to)
|
||||
const { decimals, symbol } = await getERC20DecimalsAndSymbol((tx as ServiceTx).to)
|
||||
|
||||
// some contracts may implement the same methods as in ERC20 standard
|
||||
// we may falsely treat them as tokens, so in case we get any errors when getting token info
|
||||
|
|
|
@ -47,3 +47,5 @@ export const isUserAnOwnerOfAnySafe = (safes: List<SafeRecord> | SafeRecord[], u
|
|||
safes.some((safe: SafeRecord) => isUserAnOwner(safe, userAccount))
|
||||
|
||||
export const isValidEnsName = (name: string): boolean => /^([\w-]+\.)+(eth|test|xyz|luxe|ewc)$/.test(name)
|
||||
|
||||
export const isValidCryptoDomainName = (name: string): boolean => /^([\w-]+\.)+(crypto)$/.test(name)
|
||||
|
|
|
@ -66,3 +66,12 @@ export const calculateGasOf = async (txConfig: {
|
|||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
export const getUserNonce = async (userAddress: string): Promise<number> => {
|
||||
const web3 = getWeb3()
|
||||
try {
|
||||
return await web3.eth.getTransactionCount(userAddress, 'pending')
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import Web3 from 'web3'
|
||||
import { provider as Provider } from 'web3-core'
|
||||
import { ContentHash } from 'web3-eth-ens'
|
||||
|
||||
import { sameAddress } from './ethAddresses'
|
||||
import { EMPTY_DATA } from './ethTransactions'
|
||||
import { ProviderProps } from './store/model/provider'
|
||||
import { NODE_ENV } from 'src/utils/constants'
|
||||
import { getRpcServiceUrl } from 'src/config'
|
||||
import { isValidCryptoDomainName } from 'src/logic/wallets/ethAddresses'
|
||||
import { getAddressFromUnstoppableDomain } from './utils/unstoppableDomains'
|
||||
|
||||
export const WALLET_PROVIDER = {
|
||||
SAFE: 'SAFE',
|
||||
|
@ -85,7 +86,12 @@ export const getProviderInfo = async (web3Instance: Web3, providerName = 'Wallet
|
|||
}
|
||||
}
|
||||
|
||||
export const getAddressFromENS = (name: string): Promise<string> => web3.eth.ens.getAddress(name)
|
||||
export const getAddressFromDomain = (name: string): Promise<string> => {
|
||||
if (isValidCryptoDomainName(name)) {
|
||||
return getAddressFromUnstoppableDomain(name)
|
||||
}
|
||||
return web3.eth.ens.getAddress(name)
|
||||
}
|
||||
|
||||
export const getContentFromENS = (name: string): Promise<ContentHash> => web3.eth.ens.getContenthash(name)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import ReactGA from 'react-ga'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
import addProvider from './addProvider'
|
||||
|
||||
|
@ -8,7 +9,6 @@ import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackb
|
|||
import { getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { makeProvider } from 'src/logic/wallets/store/model/provider'
|
||||
import { updateStoredTransactionsStatus } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
export const processProviderResponse = (dispatch, provider) => {
|
||||
const walletRecord = makeProvider(provider)
|
||||
|
@ -44,7 +44,6 @@ const handleProviderNotification = (provider, dispatch) => {
|
|||
action: 'Connect a wallet',
|
||||
label: provider.name,
|
||||
})
|
||||
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.WALLET_CONNECTED_MSG)))
|
||||
} else {
|
||||
dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.UNLOCK_WALLET_MSG)))
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
|||
import { createAction } from 'redux-actions'
|
||||
|
||||
import { onboard } from 'src/components/ConnectButton'
|
||||
import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications'
|
||||
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
|
||||
import { resetWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
export const REMOVE_PROVIDER = 'REMOVE_PROVIDER'
|
||||
|
@ -15,9 +13,4 @@ export default () => (dispatch: Dispatch): void => {
|
|||
resetWeb3()
|
||||
|
||||
dispatch(removeProvider())
|
||||
dispatch(
|
||||
enqueueSnackbar(
|
||||
enhanceSnackbarForAction(NOTIFICATIONS.WALLET_DISCONNECTED_MSG, NOTIFICATIONS.WALLET_DISCONNECTED_MSG.key),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import UnstoppableResolution from '@unstoppabledomains/resolution'
|
||||
import { getRpcServiceUrl } from 'src/config'
|
||||
|
||||
let unstoppableResolver
|
||||
|
||||
export const getAddressFromUnstoppableDomain = (name: string) => {
|
||||
if (!unstoppableResolver) {
|
||||
unstoppableResolver = new UnstoppableResolution({
|
||||
blockchain: {
|
||||
cns: {
|
||||
url: getRpcServiceUrl(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return unstoppableResolver.addr(name, 'ETH')
|
||||
}
|
|
@ -24,19 +24,13 @@ import { networkSelector, providerNameSelector, userAccountSelector } from 'src/
|
|||
import { useSelector } from 'react-redux'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
|
||||
import { SafeProps } from 'src/routes/open/container/Open'
|
||||
|
||||
const { useEffect } = React
|
||||
|
||||
const getSteps = () => ['Name', 'Owners and confirmations', 'Review']
|
||||
|
||||
type SafeProps = {
|
||||
name: string
|
||||
ownerAddresses: any
|
||||
ownerNames: string
|
||||
threshold: string
|
||||
}
|
||||
|
||||
type InitialValuesForm = {
|
||||
export type InitialValuesForm = {
|
||||
owner0Address?: string
|
||||
owner0Name?: string
|
||||
confirmations: string
|
||||
|
|
|
@ -157,8 +157,8 @@ const SafeOwnersForm = (props): React.ReactElement => {
|
|||
<br />
|
||||
<br />
|
||||
Add additional owners (e.g. wallets of your teammates) and specify how many of them have to confirm a
|
||||
transaction before it gets executed. In general, the more confirmations required, the more secure is your
|
||||
Safe.
|
||||
transaction before it gets executed. You can also add/remove owners and change the signature threshold after
|
||||
your Safe is created.
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<Hairline />
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { Loader } from '@gnosis.pm/safe-react-components'
|
||||
import queryString from 'query-string'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import Opening from 'src/routes/opening'
|
||||
import { Layout } from 'src/routes/open/components/Layout'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { PromiEvent, TransactionReceipt } from 'web3-core'
|
||||
|
||||
import { SafeDeployment } from 'src/routes/opening'
|
||||
import { InitialValuesForm, Layout } from 'src/routes/open/components/Layout'
|
||||
import Page from 'src/components/layout/Page'
|
||||
import { getSafeDeploymentTransaction } from 'src/logic/contracts/safeContracts'
|
||||
import { checkReceiptStatus } from 'src/logic/wallets/ethTransactions'
|
||||
|
@ -23,22 +25,51 @@ import { loadFromStorage, removeFromStorage, saveToStorage } from 'src/utils/sto
|
|||
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
|
||||
import { PromiEvent, TransactionReceipt } from 'web3-core'
|
||||
import { useAnalytics } from 'src/utils/googleAnalytics'
|
||||
|
||||
const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY'
|
||||
|
||||
const validateQueryParams = (ownerAddresses, ownerNames, threshold, safeName) => {
|
||||
interface SafeCreationQueryParams {
|
||||
ownerAddresses: string | string[] | null
|
||||
ownerNames: string | string[] | null
|
||||
threshold: number | null
|
||||
safeName: string | null
|
||||
}
|
||||
|
||||
export interface SafeProps {
|
||||
name: string
|
||||
ownerAddresses: string[]
|
||||
ownerNames: string[]
|
||||
threshold: string
|
||||
}
|
||||
|
||||
const validateQueryParams = (queryParams: SafeCreationQueryParams): boolean => {
|
||||
const { ownerAddresses, ownerNames, threshold, safeName } = queryParams
|
||||
|
||||
if (!ownerAddresses || !ownerNames || !threshold || !safeName) {
|
||||
return false
|
||||
}
|
||||
if (!ownerAddresses.length || ownerNames.length === 0) {
|
||||
|
||||
if (Number.isNaN(threshold)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (Number.isNaN(Number(threshold))) {
|
||||
return false
|
||||
return threshold > 0 && threshold <= ownerAddresses.length
|
||||
}
|
||||
|
||||
const getSafePropsValuesFromQueryParams = (queryParams: SafeCreationQueryParams): SafeProps | undefined => {
|
||||
if (!validateQueryParams(queryParams)) {
|
||||
return
|
||||
}
|
||||
|
||||
const { threshold, safeName, ownerAddresses, ownerNames } = queryParams
|
||||
|
||||
return {
|
||||
name: safeName as string,
|
||||
threshold: (threshold as number).toString(),
|
||||
ownerAddresses: Array.isArray(ownerAddresses) ? ownerAddresses : [ownerAddresses as string],
|
||||
ownerNames: Array.isArray(ownerNames) ? ownerNames : [ownerNames as string],
|
||||
}
|
||||
return threshold <= ownerAddresses.length
|
||||
}
|
||||
|
||||
export const getSafeProps = async (
|
||||
|
@ -54,7 +85,7 @@ export const getSafeProps = async (
|
|||
return safeProps
|
||||
}
|
||||
|
||||
export const createSafe = (values, userAccount) => {
|
||||
export const createSafe = (values: InitialValuesForm, userAccount: string): PromiEvent<TransactionReceipt> => {
|
||||
const confirmations = getThresholdFrom(values)
|
||||
const name = getSafeNameFrom(values)
|
||||
const ownersNames = getNamesFrom(values)
|
||||
|
@ -86,24 +117,27 @@ const Open = (): React.ReactElement => {
|
|||
const [loading, setLoading] = useState(false)
|
||||
const [showProgress, setShowProgress] = useState(false)
|
||||
const [creationTxPromise, setCreationTxPromise] = useState<PromiEvent<TransactionReceipt>>()
|
||||
const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState<any>()
|
||||
const [safePropsFromUrl, setSafePropsFromUrl] = useState()
|
||||
const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState<{ txHash?: string } | undefined>()
|
||||
const [safePropsFromUrl, setSafePropsFromUrl] = useState<SafeProps | undefined>()
|
||||
const userAccount = useSelector(userAccountSelector)
|
||||
const dispatch = useDispatch()
|
||||
const location = useLocation()
|
||||
const { trackEvent } = useAnalytics()
|
||||
|
||||
useEffect(() => {
|
||||
// #122: Allow to migrate an old Multisig by passing the parameters to the URL.
|
||||
const query = queryString.parse(window.location.search, { arrayFormat: 'comma' })
|
||||
const query = queryString.parse(location.search, { arrayFormat: 'comma' })
|
||||
const { name, owneraddresses, ownernames, threshold } = query
|
||||
if (validateQueryParams(owneraddresses, ownernames, threshold, name)) {
|
||||
setSafePropsFromUrl({
|
||||
name,
|
||||
ownerAddresses: owneraddresses,
|
||||
ownerNames: ownernames,
|
||||
threshold,
|
||||
} as any)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const safeProps = getSafePropsValuesFromQueryParams({
|
||||
ownerAddresses: owneraddresses,
|
||||
ownerNames: ownernames,
|
||||
threshold: Number(threshold),
|
||||
safeName: name as string | null,
|
||||
})
|
||||
|
||||
setSafePropsFromUrl(safeProps)
|
||||
}, [location])
|
||||
|
||||
// check if there is a safe being created
|
||||
useEffect(() => {
|
||||
|
@ -121,7 +155,7 @@ const Open = (): React.ReactElement => {
|
|||
load()
|
||||
}, [])
|
||||
|
||||
const createSafeProxy = async (formValues?: any) => {
|
||||
const createSafeProxy = async (formValues?: InitialValuesForm) => {
|
||||
let values = formValues
|
||||
|
||||
// save form values, used when the user rejects the TX and wants to retry
|
||||
|
@ -132,7 +166,7 @@ const Open = (): React.ReactElement => {
|
|||
values = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
|
||||
}
|
||||
|
||||
const promiEvent = createSafe(values, userAccount)
|
||||
const promiEvent = createSafe(values as InitialValuesForm, userAccount)
|
||||
setCreationTxPromise(promiEvent)
|
||||
setShowProgress(true)
|
||||
}
|
||||
|
@ -147,7 +181,7 @@ const Open = (): React.ReactElement => {
|
|||
|
||||
await dispatch(addOrUpdateSafe(safeProps))
|
||||
|
||||
ReactGA.event({
|
||||
trackEvent({
|
||||
category: 'User',
|
||||
action: 'Created a safe',
|
||||
})
|
||||
|
@ -186,7 +220,7 @@ const Open = (): React.ReactElement => {
|
|||
return (
|
||||
<Page>
|
||||
{showProgress ? (
|
||||
<Opening
|
||||
<SafeDeployment
|
||||
creationTxHash={safeCreationPendingInfo?.txHash}
|
||||
onCancel={onCancel}
|
||||
onRetry={onRetry}
|
||||
|
|
|
@ -20,6 +20,7 @@ import LoaderDotsSvg from './assets/loader-dots.svg'
|
|||
import SuccessSvg from './assets/success.svg'
|
||||
import VaultErrorSvg from './assets/vault-error.svg'
|
||||
import VaultSvg from './assets/vault.svg'
|
||||
import { PromiEvent, TransactionReceipt } from 'web3-core'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: grid;
|
||||
|
@ -56,13 +57,17 @@ const Body = styled.div`
|
|||
const CardTitle = styled.div`
|
||||
font-size: 20px;
|
||||
`
|
||||
const FullParagraph = styled(Paragraph)`
|
||||
background-color: ${(p) => (p.inverseColors ? connected : background)};
|
||||
color: ${(p) => (p.inverseColors ? background : connected)};
|
||||
|
||||
interface FullParagraphProps {
|
||||
inversecolors: string
|
||||
}
|
||||
|
||||
const FullParagraph = styled(Paragraph)<FullParagraphProps>`
|
||||
background-color: ${(p) => (p.inversecolors ? connected : background)};
|
||||
color: ${(p) => (p.inversecolors ? background : connected)};
|
||||
padding: 24px;
|
||||
font-size: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||
`
|
||||
|
||||
|
@ -95,16 +100,21 @@ const BackButton = styled(Button)`
|
|||
margin: 20px auto 0;
|
||||
`
|
||||
|
||||
// type Props = {
|
||||
// provider: string
|
||||
// creationTxHash: Promise<any>
|
||||
// submittedPromise: Promise<any>
|
||||
// onRetry: () => void
|
||||
// onSuccess: () => void
|
||||
// onCancel: () => void
|
||||
// }
|
||||
type Props = {
|
||||
creationTxHash?: string
|
||||
submittedPromise?: PromiEvent<TransactionReceipt>
|
||||
onRetry: () => void
|
||||
onSuccess: (createdSafeAddress: string) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submittedPromise }): React.ReactElement => {
|
||||
export const SafeDeployment = ({
|
||||
creationTxHash,
|
||||
onCancel,
|
||||
onRetry,
|
||||
onSuccess,
|
||||
submittedPromise,
|
||||
}: Props): React.ReactElement => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const [safeCreationTxHash, setSafeCreationTxHash] = useState('')
|
||||
|
@ -326,7 +336,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submitte
|
|||
<BodyLoader>{!error && stepIndex <= 4 && <Img alt="Loader dots" src={LoaderDotsSvg} />}</BodyLoader>
|
||||
|
||||
<BodyInstruction>
|
||||
<FullParagraph color="primary" inverseColors={confirmationStep} noMargin size="md">
|
||||
<FullParagraph color="primary" inversecolors={confirmationStep.toString()} noMargin size="md">
|
||||
{error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction}
|
||||
</FullParagraph>
|
||||
</BodyInstruction>
|
||||
|
@ -350,5 +360,3 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submitte
|
|||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default SafeDeployment
|
||||
|
|
|
@ -10,6 +10,7 @@ import { useDispatch, useSelector } from 'react-redux'
|
|||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { xs } from 'src/theme/variables'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
|
||||
const useStyles = makeStyles(
|
||||
createStyles({
|
||||
|
@ -48,6 +49,7 @@ export const EllipsisTransactionDetails = ({
|
|||
|
||||
const dispatch = useDispatch()
|
||||
const currentSafeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const isOwnerConnected = useSelector(grantedSelector)
|
||||
|
||||
const handleClick = (event) => setAnchorEl(event.currentTarget)
|
||||
|
||||
|
@ -65,7 +67,7 @@ export const EllipsisTransactionDetails = ({
|
|||
<Menu anchorEl={anchorEl} id="simple-menu" keepMounted onClose={closeMenuHandler} open={Boolean(anchorEl)}>
|
||||
{sendModalOpenHandler
|
||||
? [
|
||||
<MenuItem key="send-again-button" onClick={sendModalOpenHandler}>
|
||||
<MenuItem key="send-again-button" onClick={sendModalOpenHandler} disabled={!isOwnerConnected}>
|
||||
Send Again
|
||||
</MenuItem>,
|
||||
<Divider key="divider" />,
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useTransactions } from 'src/routes/safe/container/hooks/useTransactions'
|
||||
import { ButtonLink, Loader } from '@gnosis.pm/safe-react-components'
|
||||
import { Transaction } from 'src/logic/safe/store/models/types/transactions.d'
|
||||
|
||||
const Transactions = (): React.ReactElement => {
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const [limit] = useState(50)
|
||||
const [offset, setOffset] = useState(0)
|
||||
const [maxPages, setMaxPages] = useState(0)
|
||||
const { transactions, totalTransactionsCount } = useTransactions({ offset, limit })
|
||||
const [transactionsByPage, setTransactionsByPage] = useState(transactions)
|
||||
|
||||
useEffect(() => {
|
||||
const currentPage = Math.floor(offset / limit) + 1
|
||||
const maxPages = Math.ceil(totalTransactionsCount / limit)
|
||||
setCurrentPage(currentPage)
|
||||
setMaxPages(maxPages)
|
||||
const newTransactionsByPage = transactions ? transactions.slice(offset, offset * 2 || limit) : []
|
||||
setTransactionsByPage(newTransactionsByPage)
|
||||
}, [offset, limit, totalTransactionsCount, transactions])
|
||||
|
||||
// TODO: Remove this once we implement infinite scroll
|
||||
const nextPageButtonHandler = () => {
|
||||
setOffset(offset + limit)
|
||||
}
|
||||
|
||||
const previousPageButtonHandler = () => {
|
||||
setOffset(offset > 0 ? offset - limit : offset)
|
||||
}
|
||||
|
||||
if (!transactionsByPage) return <div>No txs available for safe</div>
|
||||
|
||||
if (!transactionsByPage.length) return <Loader size="lg" />
|
||||
|
||||
return (
|
||||
<>
|
||||
{transactionsByPage.map((tx: Transaction, index) => {
|
||||
let txHash = ''
|
||||
if ('transactionHash' in tx) {
|
||||
txHash = tx.transactionHash as string
|
||||
}
|
||||
if ('txHash' in tx) {
|
||||
txHash = tx.txHash
|
||||
}
|
||||
return <div key={txHash || tx.executionDate || index}>Tx hash: {txHash}</div>
|
||||
})}
|
||||
<ButtonLink color="primary" onClick={previousPageButtonHandler} disabled={currentPage === 1}>
|
||||
Previous Page
|
||||
</ButtonLink>
|
||||
<ButtonLink color="primary" onClick={nextPageButtonHandler} disabled={currentPage >= maxPages}>
|
||||
Next Page
|
||||
</ButtonLink>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Transactions
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { GenericModal, Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components'
|
||||
import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1'
|
||||
import styled from 'styled-components'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import AddressInfo from 'src/components/AddressInfo'
|
||||
import DividerLine from 'src/components/DividerLine'
|
||||
|
@ -16,17 +16,24 @@ import Img from 'src/components/layout/Img'
|
|||
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { 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 { DELEGATE_CALL, TX_NOTIFICATION_TYPES, CALL } from 'src/logic/safe/transactions'
|
||||
import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend'
|
||||
|
||||
import GasEstimationInfo from './GasEstimationInfo'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { TransactionParams } from './AppFrame'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { safeThresholdSelector } from 'src/logic/safe/store/selectors'
|
||||
import Modal from 'src/components/Modal'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import Hairline from 'src/components/layout/Hairline'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { md, lg, sm } from 'src/theme/variables'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const isTxValid = (t: Transaction): boolean => {
|
||||
if (!['string', 'number'].includes(typeof t.value)) {
|
||||
|
@ -71,6 +78,16 @@ const StyledTextBox = styled(TextBox)`
|
|||
|
||||
const Container = styled.div`
|
||||
max-width: 480px;
|
||||
padding: ${md} ${lg};
|
||||
`
|
||||
|
||||
const ModalFooter = styled(Row)`
|
||||
padding: ${md} ${lg};
|
||||
justify-content: center;
|
||||
`
|
||||
const TransactionFeesWrapper = styled.div`
|
||||
background-color: ${({ theme }) => theme.colors.background};
|
||||
padding: ${sm} ${lg};
|
||||
`
|
||||
|
||||
type OwnProps = {
|
||||
|
@ -101,8 +118,18 @@ export const ConfirmTransactionModal = ({
|
|||
onTxReject,
|
||||
}: OwnProps): React.ReactElement | null => {
|
||||
const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0)
|
||||
const threshold = useSelector(safeThresholdSelector) || 1
|
||||
|
||||
const txRecipient: string | undefined = useMemo(() => (txs.length > 1 ? MULTI_SEND_ADDRESS : txs[0]?.to), [txs])
|
||||
const txData: string | undefined = useMemo(() => (txs.length > 1 ? encodeMultiSendCall(txs) : txs[0]?.data), [txs])
|
||||
const txValue: string | undefined = useMemo(() => (txs.length > 1 ? '0' : txs[0]?.value), [txs])
|
||||
const operation = useMemo(() => (txs.length > 1 ? DELEGATE_CALL : CALL), [txs])
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
gasPriceFormatted,
|
||||
gasEstimation,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
|
@ -110,9 +137,12 @@ export const ConfirmTransactionModal = ({
|
|||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
} = useEstimateTransactionGas({
|
||||
txData: encodeMultiSendCall(txs),
|
||||
txRecipient: MULTI_SEND_ADDRESS,
|
||||
operation: DELEGATE_CALL,
|
||||
txData: txData || '',
|
||||
txRecipient,
|
||||
operation,
|
||||
txAmount: txValue,
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -136,21 +166,25 @@ export const ConfirmTransactionModal = ({
|
|||
onClose()
|
||||
}
|
||||
|
||||
const confirmTransactions = async () => {
|
||||
const txData = encodeMultiSendCall(txs)
|
||||
const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED')
|
||||
|
||||
const confirmTransactions = async (txParameters: TxParameters) => {
|
||||
await dispatch(
|
||||
createTransaction(
|
||||
{
|
||||
safeAddress,
|
||||
to: MULTI_SEND_ADDRESS,
|
||||
valueInWei: '0',
|
||||
to: txRecipient,
|
||||
valueInWei: txValue,
|
||||
txData,
|
||||
operation: DELEGATE_CALL,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
operation,
|
||||
origin: app.id,
|
||||
navigateToTransactionsTab: false,
|
||||
safeTxGas: Math.max(params?.safeTxGas || 0, estimatedSafeTxGas),
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas
|
||||
? Number(txParameters.safeTxGas)
|
||||
: Math.max(params?.safeTxGas || 0, estimatedSafeTxGas),
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
},
|
||||
handleUserConfirmation,
|
||||
handleTxRejection,
|
||||
|
@ -158,82 +192,129 @@ export const ConfirmTransactionModal = ({
|
|||
)
|
||||
}
|
||||
|
||||
const closeEditModalCallback = (txParameters: TxParameters) => {
|
||||
const oldGasPrice = Number(gasPriceFormatted)
|
||||
const newGasPrice = Number(txParameters.ethGasPrice)
|
||||
const oldSafeTxGas = Number(gasEstimation)
|
||||
const newSafeTxGas = Number(txParameters.safeTxGas)
|
||||
|
||||
if (newGasPrice && oldGasPrice !== newGasPrice) {
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
}
|
||||
|
||||
const areTxsMalformed = txs.some((t) => !isTxValid(t))
|
||||
|
||||
const body = areTxsMalformed ? (
|
||||
<>
|
||||
<IconText>
|
||||
<Icon color="error" size="md" type="info" />
|
||||
<Title size="xs">Transaction error</Title>
|
||||
</IconText>
|
||||
<Text size="lg">
|
||||
This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of this
|
||||
Safe App for more information.
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Container>
|
||||
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<DividerLine withArrow />
|
||||
{txs.map((tx, index) => (
|
||||
<Wrapper key={index}>
|
||||
<Collapse description={<AddressInfo safeAddress={tx.to} />} title={`Transaction ${index + 1}`}>
|
||||
<CollapseContent>
|
||||
<div className="section">
|
||||
<Heading tag="h3">Value</Heading>
|
||||
<div className="value-section">
|
||||
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
|
||||
<Bold>
|
||||
{fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name}
|
||||
</Bold>
|
||||
const body = areTxsMalformed
|
||||
? () => (
|
||||
<>
|
||||
<IconText>
|
||||
<Icon color="error" size="md" type="info" />
|
||||
<Title size="xs">Transaction error</Title>
|
||||
</IconText>
|
||||
<Text size="lg">
|
||||
This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of
|
||||
this Safe App for more information.
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
: (txParameters, toggleEditMode) => {
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<DividerLine withArrow />
|
||||
{txs.map((tx, index) => (
|
||||
<Wrapper key={index}>
|
||||
<Collapse description={<AddressInfo safeAddress={tx.to} />} title={`Transaction ${index + 1}`}>
|
||||
<CollapseContent>
|
||||
<div className="section">
|
||||
<Heading tag="h3">Value</Heading>
|
||||
<div className="value-section">
|
||||
<Img alt="Ether" height={40} src={getEthAsToken('0').logoUri} />
|
||||
<Bold>
|
||||
{fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name}
|
||||
</Bold>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<Heading tag="h3">Data (hex encoded)*</Heading>
|
||||
<StyledTextBox>{tx.data}</StyledTextBox>
|
||||
</div>
|
||||
</CollapseContent>
|
||||
</Collapse>
|
||||
</Wrapper>
|
||||
))}
|
||||
<DividerLine withArrow={false} />
|
||||
{params?.safeTxGas && (
|
||||
<div className="section">
|
||||
<Heading tag="h3">SafeTxGas</Heading>
|
||||
<StyledTextBox>{params?.safeTxGas}</StyledTextBox>
|
||||
<GasEstimationInfo
|
||||
appEstimation={params.safeTxGas}
|
||||
internalEstimation={estimatedSafeTxGas}
|
||||
loading={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
<Heading tag="h3">Data (hex encoded)*</Heading>
|
||||
<StyledTextBox>{tx.data}</StyledTextBox>
|
||||
</div>
|
||||
</CollapseContent>
|
||||
</Collapse>
|
||||
</Wrapper>
|
||||
))}
|
||||
<DividerLine withArrow={false} />
|
||||
{params?.safeTxGas && (
|
||||
<div className="section">
|
||||
<Heading tag="h3">SafeTxGas</Heading>
|
||||
<StyledTextBox>{params?.safeTxGas}</StyledTextBox>
|
||||
<GasEstimationInfo
|
||||
appEstimation={params.safeTxGas}
|
||||
internalEstimation={estimatedSafeTxGas}
|
||||
loading={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
parametersStatus={getParametersStatus()}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
</Container>
|
||||
{txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (
|
||||
<TransactionFeesWrapper>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</TransactionFeesWrapper>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
title={<ModalTitle title={app.name} iconUrl={app.iconUrl} />}
|
||||
body={body}
|
||||
footer={
|
||||
<ModalFooterConfirmation
|
||||
cancelText="Cancel"
|
||||
handleCancel={handleTxRejection}
|
||||
handleOk={confirmTransactions}
|
||||
okDisabled={areTxsMalformed}
|
||||
okText="Submit"
|
||||
/>
|
||||
}
|
||||
onClose={handleTxRejection}
|
||||
/>
|
||||
<Modal description="Safe App transaction" title="Safe App transaction" open>
|
||||
<EditableTxParameters
|
||||
ethGasLimit={gasLimit}
|
||||
ethGasPrice={gasPriceFormatted}
|
||||
safeTxGas={gasEstimation.toString()}
|
||||
parametersStatus={getParametersStatus()}
|
||||
closeEditModalCallback={closeEditModalCallback}
|
||||
>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<ModalTitle title={app.name} iconUrl={app.iconUrl} onClose={handleTxRejection} />
|
||||
|
||||
<Hairline />
|
||||
|
||||
{body(txParameters, toggleEditMode)}
|
||||
|
||||
<ModalFooter align="center" grow>
|
||||
<ModalFooterConfirmation
|
||||
cancelText="Cancel"
|
||||
handleCancel={handleTxRejection}
|
||||
handleOk={() => confirmTransactions(txParameters)}
|
||||
okDisabled={areTxsMalformed}
|
||||
okText="Submit"
|
||||
/>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export type StaticAppInfo = {
|
|||
export const staticAppsList: Array<StaticAppInfo> = [
|
||||
// 1inch
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUDTSghr154kCCGguyA3cbG5HRVd2tQgNR7yD69bcsjm5`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRWtuktjfU6WMAEJFgzBC4cUfqp3FF5uN9QoWb55SdGG5`,
|
||||
disabled: false,
|
||||
networks: [ETHEREUM_NETWORK.MAINNET],
|
||||
},
|
||||
|
@ -38,13 +38,13 @@ export const staticAppsList: Array<StaticAppInfo> = [
|
|||
},
|
||||
//Balancer Exchange
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmfPLXne1UrY399RQAcjD1dmBhQrPGZWgp311CDLLW3VTn`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRb2VfPVYBrv6gi2zDywgVgTg3A19ZCRMqwL13Ez5f5AS`,
|
||||
disabled: false,
|
||||
networks: [ETHEREUM_NETWORK.MAINNET],
|
||||
},
|
||||
// Balancer Pool
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmaTucdZYLKTqaewwJduVMM8qfCDhyaEqjd8tBNae26K1J`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVaxypk2FTyfcTS9oZKxmpQziPUTu2VRhhW7sso1mGysf`,
|
||||
disabled: false,
|
||||
networks: [ETHEREUM_NETWORK.MAINNET],
|
||||
},
|
||||
|
@ -57,9 +57,15 @@ export const staticAppsList: Array<StaticAppInfo> = [
|
|||
},
|
||||
// Compound
|
||||
{ url: `${gnosisAppsUrl}/compound`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY] },
|
||||
// dHedge
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmaiemnumMaaK9wE1pbMfm3YSBUpcFNgDh3Bf6VZCZq57Q`,
|
||||
disabled: false,
|
||||
networks: [ETHEREUM_NETWORK.MAINNET],
|
||||
},
|
||||
// Idle
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmZ3oug89a3BaVqdJrJEA8CKmLF4M8snuAnphR6z1yq8V8`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVkGHm6gfQumJhnRfFCh7m2oSYwLXb51EKHzChpcV9J3N`,
|
||||
disabled: false,
|
||||
networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY],
|
||||
},
|
||||
|
@ -107,7 +113,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
|
|||
},
|
||||
// Wallet-Connect
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmWwSuByB3B3hLU5ita3RQgiSEDYtBr5LjjDCRGb8YqLKF`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmT3VxxfFtfEcvq8AeaoFAyUGxePRa2zisvnxLTrQXU5Uf`,
|
||||
disabled: false,
|
||||
networks: [
|
||||
ETHEREUM_NETWORK.MAINNET,
|
||||
|
|
|
@ -1,12 +1,49 @@
|
|||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddressInfo from 'src/components/AddressInfo'
|
||||
import { getExplorerInfo, getNetworkInfo } from 'src/config'
|
||||
import { safeSelector } from 'src/logic/safe/store/selectors'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import { border, xs } from 'src/theme/variables'
|
||||
import Block from 'src/components/layout/Block'
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
||||
const SafeInfo = () => {
|
||||
const StyledBlock = styled(Block)`
|
||||
font-size: 12px;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.5px;
|
||||
background-color: ${border};
|
||||
width: fit-content;
|
||||
padding: 5px 10px;
|
||||
margin-top: ${xs};
|
||||
margin-left: 40px;
|
||||
border-radius: 3px;
|
||||
`
|
||||
|
||||
const SafeInfo = (): React.ReactElement => {
|
||||
const { address: safeAddress = '', ethBalance, name: safeName } = useSelector(safeSelector) || {}
|
||||
return <AddressInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
|
||||
return (
|
||||
<>
|
||||
<EthHashInfo
|
||||
hash={safeAddress}
|
||||
name={safeName}
|
||||
explorerUrl={getExplorerInfo(safeAddress)}
|
||||
showIdenticon
|
||||
showCopyBtn
|
||||
/>
|
||||
{ethBalance && (
|
||||
<StyledBlock>
|
||||
<Paragraph noMargin>
|
||||
Balance: <Bold data-testid="current-eth-balance">{`${ethBalance} ${nativeCoin.symbol}`}</Bold>
|
||||
</Paragraph>
|
||||
</StyledBlock>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SafeInfo
|
||||
|
|
|
@ -4,11 +4,12 @@ import cn from 'classnames'
|
|||
import React, { Suspense, useEffect, useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Erc721Transfer } from 'src/logic/safe/store/models/types/gateway'
|
||||
import { CollectibleTx } from './screens/ReviewCollectible'
|
||||
import { CustomTx } from './screens/ContractInteraction/ReviewCustomTx'
|
||||
import { ContractInteractionTx } from './screens/ContractInteraction'
|
||||
import { CustomTxProps } from './screens/ContractInteraction/SendCustomTx'
|
||||
import { ReviewTxProp } from './screens/ReviewTx'
|
||||
import { ReviewTxProp } from './screens/ReviewSendFundsTx'
|
||||
import { NFTToken } from 'src/logic/collectibles/sources/collectibles.d'
|
||||
import { SendCollectibleTxInfo } from './screens/SendCollectible'
|
||||
|
||||
|
@ -20,7 +21,7 @@ const SendCollectible = React.lazy(() => import('./screens/SendCollectible'))
|
|||
|
||||
const ReviewCollectible = React.lazy(() => import('./screens/ReviewCollectible'))
|
||||
|
||||
const ReviewTx = React.lazy(() => import('./screens/ReviewTx'))
|
||||
const ReviewSendFundsTx = React.lazy(() => import('./screens/ReviewSendFundsTx'))
|
||||
|
||||
const ContractInteraction = React.lazy(() => import('./screens/ContractInteraction'))
|
||||
|
||||
|
@ -46,12 +47,23 @@ const useStyles = makeStyles({
|
|||
},
|
||||
})
|
||||
|
||||
export type TxType =
|
||||
| 'chooseTxType'
|
||||
| 'sendFunds'
|
||||
| 'sendFundsReviewTx'
|
||||
| 'contractInteraction'
|
||||
| 'contractInteractionReview'
|
||||
| 'reviewCustomTx'
|
||||
| 'sendCollectible'
|
||||
| 'reviewCollectible'
|
||||
| ''
|
||||
|
||||
type Props = {
|
||||
activeScreenType: string
|
||||
activeScreenType: TxType
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
recipientAddress?: string
|
||||
selectedToken?: string | NFTToken
|
||||
selectedToken?: string | NFTToken | Erc721Transfer
|
||||
tokenAmount?: string
|
||||
}
|
||||
|
||||
|
@ -64,7 +76,7 @@ const SendModal = ({
|
|||
tokenAmount,
|
||||
}: Props): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState(activeScreenType || 'chooseTxType')
|
||||
const [activeScreen, setActiveScreen] = useState<TxType>(activeScreenType || 'chooseTxType')
|
||||
const [tx, setTx] = useState<unknown>({})
|
||||
const [isABI, setIsABI] = useState(true)
|
||||
|
||||
|
@ -77,7 +89,7 @@ const SendModal = ({
|
|||
const scalableModalSize = activeScreen === 'chooseTxType'
|
||||
|
||||
const handleTxCreation = (txInfo: SendCollectibleTxInfo) => {
|
||||
setActiveScreen('reviewTx')
|
||||
setActiveScreen('sendFundsReviewTx')
|
||||
setTx(txInfo)
|
||||
}
|
||||
|
||||
|
@ -118,18 +130,21 @@ const SendModal = ({
|
|||
{activeScreen === 'chooseTxType' && (
|
||||
<ChooseTxType onClose={onClose} recipientAddress={recipientAddress} setActiveScreen={setActiveScreen} />
|
||||
)}
|
||||
|
||||
{activeScreen === 'sendFunds' && (
|
||||
<SendFunds
|
||||
onClose={onClose}
|
||||
onNext={handleTxCreation}
|
||||
onReview={handleTxCreation}
|
||||
recipientAddress={recipientAddress}
|
||||
selectedToken={selectedToken as string}
|
||||
amount={tokenAmount}
|
||||
/>
|
||||
)}
|
||||
{activeScreen === 'reviewTx' && (
|
||||
<ReviewTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx as ReviewTxProp} />
|
||||
|
||||
{activeScreen === 'sendFundsReviewTx' && (
|
||||
<ReviewSendFundsTx onClose={onClose} onPrev={() => setActiveScreen('sendFunds')} tx={tx as ReviewTxProp} />
|
||||
)}
|
||||
|
||||
{activeScreen === 'contractInteraction' && isABI && (
|
||||
<ContractInteraction
|
||||
isABI={isABI}
|
||||
|
@ -140,9 +155,11 @@ const SendModal = ({
|
|||
onNext={handleContractInteractionCreation}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeScreen === 'contractInteractionReview' && isABI && tx && (
|
||||
<ContractInteractionReview onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx} />
|
||||
)}
|
||||
|
||||
{activeScreen === 'contractInteraction' && !isABI && (
|
||||
<SendCustomTx
|
||||
initialValues={tx as CustomTxProps}
|
||||
|
@ -153,9 +170,11 @@ const SendModal = ({
|
|||
contractAddress={recipientAddress}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeScreen === 'reviewCustomTx' && (
|
||||
<ReviewCustomTx onClose={onClose} onPrev={() => setActiveScreen('contractInteraction')} tx={tx as CustomTx} />
|
||||
)}
|
||||
|
||||
{activeScreen === 'sendCollectible' && (
|
||||
<SendCollectible
|
||||
initialValues={tx}
|
||||
|
@ -165,6 +184,7 @@ const SendModal = ({
|
|||
selectedToken={selectedToken as NFTToken | undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeScreen === 'reviewCollectible' && (
|
||||
<ReviewCollectible
|
||||
onClose={onClose}
|
||||
|
|
|
@ -10,8 +10,8 @@ import { FEATURES } from 'src/config/networks/network.d'
|
|||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { filterContractAddressBookEntries, filterAddressEntries } from 'src/logic/addressBook/utils'
|
||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||
import { isValidEnsName, isValidCryptoDomainName } from 'src/logic/wallets/ethAddresses'
|
||||
import { getAddressFromDomain } from 'src/logic/wallets/getWeb3'
|
||||
import {
|
||||
useTextFieldInputStyle,
|
||||
useTextFieldLabelStyle,
|
||||
|
@ -85,8 +85,11 @@ const BaseAddressBookInput = ({
|
|||
}
|
||||
|
||||
// ENS-enabled resolve/validation
|
||||
if (isFeatureEnabled(FEATURES.ENS_LOOKUP) && isValidEnsName(normalizedValue)) {
|
||||
const address = await getAddressFromENS(normalizedValue).catch(() => normalizedValue)
|
||||
if (
|
||||
isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) &&
|
||||
(isValidEnsName(normalizedValue) || isValidCryptoDomainName(normalizedValue))
|
||||
) {
|
||||
const address = await getAddressFromDomain(normalizedValue)
|
||||
|
||||
const validatedAddress = validateAddress(address)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
|
@ -15,7 +15,7 @@ interface HeaderProps {
|
|||
title: string
|
||||
}
|
||||
|
||||
const Header = ({ onClose, subTitle, title }: HeaderProps) => {
|
||||
export const Header = ({ onClose, subTitle, title }: HeaderProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
|
@ -30,5 +30,3 @@ const Header = ({ onClose, subTitle, title }: HeaderProps) => {
|
|||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
|
|
@ -6,7 +6,7 @@ import MenuItem from '@material-ui/core/MenuItem'
|
|||
import { MuiThemeProvider } from '@material-ui/core/styles'
|
||||
import SearchIcon from '@material-ui/icons/Search'
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { useField, useFormState } from 'react-final-form'
|
||||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
|
@ -24,7 +24,7 @@ interface MethodsDropdownProps {
|
|||
onChange: (method: AbiItem) => void
|
||||
}
|
||||
|
||||
const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement | null => {
|
||||
export const MethodsDropdown = ({ onChange }: MethodsDropdownProps): ReactElement | null => {
|
||||
const classes = useDropdownStyles({ buttonWidth: MENU_WIDTH })
|
||||
const {
|
||||
input: { value: abi },
|
||||
|
@ -33,13 +33,14 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement
|
|||
const {
|
||||
initialValues: { selectedMethod: selectedMethodByDefault },
|
||||
} = useFormState({ subscription: { initialValues: true } })
|
||||
const [selectedMethod, setSelectedMethod] = React.useState(selectedMethodByDefault ? selectedMethodByDefault : {})
|
||||
const [methodsList, setMethodsList] = React.useState<AbiItemExtended[]>([])
|
||||
const [methodsListFiltered, setMethodsListFiltered] = React.useState<AbiItemExtended[]>([])
|
||||
const [anchorEl, setAnchorEl] = React.useState(null)
|
||||
const [searchParams, setSearchParams] = React.useState('')
|
||||
const [selectedMethod, setSelectedMethod] = useState(selectedMethodByDefault ? selectedMethodByDefault : {})
|
||||
const [methodsList, setMethodsList] = useState<AbiItemExtended[]>([])
|
||||
const [methodsListFiltered, setMethodsListFiltered] = useState<AbiItemExtended[]>([])
|
||||
|
||||
React.useEffect(() => {
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [searchParams, setSearchParams] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (abi) {
|
||||
try {
|
||||
setMethodsList(extractUsefulMethods(JSON.parse(abi)))
|
||||
|
@ -49,7 +50,7 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement
|
|||
}
|
||||
}, [abi])
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
setMethodsListFiltered(methodsList.filter(({ name }) => name?.toLowerCase().includes(searchParams.toLowerCase())))
|
||||
}, [methodsList, searchParams])
|
||||
|
||||
|
@ -67,7 +68,11 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement
|
|||
handleClose()
|
||||
}
|
||||
|
||||
return !valid || !abi || abi === NO_CONTRACT ? null : (
|
||||
if (!valid || !abi || abi === NO_CONTRACT) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Row margin="sm">
|
||||
<Col>
|
||||
<MuiThemeProvider theme={DropdownListTheme}>
|
||||
|
@ -145,5 +150,3 @@ const MethodsDropdown = ({ onChange }: MethodsDropdownProps): React.ReactElement
|
|||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default MethodsDropdown
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Checkbox } from '@gnosis.pm/safe-react-components'
|
||||
import React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Field from 'src/components/forms/Field'
|
||||
|
@ -15,7 +15,7 @@ type Props = {
|
|||
placeholder: string
|
||||
}
|
||||
|
||||
const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElement | null => {
|
||||
export const InputComponent = ({ type, keyValue, placeholder }: Props): ReactElement | null => {
|
||||
if (!type) {
|
||||
return null
|
||||
}
|
||||
|
@ -67,5 +67,3 @@ const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElem
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default InputComponent
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { useField } from 'react-final-form'
|
||||
|
||||
import Row from 'src/components/layout/Row'
|
||||
|
||||
import InputComponent from './InputComponent'
|
||||
import { InputComponent } from './InputComponent'
|
||||
import { generateFormFieldKey } from '../utils'
|
||||
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
|
||||
const RenderInputParams = (): React.ReactElement | null => {
|
||||
export const RenderInputParams = (): ReactElement | null => {
|
||||
const {
|
||||
meta: { valid: validABI },
|
||||
} = useField('abi', { subscription: { valid: true, value: true } })
|
||||
|
@ -31,5 +31,3 @@ const RenderInputParams = (): React.ReactElement | null => {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RenderInputParams
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { useField } from 'react-final-form'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
|
@ -17,7 +17,7 @@ const useStyles = makeStyles({
|
|||
},
|
||||
})
|
||||
|
||||
const RenderOutputParams = () => {
|
||||
export const RenderOutputParams = (): ReactElement | null => {
|
||||
const classes = useStyles()
|
||||
const {
|
||||
input: { value: method },
|
||||
|
@ -27,7 +27,11 @@ const RenderOutputParams = () => {
|
|||
}: any = useField('callResults', { subscription: { value: true } })
|
||||
const multipleResults = !!method && method.outputs.length > 1
|
||||
|
||||
return results ? (
|
||||
if (!results) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="left" margin="xs">
|
||||
<Paragraph color="primary" size="lg" style={{ letterSpacing: '-0.5px' }}>
|
||||
|
@ -57,7 +61,5 @@ const RenderOutputParams = () => {
|
|||
)
|
||||
})}
|
||||
</>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
||||
export default RenderOutputParams
|
||||
|
|
|
@ -16,13 +16,20 @@ import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIServic
|
|||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||
import Header from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
||||
import { Header } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { generateFormFieldKey, getValueFromTxInputs } from '../utils'
|
||||
import { useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import {
|
||||
generateFormFieldKey,
|
||||
getValueFromTxInputs,
|
||||
} from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||
import { useEstimateTransactionGas, EstimationStatus } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
@ -37,7 +44,9 @@ export type TransactionReviewType = {
|
|||
type Props = {
|
||||
onClose: () => void
|
||||
onPrev: () => void
|
||||
onEditTxParameters: () => void
|
||||
tx: TransactionReviewType
|
||||
txParameters: TxParameters
|
||||
}
|
||||
|
||||
const { nativeCoin } = getNetworkInfo()
|
||||
|
@ -46,40 +55,51 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
|||
const classes = useStyles()
|
||||
const dispatch = useDispatch()
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const [txParameters, setTxParameters] = useState<{
|
||||
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
|
||||
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
|
||||
|
||||
const [txInfo, setTxInfo] = useState<{
|
||||
txRecipient: string
|
||||
txData: string
|
||||
txAmount: string
|
||||
}>({ txData: '', txAmount: '', txRecipient: '' })
|
||||
|
||||
const {
|
||||
gasLimit,
|
||||
gasEstimation,
|
||||
gasPriceFormatted,
|
||||
gasCostFormatted,
|
||||
txEstimationExecutionStatus,
|
||||
isExecution,
|
||||
isOffChainSignature,
|
||||
isCreation,
|
||||
} = useEstimateTransactionGas({
|
||||
txRecipient: txParameters?.txRecipient,
|
||||
txAmount: txParameters?.txAmount,
|
||||
txData: txParameters?.txData,
|
||||
txRecipient: txInfo?.txRecipient,
|
||||
txAmount: txInfo?.txAmount,
|
||||
txData: txInfo?.txData,
|
||||
safeTxGas: manualSafeTxGas,
|
||||
manualGasPrice,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setTxParameters({
|
||||
setTxInfo({
|
||||
txRecipient: tx.contractAddress as string,
|
||||
txAmount: tx.value ? toTokenUnit(tx.value, nativeCoin.decimals) : '0',
|
||||
txData: tx.data ? tx.data.trim() : '',
|
||||
})
|
||||
}, [tx.contractAddress, tx.value, tx.data, safeAddress])
|
||||
|
||||
const submitTx = async () => {
|
||||
if (safeAddress && txParameters) {
|
||||
const submitTx = async (txParameters: TxParameters) => {
|
||||
if (safeAddress && txInfo) {
|
||||
dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: txParameters?.txRecipient,
|
||||
valueInWei: txParameters?.txAmount,
|
||||
txData: txParameters?.txData,
|
||||
to: txInfo?.txRecipient,
|
||||
valueInWei: txInfo?.txAmount,
|
||||
txData: txInfo?.txData,
|
||||
txNonce: txParameters.safeNonce,
|
||||
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
|
||||
ethParameters: txParameters,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
}),
|
||||
)
|
||||
|
@ -89,106 +109,138 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
|
|||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header onClose={onClose} subTitle="2 of 2" title="Contract Interaction" />
|
||||
<Hairline />
|
||||
<Block className={classes.formContainer}>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Contract Address
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<AddressInfo safeAddress={tx.contractAddress as string} />
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Value
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Img alt="Ether" height={28} onError={setImageToPlaceholder} src={getEthAsToken('0').logoUri} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph className={classes.value} noMargin size="md" style={{ margin: 0 }}>
|
||||
{tx.value || 0}
|
||||
{' ' + nativeCoin.name}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Method
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Paragraph className={classes.value} size="md" style={{ margin: 0 }}>
|
||||
{tx.selectedMethod?.name}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
{tx.selectedMethod?.inputs?.map(({ name, type }, index) => {
|
||||
const key = generateFormFieldKey(type, tx.selectedMethod?.signatureHash || '', index)
|
||||
const value: string = getValueFromTxInputs(key, type, tx)
|
||||
const closeEditModalCallback = (txParameters: TxParameters) => {
|
||||
const oldGasPrice = Number(gasPriceFormatted)
|
||||
const newGasPrice = Number(txParameters.ethGasPrice)
|
||||
const oldSafeTxGas = Number(gasEstimation)
|
||||
const newSafeTxGas = Number(txParameters.safeTxGas)
|
||||
|
||||
return (
|
||||
<React.Fragment key={key}>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
{name} ({type})
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Paragraph className={classes.value} noMargin size="md" style={{ margin: 0 }}>
|
||||
{value}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Data (hex encoded)
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col className={classes.outerData}>
|
||||
<Row className={classes.data} size="md">
|
||||
{tx.data}
|
||||
if (newGasPrice && oldGasPrice !== newGasPrice) {
|
||||
setManualGasPrice(txParameters.ethGasPrice)
|
||||
}
|
||||
|
||||
if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) {
|
||||
setManualSafeTxGas(newSafeTxGas)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableTxParameters
|
||||
ethGasLimit={gasLimit}
|
||||
ethGasPrice={gasPriceFormatted}
|
||||
safeTxGas={gasEstimation.toString()}
|
||||
closeEditModalCallback={closeEditModalCallback}
|
||||
>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Header onClose={onClose} subTitle="2 of 2" title="Contract Interaction" />
|
||||
<Hairline />
|
||||
<Block className={classes.formContainer}>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Contract Address
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
minWidth={140}
|
||||
onClick={submitTx}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
<Row align="center" margin="md">
|
||||
<AddressInfo safeAddress={tx.contractAddress as string} />
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Value
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Img alt="Ether" height={28} onError={setImageToPlaceholder} src={getEthAsToken('0').logoUri} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph className={classes.value} noMargin size="md" style={{ margin: 0 }}>
|
||||
{tx.value || 0}
|
||||
{' ' + nativeCoin.name}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Method
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Paragraph className={classes.value} size="md" style={{ margin: 0 }}>
|
||||
{tx.selectedMethod?.name}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
{tx.selectedMethod?.inputs?.map(({ name, type }, index) => {
|
||||
const key = generateFormFieldKey(type, tx.selectedMethod?.signatureHash || '', index)
|
||||
const value: string = getValueFromTxInputs(key, type, tx)
|
||||
|
||||
return (
|
||||
<React.Fragment key={key}>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
{name} ({type})
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Paragraph className={classes.value} noMargin size="md" style={{ margin: 0 }}>
|
||||
{value}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Data (hex encoded)
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col className={classes.outerData}>
|
||||
<Row className={classes.data} size="md">
|
||||
{tx.data}
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Tx Parameters */}
|
||||
<TxParametersDetail
|
||||
txParameters={txParameters}
|
||||
onEdit={toggleEditMode}
|
||||
isTransactionCreation={isCreation}
|
||||
isTransactionExecution={isExecution}
|
||||
/>
|
||||
</Block>
|
||||
<div className={classes.gasCostsContainer}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
isExecution={isExecution}
|
||||
isCreation={isCreation}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
txEstimationExecutionStatus={txEstimationExecutionStatus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
minWidth={140}
|
||||
onClick={() => submitTx(txParameters)}
|
||||
variant="contained"
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</EditableTxParameters>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue