Merge pull request #1126 from gnosis/development
Public Release: v2.7.0
10
.gitignore
vendored
@ -1,10 +1,12 @@
|
||||
node_modules/
|
||||
build/
|
||||
/node_modules
|
||||
/build
|
||||
.DS_Store
|
||||
yarn-error.log
|
||||
.env*
|
||||
.idea/
|
||||
/.idea
|
||||
dist
|
||||
electron-builder.yml
|
||||
.yalc/
|
||||
/.yalc
|
||||
yalc.lock
|
||||
# testing
|
||||
/coverage
|
||||
|
@ -38,6 +38,7 @@ after_success:
|
||||
- ./config/travis/deploy_pull_request.sh
|
||||
# Releases (tagged commits) - Deploy it to a release environment
|
||||
- ./config/travis/deploy_release.sh
|
||||
- yarn coveralls
|
||||
|
||||
deploy:
|
||||
# Development environment
|
||||
|
@ -6,4 +6,5 @@ if [[ -n "$TRAVIS_TAG" ]]; then export REACT_APP_ENV='production'; fi
|
||||
|
||||
yarn lint:check
|
||||
yarn prettier:check
|
||||
yarn test:coverage
|
||||
yarn build
|
67
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "safe-react",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"description": "Allowing crypto users manage funds in a safer way",
|
||||
"website": "https://github.com/gnosis/safe-react#readme",
|
||||
"bugs": {
|
||||
@ -36,7 +36,9 @@
|
||||
"release": "electron-builder --mac --linux --windows -p always",
|
||||
"start-mainnet": "REACT_APP_NETWORK=mainnet yarn start",
|
||||
"start": "react-app-rewired start",
|
||||
"test": "NODE_ENV=test && react-app-rewired test --env=jsdom"
|
||||
"test": "NODE_ENV=test && react-app-rewired test --env=jsdom",
|
||||
"test:coverage": "yarn test --coverage --watchAll=false",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@ -49,6 +51,15 @@
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx,ts,tsx}",
|
||||
"!src/**/*.{.test.*}",
|
||||
"!src/**/test/**/*",
|
||||
"!src/**/assets/**",
|
||||
"!src/config/**/*"
|
||||
]
|
||||
},
|
||||
"productName": "Safe Multisig",
|
||||
"build": {
|
||||
"appId": "io.gnosis.safe.macos",
|
||||
@ -149,30 +160,32 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
|
||||
"@gnosis.pm/safe-react-components": "^0.1.3",
|
||||
"@gnosis.pm/safe-react-components": "^0.2.0",
|
||||
"@gnosis.pm/util-contracts": "2.0.6",
|
||||
"@ledgerhq/hw-transport-node-hid": "5.16.0",
|
||||
"@material-ui/core": "4.10.1",
|
||||
"@ledgerhq/hw-transport-node-hid": "5.19.0",
|
||||
"@material-ui/core": "4.11.0",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.39",
|
||||
"@openzeppelin/contracts": "3.0.2",
|
||||
"@openzeppelin/contracts": "3.1.0",
|
||||
"async-sema": "^3.1.0",
|
||||
"axios": "0.19.2",
|
||||
"bignumber.js": "9.0.0",
|
||||
"bnc-onboard": "1.10.0",
|
||||
"bnc-onboard": "1.10.2",
|
||||
"classnames": "^2.2.6",
|
||||
"concurrently": "^5.2.0",
|
||||
"connected-react-router": "6.8.0",
|
||||
"coveralls": "^3.1.0",
|
||||
"currency-flags": "2.1.2",
|
||||
"date-fns": "2.14.0",
|
||||
"electron-is-dev": "^1.1.0",
|
||||
"electron-log": "4.2.1",
|
||||
"electron-settings": "^4.0.0",
|
||||
"electron-log": "4.2.2",
|
||||
"electron-settings": "4.0.2",
|
||||
"electron-updater": "4.3.1",
|
||||
"eth-sig-util": "^2.5.3",
|
||||
"ethereum-blockies-base64": "^1.0.2",
|
||||
"exponential-backoff": "^3.0.1",
|
||||
"express": "^4.17.1",
|
||||
"final-form": "^4.20.0",
|
||||
"final-form": "4.20.1",
|
||||
"final-form-calculate": "^1.3.1",
|
||||
"history": "4.10.1",
|
||||
"immortal-db": "^1.0.2",
|
||||
@ -182,9 +195,9 @@
|
||||
"material-ui-search-bar": "^1.0.0-beta.13",
|
||||
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
|
||||
"open": "^7.0.3",
|
||||
"polished": "3.6.4",
|
||||
"polished": "3.6.5",
|
||||
"qrcode.react": "1.0.0",
|
||||
"query-string": "6.13.0",
|
||||
"query-string": "6.13.1",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-final-form": "^6.5.0",
|
||||
@ -204,42 +217,44 @@
|
||||
"semver": "7.3.2",
|
||||
"styled-components": "^5.0.1",
|
||||
"truffle-contract": "4.0.31",
|
||||
"web3": "1.2.8"
|
||||
"web3": "1.2.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "5.9.0",
|
||||
"@testing-library/react": "10.2.1",
|
||||
"@testing-library/jest-dom": "5.11.0",
|
||||
"@testing-library/react": "10.4.3",
|
||||
"@testing-library/user-event": "11.3.1",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "14.0.12",
|
||||
"@types/node": "14.0.14",
|
||||
"@types/react": "^16.9.32",
|
||||
"@types/react-dom": "^16.9.6",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@types/styled-components": "^5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "3.2.0",
|
||||
"@typescript-eslint/parser": "3.2.0",
|
||||
"autoprefixer": "9.8.0",
|
||||
"@typescript-eslint/eslint-plugin": "3.5.0",
|
||||
"@typescript-eslint/parser": "3.5.0",
|
||||
"autoprefixer": "9.8.4",
|
||||
"cross-env": "^7.0.2",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
"electron": "7.1.8",
|
||||
"electron": "7.2.4",
|
||||
"electron-builder": "22.7.0",
|
||||
"electron-notarize": "0.3.0",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-config-prettier": "6.11.0",
|
||||
"eslint-plugin-import": "2.21.1",
|
||||
"eslint-plugin-import": "2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-react": "^7.18.3",
|
||||
"eslint-plugin-sort-destructure-keys": "1.3.4",
|
||||
"eslint-plugin-react": "7.20.3",
|
||||
"eslint-plugin-sort-destructure-keys": "1.3.5",
|
||||
"ethereumjs-abi": "0.6.8",
|
||||
"husky": "^4.2.2",
|
||||
"lint-staged": "10.2.9",
|
||||
"lint-staged": "10.2.11",
|
||||
"node-sass": "^4.14.1",
|
||||
"prettier": "2.0.5",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
"truffle": "5.1.29",
|
||||
"typescript": "^3.9.5",
|
||||
"truffle": "5.1.33",
|
||||
"typescript": "3.9.6",
|
||||
"wait-on": "5.0.1",
|
||||
"web3-core": "^1.2.9",
|
||||
"web3-eth-contract": "^1.2.9",
|
||||
"web3-utils": "^1.2.8"
|
||||
}
|
||||
|
BIN
public/build/icon.ico
Normal file
After Width: | Height: | Size: 106 KiB |
@ -42,7 +42,7 @@ interface Props {
|
||||
ethBalance?: string
|
||||
}
|
||||
|
||||
const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props) => {
|
||||
const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props): React.ReactElement => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<div className="icon-section">
|
||||
@ -64,7 +64,7 @@ const AddressInfo = ({ ethBalance, safeAddress, safeName }: Props) => {
|
||||
{ethBalance && (
|
||||
<StyledBlock>
|
||||
<Paragraph noMargin>
|
||||
Balance: <Bold>{`${ethBalance} ETH`}</Bold>
|
||||
Balance: <Bold data-testid="current-eth-balance">{`${ethBalance} ETH`}</Bold>
|
||||
</Paragraph>
|
||||
</StyledBlock>
|
||||
)}
|
||||
|
@ -18,7 +18,7 @@ const useStyles = makeStyles({
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto',
|
||||
maxWidth: '100%',
|
||||
padding: `40px ${sm} 20px`,
|
||||
padding: `20px ${sm} 20px`,
|
||||
width: `${screenSm}px`,
|
||||
},
|
||||
item: {
|
||||
|
@ -6,7 +6,7 @@ import * as React from 'react'
|
||||
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Divider from 'src/components/layout/Divider'
|
||||
import { md, screenSm, sm } from 'src/theme/variables'
|
||||
import { screenSm, sm } from 'src/theme/variables'
|
||||
|
||||
const styles = () => ({
|
||||
root: {
|
||||
@ -26,8 +26,8 @@ const styles = () => ({
|
||||
flex: '1 1 auto',
|
||||
padding: sm,
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
paddingLeft: md,
|
||||
paddingRight: md,
|
||||
paddingLeft: sm,
|
||||
paddingRight: sm,
|
||||
},
|
||||
},
|
||||
expand: {
|
||||
|
@ -1,18 +1,25 @@
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Dot from '@material-ui/icons/FiberManualRecord'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import * as React from 'react'
|
||||
|
||||
import NetworkLabel from '../NetworkLabel'
|
||||
import CircleDot from 'src/components/Header/components/CircleDot'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
|
||||
import WalletIcon from '../WalletIcon'
|
||||
import { connected as connectedBg, screenSm, sm } from 'src/theme/variables'
|
||||
|
||||
const styles = () => ({
|
||||
const useStyles = makeStyles({
|
||||
network: {
|
||||
fontFamily: 'Averta, sans-serif',
|
||||
},
|
||||
networkLabel: {
|
||||
'& div': {
|
||||
paddingRight: sm,
|
||||
paddingLeft: sm,
|
||||
},
|
||||
},
|
||||
identicon: {
|
||||
display: 'none',
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
@ -33,6 +40,12 @@ const styles = () => ({
|
||||
display: 'block',
|
||||
},
|
||||
},
|
||||
providerContainer: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
width: '100px',
|
||||
},
|
||||
account: {
|
||||
alignItems: 'start',
|
||||
display: 'flex',
|
||||
@ -42,25 +55,27 @@ const styles = () => ({
|
||||
paddingRight: sm,
|
||||
},
|
||||
address: {
|
||||
marginLeft: '5px',
|
||||
letterSpacing: '-0.5px',
|
||||
},
|
||||
})
|
||||
|
||||
const ProviderInfo = ({ classes, connected, network, provider, userAddress }) => {
|
||||
const providerText = `${provider} [${network}]`
|
||||
interface ProviderInfoProps {
|
||||
connected: boolean
|
||||
provider: string
|
||||
userAddress: string
|
||||
}
|
||||
|
||||
const ProviderInfo = ({ connected, provider, userAddress }: ProviderInfoProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const cutAddress = connected ? shortVersionOf(userAddress, 4) : 'Connection Error'
|
||||
const color = connected ? 'primary' : 'warning'
|
||||
const identiconAddress = userAddress || 'random'
|
||||
|
||||
return (
|
||||
<>
|
||||
{connected && (
|
||||
<>
|
||||
<Identicon address={identiconAddress} className={classes.identicon} diameter={30} />
|
||||
<Dot className={classes.dot} />
|
||||
</>
|
||||
)}
|
||||
{!connected && <CircleDot circleSize={35} dotRight={11} dotSize={16} dotTop={24} keySize={14} mode="warning" />}
|
||||
<WalletIcon provider={provider.toUpperCase()} />
|
||||
<Col className={classes.account} layout="column" start="sm">
|
||||
<Paragraph
|
||||
className={classes.network}
|
||||
@ -70,14 +85,20 @@ const ProviderInfo = ({ classes, connected, network, provider, userAddress }) =>
|
||||
weight="bolder"
|
||||
data-testid="connected-wallet"
|
||||
>
|
||||
{providerText}
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.address} color={color} noMargin size="xs">
|
||||
{cutAddress}
|
||||
{provider}
|
||||
</Paragraph>
|
||||
<div className={classes.providerContainer}>
|
||||
{connected && <Identicon address={identiconAddress} className={classes.identicon} diameter={10} />}
|
||||
<Paragraph className={classes.address} color={color} noMargin size="xs">
|
||||
{cutAddress}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Col>
|
||||
<Col className={classes.networkLabel} layout="column" start="sm">
|
||||
<NetworkLabel />
|
||||
</Col>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(ProviderInfo)
|
||||
export default ProviderInfo
|
||||
|
@ -10,6 +10,7 @@ import Col from 'src/components/layout/Col'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { safesCountSelector } from 'src/routes/safe/store/selectors'
|
||||
import { border, md, screenSm, sm, xs } from 'src/theme/variables'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN'
|
||||
|
||||
@ -59,8 +60,4 @@ const SafeListHeader = ({ safesCount }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
// $FlowFixMe
|
||||
(state) => ({ safesCount: safesCountSelector(state) }),
|
||||
null,
|
||||
)(SafeListHeader)
|
||||
export default connect((state: AppReduxState) => ({ safesCount: safesCountSelector(state) }), null)(SafeListHeader)
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@ -0,0 +1,31 @@
|
||||
<svg width="40" height="40" viewBox="0 0 383 383" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<path d="M0.998047 0.572266L382.78 0.572266V382.354H0.998047L0.998047 0.572266Z" fill="url(#paint0_linear)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M59.1074 191.572C59.1074 264.966 118.605 324.463 191.998 324.463C265.392 324.463 324.889 264.966 324.889 191.572C324.889 118.179 265.392 58.6816 191.998 58.6816C118.605 58.6816 59.1074 118.179 59.1074 191.572ZM158.037 148.752C153.144 148.752 149.178 152.718 149.178 157.611V225.533C149.178 230.426 153.144 234.393 158.037 234.393H225.959C230.852 234.393 234.818 230.426 234.818 225.533V157.611C234.818 152.718 230.852 148.752 225.959 148.752H158.037Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd" x="-23.002" y="-7.42773" width="429.782" height="429.782" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="16"/>
|
||||
<feGaussianBlur stdDeviation="12"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="4"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear" x1="191.889" y1="0.572266" x2="191.889" y2="382.354" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2E66F8"/>
|
||||
<stop offset="1" stop-color="#124ADB"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="381.782" height="381.782" fill="white" transform="translate(0.998047 0.572266)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,11 @@
|
||||
<svg
|
||||
height="40"
|
||||
viewBox="0 0 40 40"
|
||||
width="40"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m2744.99995 1155h9.99997 10.00008v9.98139h-10.00008-9.99997-9.99998v9.9814.64001 9.28323.05815 9.9234h-9.99997v-9.9234-.05815-9.28323-.64001-9.9814-9.98139h9.99997zm9.99961 29.88552h-9.94167v-9.92324h19.93595v10.27235c0 2.55359-1.01622 5.00299-2.82437 6.80909-1.80867 1.8061-4.26182 2.82181-6.82018 2.82335h-.34973z"
|
||||
fill="#617bff"
|
||||
fill-rule="evenodd"
|
||||
transform="translate(-2725 -1155)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 519 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 450 450" width="37" height="37"><style>.st0{fill:currentColor}</style><g id="squares_1_"><path class="st0" d="M578.2 392.7V24.3h25.6v344.1h175.3v24.3H578.2zm327.5 5.1c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5H804.6c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.3 2.6-28.9 3.9-43.5 3.8zM898 135.6c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm238-21.8c19.2 0 37.1 3.8 51.2 10.2 14.1 7.7 26.9 19.2 38.4 37.1h1.3c-1.3-21.7-1.3-42.2-1.3-62.7V0h24.3v392.7h-16.6l-6.4-42.2c-20.5 30.7-51.2 47.3-89.6 47.3s-66.5-11.5-87-35.8c-20.5-23-29.4-57.6-29.4-102.3 0-47.3 10.2-83.2 29.4-108.7 19.2-25.6 48.6-37.2 85.7-37.2zm0 21.8c-29.4 0-52.4 10.2-67.8 32-15.3 20.5-23 51.2-23 92.1 0 78 30.7 116.4 90.8 116.4 30.7 0 53.7-9 67.8-26.9 14.1-17.9 21.7-47.3 21.7-89.6v-3.8c0-42.2-7.7-72.9-21.7-90.8-12.8-20.5-35.8-29.4-67.8-29.4zm379.9-16.6v17.9l-56.3 3.8c15.3 19.2 23 39.7 23 61.4 0 26.9-9 47.3-26.9 64-17.9 16.6-40.9 24.3-70.4 24.3-12.8 0-21.7 0-25.6-1.3-10.2 5.1-17.9 11.5-23 17.9-5.1 7.7-7.7 14.1-7.7 23s3.8 15.3 10.2 19.2c6.4 3.8 17.9 6.4 33.3 6.4h47.3c29.4 0 52.4 6.4 67.8 17.9s24.3 29.4 24.3 53.7c0 29.4-11.5 51.2-34.5 66.5-23 15.3-56.3 23-99.8 23-34.5 0-61.4-6.4-80.6-20.5-19.2-12.8-28.1-32-28.1-55 0-19.2 6.4-34.5 17.9-47.3s28.1-20.5 47.3-25.6c-7.7-3.8-15.3-9-19.2-15.3-5-6.2-7.7-13.8-7.7-21.7 0-17.9 11.5-34.5 34.5-48.6-15.3-6.4-28.1-16.6-37.1-30.7-9-14.1-12.8-30.7-12.8-48.6 0-26.9 9-49.9 25.6-66.5 17.9-16.6 40.9-24.3 70.4-24.3 17.9 0 32 1.3 42.2 5.1h85.7v1.3h.2zm-222.6 319.8c0 37.1 28.1 56.3 84.4 56.3 71.6 0 107.5-23 107.5-69.1 0-16.6-5.1-28.1-16.6-35.8-11.5-7.7-29.4-11.5-55-11.5h-44.8c-49.9 1.2-75.5 20.4-75.5 60.1zm21.8-235.4c0 21.7 6.4 37.1 19.2 49.9 12.8 11.5 29.4 17.9 51.2 17.9 23 0 40.9-6.4 52.4-17.9 12.8-11.5 17.9-28.1 17.9-49.9 0-23-6.4-40.9-19.2-52.4-12.8-11.5-29.4-17.9-52.4-17.9-21.7 0-39.7 6.4-51.2 19.2-12.8 11.4-17.9 29.3-17.9 51.1z"/><path class="st0" d="M1640 397.8c-39.7 0-70.4-12.8-93.4-37.1-21.7-24.3-33.3-58.8-33.3-103.6 0-43.5 10.2-79.3 32-104.9 21.7-26.9 49.9-39.7 87-39.7 32 0 57.6 11.5 76.8 33.3 19.2 23 28.1 53.7 28.1 92.1v20.5h-197c0 37.1 9 66.5 26.9 85.7 16.6 20.5 42.2 29.4 74.2 29.4 15.3 0 29.4-1.3 40.9-3.8 11.5-2.6 26.9-6.4 44.8-14.1v24.3c-15.3 6.4-29.4 11.5-42.2 14.1-14.1 2.6-28.2 3.8-44.8 3.8zm-6.4-262.2c-26.9 0-47.3 9-64 25.6-15.3 17.9-25.6 42.2-28.1 75.5h168.9c0-32-6.4-56.3-20.5-74.2-12.8-18-32-26.9-56.3-26.9zm245.6-21.8c11.5 0 24.3 1.3 37.1 3.8l-5.1 24.3c-11.8-2.6-23.8-3.9-35.8-3.8-23 0-42.2 10.2-57.6 29.4-15.3 20.5-23 44.8-23 75.5v149.7h-25.6V119h21.7l2.6 49.9h1.3c11.5-20.5 23-34.5 35.8-42.2 15.4-9 30.7-12.9 48.6-12.9zM333.9 12.8h-183v245.6h245.6V76.7c.1-34.5-28.1-63.9-62.6-63.9zm-239.2 0H64c-34.5 0-64 28.1-64 64v30.7h94.7V12.8zM0 165h94.7v94.7H0V165zm301.9 245.6h30.7c34.5 0 64-28.1 64-64V316h-94.7v94.6zm-151-94.6h94.7v94.7h-94.7V316zM0 316v30.7c0 34.5 28.1 64 64 64h30.7V316H0z"/></g></svg>
|
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.6 KiB |
BIN
src/components/Header/components/WalletIcon/icons/icon-opera.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 6.7 KiB |
170
src/components/Header/components/WalletIcon/icons/icon-torus.svg
Normal file
@ -0,0 +1,170 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="40px" height="40px" viewBox="0 0 500 500" enable-background="new 0 0 500 500" xml:space="preserve"> <image id="image0" width="500" height="500" x="0" y="0"
|
||||
href="data:image/png;base64,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
|
||||
VumtKlrVuXrc6sY1rDVla96snbr2XMXoik3rqOsU6zoqoyub19usX7H+U5Wo6np1SPXuGtOaJTXv
|
||||
Ngg2XNkYvLFhk9mm8k0ffxL/dGtzxObGWrvaii3ELQVbHm9N3nrmZ5+f67aZbCvf9nm7ZHvHjvgd
|
||||
p+q86+p2mu5cXo/WK+q7d03adfmX0F+aG1waNu9m7i7fA/Yo9jz7Nf3XG3uj9rbu89nXsN92f80B
|
||||
xoGyRqRxZmNvk6ipozm1uf3g2IOtLf4tBw6NPLT9sOXh6iOGR5YfpR4tOTpwrOhY33Hp8Z4TWSc6
|
||||
W6e23jk54eS1U+NPtZ2OOn32t/DfTp5hnzl2NuDs4XN+5w6e9znfdMHrQuNFz4sHfvf8/UCbV1vj
|
||||
Je9LzZd9L7e0j2k/eiXoyomroVd/u8a9duF6zPX2G0k3bt2cdLPjluDW09u5t1/+UfBH/535dwl3
|
||||
y+7p3qu4b3q/9j+O/9nd4dVx5EHog4sPEx7e6eR3Pn8kf/Spq+Qx/XHFE4sndU/dnx7uDu++/Gzi
|
||||
s67n0uf9PaV/6v1Z88Lhxf6/gv+62Duht+ul7OXAq6WvjV9vfzP6TWtfXN/9t3lv+9+VvTd+v+OD
|
||||
z4czH1M+Pumf8Yn0qfKz4+eWL1Ff7g7kDQxIeTKeaiuAwYZmZgLwajsA9FQAGJfh/mGi+pynEkR9
|
||||
NlUh8L+w+iyoEi8AGuBFuV3nHAdgD2x2wZB7PgDKrXpiMEA9PIaaRuSZHu5qLho88RDeDwy8NgOA
|
||||
1ALAZ9nAQP+GgYHPW2GwtwE4Pl19vlQKEZ4NfgpWoutGSZPBd/JfZxSAO16j6r8AAAAgY0hSTQAA
|
||||
eiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAcVQTFRF////jbn6c6n4ZKD4XZz3
|
||||
e6757PP+0OP9udT8o8f7lb76hrX5fbD5eq75d6z5hLT5kLv6n8T7s9H7yd784+7+/P3/U5b3Y6D4
|
||||
+/3/0uT9qcv7grL5Xp34WJn3ncP6xtz88/j+9fn/wNn8iLf5W5v3VJf3ea35rs776/P+/v//j7v6
|
||||
e6/5v9j8+Pv/vNb8b6f4X534psn77/b+Z6L48vf+y9/9bKX4+vz/aqT4uNT8Wpv3lr/69/r/a6T4
|
||||
2+n9Vpj3utX8w9r8mMD6/f7/rc37eK35m8L6tdL8yt/9msH65e/++Pr/3er9bab49vn/ZaH41eX9
|
||||
nML6bqb44u3+VZf3stD7f7H53Or9q8z7jrr6Yp/4sdD7gLH5dav5VJb37fT+5vD+r877frD5ocb7
|
||||
rMz7irj6Wpr3/v7/a6X45/D+0eP9ocX7t9P8qcr7ttP8wtr80OL9n8X7aaT4qsv79vr/9Pj+caj4
|
||||
7/X+irf61+f9pcj7nsT67vX++fv/YZ/4z+L9xNv8kr36aKP43+z+kbz6mcH68fb+osb70+T9V5j3
|
||||
jLn6yN78pMf7YJ74lL766PH+tNH84ez+yN38ZqL44e3+vtf82ej9LiPXZgAAAAFiS0dEAIgFHUgA
|
||||
AAAJcEhZcwAAD2EAAA9hAag/p2kAAAAHdElNRQfjDBUAFCYCkNuIAAAOW3pUWHRSYXcgcHJvZmls
|
||||
ZSB0eXBlIGljYwAAWIWtmWeS5Dquhf9rFW8JNKDBcmgjZv8beB9kKk1Xz8yNmKxmK0XRgDAHB8rj
|
||||
X2Mc/8cnqubD2WfryL644vJwwZ9deeZVpIQUpEgILtWkqQXnyoo89ndT53joQjyyz7HE4sQnl5wM
|
||||
d3++7//yOXfd7Ho8N/aZMcwfyf7h5/hnw73PklOJOV636e7P4chi3XleD7qcV6nCgV0J5brPsV4T
|
||||
QiwFzbmnP92SezlQ56nG60F5dpZc3vur/PR/jNf6vpBgmUvU3K4dqsOKaCvcK6/+SORKRv5yS7Tm
|
||||
3R/dkRen1rzO++2eB4vhlcWuCVuehQYGmnk//feCLtQj/02i8ndJ8y+SHueD8ueDD+u8PlWQ3/wu
|
||||
mwvr68E/NP/fP//7hVDhKOn7KCHfX0auJUhN4br1l5L9rLnnLT2FZ6FLmX4NrBhkyb4nXBP9rlhW
|
||||
Xhvc/cE1NkhskI6PHYIfFsISb4dz/pIoRFO+F5F+918bB8lsPNh4HJ8TMGzOKllue9zxHAoSFRFN
|
||||
Px5+9ddqtpWU4rPQLaoOvHnjP/u7P2d0FMV/btyW+f+bsp8dhqDwntLPhPsIKJW9RR98ehbaUtAS
|
||||
rls+lR1dI5Y0ZVl3/3WN+Gxkg/ijo2uhGM6FkHT9uVAoKYV0A024JI2x5ZSXge7HCWJa55Hzj/mf
|
||||
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" />
|
||||
</svg>
|
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,5 @@
|
||||
<svg width="40px" height="40px" viewBox="0 0 114 166" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Styles" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M17,51.453125 L17,40 C17,17.90861 34.90861,-1.0658141e-14 57,-1.0658141e-14 C79.09139,-1.0658141e-14 97,17.90861 97,40 L97,51.453125 L113.736328,51.453125 L113.736328,139.193359 L57.5,166 L0,139.193359 L0,51.453125 L17,51.453125 Z M37,51.453125 L77,51.453125 L77,40 L76.9678398,40 C76.3750564,29.406335 67.6617997,21 57,21 C46.3382003,21 37.6249436,29.406335 37.0321602,40 L37,40 L37,51.453125 Z M23,72 L23,125 L56.8681641,140.966797 L91,125 L91,72 L23,72 Z" id="Trezor-logo" fill="currentColor"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 767 B |
@ -0,0 +1,7 @@
|
||||
<svg height="40"
|
||||
viewBox="0 0 40 40"
|
||||
width="40"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m1.36250526 6.825c-1.36250526 2.675-1.36250526 6.175-1.36250526 13.175s0 10.5 1.36250526 13.1750526c1.2 2.35 3.11249474 4.2624211 5.46249474 5.4624211 2.675 1.3625263 6.175 1.3625263 13.175 1.3625263s10.5 0 13.1750526-1.3625263c2.35-1.2 4.2624211-3.1124211 5.4624211-5.4624211 1.3625263-2.6750526 1.3625263-6.1750526 1.3625263-13.1750526s0-10.5-1.3625263-13.175c-1.2-2.35-3.1124211-4.26249474-5.4624211-5.46249474-2.6750526-1.36250526-6.1750526-1.36250526-13.1750526-1.36250526s-10.5 0-13.175 1.36250526c-2.35 1.2-4.26249474 3.11249474-5.46249474 5.46249474zm28.61875794 3.9624737c.35 0 .6812631.1437895.9250526.3875789.2436842.25.3812631.5874737.3751557.9311579-.0626294 3.7250527-.2064189 6.5750527-.4751557 8.8312632-.2625263 2.2563158-.6563158 3.9312631-1.25 5.2875789-.4.9062106-.8937895 1.6562106-1.4750526 2.2936842-.7812632.8437895-1.6749474 1.4563158-2.65 2.037579-.4168421.2492631-.8502106.4953684-1.3048421.7535789-.97.5508421-2.0365264 1.1565264-3.2451579 1.9651579-.4374737.2936842-1.0062106.2936842-1.4436843 0-1.2271578-.8181052-2.3077894-1.4312631-3.2866315-1.9865263-.2176842-.1234737-.4303158-.2441052-.6384211-.3634737-1.1436842-.6625263-2.1749474-1.2937894-3.0749474-2.2063158-.6-.6-1.1187368-1.3312631-1.5312631-2.2-.5625158-1.1625263-.94376843-2.5687368-1.22501054-4.3874736-.37501052-2.4312632-.56250526-5.6125264-.63146616-10.0250527-.0060391-.3436842.12521353-.6811579.3689609-.9311579.24374737-.2437894.5812526-.3875789.9312526-.3875789h.5375263c1.6562106.0063158 5.3124211-.1562105 8.4749474-2.61871581.4687369-.36250526 1.1250526-.36250526 1.5937895 0 3.1625263 2.46250531 6.8187368 2.62503161 8.4812631 2.61871581zm-2.9062106 14.6063158c.4062106-.837579.7437895-1.9937895 1-3.6563158.3062106-1.9874737.4937895-4.6874737.5812632-8.3624211-1.95-.0563158-5.3-.4312631-8.4937895-2.5812631-3.1936842 2.1436842-6.5436842 2.5187368-8.4874737 2.5812631.0687369 3.0374737.2062106 5.4.4249474 7.2562106.25 2.1125263.6063158 3.5437894 1.05 4.55.2937895.6687368.6188421 1.15 1.0063158 1.5749473.5187368.5688421 1.1749474 1.037579 2.0687368 1.5750527.3707369.222421.7794737.4537894 1.2244211.7056842.7927368.4486315 1.7003158.9623158 2.7130526 1.6068421.9941053-.634 1.8886316-1.1424211 2.6721053-1.5877895.2362105-.1342105.4622105-.2627368.6778947-.3872632 1.1-.6312631 1.9125263-1.1562105 2.5187369-1.7687368.4063157-.4187368.7375789-.8749474 1.0437894-1.5062105z" fill="#3375bb" fill-rule="evenodd"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
@ -0,0 +1,77 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" width="40" viewBox="0 0 38 33">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M60.5108 17.1193V7.87679H62.682V17.1193C62.682 18.6187 62.1747 19.851 61.1602 20.8163C60.1659 21.7611 58.9484 22.2335 57.5077 22.2335C56.067 22.2335 54.8292 21.7611 53.8147 20.8163C52.8204 19.851 52.3131 18.6187 52.3131 17.1193V7.87679H54.5046V17.1193C54.5046 18.0025 54.7887 18.7419 55.3771 19.2965C55.9656 19.851 56.6758 20.1386 57.5077 20.1386C58.3397 20.1386 59.0296 19.851 59.618 19.2965C60.2065 18.7419 60.5108 18.0025 60.5108 17.1193ZM67.6043 16.872V22.0273H65.5143V12.4356H67.6043V13.8733C68.1116 12.8258 69.3697 12.2302 70.506 12.2302C72.7989 12.2302 74.1178 13.7295 74.1178 16.4407V22.0273H72.0278V16.6871C72.0278 15.1878 71.2568 14.3046 70.0393 14.3046C68.6595 14.3046 67.6043 15.1262 67.6043 16.872ZM79.0079 22.0274H76.9381V12.4357H79.0079V22.0274ZM77.9731 7.21899C78.7645 7.21899 79.3529 7.79376 79.3529 8.55327C79.3529 9.31279 78.7645 9.88755 77.9731 9.88755C77.2223 9.88755 76.573 9.29226 76.573 8.55327C76.573 7.81429 77.2223 7.21899 77.9731 7.21899ZM90.412 22.0281H82.6607V7.87679H84.8116V20.0359H90.412V22.0281ZM122.079 16.872V22.0273H119.989V12.4356H122.079V13.8733C122.586 12.8258 123.844 12.2302 124.981 12.2302C127.274 12.2302 128.592 13.7295 128.592 16.4407V22.0273H126.502V16.6871C126.502 15.1878 125.731 14.3046 124.514 14.3046C123.134 14.3046 122.079 15.1262 122.079 16.872Z" fill="#12083A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M96.43 20.1786C97.2417 20.1786 97.9316 19.9116 98.4794 19.3571C99.0273 18.8025 99.3114 18.0837 99.3114 17.2005C99.3114 15.4752 98.0533 14.3045 96.43 14.3045C95.6386 14.3045 94.9487 14.5715 94.4009 15.1055C93.853 15.6395 93.5689 16.3378 93.5689 17.2005C93.5689 18.0837 93.853 18.8025 94.4009 19.3571C94.9487 19.9116 95.6386 20.1786 96.43 20.1786ZM96.4298 12.23C97.8299 12.23 99.0068 12.6818 99.9605 13.6061C100.914 14.5303 101.401 15.7216 101.401 17.2004C101.401 18.6792 100.914 19.891 99.9402 20.8358C98.9865 21.7601 97.8096 22.2325 96.4298 22.2325C95.05 22.2325 93.8731 21.7601 92.9194 20.8358C91.9657 19.891 91.499 18.6792 91.499 17.2004C91.499 15.7216 91.9657 14.5303 92.9194 13.6061C93.8731 12.6818 95.05 12.23 96.4298 12.23Z" fill="#12083A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M117.412 8.55327C117.412 7.79376 116.823 7.21899 116.032 7.21899C115.281 7.21899 114.632 7.81429 114.632 8.55327C114.632 9.29226 115.281 9.88755 116.032 9.88755C116.823 9.88755 117.412 9.31279 117.412 8.55327ZM114.997 22.0275H117.067V12.4358H114.997V22.0275Z" fill="#12083A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M111.471 14.9307C111.531 14.9144 111.592 14.8962 111.652 14.8761C112.41 14.6232 112.868 14.1 113.459 13.4244C113.595 13.2693 113.738 13.1061 113.893 12.9363L112.449 11.6182C112.201 11.8898 112.011 12.1135 111.855 12.2971C111.501 12.7147 111.323 12.9251 111.034 13.0214C110.759 13.1131 110.432 13.129 109.906 12.9673C109.159 12.4521 108.286 12.1991 107.287 12.1991C106.045 12.1991 104.985 12.6018 104.127 13.4255C103.268 14.2493 102.848 15.311 102.848 16.6289C102.848 17.5631 103.059 18.3776 103.488 19.0727L102.94 19.999L102.936 20.0072C102.271 21.1831 102.279 22.5147 103.046 23.496C103.807 24.4696 105.117 24.8493 106.55 24.5763C108.04 24.2926 109.151 24.5195 109.754 24.9013C110.046 25.0858 110.197 25.2897 110.265 25.4766C110.33 25.6575 110.347 25.9067 110.221 26.2482C110.015 26.6488 109.425 27.13 108.176 27.2317C106.93 27.3333 105.218 27.0194 103.238 26.3166L102.459 27.9536C104.624 28.8929 106.667 29.3162 108.335 29.1803C110 29.0446 111.483 28.3136 112.054 26.7708C112.319 26.0566 112.346 25.4847 112.103 24.8127C111.863 24.1467 111.389 23.6224 110.8 23.2497C109.644 22.5172 107.979 22.314 106.184 22.6559C105.272 22.8297 104.784 22.5448 104.586 22.2924C104.396 22.0486 104.287 21.5955 104.634 20.9766L104.925 20.4839C105.616 20.9016 106.405 21.1137 107.287 21.1137C108.529 21.1137 109.589 20.6927 110.448 19.869C111.324 19.0269 111.763 17.9469 111.763 16.6289C111.763 16.0063 111.665 15.4409 111.471 14.9307ZM109.027 18.5371C108.565 19.0096 107.982 19.2371 107.297 19.2371C106.629 19.2371 106.047 19.0096 105.584 18.5371C105.121 18.0646 104.882 17.4521 104.882 16.6997C104.882 15.9647 105.121 15.3698 105.584 14.9148C106.047 14.4598 106.629 14.2323 107.297 14.2323C108.668 14.2323 109.73 15.2298 109.73 16.6997C109.73 17.4521 109.49 18.0646 109.027 18.5371Z" fill="#12083A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4.41438C9.35059 0 13.3522 3.95072 13.3522 8.82418V15.8177C13.3522 18.1572 15.2731 20.0537 17.6427 20.0537C20.0123 20.0537 21.9332 18.1572 21.9332 15.8177V8.82418C21.9332 3.95072 25.9348 0 30.871 0H35.36V15.5447C35.36 25.185 27.4444 33 17.68 33C7.91561 33 0 25.185 0 15.5447V0Z" fill="url(#paint0_linear)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4.41438C9.35059 0 13.3522 3.95072 13.3522 8.82418V15.8177C13.3522 18.1572 15.2731 20.0537 17.6427 20.0537C20.0123 20.0537 21.9332 18.1572 21.9332 15.8177V8.82418C21.9332 3.95072 25.9348 0 30.871 0H35.36V15.5447C35.36 25.185 27.4444 33 17.68 33C7.91561 33 0 25.185 0 15.5447V0Z" fill="url(#paint1_linear)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4.41438C9.35059 0 13.3522 3.95072 13.3522 8.82418V15.8177C13.3522 18.1572 15.2731 20.0537 17.6427 20.0537C20.0123 20.0537 21.9332 18.1572 21.9332 15.8177V8.82418C21.9332 3.95072 25.9348 0 30.871 0H35.36V15.5447C35.36 25.185 27.4444 33 17.68 33C7.91561 33 0 25.185 0 15.5447V0Z" fill="url(#paint2_linear)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4.41438C9.35059 0 13.3522 3.95072 13.3522 8.82418V15.8177C13.3522 18.1572 15.2731 20.0537 17.6427 20.0537C20.0123 20.0537 21.9332 18.1572 21.9332 15.8177V8.82418C21.9332 3.95072 25.9348 0 30.871 0H35.36V15.5447C35.36 25.185 27.4444 33 17.68 33C7.91561 33 0 25.185 0 15.5447V0Z" fill="url(#paint3_linear)"/>
|
||||
<path opacity="0.973865" fill-rule="evenodd" clip-rule="evenodd" d="M18.4081 20.0421L20.5261 21.0203L24.6422 20.6489L26.5122 20.0421L28.6999 19.2688L31.1574 17.2003L33.1717 16.1934L34.1053 13.4891L34.4379 11.2345L34.941 9.61423V6.93811L35.2474 4.47731V2.51147C34.6562 6.52673 34.3095 7.49061 33.746 8.63864C32.9008 10.3607 32.9008 10.396 31.7599 11.9479C30.6191 13.4998 29.9889 14.2432 28.2374 15.646C26.4859 17.0487 25.7999 17.4505 23.8462 18.2507C22.5438 18.7842 21.1991 19.1924 19.8123 19.4754L18.4081 20.0421Z" fill="url(#paint4_linear)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4.41438C9.35059 0 13.3522 3.95072 13.3522 8.82418V15.8177C13.3522 18.1572 15.2731 20.0537 17.6427 20.0537C20.0123 20.0537 21.9332 18.1572 21.9332 15.8177V8.82418C21.9332 3.95072 25.9348 0 30.871 0H35.36V15.5447C35.36 25.185 27.4444 33 17.68 33C7.91561 33 0 25.185 0 15.5447V0Z" fill="url(#paint5_linear)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4.41438C9.35059 0 13.3522 3.95072 13.3522 8.82418V15.7809C13.3522 18.1407 15.2898 20.0537 17.68 20.0537C20.4898 20.0537 23.2606 19.4044 25.7707 18.1579L25.9658 18.061C28.5968 16.7544 30.8439 14.8029 32.4921 12.3933L32.5075 12.3708C34.1604 9.95431 35.0942 7.12876 35.2028 4.2148L35.36 0V15.5447C35.36 25.185 27.4444 33 17.68 33C7.91561 33 0 25.185 0 15.5447V0Z" fill="url(#paint6_linear)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4.41438C9.35059 0 13.3522 3.95072 13.3522 8.82418V15.7809C13.3522 18.1407 15.2898 20.0537 17.68 20.0537C20.4898 20.0537 23.2606 19.4044 25.7707 18.1579L25.9658 18.061C28.5968 16.7544 30.8439 14.8029 32.4921 12.3933L32.5075 12.3708C34.1604 9.95431 35.0942 7.12876 35.2028 4.2148L35.36 0V15.5447C35.36 25.185 27.4444 33 17.68 33C7.91561 33 0 25.185 0 15.5447V0Z" fill="url(#paint7_linear)"/>
|
||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="36" height="33">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4.41438C9.35059 0 13.3522 3.95072 13.3522 8.82418V15.7809C13.3522 18.1407 15.2898 20.0537 17.68 20.0537C20.4898 20.0537 23.2606 19.4044 25.7707 18.1579L25.9658 18.061C28.5968 16.7544 30.8439 14.8029 32.4921 12.3933L32.5075 12.3708C34.1604 9.95431 35.0942 7.12876 35.2028 4.2148L35.36 0V15.5447C35.36 25.185 27.4444 33 17.68 33C7.91561 33 0 25.185 0 15.5447V0Z" fill="url(#paint8_linear)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M13.3353 13.4179L12.9774 15.9216L12.487 20.1442L11.1426 21.6316L10.8473 23.9072L11.5984 27.008L11.5926 29.2371L13.6093 31.286L15.4931 32.6023L16.7269 33.7804L19.1421 34.9953L21.2238 36.382L22.9979 37.2745L21.5602 36.2802C20.7138 35.6948 19.9293 35.0267 19.2186 34.286L18.7646 33.8128C18.3584 33.3893 17.8758 33.0443 17.3416 32.7953C16.823 32.5537 16.4196 32.1228 16.2162 31.5934L16.0131 31.0649C15.6693 30.1699 15.4931 29.2206 15.4931 28.2633L15.4931 19.4755L14.731 18.8772L14.0364 18.0917L13.8629 17.7928L13.8214 17.7083C13.7616 17.5864 13.7059 17.4626 13.6543 17.3372C13.6027 17.2117 13.5579 17.0837 13.5199 16.9537L13.5143 16.9346C13.4726 16.7919 13.4401 16.6467 13.4168 16.5L13.3969 16.374L13.3353 15.8616L13.3353 13.4179Z" fill="url(#paint9_linear)"/>
|
||||
</g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4.41438C9.35059 0 13.3522 3.95072 13.3522 8.82418V15.7809C13.3522 18.1407 15.2898 20.0537 17.68 20.0537C20.4898 20.0537 23.2606 19.4044 25.7707 18.1579L25.9658 18.061C28.5968 16.7544 30.8439 14.8029 32.4921 12.3933L32.5075 12.3708C34.1604 9.95431 35.0942 7.12876 35.2028 4.2148L35.36 0V15.5447C35.36 25.185 27.4444 33 17.68 33C7.91561 33 0 25.185 0 15.5447V0Z" fill="url(#paint10_linear)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4.41438C9.3506 0 13.3522 3.95072 13.3522 8.82418V10.9597V28.7272C13.3522 31.087 15.2898 33 17.68 33C7.91562 33 0 25.185 0 15.5447V0Z" fill="url(#paint11_linear)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H4.41438C9.3506 0 13.3522 3.95072 13.3522 8.82418V10.9597V28.7272C13.3522 31.087 15.2898 33 17.68 33C7.91562 33 0 25.185 0 15.5447V0Z" fill="url(#paint12_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="17.68" y1="0" x2="17.68" y2="33" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DEEDFF"/>
|
||||
<stop offset="1" stop-color="#B2D4FD"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="32.0043" y1="1.75656" x2="30.9757" y2="20.7371" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D6D9"/>
|
||||
<stop offset="1" stop-color="#455CDA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear" x1="22.8708" y1="1.75656" x2="29.9573" y2="11.9966" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D6D9"/>
|
||||
<stop offset="1" stop-color="#04CFD9" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear" x1="22.931" y1="16.5" x2="25.2362" y2="19.5271" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3F66DA" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#2E2EAE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear" x1="24.8937" y1="13.3057" x2="27.177" y2="19.1536" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2D41B3" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#141E62"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear" x1="15.3553" y1="7.8919" x2="28.5471" y2="23.0918" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3F66DA" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#2436C1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear" x1="43.3468" y1="40.5914" x2="31.3119" y2="9.34286" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#706AE1"/>
|
||||
<stop offset="1" stop-color="#4352ED"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear" x1="67.4184" y1="1.75656" x2="37.8858" y2="38.7384" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D6D9"/>
|
||||
<stop offset="1" stop-color="#0FC4DD" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear" x1="24.7435" y1="23.2722" x2="14.1909" y2="30.3859" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3F4DDA" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#2736A4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear" x1="19.1948" y1="23.354" x2="13.0623" y2="23.3828" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2D41B3" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#141E62"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear" x1="24.7435" y1="23.2722" x2="14.1909" y2="30.3859" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3F4DDA" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#2736A4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear" x1="6.79858" y1="1.44448e-07" x2="17.0045" y2="23.042" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D6D9"/>
|
||||
<stop offset="1" stop-color="#5272EE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear" x1="4.91662" y1="8.00639" x2="12.8211" y2="20.2053" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D6D9"/>
|
||||
<stop offset="1" stop-color="#0FC4DD" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,9 @@
|
||||
<svg
|
||||
height="25"
|
||||
viewBox="0 0 40 25"
|
||||
width="40"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m8.19180572 4.83416816c6.52149658-6.38508884 17.09493158-6.38508884 23.61642788 0l.7848727.76845565c.3260748.31925442.3260748.83686816 0 1.15612272l-2.6848927 2.62873374c-.1630375.15962734-.4273733.15962734-.5904108 0l-1.0800779-1.05748639c-4.5495589-4.45439756-11.9258514-4.45439756-16.4754105 0l-1.1566741 1.13248068c-.1630376.15962721-.4273735.15962721-.5904108 0l-2.68489263-2.62873375c-.32607483-.31925456-.32607483-.83686829 0-1.15612272zm29.16903948 5.43649934 2.3895596 2.3395862c.3260732.319253.3260751.8368636.0000041 1.1561187l-10.7746894 10.5494845c-.3260726.3192568-.8547443.3192604-1.1808214.0000083-.0000013-.0000013-.0000029-.0000029-.0000042-.0000043l-7.6472191-7.4872762c-.0815187-.0798136-.2136867-.0798136-.2952053 0-.0000006.0000005-.000001.000001-.0000015.0000014l-7.6470562 7.4872708c-.3260715.3192576-.8547434.319263-1.1808215.0000116-.0000019-.0000018-.0000039-.0000037-.0000059-.0000058l-10.7749893-10.5496247c-.32607469-.3192544-.32607469-.8368682 0-1.1561226l2.38956395-2.3395823c.3260747-.31925446.85474652-.31925446 1.18082136 0l7.64733029 7.4873809c.0815188.0798136.2136866.0798136.2952054 0 .0000012-.0000012.0000023-.0000023.0000035-.0000032l7.6469471-7.4873777c.3260673-.31926181.8547392-.31927378 1.1808214-.0000267.0000046.0000045.0000091.000009.0000135.0000135l7.6473203 7.4873909c.0815186.0798135.2136866.0798135.2952053 0l7.6471967-7.4872433c.3260748-.31925458.8547465-.31925458 1.1808213 0z"
|
||||
fill="#3b99fc"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
79
src/components/Header/components/WalletIcon/icons/index.ts
Normal file
@ -0,0 +1,79 @@
|
||||
// Icons
|
||||
import metamaskIcon from './icon-metamask.png'
|
||||
import walletConnectIcon from './icon-wallet-connect.svg'
|
||||
import trezorIcon from './icon-trezor.svg'
|
||||
import ledgerIcon from './icon-ledger.svg'
|
||||
import dapperIcon from './icon-dapper.png'
|
||||
import fortmaticIcon from './icon-fortmatic.svg'
|
||||
import portisIcon from './icon-portis.svg'
|
||||
import authereumIcon from './icon-authereum.png'
|
||||
import torusIcon from './icon-torus.svg'
|
||||
import uniloginIcon from './icon-unilogin.svg'
|
||||
import coinbaseIcon from './icon-coinbase.svg'
|
||||
import operaIcon from './icon-opera.png'
|
||||
|
||||
import { WALLET_PROVIDER } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
type WalletProviderNames = typeof WALLET_PROVIDER[keyof typeof WALLET_PROVIDER]
|
||||
|
||||
interface IconValue {
|
||||
src: string
|
||||
height: number
|
||||
}
|
||||
|
||||
type WalletObjectsProps<Tvalue> = {
|
||||
[key in WalletProviderNames]: Tvalue
|
||||
}
|
||||
|
||||
const WALLET_ICONS: WalletObjectsProps<IconValue> = {
|
||||
[WALLET_PROVIDER.METAMASK]: {
|
||||
src: metamaskIcon,
|
||||
height: 25,
|
||||
},
|
||||
[WALLET_PROVIDER.WALLETCONNECT]: {
|
||||
src: walletConnectIcon,
|
||||
height: 25,
|
||||
},
|
||||
[WALLET_PROVIDER.TREZOR]: {
|
||||
src: trezorIcon,
|
||||
height: 25,
|
||||
},
|
||||
[WALLET_PROVIDER.LEDGER]: {
|
||||
src: ledgerIcon,
|
||||
height: 25,
|
||||
},
|
||||
[WALLET_PROVIDER.DAPPER]: {
|
||||
src: dapperIcon,
|
||||
height: 25,
|
||||
},
|
||||
[WALLET_PROVIDER.FORTMATIC]: {
|
||||
src: fortmaticIcon,
|
||||
height: 25,
|
||||
},
|
||||
[WALLET_PROVIDER.PORTIS]: {
|
||||
src: portisIcon,
|
||||
height: 25,
|
||||
},
|
||||
[WALLET_PROVIDER.AUTHEREUM]: {
|
||||
src: authereumIcon,
|
||||
height: 25,
|
||||
},
|
||||
[WALLET_PROVIDER.TORUS]: {
|
||||
src: torusIcon,
|
||||
height: 30,
|
||||
},
|
||||
[WALLET_PROVIDER.UNILOGIN]: {
|
||||
src: uniloginIcon,
|
||||
height: 25,
|
||||
},
|
||||
[WALLET_PROVIDER.OPERA]: {
|
||||
src: operaIcon,
|
||||
height: 25,
|
||||
},
|
||||
[WALLET_PROVIDER.WALLETLINK]: {
|
||||
src: coinbaseIcon,
|
||||
height: 25,
|
||||
},
|
||||
}
|
||||
|
||||
export default WALLET_ICONS
|
37
src/components/Header/components/WalletIcon/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import * as React from 'react'
|
||||
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Img from 'src/components/layout/Img'
|
||||
import WALLET_ICONS from './icons'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
marginLeft: '5px',
|
||||
marginRight: '10px',
|
||||
letterSpacing: '-0.5px',
|
||||
},
|
||||
icon: {
|
||||
maxWidth: 'none',
|
||||
},
|
||||
})
|
||||
|
||||
interface WalletIconProps {
|
||||
provider: string
|
||||
}
|
||||
|
||||
const WalletIcon = ({ provider }: WalletIconProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
return (
|
||||
<Col className={classes.container} layout="column" start="sm">
|
||||
<Img
|
||||
alt={provider}
|
||||
className={classes.icon}
|
||||
height={WALLET_ICONS[provider].height}
|
||||
src={WALLET_ICONS[provider].src}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletIcon
|
@ -55,13 +55,13 @@ class HeaderComponent extends React.PureComponent<any, any> {
|
||||
|
||||
getProviderInfoBased = () => {
|
||||
const { hasError } = this.state
|
||||
const { available, loaded, network, provider, userAddress } = this.props
|
||||
const { available, loaded, provider, userAddress } = this.props
|
||||
|
||||
if (hasError || !loaded) {
|
||||
return <ProviderDisconnected />
|
||||
}
|
||||
|
||||
return <ProviderAccessible connected={available} network={network} provider={provider} userAddress={userAddress} />
|
||||
return <ProviderAccessible connected={available} provider={provider} userAddress={userAddress} />
|
||||
}
|
||||
|
||||
getProviderDetailsBased = () => {
|
||||
|
@ -3,8 +3,7 @@ import styled from 'styled-components'
|
||||
export const Wrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 245px auto;
|
||||
grid-template-rows: 514px;
|
||||
min-height: 525px;
|
||||
min-height: 560px;
|
||||
.background {
|
||||
box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59);
|
||||
background-color: white;
|
||||
|
@ -18,6 +18,7 @@ import { WELCOME_ADDRESS } from 'src/routes/routes'
|
||||
import setDefaultSafe from 'src/routes/safe/store/actions/setDefaultSafe'
|
||||
|
||||
import { defaultSafeSelector, safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
const { useEffect, useMemo, useState } = React
|
||||
|
||||
@ -123,8 +124,7 @@ const Sidebar = ({ children, currentSafe, defaultSafe, safes, setDefaultSafeActi
|
||||
}
|
||||
|
||||
export default connect(
|
||||
// $FlowFixMe
|
||||
(state) => ({
|
||||
(state: AppReduxState) => ({
|
||||
safes: sortedSafeListSelector(state),
|
||||
defaultSafe: defaultSafeSelector(state),
|
||||
currentSafe: safeParamAddressFromStateSelector(state),
|
||||
|
@ -4,7 +4,11 @@ import TableRow from '@material-ui/core/TableRow'
|
||||
import TableSortLabel from '@material-ui/core/TableSortLabel'
|
||||
import * as React from 'react'
|
||||
|
||||
export const cellWidth = (width) => {
|
||||
interface CellWidth {
|
||||
maxWidth: string
|
||||
}
|
||||
|
||||
export const cellWidth = (width: string | number): CellWidth | undefined => {
|
||||
if (!width) {
|
||||
return undefined
|
||||
}
|
||||
|
11
src/components/Table/types.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
export interface TableColumn {
|
||||
align?: 'inherit' | 'left' | 'center' | 'right' | 'justify'
|
||||
custom: boolean
|
||||
disablePadding: boolean
|
||||
id: string
|
||||
label: string
|
||||
order: boolean
|
||||
static?: boolean
|
||||
style?: any
|
||||
width?: number
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { List } from 'immutable'
|
||||
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
@ -40,6 +42,14 @@ export const greaterThan = (min: number | string) => (value: string) => {
|
||||
return `Should be greater than ${min}`
|
||||
}
|
||||
|
||||
export const equalOrGreaterThan = (min: number | string) => (value: string): undefined | string => {
|
||||
if (Number.isNaN(Number(value)) || Number.parseFloat(value) >= Number(min)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return `Should be equal or greater than ${min}`
|
||||
}
|
||||
|
||||
const regexQuery = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
|
||||
const url = new RegExp(regexQuery)
|
||||
export const mustBeUrl = (value: string) => {
|
||||
@ -92,7 +102,7 @@ export const minMaxLength = (minLen, maxLen) => (value) =>
|
||||
|
||||
export const ADDRESS_REPEATED_ERROR = 'Address already introduced'
|
||||
|
||||
export const uniqueAddress = (addresses: string[]) =>
|
||||
export const uniqueAddress = (addresses: string[] | List<string>) =>
|
||||
simpleMemoize((value: string[]) => {
|
||||
const addressAlreadyExists = addresses.some((address) => sameAddress(value, address))
|
||||
return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined
|
||||
|
@ -9,10 +9,17 @@
|
||||
|
||||
@media only screen and (max-width: #{$screenLg}px) {
|
||||
.page {
|
||||
padding: 72px $lg 0px $lg;
|
||||
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;
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import Img from 'src/components/layout/Img'
|
||||
import { getNetwork } from 'src/config'
|
||||
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
|
||||
import { networkSelector } from 'src/logic/wallets/store/selectors'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
const notificationStyles = {
|
||||
success: {
|
||||
@ -75,7 +76,7 @@ const PageFrame = ({ children, classes, currentNetwork }) => {
|
||||
|
||||
export default withStyles(notificationStyles)(
|
||||
connect(
|
||||
(state) => ({
|
||||
(state: AppReduxState) => ({
|
||||
currentNetwork: networkSelector(state),
|
||||
}),
|
||||
null,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { checksumAddress } from 'src/utils/checksumAddress';
|
||||
import { ensureOnce } from 'src/utils/singleton'
|
||||
import { ETHEREUM_NETWORK, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
|
||||
import {
|
||||
RELAY_API_URL,
|
||||
SIGNATURES_VIA_METAMASK,
|
||||
@ -90,7 +91,7 @@ export const getSafeLastVersion = () => process.env.REACT_APP_LATEST_SAFE_VERSIO
|
||||
|
||||
export const buildSafeCreationTxUrl = (safeAddress) => {
|
||||
const host = getTxServiceHost()
|
||||
const address = getWeb3().utils.toChecksumAddress(safeAddress)
|
||||
const address = checksumAddress(safeAddress)
|
||||
const base = getSafeCreationTxUri(address)
|
||||
|
||||
return `${host}${base}`
|
||||
|
@ -4,15 +4,67 @@ import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
|
||||
import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png'
|
||||
import { OPENSEA_API_KEY } from 'src/utils/constants'
|
||||
|
||||
export interface OpenSeaAssetContract {
|
||||
address: string
|
||||
name: string
|
||||
image_url: string
|
||||
symbol: string
|
||||
}
|
||||
|
||||
export interface OpenSeaCollection {
|
||||
name: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
export interface OpenSeaAsset {
|
||||
asset_contract: OpenSeaAssetContract
|
||||
background_color: string
|
||||
collection: OpenSeaCollection
|
||||
description: string
|
||||
image_thumbnail_url: string
|
||||
name: string
|
||||
token_id: string
|
||||
}
|
||||
|
||||
export type OpenSeaAssets = Array<OpenSeaAsset>
|
||||
|
||||
export interface NFTAsset {
|
||||
address: string
|
||||
assetContract: OpenSeaAssetContract
|
||||
collection: OpenSeaCollection
|
||||
description: string
|
||||
image: string
|
||||
name: string
|
||||
numberOfTokens: number
|
||||
slug: string
|
||||
symbol: string
|
||||
}
|
||||
export type NFTAssets = Record<string, NFTAsset>
|
||||
|
||||
export interface NFTToken {
|
||||
assetAddress: string
|
||||
color: string
|
||||
description: string
|
||||
image: string
|
||||
name: string
|
||||
tokenId: number | string
|
||||
}
|
||||
export type NFTTokens = Array<NFTToken>
|
||||
|
||||
export interface Collectibles {
|
||||
nftAssets: NFTAssets
|
||||
nftTokens: NFTTokens
|
||||
}
|
||||
|
||||
class OpenSea {
|
||||
_rateLimit = async () => {}
|
||||
_rateLimit = async (): Promise<void> => {}
|
||||
|
||||
_endpointsUrls = {
|
||||
[ETHEREUM_NETWORK.MAINNET]: 'https://api.opensea.io/api/v1',
|
||||
[ETHEREUM_NETWORK.RINKEBY]: 'https://rinkeby-api.opensea.io/api/v1',
|
||||
}
|
||||
|
||||
_fetch = async (url) => {
|
||||
_fetch = async (url: string): Promise<Response> => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return fetch(url, {
|
||||
headers: { 'X-API-KEY': OPENSEA_API_KEY || '' },
|
||||
@ -24,12 +76,12 @@ class OpenSea {
|
||||
* @param {object} options
|
||||
* @param {number} options.rps - requests per second
|
||||
*/
|
||||
constructor(options) {
|
||||
constructor(options: { rps: number }) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
this._rateLimit = RateLimit(options.rps, { timeUnit: 60 * 1000, uniformDistribution: true })
|
||||
}
|
||||
|
||||
static extractAssets(assets) {
|
||||
static extractAssets(assets: OpenSeaAssets): NFTAssets {
|
||||
const extractNFTAsset = (asset) => ({
|
||||
address: asset.asset_contract.address,
|
||||
assetContract: asset.asset_contract,
|
||||
@ -59,7 +111,7 @@ class OpenSea {
|
||||
}, {})
|
||||
}
|
||||
|
||||
static extractTokens(assets) {
|
||||
static extractTokens(assets: OpenSeaAssets): NFTTokens {
|
||||
return assets.map((asset) => ({
|
||||
assetAddress: asset.asset_contract.address,
|
||||
color: asset.background_color,
|
||||
@ -70,7 +122,7 @@ class OpenSea {
|
||||
}))
|
||||
}
|
||||
|
||||
static extractCollectiblesInfo(assetResponseJson) {
|
||||
static extractCollectiblesInfo(assetResponseJson: { assets: OpenSeaAssets }): Collectibles {
|
||||
return {
|
||||
nftAssets: OpenSea.extractAssets(assetResponseJson.assets),
|
||||
nftTokens: OpenSea.extractTokens(assetResponseJson.assets),
|
||||
@ -82,9 +134,9 @@ class OpenSea {
|
||||
* for the provided Safe Address in the specified Network
|
||||
* @param {string} safeAddress
|
||||
* @param {string} network
|
||||
* @returns {Promise<{ nftAssets: Map<string, NFTAsset>, nftTokens: Array<NFTToken> }>}
|
||||
* @returns {Promise<Collectibles>}
|
||||
*/
|
||||
async fetchAllUserCollectiblesByCategoryAsync(safeAddress, network) {
|
||||
async fetchAllUserCollectiblesByCategoryAsync(safeAddress: string, network: string): Promise<Collectibles> {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const metadataSourceUrl = this._endpointsUrls[network]
|
||||
const url = `${metadataSourceUrl}/assets/?owner=${safeAddress}`
|
||||
|
@ -2,9 +2,12 @@ import MockedOpenSea from 'src/logic/collectibles/sources/MockedOpenSea'
|
||||
import OpenSea from 'src/logic/collectibles/sources/OpenSea'
|
||||
import { COLLECTIBLES_SOURCE } from 'src/utils/constants'
|
||||
|
||||
const sources = {
|
||||
const SOURCES = {
|
||||
opensea: new OpenSea({ rps: 4 }),
|
||||
mockedopensea: new MockedOpenSea({ rps: 4 }),
|
||||
}
|
||||
|
||||
export const getConfiguredSource = () => sources[COLLECTIBLES_SOURCE.toLowerCase()]
|
||||
type Sources = typeof SOURCES
|
||||
|
||||
export const getConfiguredSource = (): Sources['opensea'] | Sources['mockedopensea'] =>
|
||||
SOURCES[COLLECTIBLES_SOURCE.toLowerCase()]
|
||||
|
@ -3,18 +3,21 @@ import { batch } from 'react-redux'
|
||||
import { getNetwork } from 'src/config'
|
||||
import { getConfiguredSource } from 'src/logic/collectibles/sources'
|
||||
import { addNftAssets, addNftTokens } from 'src/logic/collectibles/store/actions/addCollectibles'
|
||||
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
const fetchCollectibles = () => async (dispatch, getState) => {
|
||||
const network = getNetwork()
|
||||
const safeAddress = safeParamAddressFromStateSelector(getState()) || ''
|
||||
const source = getConfiguredSource()
|
||||
const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network)
|
||||
const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispatch): Promise<void> => {
|
||||
try {
|
||||
const network = getNetwork()
|
||||
const source = getConfiguredSource()
|
||||
const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network)
|
||||
|
||||
batch(() => {
|
||||
dispatch(addNftAssets(collectibles.nftAssets))
|
||||
dispatch(addNftTokens(collectibles.nftTokens))
|
||||
})
|
||||
batch(() => {
|
||||
dispatch(addNftAssets(collectibles.nftAssets))
|
||||
dispatch(addNftTokens(collectibles.nftTokens))
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('Error fetching collectibles:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export default fetchCollectibles
|
||||
|
@ -1,28 +1,31 @@
|
||||
import { List } from 'immutable'
|
||||
import { createSelector } from 'reselect'
|
||||
import { NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea'
|
||||
|
||||
import { AppReduxState } from 'src/store'
|
||||
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles'
|
||||
import { safeActiveAssetsSelector } from 'src/routes/safe/store/selectors'
|
||||
|
||||
export const nftAssetsSelector = (state) => state[NFT_ASSETS_REDUCER_ID]
|
||||
export const nftTokensSelector = (state) => state[NFT_TOKENS_REDUCER_ID]
|
||||
export const nftAssetsSelector = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID]
|
||||
export const nftTokensSelector = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID]
|
||||
|
||||
export const nftAssetsListSelector = createSelector(nftAssetsSelector, (assets) => {
|
||||
return assets ? List(Object.entries(assets).map((item) => item[1])) : List([])
|
||||
export const nftAssetsListSelector = createSelector(nftAssetsSelector, (assets): NFTAsset[] => {
|
||||
return assets ? Object.values(assets) : []
|
||||
})
|
||||
|
||||
export const activeNftAssetsListSelector = createSelector(
|
||||
nftAssetsListSelector,
|
||||
safeActiveAssetsSelector,
|
||||
(assets, activeAssetsList) => {
|
||||
return assets.filter((asset: any) => activeAssetsList.has(asset.address))
|
||||
(assets, activeAssetsList): NFTAsset[] => {
|
||||
return assets.filter(({ address }) => activeAssetsList.has(address))
|
||||
},
|
||||
)
|
||||
|
||||
export const safeActiveSelectorMap = createSelector(activeNftAssetsListSelector, (activeAssets) => {
|
||||
const assetsMap = {}
|
||||
activeAssets.forEach((asset: any) => {
|
||||
assetsMap[asset.address] = asset
|
||||
})
|
||||
return assetsMap
|
||||
})
|
||||
export const safeActiveSelectorMap = createSelector(
|
||||
activeNftAssetsListSelector,
|
||||
(activeAssets): NFTAssets => {
|
||||
return activeAssets.reduce((acc, asset) => {
|
||||
acc[asset.address] = asset
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
)
|
||||
|
@ -43,6 +43,8 @@ export const SAFE_METHODS_NAMES = {
|
||||
CHANGE_THRESHOLD: 'changeThreshold',
|
||||
REMOVE_OWNER: 'removeOwner',
|
||||
SWAP_OWNER: 'swapOwner',
|
||||
ENABLE_MODULE: 'enableModule',
|
||||
DISABLE_MODULE: 'disableModule',
|
||||
}
|
||||
|
||||
const METHOD_TO_ID = {
|
||||
@ -50,9 +52,11 @@ const METHOD_TO_ID = {
|
||||
'0x0d582f13': SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD,
|
||||
'0xf8dc5dd9': SAFE_METHODS_NAMES.REMOVE_OWNER,
|
||||
'0x694e80c3': SAFE_METHODS_NAMES.CHANGE_THRESHOLD,
|
||||
'0x610b5925': SAFE_METHODS_NAMES.ENABLE_MODULE,
|
||||
'0xe009cfde': SAFE_METHODS_NAMES.DISABLE_MODULE,
|
||||
}
|
||||
|
||||
type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES]
|
||||
export type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES]
|
||||
type TokenMethods = 'transfer' | 'transferFrom' | 'safeTransferFrom'
|
||||
|
||||
type DecodedValues = Array<{
|
||||
@ -127,6 +131,29 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
|
||||
}
|
||||
}
|
||||
|
||||
// enableModule
|
||||
case '0x610b5925': {
|
||||
const decodedParameters = web3.eth.abi.decodeParameters(['address'], params)
|
||||
return {
|
||||
method: METHOD_TO_ID[methodId],
|
||||
parameters: [
|
||||
{ name: 'module', type: '', value: decodedParameters[0] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
// disableModule
|
||||
case '0xe009cfde': {
|
||||
const decodedParameters = web3.eth.abi.decodeParameters(['address', 'address'], params)
|
||||
return {
|
||||
method: METHOD_TO_ID[methodId],
|
||||
parameters: [
|
||||
{ name: 'prevModule', type: '', value: decodedParameters[0] },
|
||||
{ name: 'module', type: '', value: decodedParameters[1] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
@ -4,10 +4,11 @@ import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.
|
||||
import SafeProxy from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafeProxy.json'
|
||||
import { ensureOnce } from 'src/utils/singleton'
|
||||
import { simpleMemoize } from 'src/components/forms/validator'
|
||||
import { getWeb3, getNetworkIdFrom } from 'src/logic/wallets/getWeb3'
|
||||
import { getNetworkIdFrom, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { calculateGasOf, calculateGasPrice } from 'src/logic/wallets/ethTransactions'
|
||||
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
|
||||
import { isProxyCode } from 'src/logic/contracts/historicProxyCode'
|
||||
import Web3 from 'web3'
|
||||
|
||||
export const SENTINEL_ADDRESS = '0x0000000000000000000000000000000000000001'
|
||||
export const MULTI_SEND_ADDRESS = '0xB522a9f781924eD250A11C54105E51840B138AdD'
|
||||
@ -19,7 +20,7 @@ export const SAFE_MASTER_COPY_ADDRESS_V10 = '0xb6029EA3B2c51D09a50B53CA8012FeEB0
|
||||
let proxyFactoryMaster
|
||||
let safeMaster
|
||||
|
||||
const createGnosisSafeContract = (web3) => {
|
||||
const createGnosisSafeContract = (web3: Web3): any => {
|
||||
const gnosisSafe = contract(GnosisSafeSol)
|
||||
gnosisSafe.setProvider(web3.currentProvider)
|
||||
|
||||
@ -72,7 +73,7 @@ export const getSafeMasterContract = async () => {
|
||||
export const getSafeDeploymentTransaction = (safeAccounts, numConfirmations, userAccount) => {
|
||||
const gnosisSafeData = safeMaster.contract.methods
|
||||
.setup(safeAccounts, numConfirmations, ZERO_ADDRESS, '0x', DEFAULT_FALLBACK_HANDLER_ADDRESS, ZERO_ADDRESS, 0, ZERO_ADDRESS)
|
||||
.encodeABI()
|
||||
.encodeABI()
|
||||
|
||||
return proxyFactoryMaster.methods.createProxy(safeMaster.address, gnosisSafeData)
|
||||
}
|
||||
@ -94,11 +95,10 @@ export const estimateGasForDeployingSafe = async (
|
||||
return gas * parseInt(gasPrice, 10)
|
||||
}
|
||||
|
||||
export const getGnosisSafeInstanceAt = simpleMemoize(async (safeAddress) => {
|
||||
export const getGnosisSafeInstanceAt = simpleMemoize(async (safeAddress): Promise<any> => {
|
||||
const web3 = getWeb3()
|
||||
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||
const gnosisSafe = await GnosisSafe.at(safeAddress)
|
||||
return gnosisSafe
|
||||
return GnosisSafe.at(safeAddress)
|
||||
})
|
||||
|
||||
const cleanByteCodeMetadata = (bytecode) => {
|
||||
|
@ -10,7 +10,7 @@ export const FEATURES = [
|
||||
{ name: 'ERC1155', validVersion: '>=1.1.1' },
|
||||
]
|
||||
|
||||
export const safeNeedsUpdate = (currentVersion, latestVersion) => {
|
||||
export const safeNeedsUpdate = (currentVersion: string, latestVersion: string): boolean => {
|
||||
if (!currentVersion || !latestVersion) {
|
||||
return false
|
||||
}
|
||||
@ -21,9 +21,10 @@ export const safeNeedsUpdate = (currentVersion, latestVersion) => {
|
||||
return latest ? semverLessThan(current, latest) : false
|
||||
}
|
||||
|
||||
export const getCurrentSafeVersion = (gnosisSafeInstance) => gnosisSafeInstance.VERSION()
|
||||
export const getCurrentSafeVersion = (gnosisSafeInstance: { VERSION: () => Promise<string> }): Promise<string> =>
|
||||
gnosisSafeInstance.VERSION()
|
||||
|
||||
export const enabledFeatures = (version) =>
|
||||
export const enabledFeatures = (version: string): Array<string> =>
|
||||
FEATURES.reduce((acc, feature) => {
|
||||
if (semverSatisfies(version, feature.validVersion)) {
|
||||
acc.push(feature.name)
|
||||
@ -31,7 +32,16 @@ export const enabledFeatures = (version) =>
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
export const checkIfSafeNeedsUpdate = async (gnosisSafeInstance, lastSafeVersion) => {
|
||||
interface SafeVersionInfo {
|
||||
current: string
|
||||
latest: string
|
||||
needUpdate: boolean
|
||||
}
|
||||
|
||||
export const checkIfSafeNeedsUpdate = async (
|
||||
gnosisSafeInstance: { VERSION: () => Promise<string> },
|
||||
lastSafeVersion: string,
|
||||
): Promise<SafeVersionInfo> => {
|
||||
if (!gnosisSafeInstance || !lastSafeVersion) {
|
||||
return null
|
||||
}
|
||||
@ -43,7 +53,7 @@ export const checkIfSafeNeedsUpdate = async (gnosisSafeInstance, lastSafeVersion
|
||||
return { current, latest, needUpdate }
|
||||
}
|
||||
|
||||
export const getCurrentMasterContractLastVersion = async () => {
|
||||
export const getCurrentMasterContractLastVersion = async (): Promise<string> => {
|
||||
const safeMaster = await getSafeMasterContract()
|
||||
let safeMasterVersion
|
||||
try {
|
||||
@ -56,7 +66,7 @@ export const getCurrentMasterContractLastVersion = async () => {
|
||||
return safeMasterVersion
|
||||
}
|
||||
|
||||
export const getSafeVersionInfo = async (safeAddress) => {
|
||||
export const getSafeVersionInfo = async (safeAddress: string): Promise<SafeVersionInfo> => {
|
||||
try {
|
||||
const safeMaster = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const lastSafeVersion = await getCurrentMasterContractLastVersion()
|
||||
|
@ -15,7 +15,7 @@ export const upgradeSafeToLatestVersion = async (safeAddress, createTransaction)
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: MULTI_SEND_ADDRESS,
|
||||
valueInWei: 0,
|
||||
valueInWei: '0',
|
||||
txData: encodeMultiSendCallData,
|
||||
notifiedTransaction: 'STANDARD_TX',
|
||||
enqueueSnackbar: () => {},
|
||||
|
@ -11,22 +11,28 @@ import { makeToken } from 'src/logic/tokens/store/model/token'
|
||||
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import updateSafe from 'src/routes/safe/store/actions/updateSafe'
|
||||
import { SAFE_REDUCER_ID } from 'src/routes/safe/store/reducer/safe'
|
||||
import { Dispatch } from 'redux'
|
||||
import { backOff } from 'exponential-backoff'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
const humanReadableBalance = (balance, decimals) => new BigNumber(balance).times(`1e-${decimals}`).toFixed()
|
||||
const noFunc = () => {}
|
||||
const updateSafeValue = (address) => (valueToUpdate) => updateSafe({ address, ...valueToUpdate })
|
||||
|
||||
const fetchSafeTokens = (safeAddress) => async (dispatch, getState) => {
|
||||
const fetchSafeTokens = (safeAddress: string) => async (
|
||||
dispatch: Dispatch,
|
||||
getState: () => AppReduxState,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const state = getState()
|
||||
const safe = state[SAFE_REDUCER_ID].getIn([SAFE_REDUCER_ID, safeAddress])
|
||||
const safe = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
|
||||
const currentTokens = state[TOKEN_REDUCER_ID]
|
||||
|
||||
if (!safe) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await fetchTokenCurrenciesBalances(safeAddress)
|
||||
const result = await backOff(() => fetchTokenCurrenciesBalances(safeAddress))
|
||||
const currentEthBalance = safe.get('ethBalance')
|
||||
const safeBalances = safe.get('balances')
|
||||
const alreadyActiveTokens = safe.get('activeTokens')
|
||||
@ -95,8 +101,6 @@ const fetchSafeTokens = (safeAddress) => async (dispatch, getState) => {
|
||||
} catch (err) {
|
||||
console.error('Error fetching active token list', err)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default fetchSafeTokens
|
||||
|
@ -3,24 +3,24 @@ import { handleActions } from 'redux-actions'
|
||||
|
||||
import { ADD_TOKEN } from 'src/logic/tokens/store/actions/addToken'
|
||||
import { ADD_TOKENS } from 'src/logic/tokens/store/actions/saveTokens'
|
||||
import { makeToken } from 'src/logic/tokens/store/model/token'
|
||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||
|
||||
export const TOKEN_REDUCER_ID = 'tokens'
|
||||
|
||||
export type TokenState = Map<string, Token>
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[ADD_TOKENS]: (state, action) => {
|
||||
[ADD_TOKENS]: (state: TokenState, action) => {
|
||||
const { tokens } = action.payload
|
||||
|
||||
const newState = state.withMutations((map) => {
|
||||
tokens.forEach((token) => {
|
||||
return state.withMutations((map) => {
|
||||
tokens.forEach((token: Token) => {
|
||||
map.set(token.address, token)
|
||||
})
|
||||
})
|
||||
|
||||
return newState
|
||||
},
|
||||
[ADD_TOKEN]: (state, action) => {
|
||||
[ADD_TOKEN]: (state: TokenState, action) => {
|
||||
const { token } = action.payload
|
||||
const { address: tokenAddress } = token
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const tokensSelector = (state) => state[TOKEN_REDUCER_ID]
|
||||
export const tokensSelector = (state: AppReduxState): TokenState => state[TOKEN_REDUCER_ID]
|
||||
|
||||
export const tokenListSelector = createSelector(tokensSelector, (tokens) => tokens.toList())
|
||||
|
||||
|
@ -15,7 +15,7 @@ import { Map } from 'immutable'
|
||||
export const ETH_ADDRESS = '0x000'
|
||||
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
|
||||
|
||||
export const getEthAsToken = (balance: string): Token => {
|
||||
export const getEthAsToken = (balance: string | number): Token => {
|
||||
return makeToken({
|
||||
address: ETH_ADDRESS,
|
||||
name: 'Ether',
|
||||
|
@ -3,18 +3,23 @@ import Web3 from 'web3'
|
||||
import { sameAddress } from './ethAddresses'
|
||||
import { EMPTY_DATA } from './ethTransactions'
|
||||
|
||||
import { getNetwork } from 'src/config/index'
|
||||
import { getNetwork } from '../../config'
|
||||
import { ContentHash } from 'web3-eth-ens'
|
||||
import { provider as Provider } from 'web3-core'
|
||||
import { ProviderProps } from './store/model/provider'
|
||||
|
||||
export const ETHEREUM_NETWORK = {
|
||||
MAINNET: 'MAINNET',
|
||||
MORDEN: 'MORDEN',
|
||||
ROPSTEN: 'ROPSTEN',
|
||||
RINKEBY: 'RINKEBY',
|
||||
GOERLI: 'GOERLI',
|
||||
KOVAN: 'KOVAN',
|
||||
UNKNOWN: 'UNKNOWN',
|
||||
MAINNET: 'MAINNET' as const,
|
||||
MORDEN: 'MORDEN' as const,
|
||||
ROPSTEN: 'ROPSTEN' as const,
|
||||
RINKEBY: 'RINKEBY' as const,
|
||||
GOERLI: 'GOERLI' as const,
|
||||
KOVAN: 'KOVAN' as const,
|
||||
UNKNOWN: 'UNKNOWN' as const,
|
||||
}
|
||||
|
||||
export type EthereumNetworks = typeof ETHEREUM_NETWORK[keyof typeof ETHEREUM_NETWORK]
|
||||
|
||||
export const WALLET_PROVIDER = {
|
||||
SAFE: 'SAFE',
|
||||
METAMASK: 'METAMASK',
|
||||
@ -23,37 +28,33 @@ export const WALLET_PROVIDER = {
|
||||
PORTIS: 'PORTIS',
|
||||
FORTMATIC: 'FORTMATIC',
|
||||
SQUARELINK: 'SQUARELINK',
|
||||
UNILOGIN: 'UNILOGIN',
|
||||
WALLETCONNECT: 'WALLETCONNECT',
|
||||
OPERA: 'OPERA',
|
||||
DAPPER: 'DAPPER',
|
||||
WALLETLINK: 'WALLETLINK',
|
||||
AUTHEREUM: 'AUTHEREUM',
|
||||
LEDGER: 'LEDGER',
|
||||
TREZOR: 'TREZOR',
|
||||
}
|
||||
|
||||
export const ETHEREUM_NETWORK_IDS = {
|
||||
// $FlowFixMe
|
||||
1: ETHEREUM_NETWORK.MAINNET,
|
||||
// $FlowFixMe
|
||||
2: ETHEREUM_NETWORK.MORDEN,
|
||||
// $FlowFixMe
|
||||
3: ETHEREUM_NETWORK.ROPSTEN,
|
||||
// $FlowFixMe
|
||||
4: ETHEREUM_NETWORK.RINKEBY,
|
||||
// $FlowFixMe
|
||||
5: ETHEREUM_NETWORK.GOERLI,
|
||||
// $FlowFixMe
|
||||
42: ETHEREUM_NETWORK.KOVAN,
|
||||
}
|
||||
|
||||
export const getEtherScanLink = (type, value) => {
|
||||
export const getEtherScanLink = (type: string, value: string): string => {
|
||||
const network = getNetwork()
|
||||
return `https://${
|
||||
network.toLowerCase() === 'mainnet' ? '' : `${network.toLowerCase()}.`
|
||||
}etherscan.io/${type}/${value}`
|
||||
}
|
||||
|
||||
export const getInfuraUrl = () => {
|
||||
export const getInfuraUrl = (): string => {
|
||||
const isMainnet = process.env.REACT_APP_NETWORK === 'mainnet'
|
||||
|
||||
return `https://${isMainnet ? 'mainnet' : 'rinkeby'}.infura.io:443/v3/${process.env.REACT_APP_INFURA_TOKEN}`
|
||||
@ -64,39 +65,41 @@ export const getInfuraUrl = () => {
|
||||
export const web3ReadOnly =
|
||||
process.env.NODE_ENV !== 'test'
|
||||
? new Web3(new Web3.providers.HttpProvider(getInfuraUrl()))
|
||||
: new Web3((window as any).web3.currentProvider)
|
||||
: new Web3(window.web3?.currentProvider || 'ws://localhost:8545')
|
||||
|
||||
let web3 = web3ReadOnly
|
||||
export const getWeb3 = () => web3
|
||||
export const getWeb3 = (): Web3 => web3
|
||||
|
||||
export const resetWeb3 = () => {
|
||||
export const resetWeb3 = (): void => {
|
||||
web3 = web3ReadOnly
|
||||
}
|
||||
|
||||
export const getAccountFrom = async (web3Provider) => {
|
||||
export const getAccountFrom = async (web3Provider: Web3): Promise<string | null> => {
|
||||
const accounts = await web3Provider.eth.getAccounts()
|
||||
|
||||
if (process.env.NODE_ENV === 'test' && (window as any).testAccountIndex) {
|
||||
return accounts[(window as any).testAccountIndex]
|
||||
if (process.env.NODE_ENV === 'test' && window.testAccountIndex) {
|
||||
return accounts[window.testAccountIndex]
|
||||
}
|
||||
|
||||
return accounts && accounts.length > 0 ? accounts[0] : null
|
||||
}
|
||||
|
||||
export const getNetworkIdFrom = (web3Provider) => web3Provider.eth.net.getId()
|
||||
export const getNetworkIdFrom = (web3Provider: Web3): Promise<number> => web3Provider.eth.net.getId()
|
||||
|
||||
const isHardwareWallet = (walletName) =>
|
||||
const isHardwareWallet = (walletName: string) =>
|
||||
sameAddress(WALLET_PROVIDER.LEDGER, walletName) || sameAddress(WALLET_PROVIDER.TREZOR, walletName)
|
||||
|
||||
const isSmartContractWallet = async (web3Provider, account) => {
|
||||
const isSmartContractWallet = async (web3Provider: Web3, account: string): Promise<boolean> => {
|
||||
const contractCode = await web3Provider.eth.getCode(account)
|
||||
|
||||
return contractCode.replace(EMPTY_DATA, '').replace(/0/g, '') !== ''
|
||||
}
|
||||
|
||||
export const getProviderInfo = async (web3Provider, providerName = 'Wallet') => {
|
||||
export const getProviderInfo = async (
|
||||
web3Provider: string | Provider,
|
||||
providerName = 'Wallet',
|
||||
): Promise<ProviderProps> => {
|
||||
web3 = new Web3(web3Provider)
|
||||
|
||||
const account = await getAccountFrom(web3)
|
||||
const network = await getNetworkIdFrom(web3)
|
||||
const smartContractWallet = await isSmartContractWallet(web3, account)
|
||||
@ -115,15 +118,15 @@ export const getProviderInfo = async (web3Provider, providerName = 'Wallet') =>
|
||||
}
|
||||
}
|
||||
|
||||
export const getAddressFromENS = (name: string) => web3.eth.ens.getAddress(name)
|
||||
export const getAddressFromENS = (name: string): Promise<string> => web3.eth.ens.getAddress(name)
|
||||
|
||||
export const getContentFromENS = (name: string) => web3.eth.ens.getContenthash(name)
|
||||
export const getContentFromENS = (name: string): Promise<ContentHash> => web3.eth.ens.getContenthash(name)
|
||||
|
||||
export const setWeb3 = (provider) => {
|
||||
export const setWeb3 = (provider: Provider): void => {
|
||||
web3 = new Web3(provider)
|
||||
}
|
||||
|
||||
export const getBalanceInEtherOf = async (safeAddress) => {
|
||||
export const getBalanceInEtherOf = async (safeAddress: string): Promise<string> => {
|
||||
if (!web3) {
|
||||
return '0'
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackb
|
||||
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS, getProviderInfo, getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { makeProvider } from 'src/logic/wallets/store/model/provider'
|
||||
import { updateStoredTransactionsStatus } from 'src/routes/safe/store/actions/transactions/utils/transactionHelpers'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
export const processProviderResponse = (dispatch, provider) => {
|
||||
const walletRecord = makeProvider(provider)
|
||||
@ -48,9 +49,9 @@ const handleProviderNotification = (provider, dispatch) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default (providerName) => async (dispatch) => {
|
||||
export default (providerName: string) => async (dispatch: Dispatch): Promise<void> => {
|
||||
const web3 = getWeb3()
|
||||
const providerInfo = await getProviderInfo(web3, providerName)
|
||||
const providerInfo = await getProviderInfo(web3.currentProvider, providerName)
|
||||
await handleProviderNotification(providerInfo, dispatch)
|
||||
processProviderResponse(dispatch, providerInfo)
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ const providerWatcherMware = (store) => (next) => async (action) => {
|
||||
|
||||
watcherInterval = setInterval(async () => {
|
||||
const web3 = getWeb3()
|
||||
const providerInfo = await getProviderInfo(web3)
|
||||
const providerInfo = await getProviderInfo(web3.currentProvider)
|
||||
|
||||
const networkChanged = currentProviderProps.network !== providerInfo.network
|
||||
|
||||
|
@ -2,14 +2,17 @@ import { handleActions } from 'redux-actions'
|
||||
|
||||
import { ADD_PROVIDER } from 'src/logic/wallets/store/actions/addProvider'
|
||||
import { REMOVE_PROVIDER } from 'src/logic/wallets/store/actions/removeProvider'
|
||||
import { makeProvider } from 'src/logic/wallets/store/model/provider'
|
||||
import { makeProvider, ProviderRecord, ProviderProps } from 'src/logic/wallets/store/model/provider'
|
||||
|
||||
export const PROVIDER_REDUCER_ID = 'providers'
|
||||
|
||||
export type ProviderState = ProviderRecord
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[ADD_PROVIDER]: (state, { payload }) => makeProvider(payload),
|
||||
[REMOVE_PROVIDER]: () => makeProvider(),
|
||||
[ADD_PROVIDER]: (state: ProviderState, { payload }: { payload: ProviderProps }): ProviderState =>
|
||||
makeProvider(payload),
|
||||
[REMOVE_PROVIDER]: (): ProviderState => makeProvider(),
|
||||
},
|
||||
makeProvider(),
|
||||
)
|
||||
|
@ -1,29 +1,33 @@
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS } from 'src/logic/wallets/getWeb3'
|
||||
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
|
||||
import { ETHEREUM_NETWORK, ETHEREUM_NETWORK_IDS, EthereumNetworks } from 'src/logic/wallets/getWeb3'
|
||||
import { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
export const providerSelector = (state) => state[PROVIDER_REDUCER_ID]
|
||||
export const providerSelector = (state: AppReduxState): ProviderState => state[PROVIDER_REDUCER_ID]
|
||||
|
||||
export const userAccountSelector = createSelector(providerSelector, (provider) => {
|
||||
export const userAccountSelector = createSelector(providerSelector, (provider: ProviderState): string => {
|
||||
const account = provider.get('account')
|
||||
|
||||
return account || ''
|
||||
})
|
||||
|
||||
export const providerNameSelector = createSelector(providerSelector, (provider) => {
|
||||
export const providerNameSelector = createSelector(providerSelector, (provider: ProviderState): string | undefined => {
|
||||
const name = provider.get('name')
|
||||
|
||||
return name ? name.toLowerCase() : undefined
|
||||
})
|
||||
|
||||
export const networkSelector = createSelector(providerSelector, (provider) => {
|
||||
const networkId = provider.get('network')
|
||||
const network = ETHEREUM_NETWORK_IDS[networkId] || ETHEREUM_NETWORK.UNKNOWN
|
||||
export const networkSelector = createSelector(
|
||||
providerSelector,
|
||||
(provider: ProviderState): EthereumNetworks => {
|
||||
const networkId = provider.get('network')
|
||||
return ETHEREUM_NETWORK_IDS[networkId] || ETHEREUM_NETWORK.UNKNOWN
|
||||
},
|
||||
)
|
||||
|
||||
return network
|
||||
})
|
||||
export const loadedSelector = createSelector(providerSelector, (provider: ProviderState): boolean =>
|
||||
provider.get('loaded'),
|
||||
)
|
||||
|
||||
export const loadedSelector = createSelector(providerSelector, (provider) => provider.get('loaded'))
|
||||
|
||||
export const availableSelector = createSelector(providerSelector, (provider) => provider.get('available'))
|
||||
export const availableSelector = createSelector(providerSelector, (provider: ProviderState): boolean =>
|
||||
provider.get('available'),
|
||||
)
|
||||
|
@ -1,13 +1,13 @@
|
||||
//
|
||||
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
|
||||
import { userAccountSelector } from '../selectors'
|
||||
import { ProviderFactory } from './builder/index.builder'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
const providerReducerTests = () => {
|
||||
describe('Provider Name Selector[userAccountSelector]', () => {
|
||||
it('should return empty when no provider is loaded', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.noProvider }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.noProvider } as AppReduxState
|
||||
|
||||
// WHEN
|
||||
const providerName = userAccountSelector(reduxStore)
|
||||
@ -18,7 +18,7 @@ const providerReducerTests = () => {
|
||||
|
||||
it('should return empty when Metamask is loaded but not available', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskLoaded }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskLoaded } as AppReduxState
|
||||
|
||||
// WHEN
|
||||
const providerName = userAccountSelector(reduxStore)
|
||||
@ -29,7 +29,7 @@ const providerReducerTests = () => {
|
||||
|
||||
it('should return account when Metamask is loaded and available', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskAvailable }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskAvailable } as AppReduxState
|
||||
|
||||
// WHEN
|
||||
const providerName = userAccountSelector(reduxStore)
|
||||
|
@ -1,13 +1,13 @@
|
||||
//
|
||||
import { PROVIDER_REDUCER_ID } from 'src/logic/wallets/store/reducer/provider'
|
||||
import { providerNameSelector } from '../selectors'
|
||||
import { ProviderFactory } from './builder/index.builder'
|
||||
import { AppReduxState } from 'src/store'
|
||||
|
||||
const providerReducerTests = () => {
|
||||
describe('Provider Name Selector[providerNameSelector]', () => {
|
||||
it('should return undefined when no provider is loaded', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.noProvider }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.noProvider } as AppReduxState
|
||||
|
||||
// WHEN
|
||||
const providerName = providerNameSelector(reduxStore)
|
||||
@ -18,7 +18,7 @@ const providerReducerTests = () => {
|
||||
|
||||
it('should return metamask when Metamask is loaded but not available', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskLoaded }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskLoaded } as AppReduxState
|
||||
|
||||
// WHEN
|
||||
const providerName = providerNameSelector(reduxStore)
|
||||
@ -29,7 +29,7 @@ const providerReducerTests = () => {
|
||||
|
||||
it('should return METAMASK when Metamask is loaded and available', () => {
|
||||
// GIVEN
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskAvailable }
|
||||
const reduxStore = { [PROVIDER_REDUCER_ID]: ProviderFactory.metamaskAvailable } as AppReduxState
|
||||
|
||||
// WHEN
|
||||
const providerName = providerNameSelector(reduxStore)
|
||||
|
@ -1,18 +1,19 @@
|
||||
function transactionDataCheck(): any {
|
||||
let completed = false
|
||||
return (stateAndHelpers) => {
|
||||
const { wallet } = stateAndHelpers
|
||||
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
|
||||
import { WALLET_PROVIDER } from 'src/logic/wallets/getWeb3'
|
||||
|
||||
if (wallet && wallet.name === 'Ledger' && !completed) {
|
||||
const USER_ENABLED_LEDGER_TX_DATA = 'USER_ENABLED_LEDGER_TX_DATA'
|
||||
function transactionDataCheck(): any {
|
||||
return async (stateAndHelpers) => {
|
||||
const { wallet } = stateAndHelpers
|
||||
const isTransactionDataEnabled = await loadFromStorage<boolean>(USER_ENABLED_LEDGER_TX_DATA)
|
||||
if (wallet && wallet.name === WALLET_PROVIDER.LEDGER && !isTransactionDataEnabled) {
|
||||
return {
|
||||
heading: 'Allow Transaction Data', // edit modal heading here
|
||||
description: 'Please allow transaction data on your Ledger device.', // edit modal description that is displayed here. You can include html strings here and they will be rendered as html elements.
|
||||
eventCode: 'allowTransactionData',
|
||||
button: {
|
||||
text: 'Done',
|
||||
onclick: () => {
|
||||
completed = true
|
||||
},
|
||||
onclick: async () => await saveToStorage(USER_ENABLED_LEDGER_TX_DATA, true),
|
||||
},
|
||||
icon: `
|
||||
<svg height="14" viewBox="0 0 18 14" width="18" xmlns="http://www.w3.org/2000/svg"><g fill="currentColor"><path d="m10.29375 4.05351563c0-.04921875 0-.09140625 0-.13007813 0-1.0546875 0-2.109375 0-3.1640625 0-.43945312.3480469-.76992188.7804688-.7453125.2003906.01054688.3585937.10546875.4992187.24609375.5800781.58359375 1.1566406 1.16367188 1.7367187 1.74023438 1.4695313 1.46953125 2.9390625 2.93906249 4.4050782 4.40859375.1335937.13359375.2425781.27421875.2707031.46757812.0351562.20742188-.0246094.421875-.1652344.58007813-.0246094.028125-.0492187.05273437-.0738281.08085937-2.0601563 2.06367188-4.1203125 4.1238281-6.1804688 6.1875-.2109375.2109375-.4570312.3023438-.7453125.2179688-.2707031-.0808594-.4464843-.2707032-.5132812-.5484375-.0140625-.0738282-.0175781-.1441407-.0140625-.2179688 0-1.0335937 0-2.0707031 0-3.1042969 0-.0386719 0-.08085935 0-.13359372h-5.06953125c-.49570313 0-.80507813-.309375-.80507813-.80859375 0-1.42382813 0-2.84414063 0-4.26796875 0-.49570313.30585938-.8015625.8015625-.8015625h4.93593748z"/><path d="m5.69882812 13.978125h-4.01132812c-.928125 0-1.6875-.8753906-1.6875-1.9511719v-10.06171872c0-1.07578125.75585938-1.95117188 1.6875-1.95117188h4.01132812c.34101563 0 .61523438.31992188.61523438.71015625 0 .39023438-.27421875.71015625-.61523438.71015625h-4.01132812c-.253125 0-.45703125.23554688-.45703125.52734375v10.06171875c0 .2917969.20390625.5273437.45703125.5273437h4.01132812c.34101563 0 .61523438.3199219.61523438.7101563s-.27773438.7171875-.61523438.7171875z"/></g></svg>
|
||||
|
@ -12,6 +12,7 @@ import ReviewInformation from 'src/routes/load/components/ReviewInformation'
|
||||
|
||||
import { history } from 'src/store'
|
||||
import { secondary, sm } from 'src/theme/variables'
|
||||
import { LoadFormValues } from '../container/Load'
|
||||
|
||||
const getSteps = () => ['Name and address', 'Owners', 'Review']
|
||||
|
||||
@ -33,7 +34,14 @@ const formMutators = {
|
||||
|
||||
const buttonLabels = ['Next', 'Review', 'Load']
|
||||
|
||||
const Layout = ({ network, onLoadSafeSubmit, provider, userAddress }) => {
|
||||
interface ILayout {
|
||||
network: string
|
||||
provider?: string
|
||||
userAddress: string
|
||||
onLoadSafeSubmit: (values: LoadFormValues) => void
|
||||
}
|
||||
|
||||
const Layout: React.FC<ILayout> = ({ network, onLoadSafeSubmit, provider, userAddress }) => {
|
||||
const steps = getSteps()
|
||||
const initialValues = {}
|
||||
|
||||
|
@ -10,7 +10,6 @@ import selector from './selector'
|
||||
import Page from 'src/components/layout/Page'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import { SAFES_KEY, saveSafes } from 'src/logic/safe/utils'
|
||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||
import { getNamesFrom, getOwnersFrom } from 'src/routes/open/utils/safeDataExtractor'
|
||||
import { SAFELIST_ADDRESS } from 'src/routes/routes'
|
||||
import { buildSafe } from 'src/routes/safe/store/actions/fetchSafe'
|
||||
@ -19,6 +18,7 @@ import { loadFromStorage } from 'src/utils/storage'
|
||||
import { Dispatch } from 'redux'
|
||||
import { SafeOwner } from '../../safe/store/models/safe'
|
||||
import { List } from 'immutable'
|
||||
import { checksumAddress } from 'src/utils/checksumAddress'
|
||||
|
||||
export const loadSafe = async (
|
||||
safeName: string,
|
||||
@ -37,16 +37,31 @@ export const loadSafe = async (
|
||||
await addSafe(safeProps)
|
||||
}
|
||||
|
||||
class Load extends React.Component<any> {
|
||||
onLoadSafeSubmit = async (values) => {
|
||||
interface ILoad {
|
||||
addSafe: Dispatch<any>
|
||||
network: string
|
||||
provider?: string
|
||||
userAddress: string
|
||||
}
|
||||
|
||||
export interface LoadFormValues {
|
||||
name: string
|
||||
address: string
|
||||
threshold: string
|
||||
}
|
||||
|
||||
const Load: React.FC<ILoad> = ({ addSafe, network, provider, userAddress }) => {
|
||||
const onLoadSafeSubmit = async (values: LoadFormValues) => {
|
||||
let safeAddress = values[FIELD_LOAD_ADDRESS]
|
||||
// TODO: review this check. It doesn't seems to be necessary at this point
|
||||
if (!safeAddress) {
|
||||
console.error('failed to load Safe address', JSON.stringify(values))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { addSafe } = this.props
|
||||
const web3 = getWeb3()
|
||||
const safeName = values[FIELD_LOAD_NAME]
|
||||
let safeAddress = values[FIELD_LOAD_ADDRESS]
|
||||
if (safeAddress) {
|
||||
safeAddress = web3.utils.toChecksumAddress(safeAddress)
|
||||
}
|
||||
safeAddress = checksumAddress(safeAddress)
|
||||
const ownerNames = getNamesFrom(values)
|
||||
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
@ -62,20 +77,11 @@ class Load extends React.Component<any> {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { network, provider, userAddress } = this.props
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Layout
|
||||
network={network}
|
||||
onLoadSafeSubmit={this.onLoadSafeSubmit}
|
||||
provider={provider}
|
||||
userAddress={userAddress}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Page>
|
||||
<Layout network={network} onLoadSafeSubmit={onLoadSafeSubmit} provider={provider} userAddress={userAddress} />
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(selector, actions)(Load)
|
||||
|
@ -69,12 +69,14 @@ export const createSafe = (values, userAccount) => {
|
||||
})
|
||||
.then(async (receipt) => {
|
||||
await checkReceiptStatus(receipt.transactionHash)
|
||||
|
||||
const safeAddress = receipt.events.ProxyCreation.returnValues.proxy
|
||||
const safeProps = await getSafeProps(safeAddress, name, ownersNames, ownerAddresses)
|
||||
// returning info for testing purposes, in app is fully async
|
||||
return { safeAddress: safeProps.address, safeTx: receipt }
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
|
||||
return promiEvent
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: stri
|
||||
href={getEtherScanLink('tx', safeCreationTxHash)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
data-testid="safe-create-etherscan-link"
|
||||
>
|
||||
Etherscan.io
|
||||
</EtherScanLink>
|
||||
|
@ -63,7 +63,7 @@ const isTxValid = (t: SafeAppTx): boolean => {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof t.value === 'string' && !/^\d+$/.test(t.value)) {
|
||||
if (typeof t.value === 'string' && !/^(0x)?[0-9a-f]+$/i.test(t.value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY'
|
||||
const APPS_LEGAL_DISCLAIMER_STORAGE_KEY = 'APPS_LEGAL_DISCLAIMER_STORAGE_KEY'
|
||||
|
||||
const StyledIframe = styled.iframe`
|
||||
padding: 24px;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -39,6 +39,10 @@ const Centered = styled.div`
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const CenteredMT = styled(Centered)`
|
||||
margin-top: 5px;
|
||||
`
|
||||
|
||||
const IframeWrapper = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
@ -409,7 +413,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
||||
</Centered>
|
||||
</Card>
|
||||
)}
|
||||
<Centered>
|
||||
<CenteredMT>
|
||||
<IconText
|
||||
color="secondary"
|
||||
iconSize="sm"
|
||||
@ -417,7 +421,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
|
||||
text="These are third-party apps, which means they are not owned, controlled, maintained or audited by Gnosis. Interacting with the apps is at your own risk."
|
||||
textSize="sm"
|
||||
/>
|
||||
</Centered>
|
||||
</CenteredMT>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ const sendTransactions = (dispatch, safeAddress, txs, enqueueSnackbar, closeSnac
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: multiSendAddress,
|
||||
valueInWei: 0,
|
||||
valueInWei: '0',
|
||||
txData: encodeMultiSendCallData,
|
||||
notifiedTransaction: 'STANDARD_TX',
|
||||
enqueueSnackbar,
|
||||
|
@ -25,6 +25,8 @@ export const staticAppsList: Array<{ url: string; disabled: boolean }> = [
|
||||
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmTBBaiDQyGa17DJ7DdviyHbc51fTVgf6Z5PW5w2YUTkgR`, disabled: false },
|
||||
// Sablier
|
||||
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmabPEk7g4zaytFefp6fE4nz8f85QMJoWmRQQZypvJViNG`, disabled: false },
|
||||
// Synthetix
|
||||
{ url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQSVfqPYSSsnmtbJQCg7s9iLykTDYKoK5k98K4Fd6bwdJ`, disabled: false },
|
||||
// TX-Builder
|
||||
{ url: `${gnosisAppsUrl}/tx-builder`, disabled: false },
|
||||
]
|
||||
|
@ -72,7 +72,7 @@ const Coins = (props) => {
|
||||
break
|
||||
}
|
||||
case BALANCE_TABLE_BALANCE_ID: {
|
||||
cellItem = <div>{row[id]}</div>
|
||||
cellItem = <div data-testid={`balance-${row[BALANCE_TABLE_ASSET_ID].symbol}`}>{row[id]}</div>
|
||||
break
|
||||
}
|
||||
case BALANCE_TABLE_VALUE_ID: {
|
||||
|
@ -90,7 +90,7 @@ const Collectibles = () => {
|
||||
return (
|
||||
<Card className={classes.cardOuter}>
|
||||
<div className={classes.cardInner}>
|
||||
{activeAssetsList.size ? (
|
||||
{activeAssetsList.length ? (
|
||||
activeAssetsList.map((nftAsset) => {
|
||||
return (
|
||||
<React.Fragment key={nftAsset.slug}>
|
||||
|
@ -14,6 +14,19 @@ import { getAddressBookListSelector } from 'src/logic/addressBook/store/selector
|
||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||
|
||||
export interface AddressBookProps {
|
||||
fieldMutator: (address: string) => void
|
||||
isCustomTx?: boolean
|
||||
pristine: boolean
|
||||
recipientAddress?: string
|
||||
setSelectedEntry: (
|
||||
entry: { address?: string; name?: string } | React.SetStateAction<{ address: string; name: string }>,
|
||||
) => void
|
||||
setIsValidAddress: (valid?: boolean) => void
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const textFieldLabelStyle = makeStyles(() => ({
|
||||
root: {
|
||||
overflow: 'hidden',
|
||||
@ -30,34 +43,39 @@ const textFieldInputStyle = makeStyles(() => ({
|
||||
},
|
||||
}))
|
||||
|
||||
const filterAddressBookWithContractAddresses = async (addressBook) => {
|
||||
const filterAddressBookWithContractAddresses = async (
|
||||
addressBook: List<{ address: string }>,
|
||||
): Promise<List<{ address: string }>> => {
|
||||
const abFlags = await Promise.all(
|
||||
addressBook.map(async ({ address }) => {
|
||||
return (await mustBeEthereumContractAddress(address)) === undefined
|
||||
}),
|
||||
addressBook.map(
|
||||
async ({ address }: { address: string }): Promise<boolean> => {
|
||||
return (await mustBeEthereumContractAddress(address)) === undefined
|
||||
},
|
||||
),
|
||||
)
|
||||
return addressBook.filter((adbkEntry, index) => abFlags[index])
|
||||
|
||||
return addressBook.filter((_, index) => abFlags[index])
|
||||
}
|
||||
|
||||
const AddressBookInput = ({
|
||||
classes,
|
||||
fieldMutator,
|
||||
isCustomTx,
|
||||
pristine,
|
||||
recipientAddress,
|
||||
setIsValidAddress,
|
||||
setSelectedEntry,
|
||||
}: any) => {
|
||||
}: AddressBookProps) => {
|
||||
const classes = useStyles()
|
||||
const addressBook = useSelector(getAddressBookListSelector)
|
||||
const [isValidForm, setIsValidForm] = useState(true)
|
||||
const [validationText, setValidationText] = useState<any>(true)
|
||||
const [validationText, setValidationText] = useState<string>('')
|
||||
const [inputTouched, setInputTouched] = useState(false)
|
||||
const [blurred, setBlurred] = useState(pristine)
|
||||
const [adbkList, setADBKList] = useState(List([]))
|
||||
const [adbkList, setADBKList] = useState<List<{ address: string }>>(List([]))
|
||||
|
||||
const [inputAddValue, setInputAddValue] = useState(recipientAddress)
|
||||
|
||||
const onAddressInputChanged = async (addressValue) => {
|
||||
const onAddressInputChanged = async (addressValue: string): Promise<void> => {
|
||||
setInputAddValue(addressValue)
|
||||
let resolvedAddress = addressValue
|
||||
let isValidText
|
||||
@ -99,7 +117,7 @@ const AddressBookInput = ({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const filterAdbkContractAddresses = async () => {
|
||||
const filterAdbkContractAddresses = async (): Promise<void> => {
|
||||
if (!isCustomTx) {
|
||||
setADBKList(addressBook)
|
||||
return
|
||||
|
@ -1,4 +1,6 @@
|
||||
export const styles = () => ({
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
export const styles = createStyles({
|
||||
itemOptionList: {
|
||||
display: 'flex',
|
||||
},
|
||||
|
@ -110,6 +110,7 @@ const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }) => {
|
||||
minWidth={260}
|
||||
onClick={() => setActiveScreen('sendFunds')}
|
||||
variant="contained"
|
||||
testId="modal-send-funds-btn"
|
||||
>
|
||||
<Img alt="Send funds" className={classNames(classes.leftIcon, classes.iconSmall)} src={Token} />
|
||||
Send funds
|
||||
@ -122,6 +123,7 @@ const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }) => {
|
||||
minWidth={260}
|
||||
onClick={() => setActiveScreen('sendCollectible')}
|
||||
variant="contained"
|
||||
testId="modal-send-collectible-btn"
|
||||
>
|
||||
<Img
|
||||
alt="Send collectible"
|
||||
@ -138,6 +140,7 @@ const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }) => {
|
||||
minWidth={260}
|
||||
onClick={() => setActiveScreen('contractInteraction')}
|
||||
variant="outlined"
|
||||
testId="modal-contract-interaction-btn"
|
||||
>
|
||||
<Img
|
||||
alt="Contract Interaction"
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useFormState, useField } from 'react-final-form'
|
||||
|
||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import Field from 'src/components/forms/Field'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import {
|
||||
@ -16,7 +18,7 @@ import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/Co
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
export interface EthAddressProps {
|
||||
export interface EthAddressInputProps {
|
||||
isContract?: boolean
|
||||
isRequired?: boolean
|
||||
name: string
|
||||
@ -24,10 +26,24 @@ export interface EthAddressProps {
|
||||
text: string
|
||||
}
|
||||
|
||||
const EthAddressInput = ({ isContract = true, isRequired = true, name, onScannedValue, text }: EthAddressProps) => {
|
||||
const EthAddressInput = ({
|
||||
isContract = true,
|
||||
isRequired = true,
|
||||
name,
|
||||
onScannedValue,
|
||||
text,
|
||||
}: EthAddressInputProps) => {
|
||||
const classes = useStyles()
|
||||
const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress]
|
||||
const validate = composeValidators(...validatorsList.filter((_) => _))
|
||||
const { pristine } = useFormState({ subscription: { pristine: true } })
|
||||
const {
|
||||
input: { value },
|
||||
} = useField('contractAddress', { subscription: { value: true } })
|
||||
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({
|
||||
address: value,
|
||||
name: '',
|
||||
})
|
||||
|
||||
const handleScan = (value, closeQrModal) => {
|
||||
let scannedAddress = value
|
||||
@ -40,19 +56,35 @@ const EthAddressInput = ({ isContract = true, isRequired = true, name, onScanned
|
||||
closeQrModal()
|
||||
}
|
||||
|
||||
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const { value } = event.target
|
||||
setSelectedEntry({ address: value })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<Field
|
||||
component={TextField}
|
||||
name={name}
|
||||
placeholder={text}
|
||||
testId={name}
|
||||
text={text}
|
||||
type="text"
|
||||
validate={validate}
|
||||
/>
|
||||
{selectedEntry?.address ? (
|
||||
<Field
|
||||
component={TextField}
|
||||
name={name}
|
||||
placeholder={text}
|
||||
onChange={handleInputChange}
|
||||
testId={name}
|
||||
text={text}
|
||||
type="text"
|
||||
validate={validate}
|
||||
/>
|
||||
) : (
|
||||
<AddressBookInput
|
||||
setSelectedEntry={setSelectedEntry}
|
||||
setIsValidAddress={() => {}}
|
||||
fieldMutator={onScannedValue}
|
||||
isCustomTx
|
||||
pristine={pristine}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||
<ScanQRWrapper handleScan={handleScan} />
|
||||
|
@ -45,7 +45,7 @@ const typePlaceholder = (text: string, type: string): string => {
|
||||
return `${text} E.g.: ["first value", "second value", "third value"]`
|
||||
}
|
||||
|
||||
const ArrayTypeInput = ({ name, text, type }: { name: string; text: string; type: string }): JSX.Element => (
|
||||
const ArrayTypeInput = ({ name, text, type }: { name: string; text: string; type: string }): React.ReactElement => (
|
||||
<TextareaField name={name} placeholder={typePlaceholder(text, type)} text={text} type="text" validate={validator} />
|
||||
)
|
||||
|
||||
|
@ -15,7 +15,7 @@ type Props = {
|
||||
placeholder: string
|
||||
}
|
||||
|
||||
const InputComponent = ({ type, keyValue, placeholder }: Props): JSX.Element => {
|
||||
const InputComponent = ({ type, keyValue, placeholder }: Props): React.ReactElement => {
|
||||
if (!type) {
|
||||
return null
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import InputComponent from './InputComponent'
|
||||
import { generateFormFieldKey } from '../utils'
|
||||
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||
|
||||
const RenderInputParams = (): JSX.Element => {
|
||||
const RenderInputParams = (): React.ReactElement => {
|
||||
const {
|
||||
meta: { valid: validABI },
|
||||
} = useField('abi', { subscription: { valid: true, value: true } })
|
||||
|
@ -19,7 +19,7 @@ import Field from 'src/components/forms/Field'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import TextareaField from 'src/components/forms/TextareaField'
|
||||
import { composeValidators, maxValue, mustBeFloat, greaterThan } from 'src/components/forms/validator'
|
||||
import { composeValidators, maxValue, mustBeFloat, equalOrGreaterThan } from 'src/components/forms/validator'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Button from 'src/components/layout/Button'
|
||||
import ButtonLink from 'src/components/layout/ButtonLink'
|
||||
@ -230,7 +230,7 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
|
||||
placeholder="Value*"
|
||||
text="Value*"
|
||||
type="text"
|
||||
validate={composeValidators(mustBeFloat, maxValue(ethBalance), greaterThan(0))}
|
||||
validate={composeValidators(mustBeFloat, maxValue(ethBalance), equalOrGreaterThan(0))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -100,7 +100,6 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({
|
||||
>
|
||||
{(submitting, validating, rest, mutators) => {
|
||||
setCallResults = mutators.setCallResults
|
||||
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.formContainer}>
|
||||
|
@ -105,7 +105,7 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Row align="center" className={classes.heading} grow data-testid="send-funds-review-step">
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Send Funds
|
||||
</Paragraph>
|
||||
@ -136,7 +136,12 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => {
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph className={classes.address} noMargin weight="bolder">
|
||||
<Paragraph
|
||||
className={classes.address}
|
||||
noMargin
|
||||
weight="bolder"
|
||||
data-testid="recipient-address-review-step"
|
||||
>
|
||||
{tx.recipientAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={tx.recipientAddress} />
|
||||
@ -151,12 +156,12 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => {
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Img alt={txToken.name} height={28} onError={setImageToPlaceholder} src={txToken.logoUri} />
|
||||
<Paragraph className={classes.amount} noMargin size="md">
|
||||
<Paragraph className={classes.amount} noMargin size="md" data-testid={`amount-${txToken.symbol}-review-step`}>
|
||||
{tx.amount} {txToken.symbol}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph>
|
||||
<Paragraph data-testid="fee-meg-review-step">
|
||||
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ETH in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
|
@ -57,7 +57,11 @@ const TokenSelectField = ({ classes, initialValue, isValid, tokens }) => (
|
||||
<ListItemIcon>
|
||||
<Img alt={token.name} height={28} onError={setImageToPlaceholder} src={token.logoUri} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={token.name} secondary={`${formatAmount(token.balance)} ${token.symbol}`} />
|
||||
<ListItemText
|
||||
primary={token.name}
|
||||
secondary={`${formatAmount(token.balance)} ${token.symbol}`}
|
||||
data-testid={`select-token-${token.name}`}
|
||||
/>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Field>
|
||||
|
@ -84,7 +84,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Row align="center" className={classes.heading} grow data-testid="modal-title-send-funds">
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
Send Funds
|
||||
</Paragraph>
|
||||
@ -221,7 +221,11 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Amount
|
||||
</Paragraph>
|
||||
<ButtonLink onClick={() => mutators.setMax(selectedTokenRecord.balance)} weight="bold">
|
||||
<ButtonLink
|
||||
onClick={() => mutators.setMax(selectedTokenRecord.balance)}
|
||||
weight="bold"
|
||||
testId="send-max-btn"
|
||||
>
|
||||
Send max
|
||||
</ButtonLink>
|
||||
</Col>
|
||||
@ -239,6 +243,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
|
||||
placeholder="Amount*"
|
||||
text="Amount*"
|
||||
type="text"
|
||||
testId="amount-input"
|
||||
validate={composeValidators(
|
||||
required,
|
||||
mustBeFloat,
|
||||
|
@ -130,12 +130,12 @@ const AssetsList = (props) => {
|
||||
</Row>
|
||||
<Hairline />
|
||||
</Block>
|
||||
{!nftAssetsList.size && (
|
||||
{!nftAssetsList.length && (
|
||||
<Block className={classes.progressContainer} justify="center">
|
||||
<CircularProgress />
|
||||
</Block>
|
||||
)}
|
||||
{nftAssetsList.size > 0 && (
|
||||
{nftAssetsList.length > 0 && (
|
||||
<MuiList className={classes.list}>
|
||||
<FixedSizeList
|
||||
height={413}
|
||||
|
@ -4,6 +4,7 @@ import { List } from 'immutable'
|
||||
import { FIXED, buildOrderFieldFrom } from 'src/components/Table/sorting'
|
||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||
import { ETH_ADDRESS } from 'src/logic/tokens/utils/tokenHelpers'
|
||||
import { TableColumn } from 'src/components/Table/types'
|
||||
|
||||
export const BALANCE_TABLE_ASSET_ID = 'asset'
|
||||
export const BALANCE_TABLE_BALANCE_ID = 'balance'
|
||||
@ -40,6 +41,7 @@ export const getBalanceData = (activeTokens, currencySelected, currencyValues, c
|
||||
name: token.name,
|
||||
logoUri: token.logoUri,
|
||||
address: token.address,
|
||||
symbol: token.symbol,
|
||||
},
|
||||
[buildOrderFieldFrom(BALANCE_TABLE_ASSET_ID)]: token.name,
|
||||
[BALANCE_TABLE_BALANCE_ID]: `${formatAmount(token.balance)} ${token.symbol}`,
|
||||
@ -51,8 +53,8 @@ export const getBalanceData = (activeTokens, currencySelected, currencyValues, c
|
||||
return rows
|
||||
}
|
||||
|
||||
export const generateColumns = () => {
|
||||
const assetColumn = {
|
||||
export const generateColumns = (): List<TableColumn> => {
|
||||
const assetColumn: TableColumn = {
|
||||
id: BALANCE_TABLE_ASSET_ID,
|
||||
order: true,
|
||||
disablePadding: false,
|
||||
@ -61,7 +63,7 @@ export const generateColumns = () => {
|
||||
width: 250,
|
||||
}
|
||||
|
||||
const balanceColumn = {
|
||||
const balanceColumn: TableColumn = {
|
||||
id: BALANCE_TABLE_BALANCE_ID,
|
||||
align: 'right',
|
||||
order: true,
|
||||
@ -70,7 +72,7 @@ export const generateColumns = () => {
|
||||
custom: false,
|
||||
}
|
||||
|
||||
const actions = {
|
||||
const actions: TableColumn = {
|
||||
id: 'actions',
|
||||
order: false,
|
||||
disablePadding: false,
|
||||
@ -79,7 +81,7 @@ export const generateColumns = () => {
|
||||
static: true,
|
||||
}
|
||||
|
||||
const value = {
|
||||
const value: TableColumn = {
|
||||
id: BALANCE_TABLE_VALUE_ID,
|
||||
order: false,
|
||||
label: 'Value',
|
||||
|
@ -47,7 +47,7 @@ const Balances = (props) => {
|
||||
const address = useSelector(safeParamAddressFromStateSelector)
|
||||
const featuresEnabled = useSelector(safeFeaturesEnabledSelector)
|
||||
|
||||
useFetchTokens()
|
||||
useFetchTokens(address)
|
||||
|
||||
useEffect(() => {
|
||||
const erc721Enabled = featuresEnabled && featuresEnabled.includes('ERC721')
|
||||
|
125
src/routes/safe/components/Settings/Advanced/ModulesTable.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import { Button, Text } from '@gnosis.pm/safe-react-components'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import styled from 'styled-components'
|
||||
import cn from 'classnames'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { generateColumns, ModuleAddressColumn, MODULES_TABLE_ADDRESS_ID } from './dataFetcher'
|
||||
import RemoveModuleModal from './RemoveModuleModal'
|
||||
import { styles } from './style'
|
||||
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { ModulePair } from 'src/routes/safe/store/models/safe'
|
||||
import Table from 'src/components/Table'
|
||||
import { TableCell, TableRow } from 'src/components/layout/Table'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Row from 'src/components/layout/Row'
|
||||
|
||||
const REMOVE_MODULE_BTN_TEST_ID = 'remove-module-btn'
|
||||
const MODULES_ROW_TEST_ID = 'owners-row'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const AddressText = styled(Text)`
|
||||
margin-left: 12px;
|
||||
`
|
||||
|
||||
const TableActionButton = styled(Button)`
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
interface ModulesTableProps {
|
||||
moduleData: ModuleAddressColumn | null
|
||||
}
|
||||
|
||||
const ModulesTable = ({ moduleData }: ModulesTableProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const columns = generateColumns()
|
||||
const autoColumns = columns.filter(({ custom }) => !custom)
|
||||
|
||||
const granted = useSelector(grantedSelector)
|
||||
|
||||
const [viewRemoveModuleModal, setViewRemoveModuleModal] = React.useState(false)
|
||||
const hideRemoveModuleModal = () => setViewRemoveModuleModal(false)
|
||||
|
||||
const [selectedModule, setSelectedModule] = React.useState(null)
|
||||
const triggerRemoveSelectedModule = (module: ModulePair): void => {
|
||||
setSelectedModule(module)
|
||||
setViewRemoveModuleModal(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={moduleData}
|
||||
defaultFixed
|
||||
defaultOrderBy={MODULES_TABLE_ADDRESS_ID}
|
||||
disablePagination
|
||||
label="Modules"
|
||||
noBorder
|
||||
size={moduleData.length}
|
||||
>
|
||||
{(sortedData) =>
|
||||
sortedData.map((row, index) => (
|
||||
<TableRow
|
||||
className={cn(classes.hide, index >= 3 && index === sortedData.size - 1 && classes.noBorderBottom)}
|
||||
data-testid={MODULES_ROW_TEST_ID}
|
||||
key={index}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{autoColumns.map((column, index) => {
|
||||
const columnId = column.id
|
||||
const rowElement = row[columnId]
|
||||
|
||||
return (
|
||||
<React.Fragment key={`${columnId}-${index}`}>
|
||||
<TableCell align={column.align} component="td" key={columnId}>
|
||||
{columnId === MODULES_TABLE_ADDRESS_ID ? (
|
||||
<Block justify="left">
|
||||
<Identicon address={rowElement[0]} diameter={32} />
|
||||
<AddressText size="lg">{rowElement[0]}</AddressText>
|
||||
</Block>
|
||||
) : (
|
||||
rowElement
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell component="td">
|
||||
<Row align="end" className={classes.actions}>
|
||||
{granted && (
|
||||
<TableActionButton
|
||||
size="md"
|
||||
iconType="delete"
|
||||
color="error"
|
||||
variant="outlined"
|
||||
onClick={() => triggerRemoveSelectedModule(rowElement)}
|
||||
data-testid={REMOVE_MODULE_BTN_TEST_ID}
|
||||
>
|
||||
{null}
|
||||
</TableActionButton>
|
||||
)}
|
||||
</Row>
|
||||
</TableCell>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{viewRemoveModuleModal && <RemoveModuleModal onClose={hideRemoveModuleModal} selectedModule={selectedModule} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModulesTable
|
@ -0,0 +1,144 @@
|
||||
import { Button } from '@gnosis.pm/safe-react-components'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||
import cn from 'classnames'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import React from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import { ModulePair } from 'src/routes/safe/store/models/safe'
|
||||
import { safeParamAddressFromStateSelector } from 'src/routes/safe/store/selectors'
|
||||
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
|
||||
import createTransaction from 'src/routes/safe/store/actions/createTransaction'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import Modal from 'src/components/Modal'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Hairline from 'src/components/layout/Hairline'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Link from 'src/components/layout/Link'
|
||||
import { getEtherScanLink } from 'src/logic/wallets/getWeb3'
|
||||
import { md, secondary } from 'src/theme/variables'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const FooterWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
`
|
||||
|
||||
const openIconStyle = {
|
||||
height: md,
|
||||
color: secondary,
|
||||
}
|
||||
|
||||
interface RemoveModuleModal {
|
||||
onClose: () => void
|
||||
selectedModule: ModulePair
|
||||
}
|
||||
|
||||
const RemoveModuleModal = ({ onClose, selectedModule }: RemoveModuleModal): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
||||
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const removeSelectedModule = async (): Promise<void> => {
|
||||
try {
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const [module, prevModule] = selectedModule
|
||||
const txData = safeInstance.contract.methods.disableModule(prevModule, module).encodeABI()
|
||||
|
||||
dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: safeAddress,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
}),
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(`failed to remove the module ${selectedModule}`, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
description="Remove the selected Module"
|
||||
handleClose={onClose}
|
||||
paperClassName={classes.modal}
|
||||
title="Remove Module"
|
||||
open
|
||||
>
|
||||
<Row align="center" className={classes.modalHeading} grow>
|
||||
<Paragraph className={classes.modalManage} noMargin weight="bolder">
|
||||
Remove Module
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.modalClose} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.modalContainer}>
|
||||
<Row className={classes.modalOwner}>
|
||||
<Col align="center" xs={1}>
|
||||
<Identicon address={selectedModule[0]} diameter={32} />
|
||||
</Col>
|
||||
<Col xs={11}>
|
||||
<Block className={cn(classes.modalName, classes.modalUserName)}>
|
||||
<Paragraph noMargin size="lg" weight="bolder">
|
||||
{selectedModule[0]}
|
||||
</Paragraph>
|
||||
<Block className={classes.modalUser} justify="center">
|
||||
<Paragraph color="disabled" noMargin size="md">
|
||||
{selectedModule[0]}
|
||||
</Paragraph>
|
||||
<Link
|
||||
className={classes.modalOpen}
|
||||
target="_blank"
|
||||
to={getEtherScanLink('address', selectedModule[0])}
|
||||
>
|
||||
<OpenInNew style={openIconStyle} />
|
||||
</Link>
|
||||
</Block>
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row className={classes.modalDescription}>
|
||||
<Paragraph noMargin>
|
||||
After removing this module, any feature or app that uses this module might no longer work. If this Safe
|
||||
requires more then one signature, the module removal will have to be confirmed by other owners as well.
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.modalButtonRow}>
|
||||
<FooterWrapper>
|
||||
<Button size="md" color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="error" size="md" variant="contained" onClick={removeSelectedModule}>
|
||||
Remove
|
||||
</Button>
|
||||
</FooterWrapper>
|
||||
</Row>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RemoveModuleModal
|
35
src/routes/safe/components/Settings/Advanced/dataFetcher.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { List } from 'immutable'
|
||||
import { TableColumn } from 'src/components/Table/types'
|
||||
import { ModulePair } from 'src/routes/safe/store/models/safe'
|
||||
|
||||
export const MODULES_TABLE_ADDRESS_ID = 'address'
|
||||
export const MODULES_TABLE_ACTIONS_ID = 'actions'
|
||||
|
||||
export type ModuleAddressColumn = { [MODULES_TABLE_ADDRESS_ID]: ModulePair }[]
|
||||
|
||||
export const getModuleData = (modulesList: ModulePair[] | null): ModuleAddressColumn | undefined => {
|
||||
return modulesList?.map((modules) => ({
|
||||
[MODULES_TABLE_ADDRESS_ID]: modules,
|
||||
}))
|
||||
}
|
||||
|
||||
export const generateColumns = (): List<TableColumn> => {
|
||||
const addressColumn: TableColumn = {
|
||||
align: 'left',
|
||||
custom: false,
|
||||
disablePadding: false,
|
||||
id: MODULES_TABLE_ADDRESS_ID,
|
||||
label: 'Address',
|
||||
order: false,
|
||||
}
|
||||
|
||||
const actionsColumn: TableColumn = {
|
||||
custom: true,
|
||||
disablePadding: false,
|
||||
id: MODULES_TABLE_ACTIONS_ID,
|
||||
label: '',
|
||||
order: false,
|
||||
}
|
||||
|
||||
return List([addressColumn, actionsColumn])
|
||||
}
|
93
src/routes/safe/components/Settings/Advanced/index.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { Loader, Text, theme, Title } from '@gnosis.pm/safe-react-components'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { getModuleData } from './dataFetcher'
|
||||
import { styles } from './style'
|
||||
import ModulesTable from './ModulesTable'
|
||||
|
||||
import Block from 'src/components/layout/Block'
|
||||
import { safeModulesSelector, safeNonceSelector } from 'src/routes/safe/store/selectors'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const InfoText = styled(Text)`
|
||||
margin-top: 16px;
|
||||
`
|
||||
|
||||
const Bold = styled.strong`
|
||||
color: ${theme.colors.text};
|
||||
`
|
||||
|
||||
const NoModuleLegend = (): React.ReactElement => (
|
||||
<InfoText color="secondaryLight" size="xl">
|
||||
No modules enabled
|
||||
</InfoText>
|
||||
)
|
||||
|
||||
const LoadingModules = (): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Block className={classes.container}>
|
||||
<Loader size="md" />
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
const Advanced = (): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const nonce = useSelector(safeNonceSelector)
|
||||
const modules = useSelector(safeModulesSelector)
|
||||
const moduleData = getModuleData(modules) ?? null
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Nonce */}
|
||||
<Block className={classes.container}>
|
||||
<Title size="xs" withoutMargin>
|
||||
Safe Nonce
|
||||
</Title>
|
||||
<InfoText size="lg">
|
||||
For security reasons, transactions made with the Safe need to be executed in order. The nonce shows you which
|
||||
transaction was executed most recently. You can find the nonce for a transaction in the transaction details.
|
||||
</InfoText>
|
||||
<InfoText color="secondaryLight" size="xl">
|
||||
Current Nonce: <Bold>{nonce}</Bold>
|
||||
</InfoText>
|
||||
</Block>
|
||||
|
||||
{/* Modules */}
|
||||
<Block className={classes.container}>
|
||||
<Title size="xs" withoutMargin>
|
||||
Safe Modules
|
||||
</Title>
|
||||
<InfoText size="lg">
|
||||
Modules allow you to customize the access-control logic of your Safe. Modules are potentially risky, so make
|
||||
sure to only use modules from trusted sources. Learn more about modules{' '}
|
||||
<a
|
||||
href="https://docs.gnosis.io/safe/docs/contracts_architecture/#3-module-management"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</InfoText>
|
||||
|
||||
{moduleData === null ? (
|
||||
<NoModuleLegend />
|
||||
) : moduleData?.length === 0 ? (
|
||||
<LoadingModules />
|
||||
) : (
|
||||
<ModulesTable moduleData={moduleData} />
|
||||
)}
|
||||
</Block>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Advanced
|
112
src/routes/safe/components/Settings/Advanced/style.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { createStyles } from '@material-ui/core'
|
||||
import { background, border, error, fontColor, lg, md, secondaryText, sm, smallFontSize, xl } from 'src/theme/variables'
|
||||
|
||||
export const styles = createStyles({
|
||||
title: {
|
||||
padding: lg,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
hide: {
|
||||
'&:hover': {
|
||||
backgroundColor: '#fff3e2',
|
||||
},
|
||||
'&:hover $actions': {
|
||||
visibility: 'initial',
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
justifyContent: 'flex-end',
|
||||
visibility: 'hidden',
|
||||
minWidth: '100px',
|
||||
},
|
||||
noBorderBottom: {
|
||||
'& > td': {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
},
|
||||
annotation: {
|
||||
paddingLeft: lg,
|
||||
},
|
||||
ownersText: {
|
||||
color: secondaryText,
|
||||
'& b': {
|
||||
color: fontColor,
|
||||
},
|
||||
},
|
||||
container: {
|
||||
padding: lg,
|
||||
},
|
||||
buttonRow: {
|
||||
padding: lg,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
justifyContent: 'flex-end',
|
||||
borderTop: `2px solid ${border}`,
|
||||
},
|
||||
modifyBtn: {
|
||||
height: xl,
|
||||
fontSize: smallFontSize,
|
||||
},
|
||||
removeModuleIcon: {
|
||||
marginLeft: lg,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
modalHeading: {
|
||||
boxSizing: 'border-box',
|
||||
justifyContent: 'space-between',
|
||||
maxHeight: '75px',
|
||||
padding: `${sm} ${lg}`,
|
||||
},
|
||||
modalContainer: {
|
||||
minHeight: '369px',
|
||||
},
|
||||
modalManage: {
|
||||
fontSize: lg,
|
||||
},
|
||||
modalClose: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
modalButtonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
modalButtonRemove: {
|
||||
color: '#fff',
|
||||
backgroundColor: error,
|
||||
height: '42px',
|
||||
},
|
||||
modalName: {
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
modalUserName: {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
modalOwner: {
|
||||
backgroundColor: background,
|
||||
padding: md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalUser: {
|
||||
justifyContent: 'left',
|
||||
},
|
||||
modalDescription: {
|
||||
padding: md,
|
||||
},
|
||||
modalOpen: {
|
||||
paddingLeft: sm,
|
||||
width: 'auto',
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
})
|
@ -34,7 +34,7 @@ export const sendAddOwner = async (values, safeAddress, ownersOld, enqueueSnackb
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: safeAddress,
|
||||
valueInWei: 0,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
enqueueSnackbar,
|
||||
|
@ -4,16 +4,31 @@ import EtherScanLink from 'src/components/EtherscanLink'
|
||||
import Identicon from 'src/components/Identicon'
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { useWindowDimensions } from '../../../../container/hooks/useWindowDimensions'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const OwnerAddressTableCell = (props) => {
|
||||
const { address, knownAddress, showLinks, userName } = props
|
||||
const [cut, setCut] = useState(undefined)
|
||||
const { width } = useWindowDimensions()
|
||||
|
||||
useEffect(() => {
|
||||
if (width <= 900) {
|
||||
setCut(6)
|
||||
} else if (width <= 1024) {
|
||||
setCut(12)
|
||||
} else {
|
||||
setCut(undefined)
|
||||
}
|
||||
}, [width])
|
||||
|
||||
return (
|
||||
<Block justify="left">
|
||||
<Identicon address={address} diameter={32} />
|
||||
{showLinks ? (
|
||||
<div style={{ marginLeft: 10, flexShrink: 1, minWidth: 0 }}>
|
||||
{userName}
|
||||
<EtherScanLink knownAddress={knownAddress} type="address" value={address} />
|
||||
<EtherScanLink knownAddress={knownAddress} type="address" value={address} cut={cut} />
|
||||
</div>
|
||||
) : (
|
||||
<Paragraph style={{ marginLeft: 10 }}>{address}</Paragraph>
|
||||
|
@ -53,7 +53,7 @@ export const sendRemoveOwner = async (
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: safeAddress,
|
||||
valueInWei: 0,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
enqueueSnackbar,
|
||||
|
@ -47,7 +47,7 @@ export const sendReplaceOwner = async (
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: safeAddress,
|
||||
valueInWei: 0,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
enqueueSnackbar,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { List } from 'immutable'
|
||||
import { TableColumn } from 'src/components/Table/types'
|
||||
|
||||
export const OWNERS_TABLE_NAME_ID = 'name'
|
||||
export const OWNERS_TABLE_ADDRESS_ID = 'address'
|
||||
@ -13,8 +14,8 @@ export const getOwnerData = (owners) => {
|
||||
return rows
|
||||
}
|
||||
|
||||
export const generateColumns = () => {
|
||||
const nameColumn = {
|
||||
export const generateColumns = (): List<TableColumn> => {
|
||||
const nameColumn: TableColumn = {
|
||||
id: OWNERS_TABLE_NAME_ID,
|
||||
order: false,
|
||||
disablePadding: false,
|
||||
@ -24,7 +25,7 @@ export const generateColumns = () => {
|
||||
align: 'left',
|
||||
}
|
||||
|
||||
const addressColumn = {
|
||||
const addressColumn: TableColumn = {
|
||||
id: OWNERS_TABLE_ADDRESS_ID,
|
||||
order: false,
|
||||
disablePadding: false,
|
||||
@ -33,7 +34,7 @@ export const generateColumns = () => {
|
||||
align: 'left',
|
||||
}
|
||||
|
||||
const actionsColumn = {
|
||||
const actionsColumn: TableColumn = {
|
||||
id: OWNERS_TABLE_ACTIONS_ID,
|
||||
order: false,
|
||||
disablePadding: false,
|
||||
|
@ -43,7 +43,7 @@ const ThresholdSettings = ({ classes, closeSnackbar, enqueueSnackbar }) => {
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: safeAddress,
|
||||
valueInWei: 0,
|
||||
valueInWei: '0',
|
||||
txData,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX,
|
||||
enqueueSnackbar,
|
||||
|
@ -1,17 +1,16 @@
|
||||
import { IconText } from '@gnosis.pm/safe-react-components'
|
||||
import Badge from '@material-ui/core/Badge'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import cn from 'classnames'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import Advanced from './Advanced'
|
||||
import ManageOwners from './ManageOwners'
|
||||
import { RemoveSafeModal } from './RemoveSafeModal'
|
||||
import SafeDetails from './SafeDetails'
|
||||
import ThresholdSettings from './ThresholdSettings'
|
||||
import { OwnersIcon } from './assets/icons/OwnersIcon'
|
||||
import { RequiredConfirmationsIcon } from './assets/icons/RequiredConfirmationsIcon'
|
||||
import { SafeDetailsIcon } from './assets/icons/SafeDetailsIcon'
|
||||
import RemoveSafeIcon from './assets/icons/bin.svg'
|
||||
import { styles } from './style'
|
||||
|
||||
@ -25,9 +24,8 @@ import Paragraph from 'src/components/layout/Paragraph'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import Span from 'src/components/layout/Span'
|
||||
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { safeNeedsUpdate } from 'src/logic/safe/utils/safeVersion'
|
||||
import { grantedSelector } from 'src/routes/safe/container/selector'
|
||||
import { safeOwnersSelector } from 'src/routes/safe/store/selectors'
|
||||
import { safeNeedsUpdateSelector, safeOwnersSelector } from 'src/routes/safe/store/selectors'
|
||||
|
||||
export const OWNERS_SETTINGS_TAB_TEST_ID = 'owner-settings-tab'
|
||||
|
||||
@ -36,10 +34,13 @@ const INITIAL_STATE = {
|
||||
menuOptionIndex: 1,
|
||||
}
|
||||
|
||||
const Settings = (props) => {
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
const classes = useStyles()
|
||||
const [state, setState] = useState(INITIAL_STATE)
|
||||
const owners = useSelector(safeOwnersSelector)
|
||||
const needsUpdate = useSelector(safeNeedsUpdate)
|
||||
const needsUpdate = useSelector(safeNeedsUpdateSelector)
|
||||
const granted = useSelector(grantedSelector)
|
||||
const addressBook = useSelector(getAddressBook)
|
||||
|
||||
@ -56,7 +57,6 @@ const Settings = (props) => {
|
||||
}
|
||||
|
||||
const { menuOptionIndex, showRemoveSafe } = state
|
||||
const { classes } = props
|
||||
|
||||
return !owners ? (
|
||||
<Loader />
|
||||
@ -73,7 +73,6 @@ const Settings = (props) => {
|
||||
<Col className={classes.menuWrapper} layout="column">
|
||||
<Block className={classes.menu}>
|
||||
<Row className={cn(classes.menuOption, menuOptionIndex === 1 && classes.active)} onClick={handleChange(1)}>
|
||||
<SafeDetailsIcon />
|
||||
<Badge
|
||||
badgeContent=" "
|
||||
color="error"
|
||||
@ -81,7 +80,13 @@ const Settings = (props) => {
|
||||
style={{ paddingRight: '10px' }}
|
||||
variant="dot"
|
||||
>
|
||||
Safe details
|
||||
<IconText
|
||||
iconSize="sm"
|
||||
textSize="xl"
|
||||
iconType="info"
|
||||
text="Safe Details"
|
||||
color={menuOptionIndex === 1 ? 'primary' : 'secondary'}
|
||||
/>
|
||||
</Badge>
|
||||
</Row>
|
||||
<Hairline className={classes.hairline} />
|
||||
@ -90,16 +95,36 @@ const Settings = (props) => {
|
||||
onClick={handleChange(2)}
|
||||
testId={OWNERS_SETTINGS_TAB_TEST_ID}
|
||||
>
|
||||
<OwnersIcon />
|
||||
Owners
|
||||
<IconText
|
||||
iconSize="sm"
|
||||
textSize="xl"
|
||||
iconType="owners"
|
||||
text="Owners"
|
||||
color={menuOptionIndex === 2 ? 'primary' : 'secondary'}
|
||||
/>
|
||||
<Paragraph className={classes.counter} size="xs">
|
||||
{owners.size}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Hairline className={classes.hairline} />
|
||||
<Row className={cn(classes.menuOption, menuOptionIndex === 3 && classes.active)} onClick={handleChange(3)}>
|
||||
<RequiredConfirmationsIcon />
|
||||
Policies
|
||||
<IconText
|
||||
iconSize="sm"
|
||||
textSize="xl"
|
||||
iconType="requiredConfirmations"
|
||||
text="Policies"
|
||||
color={menuOptionIndex === 3 ? 'primary' : 'secondary'}
|
||||
/>
|
||||
</Row>
|
||||
<Hairline className={classes.hairline} />
|
||||
<Row className={cn(classes.menuOption, menuOptionIndex === 4 && classes.active)} onClick={handleChange(4)}>
|
||||
<IconText
|
||||
iconSize="sm"
|
||||
textSize="xl"
|
||||
iconType="settingsTool"
|
||||
text="Advanced"
|
||||
color={menuOptionIndex === 4 ? 'primary' : 'secondary'}
|
||||
/>
|
||||
</Row>
|
||||
<Hairline className={classes.hairline} />
|
||||
</Block>
|
||||
@ -109,6 +134,7 @@ const Settings = (props) => {
|
||||
{menuOptionIndex === 1 && <SafeDetails />}
|
||||
{menuOptionIndex === 2 && <ManageOwners addressBook={addressBook} granted={granted} owners={owners} />}
|
||||
{menuOptionIndex === 3 && <ThresholdSettings />}
|
||||
{menuOptionIndex === 4 && <Advanced />}
|
||||
</Block>
|
||||
</Col>
|
||||
</Block>
|
||||
@ -116,4 +142,4 @@ const Settings = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles as any)(Settings)
|
||||
export default Settings
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
import {
|
||||
background,
|
||||
bolderFont,
|
||||
@ -11,7 +13,7 @@ import {
|
||||
xs,
|
||||
} from 'src/theme/variables'
|
||||
|
||||
export const styles = () => ({
|
||||
export const styles = createStyles({
|
||||
root: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: sm,
|
||||
@ -31,7 +33,7 @@ export const styles = () => ({
|
||||
menuWrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexGrow: '0',
|
||||
flexGrow: 0,
|
||||
maxWidth: '100%',
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
@ -43,7 +45,7 @@ export const styles = () => ({
|
||||
borderBottom: `solid 2px ${border}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexGrow: '1',
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
|
||||
@ -59,8 +61,8 @@ export const styles = () => ({
|
||||
borderRight: `solid 1px ${border}`,
|
||||
boxSizing: 'border-box',
|
||||
cursor: 'pointer',
|
||||
flexGrow: '1',
|
||||
flexShrink: '1',
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
fontSize: '13px',
|
||||
justifyContent: 'center',
|
||||
lineHeight: '1.2',
|
||||
@ -113,7 +115,7 @@ export const styles = () => ({
|
||||
},
|
||||
},
|
||||
container: {
|
||||
flexGrow: '1',
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
},
|
||||
|
@ -147,6 +147,7 @@ const ApproveTxModal = ({
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={approveAndExecute} color="primary" onChange={handleExecuteCheckbox} />}
|
||||
label="Execute transaction"
|
||||
data-testid="execute-checkbox"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -144,9 +144,16 @@ const OwnersColumn = ({
|
||||
|
||||
return (
|
||||
<Col className={classes.rightCol} layout="block" xs={6}>
|
||||
<Block className={cn(classes.ownerListTitle, (thresholdReached || tx.isExecuted) && classes.ownerListTitleDone)}>
|
||||
<Block
|
||||
className={cn(classes.ownerListTitle, (thresholdReached || tx.isExecuted) && classes.ownerListTitleDone)}
|
||||
data-testid={`confirmed-${tx.confirmations.size}-out-of-${txThreshold}`}
|
||||
>
|
||||
<div className={classes.circleState}>
|
||||
<Img alt="" src={thresholdReached || tx.isExecuted ? CheckLargeFilledGreenCircle : ConfirmLargeGreenCircle} />
|
||||
<Img
|
||||
alt=""
|
||||
src={thresholdReached || tx.isExecuted ? CheckLargeFilledGreenCircle : ConfirmLargeGreenCircle}
|
||||
data-testid={thresholdReached || tx.isExecuted ? 'confirmed-tx-check' : 'not-confirmed-tx-check'}
|
||||
/>
|
||||
</div>
|
||||
{tx.isExecuted
|
||||
? `Confirmed [${tx.confirmations.size}/${tx.confirmations.size}]`
|
||||
@ -169,6 +176,7 @@ const OwnersColumn = ({
|
||||
classes.ownerListTitle,
|
||||
(cancelThresholdReached || cancelTx.isExecuted) && classes.ownerListTitleCancelDone,
|
||||
)}
|
||||
data-testid={`rejected-${cancelTx.confirmations.size}-out-of-${cancelThreshold}`}
|
||||
>
|
||||
<div
|
||||
className={cn(classes.verticalLine, tx.isExecuted ? classes.verticalLineDone : classes.verticalLinePending)}
|
||||
|
@ -53,7 +53,7 @@ const RejectTxModal = ({ classes, closeSnackbar, enqueueSnackbar, isOpen, onClos
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: safeAddress,
|
||||
valueInWei: 0,
|
||||
valueInWei: '0',
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
|
@ -10,7 +10,7 @@ import Bold from 'src/components/layout/Bold'
|
||||
import LinkWithRef from 'src/components/layout/Link'
|
||||
import Paragraph from 'src/components/layout/Paragraph'
|
||||
import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors'
|
||||
import { SAFE_METHODS_NAMES } from 'src/logic/contracts/methodIds'
|
||||
import { SAFE_METHODS_NAMES, SafeMethods } from 'src/logic/contracts/methodIds'
|
||||
import { shortVersionOf } from 'src/logic/wallets/ethAddresses'
|
||||
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
|
||||
import { getTxAmount } from 'src/routes/safe/components/Transactions/TxsTable/columns'
|
||||
@ -23,6 +23,8 @@ export const TRANSACTIONS_DESC_CHANGE_THRESHOLD_TEST_ID = 'tx-description-change
|
||||
export const TRANSACTIONS_DESC_SEND_TEST_ID = 'tx-description-send'
|
||||
export const TRANSACTIONS_DESC_CUSTOM_VALUE_TEST_ID = 'tx-description-custom-value'
|
||||
export const TRANSACTIONS_DESC_CUSTOM_DATA_TEST_ID = 'tx-description-custom-data'
|
||||
export const TRANSACTIONS_DESC_ADD_MODULE_TEST_ID = 'tx-description-add-module'
|
||||
export const TRANSACTIONS_DESC_REMOVE_MODULE_TEST_ID = 'tx-description-remove-module'
|
||||
export const TRANSACTIONS_DESC_NO_DATA = 'tx-description-no-data'
|
||||
|
||||
export const styles = () => ({
|
||||
@ -43,7 +45,12 @@ export const styles = () => ({
|
||||
},
|
||||
})
|
||||
|
||||
const TransferDescription = ({ amount = '', recipient }) => {
|
||||
interface TransferDescriptionProps {
|
||||
amount: string
|
||||
recipient: string
|
||||
}
|
||||
|
||||
const TransferDescription = ({ amount = '', recipient }: TransferDescriptionProps): React.ReactElement => {
|
||||
const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient))
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_SEND_TEST_ID}>
|
||||
@ -57,7 +64,11 @@ const TransferDescription = ({ amount = '', recipient }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const RemovedOwner = ({ removedOwner }) => {
|
||||
interface RemovedOwnerProps {
|
||||
removedOwner: string
|
||||
}
|
||||
|
||||
const RemovedOwner = ({ removedOwner }: RemovedOwnerProps): React.ReactElement => {
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, removedOwner))
|
||||
|
||||
return (
|
||||
@ -72,7 +83,11 @@ const RemovedOwner = ({ removedOwner }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const AddedOwner = ({ addedOwner }) => {
|
||||
interface AddedOwnerProps {
|
||||
addedOwner: string
|
||||
}
|
||||
|
||||
const AddedOwner = ({ addedOwner }: AddedOwnerProps): React.ReactElement => {
|
||||
const ownerChangedName = useSelector((state) => getNameFromAddressBook(state, addedOwner))
|
||||
|
||||
return (
|
||||
@ -87,7 +102,11 @@ const AddedOwner = ({ addedOwner }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const NewThreshold = ({ newThreshold }) => (
|
||||
interface NewThresholdProps {
|
||||
newThreshold: string
|
||||
}
|
||||
|
||||
const NewThreshold = ({ newThreshold }: NewThresholdProps): React.ReactElement => (
|
||||
<Block data-testid={TRANSACTIONS_DESC_CHANGE_THRESHOLD_TEST_ID}>
|
||||
<Bold>Change required confirmations:</Bold>
|
||||
<Paragraph noMargin size="md">
|
||||
@ -96,7 +115,43 @@ const NewThreshold = ({ newThreshold }) => (
|
||||
</Block>
|
||||
)
|
||||
|
||||
const SettingsDescription = ({ action, addedOwner, newThreshold, removedOwner }) => {
|
||||
interface AddModuleProps {
|
||||
module: string
|
||||
}
|
||||
|
||||
const AddModule = ({ module }: AddModuleProps): React.ReactElement => (
|
||||
<Block data-testid={TRANSACTIONS_DESC_ADD_MODULE_TEST_ID}>
|
||||
<Bold>Add module:</Bold>
|
||||
<EtherscanLink value={module} knownAddress={false} type="address" />
|
||||
</Block>
|
||||
)
|
||||
|
||||
interface RemoveModuleProps {
|
||||
module: string
|
||||
}
|
||||
|
||||
const RemoveModule = ({ module }: RemoveModuleProps): React.ReactElement => (
|
||||
<Block data-testid={TRANSACTIONS_DESC_REMOVE_MODULE_TEST_ID}>
|
||||
<Bold>Remove module:</Bold>
|
||||
<EtherscanLink value={module} knownAddress={false} type="address" />
|
||||
</Block>
|
||||
)
|
||||
|
||||
interface SettingsDescriptionProps {
|
||||
action: SafeMethods
|
||||
addedOwner?: string
|
||||
newThreshold?: string
|
||||
removedOwner?: string
|
||||
module?: string
|
||||
}
|
||||
|
||||
const SettingsDescription = ({
|
||||
action,
|
||||
addedOwner,
|
||||
newThreshold,
|
||||
removedOwner,
|
||||
module,
|
||||
}: SettingsDescriptionProps): React.ReactElement => {
|
||||
if (action === SAFE_METHODS_NAMES.REMOVE_OWNER && removedOwner && newThreshold) {
|
||||
return (
|
||||
<>
|
||||
@ -128,6 +183,14 @@ const SettingsDescription = ({ action, addedOwner, newThreshold, removedOwner })
|
||||
)
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.ENABLE_MODULE && module) {
|
||||
return <AddModule module={module} />
|
||||
}
|
||||
|
||||
if (action === SAFE_METHODS_NAMES.DISABLE_MODULE && module) {
|
||||
return <RemoveModule module={module} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Block data-testid={TRANSACTIONS_DESC_NO_DATA}>
|
||||
<Bold>No data available for current transaction</Bold>
|
||||
@ -207,6 +270,7 @@ const TxDescription = ({ classes, tx }) => {
|
||||
customTx,
|
||||
data,
|
||||
modifySettingsTx,
|
||||
module,
|
||||
newThreshold,
|
||||
recipient,
|
||||
removedOwner,
|
||||
@ -221,6 +285,7 @@ const TxDescription = ({ classes, tx }) => {
|
||||
addedOwner={addedOwner}
|
||||
newThreshold={newThreshold}
|
||||
removedOwner={removedOwner}
|
||||
module={module}
|
||||
/>
|
||||
)}
|
||||
{!upgradeTx && customTx && (
|
||||
|
@ -47,6 +47,14 @@ export const getTxData = (tx) => {
|
||||
txData.action = SAFE_METHODS_NAMES.SWAP_OWNER
|
||||
txData.removedOwner = oldOwner
|
||||
txData.addedOwner = newOwner
|
||||
} else if (tx.decodedParams[SAFE_METHODS_NAMES.ENABLE_MODULE]) {
|
||||
const { module } = tx.decodedParams[SAFE_METHODS_NAMES.ENABLE_MODULE]
|
||||
txData.action = SAFE_METHODS_NAMES.ENABLE_MODULE
|
||||
txData.module = module
|
||||
} else if (tx.decodedParams[SAFE_METHODS_NAMES.DISABLE_MODULE]) {
|
||||
const { module } = tx.decodedParams[SAFE_METHODS_NAMES.DISABLE_MODULE]
|
||||
txData.action = SAFE_METHODS_NAMES.DISABLE_MODULE
|
||||
txData.module = module
|
||||
}
|
||||
} else if (tx.multiSendTx) {
|
||||
txData.recipient = tx.recipient
|
||||
|
@ -42,7 +42,7 @@ const Status = ({ classes, status }) => {
|
||||
return (
|
||||
<Block className={`${classes.container} ${classes[status]}`}>
|
||||
{typeof Icon === 'object' ? Icon : <Img alt={statusToLabel[status]} src={Icon} style={statusIconStyle} />}
|
||||
<Paragraph className={classes.statusText} noMargin>
|
||||
<Paragraph className={classes.statusText} noMargin data-testid={`tx-status-${statusToLabel[status]}`}>
|
||||
{statusToLabel[status]}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
|
@ -14,7 +14,8 @@ describe('TxsTable Columns > getTxTableData', () => {
|
||||
const txRow = txTableData.first()
|
||||
|
||||
// Then
|
||||
expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toEqual(mockedCancelTransaction)
|
||||
// expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toEqual(mockedCancelTransaction)
|
||||
expect(txRow[TX_TABLE_RAW_CANCEL_TX_ID]).toBeUndefined()
|
||||
})
|
||||
it('should not include CancelTx object inside TxTableData', () => {
|
||||
// Given
|