commit
b3541705e5
|
@ -3,6 +3,7 @@
|
|||
.DS_Store
|
||||
yarn-error.log
|
||||
.env*
|
||||
.eslintcache
|
||||
/.idea
|
||||
dist
|
||||
electron-builder.yml
|
||||
|
@ -11,3 +12,4 @@ yalc.lock
|
|||
# testing
|
||||
/coverage/
|
||||
src/types/contracts/
|
||||
tsconfig.tsbuildinfo
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module.exports = [require.resolve('./scripts/rescripts/webpack.js')]
|
|
@ -1,6 +1,5 @@
|
|||
if: (branch = development) OR (branch = master) OR (type = pull_request) OR (tag IS present)
|
||||
sudo: required
|
||||
dist: bionic
|
||||
dist: focal
|
||||
language: node_js
|
||||
node_js:
|
||||
- '12'
|
||||
|
@ -43,14 +42,15 @@ matrix:
|
|||
- REACT_APP_GOOGLE_ANALYTICS=${REACT_APP_GOOGLE_ANALYTICS_ID_EWC}
|
||||
if: (branch = master AND NOT type = pull_request) OR tag IS present
|
||||
cache:
|
||||
yarn: true
|
||||
npm: false
|
||||
yarn: false
|
||||
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;
|
||||
before_install:
|
||||
# Needed to deploy pull request and releases
|
||||
- sudo apt-get update
|
||||
- sudo apt-get -y install python-pip python-dev libusb-1.0-0-dev libudev-dev
|
||||
- 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
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
const webpack = require('webpack')
|
||||
|
||||
module.exports = function override(config) {
|
||||
if (!config.plugins) {
|
||||
config.plugins = []
|
||||
}
|
||||
config.plugins.push(
|
||||
new webpack.ContextReplacementPlugin(/@truffle\/(contract|interface-adapter)/, (data) => {
|
||||
delete data.dependencies[0].critical
|
||||
return data
|
||||
}),
|
||||
)
|
||||
return config
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import Web3 from 'web3'
|
||||
|
||||
const window = global.window || {}
|
||||
window.web3 = window.web3 || {}
|
||||
window.web3 = {}
|
||||
window.web3.currentProvider = new Web3.providers.HttpProvider('http://localhost:8545')
|
||||
|
||||
global.window = window
|
||||
|
|
66
package.json
66
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "safe-react",
|
||||
"version": "2.16.2",
|
||||
"version": "2.17.0",
|
||||
"description": "Allowing crypto users manage funds in a safer way",
|
||||
"website": "https://github.com/gnosis/safe-react#readme",
|
||||
"bugs": {
|
||||
|
@ -16,13 +16,12 @@
|
|||
"email": "safe@gnosis.io"
|
||||
},
|
||||
"main": "public/electron.js",
|
||||
"postinstall": "patch-package electron-builder install-app-deps",
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"build-desktop": "cross-env REACT_APP_BUILD_FOR_DESKTOP=true REACT_APP_ENV=production yarn build-mainnet",
|
||||
"build-mainnet": "cross-env REACT_APP_NETWORK=mainnet yarn build",
|
||||
"build": "cross-env REACT_APP_APP_VERSION=$npm_package_version react-app-rewired --max-old-space-size=8192 build",
|
||||
"eject": "react-app-rewired eject",
|
||||
"build": "cross-env REACT_APP_APP_VERSION=$npm_package_version rescripts --max-old-space-size=8192 build",
|
||||
"eject": "rescripts eject",
|
||||
"electron-build": "electron-builder --mac --windows --linux",
|
||||
"electron-dev": "concurrently \"BROWSER=none yarn start\" \"wait-on http://localhost:3000 && electron .\"",
|
||||
"format:staged": "lint-staged",
|
||||
|
@ -38,8 +37,8 @@
|
|||
"prettier": "prettier './src/**/*.{js,jsx,ts,tsx}'",
|
||||
"release": "electron-builder --mac --linux --windows -p always",
|
||||
"start-mainnet": "REACT_APP_NETWORK=mainnet yarn start",
|
||||
"start": "react-app-rewired start",
|
||||
"test": "react-app-rewired test --env=jsdom",
|
||||
"start": "rescripts start",
|
||||
"test": "rescripts test --env=jsdom",
|
||||
"test:coverage": "yarn test --coverage --watchAll=false",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"storybook": "start-storybook -p 9009 -s public",
|
||||
|
@ -49,7 +48,7 @@
|
|||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged --allow-empty",
|
||||
"pre-push": "tsc"
|
||||
"pre-push": "tsc --noEmit --incremental"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
|
@ -150,9 +149,8 @@
|
|||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"@typescript-eslint/eslint-plugin": "^4.5.0",
|
||||
"@typescript-eslint/parser": "^4.5.0",
|
||||
"node-gyp": "^7.1.0"
|
||||
"@babel/core": "^7.12.0",
|
||||
"sass-loader": "^9.0.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
@ -167,22 +165,23 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@gnosis.pm/safe-apps-sdk": "https://github.com/gnosis/safe-apps-sdk.git#3f0689f",
|
||||
"@gnosis.pm/safe-apps-sdk": "1.0.0-beta.4",
|
||||
"@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#ff29c3c",
|
||||
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#bf3a84486b7353bd25447ddff39c406f6fafecc6",
|
||||
"@gnosis.pm/util-contracts": "2.0.6",
|
||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.30.0",
|
||||
"@material-ui/core": "4.11.0",
|
||||
"@ledgerhq/hw-transport-node-hid-singleton": "5.34.0",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.56",
|
||||
"@openzeppelin/contracts": "3.1.0",
|
||||
"@sentry/react": "^5.27.0",
|
||||
"@sentry/tracing": "^5.27.0",
|
||||
"@sentry/react": "^5.28.0",
|
||||
"@sentry/tracing": "^5.28.0",
|
||||
"@truffle/contract": "4.2.30",
|
||||
"async-sema": "^3.1.0",
|
||||
"axios": "0.21.0",
|
||||
"bignumber.js": "9.0.1",
|
||||
"bnc-onboard": "1.14.0",
|
||||
"bnc-onboard": "^1.16.1",
|
||||
"classnames": "^2.2.6",
|
||||
"concurrently": "^5.3.0",
|
||||
"connected-react-router": "6.8.0",
|
||||
|
@ -215,14 +214,13 @@
|
|||
"react-dom": "16.13.1",
|
||||
"react-final-form": "^6.5.2",
|
||||
"react-final-form-listeners": "^1.0.2",
|
||||
"react-ga": "3.2.1",
|
||||
"react-ga": "3.3.0",
|
||||
"react-hot-loader": "4.13.0",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-redux": "7.2.2",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-scripts": "^3.4.3",
|
||||
"react-scripts": "^4.0.1",
|
||||
"react-window": "^1.8.6",
|
||||
"recompose": "^0.30.0",
|
||||
"redux": "4.0.5",
|
||||
"redux-actions": "^2.6.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
|
@ -235,34 +233,35 @@
|
|||
"web3-utils": "^1.2.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rescripts/cli": "^0.0.15",
|
||||
"@sentry/cli": "^1.59.0",
|
||||
"@storybook/addon-actions": "^5.3.19",
|
||||
"@storybook/addon-links": "^5.3.19",
|
||||
"@storybook/addons": "^5.3.19",
|
||||
"@storybook/preset-create-react-app": "^3.1.5",
|
||||
"@storybook/react": "^5.3.19",
|
||||
"@testing-library/jest-dom": "5.11.5",
|
||||
"@testing-library/react": "10.4.9",
|
||||
"@typechain/web3-v1": "^1.0.0",
|
||||
"@testing-library/jest-dom": "5.11.6",
|
||||
"@testing-library/react": "11.2.2",
|
||||
"@typechain/web3-v1": "^2.0.0",
|
||||
"@types/history": "4.6.2",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/jest": "^26.0.16",
|
||||
"@types/lodash.memoize": "^4.1.6",
|
||||
"@types/node": "^14.14.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/styled-components": "^5.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "4.6.1",
|
||||
"@typescript-eslint/parser": "4.6.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
||||
"@typescript-eslint/parser": "^4.6.0",
|
||||
"autoprefixer": "9.8.6",
|
||||
"cross-env": "^7.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
"electron": "^9.3.3",
|
||||
"electron": "^9.3.5",
|
||||
"electron-builder": "22.9.1",
|
||||
"electron-notarize": "1.0.0",
|
||||
"eslint": "6.8.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-prettier": "^6.14.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
|
@ -271,12 +270,11 @@
|
|||
"eslint-plugin-sort-destructure-keys": "^1.3.5",
|
||||
"ethereumjs-abi": "0.6.8",
|
||||
"husky": "^4.3.0",
|
||||
"lint-staged": "^10.5.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"lint-staged": "^10.5.2",
|
||||
"patch-package": "^6.2.2",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.1.2",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
"prettier": "^2.2.0",
|
||||
"sass": "^1.29.0",
|
||||
"typechain": "^4.0.0",
|
||||
"typescript": "4.0.5",
|
||||
"wait-on": "5.2.0"
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
const { removeWebpackPlugin } = require('@rescripts/utilities')
|
||||
|
||||
module.exports = config => {
|
||||
const webpackWithoutEsLint = removeWebpackPlugin('ESLintWebpackPlugin', config)
|
||||
return webpackWithoutEsLint
|
||||
}
|
|
@ -80,8 +80,8 @@ const App: React.FC = ({ children }) => {
|
|||
const granted = useSelector(grantedSelector)
|
||||
const sidebarItems = useSidebarItems()
|
||||
|
||||
useLoadSafe(safeAddress)
|
||||
useSafeScheduledUpdates(safeAddress)
|
||||
const safeLoaded = useLoadSafe(safeAddress)
|
||||
useSafeScheduledUpdates(safeLoaded, safeAddress)
|
||||
|
||||
const sendFunds = safeActionsState.sendFunds
|
||||
const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : ''
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import Dot from '@material-ui/icons/FiberManualRecord'
|
||||
import * as React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Img from 'src/components/layout/Img'
|
||||
import { border, fancy, screenSm, warning } from 'src/theme/variables'
|
||||
|
||||
const key = require('../assets/key.svg')
|
||||
const triangle = require('../assets/triangle.svg')
|
||||
import KeyIcon from '../assets/key.svg'
|
||||
import TriangleIcon from '../assets/triangle.svg'
|
||||
|
||||
const styles = createStyles({
|
||||
root: {
|
||||
|
@ -71,12 +71,12 @@ export const KeyRing = ({
|
|||
hideDot = false,
|
||||
keySize,
|
||||
mode,
|
||||
}: Props): React.ReactElement => {
|
||||
}: Props): ReactElement => {
|
||||
const classes = useStyles(styles)
|
||||
const keyStyle = buildKeyStyleFrom(circleSize, center, dotSize)
|
||||
const dotStyle = buildDotStyleFrom(dotSize, dotTop, dotRight, mode)
|
||||
const isWarning = mode === 'warning'
|
||||
const img = isWarning ? triangle : key
|
||||
const img = isWarning ? TriangleIcon : KeyIcon
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -9,13 +9,13 @@ import { Link } from 'react-router-dom'
|
|||
import Provider from './Provider'
|
||||
|
||||
import Spacer from 'src/components/Spacer'
|
||||
import openHoc from 'src/components/hoc/OpenHoc'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Img from 'src/components/layout/Img'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { border, headerHeight, md, screenSm, sm } from 'src/theme/variables'
|
||||
import { useStateHandler } from 'src/logic/hooks/useStateHandler'
|
||||
|
||||
const logo = require('../assets/gnosis-safe-multisig-logo.svg')
|
||||
import SafeLogo from '../assets/gnosis-safe-multisig-logo.svg'
|
||||
|
||||
const styles = () => ({
|
||||
root: {
|
||||
|
@ -55,41 +55,45 @@ const styles = () => ({
|
|||
},
|
||||
})
|
||||
|
||||
const Layout = openHoc(({ classes, clickAway, open, providerDetails, providerInfo, toggle }) => (
|
||||
<Row className={classes.summary}>
|
||||
<Col className={classes.logo} middle="xs" start="xs">
|
||||
<Link to="/">
|
||||
<Img alt="Gnosis Team Safe" height={36} src={logo} testId="heading-gnosis-logo" />
|
||||
</Link>
|
||||
</Col>
|
||||
<Spacer />
|
||||
<Provider
|
||||
info={providerInfo}
|
||||
open={open}
|
||||
toggle={toggle}
|
||||
render={(providerRef) => (
|
||||
<Popper
|
||||
anchorEl={providerRef.current}
|
||||
className={classes.popper}
|
||||
open={open}
|
||||
placement="bottom"
|
||||
popperOptions={{ positionFixed: true }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Grow {...TransitionProps}>
|
||||
<>
|
||||
<ClickAwayListener mouseEvent="onClick" onClickAway={clickAway} touchEvent={false}>
|
||||
<List className={classes.root} component="div">
|
||||
{providerDetails}
|
||||
</List>
|
||||
</ClickAwayListener>
|
||||
</>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
)}
|
||||
/>
|
||||
</Row>
|
||||
))
|
||||
const Layout = ({ classes, providerDetails, providerInfo }) => {
|
||||
const { clickAway, open, toggle } = useStateHandler()
|
||||
|
||||
return (
|
||||
<Row className={classes.summary}>
|
||||
<Col className={classes.logo} middle="xs" start="xs">
|
||||
<Link to="/">
|
||||
<Img alt="Gnosis Team Safe" height={36} src={SafeLogo} testId="heading-gnosis-logo" />
|
||||
</Link>
|
||||
</Col>
|
||||
<Spacer />
|
||||
<Provider
|
||||
info={providerInfo}
|
||||
open={open}
|
||||
toggle={toggle}
|
||||
render={(providerRef) => (
|
||||
<Popper
|
||||
anchorEl={providerRef.current}
|
||||
className={classes.popper}
|
||||
open={open}
|
||||
placement="bottom"
|
||||
popperOptions={{ positionFixed: true }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Grow {...TransitionProps}>
|
||||
<>
|
||||
<ClickAwayListener mouseEvent="onClick" onClickAway={clickAway} touchEvent={false}>
|
||||
<List className={classes.root} component="div">
|
||||
{providerDetails}
|
||||
</List>
|
||||
</ClickAwayListener>
|
||||
</>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
)}
|
||||
/>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(Layout)
|
||||
|
|
|
@ -19,7 +19,7 @@ import { KeyRing } from 'src/components/AppLayout/Header/components/KeyRing'
|
|||
import { CircleDot } from '../CircleDot'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
const walletIcon = require('../../assets/wallet.svg')
|
||||
import WalletIcon from '../../assets/wallet.svg'
|
||||
|
||||
const styles = createStyles({
|
||||
container: {
|
||||
|
@ -150,7 +150,7 @@ export const UserDetails = ({
|
|||
Wallet
|
||||
</Paragraph>
|
||||
<Spacer />
|
||||
<Img alt="Wallet icon" className={classes.logo} height={14} src={walletIcon} />
|
||||
<Img alt="Wallet icon" className={classes.logo} height={14} src={WalletIcon} />
|
||||
<Paragraph align="right" className={classes.labels} noMargin weight="bolder">
|
||||
{upperFirst(provider)}
|
||||
</Paragraph>
|
||||
|
|
|
@ -44,6 +44,7 @@ const IconContainer = styled.div`
|
|||
justify-content: space-evenly;
|
||||
`
|
||||
const StyledButton = styled(Button)`
|
||||
padding: 0 18px;
|
||||
*:first-child {
|
||||
margin: 0 4px 0 0;
|
||||
}
|
||||
|
|
|
@ -63,21 +63,17 @@ export const onboardUser = async (): Promise<boolean> => {
|
|||
return walletSelected && onboard.walletCheck()
|
||||
}
|
||||
|
||||
const ConnectButton = (props): React.ReactElement => (
|
||||
<Button
|
||||
color="primary"
|
||||
minWidth={140}
|
||||
onClick={async () => {
|
||||
const walletSelected = await onboard.walletSelect()
|
||||
export const onConnectButtonClick = async () => {
|
||||
const walletSelected = await onboard.walletSelect()
|
||||
|
||||
// perform wallet checks only if user selected a wallet
|
||||
if (walletSelected) {
|
||||
await onboard.walletCheck()
|
||||
}
|
||||
}}
|
||||
variant="contained"
|
||||
{...props}
|
||||
>
|
||||
// perform wallet checks only if user selected a wallet
|
||||
if (walletSelected) {
|
||||
await onboard.walletCheck()
|
||||
}
|
||||
}
|
||||
|
||||
const ConnectButton = (props): React.ReactElement => (
|
||||
<Button color="primary" minWidth={140} onClick={onConnectButtonClick} variant="contained" {...props}>
|
||||
Connect
|
||||
</Button>
|
||||
)
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import Bold from 'src/components/layout/Bold'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Paragraph from 'src/components/layout/Paragraph/index'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { CreateSafe } from 'src/routes/welcome/components/Layout'
|
||||
|
||||
const NoSafe = ({ provider, text }) => (
|
||||
<Row>
|
||||
<Col center="xs" margin="md" sm={10} smOffset={2} start="sm" xs={12}>
|
||||
<Paragraph size="lg">
|
||||
<Bold>{text}</Bold>
|
||||
</Paragraph>
|
||||
</Col>
|
||||
<Col center="xs" margin="md" sm={10} smOffset={2} start="sm" xs={12}>
|
||||
<CreateSafe provider={provider} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
|
||||
export default NoSafe
|
|
@ -26,7 +26,7 @@ export interface StepperPageFormProps {
|
|||
}
|
||||
|
||||
interface StepperPageProps {
|
||||
validate?: (...args: unknown[]) => undefined | string[] | Promise<undefined | Record<string, string>>
|
||||
validate?: (...args: unknown[]) => undefined | Record<string, string> | Promise<undefined | Record<string, string>>
|
||||
component: (
|
||||
...args: unknown[]
|
||||
) => (controls: React.ReactElement, formProps: StepperPageFormProps) => React.ReactElement
|
||||
|
|
|
@ -166,22 +166,16 @@ describe('Forms > Validators', () => {
|
|||
})
|
||||
|
||||
describe('uniqueAddress validator', () => {
|
||||
it('Returns undefined for an address not contained in the passed array', async () => {
|
||||
it('Returns undefined if `addresses` does not contains the provided address', async () => {
|
||||
const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe']
|
||||
|
||||
expect(uniqueAddress(addresses)()).toBeUndefined()
|
||||
expect(uniqueAddress(addresses)('0x2D6F2B448b0F711Eb81f2929566504117d67E44F')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('Returns an error message for an array with duplicated values', async () => {
|
||||
const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe']
|
||||
it('Returns an error message if address is in the `addresses` list already', async () => {
|
||||
const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', '0x2D6F2B448b0F711Eb81f2929566504117d67E44F']
|
||||
|
||||
expect(uniqueAddress(addresses)()).toEqual(ADDRESS_REPEATED_ERROR)
|
||||
})
|
||||
|
||||
it('Returns an error message for an array with duplicated checksum and not checksum values', async () => {
|
||||
const addresses = ['0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe', '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae']
|
||||
|
||||
expect(uniqueAddress(addresses)()).toEqual(ADDRESS_REPEATED_ERROR)
|
||||
expect(uniqueAddress(addresses)('0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe')).toEqual(ADDRESS_REPEATED_ERROR)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { List } from 'immutable'
|
||||
import memoize from 'lodash.memoize'
|
||||
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { isFeatureEnabled } from 'src/config'
|
||||
import { FEATURES } from 'src/config/networks/network.d'
|
||||
import { List } from 'immutable'
|
||||
|
||||
type ValidatorReturnType = string | undefined
|
||||
export type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
|
||||
|
@ -87,17 +89,9 @@ export const minMaxLength = (minLen: number, maxLen: number) => (value: string):
|
|||
|
||||
export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
|
||||
|
||||
export const uniqueAddress = (addresses: string[] | List<string>): GenericValidatorType => (): ValidatorReturnType => {
|
||||
// @ts-expect-error both list and array have signatures for map but TS thinks they're not compatible
|
||||
const lowercaseAddresses = addresses.map((address) => address.toLowerCase())
|
||||
const uniqueAddresses = new Set(lowercaseAddresses)
|
||||
const lengthPropName = 'size' in addresses ? 'size' : 'length'
|
||||
|
||||
if (uniqueAddresses.size !== addresses?.[lengthPropName]) {
|
||||
return ADDRESS_REPEATED_ERROR
|
||||
}
|
||||
|
||||
return undefined
|
||||
export const uniqueAddress = (addresses: string[] | List<string> = []) => (address?: string): string | undefined => {
|
||||
const addressExists = addresses.some((addressFromList) => sameAddress(addressFromList, address))
|
||||
return addressExists ? ADDRESS_REPEATED_ERROR : undefined
|
||||
}
|
||||
|
||||
export const composeValidators = (...validators: Validator[]) => (value: unknown): ValidatorReturnType =>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import { withStateHandlers } from 'recompose'
|
||||
|
||||
export default withStateHandlers(() => ({ open: false }), {
|
||||
toggle: ({ open }) => () => ({ open: !open }),
|
||||
clickAway: () => () => ({ open: false }),
|
||||
})
|
|
@ -7,19 +7,6 @@
|
|||
padding: 12px 0 0 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: #{$screenLg}px) {
|
||||
.page {
|
||||
padding: 72px $lg 0 $lg;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: #{$screenLg}px) and (max-width: 1360px) {
|
||||
.page {
|
||||
padding: 96px 120px 0 120px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.center {
|
||||
align-self: center;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,9 @@ import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe'
|
|||
import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage'
|
||||
import { store } from 'src/store'
|
||||
import { SENTRY_DSN } from './utils/constants'
|
||||
import { disableMMAutoRefreshWarning } from './utils/mm_warnings'
|
||||
|
||||
disableMMAutoRefreshWarning()
|
||||
|
||||
BigNumber.set({ EXPONENTIAL_AT: [-7, 255] })
|
||||
|
||||
|
|
|
@ -8,9 +8,10 @@ import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
|
|||
|
||||
export const addressBookSelector = (state: AppReduxState): AddressBookState => state[ADDRESS_BOOK_REDUCER_ID]
|
||||
|
||||
export const addressBookAddressesListSelector = createSelector(addressBookSelector, (addressBook): string[] => {
|
||||
export const addressBookAddressesListSelector = (state: AppReduxState): string[] => {
|
||||
const addressBook = addressBookSelector(state)
|
||||
return addressBook.map((entry) => entry.address)
|
||||
})
|
||||
}
|
||||
|
||||
export const getNameFromAddressBookSelector = createSelector(
|
||||
addressBookSelector,
|
||||
|
|
|
@ -64,5 +64,5 @@ export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => {
|
|||
}
|
||||
|
||||
export const isPayable = (method: AbiItem | AbiItemExtended): boolean => {
|
||||
return !!method?.payable
|
||||
return Boolean(method?.payable) || method.stateMutability === 'payable'
|
||||
}
|
||||
|
|
|
@ -57,6 +57,10 @@ const generateBatchRequests = <ReturnValues>({
|
|||
if (type !== undefined) {
|
||||
request = web3[type][method].request(...args, resolver)
|
||||
} else {
|
||||
if (address === null) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
request = contractInstance.methods[method](...args).call.request(resolver)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import axios from 'axios'
|
||||
|
||||
import { getSafeServiceBaseUrl } from 'src/config'
|
||||
import { fetchTokenCurrenciesBalances } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
|
||||
import {
|
||||
fetchTokenCurrenciesBalances,
|
||||
BalanceEndpoint,
|
||||
} from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances'
|
||||
import { aNewStore } from 'src/store'
|
||||
|
||||
jest.mock('axios')
|
||||
|
@ -20,8 +23,7 @@ describe('fetchTokenCurrenciesBalances', () => {
|
|||
// given
|
||||
const expectedResult = [
|
||||
{
|
||||
tokenAddress: null,
|
||||
token: null,
|
||||
tokenAddress: '',
|
||||
balance: '849890000000000000',
|
||||
fiatBalance: '337.2449',
|
||||
fiatConversion: '396.81',
|
||||
|
@ -30,6 +32,8 @@ describe('fetchTokenCurrenciesBalances', () => {
|
|||
{
|
||||
tokenAddress: '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa',
|
||||
token: {
|
||||
address: '0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa',
|
||||
balance: '24698677800000000000',
|
||||
name: 'Dai',
|
||||
symbol: 'DAI',
|
||||
decimals: 18,
|
||||
|
@ -41,10 +45,11 @@ describe('fetchTokenCurrenciesBalances', () => {
|
|||
fiatCode: 'USD',
|
||||
},
|
||||
]
|
||||
|
||||
const apiUrl = getSafeServiceBaseUrl(safeAddress)
|
||||
|
||||
// @ts-ignore
|
||||
axios.get.mockImplementationOnce(() => Promise.resolve(expectedResult))
|
||||
axios.get.mockImplementationOnce(() => Promise.resolve({ data: expectedResult }))
|
||||
|
||||
// when
|
||||
const result = await fetchTokenCurrenciesBalances(safeAddress, excludeSpamTokens)
|
||||
|
|
|
@ -14,9 +14,9 @@ const fetchCurrenciesRates = async (
|
|||
let rate = 0
|
||||
if (sameString(targetCurrencyValue, AVAILABLE_CURRENCIES.NETWORK)) {
|
||||
try {
|
||||
const result = await fetchTokenCurrenciesBalances(safeAddress)
|
||||
if (result?.data?.length) {
|
||||
rate = new BigNumber(1).div(result.data[0].fiatConversion).toNumber()
|
||||
const tokenCurrenciesBalances = await fetchTokenCurrenciesBalances(safeAddress)
|
||||
if (tokenCurrenciesBalances?.length) {
|
||||
rate = new BigNumber(1).div(tokenCurrenciesBalances[0].fiatConversion).toNumber()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Fetching ${AVAILABLE_CURRENCIES.NETWORK} data from the relayer errored`, error)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import axios, { AxiosResponse } from 'axios'
|
||||
import axios from 'axios'
|
||||
|
||||
import { getSafeServiceBaseUrl } from 'src/config'
|
||||
import { TokenProps } from 'src/logic/tokens/store/model/token'
|
||||
|
@ -15,8 +15,8 @@ export type BalanceEndpoint = {
|
|||
export const fetchTokenCurrenciesBalances = (
|
||||
safeAddress: string,
|
||||
excludeSpamTokens = true,
|
||||
): Promise<AxiosResponse<BalanceEndpoint[]>> => {
|
||||
): Promise<BalanceEndpoint[]> => {
|
||||
const url = `${getSafeServiceBaseUrl(safeAddress)}/balances/usd/?exclude_spam=${excludeSpamTokens}`
|
||||
|
||||
return axios.get(url)
|
||||
return axios.get(url).then(({ data }) => data)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
type ReturnValue = {
|
||||
open: boolean
|
||||
toggle: () => void
|
||||
clickAway: () => void
|
||||
}
|
||||
|
||||
export const useStateHandler = (openInitialValue = false): ReturnValue => {
|
||||
const [open, setOpen] = useState(openInitialValue)
|
||||
|
||||
return {
|
||||
open,
|
||||
toggle: () => setOpen((open) => !open),
|
||||
clickAway: () => setOpen(false),
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage'
|
||||
|
@ -10,26 +10,26 @@ import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTr
|
|||
import fetchSafeCreationTx from 'src/logic/safe/store/actions/fetchSafeCreationTx'
|
||||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
|
||||
export const useLoadSafe = (safeAddress?: string): void => {
|
||||
export const useLoadSafe = (safeAddress?: string): boolean => {
|
||||
const dispatch = useDispatch<Dispatch>()
|
||||
const [isSafeLoaded, setIsSafeLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = () => {
|
||||
const fetchData = async () => {
|
||||
if (safeAddress) {
|
||||
dispatch(fetchLatestMasterContractVersion())
|
||||
.then(() => {
|
||||
dispatch(fetchSafe(safeAddress))
|
||||
return dispatch(fetchSafeTokens(safeAddress))
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(fetchSafeCreationTx(safeAddress))
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
return dispatch(addViewedSafe(safeAddress))
|
||||
})
|
||||
await dispatch(fetchLatestMasterContractVersion())
|
||||
await dispatch(fetchSafe(safeAddress))
|
||||
setIsSafeLoaded(true)
|
||||
await dispatch(fetchSafeTokens(safeAddress))
|
||||
dispatch(fetchSafeCreationTx(safeAddress))
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
dispatch(addViewedSafe(safeAddress))
|
||||
}
|
||||
}
|
||||
dispatch(loadAddressBookFromStorage())
|
||||
|
||||
fetchData()
|
||||
}, [dispatch, safeAddress])
|
||||
|
||||
return isSafeLoaded
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe'
|
|||
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
|
||||
import { TIMEOUT } from 'src/utils/constants'
|
||||
|
||||
export const useSafeScheduledUpdates = (safeAddress?: string): void => {
|
||||
export const useSafeScheduledUpdates = (safeLoaded: boolean, safeAddress?: string): void => {
|
||||
const dispatch = useDispatch()
|
||||
const timer = useRef<number>()
|
||||
|
||||
|
@ -34,7 +34,7 @@ export const useSafeScheduledUpdates = (safeAddress?: string): void => {
|
|||
}
|
||||
}
|
||||
|
||||
if (safeAddress) {
|
||||
if (safeAddress && safeLoaded) {
|
||||
fetchSafeData(safeAddress)
|
||||
}
|
||||
|
||||
|
@ -42,5 +42,5 @@ export const useSafeScheduledUpdates = (safeAddress?: string): void => {
|
|||
mounted = false
|
||||
clearTimeout(timer.current)
|
||||
}
|
||||
}, [dispatch, safeAddress])
|
||||
}, [dispatch, safeAddress, safeLoaded])
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ export interface CreateTransactionArgs {
|
|||
type CreateTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
|
||||
type ConfirmEventHandler = (safeTxHash: string) => void
|
||||
type ErrorEventHandler = () => void
|
||||
export const METAMASK_REJECT_CONFIRM_TX_ERROR_CODE = 4001
|
||||
|
||||
const createTransaction = (
|
||||
{
|
||||
|
@ -210,20 +211,21 @@ const createTransaction = (
|
|||
? `${notificationsQueue.afterExecutionError.message} - ${err.message}`
|
||||
: notificationsQueue.afterExecutionError.message
|
||||
|
||||
console.error(`Error creating the TX: `, err)
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
if (pendingExecutionKey) {
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
}
|
||||
|
||||
dispatch(enqueueSnackbar(errorMsg))
|
||||
dispatch(enqueueSnackbar({ key: err.code, message: errorMsg, options: { persist: true, variant: 'error' } }))
|
||||
|
||||
const executeDataUsedSignatures = safeInstance.methods
|
||||
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
|
||||
.encodeABI()
|
||||
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeDataUsedSignatures, from)
|
||||
console.error(`Error creating the TX - an attempt to get the error message: ${errMsg}`)
|
||||
if (err.code !== METAMASK_REJECT_CONFIRM_TX_ERROR_CODE) {
|
||||
const executeDataUsedSignatures = safeInstance.methods
|
||||
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
|
||||
.encodeABI()
|
||||
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeDataUsedSignatures, from)
|
||||
console.error(`Error creating the TX - an attempt to get the error message: ${errMsg}`)
|
||||
}
|
||||
}
|
||||
|
||||
return txHash
|
||||
|
|
|
@ -117,19 +117,17 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch
|
|||
|
||||
const modules = await getModules(safeInfo)
|
||||
|
||||
dispatch(
|
||||
updateSafe({
|
||||
address: safeAddress,
|
||||
name: localSafe?.name,
|
||||
modules,
|
||||
spendingLimits,
|
||||
nonce: Number(remoteNonce),
|
||||
threshold: Number(remoteThreshold),
|
||||
featuresEnabled: localSafe?.currentVersion
|
||||
? enabledFeatures(localSafe?.currentVersion)
|
||||
: localSafe?.featuresEnabled,
|
||||
}),
|
||||
)
|
||||
const updatedSafe = {
|
||||
address: safeAddress,
|
||||
name: localSafe?.name,
|
||||
modules,
|
||||
spendingLimits,
|
||||
nonce: Number(remoteNonce),
|
||||
threshold: Number(remoteThreshold),
|
||||
featuresEnabled: localSafe?.currentVersion ? enabledFeatures(localSafe.currentVersion) : localSafe?.featuresEnabled,
|
||||
}
|
||||
|
||||
dispatch(updateSafe(updatedSafe))
|
||||
|
||||
// If the remote owners does not contain a local address, we remove that local owner
|
||||
localOwners.forEach((localAddress) => {
|
||||
|
|
|
@ -97,7 +97,7 @@ const processTransaction = ({
|
|||
const signature = await tryOffchainSigning(tx.safeTxHash, { ...txArgs, safeAddress }, hardwareWallet)
|
||||
|
||||
if (signature) {
|
||||
dispatch(closeSnackbarAction(beforeExecutionKey))
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
await saveTxToHistory({ ...txArgs, signature })
|
||||
// TODO: while we wait for the tx to be stored in the service and later update the tx info
|
||||
|
@ -130,7 +130,7 @@ const processTransaction = ({
|
|||
.send(sendParams)
|
||||
.once('transactionHash', async (hash: string) => {
|
||||
txHash = hash
|
||||
dispatch(closeSnackbarAction(beforeExecutionKey))
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
pendingExecutionKey = dispatch(enqueueSnackbar(notificationsQueue.pendingExecution))
|
||||
|
||||
|
@ -141,19 +141,19 @@ const processTransaction = ({
|
|||
])
|
||||
dispatch(fetchTransactions(safeAddress))
|
||||
} catch (e) {
|
||||
dispatch(closeSnackbarAction(pendingExecutionKey))
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
await storeTx({ transaction: tx, safeAddress, dispatch, state })
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
.on('error', (error) => {
|
||||
dispatch(closeSnackbarAction(pendingExecutionKey))
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
storeTx({ transaction: tx, safeAddress, dispatch, state })
|
||||
console.error('Processing transaction error: ', error)
|
||||
})
|
||||
.then(async (receipt) => {
|
||||
if (pendingExecutionKey) {
|
||||
dispatch(closeSnackbarAction(pendingExecutionKey))
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
}
|
||||
|
||||
dispatch(
|
||||
|
@ -178,17 +178,16 @@ const processTransaction = ({
|
|||
const errorMsg = err.message
|
||||
? `${notificationsQueue.afterExecutionError.message} - ${err.message}`
|
||||
: notificationsQueue.afterExecutionError.message
|
||||
console.error(err)
|
||||
|
||||
if (txHash !== undefined) {
|
||||
dispatch(closeSnackbarAction(beforeExecutionKey))
|
||||
dispatch(closeSnackbarAction({ key: beforeExecutionKey }))
|
||||
|
||||
if (pendingExecutionKey) {
|
||||
dispatch(closeSnackbarAction(pendingExecutionKey))
|
||||
}
|
||||
if (pendingExecutionKey) {
|
||||
dispatch(closeSnackbarAction({ key: pendingExecutionKey }))
|
||||
}
|
||||
|
||||
dispatch(enqueueSnackbar(errorMsg))
|
||||
dispatch(enqueueSnackbar({ key: err.code, message: errorMsg, options: { persist: true, variant: 'error' } }))
|
||||
|
||||
if (txHash) {
|
||||
const executeData = safeInstance.methods.approveHash(txHash).encodeABI()
|
||||
const errMsg = await getErrorMessage(safeInstance.options.address, 0, executeData, from)
|
||||
console.error(`Error executing the TX: ${errMsg}`)
|
||||
|
|
|
@ -18,6 +18,7 @@ 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'
|
||||
|
||||
export const SAFE_REDUCER_ID = 'safes'
|
||||
export const DEFAULT_SAFE_INITIAL_STATE = 'NOT_ASKED'
|
||||
|
@ -78,11 +79,15 @@ export default handleActions(
|
|||
const safe = action.payload
|
||||
const safeAddress = safe.address
|
||||
|
||||
return state.updateIn(
|
||||
['safes', safeAddress],
|
||||
makeSafe({ name: safe?.name || 'LOADED SAFE', address: safeAddress }),
|
||||
(prevSafe) => updateSafeProps(prevSafe, safe),
|
||||
)
|
||||
const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safeAddress]))
|
||||
|
||||
return shouldUpdate
|
||||
? state.updateIn(
|
||||
['safes', safeAddress],
|
||||
makeSafe({ name: safe?.name || 'LOADED SAFE', address: safeAddress }),
|
||||
(prevSafe) => updateSafeProps(prevSafe, safe),
|
||||
)
|
||||
: state
|
||||
},
|
||||
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerMap, action) => {
|
||||
const tokenAddress = action.payload
|
||||
|
@ -107,11 +112,15 @@ export default handleActions(
|
|||
return state.setIn(['safes', safe.address], makeSafe(safe))
|
||||
}
|
||||
|
||||
return state.updateIn(
|
||||
['safes', safe.address],
|
||||
makeSafe({ name: safe?.name || 'LOADED SAFE', address: safe.address }),
|
||||
(prevSafe) => updateSafeProps(prevSafe, safe),
|
||||
)
|
||||
const shouldUpdate = shouldSafeStoreBeUpdated(safe, state.getIn(['safes', safe.safeAddress]))
|
||||
|
||||
return shouldUpdate
|
||||
? state.updateIn(
|
||||
['safes', safe.address],
|
||||
makeSafe({ name: safe?.name || 'LOADED SAFE', address: safe.address }),
|
||||
(prevSafe) => updateSafeProps(prevSafe, safe),
|
||||
)
|
||||
: state
|
||||
},
|
||||
[REMOVE_SAFE]: (state: SafeReducerMap, action) => {
|
||||
const safeAddress = action.payload
|
||||
|
|
|
@ -72,14 +72,13 @@ export const safeTransactionsSelector = createSelector(
|
|||
},
|
||||
)
|
||||
|
||||
export const addressBookQueryParamsSelector = (state: AppReduxState): string | null => {
|
||||
export const addressBookQueryParamsSelector = (state: AppReduxState): string | undefined => {
|
||||
const { location } = state.router
|
||||
let entryAddressToEditOrCreateNew = null
|
||||
if (location && location.query) {
|
||||
|
||||
if (location?.query) {
|
||||
const { entryAddress } = location.query
|
||||
entryAddressToEditOrCreateNew = entryAddress
|
||||
return entryAddress
|
||||
}
|
||||
return entryAddressToEditOrCreateNew
|
||||
}
|
||||
|
||||
export const safeCancellationTransactionsSelector = createSelector(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Transaction } from '@gnosis.pm/safe-apps-sdk'
|
||||
import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1'
|
||||
import { AbiItem } from 'web3-utils'
|
||||
import { MultiSend } from 'src/types/contracts/MultiSend.d'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getEIP712Signer } from './EIP712Signer'
|
||||
import { ethSigner } from './ethSigner'
|
||||
import { METAMASK_REJECT_CONFIRM_TX_ERROR_CODE } from 'src/logic/safe/store/actions/createTransaction'
|
||||
|
||||
// 1. we try to sign via EIP-712 if user's wallet supports it
|
||||
// 2. If not, try to use eth_sign (Safe version has to be >1.1.1)
|
||||
|
@ -29,9 +30,8 @@ export const tryOffchainSigning = async (safeTxHash: string, txArgs, isHW: boole
|
|||
break
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
// Metamask sign request error code
|
||||
if (err.code === 4001) {
|
||||
throw new Error('User denied sign request')
|
||||
if (err.code === METAMASK_REJECT_CONFIRM_TX_ERROR_CODE) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,369 @@
|
|||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
import { List, Set, Map } from 'immutable'
|
||||
import { shouldSafeStoreBeUpdated } from 'src/logic/safe/utils/shouldSafeStoreBeUpdated'
|
||||
|
||||
const getMockedOldSafe = ({
|
||||
address,
|
||||
needsUpdate,
|
||||
balances,
|
||||
recurringUser,
|
||||
blacklistedAssets,
|
||||
blacklistedTokens,
|
||||
activeAssets,
|
||||
activeTokens,
|
||||
owners,
|
||||
featuresEnabled,
|
||||
currentVersion,
|
||||
latestIncomingTxBlock,
|
||||
ethBalance,
|
||||
threshold,
|
||||
name,
|
||||
nonce,
|
||||
modules,
|
||||
spendingLimits,
|
||||
}: Partial<SafeRecordProps>): SafeRecordProps => {
|
||||
const owner1 = {
|
||||
name: 'MockedOwner1',
|
||||
address: '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d',
|
||||
}
|
||||
const owner2 = {
|
||||
name: 'MockedOwner2',
|
||||
address: '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3',
|
||||
}
|
||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
||||
const mockedActiveAssetsAddress1 = '0x503ab2a6A70c6C6ec8b25a4C87C784e1c8f8e8CD'
|
||||
const mockedActiveAssetsAddress2 = '0xfdd4E685361CB7E89a4D27e03DCd0001448d731F'
|
||||
const mockedBlacklistedTokenAddress1 = '0xc7d892dca37a244Fb1A7461e6141e58Ead460282'
|
||||
const mockedBlacklistedAssetAddress1 = '0x0ac539137c4c99001f16Dd132E282F99A02Ddc3F'
|
||||
|
||||
return {
|
||||
name: name || 'MockedSafe',
|
||||
address: address || '0xAE173F30ec9A293d37c44BA68d3fCD35F989Ce9F',
|
||||
threshold: threshold || 2,
|
||||
ethBalance: ethBalance || '10',
|
||||
owners: owners || List([owner1, owner2]),
|
||||
modules: modules || [],
|
||||
spendingLimits: spendingLimits || [],
|
||||
activeTokens: activeTokens || Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]),
|
||||
activeAssets: activeAssets || Set([mockedActiveAssetsAddress1, mockedActiveAssetsAddress2]),
|
||||
blacklistedTokens: blacklistedTokens || Set([mockedBlacklistedTokenAddress1]),
|
||||
blacklistedAssets: blacklistedAssets || Set([mockedBlacklistedAssetAddress1]),
|
||||
balances:
|
||||
balances ||
|
||||
Map({
|
||||
[mockedActiveTokenAddress1]: '100',
|
||||
[mockedActiveTokenAddress2]: '10',
|
||||
}),
|
||||
nonce: nonce || 2,
|
||||
latestIncomingTxBlock: latestIncomingTxBlock || 1,
|
||||
recurringUser: recurringUser || false,
|
||||
currentVersion: currentVersion || 'v1.1.1',
|
||||
needsUpdate: needsUpdate || false,
|
||||
featuresEnabled: featuresEnabled || [],
|
||||
}
|
||||
}
|
||||
|
||||
describe('shouldSafeStoreBeUpdated', () => {
|
||||
it(`Given two equal safes, should return false`, () => {
|
||||
// given
|
||||
const oldSafe = getMockedOldSafe({})
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(oldSafe, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(false)
|
||||
})
|
||||
it(`Given an old safe and a new address for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldAddress = '0x123'
|
||||
const newAddress = '0x'
|
||||
const oldSafe = getMockedOldSafe({ address: oldAddress })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
address: newAddress,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old safe and a new name for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldName = 'oldName'
|
||||
const newName = 'newName'
|
||||
const oldSafe = getMockedOldSafe({ name: oldName })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
name: newName,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old safe and a new threshold for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldThreshold = 1
|
||||
const newThreshold = 2
|
||||
const oldSafe = getMockedOldSafe({ threshold: oldThreshold })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
threshold: newThreshold,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old ethBalance and a new ethBalance for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldEthBalance = '1'
|
||||
const newEthBalance = '2'
|
||||
const oldSafe = getMockedOldSafe({ ethBalance: oldEthBalance })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
ethBalance: newEthBalance,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old owners list and a new owners list for the safe, should return true`, () => {
|
||||
// given
|
||||
const owner1 = {
|
||||
name: 'MockedOwner1',
|
||||
address: '0x3bE3c2dE077FBC409ae50AFFA66a94a9aE669A8d',
|
||||
}
|
||||
const owner2 = {
|
||||
name: 'MockedOwner2',
|
||||
address: '0xA2366b0c2607de70777d87aCdD1D22F0708fA6a3',
|
||||
}
|
||||
const oldSafe = getMockedOldSafe({ owners: List([owner1, owner2]) })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
owners: List([owner1]),
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old modules list and a new modules list for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldModulesList = []
|
||||
const newModulesList = null
|
||||
const oldSafe = getMockedOldSafe({ modules: oldModulesList })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
modules: newModulesList,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old spendingLimits list and a new spendingLimits list for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldSpendingLimitsList = []
|
||||
const newSpendingLimitsList = null
|
||||
const oldSafe = getMockedOldSafe({ spendingLimits: oldSpendingLimitsList })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
modules: newSpendingLimitsList,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old activeTokens list and a new activeTokens list for the safe, should return true`, () => {
|
||||
// given
|
||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
||||
const oldActiveTokens = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
|
||||
const newActiveTokens = Set([mockedActiveTokenAddress1])
|
||||
const oldSafe = getMockedOldSafe({ activeTokens: oldActiveTokens })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
activeTokens: newActiveTokens,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old activeAssets list and a new activeAssets list for the safe, should return true`, () => {
|
||||
// given
|
||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
||||
const oldActiveAssets = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
|
||||
const newActiveAssets = Set([mockedActiveTokenAddress1])
|
||||
const oldSafe = getMockedOldSafe({ activeAssets: oldActiveAssets })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
activeAssets: newActiveAssets,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old blacklistedTokens list and a new blacklistedTokens list for the safe, should return true`, () => {
|
||||
// given
|
||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
||||
const oldBlacklistedTokens = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
|
||||
const newBlacklistedTokens = Set([mockedActiveTokenAddress1])
|
||||
const oldSafe = getMockedOldSafe({ blacklistedTokens: oldBlacklistedTokens })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
blacklistedTokens: newBlacklistedTokens,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old blacklistedAssets list and a new blacklistedAssets list for the safe, should return true`, () => {
|
||||
// given
|
||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
||||
const oldBlacklistedAssets = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2])
|
||||
const newBlacklistedAssets = Set([mockedActiveTokenAddress1])
|
||||
const oldSafe = getMockedOldSafe({ blacklistedAssets: oldBlacklistedAssets })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
blacklistedAssets: newBlacklistedAssets,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old balances list and a new balances list for the safe, should return true`, () => {
|
||||
// given
|
||||
const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1'
|
||||
const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1'
|
||||
const oldBalances = Map({
|
||||
[mockedActiveTokenAddress1]: '100',
|
||||
[mockedActiveTokenAddress2]: '10',
|
||||
})
|
||||
const newBalances = Map({
|
||||
[mockedActiveTokenAddress1]: '100',
|
||||
})
|
||||
const oldSafe = getMockedOldSafe({ balances: oldBalances })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
balances: newBalances,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old nonce and a new nonce for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldNonce = 1
|
||||
const newNonce = 2
|
||||
const oldSafe = getMockedOldSafe({ nonce: oldNonce })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
nonce: newNonce,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old newLatestIncomingTxBlock and a new newLatestIncomingTxBlock for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldLatestIncomingTxBlock = 1
|
||||
const newLatestIncomingTxBlock = 2
|
||||
const oldSafe = getMockedOldSafe({ latestIncomingTxBlock: oldLatestIncomingTxBlock })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
latestIncomingTxBlock: newLatestIncomingTxBlock,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old recurringUser and a new recurringUser for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldRecurringUser = true
|
||||
const newRecurringUser = false
|
||||
const oldSafe = getMockedOldSafe({ recurringUser: oldRecurringUser })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
recurringUser: newRecurringUser,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old recurringUser and a new recurringUser for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldCurrentVersion = '1.1.1'
|
||||
const newCurrentVersion = '1.0.0'
|
||||
const oldSafe = getMockedOldSafe({ currentVersion: oldCurrentVersion })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
currentVersion: newCurrentVersion,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old needsUpdate and a new needsUpdate for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldNeedsUpdate = false
|
||||
const newNeedsUpdate = true
|
||||
const oldSafe = getMockedOldSafe({ needsUpdate: oldNeedsUpdate })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
needsUpdate: newNeedsUpdate,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
it(`Given an old featuresEnabled and a new featuresEnabled for the safe, should return true`, () => {
|
||||
// given
|
||||
const oldFeaturesEnabled = []
|
||||
const newFeaturesEnabled = undefined
|
||||
const oldSafe = getMockedOldSafe({ featuresEnabled: oldFeaturesEnabled })
|
||||
const newSafeProps: Partial<SafeRecordProps> = {
|
||||
featuresEnabled: newFeaturesEnabled,
|
||||
}
|
||||
|
||||
// When
|
||||
const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe)
|
||||
|
||||
// Then
|
||||
expect(expectedResult).toEqual(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,26 @@
|
|||
import { Map } from 'immutable'
|
||||
|
||||
import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
|
||||
|
||||
// This function checks if an object is a Subset of a Safe State and that they have the same values
|
||||
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 type is Immutable Map, List or Object we use Immutable equals
|
||||
return superObj[key].equals(subObj[key])
|
||||
}
|
||||
return isStateSubset(superObj[key], subObj[key])
|
||||
}
|
||||
return subObj[key] === superObj[key]
|
||||
})
|
||||
}
|
||||
|
||||
export const shouldSafeStoreBeUpdated = (
|
||||
newSafeProps: Partial<SafeRecordProps>,
|
||||
oldSafeProps?: SafeRecordProps,
|
||||
): boolean => {
|
||||
if (!oldSafeProps) return true
|
||||
|
||||
return !isStateSubset(oldSafeProps, newSafeProps)
|
||||
}
|
|
@ -77,14 +77,14 @@ const fetchSafeTokens = (safeAddress: string) => async (
|
|||
return
|
||||
}
|
||||
|
||||
const result = await backOff(() => fetchTokenCurrenciesBalances(safeAddress))
|
||||
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 } = result.data.reduce<ExtractedData>(
|
||||
const { balances, currencyList, ethBalance, tokens } = tokenCurrenciesBalances.reduce<ExtractedData>(
|
||||
extractDataFromResult(currentTokens),
|
||||
{
|
||||
balances: Map(),
|
||||
|
|
|
@ -35,7 +35,7 @@ const httpProviderOptions = {
|
|||
export const web3ReadOnly = new Web3(
|
||||
process.env.NODE_ENV !== 'test'
|
||||
? new Web3.providers.HttpProvider(getRpcServiceUrl(), httpProviderOptions)
|
||||
: window.web3?.currentProvider || 'ws://localhost:8545',
|
||||
: 'ws://localhost:8545',
|
||||
)
|
||||
|
||||
let web3 = web3ReadOnly
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import closeSnackbar from 'src/logic/notifications/store/actions/closeSnackbar'
|
||||
import { WALLET_PROVIDER, getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { fetchProvider } from 'src/logic/wallets/store/actions'
|
||||
import { ADD_PROVIDER } from 'src/logic/wallets/store/actions/addProvider'
|
||||
import { REMOVE_PROVIDER } from 'src/logic/wallets/store/actions/removeProvider'
|
||||
|
@ -29,9 +29,6 @@ const providerWatcherMware = (store) => (next) => async (action) => {
|
|||
clearInterval(watcherInterval)
|
||||
}
|
||||
|
||||
if (currentProviderProps.name.toUpperCase() === WALLET_PROVIDER.METAMASK && (window as any).ethereum) {
|
||||
;(window as any).ethereum.autoRefreshOnNetworkChange = false
|
||||
}
|
||||
saveToStorage(LAST_USED_PROVIDER_KEY, currentProviderProps.name)
|
||||
|
||||
watcherInterval = setInterval(async () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Redirect, Route, Switch, withRouter } from 'react-router-dom'
|
||||
import { Redirect, Route, Switch, useLocation, useRouteMatch } from 'react-router-dom'
|
||||
|
||||
import { LOAD_ADDRESS, OPEN_ADDRESS, SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS } from './routes'
|
||||
|
||||
|
@ -19,8 +19,13 @@ const Load = React.lazy(() => import('./load/container/Load'))
|
|||
|
||||
const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}`
|
||||
|
||||
const Routes = ({ location }) => {
|
||||
const Routes = (): React.ReactElement => {
|
||||
const [isInitialLoad, setInitialLoad] = useState(true)
|
||||
const location = useLocation()
|
||||
const matchSafeWithAction = useRouteMatch<{ safeAddress: string; safeAction: string }>({
|
||||
path: `${SAFELIST_ADDRESS}/:safeAddress/:safeAction`,
|
||||
})
|
||||
|
||||
const defaultSafe = useSelector(defaultSafeSelector)
|
||||
const { trackPage } = useAnalytics()
|
||||
|
||||
|
@ -31,9 +36,18 @@ const Routes = ({ location }) => {
|
|||
}, [location.pathname, isInitialLoad])
|
||||
|
||||
useEffect(() => {
|
||||
const page = location.pathname + location.search
|
||||
trackPage(page)
|
||||
}, [location.pathname, location.search, trackPage])
|
||||
if (matchSafeWithAction) {
|
||||
// prevent logging safeAddress
|
||||
let safePage = `${SAFELIST_ADDRESS}/SAFE_ADDRESS`
|
||||
if (matchSafeWithAction.params?.safeAction) {
|
||||
safePage += `/${matchSafeWithAction.params?.safeAction}`
|
||||
}
|
||||
trackPage(safePage)
|
||||
} else {
|
||||
const page = `${location.pathname}${location.search}`
|
||||
trackPage(page)
|
||||
}
|
||||
}, [location, matchSafeWithAction, trackPage])
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
|
@ -65,4 +79,4 @@ const Routes = ({ location }) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default withRouter(Routes)
|
||||
export default Routes
|
||||
|
|
|
@ -9,7 +9,7 @@ import Row from 'src/components/layout/Row'
|
|||
import { instantiateSafeContracts } from 'src/logic/contracts/safeContracts'
|
||||
import { Review } from 'src/routes/open/components/ReviewInformation'
|
||||
import SafeNameField from 'src/routes/open/components/SafeNameForm'
|
||||
import { SafeOwnersPage } from 'src/routes/open/components/SafeOwnersConfirmationsForm'
|
||||
import { SafeOwnersPage, validateOwnersForm } from 'src/routes/open/components/SafeOwnersConfirmationsForm'
|
||||
import {
|
||||
FIELD_CONFIRMATIONS,
|
||||
FIELD_CREATION_PROXY_SALT,
|
||||
|
@ -133,7 +133,7 @@ export const Layout = (props: LayoutProps): React.ReactElement => {
|
|||
testId="create-safe-form"
|
||||
>
|
||||
<StepperPage component={SafeNameField} />
|
||||
<StepperPage component={SafeOwnersPage} />
|
||||
<StepperPage component={SafeOwnersPage} validate={validateOwnersForm} />
|
||||
<StepperPage network={network} userAccount={userAccount} component={Review} />
|
||||
</Stepper>
|
||||
</Block>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import CheckCircle from '@material-ui/icons/CheckCircle'
|
||||
import * as React from 'react'
|
||||
import { styles } from './style'
|
||||
import { getAddressValidator } from './validators'
|
||||
|
||||
import QRIcon from 'src/assets/icons/qrcode.svg'
|
||||
import trash from 'src/assets/icons/trash.svg'
|
||||
|
@ -21,6 +20,7 @@ import {
|
|||
noErrorsOn,
|
||||
required,
|
||||
minMaxLength,
|
||||
ADDRESS_REPEATED_ERROR,
|
||||
} from 'src/components/forms/validator'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
|
@ -44,30 +44,70 @@ const { useState } = React
|
|||
|
||||
export const ADD_OWNER_BUTTON = '+ Add another owner'
|
||||
|
||||
export const calculateValuesAfterRemoving = (index, notRemovedOwners, values) => {
|
||||
const initialValues = { ...values }
|
||||
/**
|
||||
* Validates the whole OwnersForm, specially checks for non-repeated addresses
|
||||
*
|
||||
* If finds a repeated address, marks it as invalid
|
||||
* @param {Object<string, string>} values
|
||||
* @return Object<string, string>
|
||||
*/
|
||||
export const validateOwnersForm = (values: Record<string, string>): Record<string, string> => {
|
||||
const { errors } = Object.keys(values).reduce(
|
||||
(result, key) => {
|
||||
if (/owner\d+Address/.test(key)) {
|
||||
const address = values[key].toLowerCase()
|
||||
|
||||
const numOwnersAfterRemoving = notRemovedOwners - 1
|
||||
if (result.addresses.includes(address)) {
|
||||
result.errors[key] = ADDRESS_REPEATED_ERROR
|
||||
}
|
||||
|
||||
for (let i = index; i < numOwnersAfterRemoving; i += 1) {
|
||||
initialValues[getOwnerNameBy(i)] = values[getOwnerNameBy(i + 1)]
|
||||
initialValues[getOwnerAddressBy(i)] = values[getOwnerAddressBy(i + 1)]
|
||||
}
|
||||
|
||||
if (+values[FIELD_CONFIRMATIONS] === notRemovedOwners) {
|
||||
initialValues[FIELD_CONFIRMATIONS] = numOwnersAfterRemoving.toString()
|
||||
}
|
||||
|
||||
delete initialValues[getOwnerNameBy(index)]
|
||||
delete initialValues[getOwnerAddressBy(index)]
|
||||
|
||||
return initialValues
|
||||
result.addresses.push(address)
|
||||
}
|
||||
return result
|
||||
},
|
||||
{ addresses: [] as string[], errors: {} },
|
||||
)
|
||||
return errors
|
||||
}
|
||||
|
||||
export const calculateValuesAfterRemoving = (index: number, values: Record<string, string>): Record<string, string> =>
|
||||
Object.keys(values)
|
||||
.sort()
|
||||
.reduce((newValues, key) => {
|
||||
const ownerRelatedField = /owner(\d+)(Name|Address)/
|
||||
|
||||
if (!ownerRelatedField.test(key)) {
|
||||
// no owner-related field
|
||||
newValues[key] = values[key]
|
||||
return newValues
|
||||
}
|
||||
|
||||
const ownerToRemove = new RegExp(`owner${index}(Name|Address)`)
|
||||
|
||||
if (ownerToRemove.test(key)) {
|
||||
// skip, doing anything with the removed field
|
||||
return newValues
|
||||
}
|
||||
|
||||
// we only have the owner-related fields to work with
|
||||
// we must reduce the index value for those owners that come after the deleted owner row
|
||||
const [, ownerOrder, ownerField] = key.match(ownerRelatedField) as RegExpMatchArray
|
||||
|
||||
if (Number(ownerOrder) > index) {
|
||||
// reduce by one the order of the owner
|
||||
newValues[`owner${Number(ownerOrder) - 1}${ownerField}`] = values[key]
|
||||
} else {
|
||||
// previous owners to the deleted row
|
||||
newValues[key] = values[key]
|
||||
}
|
||||
|
||||
return newValues
|
||||
}, {} as Record<string, string>)
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const SafeOwnersForm = (props): React.ReactElement => {
|
||||
const { errors, form, otherAccounts, values } = props
|
||||
const { errors, form, values } = props
|
||||
const classes = useStyles()
|
||||
|
||||
const validOwners = getNumOwnersFrom(values)
|
||||
|
@ -87,7 +127,7 @@ const SafeOwnersForm = (props): React.ReactElement => {
|
|||
}
|
||||
|
||||
const onRemoveRow = (index) => () => {
|
||||
const initialValues = calculateValuesAfterRemoving(index, numOwners, values)
|
||||
const initialValues = calculateValuesAfterRemoving(index, values)
|
||||
form.reset(initialValues)
|
||||
|
||||
setNumOwners(numOwners - 1)
|
||||
|
@ -171,7 +211,6 @@ const SafeOwnersForm = (props): React.ReactElement => {
|
|||
name={addressName}
|
||||
placeholder="Owner Address*"
|
||||
text="Owner Address"
|
||||
validators={[getAddressValidator(otherAccounts, index)]}
|
||||
testId={`create-safe-address-field-${index}`}
|
||||
/>
|
||||
</Col>
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { calculateValuesAfterRemoving } from 'src/routes/open/components/SafeOwnersConfirmationsForm'
|
||||
|
||||
describe('calculateValuesAfterRemoving', () => {
|
||||
it(`should properly remove the last owner row`, () => {
|
||||
// Given
|
||||
const formContent = {
|
||||
name: 'My Safe',
|
||||
owner0Name: 'Owner 0',
|
||||
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
||||
owner1Name: 'Owner 1',
|
||||
owner1Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0',
|
||||
}
|
||||
|
||||
// When
|
||||
const newFormContent = calculateValuesAfterRemoving(1, formContent)
|
||||
|
||||
// Then
|
||||
expect(newFormContent).toStrictEqual({
|
||||
name: 'My Safe',
|
||||
owner0Name: 'Owner 0',
|
||||
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
||||
})
|
||||
})
|
||||
|
||||
it(`should properly remove an owner and recalculate fields indices`, () => {
|
||||
// Given
|
||||
const formContent = {
|
||||
name: 'My Safe',
|
||||
owner0Name: 'Owner 0',
|
||||
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
||||
owner1Name: 'Owner 1',
|
||||
owner1Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0',
|
||||
owner2Name: 'Owner 2',
|
||||
owner2Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b',
|
||||
}
|
||||
|
||||
// When
|
||||
const newFormContent = calculateValuesAfterRemoving(1, formContent)
|
||||
|
||||
// Then
|
||||
expect(newFormContent).toStrictEqual({
|
||||
name: 'My Safe',
|
||||
owner0Name: 'Owner 0',
|
||||
owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
||||
owner1Name: 'Owner 2',
|
||||
owner1Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b',
|
||||
})
|
||||
})
|
||||
})
|
|
@ -150,7 +150,6 @@ const Open = (): React.ReactElement => {
|
|||
ReactGA.event({
|
||||
category: 'User',
|
||||
action: 'Created a safe',
|
||||
value: safeAddress,
|
||||
})
|
||||
|
||||
removeFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
|
||||
|
|
|
@ -16,10 +16,10 @@ import { background, connected } from 'src/theme/variables'
|
|||
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
const loaderDotsSvg = require('./assets/loader-dots.svg')
|
||||
const successSvg = require('./assets/success.svg')
|
||||
const vaultErrorSvg = require('./assets/vault-error.svg')
|
||||
const vaultSvg = require('./assets/vault.svg')
|
||||
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'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: grid;
|
||||
|
@ -140,14 +140,14 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submitte
|
|||
|
||||
const getImage = () => {
|
||||
if (error) {
|
||||
return vaultErrorSvg
|
||||
return VaultErrorSvg
|
||||
}
|
||||
|
||||
if (stepIndex <= 4) {
|
||||
return vaultSvg
|
||||
return VaultSvg
|
||||
}
|
||||
|
||||
return successSvg
|
||||
return SuccessSvg
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -323,7 +323,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submitte
|
|||
<CardTitle>{steps[stepIndex].description || steps[stepIndex].label}</CardTitle>
|
||||
</BodyDescription>
|
||||
|
||||
<BodyLoader>{!error && stepIndex <= 4 && <Img alt="Loader dots" src={loaderDotsSvg} />}</BodyLoader>
|
||||
<BodyLoader>{!error && stepIndex <= 4 && <Img alt="Loader dots" src={LoaderDotsSvg} />}</BodyLoader>
|
||||
|
||||
<BodyInstruction>
|
||||
<FullParagraph color="primary" inverseColors={confirmationStep} noMargin size="md">
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React from 'react'
|
||||
import React, { ReactElement } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { styles } from './style'
|
||||
import { useStyles } from './style'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||
|
@ -20,55 +19,69 @@ import Hairline from 'src/components/layout/Hairline'
|
|||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { addressBookAddressesListSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
|
||||
import { Entry } from 'src/routes/safe/components/AddressBook/index'
|
||||
|
||||
export const CREATE_ENTRY_INPUT_NAME_ID = 'create-entry-input-name'
|
||||
export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address'
|
||||
export const SAVE_NEW_ENTRY_BTN_ID = 'save-new-entry-btn-id'
|
||||
|
||||
const CreateEditEntryModalComponent = ({
|
||||
classes,
|
||||
const formMutators = {
|
||||
setOwnerAddress: (args, state, utils) => {
|
||||
utils.changeValue(state, 'address', () => args[0])
|
||||
},
|
||||
}
|
||||
|
||||
type CreateEditEntryModalProps = {
|
||||
editEntryModalHandler: (entry: AddressBookEntry) => void
|
||||
entryToEdit: Entry
|
||||
isOpen: boolean
|
||||
newEntryModalHandler: (entry: AddressBookEntry) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const CreateEditEntryModal = ({
|
||||
editEntryModalHandler,
|
||||
entryToEdit,
|
||||
isOpen,
|
||||
newEntryModalHandler,
|
||||
onClose,
|
||||
}) => {
|
||||
const onFormSubmitted = (values) => {
|
||||
if (entryToEdit && !entryToEdit.entry.isNew) {
|
||||
editEntryModalHandler(values)
|
||||
} else {
|
||||
}: CreateEditEntryModalProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const { isNew, ...initialValues } = entryToEdit.entry
|
||||
|
||||
const onFormSubmitted = (values: AddressBookEntry) => {
|
||||
if (isNew) {
|
||||
newEntryModalHandler(values)
|
||||
} else {
|
||||
editEntryModalHandler(values)
|
||||
}
|
||||
}
|
||||
|
||||
const addressBookAddressesList = useSelector(addressBookAddressesListSelector)
|
||||
const entryDoesntExist = uniqueAddress(addressBookAddressesList)
|
||||
|
||||
const formMutators = {
|
||||
setOwnerAddress: (args, state, utils) => {
|
||||
utils.changeValue(state, 'address', () => args[0])
|
||||
},
|
||||
}
|
||||
const storedAddresses = useSelector(addressBookAddressesListSelector)
|
||||
const isUniqueAddress = uniqueAddress(storedAddresses)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
description={entryToEdit ? 'Edit addressBook entry' : 'Create new addressBook entry'}
|
||||
description={isNew ? 'Create new addressBook entry' : 'Edit addressBook entry'}
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.smallerModalWindow}
|
||||
title={entryToEdit ? 'Edit entry' : 'Create new entry'}
|
||||
title={isNew ? 'Create new entry' : 'Edit entry'}
|
||||
>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
{entryToEdit ? 'Edit entry' : 'Create entry'}
|
||||
{isNew ? 'Create entry' : 'Edit entry'}
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.close} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted}>
|
||||
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted} initialValues={initialValues}>
|
||||
{(...args) => {
|
||||
const formState = args[2]
|
||||
const mutators = args[3]
|
||||
const handleScan = (value, closeQrModal) => {
|
||||
let scannedAddress = value
|
||||
|
@ -86,13 +99,11 @@ const CreateEditEntryModalComponent = ({
|
|||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<Field
|
||||
className={classes.addressInput}
|
||||
component={TextField}
|
||||
defaultValue={entryToEdit ? entryToEdit.entry.name : undefined}
|
||||
name="name"
|
||||
placeholder={entryToEdit ? 'Entry name' : 'New entry'}
|
||||
placeholder="Name"
|
||||
testId={CREATE_ENTRY_INPUT_NAME_ID}
|
||||
text={entryToEdit ? 'Entry*' : 'New entry*'}
|
||||
text="Name"
|
||||
type="text"
|
||||
validate={composeValidators(required, minMaxLength(1, 50))}
|
||||
/>
|
||||
|
@ -101,18 +112,16 @@ const CreateEditEntryModalComponent = ({
|
|||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<AddressInput
|
||||
className={classes.addressInput}
|
||||
defaultValue={entryToEdit ? entryToEdit.entry.address : undefined}
|
||||
disabled={!!entryToEdit}
|
||||
disabled={!isNew}
|
||||
fieldMutator={mutators.setOwnerAddress}
|
||||
name="address"
|
||||
placeholder="Owner address*"
|
||||
placeholder="Address*"
|
||||
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
|
||||
text="Owner address*"
|
||||
validators={entryToEdit ? undefined : [entryDoesntExist]}
|
||||
text="Address*"
|
||||
validators={[(value?: string) => (isNew ? isUniqueAddress(value) : undefined)]}
|
||||
/>
|
||||
</Col>
|
||||
{!entryToEdit ? (
|
||||
{isNew ? (
|
||||
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||
<ScanQRWrapper handleScan={handleScan} />
|
||||
</Col>
|
||||
|
@ -131,8 +140,9 @@ const CreateEditEntryModalComponent = ({
|
|||
testId={SAVE_NEW_ENTRY_BTN_ID}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={!formState.valid}
|
||||
>
|
||||
{entryToEdit ? 'Save' : 'Create'}
|
||||
{isNew ? 'Create' : 'Save'}
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
|
@ -142,5 +152,3 @@ const CreateEditEntryModalComponent = ({
|
|||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(CreateEditEntryModalComponent)
|
||||
|
|
|
@ -1,26 +1,30 @@
|
|||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
|
||||
import { lg, md } from 'src/theme/variables'
|
||||
|
||||
export const styles = () => ({
|
||||
heading: {
|
||||
padding: lg,
|
||||
justifyContent: 'space-between',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
manage: {
|
||||
fontSize: lg,
|
||||
},
|
||||
container: {
|
||||
padding: `${md} ${lg}`,
|
||||
},
|
||||
close: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
smallerModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
export const useStyles = makeStyles(
|
||||
createStyles({
|
||||
heading: {
|
||||
padding: lg,
|
||||
justifyContent: 'space-between',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
manage: {
|
||||
fontSize: lg,
|
||||
},
|
||||
container: {
|
||||
padding: `${md} ${lg}`,
|
||||
},
|
||||
close: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
smallerModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ import TableContainer from '@material-ui/core/TableContainer'
|
|||
import TableRow from '@material-ui/core/TableRow'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import cn from 'classnames'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { styles } from './style'
|
||||
|
@ -21,8 +21,8 @@ import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddr
|
|||
import { removeAddressBookEntry } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
|
||||
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
|
||||
import { isUserAnOwnerOfAnySafe } from 'src/logic/wallets/ethAddresses'
|
||||
import CreateEditEntryModal from 'src/routes/safe/components/AddressBook/CreateEditEntryModal'
|
||||
import { isUserAnOwnerOfAnySafe, sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { CreateEditEntryModal } from 'src/routes/safe/components/AddressBook/CreateEditEntryModal'
|
||||
import DeleteEntryModal from 'src/routes/safe/components/AddressBook/DeleteEntryModal'
|
||||
import {
|
||||
AB_ADDRESS_ID,
|
||||
|
@ -47,20 +47,24 @@ interface AddressBookSelectedEntry extends AddressBookEntry {
|
|||
isNew?: boolean
|
||||
}
|
||||
|
||||
const AddressBookTable = (): React.ReactElement => {
|
||||
export type Entry = {
|
||||
entry: AddressBookSelectedEntry
|
||||
index?: number
|
||||
isOwnerAddress?: boolean
|
||||
}
|
||||
|
||||
const initialEntryState: Entry = { entry: { address: '', name: '', isNew: true } }
|
||||
|
||||
const AddressBookTable = (): ReactElement => {
|
||||
const classes = useStyles()
|
||||
const columns = generateColumns()
|
||||
const autoColumns = columns.filter((c) => !c.custom)
|
||||
const autoColumns = columns.filter(({ custom }) => !custom)
|
||||
const dispatch = useDispatch()
|
||||
const safesList = useSelector(safesListSelector)
|
||||
const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector)
|
||||
const addressBook = useSelector(addressBookSelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
const [selectedEntry, setSelectedEntry] = useState<{
|
||||
entry?: AddressBookSelectedEntry
|
||||
index?: number
|
||||
isOwnerAddress?: boolean
|
||||
} | null>(null)
|
||||
const [selectedEntry, setSelectedEntry] = useState<Entry>(initialEntryState)
|
||||
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
|
||||
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
|
||||
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
|
||||
|
@ -78,8 +82,9 @@ const AddressBookTable = (): React.ReactElement => {
|
|||
|
||||
useEffect(() => {
|
||||
if (entryAddressToEditOrCreateNew) {
|
||||
const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew)
|
||||
const oldEntryIndex = addressBook.findIndex((entry) => entry.address === checksumEntryAdd)
|
||||
const address = checksumAddress(entryAddressToEditOrCreateNew)
|
||||
const oldEntryIndex = addressBook.findIndex((entry) => sameAddress(entry.address, address))
|
||||
|
||||
if (oldEntryIndex >= 0) {
|
||||
// Edit old entry
|
||||
setSelectedEntry({ entry: addressBook[oldEntryIndex], index: oldEntryIndex })
|
||||
|
@ -88,7 +93,7 @@ const AddressBookTable = (): React.ReactElement => {
|
|||
setSelectedEntry({
|
||||
entry: {
|
||||
name: '',
|
||||
address: checksumEntryAdd,
|
||||
address,
|
||||
isNew: true,
|
||||
},
|
||||
})
|
||||
|
@ -96,7 +101,7 @@ const AddressBookTable = (): React.ReactElement => {
|
|||
}
|
||||
}, [addressBook, entryAddressToEditOrCreateNew])
|
||||
|
||||
const newEntryModalHandler = (entry) => {
|
||||
const newEntryModalHandler = (entry: AddressBookEntry) => {
|
||||
setEditCreateEntryModalOpen(false)
|
||||
const checksumEntries = {
|
||||
...entry,
|
||||
|
@ -105,8 +110,8 @@ const AddressBookTable = (): React.ReactElement => {
|
|||
dispatch(addAddressBookEntry(makeAddressBookEntry(checksumEntries)))
|
||||
}
|
||||
|
||||
const editEntryModalHandler = (entry) => {
|
||||
setSelectedEntry(null)
|
||||
const editEntryModalHandler = (entry: AddressBookEntry) => {
|
||||
setSelectedEntry(initialEntryState)
|
||||
setEditCreateEntryModalOpen(false)
|
||||
const checksumEntries = {
|
||||
...entry,
|
||||
|
@ -116,8 +121,8 @@ const AddressBookTable = (): React.ReactElement => {
|
|||
}
|
||||
|
||||
const deleteEntryModalHandler = () => {
|
||||
const entryAddress = selectedEntry && selectedEntry.entry ? checksumAddress(selectedEntry.entry.address) : ''
|
||||
setSelectedEntry(null)
|
||||
const entryAddress = selectedEntry?.entry ? checksumAddress(selectedEntry.entry.address) : ''
|
||||
setSelectedEntry(initialEntryState)
|
||||
setDeleteEntryModalOpen(false)
|
||||
dispatch(removeAddressBookEntry(entryAddress))
|
||||
}
|
||||
|
@ -128,8 +133,8 @@ const AddressBookTable = (): React.ReactElement => {
|
|||
<Col end="sm" xs={12}>
|
||||
<ButtonLink
|
||||
onClick={() => {
|
||||
setSelectedEntry(null)
|
||||
setEditCreateEntryModalOpen(!editCreateEntryModalOpen)
|
||||
setSelectedEntry(initialEntryState)
|
||||
setEditCreateEntryModalOpen(true)
|
||||
}}
|
||||
size="lg"
|
||||
testId="manage-tokens-btn"
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import { MutableRefObject, useEffect, useState } from 'react'
|
||||
import {
|
||||
getSDKVersion,
|
||||
SDKMessageEvent,
|
||||
MethodToResponse,
|
||||
Methods,
|
||||
ErrorResponse,
|
||||
MessageFormatter,
|
||||
METHODS,
|
||||
} from '@gnosis.pm/safe-apps-sdk'
|
||||
import { SafeApp } from './types.d'
|
||||
|
||||
type MessageHandler = (
|
||||
msg: SDKMessageEvent,
|
||||
) => void | MethodToResponse[Methods] | ErrorResponse | Promise<MethodToResponse[Methods] | ErrorResponse | void>
|
||||
|
||||
class AppCommunicator {
|
||||
private iframe: HTMLIFrameElement
|
||||
private handlers = new Map<Methods, MessageHandler>()
|
||||
private app: SafeApp
|
||||
|
||||
constructor(iframeRef: MutableRefObject<HTMLIFrameElement>, app: SafeApp) {
|
||||
this.iframe = iframeRef.current
|
||||
this.app = app
|
||||
|
||||
window.addEventListener('message', this.handleIncomingMessage)
|
||||
}
|
||||
|
||||
on = (method: Methods, handler: MessageHandler): void => {
|
||||
this.handlers.set(method, handler)
|
||||
}
|
||||
|
||||
private isValidMessage = (msg: SDKMessageEvent): boolean => {
|
||||
// @ts-expect-error .parent doesn't exist on some possible types
|
||||
const sentFromIframe = msg.source.parent === window.parent
|
||||
const knownOrigin = this.app.url.includes(msg.origin)
|
||||
const knownMethod = Object.values(METHODS).includes(msg.data.method)
|
||||
|
||||
return knownOrigin && sentFromIframe && knownMethod
|
||||
}
|
||||
|
||||
private canHandleMessage = (msg: SDKMessageEvent): boolean => {
|
||||
return Boolean(this.handlers.get(msg.data.method))
|
||||
}
|
||||
|
||||
send = (data, requestId, error = false): void => {
|
||||
const sdkVersion = getSDKVersion()
|
||||
const msg = error
|
||||
? MessageFormatter.makeErrorResponse(requestId, data, sdkVersion)
|
||||
: MessageFormatter.makeResponse(requestId, data, sdkVersion)
|
||||
|
||||
this.iframe.contentWindow?.postMessage(msg, this.app.url)
|
||||
}
|
||||
|
||||
handleIncomingMessage = async (msg: SDKMessageEvent): Promise<void> => {
|
||||
const validMessage = this.isValidMessage(msg)
|
||||
const hasHandler = this.canHandleMessage(msg)
|
||||
|
||||
if (validMessage && hasHandler) {
|
||||
const handler = this.handlers.get(msg.data.method)
|
||||
try {
|
||||
// @ts-expect-error Handler existence is checked in this.canHandleMessage
|
||||
const response = await handler(msg)
|
||||
|
||||
// If response is not returned, it means the response will be send somewhere else
|
||||
if (typeof response !== 'undefined') {
|
||||
this.send(response, msg.data.id)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log({ err })
|
||||
this.send(err.message, msg.data.id, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear = (): void => {
|
||||
window.removeEventListener('message', this.handleIncomingMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const useAppCommunicator = (
|
||||
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
|
||||
app?: SafeApp,
|
||||
): AppCommunicator | undefined => {
|
||||
const [communicator, setCommunicator] = useState<AppCommunicator | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
let communicatorInstance
|
||||
const initCommunicator = (iframeRef: MutableRefObject<HTMLIFrameElement>, app: SafeApp) => {
|
||||
communicatorInstance = new AppCommunicator(iframeRef, app)
|
||||
setCommunicator(communicatorInstance)
|
||||
}
|
||||
|
||||
if (app && iframeRef.current !== null) {
|
||||
initCommunicator(iframeRef as MutableRefObject<HTMLIFrameElement>, app)
|
||||
}
|
||||
|
||||
return () => {
|
||||
communicatorInstance?.clear()
|
||||
}
|
||||
}, [app, iframeRef])
|
||||
|
||||
return communicator
|
||||
}
|
||||
|
||||
export { useAppCommunicator }
|
|
@ -11,15 +11,10 @@ import {
|
|||
Menu,
|
||||
ButtonLink,
|
||||
} from '@gnosis.pm/safe-react-components'
|
||||
import { MethodToResponse, RPCPayload } from '@gnosis.pm/safe-apps-sdk'
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom'
|
||||
import { useSelector } from 'react-redux'
|
||||
import {
|
||||
INTERFACE_MESSAGES,
|
||||
Transaction,
|
||||
RequestId,
|
||||
LowercaseNetworks,
|
||||
SendTransactionParams,
|
||||
} from '@gnosis.pm/safe-apps-sdk'
|
||||
import { INTERFACE_MESSAGES, Transaction, RequestId, LowercaseNetworks } from '@gnosis.pm/safe-apps-sdk-v1'
|
||||
|
||||
import {
|
||||
safeEthBalanceSelector,
|
||||
|
@ -27,12 +22,15 @@ import {
|
|||
safeNameSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { getNetworkName } from 'src/config'
|
||||
import { getNetworkName, getTxServiceUrl } from 'src/config'
|
||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||
import { isSameURL } from 'src/utils/url'
|
||||
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { staticAppsList } from 'src/routes/safe/components/Apps/utils'
|
||||
import { LoadingContainer } from 'src/components/LoaderContainer/index'
|
||||
import { TIMEOUT } from 'src/utils/constants'
|
||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
import ConfirmTransactionModal from '../components/ConfirmTransactionModal'
|
||||
import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler'
|
||||
|
@ -40,7 +38,7 @@ import { useLegalConsent } from '../hooks/useLegalConsent'
|
|||
import LegalDisclaimer from './LegalDisclaimer'
|
||||
import { APPS_STORAGE_KEY, getAppInfoFromUrl } from '../utils'
|
||||
import { SafeApp, StoredSafeApp } from '../types.d'
|
||||
import { LoadingContainer } from 'src/components/LoaderContainer'
|
||||
import { useAppCommunicator } from '../communicator'
|
||||
|
||||
const OwnerDisclaimer = styled.div`
|
||||
display: flex;
|
||||
|
@ -71,11 +69,15 @@ const Breadcrumb = styled.div`
|
|||
height: 51px;
|
||||
`
|
||||
|
||||
export type TransactionParams = {
|
||||
safeTxGas?: number
|
||||
}
|
||||
|
||||
type ConfirmTransactionModalState = {
|
||||
isOpen: boolean
|
||||
txs: Transaction[]
|
||||
requestId?: RequestId
|
||||
params?: SendTransactionParams
|
||||
params?: TransactionParams
|
||||
}
|
||||
|
||||
type Props = {
|
||||
|
@ -112,9 +114,26 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
|
|||
const [isAppDeletable, setIsAppDeletable] = useState<boolean | undefined>()
|
||||
|
||||
const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`)
|
||||
const timer = useRef<number>()
|
||||
const [appTimeout, setAppTimeout] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (appIsLoading) {
|
||||
timer.current = setTimeout(() => {
|
||||
setAppTimeout(true)
|
||||
}, TIMEOUT)
|
||||
} else {
|
||||
clearTimeout(timer.current)
|
||||
setAppTimeout(false)
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer.current)
|
||||
}
|
||||
}, [appIsLoading])
|
||||
|
||||
const openConfirmationModal = useCallback(
|
||||
(txs: Transaction[], params: SendTransactionParams | undefined, requestId: RequestId) =>
|
||||
(txs: Transaction[], params: TransactionParams | undefined, requestId: RequestId) =>
|
||||
setConfirmTransactionModal({
|
||||
isOpen: true,
|
||||
txs,
|
||||
|
@ -151,18 +170,78 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
|
|||
})
|
||||
}, [ethBalance, safeAddress, appUrl, sendMessageToIframe])
|
||||
|
||||
const communicator = useAppCommunicator(iframeRef, safeApp)
|
||||
|
||||
useEffect(() => {
|
||||
communicator?.on('getEnvInfo', () => ({
|
||||
txServiceUrl: getTxServiceUrl(),
|
||||
}))
|
||||
|
||||
communicator?.on('getSafeInfo', () => ({
|
||||
safeAddress,
|
||||
network: NETWORK_NAME,
|
||||
}))
|
||||
|
||||
communicator?.on('rpcCall', async (msg) => {
|
||||
const params = msg.data.params as RPCPayload
|
||||
|
||||
try {
|
||||
const response = new Promise<MethodToResponse['rpcCall']>((resolve, reject) => {
|
||||
if (
|
||||
web3ReadOnly.currentProvider !== null &&
|
||||
typeof web3ReadOnly.currentProvider !== 'string' &&
|
||||
'send' in web3ReadOnly.currentProvider
|
||||
) {
|
||||
web3ReadOnly.currentProvider?.send?.(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
method: params.call,
|
||||
params: params.params,
|
||||
id: '1',
|
||||
},
|
||||
(err, res) => {
|
||||
if (err || res?.error) {
|
||||
reject(err || res?.error)
|
||||
}
|
||||
|
||||
resolve(res?.result)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (err) {
|
||||
return err
|
||||
}
|
||||
})
|
||||
|
||||
communicator?.on('sendTransactions', (msg) => {
|
||||
// @ts-expect-error explore ways to fix this
|
||||
openConfirmationModal(msg.data.params.txs as Transaction[], msg.data.params.params, msg.data.id)
|
||||
})
|
||||
}, [communicator, openConfirmationModal, safeAddress])
|
||||
|
||||
const onUserTxConfirm = (safeTxHash: string) => {
|
||||
// Safe Apps SDK V1 Handler
|
||||
sendMessageToIframe(
|
||||
{ messageId: INTERFACE_MESSAGES.TRANSACTION_CONFIRMED, data: { safeTxHash } },
|
||||
confirmTransactionModal.requestId,
|
||||
)
|
||||
|
||||
// Safe Apps SDK V2 Handler
|
||||
communicator?.send({ safeTxHash }, confirmTransactionModal.requestId)
|
||||
}
|
||||
|
||||
const onTxReject = () => {
|
||||
// Safe Apps SDK V1 Handler
|
||||
sendMessageToIframe(
|
||||
{ messageId: INTERFACE_MESSAGES.TRANSACTION_REJECTED, data: {} },
|
||||
confirmTransactionModal.requestId,
|
||||
)
|
||||
|
||||
// Safe Apps SDK V2 Handler
|
||||
communicator?.send('Transaction was rejected', confirmTransactionModal.requestId, true)
|
||||
}
|
||||
|
||||
const openRemoveModal = () => setIsRemoveModalOpen(true)
|
||||
|
@ -235,7 +314,12 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => {
|
|||
|
||||
<StyledCard>
|
||||
{appIsLoading && (
|
||||
<LoadingContainer>
|
||||
<LoadingContainer style={{ flexDirection: 'column' }}>
|
||||
{appTimeout && (
|
||||
<Title size="xs">
|
||||
The safe-app is taking longer than usual to load. There might be a problem with the safe-app provider.
|
||||
</Title>
|
||||
)}
|
||||
<Loader size="md" />
|
||||
</LoadingContainer>
|
||||
)}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { Icon, Text, Title, GenericModal, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components'
|
||||
import { Transaction, SendTransactionParams } from '@gnosis.pm/safe-apps-sdk'
|
||||
import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1'
|
||||
import styled from 'styled-components'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
|
@ -24,6 +24,7 @@ import { estimateSafeTxGas } from 'src/logic/safe/transactions/gas'
|
|||
|
||||
import GasEstimationInfo from './GasEstimationInfo'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
import { TransactionParams } from './AppFrame'
|
||||
|
||||
const isTxValid = (t: Transaction): boolean => {
|
||||
if (!['string', 'number'].includes(typeof t.value)) {
|
||||
|
@ -70,7 +71,7 @@ type OwnProps = {
|
|||
isOpen: boolean
|
||||
app: SafeApp
|
||||
txs: Transaction[]
|
||||
params?: SendTransactionParams
|
||||
params?: TransactionParams
|
||||
safeAddress: string
|
||||
safeName: string
|
||||
ethBalance: string
|
||||
|
|
|
@ -9,8 +9,7 @@ import {
|
|||
RequestId,
|
||||
Transaction,
|
||||
LowercaseNetworks,
|
||||
SendTransactionParams,
|
||||
} from '@gnosis.pm/safe-apps-sdk'
|
||||
} from '@gnosis.pm/safe-apps-sdk-v1'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useEffect, useCallback, MutableRefObject } from 'react'
|
||||
import { getNetworkName, getTxServiceUrl } from 'src/config/'
|
||||
|
@ -19,7 +18,7 @@ import {
|
|||
safeNameSelector,
|
||||
safeParamAddressFromStateSelector,
|
||||
} from 'src/logic/safe/store/selectors'
|
||||
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
|
||||
import { TransactionParams } from '../components/AppFrame'
|
||||
import { SafeApp } from 'src/routes/safe/components/Apps/types.d'
|
||||
|
||||
type InterfaceMessageProps<T extends InterfaceMessageIds> = {
|
||||
|
@ -31,19 +30,11 @@ type ReturnType = {
|
|||
sendMessageToIframe: <T extends InterfaceMessageIds>(message: InterfaceMessageProps<T>, requestId?: RequestId) => void
|
||||
}
|
||||
|
||||
interface CustomMessageEvent extends MessageEvent {
|
||||
data: {
|
||||
requestId: RequestId
|
||||
messageId: SDKMessageIds
|
||||
data: SDKMessageToPayload[SDKMessageIds]
|
||||
}
|
||||
}
|
||||
|
||||
const NETWORK_NAME = getNetworkName()
|
||||
|
||||
const useIframeMessageHandler = (
|
||||
selectedApp: SafeApp | undefined,
|
||||
openConfirmationModal: (txs: Transaction[], params: SendTransactionParams | undefined, requestId: RequestId) => void,
|
||||
openConfirmationModal: (txs: Transaction[], params: TransactionParams | undefined, requestId: RequestId) => void,
|
||||
closeModal: () => void,
|
||||
iframeRef: MutableRefObject<HTMLIFrameElement | null>,
|
||||
): ReturnType => {
|
||||
|
@ -58,6 +49,7 @@ const useIframeMessageHandler = (
|
|||
const requestWithMessage = {
|
||||
...message,
|
||||
requestId: requestId || Math.trunc(window.performance.now()),
|
||||
version: '0.4.2',
|
||||
}
|
||||
|
||||
if (iframeRef && selectedApp) {
|
||||
|
@ -93,44 +85,6 @@ const useIframeMessageHandler = (
|
|||
break
|
||||
}
|
||||
|
||||
case SDK_MESSAGES.SEND_TRANSACTIONS_V2: {
|
||||
const payload = messagePayload as SDKMessageToPayload[typeof SDK_MESSAGES.SEND_TRANSACTIONS_V2]
|
||||
if (payload) {
|
||||
openConfirmationModal(payload.txs, payload.params, requestId)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case SDK_MESSAGES.RPC_CALL: {
|
||||
const payload = messagePayload as SDKMessageToPayload['RPC_CALL']
|
||||
|
||||
if (
|
||||
web3ReadOnly.currentProvider !== null &&
|
||||
typeof web3ReadOnly.currentProvider !== 'string' &&
|
||||
'send' in web3ReadOnly.currentProvider
|
||||
) {
|
||||
web3ReadOnly.currentProvider?.send?.(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
method: payload?.call,
|
||||
params: payload?.params,
|
||||
id: '1',
|
||||
},
|
||||
(err, res) => {
|
||||
if (!err) {
|
||||
const rpcCallMsg = {
|
||||
messageId: INTERFACE_MESSAGES.RPC_CALL_RESPONSE,
|
||||
data: res,
|
||||
}
|
||||
|
||||
sendMessageToIframe(rpcCallMsg, requestId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case SDK_MESSAGES.SAFE_APP_SDK_INITIALIZED: {
|
||||
const safeInfoMessage = {
|
||||
messageId: INTERFACE_MESSAGES.ON_SAFE_INFO,
|
||||
|
@ -157,7 +111,13 @@ const useIframeMessageHandler = (
|
|||
}
|
||||
}
|
||||
}
|
||||
const onIframeMessage = async (message: CustomMessageEvent) => {
|
||||
const onIframeMessage = async (
|
||||
message: MessageEvent<{
|
||||
requestId: RequestId
|
||||
messageId: SDKMessageIds
|
||||
data: SDKMessageToPayload[SDKMessageIds]
|
||||
}>,
|
||||
) => {
|
||||
if (message.origin === window.origin) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react'
|
||||
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import AppFrame from './components/AppFrame'
|
||||
import AppsList from './components/AppsList'
|
||||
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
const useQuery = () => {
|
||||
return new URLSearchParams(useLocation().search)
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
|
|||
},
|
||||
// Aave
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmX1NUtvm9WjbvT79sTdeg3sw1NxZAM273y44nBy5d2jZb`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQ3w2ezp2zx3u2LYQHyuNzMrLDJFjyL1rjAFTjNMcQ4cK`,
|
||||
disabled: false,
|
||||
networks: [ETHEREUM_NETWORK.MAINNET],
|
||||
},
|
||||
|
@ -95,7 +95,7 @@ export const staticAppsList: Array<StaticAppInfo> = [
|
|||
},
|
||||
// TX-Builder
|
||||
{
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmXdrr9hRbXSaqMb71iKnEp66PwwsAbJDR9XdwByUYSTxB`,
|
||||
url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmYES1Se6i6679z3PfQ62bydgVVEoSRUabvjB35DfUGPGA`,
|
||||
disabled: false,
|
||||
networks: [
|
||||
ETHEREUM_NETWORK.MAINNET,
|
||||
|
|
|
@ -34,7 +34,7 @@ const CurrencyDropdown = (): React.ReactElement | null => {
|
|||
|
||||
const currenciesList = Object.values(AVAILABLE_CURRENCIES)
|
||||
const tokenImage = nativeCoin.logoUri
|
||||
const classes = useDropdownStyles()
|
||||
const classes = useDropdownStyles({})
|
||||
const currenciesListFiltered = currenciesList.filter((currency) =>
|
||||
currency.toLowerCase().includes(searchParams.toLowerCase()),
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ import SettingsDescription from './SettingsDescription'
|
|||
import CustomDescription from './CustomDescription'
|
||||
import TransferDescription from './TransferDescription'
|
||||
|
||||
import { getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
||||
import { getRawTxAmount, getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import { Transaction, TransactionTypes } from 'src/logic/safe/store/models/types/transaction'
|
||||
|
||||
|
@ -33,8 +33,9 @@ const UpgradeDescriptionTx = ({ tx }: { tx: Transaction }): React.ReactElement =
|
|||
|
||||
const TransferDescriptionTx = ({ tx }: { tx: Transaction }): React.ReactElement => {
|
||||
const amountWithSymbol = getTxAmount(tx, false)
|
||||
const { recipient, isTokenTransfer = false } = getTxData(tx)
|
||||
return <TransferDescription {...{ amountWithSymbol, recipient, isTokenTransfer }} />
|
||||
const rawAmount = getRawTxAmount(tx)
|
||||
const { recipient, isTokenTransfer = false, tokenAddress } = getTxData(tx)
|
||||
return <TransferDescription {...{ amountWithSymbol, recipient, isTokenTransfer, rawAmount, tokenAddress }} />
|
||||
}
|
||||
|
||||
const TxDescription = ({ tx }: { tx: Transaction }): React.ReactElement => {
|
||||
|
|
|
@ -3,7 +3,7 @@ import IconButton from '@material-ui/core/IconButton'
|
|||
import TableCell from '@material-ui/core/TableCell'
|
||||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import ExpandLess from '@material-ui/icons/ExpandLess'
|
||||
import ExpandMore from '@material-ui/icons/ExpandMore'
|
||||
import cn from 'classnames'
|
||||
|
@ -25,7 +25,10 @@ import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
|
|||
|
||||
export const TRANSACTION_ROW_TEST_ID = 'transaction-row'
|
||||
|
||||
const TxsTable = ({ classes }) => {
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const TxsTable = (): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [expandedTx, setExpandedTx] = useState(null)
|
||||
const cancellationTransactions = useSelector(safeCancellationTransactionsSelector)
|
||||
const transactions = useSelector(extendedTransactionsSelector)
|
||||
|
@ -95,10 +98,7 @@ const TxsTable = ({ classes }) => {
|
|||
{autoColumns.map((column) => (
|
||||
<TableCell
|
||||
align={column.align}
|
||||
className={cn(
|
||||
classes.cell,
|
||||
['cancelled', 'failed'].includes(row.status) && classes.cancelledRow,
|
||||
)}
|
||||
className={cn(['cancelled', 'failed'].includes(row.status) && classes.cancelledRow)}
|
||||
component="td"
|
||||
key={column.id}
|
||||
style={cellWidth(column.width)}
|
||||
|
@ -139,4 +139,4 @@ const TxsTable = ({ classes }) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(TxsTable)
|
||||
export default TxsTable
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export const styles = () => ({
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = createStyles({
|
||||
container: {
|
||||
marginTop: '56px',
|
||||
},
|
||||
|
|
|
@ -3,9 +3,7 @@ import React, { useState } from 'react'
|
|||
import { useSelector } from 'react-redux'
|
||||
import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'
|
||||
|
||||
import NoSafe from 'src/components/NoSafe'
|
||||
import { providerNameSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { safeFeaturesEnabledSelector } from 'src/logic/safe/store/selectors'
|
||||
import { wrapInSuspense } from 'src/utils/wrapInSuspense'
|
||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||
import { FEATURES } from 'src/config/networks/network.d'
|
||||
|
@ -35,14 +33,8 @@ const Container = (): React.ReactElement => {
|
|||
onClose: () => {},
|
||||
})
|
||||
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
const provider = useSelector(providerNameSelector)
|
||||
const matchSafeWithAddress = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress` })
|
||||
|
||||
if (!safeAddress) {
|
||||
return <NoSafe provider={provider} text="Safe not found" />
|
||||
}
|
||||
|
||||
if (!featuresEnabled) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
@import "src/theme/variables.scss";
|
||||
|
||||
.safe {
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
margin-top: $xl;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.safeActions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.learnMoreLink {
|
||||
color: $secondary;
|
||||
}
|
||||
|
||||
.connectWallet {
|
||||
text-align: center;
|
||||
}
|
|
@ -1,118 +1,189 @@
|
|||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||
import * as React from 'react'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Title,
|
||||
Text,
|
||||
Divider,
|
||||
ButtonLink,
|
||||
Dot,
|
||||
Icon,
|
||||
Link as LinkSRC,
|
||||
} from '@gnosis.pm/safe-react-components'
|
||||
|
||||
import styles from './Layout.module.scss'
|
||||
|
||||
import ConnectButton from 'src/components/ConnectButton'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import Heading from 'src/components/layout/Heading'
|
||||
import Img from 'src/components/layout/Img'
|
||||
import Link from 'src/components/layout/Link'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import { LOAD_ADDRESS, OPEN_ADDRESS } from 'src/routes/routes'
|
||||
import { marginButtonImg, secondary } from 'src/theme/variables'
|
||||
import { onConnectButtonClick } from 'src/components/ConnectButton'
|
||||
|
||||
const plus = require('../assets/new.svg')
|
||||
const safe = require('../assets/safe.svg')
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 24px 0 0 0;
|
||||
`
|
||||
const StyledCardDouble = styled(Card)`
|
||||
display: flex;
|
||||
padding: 0;
|
||||
`
|
||||
const StyledCard = styled(Card)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin: 0 20px 0 0;
|
||||
max-width: 27%;
|
||||
height: 276px;
|
||||
`
|
||||
const CardsCol = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 24px;
|
||||
width: 50%;
|
||||
`
|
||||
const StyledButton = styled(Button)`
|
||||
margin-top: auto;
|
||||
text-decoration: none;
|
||||
`
|
||||
const TitleWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin: 0 0 16px 0;
|
||||
|
||||
const openIconStyle = {
|
||||
height: '13px',
|
||||
color: secondary,
|
||||
marginBottom: '-2px',
|
||||
h5 {
|
||||
color: white;
|
||||
}
|
||||
`
|
||||
const StyledTitle = styled(Title)`
|
||||
margin: 0 0 0 16px;
|
||||
`
|
||||
const StyledTitleOnly = styled(Title)`
|
||||
margin: 0 0 16px 0;
|
||||
`
|
||||
const StyledButtonLink = styled(ButtonLink)`
|
||||
margin: 16px 0 16px -8px;
|
||||
`
|
||||
|
||||
type Props = {
|
||||
isOldMultisigMigration?: boolean
|
||||
provider: any
|
||||
}
|
||||
|
||||
const buttonStyle = {
|
||||
marginLeft: marginButtonImg,
|
||||
}
|
||||
|
||||
export const CreateSafe = ({ provider, size }: any) => (
|
||||
<Button
|
||||
color="primary"
|
||||
component={Link}
|
||||
disabled={!provider}
|
||||
minHeight={42}
|
||||
minWidth={240}
|
||||
size={size || 'medium'}
|
||||
to={OPEN_ADDRESS}
|
||||
variant="contained"
|
||||
testId="create-new-safe-btn"
|
||||
>
|
||||
<Img alt="Safe" height={14} src={plus} />
|
||||
<div style={buttonStyle}>Create new Safe</div>
|
||||
</Button>
|
||||
)
|
||||
|
||||
export const LoadSafe = ({ provider, size }) => (
|
||||
<Button
|
||||
color="primary"
|
||||
component={Link}
|
||||
disabled={!provider}
|
||||
minWidth={240}
|
||||
size={size || 'medium'}
|
||||
to={LOAD_ADDRESS}
|
||||
variant="outlined"
|
||||
testId="load-existing-safe-btn"
|
||||
>
|
||||
<Img alt="Safe" height={14} src={safe} />
|
||||
<div style={buttonStyle}>Load existing Safe</div>
|
||||
</Button>
|
||||
)
|
||||
|
||||
const Welcome = ({ isOldMultisigMigration, provider }: any) => {
|
||||
const headingText = isOldMultisigMigration ? (
|
||||
<>
|
||||
We will replicate the owner structure from your existing Gnosis MultiSig
|
||||
<br />
|
||||
to let you test the new interface.
|
||||
<br />
|
||||
As soon as you feel comfortable, start moving funds to your new Safe.
|
||||
<br />{' '}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Gnosis Safe Multisig is the most secure way to manage crypto funds
|
||||
<br />
|
||||
collectively. It is an improvement of the Gnosis MultiSig, which is used by more than 3000 teams
|
||||
<br /> and stores over $1B USD worth of digital assets. Gnosis Safe Multisig features a modular
|
||||
<br /> design, formally verified smart contracts and vastly improved user experience.{' '}
|
||||
</>
|
||||
)
|
||||
const Welcome = ({ isOldMultisigMigration, provider }: Props): React.ReactElement => {
|
||||
return (
|
||||
<Block className={styles.safe}>
|
||||
<Heading align="center" margin="lg" tag="h1" weight="bold">
|
||||
Welcome to
|
||||
<br />
|
||||
Gnosis Safe Multisig
|
||||
</Heading>
|
||||
<Heading align="center" margin="xl" tag="h3">
|
||||
{headingText}
|
||||
<a
|
||||
className={styles.learnMoreLink}
|
||||
href="https://gnosis-safe.io/teams"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
<OpenInNew style={openIconStyle} />
|
||||
</a>
|
||||
</Heading>
|
||||
{provider ? (
|
||||
<>
|
||||
<Block className={styles.safeActions} margin="md">
|
||||
<CreateSafe provider={provider} size="large" />
|
||||
</Block>
|
||||
<Block className={styles.safeActions} margin="md">
|
||||
<LoadSafe provider={provider} size="large" />
|
||||
</Block>
|
||||
</>
|
||||
) : (
|
||||
<Block className={styles.connectWallet} margin="md">
|
||||
<Heading align="center" margin="md" tag="h3">
|
||||
Get Started by Connecting a Wallet
|
||||
</Heading>
|
||||
<ConnectButton minHeight={42} minWidth={240} data-testid="connect-btn" />
|
||||
</Block>
|
||||
)}
|
||||
<Block>
|
||||
{/* Title */}
|
||||
<Title size="md" strong>
|
||||
Welcome to Gnosis Safe Multisig.
|
||||
</Title>
|
||||
|
||||
{/* Subtitle */}
|
||||
<Title size="xs">
|
||||
{isOldMultisigMigration ? (
|
||||
<>
|
||||
We will replicate the owner structure from your existing Gnosis MultiSig to let you test the new interface.
|
||||
As soon as you feel comfortable, start moving funds to your new Safe.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Gnosis Safe Multisig is the most trusted platform to manage digital assets. <br /> Here is how to get
|
||||
started:{' '}
|
||||
</>
|
||||
)}
|
||||
</Title>
|
||||
|
||||
<>
|
||||
<Wrapper>
|
||||
{/* Connect wallet */}
|
||||
<StyledCard>
|
||||
<TitleWrapper>
|
||||
<Dot color="primary">
|
||||
{!provider ? <Title size="xs">1</Title> : <Icon color="white" type="check" size="md" />}
|
||||
</Dot>
|
||||
<StyledTitle size="sm" strong withoutMargin>
|
||||
Connect wallet
|
||||
</StyledTitle>
|
||||
</TitleWrapper>
|
||||
<Text size="xl">
|
||||
Gnosis Safe Multisig supports a wide range of wallets that you can choose to be one of the authentication
|
||||
factors.
|
||||
</Text>
|
||||
<StyledButtonLink textSize="xl" color="primary" iconType="externalLink" iconSize="sm">
|
||||
<LinkSRC
|
||||
size="xl"
|
||||
href="https://help.gnosis-safe.io/en/articles/4689442-why-do-i-need-to-connect-a-wallet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="More info about: Why do I need to connect wallet?"
|
||||
>
|
||||
Why do I need to connect wallet?
|
||||
</LinkSRC>
|
||||
</StyledButtonLink>
|
||||
<StyledButton
|
||||
size="lg"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={onConnectButtonClick}
|
||||
disabled={provider}
|
||||
data-testid="connect-btn"
|
||||
>
|
||||
<Text size="xl" color="white">
|
||||
Connect wallet
|
||||
</Text>
|
||||
</StyledButton>
|
||||
</StyledCard>
|
||||
|
||||
<StyledCardDouble disabled={!provider}>
|
||||
{/* Create safe */}
|
||||
<CardsCol>
|
||||
<TitleWrapper>
|
||||
<Dot color="primary">
|
||||
<Title size="xs">2</Title>
|
||||
</Dot>
|
||||
<StyledTitle size="sm" strong withoutMargin>
|
||||
Create Safe
|
||||
</StyledTitle>
|
||||
</TitleWrapper>
|
||||
<Text size="xl">
|
||||
Create a new Safe Multisig that is controlled by one or multiple owners. <br />
|
||||
You will be required to pay a network fee for creating your new Safe.
|
||||
</Text>
|
||||
<StyledButton size="lg" color="primary" variant="contained" component={Link} to={OPEN_ADDRESS}>
|
||||
<Text size="xl" color="white">
|
||||
+ Create new Safe
|
||||
</Text>
|
||||
</StyledButton>
|
||||
</CardsCol>
|
||||
|
||||
<Divider orientation="vertical" />
|
||||
|
||||
{/* Load safe */}
|
||||
<CardsCol>
|
||||
<StyledTitleOnly size="sm" strong withoutMargin>
|
||||
Load existing Safe
|
||||
</StyledTitleOnly>
|
||||
<Text size="xl">
|
||||
Already have a Safe? Do you want to access your Safe Multisig from a different device? Easily load your
|
||||
Safe Multisig using your Safe address.
|
||||
</Text>
|
||||
<StyledButton
|
||||
variant="bordered"
|
||||
iconType="safe"
|
||||
iconSize="sm"
|
||||
size="lg"
|
||||
color="secondary"
|
||||
component={Link}
|
||||
to={LOAD_ADDRESS}
|
||||
>
|
||||
<Text size="xl" color="secondary">
|
||||
Load existing Safe
|
||||
</Text>
|
||||
</StyledButton>
|
||||
</CardsCol>
|
||||
</StyledCardDouble>
|
||||
</Wrapper>
|
||||
</>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
//
|
||||
function useTestAccountAt(index = 0) {
|
||||
(window as any).testAccountIndex = index
|
||||
window.testAccountIndex = index
|
||||
}
|
||||
|
||||
function resetTestAccount() {
|
||||
delete (window as any).testAccountIndex
|
||||
delete window.testAccountIndex
|
||||
}
|
||||
|
||||
export { useTestAccountAt, resetTestAccount }
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import Web3 from 'web3'
|
||||
export {}
|
||||
declare global {
|
||||
interface Window {
|
||||
web3?: Web3
|
||||
testAccountIndex?: string
|
||||
ethereum?: {
|
||||
autoRefreshOnNetworkChange: boolean
|
||||
isMetaMask: boolean
|
||||
}
|
||||
testAccountIndex?: string | number
|
||||
}
|
||||
}
|
||||
declare module '@openzeppelin/contracts/build/contracts/ERC721'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import GoogleAnalytics, { EventArgs } from 'react-ga'
|
||||
import ReactGA, { EventArgs } from 'react-ga'
|
||||
import { getNetworkInfo } from 'src/config'
|
||||
|
||||
import { getGoogleAnalyticsTrackingID } from 'src/config'
|
||||
|
@ -20,8 +20,8 @@ export const loadGoogleAnalytics = (): void => {
|
|||
if (!trackingID) {
|
||||
console.error('[GoogleAnalytics] - In order to use google analytics you need to add an trackingID')
|
||||
} else {
|
||||
GoogleAnalytics.initialize(trackingID)
|
||||
GoogleAnalytics.set({
|
||||
ReactGA.initialize(trackingID)
|
||||
ReactGA.set({
|
||||
anonymizeIp: true,
|
||||
appName: `Gnosis Safe Multisig (${networkInfo.label})`,
|
||||
appId: `io.gnosis.safe.${networkInfo.label.toLowerCase()}`,
|
||||
|
@ -50,22 +50,19 @@ export const useAnalytics = (): UseAnalyticsResponse => {
|
|||
fetchCookiesFromStorage()
|
||||
}, [])
|
||||
|
||||
const trackPage = useCallback(
|
||||
(page) => {
|
||||
if (!analyticsAllowed || !analyticsLoaded) {
|
||||
return
|
||||
}
|
||||
GoogleAnalytics.pageview(page)
|
||||
},
|
||||
[analyticsAllowed],
|
||||
)
|
||||
const trackPage = (page) => {
|
||||
if (!analyticsAllowed || !analyticsLoaded) {
|
||||
return
|
||||
}
|
||||
ReactGA.pageview(page)
|
||||
}
|
||||
|
||||
const trackEvent = useCallback(
|
||||
(event: EventArgs) => {
|
||||
if (!analyticsAllowed || !analyticsLoaded) {
|
||||
return
|
||||
}
|
||||
GoogleAnalytics.event(event)
|
||||
ReactGA.event(event)
|
||||
},
|
||||
[analyticsAllowed],
|
||||
)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
// https://docs.metamask.io/guide/ethereum-provider.html#ethereum-autorefreshonnetworkchange
|
||||
export const disableMMAutoRefreshWarning = (): void => {
|
||||
if (window.ethereum && window.ethereum.isMetaMask) {
|
||||
window.ethereum.autoRefreshOnNetworkChange = false
|
||||
}
|
||||
}
|
|
@ -2,7 +2,11 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
|
@ -16,11 +20,18 @@
|
|||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
"jsx": "react",
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"paths": {
|
||||
"src/*": ["./*"]
|
||||
"src/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue