Merge pull request #129 from gnosis/79-tx-list
Feature #79: Transaction list
This commit is contained in:
commit
36aff894d5
23
.eslintrc
23
.eslintrc
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"plugin:flowtype/recommended"
|
||||
],
|
||||
"extends": ["airbnb", "plugin:flowtype/recommended"],
|
||||
"parser": "babel-eslint",
|
||||
"plugins": ["jest", "flowtype"],
|
||||
"rules": {
|
||||
|
@ -27,19 +24,23 @@
|
|||
"import/extensions": 0,
|
||||
"import/prefer-default-export": 0,
|
||||
"jsx-a11y/label-has-for": 0,
|
||||
"indent": ["error", 2],
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
"no-console": ["error", { "allow": ["warn", "error"] }],
|
||||
"flowtype/require-valid-file-annotation": [
|
||||
2,
|
||||
"always", {
|
||||
"always",
|
||||
{
|
||||
"annotationStyle": "line"
|
||||
}
|
||||
],
|
||||
"jsx-a11y/anchor-is-valid": [ "error", {
|
||||
"components": [ "Link" ],
|
||||
"specialLink": [ "to", "hrefLeft", "hrefRight" ],
|
||||
"aspects": [ "noHref", "invalidHref", "preferButton" ]
|
||||
}],
|
||||
"jsx-a11y/anchor-is-valid": [
|
||||
"error",
|
||||
{
|
||||
"components": ["Link"],
|
||||
"specialLink": ["to", "hrefLeft", "hrefRight"],
|
||||
"aspects": ["noHref", "invalidHref", "preferButton"]
|
||||
}
|
||||
],
|
||||
"react/require-default-props": 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
|
||||
-
|
|
@ -2,4 +2,5 @@ node_modules/
|
|||
build_webpack/
|
||||
build_storybook/
|
||||
.DS_Store
|
||||
build/
|
||||
build/
|
||||
yarn-error.log
|
56
package.json
56
package.json
|
@ -32,29 +32,31 @@
|
|||
"dependencies": {
|
||||
"@gnosis.pm/safe-contracts": "^1.0.0",
|
||||
"@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",
|
||||
"@testing-library/jest-dom": "^4.0.0",
|
||||
"@welldone-software/why-did-you-render": "3.2.1",
|
||||
"axios": "0.19.0",
|
||||
"bignumber.js": "9.0.0",
|
||||
"connected-react-router": "^6.3.1",
|
||||
"final-form": "4.16.1",
|
||||
"connected-react-router": "6.5.2",
|
||||
"date-fns": "1.30.1",
|
||||
"final-form": "4.18.2",
|
||||
"history": "^4.7.2",
|
||||
"immortal-db": "^1.0.2",
|
||||
"immutable": "^4.0.0-rc.9",
|
||||
"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",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-final-form": "6.3.0",
|
||||
"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-redux": "7.1.0",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"recompose": "^0.30.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux": "4.0.4",
|
||||
"redux-actions": "^2.3.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"reselect": "^4.0.0",
|
||||
|
@ -62,27 +64,27 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@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-decorators": "7.4.4",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.0.0",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.0.0",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
|
||||
"@babel/plugin-proposal-do-expressions": "7.5.0",
|
||||
"@babel/plugin-proposal-export-default-from": "7.5.2",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.5.2",
|
||||
"@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-logical-assignment-operators": "^7.0.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^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-syntax-dynamic-import": "^7.0.0",
|
||||
"@babel/plugin-syntax-import-meta": "^7.0.0",
|
||||
"@babel/plugin-transform-member-expression-literals": "^7.2.0",
|
||||
"@babel/plugin-transform-property-literals": "^7.2.0",
|
||||
"@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-react": "^7.0.0-beta.40",
|
||||
"@sambego/storybook-state": "^1.0.7",
|
||||
|
@ -90,8 +92,8 @@
|
|||
"@storybook/addon-knobs": "5.1.9",
|
||||
"@storybook/addon-links": "5.1.9",
|
||||
"@storybook/react": "5.1.9",
|
||||
"@testing-library/react": "^8.0.1",
|
||||
"autoprefixer": "9.6.0",
|
||||
"@testing-library/react": "8.0.5",
|
||||
"autoprefixer": "9.6.1",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "10.0.2",
|
||||
"babel-jest": "24.8.0",
|
||||
|
@ -102,12 +104,12 @@
|
|||
"classnames": "^2.2.5",
|
||||
"css-loader": "3.0.0",
|
||||
"detect-port": "^1.2.2",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-airbnb": "^17.1.0",
|
||||
"eslint": "6.0.1",
|
||||
"eslint-config-airbnb": "17.1.1",
|
||||
"eslint-plugin-flowtype": "3.11.1",
|
||||
"eslint-plugin-import": "2.18.0",
|
||||
"eslint-plugin-jest": "22.7.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-jest": "22.9.0",
|
||||
"eslint-plugin-jsx-a11y": "6.2.3",
|
||||
"eslint-plugin-react": "7.14.2",
|
||||
"ethereumjs-abi": "^0.6.7",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
|
@ -117,25 +119,25 @@
|
|||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.0.4",
|
||||
"jest": "24.8.0",
|
||||
"jest-dom": "^3.4.0",
|
||||
"jest-dom": "4.0.0",
|
||||
"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-mixins": "^6.2.0",
|
||||
"postcss-simple-vars": "^5.0.2",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier-eslint-cli": "5.0.0",
|
||||
"run-with-testrpc": "0.3.1",
|
||||
"storybook-host": "^5.0.3",
|
||||
"storybook-host": "5.1.0",
|
||||
"storybook-router": "^0.3.3",
|
||||
"style-loader": "^0.23.1",
|
||||
"truffle": "5.0.26",
|
||||
"truffle-contract": "4.0.23",
|
||||
"truffle-solidity-loader": "0.1.25",
|
||||
"truffle": "5.0.27",
|
||||
"truffle-contract": "4.0.24",
|
||||
"truffle-solidity-loader": "0.1.26",
|
||||
"uglifyjs-webpack-plugin": "2.1.3",
|
||||
"webpack": "4.35.2",
|
||||
"webpack": "4.35.3",
|
||||
"webpack-bundle-analyzer": "3.3.2",
|
||||
"webpack-cli": "3.3.5",
|
||||
"webpack-cli": "3.3.6",
|
||||
"webpack-dev-server": "3.7.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 = () => (
|
||||
<Block className={styles.footer}>
|
||||
<Link to={WELCOME_ADDRESS}>
|
||||
<Paragraph size="sm" color="primary" noMargin>Welcome</Paragraph>
|
||||
<Paragraph size="sm" color="primary" noMargin>Add Safe</Paragraph>
|
||||
</Link>
|
||||
<Link to={SAFELIST_ADDRESS}>
|
||||
<Paragraph size="sm" color="primary" noMargin>Safe List</Paragraph>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Dot from '@material-ui/icons/FiberManualRecord'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Img from '~/components/layout/Img'
|
||||
import { fancy, border, warning } from '~/theme/variables'
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@ import * as React from 'react'
|
|||
import classNames from 'classnames'
|
||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Dot from '@material-ui/icons/FiberManualRecord'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Link from '~/components/layout/Link'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Identicon from '~/components/Identicon'
|
||||
import Dot from '@material-ui/icons/FiberManualRecord'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Row from '~/components/layout/Row'
|
||||
|
@ -123,7 +123,7 @@ const UserDetails = ({
|
|||
{address}
|
||||
</Paragraph>
|
||||
{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} />
|
||||
</Link>
|
||||
)}
|
||||
|
|
|
@ -5,11 +5,18 @@ import { toDataUrl } from './blockies'
|
|||
type Props = {
|
||||
address: string,
|
||||
diameter: number,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
type IdenticonRef = { current: null | HTMLDivElement }
|
||||
|
||||
export default class Identicon extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
}
|
||||
|
||||
identicon: IdenticonRef
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
|
@ -55,13 +62,13 @@ export default class Identicon extends React.PureComponent<Props> {
|
|||
return image
|
||||
}
|
||||
|
||||
identicon: IdenticonRef
|
||||
|
||||
render() {
|
||||
const style = this.getStyleFrom(this.props.diameter)
|
||||
const { diameter, className } = this.props
|
||||
const style = this.getStyleFrom(diameter)
|
||||
|
||||
return (
|
||||
<div style={style} ref={this.identicon} />
|
||||
<div className={className} style={style} ref={this.identicon} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ type Props<K> = {
|
|||
children: Function,
|
||||
size: number,
|
||||
defaultFixed?: boolean,
|
||||
defaultOrder?: 'desc' | 'asc',
|
||||
noBorder: boolean,
|
||||
}
|
||||
|
||||
|
@ -59,21 +60,48 @@ const styles = {
|
|||
|
||||
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> {
|
||||
state = {
|
||||
page: 0,
|
||||
order: 'asc',
|
||||
order: undefined,
|
||||
orderBy: undefined,
|
||||
fixed: undefined,
|
||||
orderProp: false,
|
||||
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) => {
|
||||
const { order, orderBy } = this.state
|
||||
const { defaultOrder } = this.props
|
||||
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'
|
||||
}
|
||||
|
||||
|
@ -100,29 +128,22 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
|||
|
||||
render() {
|
||||
const {
|
||||
data, label, columns, classes, children, size, defaultOrderBy, defaultFixed, noBorder,
|
||||
data, label, columns, classes, children, size, defaultOrderBy, defaultOrder, defaultFixed, noBorder,
|
||||
} = this.props
|
||||
const {
|
||||
order, orderBy, page, orderProp, rowsPerPage, fixed,
|
||||
} = this.state
|
||||
const orderByParam = orderBy || defaultOrderBy
|
||||
const orderParam = order || defaultOrder
|
||||
const fixedParam = typeof fixed !== 'undefined' ? fixed : !!defaultFixed
|
||||
|
||||
const backProps = {
|
||||
'aria-label': 'Previous Page',
|
||||
}
|
||||
|
||||
const nextProps = {
|
||||
'aria-label': 'Next Page',
|
||||
}
|
||||
|
||||
const paginationClasses = {
|
||||
selectRoot: classes.selectRoot,
|
||||
root: !noBorder && classes.paginationRoot,
|
||||
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 + rowsPerPage,
|
||||
)
|
||||
|
@ -163,4 +184,8 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
|||
}
|
||||
}
|
||||
|
||||
GnoTable.defaultProps = {
|
||||
defaultOrder: 'asc',
|
||||
}
|
||||
|
||||
export default withStyles(styles)(GnoTable)
|
||||
|
|
|
@ -1,31 +1,27 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
|
||||
const styles = {
|
||||
root: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
}
|
||||
|
||||
type Props = {
|
||||
minWidth?: number,
|
||||
minHeight?: number,
|
||||
testId: string,
|
||||
rounded?: boolean,
|
||||
testId?: string,
|
||||
style?: Object,
|
||||
}
|
||||
|
||||
const calculateStyleBased = (minWidth, minHeight) => ({
|
||||
const calculateStyleBased = (minWidth, minHeight, rounded) => ({
|
||||
minWidth: minWidth && `${minWidth}px`,
|
||||
minHeight: minHeight && `${minHeight}px`,
|
||||
borderRadius: rounded ? '4px' : 0,
|
||||
})
|
||||
|
||||
const GnoButton = ({
|
||||
minWidth, minHeight, testId = '', ...props
|
||||
minWidth, minHeight = 27, testId = '', rounded, style = {}, ...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)
|
||||
|
||||
type Props = {
|
||||
type: 'button' | 'submit' | 'reset',
|
||||
type?: 'button' | 'submit' | 'reset',
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl',
|
||||
weight?: 'light' | 'regular' | 'bolder' | 'bold',
|
||||
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled' | 'error',
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
// @flow
|
||||
import React, { PureComponent } from 'react'
|
||||
import * as React from 'react'
|
||||
|
||||
type Props = {
|
||||
children: React.Node
|
||||
children: React.Node,
|
||||
}
|
||||
|
||||
class Span extends PureComponent<Props> {
|
||||
class Span extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { children, ...props } = this.props
|
||||
|
||||
return (
|
||||
<span {...props}>
|
||||
{ children }
|
||||
</span>
|
||||
)
|
||||
return <span {...props}>{children}</span>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,10 +7,10 @@ import {
|
|||
} from '~/config/names'
|
||||
|
||||
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,
|
||||
[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
|
||||
|
|
|
@ -7,10 +7,10 @@ import {
|
|||
} from '~/config/names'
|
||||
|
||||
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,
|
||||
[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
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from '~/config/names'
|
||||
|
||||
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,
|
||||
[SIGNATURES_VIA_METAMASK]: false,
|
||||
[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
|
||||
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 GnosisSafeSol from '@gnosis.pm/safe-contracts/build/contracts/GnosisSafe.json'
|
||||
import { ensureOnce } from '~/utils/singleton'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
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 './safeBlockchainOperations'
|
||||
export * from './safeTxSignerEIP712'
|
||||
export * from './txHistory'
|
||||
|
|
|
@ -1,60 +1,14 @@
|
|||
// @flow
|
||||
import { List } from 'immutable'
|
||||
import { calculateGasOf, checkReceiptStatus, calculateGasPrice } from '~/logic/wallets/ethTransactions'
|
||||
import { type Operation, submitOperation } from '~/logic/safe/safeTxHistory'
|
||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||
import { buildSignaturesFrom } from '~/logic/safe/safeTxSigner'
|
||||
import { generateMetamaskSignature, generateTxGasEstimateFrom, estimateDataGas } from '~/logic/safe/transactions'
|
||||
import { storeSignature, getSignaturesFrom } from '~/utils/storage/signatures'
|
||||
import { signaturesViaMetamask } from '~/config'
|
||||
// // @flow
|
||||
// import { List } from 'immutable'
|
||||
// import { calculateGasOf, checkReceiptStatus, calculateGasPrice } from '~/logic/wallets/ethTransactions'
|
||||
// import { type Operation, saveTxToHistory } from '~/logic/safe/transactions'
|
||||
// import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||
// import { buildSignaturesFrom } from '~/logic/safe/safeTxSigner'
|
||||
// import { generateMetamaskSignature, generateTxGasEstimateFrom, estimateDataGas } from '~/logic/safe/transactions'
|
||||
// import { storeSignature, getSignaturesFrom } from '~/utils/storage/signatures'
|
||||
// import { signaturesViaMetamask } from '~/config'
|
||||
|
||||
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 (
|
||||
// export const approveTransaction = async (
|
||||
// safeAddress: string,
|
||||
// to: string,
|
||||
// valueInWei: number,
|
||||
|
@ -62,17 +16,17 @@ export const approveTransaction = async (
|
|||
// operation: Operation,
|
||||
// nonce: number,
|
||||
// sender: string,
|
||||
// ownersWhoHasSigned: List<string>,
|
||||
// ) => {
|
||||
// const gasPrice = await calculateGasPrice()
|
||||
|
||||
// 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 signature = await generateMetamaskSignature(
|
||||
// safe,
|
||||
// safeAddress,
|
||||
// sender,
|
||||
// // sender
|
||||
// to,
|
||||
// valueInWei,
|
||||
// nonce,
|
||||
|
@ -82,61 +36,107 @@ export const approveTransaction = async (
|
|||
// )
|
||||
// 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
|
||||
// return undefined
|
||||
// }
|
||||
|
||||
// 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 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, 'execution')
|
||||
// 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')
|
||||
|
||||
// // return txHash
|
||||
// // }
|
||||
|
|
|
@ -5,31 +5,83 @@ import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
|||
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
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
|
||||
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 (
|
||||
safeInstance: any,
|
||||
to: string,
|
||||
valueInWei: number | string,
|
||||
data: string,
|
||||
operation: number | string,
|
||||
operation: Operation,
|
||||
nonce: string | number,
|
||||
sender: string,
|
||||
signatures?: string,
|
||||
) => {
|
||||
try {
|
||||
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||
const sigs = `0x000000000000000000000000${sender.replace(
|
||||
let sigs = signatures
|
||||
|
||||
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||
if (!sigs) {
|
||||
sigs = `0x000000000000000000000000${sender.replace(
|
||||
'0x',
|
||||
'',
|
||||
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||
}
|
||||
|
||||
const tx = await safeInstance.execTransaction(
|
||||
try {
|
||||
const receipt = await safeInstance.execTransaction(
|
||||
to,
|
||||
valueInWei,
|
||||
data,
|
||||
CALL,
|
||||
operation,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
@ -39,10 +91,31 @@ export const executeTransaction = async (
|
|||
{ 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) {
|
||||
// eslint-disable-next-line
|
||||
console.log('Error executing the TX: ' + error)
|
||||
/* eslint-disable */
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +125,7 @@ export const createTransaction = async (safeAddress: string, to: string, valueIn
|
|||
const web3 = getWeb3()
|
||||
const from = web3.currentProvider.selectedAddress
|
||||
const threshold = await safeInstance.getThreshold()
|
||||
const nonce = await safeInstance.nonce()
|
||||
const nonce = (await safeInstance.nonce()).toString()
|
||||
const valueInWei = web3.utils.toWei(valueInEth, 'ether')
|
||||
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}`)
|
||||
|
||||
return data ? Map(data) : Map()
|
||||
}
|
||||
|
||||
export const removeOwners = async (safeAddress: string) => {
|
||||
export const removeOwners = async (safeAddress: string): Promise<void> => {
|
||||
try {
|
||||
await removeFromStorage(`${OWNERS_KEY}-${safeAddress}`)
|
||||
} 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 HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanFriendlyToken.json'
|
||||
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 { fetchTokenList } from '~/logic/tokens/api'
|
||||
import { ensureOnce } from '~/utils/singleton'
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
// @flow
|
||||
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 logo from '~/assets/icons/icon_etherTokens.svg'
|
||||
|
||||
export const ETH_ADDRESS = '0x000'
|
||||
export const isEther = (symbol: string) => symbol === 'ETH'
|
||||
|
@ -31,3 +32,19 @@ export const calculateActiveErc20TokensFrom = (tokens: List<Token>) => {
|
|||
|
||||
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) => {
|
||||
const initial = 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,
|
||||
}
|
||||
|
||||
export const openTxInEtherScan = (tx: string, network: string) => `https://${network}.etherscan.io/tx/${tx}`
|
||||
|
||||
export const getEtherScanLink = (address: string, network: string) => `https://${network}.etherscan.io/address/${address}`
|
||||
export const getEtherScanLink = (type: 'address' | 'tx', value: string, network: string) => `https://${network === 'mainnet' ? '' : `${network}.`}etherscan.io/${type}/${value}`
|
||||
|
||||
let web3
|
||||
export const getWeb3 = () => web3 || (window.web3 && new Web3(window.web3.currentProvider)) || (window.ethereum && new Web3(window.ethereum))
|
||||
|
@ -43,14 +41,14 @@ const getProviderName: Function = (web3Provider): boolean => {
|
|||
let name
|
||||
|
||||
switch (web3Provider.currentProvider.constructor.name) {
|
||||
case 'SafeWeb3Provider':
|
||||
name = WALLET_PROVIDER.SAFE
|
||||
break
|
||||
case 'MetamaskInpageProvider':
|
||||
name = WALLET_PROVIDER.METAMASK
|
||||
break
|
||||
default:
|
||||
name = 'UNKNOWN'
|
||||
case 'SafeWeb3Provider':
|
||||
name = WALLET_PROVIDER.SAFE
|
||||
break
|
||||
case 'MetamaskInpageProvider':
|
||||
name = WALLET_PROVIDER.METAMASK
|
||||
break
|
||||
default:
|
||||
name = 'UNKNOWN'
|
||||
}
|
||||
|
||||
return name
|
||||
|
|
|
@ -151,7 +151,7 @@ const OwnerListComponent = (props: Props) => {
|
|||
<Paragraph size="md" color="disabled" noMargin className={classes.address}>
|
||||
{address}
|
||||
</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} />
|
||||
</Link>
|
||||
</Row>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import Block from '~/components/layout/Block'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Identicon from '~/components/Identicon'
|
||||
import OpenPaper from '~/components/Stepper/OpenPaper'
|
||||
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}>
|
||||
{shortVersionOf(safeAddress, 4)}
|
||||
</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} />
|
||||
</Link>
|
||||
</Row>
|
||||
|
@ -177,7 +177,7 @@ class ReviewComponent extends React.PureComponent<Props, State> {
|
|||
<Paragraph size="md" color="disabled" noMargin>
|
||||
{owners[index]}
|
||||
</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} />
|
||||
</Link>
|
||||
</Block>
|
||||
|
|
|
@ -20,7 +20,7 @@ export const loadSafe = async (
|
|||
safeName: string,
|
||||
safeAddress: string,
|
||||
owners: Array,
|
||||
addSafe: Function
|
||||
addSafe: Function,
|
||||
) => {
|
||||
const safeProps = await buildSafe(safeAddress, safeName)
|
||||
safeProps.owners = owners
|
||||
|
|
|
@ -129,7 +129,7 @@ const ReviewComponent = ({ values, classes, network }: Props) => {
|
|||
<Paragraph size="md" color="disabled" noMargin>
|
||||
{addresses[index]}
|
||||
</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} />
|
||||
</Link>
|
||||
</Block>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import Block from '~/components/layout/Block'
|
||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import LinearProgress from '@material-ui/core/LinearProgress'
|
||||
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 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 { type SelectorProps } from '../container/selector'
|
||||
|
||||
|
@ -74,7 +74,7 @@ const Opening = ({
|
|||
Follow progress on
|
||||
{' '}
|
||||
<a
|
||||
href={openTxInEtherScan(tx, network)}
|
||||
href={getEtherScanLink('tx', tx, network)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={classes.etherscan}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// @flow
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { List } from 'immutable'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import cn from 'classnames'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import Modal from '~/components/Modal'
|
||||
import ChooseTxType from './screens/ChooseTxType'
|
||||
import SendFunds from './screens/SendFunds'
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
import React from 'react'
|
||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import Link from '~/components/layout/Link'
|
||||
|
@ -62,7 +62,6 @@ const ReviewTx = ({
|
|||
let txData = EMPTY_DATA
|
||||
let txAmount = web3.utils.toWei(tx.amount, 'ether')
|
||||
|
||||
|
||||
if (!isSendingETH) {
|
||||
const StandardToken = await getStandardTokenContract()
|
||||
const tokenInstance = await StandardToken.at(tx.token.address)
|
||||
|
@ -139,7 +138,7 @@ const ReviewTx = ({
|
|||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
<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
|
||||
</Button>
|
||||
<Button
|
||||
|
@ -148,6 +147,7 @@ const ReviewTx = ({
|
|||
onClick={submitTx}
|
||||
variant="contained"
|
||||
minWidth={140}
|
||||
minHeight={42}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
>
|
||||
|
|
|
@ -158,13 +158,13 @@ const SendFunds = ({
|
|||
</Row>
|
||||
<Hairline />
|
||||
<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
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className={classes.button}
|
||||
variant="contained"
|
||||
minHeight={42}
|
||||
minWidth={140}
|
||||
color="primary"
|
||||
data-testid="review-tx-btn"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// @flow
|
||||
import { List } from 'immutable'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||
import { isAddressAToken } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import { simpleMemoize } from '~/components/forms/validator'
|
||||
// import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
|
||||
|
||||
|
@ -17,10 +17,9 @@ export const addressIsTokenContract = simpleMemoize(async (tokenAddress: string)
|
|||
// return 'Not a token address'
|
||||
// }
|
||||
|
||||
const web3 = getWeb3()
|
||||
const call = await web3.eth.call({ to: tokenAddress, data: web3.utils.sha3('totalSupply()') })
|
||||
const isToken = await isAddressAToken(tokenAddress)
|
||||
|
||||
if (call === '0x') {
|
||||
if (!isToken) {
|
||||
return 'Not a token address'
|
||||
}
|
||||
})
|
||||
|
|
|
@ -5,19 +5,19 @@ import cn from 'classnames'
|
|||
import SearchBar from 'material-ui-search-bar'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
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 ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
|
||||
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 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 Hairline from '~/components/layout/Hairline'
|
||||
import Spacer from '~/components/Spacer'
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
|
@ -141,7 +141,7 @@ class Tokens extends React.Component<Props, State> {
|
|||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
color="secondary"
|
||||
color="primary"
|
||||
className={classes.add}
|
||||
onClick={switchToAddCustomTokenScreen}
|
||||
testId={ADD_CUSTOM_TOKEN_BUTTON_TEST_ID}
|
||||
|
|
|
@ -52,6 +52,7 @@ export const generateColumns = () => {
|
|||
disablePadding: false,
|
||||
label: '',
|
||||
custom: true,
|
||||
static: true,
|
||||
}
|
||||
|
||||
return List([assetColumn, balanceColumn, actions])
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
import * as React from 'react'
|
||||
import { List } from 'immutable'
|
||||
import classNames from 'classnames/bind'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import CallMade from '@material-ui/icons/CallMade'
|
||||
import CallReceived from '@material-ui/icons/CallReceived'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Checkbox from '@material-ui/core/Checkbox'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Row from '~/components/layout/Row'
|
||||
import Button from '~/components/layout/Button'
|
||||
import ButtonLink from '~/components/layout/ButtonLink'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Modal from '~/components/Modal'
|
||||
|
@ -168,10 +168,11 @@ class Balances extends React.Component<Props, State> {
|
|||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
color="secondary"
|
||||
color="primary"
|
||||
className={classes.send}
|
||||
onClick={() => this.showSendFunds(row.asset.name)}
|
||||
data-testid="balance-send-btn"
|
||||
rounded
|
||||
testId="balance-send-btn"
|
||||
>
|
||||
<CallMade className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||
Send
|
||||
|
@ -180,9 +181,10 @@ class Balances extends React.Component<Props, State> {
|
|||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
color="secondary"
|
||||
color="primary"
|
||||
className={classes.receive}
|
||||
onClick={this.onShow('Receive')}
|
||||
rounded
|
||||
>
|
||||
<CallReceived className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||
Receive
|
||||
|
|
|
@ -19,20 +19,23 @@ import {
|
|||
} from '~/theme/variables'
|
||||
import { copyToClipboard } from '~/utils/clipboard'
|
||||
import Balances from './Balances'
|
||||
import Transactions from './TransactionsNew'
|
||||
import Settings from './Settings'
|
||||
|
||||
export const SETTINGS_TAB_BTN_TESTID = 'settings-tab-btn'
|
||||
export const SAFE_VIEW_NAME_HEADING_TESTID = 'safe-name-heading'
|
||||
|
||||
type State = {
|
||||
tabIndex: number,
|
||||
}
|
||||
|
||||
type Props = SelectorProps & {
|
||||
classes: Object,
|
||||
granted: boolean,
|
||||
createTransaction: Function,
|
||||
updateSafe: Function,
|
||||
}
|
||||
|
||||
type State = {
|
||||
tabIndex: number,
|
||||
createTransaction: Function,
|
||||
processTransaction: Function,
|
||||
fetchTransactions: Function,
|
||||
}
|
||||
|
||||
const openIconStyle = {
|
||||
|
@ -102,7 +105,10 @@ class Layout extends React.Component<Props, State> {
|
|||
tokens,
|
||||
activeTokens,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
fetchTransactions,
|
||||
updateSafe,
|
||||
transactions,
|
||||
userAddress,
|
||||
} = this.props
|
||||
const { tabIndex } = this.state
|
||||
|
@ -112,7 +118,7 @@ class Layout extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
const { address, ethBalance, name } = safe
|
||||
const etherScanLink = getEtherScanLink(address, network)
|
||||
const etherScanLink = getEtherScanLink('address', address, network)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@ -155,6 +161,19 @@ class Layout extends React.Component<Props, State> {
|
|||
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 && (
|
||||
<Settings
|
||||
granted={granted}
|
||||
|
|
|
@ -12,7 +12,7 @@ import Row from '~/components/layout/Row'
|
|||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Button from '~/components/layout/Button'
|
||||
import { sm, boldFont } from '~/theme/variables'
|
||||
import { sm } from '~/theme/variables'
|
||||
import { styles } from './style'
|
||||
|
||||
const controlsStyle = {
|
||||
|
@ -20,11 +20,6 @@ const controlsStyle = {
|
|||
padding: sm,
|
||||
}
|
||||
|
||||
const saveButtonStyle = {
|
||||
marginRight: sm,
|
||||
fontWeight: boldFont,
|
||||
}
|
||||
|
||||
export const SAFE_NAME_INPUT_TESTID = 'safe-name-input'
|
||||
export const SAFE_NAME_SUBMIT_BTN_TESTID = 'change-safe-name-btn'
|
||||
|
||||
|
@ -73,7 +68,7 @@ const ChangeSafeName = (props: Props) => {
|
|||
<Col end="xs">
|
||||
<Button
|
||||
type="submit"
|
||||
style={saveButtonStyle}
|
||||
className={classes.saveBtn}
|
||||
size="small"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { lg } from '~/theme/variables'
|
||||
import { lg, sm, boldFont } from '~/theme/variables'
|
||||
|
||||
export const styles = () => ({
|
||||
formContainer: {
|
||||
|
@ -10,4 +10,8 @@ export const styles = () => ({
|
|||
display: 'flex',
|
||||
maxWidth: '460px',
|
||||
},
|
||||
saveBtn: {
|
||||
marginRight: sm,
|
||||
fontWeight: boldFont,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -112,7 +112,7 @@ const ReviewAddOwner = ({
|
|||
<Paragraph size="md" color="disabled" noMargin>
|
||||
{owner.address}
|
||||
</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} />
|
||||
</Link>
|
||||
</Block>
|
||||
|
@ -141,7 +141,7 @@ const ReviewAddOwner = ({
|
|||
<Paragraph size="md" color="disabled" noMargin>
|
||||
{values.ownerAddress}
|
||||
</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} />
|
||||
</Link>
|
||||
</Block>
|
||||
|
|
|
@ -56,12 +56,12 @@ const ThresholdForm = ({
|
|||
<Block className={classes.formContainer}>
|
||||
<Row>
|
||||
<Paragraph weight="bolder" className={classes.headingText}>
|
||||
Set the required owner confirmations:
|
||||
Set the required owner confirmations:
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row>
|
||||
<Paragraph weight="bolder">
|
||||
Any transaction over any daily limit requires the confirmation of:
|
||||
Any transaction over any daily limit requires the confirmation of:
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row margin="xl" align="center" className={classes.inputRow}>
|
||||
|
@ -90,7 +90,7 @@ const ThresholdForm = ({
|
|||
</Col>
|
||||
<Col xs={10}>
|
||||
<Paragraph size="lg" color="primary" noMargin className={classes.ownersText}>
|
||||
out of
|
||||
out of
|
||||
{' '}
|
||||
{owners.size + 1}
|
||||
{' '}
|
||||
|
@ -102,7 +102,7 @@ owner(s)
|
|||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
||||
Back
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
|
@ -112,7 +112,7 @@ owner(s)
|
|||
color="primary"
|
||||
testId={ADD_OWNER_THRESHOLD_NEXT_BTN_TESTID}
|
||||
>
|
||||
Review
|
||||
Review
|
||||
</Button>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -94,7 +94,7 @@ const EditOwnerComponent = ({
|
|||
<Paragraph style={{ marginLeft: 10 }} size="md" color="disabled" noMargin>
|
||||
{ownerAddress}
|
||||
</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} />
|
||||
</Link>
|
||||
</Block>
|
||||
|
|
|
@ -76,7 +76,7 @@ const CheckOwner = ({
|
|||
<Paragraph size="md" color="disabled" noMargin>
|
||||
{ownerAddress}
|
||||
</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} />
|
||||
</Link>
|
||||
</Block>
|
||||
|
|
|
@ -127,7 +127,7 @@ const ReviewRemoveOwner = ({
|
|||
</Paragraph>
|
||||
<Link
|
||||
className={classes.open}
|
||||
to={getEtherScanLink(owner.address, network)}
|
||||
to={getEtherScanLink('address', owner.address, network)}
|
||||
target="_blank"
|
||||
>
|
||||
<OpenInNew style={openIconStyle} />
|
||||
|
@ -159,7 +159,7 @@ const ReviewRemoveOwner = ({
|
|||
<Paragraph size="md" color="disabled" noMargin>
|
||||
{ownerAddress}
|
||||
</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} />
|
||||
</Link>
|
||||
</Block>
|
||||
|
|
|
@ -94,7 +94,7 @@ const OwnerForm = ({
|
|||
<Paragraph size="md" color="disabled" noMargin>
|
||||
{ownerAddress}
|
||||
</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} />
|
||||
</Link>
|
||||
</Block>
|
||||
|
|
|
@ -129,7 +129,7 @@ const ReviewRemoveOwner = ({
|
|||
</Paragraph>
|
||||
<Link
|
||||
className={classes.open}
|
||||
to={getEtherScanLink(owner.address, network)}
|
||||
to={getEtherScanLink('address', owner.address, network)}
|
||||
target="_blank"
|
||||
>
|
||||
<OpenInNew style={openIconStyle} />
|
||||
|
@ -161,7 +161,7 @@ const ReviewRemoveOwner = ({
|
|||
<Paragraph size="md" color="disabled" noMargin>
|
||||
{ownerAddress}
|
||||
</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} />
|
||||
</Link>
|
||||
</Block>
|
||||
|
@ -187,7 +187,7 @@ const ReviewRemoveOwner = ({
|
|||
<Paragraph size="md" color="disabled" noMargin>
|
||||
{values.ownerAddress}
|
||||
</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} />
|
||||
</Link>
|
||||
</Block>
|
||||
|
|
|
@ -4,8 +4,8 @@ import { List } from 'immutable'
|
|||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import SelectField from '~/components/forms/SelectField'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import SelectField from '~/components/forms/SelectField'
|
||||
import {
|
||||
composeValidators, minValue, mustBeInteger, required, differentFrom,
|
||||
} 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 fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||
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 fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
|
||||
|
||||
export type Actions = {
|
||||
fetchSafe: typeof fetchSafe,
|
||||
fetchTokenBalances: typeof fetchTokenBalances,
|
||||
createTransaction: typeof createTransaction,
|
||||
fetchTransactions: typeof fetchTransactions,
|
||||
updateSafe: typeof updateSafe,
|
||||
fetchTokens: typeof fetchTokens,
|
||||
processTransaction: typeof processTransaction,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchSafe,
|
||||
fetchTokenBalances,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
fetchTokens,
|
||||
fetchTransactions,
|
||||
updateSafe,
|
||||
}
|
||||
|
|
|
@ -16,11 +16,13 @@ const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000
|
|||
class SafeView extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const {
|
||||
fetchSafe, activeTokens, safeUrl, fetchTokenBalances,
|
||||
fetchSafe, activeTokens, safeUrl, fetchTokenBalances, fetchTokens,
|
||||
} = this.props
|
||||
|
||||
fetchSafe(safeUrl)
|
||||
fetchTokenBalances(safeUrl, activeTokens)
|
||||
// fetch tokens there to get symbols for tokens in TXs list
|
||||
fetchTokens()
|
||||
|
||||
this.intervalId = setInterval(() => {
|
||||
this.checkForUpdates()
|
||||
|
@ -61,7 +63,10 @@ class SafeView extends React.Component<Props> {
|
|||
network,
|
||||
tokens,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
fetchTransactions,
|
||||
updateSafe,
|
||||
transactions,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
|
@ -75,7 +80,10 @@ class SafeView extends React.Component<Props> {
|
|||
network={network}
|
||||
granted={granted}
|
||||
createTransaction={createTransaction}
|
||||
processTransaction={processTransaction}
|
||||
fetchTransactions={fetchTransactions}
|
||||
updateSafe={updateSafe}
|
||||
transactions={transactions}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import { List, Map } from 'immutable'
|
||||
import { createSelector, createStructuredSelector, type Selector } from 'reselect'
|
||||
import { isAfter } from 'date-fns'
|
||||
import {
|
||||
safeSelector,
|
||||
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 GlobalState } from '~/store'
|
||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
||||
import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors'
|
||||
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 { safeParamAddressSelector } from '../store/selectors'
|
||||
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
|
||||
|
@ -27,6 +30,21 @@ export type SelectorProps = {
|
|||
userAddress: string,
|
||||
network: 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(
|
||||
|
@ -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, *>({
|
||||
safe: safeSelector,
|
||||
provider: providerNameSelector,
|
||||
|
@ -95,4 +139,5 @@ export default createStructuredSelector<Object, *>({
|
|||
userAddress: userAccountSelector,
|
||||
network: networkSelector,
|
||||
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
|
||||
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
||||
import { createAction } from 'redux-actions'
|
||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
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 { executeTransaction, CALL } from '~/logic/safe/transactions'
|
||||
|
||||
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
|
||||
export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)
|
||||
import { approveTransaction, executeTransaction, CALL } from '~/logic/safe/transactions'
|
||||
|
||||
const createTransaction = (
|
||||
safeAddress: string,
|
||||
|
@ -16,14 +13,15 @@ const createTransaction = (
|
|||
valueInWei: string,
|
||||
txData: string = EMPTY_DATA,
|
||||
openSnackbar: Function,
|
||||
shouldExecute?: boolean,
|
||||
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
|
||||
const state: GlobalState = getState()
|
||||
|
||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const from = userAccountSelector(state)
|
||||
const threshold = await safeInstance.getThreshold()
|
||||
const nonce = await safeInstance.nonce()
|
||||
const isExecution = threshold.toNumber() === 1
|
||||
const nonce = (await safeInstance.nonce()).toString()
|
||||
const isExecution = threshold.toNumber() === 1 || shouldExecute
|
||||
|
||||
let txHash
|
||||
if (isExecution) {
|
||||
|
@ -31,10 +29,14 @@ const createTransaction = (
|
|||
txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
|
||||
openSnackbar('Transaction has been confirmed', 'success')
|
||||
} else {
|
||||
console.log('Temporal error: threshold != 1')
|
||||
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
|
||||
openSnackbar('Approval transaction has been submitted', 'success')
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,74 +1,123 @@
|
|||
// // @flow
|
||||
// import { List, Map } from 'immutable'
|
||||
// import axios from 'axios'
|
||||
// import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
// import { type GlobalState } from '~/store/index'
|
||||
// import { makeOwner } from '~/routes/safe/store/models/owner'
|
||||
// import { makeTransaction, type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
// import { makeConfirmation } from '~/routes/safe/store/models/confirmation'
|
||||
// import { loadSafeSubjects } from '~/utils/storage/transactions'
|
||||
// import { buildTxServiceUrlFrom, type TxServiceType } from '~/logic/safe/safeTxHistory'
|
||||
// import { getOwners } from '~/logic/safe/utils'
|
||||
// import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||
// import addTransactions from './addTransactions'
|
||||
// @flow
|
||||
import { List, Map } from 'immutable'
|
||||
import axios from 'axios'
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
import { makeOwner } from '~/routes/safe/store/models/owner'
|
||||
import { makeTransaction, type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
import { makeConfirmation } from '~/routes/safe/store/models/confirmation'
|
||||
import { loadSafeSubjects } from '~/utils/storage/transactions'
|
||||
import { buildTxServiceUrl, type TxServiceType } from '~/logic/safe/transactions/txHistory'
|
||||
import { getOwners } from '~/logic/safe/utils'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
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 = {
|
||||
// owner: string,
|
||||
// submissionDate: Date,
|
||||
// type: string,
|
||||
// transactionHash: string,
|
||||
// }
|
||||
let web3
|
||||
|
||||
// type TxServiceModel = {
|
||||
// to: string,
|
||||
// value: number,
|
||||
// data: string,
|
||||
// operation: number,
|
||||
// nonce: number,
|
||||
// submissionDate: Date,
|
||||
// executionDate: Date,
|
||||
// confirmations: ConfirmationServiceModel[],
|
||||
// isExecuted: boolean,
|
||||
// }
|
||||
type ConfirmationServiceModel = {
|
||||
owner: string,
|
||||
submissionDate: Date,
|
||||
confirmationType: string,
|
||||
transactionHash: string,
|
||||
}
|
||||
|
||||
// const buildTransactionFrom = (safeAddress: string, tx: TxServiceModel, safeSubjects: Map<string, string>) => {
|
||||
// const name = safeSubjects.get(String(tx.nonce)) || 'Unknown'
|
||||
// const storedOwners = getOwners(safeAddress)
|
||||
// const confirmations = List(
|
||||
// tx.confirmations.map((conf: ConfirmationServiceModel) => {
|
||||
// const ownerName = storedOwners.get(conf.owner.toLowerCase()) || 'UNKNOWN'
|
||||
type TxServiceModel = {
|
||||
to: string,
|
||||
value: number,
|
||||
data: string,
|
||||
operation: number,
|
||||
nonce: number,
|
||||
submissionDate: Date,
|
||||
executionDate: Date,
|
||||
confirmations: ConfirmationServiceModel[],
|
||||
isExecuted: boolean,
|
||||
}
|
||||
|
||||
// return makeConfirmation({
|
||||
// owner: makeOwner({ address: conf.owner, name: ownerName }),
|
||||
// type: ((conf.type.toLowerCase(): any): TxServiceType),
|
||||
// hash: conf.transactionHash,
|
||||
// })
|
||||
// }),
|
||||
// )
|
||||
const buildTransactionFrom = async (safeAddress: string, tx: TxServiceModel, safeSubjects: Map<string, string>) => {
|
||||
const name = safeSubjects.get(String(tx.nonce)) || 'Unknown'
|
||||
const storedOwners = await getOwners(safeAddress)
|
||||
const confirmations = List(
|
||||
tx.confirmations.map((conf: ConfirmationServiceModel) => {
|
||||
const ownerName = storedOwners.get(conf.owner.toLowerCase()) || 'UNKNOWN'
|
||||
|
||||
// return makeTransaction({
|
||||
// name,
|
||||
// nonce: tx.nonce,
|
||||
// value: Number(tx.value),
|
||||
// confirmations,
|
||||
// destination: tx.to,
|
||||
// data: tx.data ? tx.data : EMPTY_DATA,
|
||||
// isExecuted: tx.isExecuted,
|
||||
// })
|
||||
// }
|
||||
return makeConfirmation({
|
||||
owner: makeOwner({ address: conf.owner, name: ownerName }),
|
||||
type: ((conf.confirmationType.toLowerCase(): any): TxServiceType),
|
||||
hash: conf.transactionHash,
|
||||
})
|
||||
}),
|
||||
)
|
||||
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) => {
|
||||
// const url = buildTxServiceUrlFrom(safeAddress)
|
||||
// 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))
|
||||
let executionTxHash
|
||||
const executionTx = confirmations.find(conf => conf.type === TX_TYPE_EXECUTION)
|
||||
|
||||
// return Map().set(safeAddress, List(txsRecord))
|
||||
// }
|
||||
if (executionTx) {
|
||||
executionTxHash = executionTx.hash
|
||||
}
|
||||
|
||||
// export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
// const transactions: Map<string, List<Transaction>> = await loadSafeTransactions(safeAddress)
|
||||
let symbol = 'ETH'
|
||||
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
|
||||
import type { Store, AnyAction } from 'redux'
|
||||
import { List } from 'immutable'
|
||||
import { ADD_SAFE } from '~/routes/safe/store/actions/addSafe'
|
||||
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
|
||||
import { REMOVE_SAFE } from '~/routes/safe/store/actions/removeSafe'
|
||||
|
@ -38,75 +37,75 @@ const safeStorageMware = (store: Store<GlobalState>) => (next: Function) => asyn
|
|||
await saveSafes(safes.toJSON())
|
||||
|
||||
switch (action.type) {
|
||||
case ACTIVATE_TOKEN_FOR_ALL_SAFES: {
|
||||
let { activeTokens } = action.payload
|
||||
if (activeTokens) {
|
||||
const tokens = tokensSelector(state)
|
||||
const activeTokenAddresses = getActiveTokensAddressesForAllSafes(state)
|
||||
case ACTIVATE_TOKEN_FOR_ALL_SAFES: {
|
||||
let { activeTokens } = action.payload
|
||||
if (activeTokens) {
|
||||
const tokens = tokensSelector(state)
|
||||
const activeTokenAddresses = getActiveTokensAddressesForAllSafes(state)
|
||||
|
||||
activeTokens = tokens.withMutations((map) => {
|
||||
map.forEach((token: Token) => {
|
||||
if (!activeTokenAddresses.has(token.address)) {
|
||||
map.remove(token.address)
|
||||
}
|
||||
activeTokens = tokens.withMutations((map) => {
|
||||
map.forEach((token: Token) => {
|
||||
if (!activeTokenAddresses.has(token.address)) {
|
||||
map.remove(token.address)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
saveActiveTokens(activeTokens)
|
||||
saveActiveTokens(activeTokens)
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
case ADD_SAFE: {
|
||||
const { safe } = action.payload
|
||||
setOwners(safe.address, safe.owners)
|
||||
break
|
||||
}
|
||||
case UPDATE_SAFE: {
|
||||
const { safeAddress, owners } = action.payload
|
||||
if (safeAddress && owners) {
|
||||
setOwners(safeAddress, owners)
|
||||
case ADD_SAFE: {
|
||||
const { safe } = action.payload
|
||||
setOwners(safe.address, safe.owners)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
case REMOVE_SAFE: {
|
||||
const { safeAddress } = action.payload
|
||||
await removeOwners(safeAddress)
|
||||
break
|
||||
}
|
||||
case ADD_SAFE_OWNER: {
|
||||
const { safeAddress, ownerAddress, ownerName } = action.payload
|
||||
const owners = List(safes.get(safeAddress).owners)
|
||||
setOwners(safeAddress, owners.push(makeOwner({ address: ownerAddress, name: ownerName })))
|
||||
break
|
||||
}
|
||||
case REMOVE_SAFE_OWNER: {
|
||||
const { safeAddress, ownerAddress } = action.payload
|
||||
const owners = List(safes.get(safeAddress).owners)
|
||||
setOwners(safeAddress, owners.filter(o => o.address.toLowerCase() !== ownerAddress.toLowerCase()))
|
||||
break
|
||||
}
|
||||
case REPLACE_SAFE_OWNER: {
|
||||
const {
|
||||
safeAddress, ownerAddress, ownerName, oldOwnerAddress,
|
||||
} = action.payload
|
||||
const owners = List(safes.get(safeAddress).owners)
|
||||
setOwners(
|
||||
safeAddress,
|
||||
owners
|
||||
.filter(o => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase())
|
||||
.push(makeOwner({ address: ownerAddress, name: ownerName })),
|
||||
)
|
||||
break
|
||||
}
|
||||
case EDIT_SAFE_OWNER: {
|
||||
const { safeAddress, ownerAddress, ownerName } = action.payload
|
||||
const owners = List(safes.get(safeAddress).owners)
|
||||
const ownerToUpdateIndex = owners.findIndex(o => o.address.toLowerCase() === ownerAddress.toLowerCase())
|
||||
setOwners(safeAddress, owners.update(ownerToUpdateIndex, owner => owner.set('name', ownerName)))
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
case UPDATE_SAFE: {
|
||||
const { safeAddress, owners } = action.payload
|
||||
if (safeAddress && owners) {
|
||||
setOwners(safeAddress, owners)
|
||||
}
|
||||
break
|
||||
}
|
||||
case REMOVE_SAFE: {
|
||||
const { safeAddress } = action.payload
|
||||
await removeOwners(safeAddress)
|
||||
break
|
||||
}
|
||||
case ADD_SAFE_OWNER: {
|
||||
const { safeAddress, ownerAddress, ownerName } = action.payload
|
||||
const { owners } = safes.get(safeAddress)
|
||||
setOwners(safeAddress, owners.push(makeOwner({ address: ownerAddress, name: ownerName })))
|
||||
break
|
||||
}
|
||||
case REMOVE_SAFE_OWNER: {
|
||||
const { safeAddress, ownerAddress } = action.payload
|
||||
const { owners } = safes.get(safeAddress)
|
||||
setOwners(safeAddress, owners.filter(o => o.address.toLowerCase() !== ownerAddress.toLowerCase()))
|
||||
break
|
||||
}
|
||||
case REPLACE_SAFE_OWNER: {
|
||||
const {
|
||||
safeAddress, ownerAddress, ownerName, oldOwnerAddress,
|
||||
} = action.payload
|
||||
const { owners } = safes.get(safeAddress)
|
||||
setOwners(
|
||||
safeAddress,
|
||||
owners
|
||||
.filter(o => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase())
|
||||
.push(makeOwner({ address: ownerAddress, name: ownerName })),
|
||||
)
|
||||
break
|
||||
}
|
||||
case EDIT_SAFE_OWNER: {
|
||||
const { safeAddress, ownerAddress, ownerName } = action.payload
|
||||
const { owners } = safes.get(safeAddress)
|
||||
const ownerToUpdateIndex = owners.findIndex(o => o.address.toLowerCase() === ownerAddress.toLowerCase())
|
||||
setOwners(safeAddress, owners.update(ownerToUpdateIndex, owner => owner.set('name', ownerName)))
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { Record } from 'immutable'
|
||||
import type { RecordFactory, RecordOf } from 'immutable'
|
||||
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 = {
|
||||
owner: Owner,
|
||||
|
|
|
@ -3,14 +3,27 @@ import { List, Record } from 'immutable'
|
|||
import type { RecordFactory, RecordOf } from 'immutable'
|
||||
import { type Confirmation } from '~/routes/safe/store/models/confirmation'
|
||||
|
||||
export type TransactionStatus = 'awaiting_confirmations' | 'success' | 'cancelled' | 'awaiting_execution'
|
||||
|
||||
export type TransactionProps = {
|
||||
name: string,
|
||||
nonce: number,
|
||||
value: number,
|
||||
value: string,
|
||||
confirmations: List<Confirmation>,
|
||||
destination: string,
|
||||
recipient: string,
|
||||
data: string,
|
||||
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({
|
||||
|
@ -18,9 +31,20 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
|||
nonce: 0,
|
||||
value: 0,
|
||||
confirmations: List([]),
|
||||
destination: '',
|
||||
recipient: '',
|
||||
data: '',
|
||||
isExecuted: false,
|
||||
submissionDate: '',
|
||||
executionDate: '',
|
||||
symbol: '',
|
||||
executionTxHash: undefined,
|
||||
creationTxHash: '',
|
||||
cancelled: false,
|
||||
modifySettingsTx: false,
|
||||
cancellationTx: false,
|
||||
status: 'awaiting',
|
||||
isTokenTransfer: false,
|
||||
decodedParams: {},
|
||||
})
|
||||
|
||||
export type Transaction = RecordOf<TransactionProps>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import { List, Map } from 'immutable'
|
||||
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'
|
||||
|
||||
export const TRANSACTIONS_REDUCER_ID = 'transactions'
|
||||
|
|
|
@ -23,17 +23,15 @@ type TransactionProps = {
|
|||
transaction: Transaction,
|
||||
}
|
||||
|
||||
const safePropAddressSelector = (state: GlobalState, props: SafeProps) => props.safeAddress
|
||||
|
||||
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
||||
|
||||
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
|
||||
|
||||
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,
|
||||
safePropAddressSelector,
|
||||
safeParamAddressSelector,
|
||||
(transactions: TransactionsState, address: string): List<Transaction> => {
|
||||
if (!transactions) {
|
||||
return List([])
|
||||
|
|
|
@ -34,6 +34,7 @@ export const CreateSafe = ({ size, provider }: SafeProps) => (
|
|||
color="primary"
|
||||
disabled={!provider}
|
||||
minWidth={240}
|
||||
minHeight={42}
|
||||
>
|
||||
<Img src={plus} height={14} alt="Safe" />
|
||||
<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 fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
import { sleep } from '~/utils/timer'
|
||||
import { history } from '~/store'
|
||||
import { history, type GlobalState } from '~/store'
|
||||
import AppRoutes from '~/routes'
|
||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||
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 { calculateBalanceOf } from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||
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 { 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 { renderSafeView } from '~/test/builder/safe.dom.utils'
|
||||
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 { 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 { renderSafeView } from '~/test/builder/safe.dom.utils'
|
||||
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 { OWNERS_SETTINGS_TAB_TESTID } from '~/routes/safe/components/Settings'
|
||||
import {
|
||||
|
@ -162,7 +162,7 @@ describe('DOM > Feature > Settings - Manage owners', () => {
|
|||
fireEvent.click(SafeDom.getByTestId(ADD_OWNER_THRESHOLD_NEXT_BTN_TESTID))
|
||||
await sleep(200)
|
||||
fireEvent.click(SafeDom.getByTestId(ADD_OWNER_SUBMIT_BTN_TESTID))
|
||||
await sleep(1000)
|
||||
await sleep(1500)
|
||||
|
||||
// check if owner was added
|
||||
ownerRows = SafeDom.getAllByTestId(OWNERS_ROW_TESTID)
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
ADD_CUSTOM_TOKEN_FORM,
|
||||
} from '~/routes/safe/components/Balances/Tokens/screens/AddCustomToken'
|
||||
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
|
||||
const originalError = console.error
|
||||
|
|
|
@ -10,7 +10,7 @@ import saveTokens from '~/logic/tokens/store/actions/saveTokens'
|
|||
import { clickOnManageTokens, toggleToken, closeManageTokensModal } from './utils/DOMNavigation'
|
||||
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
|
||||
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', () => {
|
||||
let web3
|
||||
|
|
|
@ -43,6 +43,9 @@ export default createMuiTheme({
|
|||
},
|
||||
overrides: {
|
||||
MuiButton: {
|
||||
label: {
|
||||
lineHeight: 1,
|
||||
},
|
||||
root: {
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
letterSpacing: '0.9px',
|
||||
|
@ -57,6 +60,9 @@ export default createMuiTheme({
|
|||
containedPrimary: {
|
||||
backgroundColor: secondary,
|
||||
},
|
||||
containedSecondary: {
|
||||
backgroundColor: error,
|
||||
},
|
||||
sizeLarge: {
|
||||
padding: `${md} ${lg}`,
|
||||
minHeight: '52px',
|
||||
|
@ -78,6 +84,16 @@ export default createMuiTheme({
|
|||
padding: '24px 0 0 15px',
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
root: {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
root: {
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
},
|
||||
},
|
||||
MuiStepIcon: {
|
||||
root: {
|
||||
fontSize: '22px',
|
||||
|
|
Loading…
Reference in New Issue