Merge pull request #1126 from gnosis/development

Public Release: v2.7.0
This commit is contained in:
Mikhail Mikheev 2020-07-21 01:19:04 +04:00 committed by GitHub
commit 02c1b1a0fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
139 changed files with 2493 additions and 2018 deletions

10
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

View 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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
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
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" />
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
export const styles = () => ({
import { createStyles } from '@material-ui/core'
export const styles = createStyles({
itemOptionList: {
display: 'flex',
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -100,7 +100,6 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({
>
{(submitting, validating, rest, mutators) => {
setCallResults = mutators.setCallResults
return (
<>
<Block className={classes.formContainer}>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

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

View 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

View 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',
},
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -147,6 +147,7 @@ const ApproveTxModal = ({
<FormControlLabel
control={<Checkbox checked={approveAndExecute} color="primary" onChange={handleExecuteCheckbox} />}
label="Execute transaction"
data-testid="execute-checkbox"
/>
)}
</>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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