Merge pull request #129 from gnosis/79-tx-list
Feature #79: Transaction list
This commit is contained in:
commit
36aff894d5
17
.eslintrc
17
.eslintrc
|
@ -1,8 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["airbnb", "plugin:flowtype/recommended"],
|
||||||
"airbnb",
|
|
||||||
"plugin:flowtype/recommended"
|
|
||||||
],
|
|
||||||
"parser": "babel-eslint",
|
"parser": "babel-eslint",
|
||||||
"plugins": ["jest", "flowtype"],
|
"plugins": ["jest", "flowtype"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
@ -27,19 +24,23 @@
|
||||||
"import/extensions": 0,
|
"import/extensions": 0,
|
||||||
"import/prefer-default-export": 0,
|
"import/prefer-default-export": 0,
|
||||||
"jsx-a11y/label-has-for": 0,
|
"jsx-a11y/label-has-for": 0,
|
||||||
"indent": ["error", 2],
|
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||||
"no-console": ["error", { "allow": ["warn", "error"] }],
|
"no-console": ["error", { "allow": ["warn", "error"] }],
|
||||||
"flowtype/require-valid-file-annotation": [
|
"flowtype/require-valid-file-annotation": [
|
||||||
2,
|
2,
|
||||||
"always", {
|
"always",
|
||||||
|
{
|
||||||
"annotationStyle": "line"
|
"annotationStyle": "line"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"jsx-a11y/anchor-is-valid": [ "error", {
|
"jsx-a11y/anchor-is-valid": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
"components": ["Link"],
|
"components": ["Link"],
|
||||||
"specialLink": ["to", "hrefLeft", "hrefRight"],
|
"specialLink": ["to", "hrefLeft", "hrefRight"],
|
||||||
"aspects": ["noHref", "invalidHref", "preferButton"]
|
"aspects": ["noHref", "invalidHref", "preferButton"]
|
||||||
}],
|
}
|
||||||
|
],
|
||||||
"react/require-default-props": 0,
|
"react/require-default-props": 0,
|
||||||
"react/no-array-index-key": 0
|
"react/no-array-index-key": 0
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: Epic
|
||||||
|
about: Create an epic for the Gnosis Safe
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# What is this feature about? (1 sentence)
|
||||||
|
|
||||||
|
|
||||||
|
# Why is it needed? What is the value? For whom do we build it?
|
||||||
|
|
||||||
|
|
||||||
|
# High-level overview of the feature
|
||||||
|
-
|
||||||
|
|
||||||
|
# Screens
|
||||||
|
- Link to Google docs with screens. Template can be found on [Google drive](https://drive.google.com/drive/u/0/folders/1d7LH762wUBCSf2Shmb-f2O5OdAaQcOAW)
|
||||||
|
|
||||||
|
# Other things
|
||||||
|
-
|
|
@ -3,3 +3,4 @@ build_webpack/
|
||||||
build_storybook/
|
build_storybook/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
build/
|
build/
|
||||||
|
yarn-error.log
|
56
package.json
56
package.json
|
@ -32,29 +32,31 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gnosis.pm/safe-contracts": "^1.0.0",
|
"@gnosis.pm/safe-contracts": "^1.0.0",
|
||||||
"@gnosis.pm/util-contracts": "2.0.1",
|
"@gnosis.pm/util-contracts": "2.0.1",
|
||||||
"@material-ui/core": "4.1.3",
|
"@material-ui/core": "4.2.0",
|
||||||
"@material-ui/icons": "4.2.1",
|
"@material-ui/icons": "4.2.1",
|
||||||
|
"@testing-library/jest-dom": "^4.0.0",
|
||||||
"@welldone-software/why-did-you-render": "3.2.1",
|
"@welldone-software/why-did-you-render": "3.2.1",
|
||||||
"axios": "0.19.0",
|
"axios": "0.19.0",
|
||||||
"bignumber.js": "9.0.0",
|
"bignumber.js": "9.0.0",
|
||||||
"connected-react-router": "^6.3.1",
|
"connected-react-router": "6.5.2",
|
||||||
"final-form": "4.16.1",
|
"date-fns": "1.30.1",
|
||||||
|
"final-form": "4.18.2",
|
||||||
"history": "^4.7.2",
|
"history": "^4.7.2",
|
||||||
"immortal-db": "^1.0.2",
|
"immortal-db": "^1.0.2",
|
||||||
"immutable": "^4.0.0-rc.9",
|
"immutable": "^4.0.0-rc.9",
|
||||||
"material-ui-search-bar": "^1.0.0-beta.13",
|
"material-ui-search-bar": "^1.0.0-beta.13",
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||||
"qrcode.react": "^0.9.3",
|
"qrcode.react": "^0.9.3",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"react-final-form": "6.3.0",
|
"react-final-form": "6.3.0",
|
||||||
"react-final-form-listeners": "^1.0.2",
|
"react-final-form-listeners": "^1.0.2",
|
||||||
"react-hot-loader": "4.12.3",
|
"react-hot-loader": "4.12.7",
|
||||||
"react-infinite-scroll-component": "^4.5.2",
|
"react-infinite-scroll-component": "^4.5.2",
|
||||||
"react-redux": "7.1.0",
|
"react-redux": "7.1.0",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
"recompose": "^0.30.0",
|
"recompose": "^0.30.0",
|
||||||
"redux": "^4.0.1",
|
"redux": "4.0.4",
|
||||||
"redux-actions": "^2.3.0",
|
"redux-actions": "^2.3.0",
|
||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^2.2.0",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
|
@ -62,27 +64,27 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "7.5.0",
|
"@babel/cli": "7.5.0",
|
||||||
"@babel/core": "7.5.0",
|
"@babel/core": "7.5.4",
|
||||||
"@babel/plugin-proposal-class-properties": "7.5.0",
|
"@babel/plugin-proposal-class-properties": "7.5.0",
|
||||||
"@babel/plugin-proposal-decorators": "7.4.4",
|
"@babel/plugin-proposal-decorators": "7.4.4",
|
||||||
"@babel/plugin-proposal-do-expressions": "^7.0.0",
|
"@babel/plugin-proposal-do-expressions": "7.5.0",
|
||||||
"@babel/plugin-proposal-export-default-from": "^7.0.0",
|
"@babel/plugin-proposal-export-default-from": "7.5.2",
|
||||||
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
|
"@babel/plugin-proposal-export-namespace-from": "7.5.2",
|
||||||
"@babel/plugin-proposal-function-bind": "^7.0.0",
|
"@babel/plugin-proposal-function-bind": "^7.0.0",
|
||||||
"@babel/plugin-proposal-function-sent": "^7.0.0",
|
"@babel/plugin-proposal-function-sent": "7.5.0",
|
||||||
"@babel/plugin-proposal-json-strings": "^7.0.0",
|
"@babel/plugin-proposal-json-strings": "^7.0.0",
|
||||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.0.0",
|
"@babel/plugin-proposal-logical-assignment-operators": "^7.0.0",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4",
|
"@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4",
|
||||||
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
|
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
|
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
|
||||||
"@babel/plugin-proposal-pipeline-operator": "^7.0.0",
|
"@babel/plugin-proposal-pipeline-operator": "7.5.0",
|
||||||
"@babel/plugin-proposal-throw-expressions": "^7.0.0",
|
"@babel/plugin-proposal-throw-expressions": "^7.0.0",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||||
"@babel/plugin-syntax-import-meta": "^7.0.0",
|
"@babel/plugin-syntax-import-meta": "^7.0.0",
|
||||||
"@babel/plugin-transform-member-expression-literals": "^7.2.0",
|
"@babel/plugin-transform-member-expression-literals": "^7.2.0",
|
||||||
"@babel/plugin-transform-property-literals": "^7.2.0",
|
"@babel/plugin-transform-property-literals": "^7.2.0",
|
||||||
"@babel/polyfill": "7.4.4",
|
"@babel/polyfill": "7.4.4",
|
||||||
"@babel/preset-env": "7.5.0",
|
"@babel/preset-env": "7.5.4",
|
||||||
"@babel/preset-flow": "^7.0.0-beta.40",
|
"@babel/preset-flow": "^7.0.0-beta.40",
|
||||||
"@babel/preset-react": "^7.0.0-beta.40",
|
"@babel/preset-react": "^7.0.0-beta.40",
|
||||||
"@sambego/storybook-state": "^1.0.7",
|
"@sambego/storybook-state": "^1.0.7",
|
||||||
|
@ -90,8 +92,8 @@
|
||||||
"@storybook/addon-knobs": "5.1.9",
|
"@storybook/addon-knobs": "5.1.9",
|
||||||
"@storybook/addon-links": "5.1.9",
|
"@storybook/addon-links": "5.1.9",
|
||||||
"@storybook/react": "5.1.9",
|
"@storybook/react": "5.1.9",
|
||||||
"@testing-library/react": "^8.0.1",
|
"@testing-library/react": "8.0.5",
|
||||||
"autoprefixer": "9.6.0",
|
"autoprefixer": "9.6.1",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-eslint": "10.0.2",
|
"babel-eslint": "10.0.2",
|
||||||
"babel-jest": "24.8.0",
|
"babel-jest": "24.8.0",
|
||||||
|
@ -102,12 +104,12 @@
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"css-loader": "3.0.0",
|
"css-loader": "3.0.0",
|
||||||
"detect-port": "^1.2.2",
|
"detect-port": "^1.2.2",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "6.0.1",
|
||||||
"eslint-config-airbnb": "^17.1.0",
|
"eslint-config-airbnb": "17.1.1",
|
||||||
"eslint-plugin-flowtype": "3.11.1",
|
"eslint-plugin-flowtype": "3.11.1",
|
||||||
"eslint-plugin-import": "2.18.0",
|
"eslint-plugin-import": "2.18.0",
|
||||||
"eslint-plugin-jest": "22.7.2",
|
"eslint-plugin-jest": "22.9.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
"eslint-plugin-jsx-a11y": "6.2.3",
|
||||||
"eslint-plugin-react": "7.14.2",
|
"eslint-plugin-react": "7.14.2",
|
||||||
"ethereumjs-abi": "^0.6.7",
|
"ethereumjs-abi": "^0.6.7",
|
||||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||||
|
@ -117,25 +119,25 @@
|
||||||
"html-loader": "^0.5.5",
|
"html-loader": "^0.5.5",
|
||||||
"html-webpack-plugin": "^3.0.4",
|
"html-webpack-plugin": "^3.0.4",
|
||||||
"jest": "24.8.0",
|
"jest": "24.8.0",
|
||||||
"jest-dom": "^3.4.0",
|
"jest-dom": "4.0.0",
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
"mini-css-extract-plugin": "0.7.0",
|
"mini-css-extract-plugin": "0.8.0",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"postcss-mixins": "^6.2.0",
|
"postcss-mixins": "^6.2.0",
|
||||||
"postcss-simple-vars": "^5.0.2",
|
"postcss-simple-vars": "^5.0.2",
|
||||||
"pre-commit": "^1.2.2",
|
"pre-commit": "^1.2.2",
|
||||||
"prettier-eslint-cli": "5.0.0",
|
"prettier-eslint-cli": "5.0.0",
|
||||||
"run-with-testrpc": "0.3.1",
|
"run-with-testrpc": "0.3.1",
|
||||||
"storybook-host": "^5.0.3",
|
"storybook-host": "5.1.0",
|
||||||
"storybook-router": "^0.3.3",
|
"storybook-router": "^0.3.3",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"truffle": "5.0.26",
|
"truffle": "5.0.27",
|
||||||
"truffle-contract": "4.0.23",
|
"truffle-contract": "4.0.24",
|
||||||
"truffle-solidity-loader": "0.1.25",
|
"truffle-solidity-loader": "0.1.26",
|
||||||
"uglifyjs-webpack-plugin": "2.1.3",
|
"uglifyjs-webpack-plugin": "2.1.3",
|
||||||
"webpack": "4.35.2",
|
"webpack": "4.35.3",
|
||||||
"webpack-bundle-analyzer": "3.3.2",
|
"webpack-bundle-analyzer": "3.3.2",
|
||||||
"webpack-cli": "3.3.5",
|
"webpack-cli": "3.3.6",
|
||||||
"webpack-dev-server": "3.7.2",
|
"webpack-dev-server": "3.7.2",
|
||||||
"webpack-manifest-plugin": "^2.0.0-rc.2"
|
"webpack-manifest-plugin": "^2.0.0-rc.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
|
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
||||||
|
import { shortVersionOf } from '~/logic/wallets/ethAddresses'
|
||||||
|
import { secondary } from '~/theme/variables'
|
||||||
|
import { networkSelector } from '~/logic/wallets/store/selectors'
|
||||||
|
|
||||||
|
const openIconStyle = {
|
||||||
|
height: '13px',
|
||||||
|
color: secondary,
|
||||||
|
}
|
||||||
|
|
||||||
|
type EtherscanLinkProps = {
|
||||||
|
type: 'tx' | 'address',
|
||||||
|
value: string,
|
||||||
|
currentNetwork: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EtherscanLink = ({ type, value, currentNetwork }: EtherscanLinkProps) => (
|
||||||
|
<a href={getEtherScanLink(type, value, currentNetwork)} target="_blank" rel="noopener noreferrer">
|
||||||
|
{shortVersionOf(value, 4)}
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default connect<Object, Object, ?Function, ?Object>(
|
||||||
|
state => ({ currentNetwork: networkSelector(state) }),
|
||||||
|
null,
|
||||||
|
)(EtherscanLink)
|
|
@ -9,7 +9,7 @@ import styles from './index.scss'
|
||||||
const Footer = () => (
|
const Footer = () => (
|
||||||
<Block className={styles.footer}>
|
<Block className={styles.footer}>
|
||||||
<Link to={WELCOME_ADDRESS}>
|
<Link to={WELCOME_ADDRESS}>
|
||||||
<Paragraph size="sm" color="primary" noMargin>Welcome</Paragraph>
|
<Paragraph size="sm" color="primary" noMargin>Add Safe</Paragraph>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to={SAFELIST_ADDRESS}>
|
<Link to={SAFELIST_ADDRESS}>
|
||||||
<Paragraph size="sm" color="primary" noMargin>Safe List</Paragraph>
|
<Paragraph size="sm" color="primary" noMargin>Safe List</Paragraph>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import Block from '~/components/layout/Block'
|
|
||||||
import Dot from '@material-ui/icons/FiberManualRecord'
|
import Dot from '@material-ui/icons/FiberManualRecord'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
import Img from '~/components/layout/Img'
|
import Img from '~/components/layout/Img'
|
||||||
import { fancy, border, warning } from '~/theme/variables'
|
import { fancy, border, warning } from '~/theme/variables'
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,11 @@ import * as React from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Dot from '@material-ui/icons/FiberManualRecord'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Link from '~/components/layout/Link'
|
import Link from '~/components/layout/Link'
|
||||||
import Button from '~/components/layout/Button'
|
import Button from '~/components/layout/Button'
|
||||||
import Identicon from '~/components/Identicon'
|
import Identicon from '~/components/Identicon'
|
||||||
import Dot from '@material-ui/icons/FiberManualRecord'
|
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import Img from '~/components/layout/Img'
|
import Img from '~/components/layout/Img'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
|
@ -123,7 +123,7 @@ const UserDetails = ({
|
||||||
{address}
|
{address}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
{userAddress && (
|
{userAddress && (
|
||||||
<Link className={classes.open} to={getEtherScanLink(userAddress, network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', userAddress, network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -5,11 +5,18 @@ import { toDataUrl } from './blockies'
|
||||||
type Props = {
|
type Props = {
|
||||||
address: string,
|
address: string,
|
||||||
diameter: number,
|
diameter: number,
|
||||||
|
className?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdenticonRef = { current: null | HTMLDivElement }
|
type IdenticonRef = { current: null | HTMLDivElement }
|
||||||
|
|
||||||
export default class Identicon extends React.PureComponent<Props> {
|
export default class Identicon extends React.PureComponent<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
className: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
identicon: IdenticonRef
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
@ -55,13 +62,13 @@ export default class Identicon extends React.PureComponent<Props> {
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
identicon: IdenticonRef
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const style = this.getStyleFrom(this.props.diameter)
|
const { diameter, className } = this.props
|
||||||
|
const style = this.getStyleFrom(diameter)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style} ref={this.identicon} />
|
<div className={className} style={style} ref={this.identicon} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ type Props<K> = {
|
||||||
children: Function,
|
children: Function,
|
||||||
size: number,
|
size: number,
|
||||||
defaultFixed?: boolean,
|
defaultFixed?: boolean,
|
||||||
|
defaultOrder?: 'desc' | 'asc',
|
||||||
noBorder: boolean,
|
noBorder: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,21 +60,48 @@ const styles = {
|
||||||
|
|
||||||
const FIXED_HEIGHT = 49
|
const FIXED_HEIGHT = 49
|
||||||
|
|
||||||
|
const backProps = {
|
||||||
|
'aria-label': 'Previous Page',
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextProps = {
|
||||||
|
'aria-label': 'Next Page',
|
||||||
|
}
|
||||||
|
|
||||||
class GnoTable<K> extends React.Component<Props<K>, State> {
|
class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
state = {
|
state = {
|
||||||
page: 0,
|
page: 0,
|
||||||
order: 'asc',
|
order: undefined,
|
||||||
orderBy: undefined,
|
orderBy: undefined,
|
||||||
fixed: undefined,
|
fixed: undefined,
|
||||||
orderProp: false,
|
orderProp: false,
|
||||||
rowsPerPage: 5,
|
rowsPerPage: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { defaultOrderBy, columns } = this.props
|
||||||
|
|
||||||
|
if (defaultOrderBy && columns) {
|
||||||
|
const defaultOrderCol = columns.find(({ id }) => id === defaultOrderBy)
|
||||||
|
|
||||||
|
if (defaultOrderCol.order) {
|
||||||
|
this.setState({
|
||||||
|
orderProp: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onSort = (newOrderBy: string, orderProp: boolean) => {
|
onSort = (newOrderBy: string, orderProp: boolean) => {
|
||||||
const { order, orderBy } = this.state
|
const { order, orderBy } = this.state
|
||||||
|
const { defaultOrder } = this.props
|
||||||
let newOrder = 'desc'
|
let newOrder = 'desc'
|
||||||
|
|
||||||
if (orderBy === newOrderBy && order === 'desc') {
|
// if table was previously sorted by the user
|
||||||
|
if (order && orderBy === newOrderBy && order === 'desc') {
|
||||||
|
newOrder = 'asc'
|
||||||
|
} else if (!order && defaultOrder === 'desc') {
|
||||||
|
// if it was not sorted and defaultOrder is used
|
||||||
newOrder = 'asc'
|
newOrder = 'asc'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,29 +128,22 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
data, label, columns, classes, children, size, defaultOrderBy, defaultFixed, noBorder,
|
data, label, columns, classes, children, size, defaultOrderBy, defaultOrder, defaultFixed, noBorder,
|
||||||
} = this.props
|
} = this.props
|
||||||
const {
|
const {
|
||||||
order, orderBy, page, orderProp, rowsPerPage, fixed,
|
order, orderBy, page, orderProp, rowsPerPage, fixed,
|
||||||
} = this.state
|
} = this.state
|
||||||
const orderByParam = orderBy || defaultOrderBy
|
const orderByParam = orderBy || defaultOrderBy
|
||||||
|
const orderParam = order || defaultOrder
|
||||||
const fixedParam = typeof fixed !== 'undefined' ? fixed : !!defaultFixed
|
const fixedParam = typeof fixed !== 'undefined' ? fixed : !!defaultFixed
|
||||||
|
|
||||||
const backProps = {
|
|
||||||
'aria-label': 'Previous Page',
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextProps = {
|
|
||||||
'aria-label': 'Next Page',
|
|
||||||
}
|
|
||||||
|
|
||||||
const paginationClasses = {
|
const paginationClasses = {
|
||||||
selectRoot: classes.selectRoot,
|
selectRoot: classes.selectRoot,
|
||||||
root: !noBorder && classes.paginationRoot,
|
root: !noBorder && classes.paginationRoot,
|
||||||
input: classes.white,
|
input: classes.white,
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedData = stableSort(data, getSorting(order, orderByParam, orderProp), fixedParam).slice(
|
const sortedData = stableSort(data, getSorting(orderParam, orderByParam, orderProp), fixedParam).slice(
|
||||||
page * rowsPerPage,
|
page * rowsPerPage,
|
||||||
page * rowsPerPage + rowsPerPage,
|
page * rowsPerPage + rowsPerPage,
|
||||||
)
|
)
|
||||||
|
@ -163,4 +184,8 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GnoTable.defaultProps = {
|
||||||
|
defaultOrder: 'asc',
|
||||||
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(GnoTable)
|
export default withStyles(styles)(GnoTable)
|
||||||
|
|
|
@ -1,31 +1,27 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import Button from '@material-ui/core/Button'
|
import Button from '@material-ui/core/Button'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
|
||||||
|
|
||||||
const styles = {
|
|
||||||
root: {
|
|
||||||
borderRadius: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
minWidth?: number,
|
minWidth?: number,
|
||||||
minHeight?: number,
|
minHeight?: number,
|
||||||
testId: string,
|
rounded?: boolean,
|
||||||
|
testId?: string,
|
||||||
|
style?: Object,
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateStyleBased = (minWidth, minHeight) => ({
|
const calculateStyleBased = (minWidth, minHeight, rounded) => ({
|
||||||
minWidth: minWidth && `${minWidth}px`,
|
minWidth: minWidth && `${minWidth}px`,
|
||||||
minHeight: minHeight && `${minHeight}px`,
|
minHeight: minHeight && `${minHeight}px`,
|
||||||
|
borderRadius: rounded ? '4px' : 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const GnoButton = ({
|
const GnoButton = ({
|
||||||
minWidth, minHeight, testId = '', ...props
|
minWidth, minHeight = 27, testId = '', rounded, style = {}, ...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const style = calculateStyleBased(minWidth, minHeight)
|
const calculatedStyle = calculateStyleBased(minWidth, minHeight, rounded)
|
||||||
|
|
||||||
return <Button style={style} data-testid={testId} {...props} />
|
return <Button style={{ ...calculatedStyle, ...style }} data-testid={testId} {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(GnoButton)
|
export default GnoButton
|
||||||
|
|
|
@ -7,7 +7,7 @@ import styles from './index.scss'
|
||||||
const cx = cn.bind(styles)
|
const cx = cn.bind(styles)
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: 'button' | 'submit' | 'reset',
|
type?: 'button' | 'submit' | 'reset',
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl',
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl',
|
||||||
weight?: 'light' | 'regular' | 'bolder' | 'bold',
|
weight?: 'light' | 'regular' | 'bolder' | 'bold',
|
||||||
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled' | 'error',
|
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled' | 'error',
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { PureComponent } from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.Node
|
children: React.Node,
|
||||||
}
|
}
|
||||||
|
|
||||||
class Span extends PureComponent<Props> {
|
class Span extends React.PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { children, ...props } = this.props
|
const { children, ...props } = this.props
|
||||||
|
|
||||||
return (
|
return <span {...props}>{children}</span>
|
||||||
<span {...props}>
|
|
||||||
{ children }
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,10 @@ import {
|
||||||
} from '~/config/names'
|
} from '~/config/names'
|
||||||
|
|
||||||
const devConfig = {
|
const devConfig = {
|
||||||
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
[TX_SERVICE_HOST]: 'https://safe-transaction-service.dev.gnosisdev.com/api/v1/',
|
||||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||||
[SIGNATURES_VIA_METAMASK]: false,
|
[SIGNATURES_VIA_METAMASK]: false,
|
||||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default devConfig
|
export default devConfig
|
||||||
|
|
|
@ -7,10 +7,10 @@ import {
|
||||||
} from '~/config/names'
|
} from '~/config/names'
|
||||||
|
|
||||||
const prodConfig = {
|
const prodConfig = {
|
||||||
[TX_SERVICE_HOST]: 'https://safe-transaction-history.dev.gnosisdev.com/api/v1/',
|
[TX_SERVICE_HOST]: 'https://safe-transaction-service.dev.gnosisdev.com/api/v1/',
|
||||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||||
[SIGNATURES_VIA_METAMASK]: false,
|
[SIGNATURES_VIA_METAMASK]: false,
|
||||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default prodConfig
|
export default prodConfig
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from '~/config/names'
|
} from '~/config/names'
|
||||||
|
|
||||||
const testConfig = {
|
const testConfig = {
|
||||||
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
[TX_SERVICE_HOST]: 'https://safe-transaction-service.dev.gnosisdev.com/api/v1/',
|
||||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||||
[SIGNATURES_VIA_METAMASK]: false,
|
[SIGNATURES_VIA_METAMASK]: false,
|
||||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
// @flow
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
|
||||||
|
// SAFE METHODS TO ITS ID
|
||||||
|
// https://github.com/gnosis/safe-contracts/blob/development/test/safeMethodNaming.js
|
||||||
|
// https://github.com/gnosis/safe-contracts/blob/development/contracts/GnosisSafe.sol
|
||||||
|
// [
|
||||||
|
// { name: "addOwnerWithThreshold", id: "0x0d582f13" },
|
||||||
|
// { name: "DOMAIN_SEPARATOR_TYPEHASH", id: "0x1db61b54" },
|
||||||
|
// { name: "isOwner", id: "0x2f54bf6e" },
|
||||||
|
// { name: "execTransactionFromModule", id: "0x468721a7" },
|
||||||
|
// { name: "signedMessages", id: "0x5ae6bd37" },
|
||||||
|
// { name: "enableModule", id: "0x610b5925" },
|
||||||
|
// { name: "changeThreshold", id: "0x694e80c3" },
|
||||||
|
// { name: "approvedHashes", id: "0x7d832974" },
|
||||||
|
// { name: "changeMasterCopy", id: "0x7de7edef" },
|
||||||
|
// { name: "SENTINEL_MODULES", id: "0x85e332cd" },
|
||||||
|
// { name: "SENTINEL_OWNERS", id: "0x8cff6355" },
|
||||||
|
// { name: "getOwners", id: "0xa0e67e2b" },
|
||||||
|
// { name: "NAME", id: "0xa3f4df7e" },
|
||||||
|
// { name: "nonce", id: "0xaffed0e0" },
|
||||||
|
// { name: "getModules", id: "0xb2494df3" },
|
||||||
|
// { name: "SAFE_MSG_TYPEHASH", id: "0xc0856ffc" },
|
||||||
|
// { name: "SAFE_TX_TYPEHASH", id: "0xccafc387" },
|
||||||
|
// { name: "disableModule", id: "0xe009cfde" },
|
||||||
|
// { name: "swapOwner", id: "0xe318b52b" },
|
||||||
|
// { name: "getThreshold", id: "0xe75235b8" },
|
||||||
|
// { name: "domainSeparator", id: "0xf698da25" },
|
||||||
|
// { name: "removeOwner", id: "0xf8dc5dd9" },
|
||||||
|
// { name: "VERSION", id: "0xffa1ad74" },
|
||||||
|
// { name: "setup", id: "0xa97ab18a" },
|
||||||
|
// { name: "execTransaction", id: "0x6a761202" },
|
||||||
|
// { name: "requiredTxGas", id: "0xc4ca3a9c" },
|
||||||
|
// { name: "approveHash", id: "0xd4d9bdcd" },
|
||||||
|
// { name: "signMessage", id: "0x85a5affe" },
|
||||||
|
// { name: "isValidSignature", id: "0x20c13b0b" },
|
||||||
|
// { name: "getMessageHash", id: "0x0a1028c4" },
|
||||||
|
// { name: "encodeTransactionData", id: "0xe86637db" },
|
||||||
|
// { name: "getTransactionHash", id: "0xd8d11f78" }
|
||||||
|
// ]
|
||||||
|
|
||||||
|
const METHOD_TO_ID = {
|
||||||
|
'0xe318b52b': 'swapOwner',
|
||||||
|
'0x0d582f13': 'addOwnerWithThreshold',
|
||||||
|
'0xf8dc5dd9': 'removeOwner',
|
||||||
|
'0x694e80c3': 'changeThreshold',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decodeParamsFromSafeMethod = async (data: string) => {
|
||||||
|
const web3 = await getWeb3()
|
||||||
|
const [methodId, params] = [data.slice(0, 10), data.slice(10)]
|
||||||
|
|
||||||
|
switch (methodId) {
|
||||||
|
// swapOwner
|
||||||
|
case '0xe318b52b':
|
||||||
|
return {
|
||||||
|
methodName: METHOD_TO_ID[methodId],
|
||||||
|
args: web3.eth.abi.decodeParameters(['address', 'address', 'address'], params),
|
||||||
|
}
|
||||||
|
|
||||||
|
// addOwnerWithThreshold
|
||||||
|
case '0x0d582f13':
|
||||||
|
return {
|
||||||
|
methodName: METHOD_TO_ID[methodId],
|
||||||
|
args: web3.eth.abi.decodeParameters(['address', 'uint'], params),
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeOwner
|
||||||
|
case '0xf8dc5dd9':
|
||||||
|
return {
|
||||||
|
methodName: METHOD_TO_ID[methodId],
|
||||||
|
args: web3.eth.abi.decodeParameters(['address', 'address', 'uint'], params),
|
||||||
|
}
|
||||||
|
|
||||||
|
// changeThreshold
|
||||||
|
case '0x694e80c3':
|
||||||
|
return {
|
||||||
|
methodName: METHOD_TO_ID[methodId],
|
||||||
|
args: web3.eth.abi.decodeParameters(['uint'], params),
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import contract from 'truffle-contract'
|
import contract from 'truffle-contract'
|
||||||
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
|
||||||
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/ProxyFactory.json'
|
import ProxyFactorySol from '@gnosis.pm/safe-contracts/build/contracts/ProxyFactory.json'
|
||||||
|
import GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
||||||
import { ensureOnce } from '~/utils/singleton'
|
import { ensureOnce } from '~/utils/singleton'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions'
|
import { calculateGasOf, calculateGasPrice } from '~/logic/wallets/ethTransactions'
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
|
||||||
import { getTxServiceUriFrom, getTxServiceHost } from '~/config'
|
|
||||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
|
||||||
|
|
||||||
export type TxServiceType = 'confirmation' | 'execution' | 'initialised'
|
|
||||||
export type Operation = 0 | 1 | 2
|
|
||||||
|
|
||||||
const calculateBodyFrom = async (
|
|
||||||
safeAddress: string,
|
|
||||||
to: string,
|
|
||||||
valueInWei: number,
|
|
||||||
data: string,
|
|
||||||
operation: Operation,
|
|
||||||
nonce: number,
|
|
||||||
transactionHash: string,
|
|
||||||
sender: string,
|
|
||||||
type: TxServiceType,
|
|
||||||
) => {
|
|
||||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
|
||||||
const contractTransactionHash = await gnosisSafe.getTransactionHash(
|
|
||||||
to,
|
|
||||||
valueInWei,
|
|
||||||
data,
|
|
||||||
operation,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
nonce,
|
|
||||||
)
|
|
||||||
|
|
||||||
return JSON.stringify({
|
|
||||||
to: getWeb3().toChecksumAddress(to),
|
|
||||||
value: valueInWei,
|
|
||||||
data,
|
|
||||||
operation,
|
|
||||||
nonce,
|
|
||||||
safeTxGas: 0,
|
|
||||||
dataGas: 0,
|
|
||||||
gasPrice: 0,
|
|
||||||
gasToken: null,
|
|
||||||
refundReceiver: null,
|
|
||||||
contractTransactionHash,
|
|
||||||
transactionHash,
|
|
||||||
sender: getWeb3().toChecksumAddress(sender),
|
|
||||||
type,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const buildTxServiceUrlFrom = (safeAddress: string) => {
|
|
||||||
const host = getTxServiceHost()
|
|
||||||
const address = getWeb3().toChecksumAddress(safeAddress)
|
|
||||||
const base = getTxServiceUriFrom(address)
|
|
||||||
return `${host}${base}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const submitOperation = async (
|
|
||||||
safeAddress: string,
|
|
||||||
to: string,
|
|
||||||
valueInWei: number,
|
|
||||||
data: string,
|
|
||||||
operation: Operation,
|
|
||||||
nonce: number,
|
|
||||||
txHash: string,
|
|
||||||
sender: string,
|
|
||||||
type: TxServiceType,
|
|
||||||
) => {
|
|
||||||
const url = buildTxServiceUrlFrom(safeAddress)
|
|
||||||
const headers = {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
const body = await calculateBodyFrom(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, type)
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.status !== 202) {
|
|
||||||
return Promise.reject(new Error('Error submitting the transaction'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
|
@ -3,3 +3,4 @@ export * from './gas'
|
||||||
export * from './send'
|
export * from './send'
|
||||||
export * from './safeBlockchainOperations'
|
export * from './safeBlockchainOperations'
|
||||||
export * from './safeTxSignerEIP712'
|
export * from './safeTxSignerEIP712'
|
||||||
|
export * from './txHistory'
|
||||||
|
|
|
@ -1,60 +1,14 @@
|
||||||
// @flow
|
// // @flow
|
||||||
import { List } from 'immutable'
|
// import { List } from 'immutable'
|
||||||
import { calculateGasOf, checkReceiptStatus, calculateGasPrice } from '~/logic/wallets/ethTransactions'
|
// import { calculateGasOf, checkReceiptStatus, calculateGasPrice } from '~/logic/wallets/ethTransactions'
|
||||||
import { type Operation, submitOperation } from '~/logic/safe/safeTxHistory'
|
// import { type Operation, saveTxToHistory } from '~/logic/safe/transactions'
|
||||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
// import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
import { buildSignaturesFrom } from '~/logic/safe/safeTxSigner'
|
// import { buildSignaturesFrom } from '~/logic/safe/safeTxSigner'
|
||||||
import { generateMetamaskSignature, generateTxGasEstimateFrom, estimateDataGas } from '~/logic/safe/transactions'
|
// import { generateMetamaskSignature, generateTxGasEstimateFrom, estimateDataGas } from '~/logic/safe/transactions'
|
||||||
import { storeSignature, getSignaturesFrom } from '~/utils/storage/signatures'
|
// import { storeSignature, getSignaturesFrom } from '~/utils/storage/signatures'
|
||||||
import { signaturesViaMetamask } from '~/config'
|
// import { signaturesViaMetamask } from '~/config'
|
||||||
|
|
||||||
export const approveTransaction = async (
|
// export const approveTransaction = async (
|
||||||
safeAddress: string,
|
|
||||||
to: string,
|
|
||||||
valueInWei: number,
|
|
||||||
data: string,
|
|
||||||
operation: Operation,
|
|
||||||
nonce: number,
|
|
||||||
sender: string,
|
|
||||||
) => {
|
|
||||||
const gasPrice = await calculateGasPrice()
|
|
||||||
|
|
||||||
if (signaturesViaMetamask()) {
|
|
||||||
// return executeTransaction(safeAddress, to, valueInWei, data, operation, nonce, sender)
|
|
||||||
const safe = await getGnosisSafeInstanceAt(safeAddress)
|
|
||||||
const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
|
|
||||||
const signature = await generateMetamaskSignature(
|
|
||||||
safe,
|
|
||||||
safeAddress,
|
|
||||||
sender,
|
|
||||||
to,
|
|
||||||
valueInWei,
|
|
||||||
nonce,
|
|
||||||
data,
|
|
||||||
operation,
|
|
||||||
txGasEstimate,
|
|
||||||
)
|
|
||||||
storeSignature(safeAddress, nonce, signature)
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
|
||||||
const contractTxHash = await gnosisSafe.getTransactionHash(to, valueInWei, data, operation, 0, 0, 0, 0, 0, nonce)
|
|
||||||
|
|
||||||
const approveData = gnosisSafe.contract.methods.approveHash(contractTxHash).encodeABI()
|
|
||||||
const gas = await calculateGasOf(approveData, sender, safeAddress)
|
|
||||||
const txReceipt = await gnosisSafe.approveHash(contractTxHash, { from: sender, gas, gasPrice })
|
|
||||||
|
|
||||||
const txHash = txReceipt.tx
|
|
||||||
await checkReceiptStatus(txHash)
|
|
||||||
|
|
||||||
await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'confirmation')
|
|
||||||
|
|
||||||
return txHash
|
|
||||||
}
|
|
||||||
|
|
||||||
// export const executeTransaction = async (
|
|
||||||
// safeAddress: string,
|
// safeAddress: string,
|
||||||
// to: string,
|
// to: string,
|
||||||
// valueInWei: number,
|
// valueInWei: number,
|
||||||
|
@ -62,17 +16,17 @@ export const approveTransaction = async (
|
||||||
// operation: Operation,
|
// operation: Operation,
|
||||||
// nonce: number,
|
// nonce: number,
|
||||||
// sender: string,
|
// sender: string,
|
||||||
// ownersWhoHasSigned: List<string>,
|
|
||||||
// ) => {
|
// ) => {
|
||||||
// const gasPrice = await calculateGasPrice()
|
// const gasPrice = await calculateGasPrice()
|
||||||
|
|
||||||
// if (signaturesViaMetamask()) {
|
// if (signaturesViaMetamask()) {
|
||||||
// const safe = await getSafeEthereumInstance(safeAddress)
|
// // return executeTransaction(safeAddress, to, valueInWei, data, operation, nonce, sender)
|
||||||
|
// const safe = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
// const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
|
// const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
|
||||||
// const signature = await generateMetamaskSignature(
|
// const signature = await generateMetamaskSignature(
|
||||||
// safe,
|
// safe,
|
||||||
// safeAddress,
|
// safeAddress,
|
||||||
// sender,
|
// // sender
|
||||||
// to,
|
// to,
|
||||||
// valueInWei,
|
// valueInWei,
|
||||||
// nonce,
|
// nonce,
|
||||||
|
@ -82,61 +36,107 @@ export const approveTransaction = async (
|
||||||
// )
|
// )
|
||||||
// storeSignature(safeAddress, nonce, signature)
|
// storeSignature(safeAddress, nonce, signature)
|
||||||
|
|
||||||
// const sigs = getSignaturesFrom(safeAddress, nonce)
|
// return undefined
|
||||||
// const threshold = await safe.getThreshold()
|
// }
|
||||||
// const gas = await estimateDataGas(
|
|
||||||
// safe,
|
|
||||||
// to,
|
|
||||||
// valueInWei,
|
|
||||||
// data,
|
|
||||||
// operation,
|
|
||||||
// txGasEstimate,
|
|
||||||
// 0,
|
|
||||||
// nonce,
|
|
||||||
// Number(threshold),
|
|
||||||
// 0,
|
|
||||||
// )
|
|
||||||
// const numOwners = await safe.getOwners()
|
|
||||||
// const gasIncludingRemovingStoreUpfront = gas + txGasEstimate + numOwners.length * 15000
|
|
||||||
|
|
||||||
// const txReceipt = await safe.execTransaction(
|
// const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
// to,
|
// const contractTxHash = await gnosisSafe.getTransactionHash(to, valueInWei, data, operation, 0, 0, 0, 0, 0, nonce)
|
||||||
// valueInWei,
|
|
||||||
// data,
|
// const approveData = gnosisSafe.contract.methods.approveHash(contractTxHash).encodeABI()
|
||||||
// operation,
|
// const gas = await calculateGasOf(approveData, sender, safeAddress)
|
||||||
// txGasEstimate,
|
// const txReceipt = await gnosisSafe.approveHash(contractTxHash, { from: sender, gas, gasPrice })
|
||||||
// 0, // dataGasEstimate
|
|
||||||
// 0, // gasPrice
|
|
||||||
// 0, // txGasToken
|
|
||||||
// 0, // refundReceiver
|
|
||||||
// sigs,
|
|
||||||
// { from: sender, gas: gasIncludingRemovingStoreUpfront, gasPrice },
|
|
||||||
// )
|
|
||||||
|
|
||||||
// const txHash = txReceipt.tx
|
// const txHash = txReceipt.tx
|
||||||
// await checkReceiptStatus(txHash)
|
// await checkReceiptStatus(txHash)
|
||||||
|
|
||||||
|
// await saveTxToHistory(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'confirmation')
|
||||||
|
|
||||||
|
// return txHash
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // export const executeTransaction = async (
|
||||||
|
// // safeAddress: string,
|
||||||
|
// // to: string,
|
||||||
|
// // valueInWei: number,
|
||||||
|
// // data: string,
|
||||||
|
// // operation: Operation,
|
||||||
|
// // nonce: number,
|
||||||
|
// // sender: string,
|
||||||
|
// // ownersWhoHasSigned: List<string>,
|
||||||
|
// // ) => {
|
||||||
|
// // const gasPrice = await calculateGasPrice()
|
||||||
|
|
||||||
|
// // if (signaturesViaMetamask()) {
|
||||||
|
// // const safe = await getSafeEthereumInstance(safeAddress)
|
||||||
|
// // const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
|
||||||
|
// // const signature = await generateMetamaskSignature(
|
||||||
|
// // safe,
|
||||||
|
// // safeAddress,
|
||||||
|
// // sender,
|
||||||
|
// // to,
|
||||||
|
// // valueInWei,
|
||||||
|
// // nonce,
|
||||||
|
// // data,
|
||||||
|
// // operation,
|
||||||
|
// // txGasEstimate,
|
||||||
|
// // )
|
||||||
|
// // storeSignature(safeAddress, nonce, signature)
|
||||||
|
|
||||||
|
// // const sigs = getSignaturesFrom(safeAddress, nonce)
|
||||||
|
// // const threshold = await safe.getThreshold()
|
||||||
|
// // const gas = await estimateDataGas(
|
||||||
|
// // safe,
|
||||||
|
// // to,
|
||||||
|
// // valueInWei,
|
||||||
|
// // data,
|
||||||
|
// // operation,
|
||||||
|
// // txGasEstimate,
|
||||||
|
// // 0,
|
||||||
|
// // nonce,
|
||||||
|
// // Number(threshold),
|
||||||
|
// // 0,
|
||||||
|
// // )
|
||||||
|
// // const numOwners = await safe.getOwners()
|
||||||
|
// // const gasIncludingRemovingStoreUpfront = gas + txGasEstimate + numOwners.length * 15000
|
||||||
|
|
||||||
|
// // const txReceipt = await safe.execTransaction(
|
||||||
|
// // to,
|
||||||
|
// // valueInWei,
|
||||||
|
// // data,
|
||||||
|
// // operation,
|
||||||
|
// // txGasEstimate,
|
||||||
|
// // 0, // dataGasEstimate
|
||||||
|
// // 0, // gasPrice
|
||||||
|
// // 0, // txGasToken
|
||||||
|
// // 0, // refundReceiver
|
||||||
|
// // sigs,
|
||||||
|
// // { from: sender, gas: gasIncludingRemovingStoreUpfront, gasPrice },
|
||||||
|
// // )
|
||||||
|
|
||||||
|
// // const txHash = txReceipt.tx
|
||||||
|
// // await checkReceiptStatus(txHash)
|
||||||
|
// // // await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'execution')
|
||||||
|
|
||||||
|
// // return txHash
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // const gnosisSafe = await getSafeEthereumInstance(safeAddress)
|
||||||
|
// // const signatures = buildSignaturesFrom(ownersWhoHasSigned, sender)
|
||||||
|
// // const txExecutionData = gnosisSafe.contract.methods
|
||||||
|
// // .execTransaction(to, valueInWei, data, operation, 0, 0, 0, 0, 0, signatures)
|
||||||
|
// // .encodeABI()
|
||||||
|
// // const gas = await calculateGasOf(txExecutionData, sender, safeAddress)
|
||||||
|
// // const numOwners = await gnosisSafe.getOwners()
|
||||||
|
// // const gasIncludingRemovingStoreUpfront = gas + numOwners.length * 15000
|
||||||
|
// // const txReceipt = await gnosisSafe.execTransaction(to, valueInWei, data, operation, 0, 0, 0, 0, 0, signatures, {
|
||||||
|
// // from: sender,
|
||||||
|
// // gas: gasIncludingRemovingStoreUpfront,
|
||||||
|
// // gasPrice,
|
||||||
|
// // })
|
||||||
|
// // const txHash = txReceipt.tx
|
||||||
|
// // await checkReceiptStatus(txHash)
|
||||||
|
|
||||||
// // await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'execution')
|
// // await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'execution')
|
||||||
|
|
||||||
// return txHash
|
// // return txHash
|
||||||
// }
|
// // }
|
||||||
|
|
||||||
// const gnosisSafe = await getSafeEthereumInstance(safeAddress)
|
|
||||||
// const signatures = buildSignaturesFrom(ownersWhoHasSigned, sender)
|
|
||||||
// const txExecutionData = gnosisSafe.contract.methods
|
|
||||||
// .execTransaction(to, valueInWei, data, operation, 0, 0, 0, 0, 0, signatures)
|
|
||||||
// .encodeABI()
|
|
||||||
// const gas = await calculateGasOf(txExecutionData, sender, safeAddress)
|
|
||||||
// const numOwners = await gnosisSafe.getOwners()
|
|
||||||
// const gasIncludingRemovingStoreUpfront = gas + numOwners.length * 15000
|
|
||||||
// const txReceipt = await gnosisSafe.execTransaction(to, valueInWei, data, operation, 0, 0, 0, 0, 0, signatures, {
|
|
||||||
// from: sender,
|
|
||||||
// gas: gasIncludingRemovingStoreUpfront,
|
|
||||||
// gasPrice,
|
|
||||||
// })
|
|
||||||
// const txHash = txReceipt.tx
|
|
||||||
// await checkReceiptStatus(txHash)
|
|
||||||
|
|
||||||
// await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'execution')
|
|
||||||
|
|
||||||
// return txHash
|
|
||||||
// }
|
|
||||||
|
|
|
@ -5,31 +5,83 @@ import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
|
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
|
import { type Operation, saveTxToHistory } from '~/logic/safe/transactions'
|
||||||
|
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
|
||||||
|
import { getErrorMessage } from '~/test/utils/ethereumErrors'
|
||||||
|
|
||||||
export const CALL = 0
|
export const CALL = 0
|
||||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
export const TX_TYPE_EXECUTION = 'execution'
|
||||||
|
export const TX_TYPE_CONFIRMATION = 'confirmation'
|
||||||
|
|
||||||
|
export const approveTransaction = async (
|
||||||
|
safeInstance: any,
|
||||||
|
to: string,
|
||||||
|
valueInWei: number | string,
|
||||||
|
data: string,
|
||||||
|
operation: Operation,
|
||||||
|
nonce: number,
|
||||||
|
sender: string,
|
||||||
|
) => {
|
||||||
|
const contractTxHash = await safeInstance.getTransactionHash(
|
||||||
|
to,
|
||||||
|
valueInWei,
|
||||||
|
data,
|
||||||
|
operation,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
ZERO_ADDRESS,
|
||||||
|
ZERO_ADDRESS,
|
||||||
|
nonce,
|
||||||
|
{
|
||||||
|
from: sender,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const receipt = await safeInstance.approveHash(contractTxHash, { from: sender })
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
await saveTxToHistory(
|
||||||
|
safeInstance,
|
||||||
|
to,
|
||||||
|
valueInWei,
|
||||||
|
data,
|
||||||
|
operation,
|
||||||
|
nonce,
|
||||||
|
receipt.tx, // tx hash,
|
||||||
|
sender,
|
||||||
|
TX_TYPE_CONFIRMATION,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return receipt
|
||||||
|
}
|
||||||
|
|
||||||
export const executeTransaction = async (
|
export const executeTransaction = async (
|
||||||
safeInstance: any,
|
safeInstance: any,
|
||||||
to: string,
|
to: string,
|
||||||
valueInWei: number | string,
|
valueInWei: number | string,
|
||||||
data: string,
|
data: string,
|
||||||
operation: number | string,
|
operation: Operation,
|
||||||
nonce: string | number,
|
nonce: string | number,
|
||||||
sender: string,
|
sender: string,
|
||||||
|
signatures?: string,
|
||||||
) => {
|
) => {
|
||||||
try {
|
let sigs = signatures
|
||||||
|
|
||||||
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||||
const sigs = `0x000000000000000000000000${sender.replace(
|
if (!sigs) {
|
||||||
|
sigs = `0x000000000000000000000000${sender.replace(
|
||||||
'0x',
|
'0x',
|
||||||
'',
|
'',
|
||||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||||
|
}
|
||||||
|
|
||||||
const tx = await safeInstance.execTransaction(
|
try {
|
||||||
|
const receipt = await safeInstance.execTransaction(
|
||||||
to,
|
to,
|
||||||
valueInWei,
|
valueInWei,
|
||||||
data,
|
data,
|
||||||
CALL,
|
operation,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
@ -39,10 +91,31 @@ export const executeTransaction = async (
|
||||||
{ from: sender },
|
{ from: sender },
|
||||||
)
|
)
|
||||||
|
|
||||||
return tx
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
await saveTxToHistory(
|
||||||
|
safeInstance,
|
||||||
|
to,
|
||||||
|
valueInWei,
|
||||||
|
data,
|
||||||
|
operation,
|
||||||
|
nonce,
|
||||||
|
receipt.tx, // tx hash,
|
||||||
|
sender,
|
||||||
|
TX_TYPE_EXECUTION,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return receipt
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line
|
/* eslint-disable */
|
||||||
console.log('Error executing the TX: ' + error)
|
const executeDataUsedSignatures = safeInstance.contract.methods
|
||||||
|
.execTransaction(to, valueInWei, data, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
|
||||||
|
.encodeABI()
|
||||||
|
const errMsg = await getErrorMessage(safeInstance.address, 0, executeDataUsedSignatures, sender)
|
||||||
|
|
||||||
|
console.log(`Error executing the TX: ${error}`)
|
||||||
|
console.log(`Error executing the TX: ${errMsg}`)
|
||||||
|
/* eslint-enable */
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +125,7 @@ export const createTransaction = async (safeAddress: string, to: string, valueIn
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
const from = web3.currentProvider.selectedAddress
|
const from = web3.currentProvider.selectedAddress
|
||||||
const threshold = await safeInstance.getThreshold()
|
const threshold = await safeInstance.getThreshold()
|
||||||
const nonce = await safeInstance.nonce()
|
const nonce = (await safeInstance.nonce()).toString()
|
||||||
const valueInWei = web3.utils.toWei(valueInEth, 'ether')
|
const valueInWei = web3.utils.toWei(valueInEth, 'ether')
|
||||||
const isExecution = threshold.toNumber() === 1
|
const isExecution = threshold.toNumber() === 1
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
// @flow
|
||||||
|
import axios from 'axios'
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
import { getTxServiceUriFrom, getTxServiceHost } from '~/config'
|
||||||
|
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
|
||||||
|
|
||||||
|
export type TxServiceType = 'confirmation' | 'execution' | 'initialised'
|
||||||
|
export type Operation = 0 | 1 | 2
|
||||||
|
|
||||||
|
const calculateBodyFrom = async (
|
||||||
|
safeInstance: any,
|
||||||
|
to: string,
|
||||||
|
valueInWei: number | string,
|
||||||
|
data: string,
|
||||||
|
operation: Operation,
|
||||||
|
nonce: string | number,
|
||||||
|
transactionHash: string,
|
||||||
|
sender: string,
|
||||||
|
confirmationType: TxServiceType,
|
||||||
|
) => {
|
||||||
|
const contractTransactionHash = await safeInstance.getTransactionHash(
|
||||||
|
to,
|
||||||
|
valueInWei,
|
||||||
|
data,
|
||||||
|
operation,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
ZERO_ADDRESS,
|
||||||
|
ZERO_ADDRESS,
|
||||||
|
nonce,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
to: getWeb3().utils.toChecksumAddress(to),
|
||||||
|
value: valueInWei,
|
||||||
|
data,
|
||||||
|
operation,
|
||||||
|
nonce,
|
||||||
|
safeTxGas: 0,
|
||||||
|
baseGas: 0,
|
||||||
|
gasPrice: 0,
|
||||||
|
gasToken: ZERO_ADDRESS,
|
||||||
|
refundReceiver: ZERO_ADDRESS,
|
||||||
|
contractTransactionHash,
|
||||||
|
transactionHash,
|
||||||
|
sender: getWeb3().utils.toChecksumAddress(sender),
|
||||||
|
confirmationType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildTxServiceUrl = (safeAddress: string) => {
|
||||||
|
const host = getTxServiceHost()
|
||||||
|
const address = getWeb3().utils.toChecksumAddress(safeAddress)
|
||||||
|
const base = getTxServiceUriFrom(address)
|
||||||
|
return `${host}${base}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveTxToHistory = async (
|
||||||
|
safeInstance: any,
|
||||||
|
to: string,
|
||||||
|
valueInWei: number | string,
|
||||||
|
data: string,
|
||||||
|
operation: Operation,
|
||||||
|
nonce: number | string,
|
||||||
|
txHash: string,
|
||||||
|
sender: string,
|
||||||
|
type: TxServiceType,
|
||||||
|
) => {
|
||||||
|
const url = buildTxServiceUrl(safeInstance.address)
|
||||||
|
const body = await calculateBodyFrom(safeInstance, to, valueInWei, data, operation, nonce, txHash, sender, type)
|
||||||
|
const response = await axios.post(url, body)
|
||||||
|
|
||||||
|
if (response.status !== 202) {
|
||||||
|
return Promise.reject(new Error('Error submitting the transaction'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
|
@ -36,13 +36,13 @@ export const setOwners = async (safeAddress: string, owners: List<Owner>) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOwners = async (safeAddress: string): Map<string, string> => {
|
export const getOwners = async (safeAddress: string): Promise<Map<string, string>> => {
|
||||||
const data: Object = await loadFromStorage(`${OWNERS_KEY}-${safeAddress}`)
|
const data: Object = await loadFromStorage(`${OWNERS_KEY}-${safeAddress}`)
|
||||||
|
|
||||||
return data ? Map(data) : Map()
|
return data ? Map(data) : Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeOwners = async (safeAddress: string) => {
|
export const removeOwners = async (safeAddress: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await removeFromStorage(`${OWNERS_KEY}-${safeAddress}`)
|
await removeFromStorage(`${OWNERS_KEY}-${safeAddress}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type { Dispatch as ReduxDispatch } from 'redux'
|
||||||
import StandardToken from '@gnosis.pm/util-contracts/build/contracts/GnosisStandardToken.json'
|
import StandardToken from '@gnosis.pm/util-contracts/build/contracts/GnosisStandardToken.json'
|
||||||
import HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanFriendlyToken.json'
|
import HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanFriendlyToken.json'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import { type GlobalState } from '~/store/index'
|
import { type GlobalState } from '~/store'
|
||||||
import { makeToken, type TokenProps } from '~/logic/tokens/store/model/token'
|
import { makeToken, type TokenProps } from '~/logic/tokens/store/model/token'
|
||||||
import { fetchTokenList } from '~/logic/tokens/api'
|
import { fetchTokenList } from '~/logic/tokens/api'
|
||||||
import { ensureOnce } from '~/utils/singleton'
|
import { ensureOnce } from '~/utils/singleton'
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import logo from '~/assets/icons/icon_etherTokens.svg'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import { makeToken, type Token } from '~/logic/tokens/store/model/token'
|
import { makeToken, type Token } from '~/logic/tokens/store/model/token'
|
||||||
|
import logo from '~/assets/icons/icon_etherTokens.svg'
|
||||||
|
|
||||||
export const ETH_ADDRESS = '0x000'
|
export const ETH_ADDRESS = '0x000'
|
||||||
export const isEther = (symbol: string) => symbol === 'ETH'
|
export const isEther = (symbol: string) => symbol === 'ETH'
|
||||||
|
@ -31,3 +32,19 @@ export const calculateActiveErc20TokensFrom = (tokens: List<Token>) => {
|
||||||
|
|
||||||
return activeTokens
|
return activeTokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isAddressAToken = async (tokenAddress: string) => {
|
||||||
|
// SECOND APPROACH:
|
||||||
|
// They both seem to work the same
|
||||||
|
// const tokenContract = await getStandardTokenContract()
|
||||||
|
// try {
|
||||||
|
// await tokenContract.at(tokenAddress)
|
||||||
|
// } catch {
|
||||||
|
// return 'Not a token address'
|
||||||
|
// }
|
||||||
|
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const call = await web3.eth.call({ to: tokenAddress, data: web3.utils.sha3('totalSupply()') })
|
||||||
|
|
||||||
|
return call !== '0x'
|
||||||
|
}
|
||||||
|
|
|
@ -14,8 +14,7 @@ export const sameAddress = (firstAddress: string, secondAddress: string): boolea
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shortVersionOf = (address: string, cut: number) => {
|
export const shortVersionOf = (address: string, cut: number) => {
|
||||||
const initial = cut
|
|
||||||
const final = 42 - cut
|
const final = 42 - cut
|
||||||
|
|
||||||
return `${address.substring(0, initial)}...${address.substring(final)}`
|
return `${address.substring(0, cut)}...${address.substring(final)}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,9 +32,7 @@ export const ETHEREUM_NETWORK_IDS = {
|
||||||
42: ETHEREUM_NETWORK.KOVAN,
|
42: ETHEREUM_NETWORK.KOVAN,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openTxInEtherScan = (tx: string, network: string) => `https://${network}.etherscan.io/tx/${tx}`
|
export const getEtherScanLink = (type: 'address' | 'tx', value: string, network: string) => `https://${network === 'mainnet' ? '' : `${network}.`}etherscan.io/${type}/${value}`
|
||||||
|
|
||||||
export const getEtherScanLink = (address: string, network: string) => `https://${network}.etherscan.io/address/${address}`
|
|
||||||
|
|
||||||
let web3
|
let web3
|
||||||
export const getWeb3 = () => web3 || (window.web3 && new Web3(window.web3.currentProvider)) || (window.ethereum && new Web3(window.ethereum))
|
export const getWeb3 = () => web3 || (window.web3 && new Web3(window.web3.currentProvider)) || (window.ethereum && new Web3(window.ethereum))
|
||||||
|
|
|
@ -151,7 +151,7 @@ const OwnerListComponent = (props: Props) => {
|
||||||
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
|
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
|
||||||
{address}
|
{address}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(address, network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', address, network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import Block from '~/components/layout/Block'
|
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
import Identicon from '~/components/Identicon'
|
import Identicon from '~/components/Identicon'
|
||||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
import OpenPaper from '~/components/Stepper/OpenPaper'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
|
@ -132,7 +132,7 @@ class ReviewComponent extends React.PureComponent<Props, State> {
|
||||||
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
|
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
|
||||||
{shortVersionOf(safeAddress, 4)}
|
{shortVersionOf(safeAddress, 4)}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(safeAddress, network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', safeAddress, network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -177,7 +177,7 @@ class ReviewComponent extends React.PureComponent<Props, State> {
|
||||||
<Paragraph size="md" color="disabled" noMargin>
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
{owners[index]}
|
{owners[index]}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(owners[index], network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', owners[index], network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -20,7 +20,7 @@ export const loadSafe = async (
|
||||||
safeName: string,
|
safeName: string,
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
owners: Array,
|
owners: Array,
|
||||||
addSafe: Function
|
addSafe: Function,
|
||||||
) => {
|
) => {
|
||||||
const safeProps = await buildSafe(safeAddress, safeName)
|
const safeProps = await buildSafe(safeAddress, safeName)
|
||||||
safeProps.owners = owners
|
safeProps.owners = owners
|
||||||
|
|
|
@ -129,7 +129,7 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
|
||||||
<Paragraph size="md" color="disabled" noMargin>
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
{addresses[index]}
|
{addresses[index]}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(addresses[index], network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', addresses[index], network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import Block from '~/components/layout/Block'
|
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
|
||||||
import LinearProgress from '@material-ui/core/LinearProgress'
|
import LinearProgress from '@material-ui/core/LinearProgress'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Img from '~/components/layout/Img'
|
import Img from '~/components/layout/Img'
|
||||||
import Page from '~/components/layout/Page'
|
import Page from '~/components/layout/Page'
|
||||||
import { openTxInEtherScan } from '~/logic/wallets/getWeb3'
|
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
||||||
import { mediumFontSize, secondary, xs } from '~/theme/variables'
|
import { mediumFontSize, secondary, xs } from '~/theme/variables'
|
||||||
import { type SelectorProps } from '../container/selector'
|
import { type SelectorProps } from '../container/selector'
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ const Opening = ({
|
||||||
Follow progress on
|
Follow progress on
|
||||||
{' '}
|
{' '}
|
||||||
<a
|
<a
|
||||||
href={openTxInEtherScan(tx, network)}
|
href={getEtherScanLink('tx', tx, network)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={classes.etherscan}
|
className={classes.etherscan}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
import Modal from '~/components/Modal'
|
import Modal from '~/components/Modal'
|
||||||
import ChooseTxType from './screens/ChooseTxType'
|
import ChooseTxType from './screens/ChooseTxType'
|
||||||
import SendFunds from './screens/SendFunds'
|
import SendFunds from './screens/SendFunds'
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
|
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import Link from '~/components/layout/Link'
|
import Link from '~/components/layout/Link'
|
||||||
|
@ -62,7 +62,6 @@ const ReviewTx = ({
|
||||||
let txData = EMPTY_DATA
|
let txData = EMPTY_DATA
|
||||||
let txAmount = web3.utils.toWei(tx.amount, 'ether')
|
let txAmount = web3.utils.toWei(tx.amount, 'ether')
|
||||||
|
|
||||||
|
|
||||||
if (!isSendingETH) {
|
if (!isSendingETH) {
|
||||||
const StandardToken = await getStandardTokenContract()
|
const StandardToken = await getStandardTokenContract()
|
||||||
const tokenInstance = await StandardToken.at(tx.token.address)
|
const tokenInstance = await StandardToken.at(tx.token.address)
|
||||||
|
@ -139,7 +138,7 @@ const ReviewTx = ({
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClickBack}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
@ -148,6 +147,7 @@ const ReviewTx = ({
|
||||||
onClick={submitTx}
|
onClick={submitTx}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
|
minHeight={42}
|
||||||
color="primary"
|
color="primary"
|
||||||
data-testid="submit-tx-btn"
|
data-testid="submit-tx-btn"
|
||||||
>
|
>
|
||||||
|
|
|
@ -158,13 +158,13 @@ const SendFunds = ({
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={classes.button}
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
minHeight={42}
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
color="primary"
|
color="primary"
|
||||||
data-testid="review-tx-btn"
|
data-testid="review-tx-btn"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||||
|
import { isAddressAToken } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
import { simpleMemoize } from '~/components/forms/validator'
|
import { simpleMemoize } from '~/components/forms/validator'
|
||||||
// import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
|
// import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
|
|
||||||
|
@ -17,10 +17,9 @@ export const addressIsTokenContract = simpleMemoize(async (tokenAddress: string)
|
||||||
// return 'Not a token address'
|
// return 'Not a token address'
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const web3 = getWeb3()
|
const isToken = await isAddressAToken(tokenAddress)
|
||||||
const call = await web3.eth.call({ to: tokenAddress, data: web3.utils.sha3('totalSupply()') })
|
|
||||||
|
|
||||||
if (call === '0x') {
|
if (!isToken) {
|
||||||
return 'Not a token address'
|
return 'Not a token address'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,19 +5,19 @@ import cn from 'classnames'
|
||||||
import SearchBar from 'material-ui-search-bar'
|
import SearchBar from 'material-ui-search-bar'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import MuiList from '@material-ui/core/List'
|
import MuiList from '@material-ui/core/List'
|
||||||
import Img from '~/components/layout/Img'
|
|
||||||
import Block from '~/components/layout/Block'
|
|
||||||
import ListItem from '@material-ui/core/ListItem'
|
import ListItem from '@material-ui/core/ListItem'
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||||
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
|
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
|
||||||
import ListItemText from '@material-ui/core/ListItemText'
|
import ListItemText from '@material-ui/core/ListItemText'
|
||||||
import Search from '@material-ui/icons/Search'
|
|
||||||
import Button from '~/components/layout/Button'
|
|
||||||
import Switch from '@material-ui/core/Switch'
|
import Switch from '@material-ui/core/Switch'
|
||||||
|
import Search from '@material-ui/icons/Search'
|
||||||
|
import Img from '~/components/layout/Img'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
import Divider from '~/components/layout/Divider'
|
import Divider from '~/components/layout/Divider'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import Spacer from '~/components/Spacer'
|
import Spacer from '~/components/Spacer'
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
|
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
|
@ -141,7 +141,7 @@ class Tokens extends React.Component<Props, State> {
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary"
|
color="primary"
|
||||||
className={classes.add}
|
className={classes.add}
|
||||||
onClick={switchToAddCustomTokenScreen}
|
onClick={switchToAddCustomTokenScreen}
|
||||||
testId={ADD_CUSTOM_TOKEN_BUTTON_TEST_ID}
|
testId={ADD_CUSTOM_TOKEN_BUTTON_TEST_ID}
|
||||||
|
|
|
@ -52,6 +52,7 @@ export const generateColumns = () => {
|
||||||
disablePadding: false,
|
disablePadding: false,
|
||||||
label: '',
|
label: '',
|
||||||
custom: true,
|
custom: true,
|
||||||
|
static: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
return List([assetColumn, balanceColumn, actions])
|
return List([assetColumn, balanceColumn, actions])
|
||||||
|
|
|
@ -2,16 +2,16 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import classNames from 'classnames/bind'
|
import classNames from 'classnames/bind'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
|
||||||
import CallMade from '@material-ui/icons/CallMade'
|
import CallMade from '@material-ui/icons/CallMade'
|
||||||
import CallReceived from '@material-ui/icons/CallReceived'
|
import CallReceived from '@material-ui/icons/CallReceived'
|
||||||
import Button from '@material-ui/core/Button'
|
|
||||||
import Checkbox from '@material-ui/core/Checkbox'
|
import Checkbox from '@material-ui/core/Checkbox'
|
||||||
import TableRow from '@material-ui/core/TableRow'
|
import TableRow from '@material-ui/core/TableRow'
|
||||||
import TableCell from '@material-ui/core/TableCell'
|
import TableCell from '@material-ui/core/TableCell'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
import ButtonLink from '~/components/layout/ButtonLink'
|
import ButtonLink from '~/components/layout/ButtonLink'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Modal from '~/components/Modal'
|
import Modal from '~/components/Modal'
|
||||||
|
@ -168,10 +168,11 @@ class Balances extends React.Component<Props, State> {
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary"
|
color="primary"
|
||||||
className={classes.send}
|
className={classes.send}
|
||||||
onClick={() => this.showSendFunds(row.asset.name)}
|
onClick={() => this.showSendFunds(row.asset.name)}
|
||||||
data-testid="balance-send-btn"
|
rounded
|
||||||
|
testId="balance-send-btn"
|
||||||
>
|
>
|
||||||
<CallMade className={classNames(classes.leftIcon, classes.iconSmall)} />
|
<CallMade className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||||
Send
|
Send
|
||||||
|
@ -180,9 +181,10 @@ class Balances extends React.Component<Props, State> {
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary"
|
color="primary"
|
||||||
className={classes.receive}
|
className={classes.receive}
|
||||||
onClick={this.onShow('Receive')}
|
onClick={this.onShow('Receive')}
|
||||||
|
rounded
|
||||||
>
|
>
|
||||||
<CallReceived className={classNames(classes.leftIcon, classes.iconSmall)} />
|
<CallReceived className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||||
Receive
|
Receive
|
||||||
|
|
|
@ -19,20 +19,23 @@ import {
|
||||||
} from '~/theme/variables'
|
} from '~/theme/variables'
|
||||||
import { copyToClipboard } from '~/utils/clipboard'
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
import Balances from './Balances'
|
import Balances from './Balances'
|
||||||
|
import Transactions from './TransactionsNew'
|
||||||
import Settings from './Settings'
|
import Settings from './Settings'
|
||||||
|
|
||||||
export const SETTINGS_TAB_BTN_TESTID = 'settings-tab-btn'
|
export const SETTINGS_TAB_BTN_TESTID = 'settings-tab-btn'
|
||||||
export const SAFE_VIEW_NAME_HEADING_TESTID = 'safe-name-heading'
|
export const SAFE_VIEW_NAME_HEADING_TESTID = 'safe-name-heading'
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
tabIndex: number,
|
||||||
|
}
|
||||||
|
|
||||||
type Props = SelectorProps & {
|
type Props = SelectorProps & {
|
||||||
classes: Object,
|
classes: Object,
|
||||||
granted: boolean,
|
granted: boolean,
|
||||||
createTransaction: Function,
|
|
||||||
updateSafe: Function,
|
updateSafe: Function,
|
||||||
}
|
createTransaction: Function,
|
||||||
|
processTransaction: Function,
|
||||||
type State = {
|
fetchTransactions: Function,
|
||||||
tabIndex: number,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const openIconStyle = {
|
const openIconStyle = {
|
||||||
|
@ -102,7 +105,10 @@ class Layout extends React.Component<Props, State> {
|
||||||
tokens,
|
tokens,
|
||||||
activeTokens,
|
activeTokens,
|
||||||
createTransaction,
|
createTransaction,
|
||||||
|
processTransaction,
|
||||||
|
fetchTransactions,
|
||||||
updateSafe,
|
updateSafe,
|
||||||
|
transactions,
|
||||||
userAddress,
|
userAddress,
|
||||||
} = this.props
|
} = this.props
|
||||||
const { tabIndex } = this.state
|
const { tabIndex } = this.state
|
||||||
|
@ -112,7 +118,7 @@ class Layout extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { address, ethBalance, name } = safe
|
const { address, ethBalance, name } = safe
|
||||||
const etherScanLink = getEtherScanLink(address, network)
|
const etherScanLink = getEtherScanLink('address', address, network)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -155,6 +161,19 @@ class Layout extends React.Component<Props, State> {
|
||||||
createTransaction={createTransaction}
|
createTransaction={createTransaction}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{tabIndex === 1 && (
|
||||||
|
<Transactions
|
||||||
|
threshold={safe.threshold}
|
||||||
|
owners={safe.owners}
|
||||||
|
transactions={transactions}
|
||||||
|
fetchTransactions={fetchTransactions}
|
||||||
|
safeAddress={address}
|
||||||
|
userAddress={userAddress}
|
||||||
|
granted={granted}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
processTransaction={processTransaction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{tabIndex === 2 && (
|
{tabIndex === 2 && (
|
||||||
<Settings
|
<Settings
|
||||||
granted={granted}
|
granted={granted}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Row from '~/components/layout/Row'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import Button from '~/components/layout/Button'
|
import Button from '~/components/layout/Button'
|
||||||
import { sm, boldFont } from '~/theme/variables'
|
import { sm } from '~/theme/variables'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
const controlsStyle = {
|
const controlsStyle = {
|
||||||
|
@ -20,11 +20,6 @@ const controlsStyle = {
|
||||||
padding: sm,
|
padding: sm,
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveButtonStyle = {
|
|
||||||
marginRight: sm,
|
|
||||||
fontWeight: boldFont,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SAFE_NAME_INPUT_TESTID = 'safe-name-input'
|
export const SAFE_NAME_INPUT_TESTID = 'safe-name-input'
|
||||||
export const SAFE_NAME_SUBMIT_BTN_TESTID = 'change-safe-name-btn'
|
export const SAFE_NAME_SUBMIT_BTN_TESTID = 'change-safe-name-btn'
|
||||||
|
|
||||||
|
@ -73,7 +68,7 @@ const ChangeSafeName = (props: Props) => {
|
||||||
<Col end="xs">
|
<Col end="xs">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
style={saveButtonStyle}
|
className={classes.saveBtn}
|
||||||
size="small"
|
size="small"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { lg } from '~/theme/variables'
|
import { lg, sm, boldFont } from '~/theme/variables'
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = () => ({
|
||||||
formContainer: {
|
formContainer: {
|
||||||
|
@ -10,4 +10,8 @@ export const styles = () => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
maxWidth: '460px',
|
maxWidth: '460px',
|
||||||
},
|
},
|
||||||
|
saveBtn: {
|
||||||
|
marginRight: sm,
|
||||||
|
fontWeight: boldFont,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -112,7 +112,7 @@ const ReviewAddOwner = ({
|
||||||
<Paragraph size="md" color="disabled" noMargin>
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
{owner.address}
|
{owner.address}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(owner.address, network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', owner.address, network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -141,7 +141,7 @@ const ReviewAddOwner = ({
|
||||||
<Paragraph size="md" color="disabled" noMargin>
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
{values.ownerAddress}
|
{values.ownerAddress}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(values.ownerAddress, network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', values.ownerAddress, network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -94,7 +94,7 @@ const EditOwnerComponent = ({
|
||||||
<Paragraph style={{ marginLeft: 10 }} size="md" color="disabled" noMargin>
|
<Paragraph style={{ marginLeft: 10 }} size="md" color="disabled" noMargin>
|
||||||
{ownerAddress}
|
{ownerAddress}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(ownerAddress, network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress, network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -76,7 +76,7 @@ const CheckOwner = ({
|
||||||
<Paragraph size="md" color="disabled" noMargin>
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
{ownerAddress}
|
{ownerAddress}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(ownerAddress, network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress, network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -127,7 +127,7 @@ const ReviewRemoveOwner = ({
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link
|
<Link
|
||||||
className={classes.open}
|
className={classes.open}
|
||||||
to={getEtherScanLink(owner.address, network)}
|
to={getEtherScanLink('address', owner.address, network)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
|
@ -159,7 +159,7 @@ const ReviewRemoveOwner = ({
|
||||||
<Paragraph size="md" color="disabled" noMargin>
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
{ownerAddress}
|
{ownerAddress}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(ownerAddress, network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress, network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -94,7 +94,7 @@ const OwnerForm = ({
|
||||||
<Paragraph size="md" color="disabled" noMargin>
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
{ownerAddress}
|
{ownerAddress}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(ownerAddress, network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress, network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -129,7 +129,7 @@ const ReviewRemoveOwner = ({
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link
|
<Link
|
||||||
className={classes.open}
|
className={classes.open}
|
||||||
to={getEtherScanLink(owner.address, network)}
|
to={getEtherScanLink('address', owner.address, network)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
|
@ -161,7 +161,7 @@ const ReviewRemoveOwner = ({
|
||||||
<Paragraph size="md" color="disabled" noMargin>
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
{ownerAddress}
|
{ownerAddress}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(ownerAddress, network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', ownerAddress, network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
|
@ -187,7 +187,7 @@ const ReviewRemoveOwner = ({
|
||||||
<Paragraph size="md" color="disabled" noMargin>
|
<Paragraph size="md" color="disabled" noMargin>
|
||||||
{values.ownerAddress}
|
{values.ownerAddress}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Link className={classes.open} to={getEtherScanLink(values.ownerAddress, network)} target="_blank">
|
<Link className={classes.open} to={getEtherScanLink('address', values.ownerAddress, network)} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { List } from 'immutable'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import SelectField from '~/components/forms/SelectField'
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem'
|
import MenuItem from '@material-ui/core/MenuItem'
|
||||||
|
import SelectField from '~/components/forms/SelectField'
|
||||||
import {
|
import {
|
||||||
composeValidators, minValue, mustBeInteger, required, differentFrom,
|
composeValidators, minValue, mustBeInteger, required, differentFrom,
|
||||||
} from '~/components/forms/validator'
|
} from '~/components/forms/validator'
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import Bold from '~/components/layout/Bold'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph/index'
|
||||||
|
|
||||||
|
const NoTransactions = () => (
|
||||||
|
<Row>
|
||||||
|
<Col xs={12} center="xs" sm={10} smOffset={2} start="sm" margin="md">
|
||||||
|
<Paragraph size="lg">
|
||||||
|
<Bold>No transactions found for this safe</Bold>
|
||||||
|
</Paragraph>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default NoTransactions
|
|
@ -0,0 +1,127 @@
|
||||||
|
// @flow
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||||
|
import Checkbox from '@material-ui/core/Checkbox'
|
||||||
|
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
|
||||||
|
import Modal from '~/components/Modal'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Bold from '~/components/layout/Bold'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
classes: Object,
|
||||||
|
isOpen: boolean,
|
||||||
|
processTransaction: Function,
|
||||||
|
tx: Transaction,
|
||||||
|
nonce: string,
|
||||||
|
safeAddress: string,
|
||||||
|
threshold: number,
|
||||||
|
thresholdReached: boolean,
|
||||||
|
userAddress: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const getModalTitleAndDescription = (thresholdReached: boolean) => {
|
||||||
|
const title = thresholdReached ? 'Execute Transaction' : 'Approve Transaction'
|
||||||
|
const description = `This action will ${
|
||||||
|
thresholdReached ? 'execute' : 'approve'
|
||||||
|
} this transaction. A separate transaction will be performed to submit the ${
|
||||||
|
thresholdReached ? 'execution' : 'approval'
|
||||||
|
}.`
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApproveTxModal = ({
|
||||||
|
onClose,
|
||||||
|
isOpen,
|
||||||
|
classes,
|
||||||
|
processTransaction,
|
||||||
|
tx,
|
||||||
|
safeAddress,
|
||||||
|
threshold,
|
||||||
|
thresholdReached,
|
||||||
|
userAddress,
|
||||||
|
}: Props) => {
|
||||||
|
const [approveAndExecute, setApproveAndExecute] = useState<boolean>(false)
|
||||||
|
const { title, description } = getModalTitleAndDescription(thresholdReached)
|
||||||
|
const oneConfirmationLeft = tx.confirmations.size + 1 === threshold
|
||||||
|
|
||||||
|
const handleExecuteCheckbox = () => setApproveAndExecute(prevApproveAndExecute => !prevApproveAndExecute)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SharedSnackbarConsumer>
|
||||||
|
{({ openSnackbar }) => {
|
||||||
|
const approveTx = () => {
|
||||||
|
processTransaction(safeAddress, tx, openSnackbar, userAddress, approveAndExecute)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title={title} description={description} handleClose={onClose} open={isOpen}>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.headingText} noMargin>
|
||||||
|
{title}
|
||||||
|
</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Block className={classes.container}>
|
||||||
|
<Row>
|
||||||
|
<Paragraph>{description}</Paragraph>
|
||||||
|
<Paragraph size="sm" color="medium">
|
||||||
|
Transaction nonce:
|
||||||
|
<br />
|
||||||
|
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
|
||||||
|
</Paragraph>
|
||||||
|
{!thresholdReached && oneConfirmationLeft && (
|
||||||
|
<>
|
||||||
|
<Paragraph color="error">
|
||||||
|
Approving transaction does not execute it immediately. If you want to approve and execute the
|
||||||
|
transaction right away, click on checkbox below.
|
||||||
|
</Paragraph>
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Checkbox onChange={handleExecuteCheckbox} checked={approveAndExecute} color="primary" />}
|
||||||
|
label="Execute transaction"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClose}>
|
||||||
|
Exit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={classes.button}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={214}
|
||||||
|
minHeight={42}
|
||||||
|
color="primary"
|
||||||
|
onClick={approveTx}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</SharedSnackbarConsumer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ApproveTxModal)
|
|
@ -0,0 +1,35 @@
|
||||||
|
// @flow
|
||||||
|
import {
|
||||||
|
lg, md, sm, border,
|
||||||
|
} from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
heading: {
|
||||||
|
padding: `${sm} ${lg}`,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '75px',
|
||||||
|
},
|
||||||
|
headingText: {
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
closeIcon: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
borderTop: `1px solid ${border}`,
|
||||||
|
},
|
||||||
|
nonceNumber: {
|
||||||
|
marginTop: sm,
|
||||||
|
fontSize: md,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,89 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
|
||||||
|
import Modal from '~/components/Modal'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Bold from '~/components/layout/Bold'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
classes: Object,
|
||||||
|
isOpen: boolean,
|
||||||
|
createTransaction: Function,
|
||||||
|
tx: Transaction,
|
||||||
|
safeAddress: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CancelTxModal = ({
|
||||||
|
onClose, isOpen, classes, createTransaction, tx, safeAddress,
|
||||||
|
}: Props) => (
|
||||||
|
<SharedSnackbarConsumer>
|
||||||
|
{({ openSnackbar }) => {
|
||||||
|
const sendReplacementTransaction = () => {
|
||||||
|
createTransaction(safeAddress, safeAddress, 0, EMPTY_DATA, openSnackbar)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Cancel Transaction"
|
||||||
|
description="Cancel Transaction"
|
||||||
|
handleClose={onClose}
|
||||||
|
open={isOpen}
|
||||||
|
// paperClassName={cn(smallerModalSize && classes.smallerModalWindow)}
|
||||||
|
>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.headingText} noMargin>
|
||||||
|
Cancel transaction
|
||||||
|
</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Block className={classes.container}>
|
||||||
|
<Row>
|
||||||
|
<Paragraph>
|
||||||
|
This action will cancel this transaction. A separate transaction will be performed to submit the
|
||||||
|
cancellation.
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph size="sm" color="medium">
|
||||||
|
Transaction nonce:
|
||||||
|
<br />
|
||||||
|
<Bold className={classes.nonceNumber}>{tx.nonce}</Bold>
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} minHeight={42} onClick={onClose}>
|
||||||
|
Exit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={classes.button}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={214}
|
||||||
|
minHeight={42}
|
||||||
|
color="secondary"
|
||||||
|
onClick={sendReplacementTransaction}
|
||||||
|
>
|
||||||
|
Cancel Transaction
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</SharedSnackbarConsumer>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withStyles(styles)(CancelTxModal)
|
|
@ -0,0 +1,35 @@
|
||||||
|
// @flow
|
||||||
|
import {
|
||||||
|
lg, md, sm, border,
|
||||||
|
} from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
heading: {
|
||||||
|
padding: `${sm} ${lg}`,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '75px',
|
||||||
|
},
|
||||||
|
headingText: {
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
closeIcon: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
borderTop: `1px solid ${border}`,
|
||||||
|
},
|
||||||
|
nonceNumber: {
|
||||||
|
marginTop: sm,
|
||||||
|
fontSize: md,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,73 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import EditIcon from '@material-ui/icons/Edit'
|
||||||
|
import BlockIcon from '@material-ui/icons/Block'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import { sm, lg } from '~/theme/variables'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onTxConfirm: Function,
|
||||||
|
onTxCancel: Function,
|
||||||
|
onTxExecute: Function,
|
||||||
|
classes: Object,
|
||||||
|
showConfirmBtn: boolean,
|
||||||
|
showCancelBtn: boolean,
|
||||||
|
showExecuteBtn: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = () => ({
|
||||||
|
buttonRow: {
|
||||||
|
height: '56px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#f7f8fb',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
height: '32px',
|
||||||
|
'&:last-child': {
|
||||||
|
marginLeft: lg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: '14px',
|
||||||
|
height: '14px',
|
||||||
|
marginRight: sm,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const ButtonRow = ({
|
||||||
|
classes,
|
||||||
|
onTxCancel,
|
||||||
|
onTxConfirm,
|
||||||
|
showConfirmBtn,
|
||||||
|
showCancelBtn,
|
||||||
|
showExecuteBtn,
|
||||||
|
onTxExecute,
|
||||||
|
}: Props) => (
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
{showCancelBtn && (
|
||||||
|
<Button className={classes.button} variant="contained" minWidth={140} color="secondary" onClick={onTxCancel}>
|
||||||
|
<BlockIcon className={classes.icon} />
|
||||||
|
{' '}
|
||||||
|
Cancel TX
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showConfirmBtn && (
|
||||||
|
<Button className={classes.button} variant="contained" minWidth={140} color="primary" onClick={onTxConfirm}>
|
||||||
|
<EditIcon className={classes.icon} />
|
||||||
|
{' '}
|
||||||
|
Confirm TX
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showExecuteBtn && (
|
||||||
|
<Button className={classes.button} variant="contained" minWidth={140} color="primary" onClick={onTxExecute}>
|
||||||
|
<EditIcon className={classes.icon} />
|
||||||
|
{' '}
|
||||||
|
Execute TX
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withStyles(styles)(ButtonRow)
|
|
@ -0,0 +1,67 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
|
import MuiList from '@material-ui/core/List'
|
||||||
|
import ListItem from '@material-ui/core/ListItem'
|
||||||
|
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||||
|
import ListItemText from '@material-ui/core/ListItemText'
|
||||||
|
import Chip from '@material-ui/core/Chip'
|
||||||
|
import Identicon from '~/components/Identicon'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
|
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
||||||
|
import { secondary } from '~/theme/variables'
|
||||||
|
import { shortVersionOf } from '~/logic/wallets/ethAddresses'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type ListProps = {
|
||||||
|
owners: List<Owner>,
|
||||||
|
classes: Object,
|
||||||
|
executionConfirmation?: Owner,
|
||||||
|
}
|
||||||
|
|
||||||
|
type OwnerProps = {
|
||||||
|
owner: Owner,
|
||||||
|
classes: Object,
|
||||||
|
isExecutor?: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const openIconStyle = {
|
||||||
|
height: '13px',
|
||||||
|
color: secondary,
|
||||||
|
}
|
||||||
|
|
||||||
|
const OwnerComponent = withStyles(styles)(({ owner, classes, isExecutor }: OwnerProps) => (
|
||||||
|
<ListItem key={owner.address} className={classes.owner}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Identicon address={owner.address} diameter={32} className={classes.icon} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={owner.name}
|
||||||
|
secondary={(
|
||||||
|
<a href={getEtherScanLink('address', owner.address, 'rinkeby')} target="_blank" rel="noopener noreferrer">
|
||||||
|
{shortVersionOf(owner.address, 4)}
|
||||||
|
{' '}
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{isExecutor && <Chip label="EXECUTOR" color="secondary" />}
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
|
||||||
|
const OwnersList = ({ owners, classes, executionConfirmation }: ListProps) => (
|
||||||
|
<>
|
||||||
|
<MuiList className={classes.ownersList}>
|
||||||
|
{executionConfirmation && <OwnerComponent owner={executionConfirmation} isExecutor />}
|
||||||
|
{owners.map(owner => (
|
||||||
|
<OwnerComponent key={owner.address} owner={owner} />
|
||||||
|
))}
|
||||||
|
</MuiList>
|
||||||
|
<Hairline color="#c8ced4" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withStyles(styles)(OwnersList)
|
|
@ -0,0 +1,120 @@
|
||||||
|
// @flow
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Tabs from '@material-ui/core/Tabs'
|
||||||
|
import Tab from '@material-ui/core/Tab'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
import { TX_TYPE_CONFIRMATION } from '~/logic/safe/transactions/send'
|
||||||
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
|
import OwnersList from './List'
|
||||||
|
import ButtonRow from './ButtonRow'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
tx: Transaction,
|
||||||
|
owners: List<Owner>,
|
||||||
|
classes: Object,
|
||||||
|
granted: boolean,
|
||||||
|
threshold: number,
|
||||||
|
userAddress: string,
|
||||||
|
thresholdReached: boolean,
|
||||||
|
safeAddress: string,
|
||||||
|
onTxConfirm: Function,
|
||||||
|
onTxCancel: Function,
|
||||||
|
onTxExecute: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCancellationTransaction = (tx: Transaction, safeAddress: string) =>
|
||||||
|
!tx.value && tx.data === EMPTY_DATA && tx.recipient === safeAddress
|
||||||
|
|
||||||
|
const OwnersColumn = ({
|
||||||
|
tx,
|
||||||
|
owners,
|
||||||
|
classes,
|
||||||
|
granted,
|
||||||
|
threshold,
|
||||||
|
userAddress,
|
||||||
|
thresholdReached,
|
||||||
|
safeAddress,
|
||||||
|
onTxConfirm,
|
||||||
|
onTxCancel,
|
||||||
|
onTxExecute,
|
||||||
|
}: Props) => {
|
||||||
|
const [tabIndex, setTabIndex] = useState(0)
|
||||||
|
const handleTabChange = (event, tabClicked) => {
|
||||||
|
setTabIndex(tabClicked)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancellationTx = isCancellationTransaction(tx, safeAddress)
|
||||||
|
|
||||||
|
const ownersWhoConfirmed = []
|
||||||
|
let currentUserAlreadyConfirmed = false
|
||||||
|
let executionConfirmation
|
||||||
|
|
||||||
|
tx.confirmations.forEach((conf) => {
|
||||||
|
if (conf.owner.address === userAddress) {
|
||||||
|
currentUserAlreadyConfirmed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conf.type === TX_TYPE_CONFIRMATION) {
|
||||||
|
ownersWhoConfirmed.push(conf.owner)
|
||||||
|
} else {
|
||||||
|
executionConfirmation = conf.owner
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const ownersUnconfirmed = owners.filter(
|
||||||
|
owner => tx.confirmations.findIndex(conf => conf.owner.address === owner.address) === -1,
|
||||||
|
)
|
||||||
|
|
||||||
|
let displayButtonRow = true
|
||||||
|
if (tx.executionTxHash) {
|
||||||
|
// One of owners already executed the tx
|
||||||
|
displayButtonRow = false
|
||||||
|
} else if (tx.status === 'cancelled') {
|
||||||
|
// tx is cancelled (replaced) by another one
|
||||||
|
displayButtonRow = false
|
||||||
|
} else if (cancellationTx && currentUserAlreadyConfirmed && !thresholdReached) {
|
||||||
|
// the TX is the cancellation (replacement) transaction for previous TX,
|
||||||
|
// current user has already confirmed it and threshold is not reached (so he can't execute/cancel it)
|
||||||
|
displayButtonRow = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let confirmedLabel = `Confirmed [${tx.confirmations.size}/${threshold}]`
|
||||||
|
if (tx.executionTxHash) {
|
||||||
|
confirmedLabel = `Confirmed [${tx.confirmations.size}]`
|
||||||
|
}
|
||||||
|
const unconfirmedLabel = `Unconfirmed [${ownersUnconfirmed.size}]`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col xs={6} className={classes.rightCol} layout="block">
|
||||||
|
<Row>
|
||||||
|
<Tabs value={tabIndex} onChange={handleTabChange} indicatorColor="secondary" textColor="secondary">
|
||||||
|
<Tab label={confirmedLabel} />
|
||||||
|
<Tab label={unconfirmedLabel} />
|
||||||
|
</Tabs>
|
||||||
|
<Hairline color="#c8ced4" />
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
{tabIndex === 0 && <OwnersList owners={ownersWhoConfirmed} executionConfirmation={executionConfirmation} />}
|
||||||
|
</Row>
|
||||||
|
<Row>{tabIndex === 1 && <OwnersList owners={ownersUnconfirmed} />}</Row>
|
||||||
|
{granted && displayButtonRow && (
|
||||||
|
<ButtonRow
|
||||||
|
onTxConfirm={onTxConfirm}
|
||||||
|
onTxCancel={onTxCancel}
|
||||||
|
showConfirmBtn={!currentUserAlreadyConfirmed && !thresholdReached}
|
||||||
|
showCancelBtn={!cancellationTx}
|
||||||
|
showExecuteBtn={thresholdReached}
|
||||||
|
onTxExecute={onTxExecute}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(OwnersColumn)
|
|
@ -0,0 +1,21 @@
|
||||||
|
// @flow
|
||||||
|
import { border, sm } from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
ownersList: {
|
||||||
|
width: '100%',
|
||||||
|
padding: 0,
|
||||||
|
height: '192px',
|
||||||
|
overflowY: 'scroll',
|
||||||
|
},
|
||||||
|
rightCol: {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
borderLeft: 'solid 1px #c8ced4',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginRight: sm,
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
borderBottom: `1px solid ${border}`,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,94 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
import Bold from '~/components/layout/Bold'
|
||||||
|
import EtherscanLink from '~/components/EtherscanLink'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import { md, lg, secondary } from '~/theme/variables'
|
||||||
|
import { getTxData } from './utils'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
txDataContainer: {
|
||||||
|
padding: `${lg} ${md}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
classes: Object,
|
||||||
|
tx: Transaction,
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransferDescProps = {
|
||||||
|
value: string,
|
||||||
|
symbol: string,
|
||||||
|
recipient: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
type DescriptionDescProps = {
|
||||||
|
removedOwner?: string,
|
||||||
|
addedOwner?: string,
|
||||||
|
newThreshold?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransferDescription = ({ value = '', symbol, recipient }: TransferDescProps) => (
|
||||||
|
<Paragraph noMargin>
|
||||||
|
<Bold>
|
||||||
|
Send
|
||||||
|
{' '}
|
||||||
|
{value}
|
||||||
|
{' '}
|
||||||
|
{symbol}
|
||||||
|
{' '}
|
||||||
|
to:
|
||||||
|
</Bold>
|
||||||
|
<br />
|
||||||
|
<EtherscanLink type="address" value={recipient} />
|
||||||
|
</Paragraph>
|
||||||
|
)
|
||||||
|
|
||||||
|
const SettingsDescription = ({ removedOwner, addedOwner, newThreshold }: DescriptionDescProps) => (
|
||||||
|
<>
|
||||||
|
{removedOwner && (
|
||||||
|
<Paragraph>
|
||||||
|
<Bold>Remove owner:</Bold>
|
||||||
|
<br />
|
||||||
|
<EtherscanLink type="address" value={removedOwner} />
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
{addedOwner && (
|
||||||
|
<Paragraph>
|
||||||
|
<Bold>Add owner:</Bold>
|
||||||
|
<br />
|
||||||
|
<EtherscanLink type="address" value={addedOwner} />
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
{newThreshold && (
|
||||||
|
<Paragraph>
|
||||||
|
<Bold>Change required confirmations:</Bold>
|
||||||
|
<br />
|
||||||
|
{newThreshold}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
const TxDescription = ({ tx, classes }: Props) => {
|
||||||
|
const {
|
||||||
|
recipient, value, modifySettingsTx, removedOwner, addedOwner, newThreshold, cancellationTx,
|
||||||
|
} = getTxData(tx)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Block className={classes.txDataContainer}>
|
||||||
|
{modifySettingsTx && (
|
||||||
|
<SettingsDescription removedOwner={removedOwner} newThreshold={newThreshold} addedOwner={addedOwner} />
|
||||||
|
)}
|
||||||
|
{!cancellationTx && !modifySettingsTx && (
|
||||||
|
<TransferDescription value={value} symbol={tx.symbol} recipient={recipient} />
|
||||||
|
)}
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(TxDescription)
|
|
@ -0,0 +1,53 @@
|
||||||
|
// @flow
|
||||||
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const { toBN, fromWei } = web3.utils
|
||||||
|
|
||||||
|
type DecodedTxData = {
|
||||||
|
recipient: string,
|
||||||
|
value?: string,
|
||||||
|
modifySettingsTx?: boolean,
|
||||||
|
removedOwner?: string,
|
||||||
|
newThreshold?: string,
|
||||||
|
addedOwner?: string,
|
||||||
|
cancellationTx?: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTxData = (tx: Transaction): DecodedTxData => {
|
||||||
|
const txData = {}
|
||||||
|
|
||||||
|
if (tx.isTokenTransfer && tx.decodedParams) {
|
||||||
|
txData.recipient = tx.decodedParams.recipient
|
||||||
|
txData.value = fromWei(toBN(tx.decodedParams.value), 'ether')
|
||||||
|
} else if (Number(tx.value) > 0) {
|
||||||
|
txData.recipient = tx.recipient
|
||||||
|
txData.value = fromWei(toBN(tx.value), 'ether')
|
||||||
|
} else if (tx.modifySettingsTx) {
|
||||||
|
txData.recipient = tx.recipient
|
||||||
|
txData.modifySettingsTx = true
|
||||||
|
|
||||||
|
if (tx.decodedParams) {
|
||||||
|
/* eslint-disable */
|
||||||
|
if (tx.decodedParams.methodName === 'removeOwner') {
|
||||||
|
txData.removedOwner = tx.decodedParams.args[1]
|
||||||
|
txData.newThreshold = tx.decodedParams.args[2]
|
||||||
|
} else if (tx.decodedParams.methodName === 'changeThreshold') {
|
||||||
|
txData.newThreshold = tx.decodedParams.args[0]
|
||||||
|
} else if (tx.decodedParams.methodName === 'addOwnerWithThreshold') {
|
||||||
|
txData.addedOwner = tx.decodedParams.args[0]
|
||||||
|
txData.newThreshold = tx.decodedParams.args[1]
|
||||||
|
} else if (tx.decodedParams.methodName === 'swapOwner') {
|
||||||
|
txData.addedOwner = tx.decodedParams.args[0]
|
||||||
|
txData.removedOwner = tx.decodedParams.args[1]
|
||||||
|
txData.newThreshold = tx.decodedParams.args[2]
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
}
|
||||||
|
} else if (tx.cancellationTx) {
|
||||||
|
txData.cancellationTx = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return txData
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
// @flow
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Bold from '~/components/layout/Bold'
|
||||||
|
import Span from '~/components/layout/Span'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
|
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
|
||||||
|
import { shortVersionOf } from '~/logic/wallets/ethAddresses'
|
||||||
|
import { secondary } from '~/theme/variables'
|
||||||
|
import TxDescription from './TxDescription'
|
||||||
|
import OwnersColumn from './OwnersColumn'
|
||||||
|
import CancelTxModal from './CancelTxModal'
|
||||||
|
import ApproveTxModal from './ApproveTxModal'
|
||||||
|
import { styles } from './style'
|
||||||
|
import { formatDate } from '../columns'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
classes: Object,
|
||||||
|
tx: Transaction,
|
||||||
|
threshold: number,
|
||||||
|
owners: List<Owner>,
|
||||||
|
granted: boolean,
|
||||||
|
userAddress: string,
|
||||||
|
safeAddress: string,
|
||||||
|
createTransaction: Function,
|
||||||
|
processTransaction: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenModal = 'cancelTx' | 'approveTx' | null
|
||||||
|
|
||||||
|
const openIconStyle = {
|
||||||
|
height: '13px',
|
||||||
|
color: secondary,
|
||||||
|
}
|
||||||
|
|
||||||
|
const txStatusToLabel = {
|
||||||
|
success: 'Success',
|
||||||
|
awaiting_confirmations: 'Awaiting confirmations',
|
||||||
|
cancelled: 'Cancelled',
|
||||||
|
awaiting_execution: 'Awaiting execution',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpandedTx = ({
|
||||||
|
classes,
|
||||||
|
tx,
|
||||||
|
threshold,
|
||||||
|
owners,
|
||||||
|
granted,
|
||||||
|
userAddress,
|
||||||
|
safeAddress,
|
||||||
|
createTransaction,
|
||||||
|
processTransaction,
|
||||||
|
}: Props) => {
|
||||||
|
const [openModal, setOpenModal] = useState<OpenModal>(null)
|
||||||
|
const openApproveModal = () => setOpenModal('approveTx')
|
||||||
|
const openCancelModal = () => setOpenModal('cancelTx')
|
||||||
|
const closeModal = () => setOpenModal(null)
|
||||||
|
const thresholdReached = threshold <= tx.confirmations.size
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Block>
|
||||||
|
<Row>
|
||||||
|
<Col xs={6} layout="column">
|
||||||
|
<Block className={classes.txDataContainer}>
|
||||||
|
<Paragraph noMargin>
|
||||||
|
<Bold>TX hash: </Bold>
|
||||||
|
{tx.executionTxHash ? (
|
||||||
|
<a href={getEtherScanLink('tx', tx.executionTxHash, 'rinkeby')} target="_blank" rel="noopener noreferrer">
|
||||||
|
{shortVersionOf(tx.executionTxHash, 4)}
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph noMargin>
|
||||||
|
<Bold>TX status: </Bold>
|
||||||
|
<Span className={classes[tx.status]} style={{ fontWeight: 'bold' }}>
|
||||||
|
{txStatusToLabel[tx.status]}
|
||||||
|
</Span>
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph noMargin>
|
||||||
|
<Bold>TX created: </Bold>
|
||||||
|
{formatDate(tx.submissionDate)}
|
||||||
|
</Paragraph>
|
||||||
|
{tx.executionDate && (
|
||||||
|
<Paragraph noMargin>
|
||||||
|
<Bold>TX executed: </Bold>
|
||||||
|
{formatDate(tx.executionDate)}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
</Block>
|
||||||
|
<Hairline />
|
||||||
|
<TxDescription tx={tx} />
|
||||||
|
</Col>
|
||||||
|
<OwnersColumn
|
||||||
|
tx={tx}
|
||||||
|
owners={owners}
|
||||||
|
granted={granted}
|
||||||
|
threshold={threshold}
|
||||||
|
userAddress={userAddress}
|
||||||
|
thresholdReached={thresholdReached}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
onTxConfirm={openApproveModal}
|
||||||
|
onTxCancel={openCancelModal}
|
||||||
|
onTxExecute={openApproveModal}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<CancelTxModal
|
||||||
|
isOpen={openModal === 'cancelTx'}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
onClose={closeModal}
|
||||||
|
tx={tx}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
/>
|
||||||
|
<ApproveTxModal
|
||||||
|
isOpen={openModal === 'approveTx'}
|
||||||
|
processTransaction={processTransaction}
|
||||||
|
onClose={closeModal}
|
||||||
|
tx={tx}
|
||||||
|
userAddress={userAddress}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
threshold={threshold}
|
||||||
|
thresholdReached={thresholdReached}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(ExpandedTx)
|
|
@ -0,0 +1,22 @@
|
||||||
|
// @flow
|
||||||
|
import {
|
||||||
|
md, lg, connected, error,
|
||||||
|
} from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
txDataContainer: {
|
||||||
|
padding: `${lg} ${md}`,
|
||||||
|
},
|
||||||
|
awaiting_confirmations: {
|
||||||
|
color: '#2e73d9',
|
||||||
|
},
|
||||||
|
awaiting_execution: {
|
||||||
|
color: '#2e73d9',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
color: connected,
|
||||||
|
},
|
||||||
|
cancelled: {
|
||||||
|
color: error,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="13" viewBox="0 0 14 13">
|
||||||
|
<path fill="#2E73D9" fill-rule="nonzero" d="M2.332 0C1.412 0 .666.746.666 1.666v.333A.666.666 0 0 0 0 2.665V5.33c0 .368.298.666.666.666h3.332a.666.666 0 0 0 .666-.666V2.665A.666.666 0 0 0 3.998 2v-.333C3.998.746 3.252 0 2.332 0zm0 .666a1 1 0 0 1 1 1v.333h-2v-.333a1 1 0 0 1 1-1zm9.434 0a.634.634 0 0 0-.46.187l-1.225 1.232 2.498 2.499 1.226-1.233a.655.655 0 0 0 0-.932L12.246.853a.703.703 0 0 0-.48-.187zM9.368 2.792l-7.37 7.369v2.498h2.5l7.368-7.369-2.498-2.498z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 562 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
|
||||||
|
<path fill="#FD7890" fill-rule="nonzero" d="M7.7 7.7H6.3V3.5h1.4v4.2zm0 2.8H6.3V9.1h1.4v1.4zM7 0a7 7 0 1 0 0 14A7 7 0 0 0 7 0z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 225 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
|
||||||
|
<path fill="#346D6D" fill-rule="nonzero" d="M7 0a7 7 0 1 1 0 14A7 7 0 0 1 7 0zm-.913 9.8L11.2 5.139 10.17 4.2 6.087 7.916 3.83 5.865l-1.03.939L6.087 9.8z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 252 B |
|
@ -0,0 +1,44 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph/'
|
||||||
|
import Img from '~/components/layout/Img'
|
||||||
|
import { type TransactionStatus } from '~/routes/safe/store/models/transaction'
|
||||||
|
import ErrorIcon from './assets/error.svg'
|
||||||
|
import OkIcon from './assets/ok.svg'
|
||||||
|
import AwaitingIcon from './assets/awaiting.svg'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
classes: Object,
|
||||||
|
status: TransactionStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusToIcon = {
|
||||||
|
success: OkIcon,
|
||||||
|
cancelled: ErrorIcon,
|
||||||
|
awaiting_confirmations: AwaitingIcon,
|
||||||
|
awaiting_execution: AwaitingIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusToLabel = {
|
||||||
|
success: 'Success',
|
||||||
|
cancelled: 'Cancelled',
|
||||||
|
awaiting_confirmations: 'Awaiting',
|
||||||
|
awaiting_execution: 'Awaiting',
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusIconStyle = {
|
||||||
|
height: '14px',
|
||||||
|
width: '14px',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Status = ({ classes, status }: Props) => (
|
||||||
|
<Block className={`${classes.container} ${classes[status]}`}>
|
||||||
|
<Img src={statusToIcon[status]} alt="OK Icon" style={statusIconStyle} />
|
||||||
|
<Paragraph noMargin className={classes.statusText}>{statusToLabel[status]}</Paragraph>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withStyles(styles)(Status)
|
|
@ -0,0 +1,34 @@
|
||||||
|
// @flow
|
||||||
|
import { smallFontSize, boldFont, sm } from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
fontSize: smallFontSize,
|
||||||
|
fontWeight: boldFont,
|
||||||
|
width: '100px',
|
||||||
|
padding: sm,
|
||||||
|
alignItems: 'center',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
backgroundColor: '#d7f3f3',
|
||||||
|
color: '#346d6d',
|
||||||
|
},
|
||||||
|
cancelled: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: '#fd7890',
|
||||||
|
},
|
||||||
|
awaiting_confirmations: {
|
||||||
|
backgroundColor: '#dfebff',
|
||||||
|
color: '#2e73d9',
|
||||||
|
},
|
||||||
|
awaiting_execution: {
|
||||||
|
backgroundColor: '#dfebff',
|
||||||
|
color: '#2e73d9',
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,124 @@
|
||||||
|
// @flow
|
||||||
|
import { format, getTime } from 'date-fns'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
import { type SortRow, buildOrderFieldFrom } from '~/components/Table/sorting'
|
||||||
|
import { type Column } from '~/components/Table/TableHead'
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
|
||||||
|
export const TX_TABLE_NONCE_ID = 'nonce'
|
||||||
|
export const TX_TABLE_TYPE_ID = 'type'
|
||||||
|
export const TX_TABLE_DATE_ID = 'date'
|
||||||
|
export const TX_TABLE_AMOUNT_ID = 'amount'
|
||||||
|
export const TX_TABLE_STATUS_ID = 'status'
|
||||||
|
export const TX_TABLE_RAW_TX_ID = 'tx'
|
||||||
|
export const TX_TABLE_EXPAND_ICON = 'expand'
|
||||||
|
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const { toBN, fromWei } = web3.utils
|
||||||
|
|
||||||
|
type TxData = {
|
||||||
|
nonce: number,
|
||||||
|
type: string,
|
||||||
|
date: string,
|
||||||
|
amount: number | string,
|
||||||
|
tx: Transaction,
|
||||||
|
status?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatDate = (date: Date): string => format(date, 'MMM D, YYYY - HH:mm:ss')
|
||||||
|
|
||||||
|
export const getTxAmount = (tx: Transaction) => {
|
||||||
|
let txAmount = 'n/a'
|
||||||
|
|
||||||
|
if (tx.isTokenTransfer && tx.decodedParams) {
|
||||||
|
txAmount = `${fromWei(toBN(tx.decodedParams.value), 'ether')} ${tx.symbol}`
|
||||||
|
} else if (Number(tx.value) > 0) {
|
||||||
|
txAmount = `${fromWei(toBN(tx.value), 'ether')} ${tx.symbol}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return txAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TransactionRow = SortRow<TxData>
|
||||||
|
|
||||||
|
export const getTxTableData = (transactions: List<Transaction>): List<TransactionRow> => {
|
||||||
|
const rows = transactions.map((tx: Transaction) => {
|
||||||
|
const txDate = tx.isExecuted ? tx.executionDate : tx.submissionDate
|
||||||
|
let txType = 'Outgoing transfer'
|
||||||
|
if (tx.modifySettingsTx) {
|
||||||
|
txType = 'Modify Safe Settings'
|
||||||
|
} else if (tx.cancellationTx) {
|
||||||
|
txType = 'Cancellation transaction'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[TX_TABLE_NONCE_ID]: tx.nonce,
|
||||||
|
[TX_TABLE_TYPE_ID]: txType,
|
||||||
|
[TX_TABLE_DATE_ID]: formatDate(tx.isExecuted ? tx.executionDate : tx.submissionDate),
|
||||||
|
[buildOrderFieldFrom(TX_TABLE_DATE_ID)]: getTime(txDate),
|
||||||
|
[TX_TABLE_AMOUNT_ID]: getTxAmount(tx),
|
||||||
|
[TX_TABLE_STATUS_ID]: tx.status,
|
||||||
|
[TX_TABLE_RAW_TX_ID]: tx,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateColumns = () => {
|
||||||
|
const nonceColumn: Column = {
|
||||||
|
id: TX_TABLE_NONCE_ID,
|
||||||
|
disablePadding: false,
|
||||||
|
label: 'Nonce',
|
||||||
|
custom: false,
|
||||||
|
order: false,
|
||||||
|
width: 50,
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeColumn: Column = {
|
||||||
|
id: TX_TABLE_TYPE_ID,
|
||||||
|
order: false,
|
||||||
|
disablePadding: false,
|
||||||
|
label: 'Type',
|
||||||
|
custom: false,
|
||||||
|
width: 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueColumn: Column = {
|
||||||
|
id: TX_TABLE_AMOUNT_ID,
|
||||||
|
order: false,
|
||||||
|
disablePadding: false,
|
||||||
|
label: 'Amount',
|
||||||
|
custom: false,
|
||||||
|
width: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateColumn: Column = {
|
||||||
|
id: TX_TABLE_DATE_ID,
|
||||||
|
disablePadding: false,
|
||||||
|
order: true,
|
||||||
|
label: 'Date',
|
||||||
|
custom: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusColumn: Column = {
|
||||||
|
id: TX_TABLE_STATUS_ID,
|
||||||
|
order: false,
|
||||||
|
disablePadding: false,
|
||||||
|
label: 'Status',
|
||||||
|
custom: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandIconColumn: Column = {
|
||||||
|
id: TX_TABLE_EXPAND_ICON,
|
||||||
|
order: false,
|
||||||
|
disablePadding: true,
|
||||||
|
label: '',
|
||||||
|
custom: true,
|
||||||
|
width: 50,
|
||||||
|
static: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return List([nonceColumn, typeColumn, valueColumn, dateColumn, statusColumn, expandIconColumn])
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
// @flow
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import cn from 'classnames'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import Collapse from '@material-ui/core/Collapse'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import ExpandLess from '@material-ui/icons/ExpandLess'
|
||||||
|
import ExpandMore from '@material-ui/icons/ExpandMore'
|
||||||
|
import TableRow from '@material-ui/core/TableRow'
|
||||||
|
import TableCell from '@material-ui/core/TableCell'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import { type Column, cellWidth } from '~/components/Table/TableHead'
|
||||||
|
import Table from '~/components/Table'
|
||||||
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
|
import ExpandedTxComponent from './ExpandedTx'
|
||||||
|
import {
|
||||||
|
getTxTableData, generateColumns, TX_TABLE_DATE_ID, type TransactionRow, TX_TABLE_RAW_TX_ID,
|
||||||
|
} from './columns'
|
||||||
|
import { styles } from './style'
|
||||||
|
import Status from './Status'
|
||||||
|
|
||||||
|
const expandCellStyle = {
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
classes: Object,
|
||||||
|
transactions: List<Transaction>,
|
||||||
|
threshold: number,
|
||||||
|
owners: List<Owner>,
|
||||||
|
userAddress: string,
|
||||||
|
granted: boolean,
|
||||||
|
safeAddress: string,
|
||||||
|
createTransaction: Function,
|
||||||
|
processTransaction: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TxsTable = ({
|
||||||
|
classes,
|
||||||
|
transactions,
|
||||||
|
threshold,
|
||||||
|
owners,
|
||||||
|
granted,
|
||||||
|
userAddress,
|
||||||
|
safeAddress,
|
||||||
|
createTransaction,
|
||||||
|
processTransaction,
|
||||||
|
}: Props) => {
|
||||||
|
const [expandedTx, setExpandedTx] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const handleTxExpand = (creationTxHash) => {
|
||||||
|
setExpandedTx(prevTx => (prevTx === creationTxHash ? null : creationTxHash))
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = generateColumns()
|
||||||
|
const autoColumns = columns.filter(c => !c.custom)
|
||||||
|
const filteredData = getTxTableData(transactions)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Block className={classes.container}>
|
||||||
|
<Table
|
||||||
|
label="Transactions"
|
||||||
|
defaultOrderBy={TX_TABLE_DATE_ID}
|
||||||
|
defaultOrder="desc"
|
||||||
|
columns={columns}
|
||||||
|
data={filteredData}
|
||||||
|
size={filteredData.size}
|
||||||
|
defaultFixed
|
||||||
|
>
|
||||||
|
{(sortedData: Array<TransactionRow>) => sortedData.map((row: any, index: number) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<TableRow
|
||||||
|
tabIndex={-1}
|
||||||
|
className={cn(classes.row, expandedTx === row.tx.creationTxHash && classes.expandedRow)}
|
||||||
|
onClick={() => handleTxExpand(row.tx.creationTxHash)}
|
||||||
|
>
|
||||||
|
{autoColumns.map((column: Column) => (
|
||||||
|
<TableCell
|
||||||
|
key={column.id}
|
||||||
|
className={classes.cell}
|
||||||
|
style={cellWidth(column.width)}
|
||||||
|
align={column.align}
|
||||||
|
component="td"
|
||||||
|
>
|
||||||
|
{row[column.id]}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
<TableCell component="td">
|
||||||
|
<Row align="end" className={classes.actions}>
|
||||||
|
<Status status={row.status} />
|
||||||
|
</Row>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell style={expandCellStyle}>
|
||||||
|
<IconButton disableRipple>{expandedTx === row.nonce ? <ExpandLess /> : <ExpandMore />}</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
style={{ paddingBottom: 0, paddingTop: 0 }}
|
||||||
|
colSpan={6}
|
||||||
|
className={classes.extendedTxContainer}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
in={expandedTx === row.tx.creationTxHash}
|
||||||
|
timeout="auto"
|
||||||
|
component={ExpandedTxComponent}
|
||||||
|
unmountOnExit
|
||||||
|
tx={row[TX_TABLE_RAW_TX_ID]}
|
||||||
|
threshold={threshold}
|
||||||
|
owners={owners}
|
||||||
|
granted={granted}
|
||||||
|
userAddress={userAddress}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
processTransaction={processTransaction}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Table>
|
||||||
|
</Block>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(TxsTable)
|
|
@ -0,0 +1,24 @@
|
||||||
|
// @flow
|
||||||
|
import { lg } from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
container: {
|
||||||
|
marginTop: lg,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#fff3e2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expandedRow: {
|
||||||
|
backgroundColor: '#fff3e2',
|
||||||
|
},
|
||||||
|
extendedTxContainer: {
|
||||||
|
padding: 0,
|
||||||
|
'&:last-child': {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
backgroundColor: '#fffaf4',
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,58 @@
|
||||||
|
// @flow
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import NoTransactions from '~/routes/safe/components/TransactionsNew/NoTransactions'
|
||||||
|
import TxsTable from '~/routes/safe/components/TransactionsNew/TxsTable'
|
||||||
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
safeAddress: string,
|
||||||
|
threshold: number,
|
||||||
|
fetchTransactions: Function,
|
||||||
|
transactions: List<Transaction>,
|
||||||
|
owners: List<Owner>,
|
||||||
|
userAddress: string,
|
||||||
|
granted: boolean,
|
||||||
|
createTransaction: Function,
|
||||||
|
processTransaction: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Transactions = ({
|
||||||
|
transactions = List(),
|
||||||
|
owners,
|
||||||
|
threshold,
|
||||||
|
userAddress,
|
||||||
|
granted,
|
||||||
|
safeAddress,
|
||||||
|
createTransaction,
|
||||||
|
processTransaction,
|
||||||
|
fetchTransactions,
|
||||||
|
}: Props) => {
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTransactions(safeAddress)
|
||||||
|
}, [safeAddress])
|
||||||
|
|
||||||
|
const hasTransactions = transactions.size > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{hasTransactions ? (
|
||||||
|
<TxsTable
|
||||||
|
transactions={transactions}
|
||||||
|
threshold={threshold}
|
||||||
|
owners={owners}
|
||||||
|
userAddress={userAddress}
|
||||||
|
granted={granted}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
processTransaction={processTransaction}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<NoTransactions />
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Transactions
|
|
@ -2,18 +2,27 @@
|
||||||
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
|
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
|
||||||
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||||
|
import processTransaction from '~/routes/safe/store/actions/processTransaction'
|
||||||
|
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
||||||
|
import fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
|
|
||||||
export type Actions = {
|
export type Actions = {
|
||||||
fetchSafe: typeof fetchSafe,
|
fetchSafe: typeof fetchSafe,
|
||||||
fetchTokenBalances: typeof fetchTokenBalances,
|
fetchTokenBalances: typeof fetchTokenBalances,
|
||||||
createTransaction: typeof createTransaction,
|
createTransaction: typeof createTransaction,
|
||||||
|
fetchTransactions: typeof fetchTransactions,
|
||||||
updateSafe: typeof updateSafe,
|
updateSafe: typeof updateSafe,
|
||||||
|
fetchTokens: typeof fetchTokens,
|
||||||
|
processTransaction: typeof processTransaction,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fetchSafe,
|
fetchSafe,
|
||||||
fetchTokenBalances,
|
fetchTokenBalances,
|
||||||
createTransaction,
|
createTransaction,
|
||||||
|
processTransaction,
|
||||||
|
fetchTokens,
|
||||||
|
fetchTransactions,
|
||||||
updateSafe,
|
updateSafe,
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,13 @@ const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000
|
||||||
class SafeView extends React.Component<Props> {
|
class SafeView extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {
|
const {
|
||||||
fetchSafe, activeTokens, safeUrl, fetchTokenBalances,
|
fetchSafe, activeTokens, safeUrl, fetchTokenBalances, fetchTokens,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
fetchSafe(safeUrl)
|
fetchSafe(safeUrl)
|
||||||
fetchTokenBalances(safeUrl, activeTokens)
|
fetchTokenBalances(safeUrl, activeTokens)
|
||||||
|
// fetch tokens there to get symbols for tokens in TXs list
|
||||||
|
fetchTokens()
|
||||||
|
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = setInterval(() => {
|
||||||
this.checkForUpdates()
|
this.checkForUpdates()
|
||||||
|
@ -61,7 +63,10 @@ class SafeView extends React.Component<Props> {
|
||||||
network,
|
network,
|
||||||
tokens,
|
tokens,
|
||||||
createTransaction,
|
createTransaction,
|
||||||
|
processTransaction,
|
||||||
|
fetchTransactions,
|
||||||
updateSafe,
|
updateSafe,
|
||||||
|
transactions,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -75,7 +80,10 @@ class SafeView extends React.Component<Props> {
|
||||||
network={network}
|
network={network}
|
||||||
granted={granted}
|
granted={granted}
|
||||||
createTransaction={createTransaction}
|
createTransaction={createTransaction}
|
||||||
|
processTransaction={processTransaction}
|
||||||
|
fetchTransactions={fetchTransactions}
|
||||||
updateSafe={updateSafe}
|
updateSafe={updateSafe}
|
||||||
|
transactions={transactions}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
import { createSelector, createStructuredSelector, type Selector } from 'reselect'
|
import { createSelector, createStructuredSelector, type Selector } from 'reselect'
|
||||||
|
import { isAfter } from 'date-fns'
|
||||||
import {
|
import {
|
||||||
safeSelector,
|
safeSelector,
|
||||||
safeActiveTokensSelector,
|
safeActiveTokensSelector,
|
||||||
|
@ -13,8 +14,10 @@ import { type Safe } from '~/routes/safe/store/models/safe'
|
||||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||||
import { type GlobalState } from '~/store'
|
import { type GlobalState } from '~/store'
|
||||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||||
|
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
||||||
import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors'
|
import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
|
import { type Transaction, type TransactionStatus } from '~/routes/safe/store/models/transaction'
|
||||||
import { type TokenBalance } from '~/routes/safe/store/models/tokenBalance'
|
import { type TokenBalance } from '~/routes/safe/store/models/tokenBalance'
|
||||||
import { safeParamAddressSelector } from '../store/selectors'
|
import { safeParamAddressSelector } from '../store/selectors'
|
||||||
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
|
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
|
@ -27,6 +30,21 @@ export type SelectorProps = {
|
||||||
userAddress: string,
|
userAddress: string,
|
||||||
network: string,
|
network: string,
|
||||||
safeUrl: string,
|
safeUrl: string,
|
||||||
|
transactions: List<Transaction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTxStatus = (tx: Transaction, safe: Safe): TransactionStatus => {
|
||||||
|
let txStatus = 'awaiting_confirmations'
|
||||||
|
|
||||||
|
if (tx.executionTxHash) {
|
||||||
|
txStatus = 'success'
|
||||||
|
} else if (tx.cancelled) {
|
||||||
|
txStatus = 'cancelled'
|
||||||
|
} else if (tx.confirmations.size === safe.threshold) {
|
||||||
|
txStatus = 'awaiting_execution'
|
||||||
|
}
|
||||||
|
|
||||||
|
return txStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = createSelector(
|
export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = createSelector(
|
||||||
|
@ -86,6 +104,32 @@ const extendedSafeTokensSelector: Selector<GlobalState, RouterProps, List<Token>
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const extendedTransactionsSelector: Selector<GlobalState, RouterProps, List<Transaction>> = createSelector(
|
||||||
|
safeSelector,
|
||||||
|
safeTransactionsSelector,
|
||||||
|
(safe, transactions) => {
|
||||||
|
const extendedTransactions = transactions.map((tx: Transaction) => {
|
||||||
|
let extendedTx = tx
|
||||||
|
|
||||||
|
// If transactions is not executed, but there's a transaction with the same nonce submitted later
|
||||||
|
// it means that the transaction was cancelled (Replaced) and shouldn't get executed
|
||||||
|
let replacementTransaction
|
||||||
|
if (!tx.isExecuted) {
|
||||||
|
replacementTransaction = transactions.findLast(
|
||||||
|
transaction => transaction.nonce === tx.nonce && isAfter(transaction.submissionDate, tx.submissionDate),
|
||||||
|
)
|
||||||
|
if (replacementTransaction) {
|
||||||
|
extendedTx = tx.set('cancelled', true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extendedTx.set('status', getTxStatus(extendedTx, safe))
|
||||||
|
})
|
||||||
|
|
||||||
|
return extendedTransactions
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export default createStructuredSelector<Object, *>({
|
export default createStructuredSelector<Object, *>({
|
||||||
safe: safeSelector,
|
safe: safeSelector,
|
||||||
provider: providerNameSelector,
|
provider: providerNameSelector,
|
||||||
|
@ -95,4 +139,5 @@ export default createStructuredSelector<Object, *>({
|
||||||
userAddress: userAccountSelector,
|
userAddress: userAccountSelector,
|
||||||
network: networkSelector,
|
network: networkSelector,
|
||||||
safeUrl: safeParamAddressSelector,
|
safeUrl: safeParamAddressSelector,
|
||||||
|
transactions: extendedTransactionsSelector,
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
// @flow
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
|
||||||
|
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
|
||||||
|
export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)
|
|
@ -1,14 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||||
|
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
import { type GlobalState } from '~/store'
|
import { type GlobalState } from '~/store'
|
||||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
import { executeTransaction, CALL } from '~/logic/safe/transactions'
|
import { approveTransaction, executeTransaction, CALL } from '~/logic/safe/transactions'
|
||||||
|
|
||||||
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
|
|
||||||
export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)
|
|
||||||
|
|
||||||
const createTransaction = (
|
const createTransaction = (
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
|
@ -16,14 +13,15 @@ const createTransaction = (
|
||||||
valueInWei: string,
|
valueInWei: string,
|
||||||
txData: string = EMPTY_DATA,
|
txData: string = EMPTY_DATA,
|
||||||
openSnackbar: Function,
|
openSnackbar: Function,
|
||||||
|
shouldExecute?: boolean,
|
||||||
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
|
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
|
||||||
const state: GlobalState = getState()
|
const state: GlobalState = getState()
|
||||||
|
|
||||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
const from = userAccountSelector(state)
|
const from = userAccountSelector(state)
|
||||||
const threshold = await safeInstance.getThreshold()
|
const threshold = await safeInstance.getThreshold()
|
||||||
const nonce = await safeInstance.nonce()
|
const nonce = (await safeInstance.nonce()).toString()
|
||||||
const isExecution = threshold.toNumber() === 1
|
const isExecution = threshold.toNumber() === 1 || shouldExecute
|
||||||
|
|
||||||
let txHash
|
let txHash
|
||||||
if (isExecution) {
|
if (isExecution) {
|
||||||
|
@ -31,10 +29,14 @@ const createTransaction = (
|
||||||
txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
|
txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
|
||||||
openSnackbar('Transaction has been confirmed', 'success')
|
openSnackbar('Transaction has been confirmed', 'success')
|
||||||
} else {
|
} else {
|
||||||
console.log('Temporal error: threshold != 1')
|
openSnackbar('Approval transaction has been submitted', 'success')
|
||||||
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
|
txHash = await approveTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
|
||||||
|
openSnackbar('Approval transaction has been confirmed', 'success')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.NODE_ENV === 'test') {
|
||||||
|
dispatch(fetchTransactions(safeAddress))
|
||||||
}
|
}
|
||||||
// dispatch(addTransactions(txHash))
|
|
||||||
|
|
||||||
return txHash
|
return txHash
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,74 +1,123 @@
|
||||||
// // @flow
|
// @flow
|
||||||
// import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
// import axios from 'axios'
|
import axios from 'axios'
|
||||||
// import type { Dispatch as ReduxDispatch } from 'redux'
|
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||||
// import { type GlobalState } from '~/store/index'
|
import { type GlobalState } from '~/store/index'
|
||||||
// import { makeOwner } from '~/routes/safe/store/models/owner'
|
import { makeOwner } from '~/routes/safe/store/models/owner'
|
||||||
// import { makeTransaction, type Transaction } from '~/routes/safe/store/models/transaction'
|
import { makeTransaction, type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
// import { makeConfirmation } from '~/routes/safe/store/models/confirmation'
|
import { makeConfirmation } from '~/routes/safe/store/models/confirmation'
|
||||||
// import { loadSafeSubjects } from '~/utils/storage/transactions'
|
import { loadSafeSubjects } from '~/utils/storage/transactions'
|
||||||
// import { buildTxServiceUrlFrom, type TxServiceType } from '~/logic/safe/safeTxHistory'
|
import { buildTxServiceUrl, type TxServiceType } from '~/logic/safe/transactions/txHistory'
|
||||||
// import { getOwners } from '~/logic/safe/utils'
|
import { getOwners } from '~/logic/safe/utils'
|
||||||
// import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
// import addTransactions from './addTransactions'
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
|
import { addTransactions } from './addTransactions'
|
||||||
|
import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
|
import { isAddressAToken } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
|
import { TX_TYPE_EXECUTION } from '~/logic/safe/transactions/send'
|
||||||
|
import { decodeParamsFromSafeMethod } from '~/logic/contracts/methodIds'
|
||||||
|
|
||||||
// type ConfirmationServiceModel = {
|
let web3
|
||||||
// owner: string,
|
|
||||||
// submissionDate: Date,
|
|
||||||
// type: string,
|
|
||||||
// transactionHash: string,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// type TxServiceModel = {
|
type ConfirmationServiceModel = {
|
||||||
// to: string,
|
owner: string,
|
||||||
// value: number,
|
submissionDate: Date,
|
||||||
// data: string,
|
confirmationType: string,
|
||||||
// operation: number,
|
transactionHash: string,
|
||||||
// nonce: number,
|
}
|
||||||
// submissionDate: Date,
|
|
||||||
// executionDate: Date,
|
|
||||||
// confirmations: ConfirmationServiceModel[],
|
|
||||||
// isExecuted: boolean,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const buildTransactionFrom = (safeAddress: string, tx: TxServiceModel, safeSubjects: Map<string, string>) => {
|
type TxServiceModel = {
|
||||||
// const name = safeSubjects.get(String(tx.nonce)) || 'Unknown'
|
to: string,
|
||||||
// const storedOwners = getOwners(safeAddress)
|
value: number,
|
||||||
// const confirmations = List(
|
data: string,
|
||||||
// tx.confirmations.map((conf: ConfirmationServiceModel) => {
|
operation: number,
|
||||||
// const ownerName = storedOwners.get(conf.owner.toLowerCase()) || 'UNKNOWN'
|
nonce: number,
|
||||||
|
submissionDate: Date,
|
||||||
|
executionDate: Date,
|
||||||
|
confirmations: ConfirmationServiceModel[],
|
||||||
|
isExecuted: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
// return makeConfirmation({
|
const buildTransactionFrom = async (safeAddress: string, tx: TxServiceModel, safeSubjects: Map<string, string>) => {
|
||||||
// owner: makeOwner({ address: conf.owner, name: ownerName }),
|
const name = safeSubjects.get(String(tx.nonce)) || 'Unknown'
|
||||||
// type: ((conf.type.toLowerCase(): any): TxServiceType),
|
const storedOwners = await getOwners(safeAddress)
|
||||||
// hash: conf.transactionHash,
|
const confirmations = List(
|
||||||
// })
|
tx.confirmations.map((conf: ConfirmationServiceModel) => {
|
||||||
// }),
|
const ownerName = storedOwners.get(conf.owner.toLowerCase()) || 'UNKNOWN'
|
||||||
// )
|
|
||||||
|
|
||||||
// return makeTransaction({
|
return makeConfirmation({
|
||||||
// name,
|
owner: makeOwner({ address: conf.owner, name: ownerName }),
|
||||||
// nonce: tx.nonce,
|
type: ((conf.confirmationType.toLowerCase(): any): TxServiceType),
|
||||||
// value: Number(tx.value),
|
hash: conf.transactionHash,
|
||||||
// confirmations,
|
})
|
||||||
// destination: tx.to,
|
}),
|
||||||
// data: tx.data ? tx.data : EMPTY_DATA,
|
)
|
||||||
// isExecuted: tx.isExecuted,
|
const modifySettingsTx = tx.to === safeAddress && Number(tx.value) === 0 && !!tx.data
|
||||||
// })
|
const cancellationTx = tx.to === safeAddress && Number(tx.value) === 0 && !tx.data
|
||||||
// }
|
const isTokenTransfer = await isAddressAToken(tx.to)
|
||||||
|
const creationTxHash = confirmations.last().hash
|
||||||
|
|
||||||
// export const loadSafeTransactions = async (safeAddress: string) => {
|
let executionTxHash
|
||||||
// const url = buildTxServiceUrlFrom(safeAddress)
|
const executionTx = confirmations.find(conf => conf.type === TX_TYPE_EXECUTION)
|
||||||
// const response = await axios.get(url)
|
|
||||||
// const transactions: TxServiceModel[] = response.data.results
|
|
||||||
// const safeSubjects = loadSafeSubjects(safeAddress)
|
|
||||||
// const txsRecord = transactions.map((tx: TxServiceModel) => buildTransactionFrom(safeAddress, tx, safeSubjects))
|
|
||||||
|
|
||||||
// return Map().set(safeAddress, List(txsRecord))
|
if (executionTx) {
|
||||||
// }
|
executionTxHash = executionTx.hash
|
||||||
|
}
|
||||||
|
|
||||||
// export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
let symbol = 'ETH'
|
||||||
// const transactions: Map<string, List<Transaction>> = await loadSafeTransactions(safeAddress)
|
let decodedParams
|
||||||
|
if (isTokenTransfer) {
|
||||||
|
const tokenContract = await getHumanFriendlyToken()
|
||||||
|
const tokenInstance = await tokenContract.at(tx.to)
|
||||||
|
symbol = await tokenInstance.symbol()
|
||||||
|
|
||||||
// return dispatch(addTransactions(transactions))
|
const params = web3.eth.abi.decodeParameters(['address', 'uint256'], tx.data.slice(10))
|
||||||
// }
|
decodedParams = {
|
||||||
|
recipient: params[0],
|
||||||
|
value: params[1],
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
modifySettingsTx && tx.data
|
||||||
|
) {
|
||||||
|
decodedParams = await decodeParamsFromSafeMethod(tx.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeTransaction({
|
||||||
|
name,
|
||||||
|
symbol,
|
||||||
|
nonce: tx.nonce,
|
||||||
|
value: tx.value.toString(),
|
||||||
|
confirmations,
|
||||||
|
recipient: tx.to,
|
||||||
|
data: tx.data ? tx.data : EMPTY_DATA,
|
||||||
|
isExecuted: tx.isExecuted,
|
||||||
|
submissionDate: tx.submissionDate,
|
||||||
|
executionDate: tx.executionDate,
|
||||||
|
executionTxHash,
|
||||||
|
creationTxHash,
|
||||||
|
isTokenTransfer,
|
||||||
|
decodedParams,
|
||||||
|
modifySettingsTx,
|
||||||
|
cancellationTx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadSafeTransactions = async (safeAddress: string) => {
|
||||||
|
web3 = await getWeb3()
|
||||||
|
|
||||||
|
const url = buildTxServiceUrl(safeAddress)
|
||||||
|
const response = await axios.get(url)
|
||||||
|
const transactions: TxServiceModel[] = response.data.results
|
||||||
|
const safeSubjects = loadSafeSubjects(safeAddress)
|
||||||
|
const txsRecord = await Promise.all(
|
||||||
|
transactions.map((tx: TxServiceModel) => buildTransactionFrom(safeAddress, tx, safeSubjects)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Map().set(safeAddress, List(txsRecord))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
|
const transactions: Map<string, List<Transaction>> = await loadSafeTransactions(safeAddress)
|
||||||
|
|
||||||
|
return dispatch(addTransactions(transactions))
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
// @flow
|
||||||
|
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
||||||
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||||
|
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
|
import { type GlobalState } from '~/store'
|
||||||
|
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
|
import { approveTransaction, executeTransaction, CALL } from '~/logic/safe/transactions'
|
||||||
|
|
||||||
|
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||||
|
// https://github.com/gnosis/safe-contracts/blob/master/test/gnosisSafeTeamEdition.js#L26
|
||||||
|
const generateSignaturesFromTxConfirmations = (tx: Transaction, preApprovingOwner?: string) => {
|
||||||
|
// The constant parts need to be sorted so that the recovered signers are sorted ascending
|
||||||
|
// (natural order) by address (not checksummed).
|
||||||
|
let confirmedAdresses = tx.confirmations.map(conf => conf.owner.address)
|
||||||
|
|
||||||
|
if (preApprovingOwner) {
|
||||||
|
confirmedAdresses = confirmedAdresses.push(preApprovingOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
let sigs = '0x'
|
||||||
|
confirmedAdresses.sort().forEach((addr) => {
|
||||||
|
sigs += `000000000000000000000000${addr.replace(
|
||||||
|
'0x',
|
||||||
|
'',
|
||||||
|
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||||
|
})
|
||||||
|
return sigs
|
||||||
|
}
|
||||||
|
|
||||||
|
const processTransaction = (
|
||||||
|
safeAddress: string,
|
||||||
|
tx: Transaction,
|
||||||
|
openSnackbar: Function,
|
||||||
|
userAddress: string,
|
||||||
|
approveAndExecute?: boolean,
|
||||||
|
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
|
||||||
|
const state: GlobalState = getState()
|
||||||
|
|
||||||
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
const from = userAccountSelector(state)
|
||||||
|
const nonce = (await safeInstance.nonce()).toString()
|
||||||
|
const threshold = (await safeInstance.getThreshold()).toNumber()
|
||||||
|
const shouldExecute = threshold === tx.confirmations.size || approveAndExecute
|
||||||
|
const sigs = generateSignaturesFromTxConfirmations(tx, approveAndExecute && userAddress)
|
||||||
|
|
||||||
|
|
||||||
|
let txHash
|
||||||
|
if (shouldExecute) {
|
||||||
|
openSnackbar('Transaction has been submitted', 'success')
|
||||||
|
txHash = await executeTransaction(safeInstance, tx.recipient, tx.value, tx.data, CALL, nonce, from, sigs)
|
||||||
|
openSnackbar('Transaction has been confirmed', 'success')
|
||||||
|
} else {
|
||||||
|
openSnackbar('Approval transaction has been submitted', 'success')
|
||||||
|
txHash = await approveTransaction(safeInstance, tx.recipient, tx.value, tx.data, CALL, nonce, from)
|
||||||
|
openSnackbar('Approval transaction has been confirmed', 'success')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.NODE_ENV === 'test') {
|
||||||
|
dispatch(fetchTransactions(safeAddress))
|
||||||
|
}
|
||||||
|
|
||||||
|
return txHash
|
||||||
|
}
|
||||||
|
|
||||||
|
export default processTransaction
|
|
@ -0,0 +1,10 @@
|
||||||
|
// @flow
|
||||||
|
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||||
|
import { type GlobalState } from '~/store'
|
||||||
|
import updateSafe from './updateSafe'
|
||||||
|
|
||||||
|
const updateSafeName = (safeAddress: string, safeName: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
|
dispatch(updateSafe({ address: safeAddress, name: safeName }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updateSafeName
|
|
@ -1,6 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Store, AnyAction } from 'redux'
|
import type { Store, AnyAction } from 'redux'
|
||||||
import { List } from 'immutable'
|
|
||||||
import { ADD_SAFE } from '~/routes/safe/store/actions/addSafe'
|
import { ADD_SAFE } from '~/routes/safe/store/actions/addSafe'
|
||||||
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
|
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
|
||||||
import { REMOVE_SAFE } from '~/routes/safe/store/actions/removeSafe'
|
import { REMOVE_SAFE } from '~/routes/safe/store/actions/removeSafe'
|
||||||
|
@ -75,13 +74,13 @@ const safeStorageMware = (store: Store<GlobalState>) => (next: Function) => asyn
|
||||||
}
|
}
|
||||||
case ADD_SAFE_OWNER: {
|
case ADD_SAFE_OWNER: {
|
||||||
const { safeAddress, ownerAddress, ownerName } = action.payload
|
const { safeAddress, ownerAddress, ownerName } = action.payload
|
||||||
const owners = List(safes.get(safeAddress).owners)
|
const { owners } = safes.get(safeAddress)
|
||||||
setOwners(safeAddress, owners.push(makeOwner({ address: ownerAddress, name: ownerName })))
|
setOwners(safeAddress, owners.push(makeOwner({ address: ownerAddress, name: ownerName })))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case REMOVE_SAFE_OWNER: {
|
case REMOVE_SAFE_OWNER: {
|
||||||
const { safeAddress, ownerAddress } = action.payload
|
const { safeAddress, ownerAddress } = action.payload
|
||||||
const owners = List(safes.get(safeAddress).owners)
|
const { owners } = safes.get(safeAddress)
|
||||||
setOwners(safeAddress, owners.filter(o => o.address.toLowerCase() !== ownerAddress.toLowerCase()))
|
setOwners(safeAddress, owners.filter(o => o.address.toLowerCase() !== ownerAddress.toLowerCase()))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -89,7 +88,7 @@ const safeStorageMware = (store: Store<GlobalState>) => (next: Function) => asyn
|
||||||
const {
|
const {
|
||||||
safeAddress, ownerAddress, ownerName, oldOwnerAddress,
|
safeAddress, ownerAddress, ownerName, oldOwnerAddress,
|
||||||
} = action.payload
|
} = action.payload
|
||||||
const owners = List(safes.get(safeAddress).owners)
|
const { owners } = safes.get(safeAddress)
|
||||||
setOwners(
|
setOwners(
|
||||||
safeAddress,
|
safeAddress,
|
||||||
owners
|
owners
|
||||||
|
@ -100,7 +99,7 @@ const safeStorageMware = (store: Store<GlobalState>) => (next: Function) => asyn
|
||||||
}
|
}
|
||||||
case EDIT_SAFE_OWNER: {
|
case EDIT_SAFE_OWNER: {
|
||||||
const { safeAddress, ownerAddress, ownerName } = action.payload
|
const { safeAddress, ownerAddress, ownerName } = action.payload
|
||||||
const owners = List(safes.get(safeAddress).owners)
|
const { owners } = safes.get(safeAddress)
|
||||||
const ownerToUpdateIndex = owners.findIndex(o => o.address.toLowerCase() === ownerAddress.toLowerCase())
|
const ownerToUpdateIndex = owners.findIndex(o => o.address.toLowerCase() === ownerAddress.toLowerCase())
|
||||||
setOwners(safeAddress, owners.update(ownerToUpdateIndex, owner => owner.set('name', ownerName)))
|
setOwners(safeAddress, owners.update(ownerToUpdateIndex, owner => owner.set('name', ownerName)))
|
||||||
break
|
break
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { Record } from 'immutable'
|
import { Record } from 'immutable'
|
||||||
import type { RecordFactory, RecordOf } from 'immutable'
|
import type { RecordFactory, RecordOf } from 'immutable'
|
||||||
import { makeOwner, type Owner } from '~/routes/safe/store/models/owner'
|
import { makeOwner, type Owner } from '~/routes/safe/store/models/owner'
|
||||||
import { type TxServiceType } from '~/logic/safe/safeTxHistory'
|
import { type TxServiceType } from '~/logic/safe/transactions/txHistory'
|
||||||
|
|
||||||
export type ConfirmationProps = {
|
export type ConfirmationProps = {
|
||||||
owner: Owner,
|
owner: Owner,
|
||||||
|
|
|
@ -3,14 +3,27 @@ import { List, Record } from 'immutable'
|
||||||
import type { RecordFactory, RecordOf } from 'immutable'
|
import type { RecordFactory, RecordOf } from 'immutable'
|
||||||
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
|
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
|
||||||
|
|
||||||
|
export type TransactionStatus = 'awaiting_confirmations' | 'success' | 'cancelled' | 'awaiting_execution'
|
||||||
|
|
||||||
export type TransactionProps = {
|
export type TransactionProps = {
|
||||||
name: string,
|
name: string,
|
||||||
nonce: number,
|
nonce: number,
|
||||||
value: number,
|
value: string,
|
||||||
confirmations: List<Confirmation>,
|
confirmations: List<Confirmation>,
|
||||||
destination: string,
|
recipient: string,
|
||||||
data: string,
|
data: string,
|
||||||
isExecuted: boolean,
|
isExecuted: boolean,
|
||||||
|
submissionDate: Date,
|
||||||
|
executionDate: Date,
|
||||||
|
symbol: string,
|
||||||
|
modifySettingsTx: boolean,
|
||||||
|
cancellationTx: boolean,
|
||||||
|
creationTxHash: string,
|
||||||
|
executionTxHash?: string,
|
||||||
|
cancelled?: boolean,
|
||||||
|
status?: TransactionStatus,
|
||||||
|
isTokenTransfer: boolean,
|
||||||
|
decodedParams?: Object,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
||||||
|
@ -18,9 +31,20 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
value: 0,
|
value: 0,
|
||||||
confirmations: List([]),
|
confirmations: List([]),
|
||||||
destination: '',
|
recipient: '',
|
||||||
data: '',
|
data: '',
|
||||||
isExecuted: false,
|
isExecuted: false,
|
||||||
|
submissionDate: '',
|
||||||
|
executionDate: '',
|
||||||
|
symbol: '',
|
||||||
|
executionTxHash: undefined,
|
||||||
|
creationTxHash: '',
|
||||||
|
cancelled: false,
|
||||||
|
modifySettingsTx: false,
|
||||||
|
cancellationTx: false,
|
||||||
|
status: 'awaiting',
|
||||||
|
isTokenTransfer: false,
|
||||||
|
decodedParams: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Transaction = RecordOf<TransactionProps>
|
export type Transaction = RecordOf<TransactionProps>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
import { handleActions, type ActionType } from 'redux-actions'
|
import { handleActions, type ActionType } from 'redux-actions'
|
||||||
import { ADD_TRANSACTIONS } from '~/routes/safe/store/actions/createTransaction'
|
import { ADD_TRANSACTIONS } from '~/routes/safe/store/actions/addTransactions'
|
||||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
|
||||||
export const TRANSACTIONS_REDUCER_ID = 'transactions'
|
export const TRANSACTIONS_REDUCER_ID = 'transactions'
|
||||||
|
|
|
@ -23,17 +23,15 @@ type TransactionProps = {
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
}
|
}
|
||||||
|
|
||||||
const safePropAddressSelector = (state: GlobalState, props: SafeProps) => props.safeAddress
|
|
||||||
|
|
||||||
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
||||||
|
|
||||||
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
|
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
|
||||||
|
|
||||||
export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || ''
|
export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || ''
|
||||||
|
|
||||||
export const safeTransactionsSelector: Selector<GlobalState, SafeProps, List<Transaction>> = createSelector(
|
export const safeTransactionsSelector: Selector<GlobalState, RouterProps, List<Transaction>> = createSelector(
|
||||||
transactionsSelector,
|
transactionsSelector,
|
||||||
safePropAddressSelector,
|
safeParamAddressSelector,
|
||||||
(transactions: TransactionsState, address: string): List<Transaction> => {
|
(transactions: TransactionsState, address: string): List<Transaction> => {
|
||||||
if (!transactions) {
|
if (!transactions) {
|
||||||
return List([])
|
return List([])
|
||||||
|
|
|
@ -34,6 +34,7 @@ export const CreateSafe = ({ size, provider }: SafeProps) => (
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={!provider}
|
disabled={!provider}
|
||||||
minWidth={240}
|
minWidth={240}
|
||||||
|
minHeight={42}
|
||||||
>
|
>
|
||||||
<Img src={plus} height={14} alt="Safe" />
|
<Img src={plus} height={14} alt="Safe" />
|
||||||
<div style={buttonStyle}>Create new Safe</div>
|
<div style={buttonStyle}>Create new Safe</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import PageFrame from '~/components/layout/PageFrame'
|
||||||
import ListItemText from '~/components/List/ListItemText/index'
|
import ListItemText from '~/components/List/ListItemText/index'
|
||||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
import { sleep } from '~/utils/timer'
|
import { sleep } from '~/utils/timer'
|
||||||
import { history } from '~/store'
|
import { history, type GlobalState } from '~/store'
|
||||||
import AppRoutes from '~/routes'
|
import AppRoutes from '~/routes'
|
||||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { sleep } from '~/utils/timer'
|
||||||
import TokenBalanceRecord from '~/routes/safe/store/models/tokenBalance'
|
import TokenBalanceRecord from '~/routes/safe/store/models/tokenBalance'
|
||||||
import { calculateBalanceOf } from '~/routes/safe/store/actions/fetchTokenBalances'
|
import { calculateBalanceOf } from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||||
import updateActiveTokens from '~/routes/safe/store/actions/updateActiveTokens'
|
import updateActiveTokens from '~/routes/safe/store/actions/updateActiveTokens'
|
||||||
import 'jest-dom/extend-expect'
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
||||||
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
|
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { aNewStore } from '~/store'
|
||||||
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
||||||
import { renderSafeView } from '~/test/builder/safe.dom.utils'
|
import { renderSafeView } from '~/test/builder/safe.dom.utils'
|
||||||
import { sleep } from '~/utils/timer'
|
import { sleep } from '~/utils/timer'
|
||||||
import 'jest-dom/extend-expect'
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
import { SETTINGS_TAB_BTN_TESTID, SAFE_VIEW_NAME_HEADING_TESTID } from '~/routes/safe/components/Layout'
|
import { SETTINGS_TAB_BTN_TESTID, SAFE_VIEW_NAME_HEADING_TESTID } from '~/routes/safe/components/Layout'
|
||||||
import { SAFE_NAME_INPUT_TESTID, SAFE_NAME_SUBMIT_BTN_TESTID } from '~/routes/safe/components/Settings/ChangeSafeName'
|
import { SAFE_NAME_INPUT_TESTID, SAFE_NAME_SUBMIT_BTN_TESTID } from '~/routes/safe/components/Settings/ChangeSafeName'
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { aNewStore } from '~/store'
|
||||||
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
|
||||||
import { renderSafeView } from '~/test/builder/safe.dom.utils'
|
import { renderSafeView } from '~/test/builder/safe.dom.utils'
|
||||||
import { sleep } from '~/utils/timer'
|
import { sleep } from '~/utils/timer'
|
||||||
import 'jest-dom/extend-expect'
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
import { SETTINGS_TAB_BTN_TESTID } from '~/routes/safe/components/Layout'
|
import { SETTINGS_TAB_BTN_TESTID } from '~/routes/safe/components/Layout'
|
||||||
import { OWNERS_SETTINGS_TAB_TESTID } from '~/routes/safe/components/Settings'
|
import { OWNERS_SETTINGS_TAB_TESTID } from '~/routes/safe/components/Settings'
|
||||||
import {
|
import {
|
||||||
|
@ -162,7 +162,7 @@ describe('DOM > Feature > Settings - Manage owners', () => {
|
||||||
fireEvent.click(SafeDom.getByTestId(ADD_OWNER_THRESHOLD_NEXT_BTN_TESTID))
|
fireEvent.click(SafeDom.getByTestId(ADD_OWNER_THRESHOLD_NEXT_BTN_TESTID))
|
||||||
await sleep(200)
|
await sleep(200)
|
||||||
fireEvent.click(SafeDom.getByTestId(ADD_OWNER_SUBMIT_BTN_TESTID))
|
fireEvent.click(SafeDom.getByTestId(ADD_OWNER_SUBMIT_BTN_TESTID))
|
||||||
await sleep(1000)
|
await sleep(1500)
|
||||||
|
|
||||||
// check if owner was added
|
// check if owner was added
|
||||||
ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TESTID)
|
ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TESTID)
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
ADD_CUSTOM_TOKEN_FORM,
|
ADD_CUSTOM_TOKEN_FORM,
|
||||||
} from '~/routes/safe/components/Balances/Tokens/screens/AddCustomToken'
|
} from '~/routes/safe/components/Balances/Tokens/screens/AddCustomToken'
|
||||||
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances/'
|
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances/'
|
||||||
import 'jest-dom/extend-expect'
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
|
|
||||||
// https://github.com/testing-library/@testing-library/react/issues/281
|
// https://github.com/testing-library/@testing-library/react/issues/281
|
||||||
const originalError = console.error
|
const originalError = console.error
|
||||||
|
|
|
@ -10,7 +10,7 @@ import saveTokens from '~/logic/tokens/store/actions/saveTokens'
|
||||||
import { clickOnManageTokens, toggleToken, closeManageTokensModal } from './utils/DOMNavigation'
|
import { clickOnManageTokens, toggleToken, closeManageTokensModal } from './utils/DOMNavigation'
|
||||||
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
|
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
|
||||||
import { makeToken } from '~/logic/tokens/store/model/token'
|
import { makeToken } from '~/logic/tokens/store/model/token'
|
||||||
import 'jest-dom/extend-expect'
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
|
|
||||||
describe('DOM > Feature > Enable and disable default tokens', () => {
|
describe('DOM > Feature > Enable and disable default tokens', () => {
|
||||||
let web3
|
let web3
|
||||||
|
|
|
@ -43,6 +43,9 @@ export default createMuiTheme({
|
||||||
},
|
},
|
||||||
overrides: {
|
overrides: {
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
|
label: {
|
||||||
|
lineHeight: 1,
|
||||||
|
},
|
||||||
root: {
|
root: {
|
||||||
fontFamily: 'Roboto Mono, monospace',
|
fontFamily: 'Roboto Mono, monospace',
|
||||||
letterSpacing: '0.9px',
|
letterSpacing: '0.9px',
|
||||||
|
@ -57,6 +60,9 @@ export default createMuiTheme({
|
||||||
containedPrimary: {
|
containedPrimary: {
|
||||||
backgroundColor: secondary,
|
backgroundColor: secondary,
|
||||||
},
|
},
|
||||||
|
containedSecondary: {
|
||||||
|
backgroundColor: error,
|
||||||
|
},
|
||||||
sizeLarge: {
|
sizeLarge: {
|
||||||
padding: `${md} ${lg}`,
|
padding: `${md} ${lg}`,
|
||||||
minHeight: '52px',
|
minHeight: '52px',
|
||||||
|
@ -78,6 +84,16 @@ export default createMuiTheme({
|
||||||
padding: '24px 0 0 15px',
|
padding: '24px 0 0 15px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiIconButton: {
|
||||||
|
root: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiChip: {
|
||||||
|
root: {
|
||||||
|
fontFamily: 'Roboto Mono, monospace',
|
||||||
|
},
|
||||||
|
},
|
||||||
MuiStepIcon: {
|
MuiStepIcon: {
|
||||||
root: {
|
root: {
|
||||||
fontSize: '22px',
|
fontSize: '22px',
|
||||||
|
|
Loading…
Reference in New Issue