Merge pull request #1877 from gnosis/release/v2.20.0

This commit is contained in:
Daniel Sanchez 2021-02-15 20:00:10 +01:00 committed by GitHub
commit e4c122ddef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
190 changed files with 6989 additions and 1977 deletions

View File

@ -44,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;
@ -54,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

View File

@ -1,6 +1,6 @@
{
"name": "safe-react",
"version": "2.19.2",
"version": "3.0.0",
"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,20 +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#2e7574f",
"@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",
"@unstoppabledomains/resolution": "^1.11.1",
"@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",
@ -199,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",
@ -211,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",
@ -229,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",
@ -240,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",
@ -267,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"

View File

@ -50,7 +50,7 @@ const Footer = (): React.ReactElement => {
const dispatch = useDispatch()
const openCookiesHandler = () => {
dispatch(openCookieBanner(true))
dispatch(openCookieBanner({ cookieBannerOpen: true }))
}
return (

View File

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

View File

@ -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, useRef, 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 dispatch = useRef(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.current(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.current(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.current(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 &gt;
</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 &quot;Accept all&quot;, 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 &quot;Accept all&quot;, 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.current(openCookieBanner({ cookieBannerOpen: true, intercomAlertDisplayed: true }))}
/>
)}
{!isDesktop && showBanner?.cookieBannerOpen && (
<CookiesBannerForm alertMessage={showBanner?.intercomAlertDisplayed} />
)}
</>
)
}
export default CookiesBanner

View File

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

View File

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

View File

@ -3,6 +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 { Text } from '@gnosis.pm/safe-react-components'
type TransactionFailTextProps = {
txEstimationExecutionStatus: EstimationStatus
@ -34,11 +35,18 @@ export const TransactionFees = ({
return (
<>
<Paragraph>
<Paragraph size="lg" align="center">
You&apos;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} />
</>

View File

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

View File

@ -68,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
@ -81,6 +83,11 @@ export const getGasPriceOracle = (): GasPriceOracle | undefined => getConfig()?.
export const getRpcServiceUrl = (): string =>
usesInfuraRPC ? `${getConfig().rpcServiceUrl}/${INFURA_TOKEN}` : getConfig().rpcServiceUrl
export const getSafeClientGatewayBaseUrl = (safeAddress: string) => `${getClientGatewayUrl()}/safes/${safeAddress}`
export const getTxDetailsUrl = (clientGatewayTxId: string) =>
`${getClientGatewayUrl()}/transactions/${clientGatewayTxId}`
export const getSafeServiceBaseUrl = (safeAddress: string) => `${getTxServiceUrl()}/safes/${safeAddress}`
export const getTokensServiceBaseUrl = () => `${getTxServiceUrl()}/tokens`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -101,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) {
@ -148,7 +155,7 @@ type UseEstimateTransactionGasProps = {
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)

View File

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

View File

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

View File

@ -1,8 +0,0 @@
import { Record } from 'immutable'
export const makeNotification = Record({
key: 0,
message: '',
options: {},
dismissed: false,
})

View File

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

View File

@ -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', () => {

View File

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

View File

@ -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'
@ -60,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,
@ -105,8 +95,6 @@ const createTransaction = (
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin)
const beforeExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.beforeExecution))
let pendingExecutionKey
let txHash
const txArgs: TxArgs = {
safeInstance,
@ -124,13 +112,13 @@ const createTransaction = (
sigs,
}
const safeTxHash = generateSafeTxHash(safeAddress, txArgs)
try {
if (checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion)) {
const signature = await tryOffchainSigning(safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
if (signature) {
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
dispatch(enqueueSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded))
dispatch(fetchTransactions(safeAddress))
await saveTxToHistory({ ...txArgs, signature, origin })
@ -148,57 +136,28 @@ const createTransaction = (
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
@ -210,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) {
@ -227,5 +182,3 @@ const createTransaction = (
return txHash
}
export default createTransaction

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { List } from 'immutable'
import { AnyAction } from 'redux'
import { ThunkAction } from 'redux-thunk'
@ -17,22 +18,38 @@ 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
@ -71,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,
@ -99,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
@ -119,52 +135,47 @@ export const processTransaction = ({
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) {
@ -180,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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,378 @@
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 { getLocalStartOfDate } 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 = getLocalStartOfDate(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) => {
// prevent setting `PENDING_FAILED` status, if previous status wasn't `PENDING`
if (txStatus === 'PENDING_FAILED' && txToUpdate.txStatus !== 'PENDING') {
return 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) => {
// prevent setting `PENDING_FAILED` status, if previous status wasn't `PENDING`
if (txStatus === 'PENDING_FAILED' && txToUpdate.txStatus !== 'PENDING') {
return 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,
},
}
},
},
{},
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -207,6 +207,7 @@ export const estimateGasForTransactionCreation = async (
to: string,
valueInWei: string,
operation: number,
safeTxGas?: number,
): Promise<number> => {
try {
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
@ -216,8 +217,13 @@ export const estimateGasForTransactionCreation = async (
to: safeAddress,
from: safeAddress,
data: estimateData,
gas: safeTxGas ? safeTxGas : undefined,
})
if (safeTxGas) {
return gasEstimationResponse
}
const dataGasEstimation = parseRequiredTxGasResponse(estimateData)
const additionalGasBatches = [0, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000, 5120000]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,10 +16,11 @@ 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 { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import GasEstimationInfo from './GasEstimationInfo'
import { getNetworkInfo } from 'src/config'
@ -32,7 +33,8 @@ import Hairline from 'src/components/layout/Hairline'
import { TransactionFees } from 'src/components/TransactionsFees'
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
import { md, lg } from 'src/theme/variables'
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)) {
@ -84,6 +86,10 @@ 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 = {
isOpen: boolean
@ -100,6 +106,10 @@ type OwnProps = {
const { nativeCoin } = getNetworkInfo()
const parseTxValue = (value: string | number): string => {
return web3ReadOnly.utils.toBN(value).toString()
}
export const ConfirmTransactionModal = ({
isOpen,
app,
@ -114,14 +124,16 @@ export const ConfirmTransactionModal = ({
}: OwnProps): React.ReactElement | null => {
const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0)
const threshold = useSelector(safeThresholdSelector) || 1
// FIXME #issue-1848 check why this generates bugs with WalletConnect Safe app and some interactions
// const txRecipient: string | undefined = useMemo(() => (txs.length > 1 ? MULTI_SEND_ADDRESS : txs[0]?.to), [txs])
// const txData: string | undefined = useMemo(() => (txs.length > 1 ? encodeMultiSendCall(txs) : txs[0]?.data), [txs])
// const operation = useMemo(() => (txs.length > 1 ? DELEGATE_CALL : CALL), [txs])
// #issue-1848 Remove this when non multisend transactions are checked
const txRecipient: string | undefined = MULTI_SEND_ADDRESS
const txData: string | undefined = useMemo(() => encodeMultiSendCall(txs), [txs])
const operation = DELEGATE_CALL
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 && parseTxValue(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,
@ -136,6 +148,9 @@ export const ConfirmTransactionModal = ({
txData: txData || '',
txRecipient,
operation,
txAmount: txValue,
safeTxGas: manualSafeTxGas,
manualGasPrice,
})
useEffect(() => {
@ -161,19 +176,23 @@ export const ConfirmTransactionModal = ({
const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED')
const confirmTransactions = async () => {
const confirmTransactions = async (txParameters: TxParameters) => {
await dispatch(
createTransaction(
{
safeAddress,
to: txRecipient,
valueInWei: '0',
valueInWei: txValue,
txData,
operation,
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
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,
@ -181,6 +200,21 @@ 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
@ -198,61 +232,65 @@ export const ConfirmTransactionModal = ({
)
: (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>
<>
<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>
<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 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>
)}
{/* Tx Parameters */}
<TxParametersDetail
txParameters={txParameters}
onEdit={toggleEditMode}
parametersStatus={getParametersStatus()}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
/>
<Row>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
isCreation={isCreation}
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
{/* Tx Parameters */}
<TxParametersDetail
txParameters={txParameters}
onEdit={toggleEditMode}
parametersStatus={getParametersStatus()}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
/>
</Row>
</Container>
</Container>
{txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (
<TransactionFeesWrapper>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
isCreation={isCreation}
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</TransactionFeesWrapper>
)}
</>
)
}
@ -263,6 +301,7 @@ export const ConfirmTransactionModal = ({
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
parametersStatus={getParametersStatus()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => (
<>
@ -272,12 +311,11 @@ export const ConfirmTransactionModal = ({
{body(txParameters, toggleEditMode)}
<Hairline />
<ModalFooter align="center" grow>
<ModalFooterConfirmation
cancelText="Cancel"
handleCancel={handleTxRejection}
handleOk={confirmTransactions}
handleOk={() => confirmTransactions(txParameters)}
okDisabled={areTxsMalformed}
okText="Submit"
/>

View File

@ -44,7 +44,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
},
// Balancer Pool
{
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQsmxUVtcEWmKcXxKwYsZFKJ2kDdqqjqdExujiGY1g3tV`,
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVaxypk2FTyfcTS9oZKxmpQziPUTu2VRhhW7sso1mGysf`,
disabled: false,
networks: [ETHEREUM_NETWORK.MAINNET],
},

View File

@ -14,7 +14,7 @@ const { nativeCoin } = getNetworkInfo()
const StyledBlock = styled(Block)`
font-size: 12px;
line-height: 1.08;
letter-spacing: -0.5;
letter-spacing: -0.5px;
background-color: ${border};
width: fit-content;
padding: 5px 10px;

View File

@ -4,6 +4,7 @@ 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'
@ -62,7 +63,7 @@ type Props = {
isOpen: boolean
onClose: () => void
recipientAddress?: string
selectedToken?: string | NFTToken
selectedToken?: string | NFTToken | Erc721Transfer
tokenAmount?: string
}

View File

@ -18,7 +18,7 @@ import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
import { Header } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { 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'
@ -55,6 +55,8 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
const classes = useStyles()
const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
const [txInfo, setTxInfo] = useState<{
txRecipient: string
@ -75,6 +77,8 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
txRecipient: txInfo?.txRecipient,
txAmount: txInfo?.txAmount,
txData: txInfo?.txData,
safeTxGas: manualSafeTxGas,
manualGasPrice,
})
useEffect(() => {
@ -105,8 +109,28 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
onClose()
}
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)
}
}
return (
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
<EditableTxParameters
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => (
<>
<Header onClose={onClose} subTitle="2 of 2" title="Contract Interaction" />
@ -187,18 +211,17 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
/>
<Row>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
isCreation={isCreation}
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</Row>
</Block>
<Hairline />
<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

View File

@ -16,7 +16,7 @@ import Hairline from 'src/components/layout/Hairline'
import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
@ -169,8 +169,9 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
/>
<Row>
</Block>
{txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (
<Block className={classes.gasCostsContainer}>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
@ -178,9 +179,8 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => {
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</Row>
</Block>
<Hairline />
</Block>
)}
<Row align="center" className={classes.buttonRow}>
<Button minWidth={140} onClick={onPrev}>
Back

View File

@ -1,4 +1,4 @@
import { border, lg, md, secondaryText, sm } from 'src/theme/variables'
import { background, border, lg, md, secondaryText, sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = createStyles({
@ -55,4 +55,8 @@ export const styles = createStyles({
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
marginLeft: '15px',
},
gasCostsContainer: {
backgroundColor: background,
padding: `${sm} ${lg}`,
},
})

View File

@ -1,4 +1,4 @@
import { lg, md, secondaryText, sm, border } from 'src/theme/variables'
import { lg, md, secondaryText, sm, border, background } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = createStyles({
@ -71,4 +71,8 @@ export const styles = createStyles({
fullWidth: {
justifyContent: 'space-between',
},
gasCostsContainer: {
backgroundColor: background,
padding: `0 ${lg}`,
},
})

View File

@ -14,7 +14,7 @@ import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { nftTokensSelector } from 'src/logic/collectibles/store/selectors'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
@ -54,6 +54,8 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const nftTokens = useSelector(nftTokensSelector)
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
const txToken = nftTokens.find(
({ assetAddress, tokenId }) => assetAddress === tx.assetAddress && tokenId === tx.nftTokenId,
@ -72,6 +74,8 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
} = useEstimateTransactionGas({
txData: data,
txRecipient: tx.assetAddress,
safeTxGas: manualSafeTxGas,
manualGasPrice,
})
useEffect(() => {
@ -119,8 +123,28 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
}
}
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)
}
}
return (
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
<EditableTxParameters
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => (
<>
<Row align="center" className={classes.heading} grow>
@ -183,18 +207,16 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement =
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
/>
<Row>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
isCreation={isCreation}
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</Row>
</Block>
<Hairline style={{ position: 'absolute', bottom: 85 }} />
<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

View File

@ -1,4 +1,4 @@
import { lg, md, secondaryText, sm } from 'src/theme/variables'
import { background, lg, md, secondaryText, sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = createStyles({
@ -42,4 +42,8 @@ export const styles = createStyles({
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
marginLeft: '15px',
},
gasCostsContainer: {
backgroundColor: background,
padding: `0 ${lg}`,
},
})

View File

@ -16,7 +16,7 @@ import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { getSpendingLimitContract } from 'src/logic/contracts/safeContracts'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { getHumanFriendlyToken } from 'src/logic/tokens/store/actions/fetchTokens'
@ -98,8 +98,9 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
const txRecipient = isSendingNativeToken ? tx.recipientAddress : txToken?.address || ''
const txValue = isSendingNativeToken ? toTokenUnit(tx.amount, nativeCoin.decimals) : '0'
const data = useTxData(isSendingNativeToken, tx.amount, tx.recipientAddress, txToken)
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
/* Get GasInfo */
const {
gasCostFormatted,
gasPriceFormatted,
@ -113,6 +114,8 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
txData: data,
txRecipient,
txType: tx.txType,
safeTxGas: manualSafeTxGas,
manualGasPrice,
})
const submitTx = async (txParameters: TxParameters) => {
@ -157,8 +160,28 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
}
}
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)
}
}
return (
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
<EditableTxParameters
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => (
<>
{/* Header */}
@ -239,7 +262,9 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
/>
{/* Disclaimer */}
<Row>
</Block>
{txEstimationExecutionStatus !== EstimationStatus.LOADING && (
<div className={classes.gasCostsContainer}>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
@ -247,10 +272,8 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</Row>
</Block>
<Hairline style={{ position: 'absolute', bottom: 85 }} />
</div>
)}
{/* Footer */}
<Row align="center" className={classes.buttonRow}>

View File

@ -1,4 +1,4 @@
import { lg, md, secondaryText, sm } from 'src/theme/variables'
import { background, lg, md, secondaryText, sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = createStyles({
@ -42,4 +42,8 @@ export const styles = createStyles({
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
marginLeft: '15px',
},
gasCostsContainer: {
backgroundColor: background,
padding: `0 ${lg}`,
},
})

View File

@ -19,6 +19,7 @@ import WhenFieldChanges from 'src/components/WhenFieldChanges'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors'
import { Erc721Transfer } from 'src/logic/safe/store/models/types/gateway'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
import { AddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import { NFTToken } from 'src/logic/collectibles/sources/collectibles.d'
@ -52,7 +53,7 @@ type SendCollectibleProps = {
onClose: () => void
onNext: (txInfo: SendCollectibleTxInfo) => void
recipientAddress?: string
selectedToken?: NFTToken
selectedToken?: NFTToken | Erc721Transfer
}
export type SendCollectibleTxInfo = {
@ -243,7 +244,12 @@ const SendCollectible = ({
</Row>
<Row margin="sm">
<Col>
<TokenSelectField assets={nftAssets} initialValue={selectedToken?.assetAddress} />
<TokenSelectField
assets={nftAssets}
initialValue={
(selectedToken as NFTToken)?.assetAddress ?? (selectedToken as Erc721Transfer)?.tokenAddress
}
/>
</Col>
</Row>
<Row margin="xs">

View File

@ -18,7 +18,7 @@ import Row from 'src/components/layout/Row'
import Modal from 'src/components/Modal'
import { getExplorerInfo } from 'src/config'
import { getDisableModuleTxData } from 'src/logic/safe/utils/modules'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { ModulePair } from 'src/logic/safe/store/models/safe'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
@ -55,6 +55,8 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const [txData, setTxData] = useState('')
const dispatch = useDispatch()
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
const [, moduleAddress] = selectedModulePair
const explorerInfo = getExplorerInfo(moduleAddress)
@ -73,6 +75,8 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
txData,
txRecipient: safeAddress,
txAmount: '0',
safeTxGas: manualSafeTxGas,
manualGasPrice,
})
useEffect(() => {
@ -82,13 +86,13 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
const removeSelectedModule = async (txParameters: TxParameters): Promise<void> => {
try {
dispatch(
await dispatch(
createTransaction({
safeAddress,
to: safeAddress,
valueInWei: '0',
txData,
txNonce: txParameters.ethNonce,
txNonce: txParameters.safeNonce,
safeTxGas: txParameters.safeTxGas ? Number(txParameters.safeTxGas) : undefined,
ethParameters: txParameters,
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
@ -99,6 +103,21 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
}
}
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)
}
}
return (
<Modal
description="Remove the selected Module"
@ -107,7 +126,12 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
title="Remove Module"
open
>
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
<EditableTxParameters
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => {
return (
<>
@ -120,7 +144,7 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
</IconButton>
</Row>
<Hairline />
<Block className={classes.modalContainer}>
<Block>
<Row className={classes.modalOwner}>
<Col align="center" xs={1}>
<Identicon address={moduleAddress} diameter={32} />
@ -149,25 +173,25 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
as well.
</Paragraph>
</Row>
</Block>
<Block className={classes.accordionContainer}>
{/* Tx Parameters */}
<TxParametersDetail
txParameters={txParameters}
onEdit={toggleEditMode}
compact={false}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
/>
<Row className={classes.modalDescription}>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
isCreation={isCreation}
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</Row>
</Block>
<Hairline />
<Row className={cn(classes.modalDescription, classes.gasCostsContainer)}>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
isCreation={isCreation}
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</Row>
<Row align="center" className={classes.modalButtonRow}>
<FooterWrapper>
<Button size="md" color="secondary" onClick={onClose}>

View File

@ -60,9 +60,6 @@ export const styles = createStyles({
maxHeight: '75px',
padding: `${sm} ${lg}`,
},
modalContainer: {
minHeight: '369px',
},
modalManage: {
fontSize: lg,
},
@ -109,4 +106,11 @@ export const styles = createStyles({
maxWidth: 'calc(100% - 30px)',
overflow: 'hidden',
},
gasCostsContainer: {
backgroundColor: background,
padding: `0 ${lg}`,
},
accordionContainer: {
margin: `0 ${md}`,
},
})

View File

@ -7,7 +7,7 @@ import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import addSafeOwner from 'src/logic/safe/store/actions/addSafeOwner'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'

View File

@ -43,6 +43,8 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
const safeAddress = useSelector(safeParamAddressFromStateSelector) as string
const safeName = useSelector(safeNameSelector)
const owners = useSelector(safeOwnersSelector)
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
const {
gasLimit,
@ -56,6 +58,8 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
} = useEstimateTransactionGas({
txData: data,
txRecipient: safeAddress,
safeTxGas: manualSafeTxGas,
manualGasPrice,
})
useEffect(() => {
@ -80,8 +84,28 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie
}
}, [safeAddress, values.ownerAddress, values.threshold])
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)
}
}
return (
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
<EditableTxParameters
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => (
<>
<Row align="center" className={classes.heading} grow>

View File

@ -82,5 +82,6 @@ export const styles = createStyles({
alignItems: 'center',
textAlign: 'center',
width: '100%',
backgroundColor: background,
},
})

View File

@ -9,7 +9,7 @@ import ThresholdForm from './screens/ThresholdForm'
import Modal from 'src/components/Modal'
import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import removeSafeOwner from 'src/logic/safe/store/actions/removeSafeOwner'
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'

View File

@ -56,6 +56,8 @@ export const ReviewRemoveOwnerModal = ({
const owners = useSelector(safeOwnersSelector)
const addressBook = useSelector(addressBookSelector)
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
const {
gasLimit,
@ -69,6 +71,8 @@ export const ReviewRemoveOwnerModal = ({
} = useEstimateTransactionGas({
txData: data,
txRecipient: safeAddress,
safeTxGas: manualSafeTxGas,
manualGasPrice,
})
useEffect(() => {
@ -101,8 +105,28 @@ export const ReviewRemoveOwnerModal = ({
}
}, [safeAddress, ownerAddress, threshold])
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)
}
}
return (
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
<EditableTxParameters
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => (
<>
<Row align="center" className={classes.heading} grow>
@ -225,7 +249,6 @@ export const ReviewRemoveOwnerModal = ({
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
<Hairline />
</Block>
)}
<Row align="center" className={classes.buttonRow}>

View File

@ -82,5 +82,6 @@ export const styles = createStyles({
alignItems: 'center',
textAlign: 'center',
width: '100%',
backgroundColor: background,
},
})

View File

@ -6,7 +6,7 @@ import Modal from 'src/components/Modal'
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { SENTINEL_ADDRESS, getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import replaceSafeOwner from 'src/logic/safe/store/actions/replaceSafeOwner'
import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress'

View File

@ -65,6 +65,8 @@ export const ReviewReplaceOwnerModal = ({
const threshold = useSelector(safeThresholdSelector) || 1
const addressBook = useSelector(addressBookSelector)
const ownersWithAddressBookName = owners ? getOwnersWithNameFromAddressBook(addressBook, owners) : List([])
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
const {
gasLimit,
@ -78,6 +80,8 @@ export const ReviewReplaceOwnerModal = ({
} = useEstimateTransactionGas({
txData: data,
txRecipient: safeAddress,
safeTxGas: manualSafeTxGas,
manualGasPrice,
})
useEffect(() => {
@ -99,8 +103,28 @@ export const ReviewReplaceOwnerModal = ({
}
}, [ownerAddress, safeAddress, values.newOwnerAddress])
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)
}
}
return (
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
<EditableTxParameters
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => (
<>
<Row align="center" className={classes.heading} grow>
@ -248,7 +272,6 @@ export const ReviewReplaceOwnerModal = ({
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minHeight={42} minWidth={140} onClick={onClickBack}>
Back

View File

@ -87,5 +87,6 @@ export const styles = createStyles({
alignItems: 'center',
textAlign: 'center',
width: '100%',
backgroundColor: background,
},
})

View File

@ -9,7 +9,7 @@ interface GenericInfoProps {
const DataDisplay = ({ title, children }: GenericInfoProps): ReactElement => (
<>
{title && (
<Text size="lg" color="secondaryLight">
<Text size="md" color="secondaryLight">
{title}
</Text>
)}

View File

@ -6,7 +6,7 @@ import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
import Row from 'src/components/layout/Row'
import { getNetworkInfo } from 'src/config'
import createTransaction, { CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction, CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction'
import { SafeRecordProps, SpendingLimit } from 'src/logic/safe/store/models/safe'
import {
addSpendingLimitBeneficiaryMultiSendTx,
@ -158,6 +158,8 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
to: '',
txData: '',
})
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
const {
gasCostFormatted,
@ -173,6 +175,8 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
txData: estimateGasArgs.txData as string,
txRecipient: estimateGasArgs.to as string,
operation: estimateGasArgs.operation,
safeTxGas: manualSafeTxGas,
manualGasPrice,
})
useEffect(() => {
@ -217,8 +221,28 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
RESET_TIME_OPTIONS.find(({ value }) => value === (+existentSpendingLimit.resetTimeMin / 60 / 24).toString())
?.label ?? 'One-time spending limit'
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)
}
}
return (
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
<EditableTxParameters
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => (
<>
<Modal.TopBar title="New Spending Limit" titleNote="2 of 2" onClose={onClose} />
@ -251,9 +275,11 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
</Col>
{existentSpendingLimit && (
<Text size="xl" color="error" center strong>
You are about to replace an existent spending limit
</Text>
<Col margin="md">
<Text size="xl" color="error" center strong>
You are about to replace an existent spending limit
</Text>
</Col>
)}
{/* Tx Parameters */}
<TxParametersDetail
@ -262,16 +288,16 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
/>
<Row>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
isCreation={isCreation}
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</Row>
</Block>
<div className={classes.gasCostsContainer}>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}
isCreation={isCreation}
isOffChainSignature={isOffChainSignature}
txEstimationExecutionStatus={txEstimationExecutionStatus}
/>
</div>
<Modal.Footer>
<Button

View File

@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux'
import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col'
import useTokenInfo from 'src/logic/safe/hooks/useTokenInfo'
import createTransaction from 'src/logic/safe/store/actions/createTransaction'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { getDeleteAllowanceTxData } from 'src/logic/safe/utils/spendingLimits'
@ -23,6 +23,7 @@ import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionPara
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
import Row from 'src/components/layout/Row'
import { TransactionFees } from 'src/components/TransactionsFees'
import cn from 'classnames'
interface RemoveSpendingLimitModalProps {
onClose: () => void
@ -38,6 +39,8 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const [txData, setTxData] = useState('')
const dispatch = useDispatch()
const [manualSafeTxGas, setManualSafeTxGas] = useState(0)
const [manualGasPrice, setManualGasPrice] = useState<string | undefined>()
useEffect(() => {
const {
@ -61,6 +64,8 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
txData,
txRecipient: SPENDING_LIMIT_MODULE_ADDRESS,
txAmount: '0',
safeTxGas: manualSafeTxGas,
manualGasPrice,
})
const removeSelectedSpendingLimit = async (txParameters: TxParameters): Promise<void> => {
@ -88,6 +93,21 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
const resetTimeLabel =
RESET_TIME_OPTIONS.find(({ value }) => +value === +spendingLimit.resetTime.resetTimeMin / 24 / 60)?.label ?? ''
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)
}
}
return (
<Modal
handleClose={onClose}
@ -95,7 +115,12 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
title="Remove Spending Limit"
description="Remove the selected Spending Limit"
>
<EditableTxParameters ethGasLimit={gasLimit} ethGasPrice={gasPriceFormatted} safeTxGas={gasEstimation.toString()}>
<EditableTxParameters
ethGasLimit={gasLimit}
ethGasPrice={gasPriceFormatted}
safeTxGas={gasEstimation.toString()}
closeEditModalCallback={closeEditModalCallback}
>
{(txParameters, toggleEditMode) => {
return (
<>
@ -117,17 +142,16 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
<Col margin="lg">
<ResetTimeInfo title="Reset Time" label={resetTimeLabel} />
</Col>
{/* Tx Parameters */}
<TxParametersDetail
txParameters={txParameters}
onEdit={toggleEditMode}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
/>
</Block>
{/* Tx Parameters */}
<TxParametersDetail
txParameters={txParameters}
onEdit={toggleEditMode}
compact={false}
isTransactionCreation={isCreation}
isTransactionExecution={isExecution}
/>
<Row className={classes.modalDescription}>
<Row className={cn(classes.modalDescription, classes.gasCostsContainer)}>
<TransactionFees
gasCostFormatted={gasCostFormatted}
isExecution={isExecution}

Some files were not shown because too many files have changed in this diff Show More