Merge pull request #108 from gnosis/80-send-funds
#80 Send funds for threshold = 1
This commit is contained in:
commit
04db6a31c4
70
package.json
70
package.json
|
@ -65,14 +65,15 @@
|
||||||
"verbose": true
|
"verbose": true
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gnosis.pm/util-contracts": "2.0.1",
|
||||||
"@gnosis.pm/safe-contracts": "^1.0.0",
|
"@gnosis.pm/safe-contracts": "^1.0.0",
|
||||||
"@gnosis.pm/util-contracts": "^2.0.0",
|
"@material-ui/core": "4.0.0",
|
||||||
"@material-ui/core": "^3.9.3",
|
"@material-ui/icons": "4.0.0",
|
||||||
"@material-ui/icons": "^3.0.1",
|
"@welldone-software/why-did-you-render": "^3.0.9",
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
"bignumber.js": "^8.1.1",
|
"bignumber.js": "^8.1.1",
|
||||||
"connected-react-router": "^6.3.1",
|
"connected-react-router": "^6.3.1",
|
||||||
"final-form": "^4.2.1",
|
"final-form": "4.13.0",
|
||||||
"history": "^4.7.2",
|
"history": "^4.7.2",
|
||||||
"immortal-db": "^1.0.2",
|
"immortal-db": "^1.0.2",
|
||||||
"immutable": "^4.0.0-rc.9",
|
"immutable": "^4.0.0-rc.9",
|
||||||
|
@ -81,10 +82,11 @@
|
||||||
"qrcode.react": "^0.9.3",
|
"qrcode.react": "^0.9.3",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"react-final-form": "^4.1.0",
|
"react-final-form": "6.0.0",
|
||||||
"react-hot-loader": "4.8.4",
|
"react-final-form-listeners": "^1.0.2",
|
||||||
|
"react-hot-loader": "4.8.8",
|
||||||
"react-infinite-scroll-component": "^4.5.2",
|
"react-infinite-scroll-component": "^4.5.2",
|
||||||
"react-redux": "7.0.2",
|
"react-redux": "7.0.3",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
"recompose": "^0.30.0",
|
"recompose": "^0.30.0",
|
||||||
"redux": "^4.0.1",
|
"redux": "^4.0.1",
|
||||||
|
@ -94,10 +96,10 @@
|
||||||
"web3": "1.0.0-beta.37"
|
"web3": "1.0.0-beta.37"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.0.0-beta.40",
|
"@babel/cli": "7.4.4",
|
||||||
"@babel/core": "^7.4.0",
|
"@babel/core": "7.4.5",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.4.0",
|
"@babel/plugin-proposal-class-properties": "7.4.4",
|
||||||
"@babel/plugin-proposal-decorators": "^7.4.0",
|
"@babel/plugin-proposal-decorators": "7.4.4",
|
||||||
"@babel/plugin-proposal-do-expressions": "^7.0.0",
|
"@babel/plugin-proposal-do-expressions": "^7.0.0",
|
||||||
"@babel/plugin-proposal-export-default-from": "^7.0.0",
|
"@babel/plugin-proposal-export-default-from": "^7.0.0",
|
||||||
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
|
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
|
||||||
|
@ -105,7 +107,7 @@
|
||||||
"@babel/plugin-proposal-function-sent": "^7.0.0",
|
"@babel/plugin-proposal-function-sent": "^7.0.0",
|
||||||
"@babel/plugin-proposal-json-strings": "^7.0.0",
|
"@babel/plugin-proposal-json-strings": "^7.0.0",
|
||||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.0.0",
|
"@babel/plugin-proposal-logical-assignment-operators": "^7.0.0",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
|
"@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4",
|
||||||
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
|
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
|
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
|
||||||
"@babel/plugin-proposal-pipeline-operator": "^7.0.0",
|
"@babel/plugin-proposal-pipeline-operator": "^7.0.0",
|
||||||
|
@ -114,20 +116,20 @@
|
||||||
"@babel/plugin-syntax-import-meta": "^7.0.0",
|
"@babel/plugin-syntax-import-meta": "^7.0.0",
|
||||||
"@babel/plugin-transform-member-expression-literals": "^7.2.0",
|
"@babel/plugin-transform-member-expression-literals": "^7.2.0",
|
||||||
"@babel/plugin-transform-property-literals": "^7.2.0",
|
"@babel/plugin-transform-property-literals": "^7.2.0",
|
||||||
"@babel/polyfill": "^7.4.0",
|
"@babel/polyfill": "7.4.4",
|
||||||
"@babel/preset-env": "^7.4.2",
|
"@babel/preset-env": "7.4.5",
|
||||||
"@babel/preset-flow": "^7.0.0-beta.40",
|
"@babel/preset-flow": "^7.0.0-beta.40",
|
||||||
"@babel/preset-react": "^7.0.0-beta.40",
|
"@babel/preset-react": "^7.0.0-beta.40",
|
||||||
"@sambego/storybook-state": "^1.0.7",
|
"@sambego/storybook-state": "^1.0.7",
|
||||||
"@storybook/addon-actions": "5.0.10",
|
"@storybook/addon-actions": "5.0.11",
|
||||||
"@storybook/addon-knobs": "5.0.10",
|
"@storybook/addon-knobs": "5.0.11",
|
||||||
"@storybook/addon-links": "5.0.10",
|
"@storybook/addon-links": "5.0.11",
|
||||||
"@storybook/react": "5.0.10",
|
"@storybook/react": "5.0.11",
|
||||||
"autoprefixer": "9.5.1",
|
"autoprefixer": "9.5.1",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
"babel-jest": "^24.1.0",
|
"babel-jest": "24.8.0",
|
||||||
"babel-loader": "^8.0.0-beta.0",
|
"babel-loader": "8.0.6",
|
||||||
"babel-plugin-dynamic-import-node": "^2.2.0",
|
"babel-plugin-dynamic-import-node": "^2.2.0",
|
||||||
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
|
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
|
||||||
"babel-plugin-transform-es3-property-literals": "^6.22.0",
|
"babel-plugin-transform-es3-property-literals": "^6.22.0",
|
||||||
|
@ -136,19 +138,19 @@
|
||||||
"detect-port": "^1.2.2",
|
"detect-port": "^1.2.2",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-config-airbnb": "^17.1.0",
|
"eslint-config-airbnb": "^17.1.0",
|
||||||
"eslint-plugin-flowtype": "3.6.1",
|
"eslint-plugin-flowtype": "3.9.1",
|
||||||
"eslint-plugin-import": "2.17.2",
|
"eslint-plugin-import": "2.17.3",
|
||||||
"eslint-plugin-jest": "22.5.1",
|
"eslint-plugin-jest": "22.6.4",
|
||||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||||
"eslint-plugin-react": "^7.7.0",
|
"eslint-plugin-react": "7.13.0",
|
||||||
"ethereumjs-abi": "^0.6.7",
|
"ethereumjs-abi": "^0.6.7",
|
||||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||||
"file-loader": "^3.0.1",
|
"file-loader": "^3.0.1",
|
||||||
"flow-bin": "0.97.0",
|
"flow-bin": "0.98.1",
|
||||||
"fs-extra": "^7.0.1",
|
"fs-extra": "8.0.1",
|
||||||
"html-loader": "^0.5.5",
|
"html-loader": "^0.5.5",
|
||||||
"html-webpack-plugin": "^3.0.4",
|
"html-webpack-plugin": "^3.0.4",
|
||||||
"jest": "^24.1.0",
|
"jest": "24.8.0",
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
"mini-css-extract-plugin": "0.6.0",
|
"mini-css-extract-plugin": "0.6.0",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
|
@ -160,14 +162,14 @@
|
||||||
"storybook-host": "^5.0.3",
|
"storybook-host": "^5.0.3",
|
||||||
"storybook-router": "^0.3.3",
|
"storybook-router": "^0.3.3",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"truffle": "5.0.14",
|
"truffle": "5.0.19",
|
||||||
"truffle-contract": "4.0.13",
|
"truffle-contract": "4.0.17",
|
||||||
"truffle-solidity-loader": "0.1.14",
|
"truffle-solidity-loader": "0.1.18",
|
||||||
"uglifyjs-webpack-plugin": "^2.1.2",
|
"uglifyjs-webpack-plugin": "2.1.3",
|
||||||
"webpack": "^4.1.1",
|
"webpack": "4.32.2",
|
||||||
"webpack-bundle-analyzer": "3.3.2",
|
"webpack-bundle-analyzer": "3.3.2",
|
||||||
"webpack-cli": "3.3.1",
|
"webpack-cli": "3.3.2",
|
||||||
"webpack-dev-server": "3.3.1",
|
"webpack-dev-server": "3.4.1",
|
||||||
"webpack-manifest-plugin": "^2.0.0-rc.2"
|
"webpack-manifest-plugin": "^2.0.0-rc.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,15 @@ const buildDotStyleFrom = (size: number, top: number, right: number, mode: Mode)
|
||||||
})
|
})
|
||||||
|
|
||||||
const KeyRing = ({
|
const KeyRing = ({
|
||||||
classes, circleSize, keySize, dotSize, dotTop, dotRight, mode, center = false, hideDot = false,
|
classes,
|
||||||
|
circleSize,
|
||||||
|
keySize,
|
||||||
|
dotSize,
|
||||||
|
dotTop,
|
||||||
|
dotRight,
|
||||||
|
mode,
|
||||||
|
center = false,
|
||||||
|
hideDot = false,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const keyStyle = buildKeyStyleFrom(circleSize, center, dotSize)
|
const keyStyle = buildKeyStyleFrom(circleSize, center, dotSize)
|
||||||
const dotStyle = buildDotStyleFrom(dotSize, dotTop, dotRight, mode)
|
const dotStyle = buildDotStyleFrom(dotSize, dotTop, dotRight, mode)
|
||||||
|
@ -80,7 +88,7 @@ const KeyRing = ({
|
||||||
className={isWarning ? classes.warning : undefined}
|
className={isWarning ? classes.warning : undefined}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
{ !hideDot && <Dot className={classes.dot} style={dotStyle} /> }
|
{!hideDot && <Dot className={classes.dot} style={dotStyle} />}
|
||||||
</Block>
|
</Block>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
@ -55,16 +55,13 @@ class Provider extends React.Component<Props> {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div ref={this.myRef} className={classes.root}>
|
<div ref={this.myRef} className={classes.root}>
|
||||||
<Col end="sm" middle="xs" className={classes.provider} onClick={toggle}>
|
<Col end="sm" middle="xs" className={classes.provider} onClick={toggle}>
|
||||||
{ info }
|
{info}
|
||||||
<IconButton
|
<IconButton disableRipple className={classes.expand}>
|
||||||
disableRipple
|
{open ? <ExpandLess /> : <ExpandMore />}
|
||||||
className={classes.expand}
|
|
||||||
>
|
|
||||||
{ open ? <ExpandLess /> : <ExpandMore />}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Col>
|
</Col>
|
||||||
</div>
|
</div>
|
||||||
{ children(this.myRef) }
|
{children(this.myRef)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,18 +54,20 @@ const ProviderInfo = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{ connected &&
|
{connected && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Identicon address={identiconAddress} diameter={30} />
|
<Identicon address={identiconAddress} diameter={30} />
|
||||||
<Dot className={classes.logo} />
|
<Dot className={classes.logo} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
)}
|
||||||
{ !connected &&
|
{!connected && <CircleDot keySize={14} circleSize={35} dotSize={16} dotTop={24} dotRight={11} mode="warning" />}
|
||||||
<CircleDot keySize={14} circleSize={35} dotSize={16} dotTop={24} dotRight={11} mode="warning" />
|
|
||||||
}
|
|
||||||
<Col start="sm" layout="column" className={classes.account}>
|
<Col start="sm" layout="column" className={classes.account}>
|
||||||
<Paragraph size="sm" transform="capitalize" className={classes.network} noMargin weight="bolder">{providerText}</Paragraph>
|
<Paragraph size="sm" transform="capitalize" className={classes.network} noMargin weight="bolder">
|
||||||
<Paragraph size="sm" className={classes.address} noMargin color={color}>{cutAddress}</Paragraph>
|
{providerText}
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph size="sm" className={classes.address} noMargin color={color}>
|
||||||
|
{cutAddress}
|
||||||
|
</Paragraph>
|
||||||
</Col>
|
</Col>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
@ -33,8 +33,12 @@ const ProviderDesconnected = ({ classes }: Props) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<CircleDot keySize={17} circleSize={35} dotSize={16} dotTop={24} dotRight={11} mode="error" />
|
<CircleDot keySize={17} circleSize={35} dotSize={16} dotTop={24} dotRight={11} mode="error" />
|
||||||
<Col end="sm" middle="xs" layout="column" className={classes.account}>
|
<Col end="sm" middle="xs" layout="column" className={classes.account}>
|
||||||
<Paragraph size="sm" transform="capitalize" className={classes.network} noMargin weight="bold">Not Connected</Paragraph>
|
<Paragraph size="sm" transform="capitalize" className={classes.network} noMargin weight="bold">
|
||||||
<Paragraph size="sm" color="fancy" className={classes.connect} noMargin>Connect Wallet</Paragraph>
|
Not Connected
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph size="sm" color="fancy" className={classes.connect} noMargin>
|
||||||
|
Connect Wallet
|
||||||
|
</Paragraph>
|
||||||
</Col>
|
</Col>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,9 +12,10 @@ import Layout from './component/Layout'
|
||||||
import actions, { type Actions } from './actions'
|
import actions, { type Actions } from './actions'
|
||||||
import selector, { type SelectorProps } from './selector'
|
import selector, { type SelectorProps } from './selector'
|
||||||
|
|
||||||
type Props = Actions & SelectorProps & {
|
type Props = Actions &
|
||||||
openSnackbar: (message: string, variant: Variant) => void,
|
SelectorProps & {
|
||||||
}
|
openSnackbar: (message: string, variant: Variant) => void,
|
||||||
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
hasError: boolean,
|
hasError: boolean,
|
||||||
|
@ -26,22 +27,19 @@ class HeaderComponent extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchProvider(this.props.openSnackbar)
|
this.onConnect()
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error: Error, info: Info) {
|
|
||||||
this.setState({ hasError: true })
|
|
||||||
this.props.openSnackbar(WALLET_ERROR_MSG, 'error')
|
|
||||||
|
|
||||||
logComponentStack(error, info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDisconnect = () => {
|
onDisconnect = () => {
|
||||||
this.props.removeProvider(this.props.openSnackbar)
|
const { removeProvider, openSnackbar } = this.props
|
||||||
|
|
||||||
|
removeProvider(openSnackbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
onConnect = () => {
|
onConnect = () => {
|
||||||
this.props.fetchProvider(this.props.openSnackbar)
|
const { fetchProvider, openSnackbar } = this.props
|
||||||
|
|
||||||
|
fetchProvider(openSnackbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
getProviderInfoBased = () => {
|
getProviderInfoBased = () => {
|
||||||
|
@ -67,13 +65,23 @@ class HeaderComponent extends React.PureComponent<Props, State> {
|
||||||
return <ConnectDetails onConnect={this.onConnect} />
|
return <ConnectDetails onConnect={this.onConnect} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<UserDetails
|
return (
|
||||||
provider={provider}
|
<UserDetails
|
||||||
network={network}
|
provider={provider}
|
||||||
userAddress={userAddress}
|
network={network}
|
||||||
connected={available}
|
userAddress={userAddress}
|
||||||
onDisconnect={this.onDisconnect}
|
connected={available}
|
||||||
/>)
|
onDisconnect={this.onDisconnect}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: Info) {
|
||||||
|
const { openSnackbar } = this.props
|
||||||
|
this.setState({ hasError: true })
|
||||||
|
openSnackbar(WALLET_ERROR_MSG, 'error')
|
||||||
|
|
||||||
|
logComponentStack(error, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -84,14 +92,13 @@ class HeaderComponent extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header = connect(selector, actions)(HeaderComponent)
|
const Header = connect(
|
||||||
|
selector,
|
||||||
|
actions,
|
||||||
|
)(HeaderComponent)
|
||||||
|
|
||||||
const HeaderSnack = () => (
|
const HeaderSnack = () => (
|
||||||
<SharedSnackbarConsumer>
|
<SharedSnackbarConsumer>{({ openSnackbar }) => <Header openSnackbar={openSnackbar} />}</SharedSnackbarConsumer>
|
||||||
{({ openSnackbar }) => (
|
|
||||||
<Header openSnackbar={openSnackbar} />
|
|
||||||
)}
|
|
||||||
</SharedSnackbarConsumer>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export default HeaderSnack
|
export default HeaderSnack
|
||||||
|
|
|
@ -16,7 +16,7 @@ export type SelectorProps = {
|
||||||
available: boolean,
|
available: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createStructuredSelector({
|
export default createStructuredSelector<Object, *>({
|
||||||
provider: providerNameSelector,
|
provider: providerNameSelector,
|
||||||
userAddress: userAccountSelector,
|
userAddress: userAccountSelector,
|
||||||
network: networkSelector,
|
network: networkSelector,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import cn from 'classnames'
|
||||||
import Modal from '@material-ui/core/Modal'
|
import Modal from '@material-ui/core/Modal'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
|
||||||
|
@ -10,6 +11,8 @@ type Props = {
|
||||||
handleClose: Function,
|
handleClose: Function,
|
||||||
children: React$Node,
|
children: React$Node,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
|
modalClassName: ?string,
|
||||||
|
paperClassName: ?string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = () => ({
|
const styles = () => ({
|
||||||
|
@ -35,18 +38,16 @@ const styles = () => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
const GnoModal = ({
|
const GnoModal = ({
|
||||||
title, description, open, children, handleClose, classes,
|
title, description, open, children, handleClose, modalClassName, classes, paperClassName,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<Modal
|
<Modal
|
||||||
aria-labelledby={title}
|
aria-labelledby={title}
|
||||||
aria-describedby={description}
|
aria-describedby={description}
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
className={classes.root}
|
className={cn(classes.root, modalClassName)}
|
||||||
>
|
>
|
||||||
<div className={classes.paper}>
|
<div className={cn(classes.paper, paperClassName)}>{children}</div>
|
||||||
{ children }
|
|
||||||
</div>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ const styles = theme => ({
|
||||||
},
|
},
|
||||||
iconVariant: {
|
iconVariant: {
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
marginRight: theme.spacing.unit,
|
marginRight: theme.spacing(1),
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
|
@ -15,6 +15,7 @@ type Props = {
|
||||||
padding?: number,
|
padding?: number,
|
||||||
validation?: (values: Object) => Object | Promise<Object>,
|
validation?: (values: Object) => Object | Promise<Object>,
|
||||||
initialValues?: Object,
|
initialValues?: Object,
|
||||||
|
formMutators?: Object,
|
||||||
}
|
}
|
||||||
|
|
||||||
const stylesBasedOn = (padding: number): $Shape<CSSStyleDeclaration> => ({
|
const stylesBasedOn = (padding: number): $Shape<CSSStyleDeclaration> => ({
|
||||||
|
@ -24,15 +25,16 @@ const stylesBasedOn = (padding: number): $Shape<CSSStyleDeclaration> => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
const GnoForm = ({
|
const GnoForm = ({
|
||||||
onSubmit, validation, initialValues, children, padding = 0,
|
onSubmit, validation, initialValues, children, padding = 0, formMutators,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<Form
|
<Form
|
||||||
validate={validation}
|
validate={validation}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
|
mutators={formMutators}
|
||||||
render={({ handleSubmit, ...rest }) => (
|
render={({ handleSubmit, ...rest }) => (
|
||||||
<form onSubmit={handleSubmit} style={stylesBasedOn(padding)}>
|
<form onSubmit={handleSubmit} style={stylesBasedOn(padding)}>
|
||||||
{children(rest.submitting, rest.validating, rest)}
|
{children(rest.submitting, rest.validating, rest, rest.form.mutators)}
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -16,32 +16,30 @@ const SelectInput = ({
|
||||||
meta,
|
meta,
|
||||||
label,
|
label,
|
||||||
formControlProps,
|
formControlProps,
|
||||||
|
classes,
|
||||||
|
renderValue,
|
||||||
...rest
|
...rest
|
||||||
}: SelectFieldProps) => {
|
}: SelectFieldProps) => {
|
||||||
const showError = ((meta.submitError && !meta.dirtySinceLastSubmit) || meta.error) && meta.touched
|
const showError = ((meta.submitError && !meta.dirtySinceLastSubmit) || meta.error) && meta.touched
|
||||||
const inputProps = { ...restInput, name }
|
const inputProps = {
|
||||||
|
...restInput,
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<FormControl {...formControlProps} error={showError} style={style}>
|
||||||
{...formControlProps}
|
|
||||||
error={showError}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<InputLabel htmlFor={name}>{label}</InputLabel>
|
<InputLabel htmlFor={name}>{label}</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
{...rest}
|
classes={classes}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
renderValue={renderValue}
|
||||||
inputProps={inputProps}
|
inputProps={inputProps}
|
||||||
value={value}
|
value={value}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{ showError &&
|
{showError && <FormHelperText>{meta.error || meta.submitError}</FormHelperText>}
|
||||||
<FormHelperText>
|
|
||||||
{meta.error || meta.submitError}
|
|
||||||
</FormHelperText>
|
|
||||||
}
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default SelectInput
|
export default SelectInput
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import cn from 'classnames'
|
|
||||||
import MuiTextField, { TextFieldProps } from '@material-ui/core/TextField'
|
import MuiTextField, { TextFieldProps } from '@material-ui/core/TextField'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import { lg } from '~/theme/variables'
|
import { lg } from '~/theme/variables'
|
||||||
|
@ -17,9 +16,6 @@ const styles = () => ({
|
||||||
paddingBottom: '12px',
|
paddingBottom: '12px',
|
||||||
lineHeight: 0,
|
lineHeight: 0,
|
||||||
},
|
},
|
||||||
input: {
|
|
||||||
borderRadius: '5px',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
class TextField extends React.PureComponent<TextFieldProps> {
|
class TextField extends React.PureComponent<TextFieldProps> {
|
||||||
|
@ -41,7 +37,7 @@ class TextField extends React.PureComponent<TextFieldProps> {
|
||||||
|
|
||||||
const inputRoot = helperText ? classes.root : undefined
|
const inputRoot = helperText ? classes.root : undefined
|
||||||
const inputProps = { ...restInput, autoComplete: 'off' }
|
const inputProps = { ...restInput, autoComplete: 'off' }
|
||||||
const inputRootProps = { ...inputAdornment, disableUnderline: !underline, className: cn(inputRoot, classes.input) }
|
const inputRootProps = { ...inputAdornment, disableUnderline: !underline, className: inputRoot }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MuiTextField
|
<MuiTextField
|
||||||
|
|
|
@ -11,14 +11,16 @@ const styles = {
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
minWidth?: number,
|
minWidth?: number,
|
||||||
|
minHeight?: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateStyleBased = minWidth => ({
|
const calculateStyleBased = (minWidth, minHeight) => ({
|
||||||
minWidth: `${minWidth}px`,
|
minWidth: minWidth && `${minWidth}px`,
|
||||||
|
minHeight: minHeight && `${minHeight}px`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const GnoButton = ({ minWidth, ...props }: Props) => {
|
const GnoButton = ({ minWidth, minHeight, ...props }: Props) => {
|
||||||
const style = minWidth ? calculateStyleBased(minWidth) : undefined
|
const style = calculateStyleBased(minWidth, minHeight)
|
||||||
|
|
||||||
return <Button style={style} {...props} />
|
return <Button style={style} {...props} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// @flow
|
||||||
|
/* eslint-disable react/button-has-type */
|
||||||
|
/* eslint-disable react/default-props-match-prop-types */
|
||||||
|
import * as React from 'react'
|
||||||
|
import cn from 'classnames/bind'
|
||||||
|
import styles from './index.scss'
|
||||||
|
|
||||||
|
const cx = cn.bind(styles)
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
const GnoButtonLink = ({
|
||||||
|
type, size, weight, color, ...props
|
||||||
|
}: Props) => (
|
||||||
|
<button type={type} className={cx(styles.btnLink, size, color, weight)} {...props} />
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
GnoButtonLink.defaultProps = {
|
||||||
|
type: 'button',
|
||||||
|
size: 'md',
|
||||||
|
weight: 'regular',
|
||||||
|
color: 'secondary',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GnoButtonLink
|
|
@ -0,0 +1,79 @@
|
||||||
|
.btnLink {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm {
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md {
|
||||||
|
font-size: $mediumFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg {
|
||||||
|
font-size: $largeFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xl {
|
||||||
|
font-size: $extraLargeFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xxl {
|
||||||
|
font-size: $xxlFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
font-weight: $lightFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regular {
|
||||||
|
font-weight: $regularFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bolder {
|
||||||
|
font-weight: $bolderFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: $boldFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
.soft {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
color: #686868;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancy {
|
||||||
|
color: $fancy;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: $warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
color: $fontColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
color: $secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
color: $disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white {
|
||||||
|
color: white;
|
||||||
|
}
|
|
@ -184,19 +184,19 @@
|
||||||
text-align: end;
|
text-align: end;
|
||||||
}
|
}
|
||||||
.top$(size) {
|
.top$(size) {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
.middle$(size) {
|
.middle$(size) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.bottom$(size) {
|
.bottom$(size) {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
.around$(size) {
|
.around$(size) {
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
.between$(size) {
|
.between$(size) {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { border } from '~/theme/variables'
|
||||||
|
|
||||||
const calculateStyleFrom = (color?: string, margin?: Size) => ({
|
const calculateStyleFrom = (color?: string, margin?: Size) => ({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
minHeight: '1px',
|
||||||
height: '1px',
|
height: '1px',
|
||||||
backgroundColor: color || border,
|
backgroundColor: color || border,
|
||||||
margin: `${getSize(margin)} 0px`,
|
margin: `${getSize(margin)} 0px`,
|
||||||
|
@ -13,12 +14,14 @@ const calculateStyleFrom = (color?: string, margin?: Size) => ({
|
||||||
type Props = {
|
type Props = {
|
||||||
margin?: Size,
|
margin?: Size,
|
||||||
color?: string,
|
color?: string,
|
||||||
|
style?: Object
|
||||||
}
|
}
|
||||||
|
|
||||||
const Hairline = ({ margin, color }: Props) => {
|
const Hairline = ({ margin, color, style }: Props) => {
|
||||||
const style = calculateStyleFrom(color, margin)
|
const calculatedStyles = calculateStyleFrom(color, margin)
|
||||||
|
const mergedStyles = { ...calculatedStyles, ...(style || {}) }
|
||||||
|
|
||||||
return <div style={style} />
|
return <div style={mergedStyles} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Hairline
|
export default Hairline
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
import classNames from 'classnames/bind'
|
import classNames from 'classnames/bind'
|
||||||
import React from 'react'
|
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { capitalize } from '~/utils/css'
|
import { capitalize } from '~/utils/css'
|
||||||
import styles from './index.scss'
|
import styles from './index.scss'
|
||||||
|
@ -13,20 +13,35 @@ type Props = {
|
||||||
children: React$Node,
|
children: React$Node,
|
||||||
color?: 'regular' | 'white',
|
color?: 'regular' | 'white',
|
||||||
className?: string,
|
className?: string,
|
||||||
|
innerRef: React.ElementRef<any>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const GnosisLink = ({
|
const GnosisLink = ({
|
||||||
to, children, color, className, padding, ...props
|
to, children, color, className, padding, innerRef, ...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const internal = /^\/(?!\/)/.test(to)
|
const internal = /^\/(?!\/)/.test(to)
|
||||||
const classes = cx(styles.link, color || 'regular', padding ? capitalize(padding, 'padding') : undefined, className)
|
const classes = cx(styles.link, color || 'regular', padding ? capitalize(padding, 'padding') : undefined, className)
|
||||||
const LinkElement = internal ? Link : 'a'
|
const LinkElement = internal ? Link : 'a'
|
||||||
|
const refs = {}
|
||||||
|
if (internal) {
|
||||||
|
// To avoid warning about React not recognizing the prop innerRef on native element (a) if the link is external
|
||||||
|
refs.innerRef = innerRef
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinkElement className={classes} href={internal ? null : to} to={internal ? to : null} {...props}>
|
<LinkElement
|
||||||
|
className={classes}
|
||||||
|
href={internal ? null : to}
|
||||||
|
to={internal ? to : null}
|
||||||
|
{...refs}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</LinkElement>
|
</LinkElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GnosisLink
|
// https://material-ui.com/guides/composition/#caveat-with-refs
|
||||||
|
const LinkWithRef = React.forwardRef<Props, typeof GnosisLink>((props, ref) => <GnosisLink {...props} innerRef={ref} />)
|
||||||
|
|
||||||
|
export default LinkWithRef
|
||||||
|
|
|
@ -9,7 +9,7 @@ const cx = classNames.bind(styles)
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string,
|
className?: string,
|
||||||
children: React$Node,
|
children: React$Node,
|
||||||
margin?: 'sm' | 'md' | 'lg' | 'xl',
|
margin?: 'xs' | 'sm' | 'md' | 'lg' | 'xl',
|
||||||
align?: 'center' | 'end' | 'start',
|
align?: 'center' | 'end' | 'start',
|
||||||
grow?: boolean,
|
grow?: boolean,
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,11 @@
|
||||||
.grow {
|
.grow {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.marginXs {
|
||||||
|
margin-bottom: $xs;
|
||||||
|
}
|
||||||
|
|
||||||
.marginSm {
|
.marginSm {
|
||||||
margin-bottom: $sm;
|
margin-bottom: $sm;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ const devConfig = {
|
||||||
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
||||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||||
[SIGNATURES_VIA_METAMASK]: false,
|
[SIGNATURES_VIA_METAMASK]: false,
|
||||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
|
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default devConfig
|
export default devConfig
|
||||||
|
|
|
@ -10,7 +10,7 @@ const prodConfig = {
|
||||||
[TX_SERVICE_HOST]: 'https://safe-transaction-history.dev.gnosisdev.com/api/v1/',
|
[TX_SERVICE_HOST]: 'https://safe-transaction-history.dev.gnosisdev.com/api/v1/',
|
||||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||||
[SIGNATURES_VIA_METAMASK]: false,
|
[SIGNATURES_VIA_METAMASK]: false,
|
||||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
|
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default prodConfig
|
export default prodConfig
|
||||||
|
|
|
@ -10,7 +10,7 @@ const testConfig = {
|
||||||
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
||||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||||
[SIGNATURES_VIA_METAMASK]: false,
|
[SIGNATURES_VIA_METAMASK]: false,
|
||||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
|
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default testConfig
|
export default testConfig
|
||||||
|
|
|
@ -8,6 +8,12 @@ import { store } from '~/store'
|
||||||
import loadSafesFromStorage from '~/routes/safe/store/actions/loadSafesFromStorage'
|
import loadSafesFromStorage from '~/routes/safe/store/actions/loadSafesFromStorage'
|
||||||
import loadActiveTokens from '~/logic/tokens/store/actions/loadActiveTokens'
|
import loadActiveTokens from '~/logic/tokens/store/actions/loadActiveTokens'
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
||||||
|
whyDidYouRender(React)
|
||||||
|
}
|
||||||
|
|
||||||
store.dispatch(loadActiveTokens())
|
store.dispatch(loadActiveTokens())
|
||||||
store.dispatch(loadSafesFromStorage())
|
store.dispatch(loadSafesFromStorage())
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
// @flow
|
||||||
|
import axios from 'axios'
|
||||||
|
import { getRelayUrl } from '~/config/index'
|
||||||
|
|
||||||
|
export const estimateTxGas = (safeAddress: string, to: string, value: string, data, operation = 0) => {
|
||||||
|
const apiUrl = getRelayUrl()
|
||||||
|
const url = `${apiUrl}/safes/${safeAddress}/transactions/estimate/`
|
||||||
|
// const estimationValue = isTokenTransfer(tx.data) ? '0' : value.toString(10)
|
||||||
|
|
||||||
|
return axios.post(url, {
|
||||||
|
safe: safeAddress,
|
||||||
|
to,
|
||||||
|
data: '0x',
|
||||||
|
value,
|
||||||
|
operation,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export * from './estimateTxGas'
|
|
@ -1,142 +0,0 @@
|
||||||
// @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/safeTxSignerEIP712'
|
|
||||||
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 (
|
|
||||||
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 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)
|
|
||||||
|
|
||||||
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 getGnosisSafeInstanceAt(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
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
import { executeTransaction, approveTransaction } from '~/logic/safe/safeBlockchainOperations'
|
import { executeTransaction, approveTransaction } from '~/logic/safe/transactions'
|
||||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import { type Safe } from '~/routes/safe/store/models/safe'
|
import { type Safe } from '~/routes/safe/store/models/safe'
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
// @flow
|
||||||
|
import { BigNumber } from 'bignumber.js'
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
|
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
|
|
||||||
|
const estimateDataGasCosts = (data) => {
|
||||||
|
const reducer = (accumulator, currentValue) => {
|
||||||
|
if (currentValue === EMPTY_DATA) {
|
||||||
|
return accumulator + 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentValue === '00') {
|
||||||
|
return accumulator + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
return accumulator + 68
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.match(/.{2}/g).reduce(reducer, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://gnosis-safe.readthedocs.io/en/latest/contracts/transactions.html#safe-transaction-data-gas-estimation
|
||||||
|
// https://github.com/gnosis/safe-contracts/blob/a97c6fd24f79c0b159ddd25a10a2ebd3ea2ef926/test/utils/execution.js
|
||||||
|
export const estimateDataGas = (
|
||||||
|
safe: any,
|
||||||
|
to: string,
|
||||||
|
valueInWei: number,
|
||||||
|
from: string,
|
||||||
|
data: string,
|
||||||
|
operation: number,
|
||||||
|
txGasEstimate: number,
|
||||||
|
gasToken: number,
|
||||||
|
nonce: number,
|
||||||
|
signatureCount: number,
|
||||||
|
refundReceiver: number,
|
||||||
|
) => {
|
||||||
|
// numbers < 256 are 192 -> 31 * 4 + 68
|
||||||
|
// numbers < 65k are 256 -> 30 * 4 + 2 * 68
|
||||||
|
// For signature array length and dataGasEstimate we already calculated
|
||||||
|
// the 0 bytes so we just add 64 for each non-zero byte
|
||||||
|
const gasPrice = 0 // no need to get refund when we submit txs to metamask
|
||||||
|
const signatureCost = signatureCount * (68 + 2176 + 2176 + 6000) // array count (3 -> r, s, v) * signature count
|
||||||
|
|
||||||
|
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||||
|
const sigs = `0x000000000000000000000000${from.replace(
|
||||||
|
'0x',
|
||||||
|
'',
|
||||||
|
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||||
|
const payload = safe.contract.methods
|
||||||
|
.execTransaction(to, valueInWei, data, operation, txGasEstimate, 0, gasPrice, gasToken, refundReceiver, sigs)
|
||||||
|
.encodeABI()
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const dataGasEstimate = estimateDataGasCosts(payload) + signatureCost + (nonce > 0 ? 5000 : 20000) + 1500 // 1500 -> hash generation costs
|
||||||
|
|
||||||
|
return dataGasEstimate + 32000 // Add aditional gas costs (e.g. base tx costs, transfer costs)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateTxGasEstimateFrom = async (
|
||||||
|
safe: any,
|
||||||
|
safeAddress: string,
|
||||||
|
data: string,
|
||||||
|
to: string,
|
||||||
|
valueInWei: number,
|
||||||
|
operation: number,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
let safeInstance = safe
|
||||||
|
if (!safeInstance) {
|
||||||
|
safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
const estimateData = safeInstance.contract.methods.requiredTxGas(to, valueInWei, data, operation).encodeABI()
|
||||||
|
const estimateResponse = await getWeb3().eth.call({
|
||||||
|
to: safeAddress,
|
||||||
|
from: safeAddress,
|
||||||
|
data: estimateData,
|
||||||
|
})
|
||||||
|
const txGasEstimate = new BigNumber(estimateResponse.substring(138), 16)
|
||||||
|
|
||||||
|
// Add 10k else we will fail in case of nested calls
|
||||||
|
return txGasEstimate.toNumber() + 10000
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log('Error calculating tx gas estimation ' + error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const calculateTxFee = async (
|
||||||
|
safe: any,
|
||||||
|
safeAddress: string,
|
||||||
|
from: string,
|
||||||
|
data: string,
|
||||||
|
to: string,
|
||||||
|
valueInWei: number,
|
||||||
|
operation: number,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
let safeInstance = safe
|
||||||
|
if (!safeInstance) {
|
||||||
|
safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||||
|
const sigs = `0x000000000000000000000000${from.replace(
|
||||||
|
'0x',
|
||||||
|
'',
|
||||||
|
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||||
|
|
||||||
|
// we get gas limit from this call, then it needs to be multiplied by the gas price
|
||||||
|
// https://safe-relay.gnosis.pm/api/v1/gas-station/
|
||||||
|
// https://safe-relay.rinkeby.gnosis.pm/api/v1/about/
|
||||||
|
const estimate = await safeInstance.execTransaction.estimateGas(
|
||||||
|
to,
|
||||||
|
valueInWei,
|
||||||
|
data,
|
||||||
|
operation,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
'0x0000000000000000000000000000000000000000',
|
||||||
|
'0x0000000000000000000000000000000000000000',
|
||||||
|
sigs,
|
||||||
|
{ from: '0xbc2BB26a6d821e69A38016f3858561a1D80d4182' },
|
||||||
|
)
|
||||||
|
|
||||||
|
return estimate
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log('Error calculating tx gas estimation ' + error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
// @flow
|
||||||
|
export * from './gas'
|
||||||
|
export * from './send'
|
||||||
|
export * from './safeBlockchainOperations'
|
||||||
|
export * from './safeTxSignerEIP712'
|
|
@ -0,0 +1,142 @@
|
||||||
|
// @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'
|
||||||
|
|
||||||
|
export const approveTransaction = async (
|
||||||
|
safeAddress: string,
|
||||||
|
to: string,
|
||||||
|
valueInWei: number,
|
||||||
|
data: string,
|
||||||
|
operation: Operation,
|
||||||
|
nonce: number,
|
||||||
|
sender: string,
|
||||||
|
) => {
|
||||||
|
const gasPrice = await calculateGasPrice()
|
||||||
|
|
||||||
|
if (signaturesViaMetamask()) {
|
||||||
|
// return executeTransaction(safeAddress, to, valueInWei, data, operation, nonce, sender)
|
||||||
|
const safe = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
const txGasEstimate = await generateTxGasEstimateFrom(safe, safeAddress, data, to, valueInWei, operation)
|
||||||
|
const signature = await generateMetamaskSignature(
|
||||||
|
safe,
|
||||||
|
safeAddress,
|
||||||
|
sender,
|
||||||
|
to,
|
||||||
|
valueInWei,
|
||||||
|
nonce,
|
||||||
|
data,
|
||||||
|
operation,
|
||||||
|
txGasEstimate,
|
||||||
|
)
|
||||||
|
storeSignature(safeAddress, nonce, signature)
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
const contractTxHash = await gnosisSafe.getTransactionHash(to, valueInWei, data, operation, 0, 0, 0, 0, 0, nonce)
|
||||||
|
|
||||||
|
const approveData = gnosisSafe.contract.methods.approveHash(contractTxHash).encodeABI()
|
||||||
|
const gas = await calculateGasOf(approveData, sender, safeAddress)
|
||||||
|
const txReceipt = await gnosisSafe.approveHash(contractTxHash, { from: sender, gas, gasPrice })
|
||||||
|
|
||||||
|
const txHash = txReceipt.tx
|
||||||
|
await checkReceiptStatus(txHash)
|
||||||
|
|
||||||
|
await submitOperation(safeAddress, to, valueInWei, data, operation, nonce, txHash, sender, 'confirmation')
|
||||||
|
|
||||||
|
return txHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const executeTransaction = async (
|
||||||
|
// safeAddress: string,
|
||||||
|
// 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
|
||||||
|
// }
|
|
@ -1,84 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import { BigNumber } from 'bignumber.js'
|
|
||||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
import { getSignaturesFrom } from '~/utils/storage/signatures'
|
|
||||||
|
|
||||||
const estimateDataGasCosts = (data) => {
|
|
||||||
const reducer = (accumulator, currentValue) => {
|
|
||||||
if (currentValue === EMPTY_DATA) {
|
|
||||||
return accumulator + 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentValue === '00') {
|
|
||||||
return accumulator + 4
|
|
||||||
}
|
|
||||||
|
|
||||||
return accumulator + 68
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.match(/.{2}/g).reduce(reducer, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const estimateDataGas = (
|
|
||||||
safe: any,
|
|
||||||
to: string,
|
|
||||||
valueInWei: number,
|
|
||||||
data: string,
|
|
||||||
operation: number,
|
|
||||||
txGasEstimate: number,
|
|
||||||
gasToken: number,
|
|
||||||
nonce: number,
|
|
||||||
signatureCount: number,
|
|
||||||
refundReceiver: number,
|
|
||||||
) => {
|
|
||||||
// numbers < 256 are 192 -> 31 * 4 + 68
|
|
||||||
// numbers < 65k are 256 -> 30 * 4 + 2 * 68
|
|
||||||
// For signature array length and dataGasEstimate we already calculated
|
|
||||||
// the 0 bytes so we just add 64 for each non-zero byte
|
|
||||||
const gasPrice = 0 // no need to get refund when we submit txs to metamask
|
|
||||||
const signatureCost = signatureCount * (68 + 2176 + 2176) // array count (3 -> r, s, v) * signature count
|
|
||||||
|
|
||||||
const sigs = getSignaturesFrom(safe.address, nonce)
|
|
||||||
const payload = safe.contract.methods
|
|
||||||
.execTransaction(to, valueInWei, data, operation, txGasEstimate, 0, gasPrice, gasToken, refundReceiver, sigs)
|
|
||||||
.encodeABI()
|
|
||||||
|
|
||||||
let dataGasEstimate = estimateDataGasCosts(payload) + signatureCost
|
|
||||||
if (dataGasEstimate > 65536) {
|
|
||||||
dataGasEstimate += 64
|
|
||||||
} else {
|
|
||||||
dataGasEstimate += 128
|
|
||||||
}
|
|
||||||
return dataGasEstimate + 34000 // Add aditional gas costs (e.g. base tx costs, transfer costs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
export const generateTxGasEstimateFrom = async (
|
|
||||||
safe: any,
|
|
||||||
safeAddress: string,
|
|
||||||
data: string,
|
|
||||||
to: string,
|
|
||||||
valueInWei: number,
|
|
||||||
operation: number,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const estimateData = safe.contract.methods.requiredTxGas(to, valueInWei, data, operation).encodeABI()
|
|
||||||
const estimateResponse = await getWeb3().eth.call({
|
|
||||||
to: safeAddress,
|
|
||||||
from: safeAddress,
|
|
||||||
data: estimateData,
|
|
||||||
})
|
|
||||||
const txGasEstimate = new BigNumber(estimateResponse.substring(138), 16)
|
|
||||||
|
|
||||||
// Add 10k else we will fail in case of nested calls
|
|
||||||
return Promise.resolve(txGasEstimate.toNumber() + 10000)
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
console.log('Error calculating tx gas estimation ' + error)
|
|
||||||
return Promise.resolve(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateTypedDataFrom = async (
|
const generateTypedDataFrom = async (
|
||||||
safe: any,
|
safe: any,
|
||||||
|
@ -167,7 +89,7 @@ export const generateMetamaskSignature = async (
|
||||||
// To change once Metamask fixes their status
|
// To change once Metamask fixes their status
|
||||||
// https://github.com/MetaMask/metamask-extension/pull/5368
|
// https://github.com/MetaMask/metamask-extension/pull/5368
|
||||||
// https://github.com/MetaMask/metamask-extension/issues/5366
|
// https://github.com/MetaMask/metamask-extension/issues/5366
|
||||||
params: [jsonTypedData, sender],
|
params: [sender, jsonTypedData],
|
||||||
from: sender,
|
from: sender,
|
||||||
}
|
}
|
||||||
const txSignedResponse = await web3.currentProvider.sendAsync(signedTypedData)
|
const txSignedResponse = await web3.currentProvider.sendAsync(signedTypedData)
|
|
@ -0,0 +1,75 @@
|
||||||
|
// @flow
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
|
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'
|
||||||
|
|
||||||
|
export const CALL = 0
|
||||||
|
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
||||||
|
|
||||||
|
export const executeTransaction = async (
|
||||||
|
safeInstance: any,
|
||||||
|
to: string,
|
||||||
|
valueInWei: number,
|
||||||
|
data: string,
|
||||||
|
operation: number | string,
|
||||||
|
nonce: string | number,
|
||||||
|
sender: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
// https://gnosis-safe.readthedocs.io/en/latest/contracts/signatures.html#pre-validated-signatures
|
||||||
|
const sigs = `0x000000000000000000000000${sender.replace(
|
||||||
|
'0x',
|
||||||
|
'',
|
||||||
|
)}000000000000000000000000000000000000000000000000000000000000000001`
|
||||||
|
|
||||||
|
const tx = await safeInstance.execTransaction(
|
||||||
|
to,
|
||||||
|
valueInWei,
|
||||||
|
data,
|
||||||
|
CALL,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
ZERO_ADDRESS,
|
||||||
|
ZERO_ADDRESS,
|
||||||
|
sigs,
|
||||||
|
{ from: sender },
|
||||||
|
)
|
||||||
|
|
||||||
|
return tx
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log('Error executing the TX: ' + error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createTransaction = async (safeAddress: string, to: string, valueInEth: string, token: Token) => {
|
||||||
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const from = web3.currentProvider.selectedAddress
|
||||||
|
const threshold = await safeInstance.getThreshold()
|
||||||
|
const nonce = await safeInstance.nonce()
|
||||||
|
const valueInWei = web3.utils.toWei(valueInEth, 'ether')
|
||||||
|
const isExecution = threshold.toNumber() === 1
|
||||||
|
|
||||||
|
let txData = EMPTY_DATA
|
||||||
|
if (!isEther(token.symbol)) {
|
||||||
|
const StandardToken = await getStandardTokenContract()
|
||||||
|
const sendToken = await StandardToken.at(token.address)
|
||||||
|
|
||||||
|
txData = sendToken.contract.transfer(to, valueInWei).encodeABI()
|
||||||
|
}
|
||||||
|
|
||||||
|
let txHash
|
||||||
|
if (isExecution) {
|
||||||
|
txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
|
||||||
|
} else {
|
||||||
|
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
return txHash
|
||||||
|
}
|
|
@ -43,10 +43,7 @@ const styles = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Opening = ({
|
const Opening = ({
|
||||||
classes,
|
classes, name = 'Safe creation process', tx, network,
|
||||||
name = 'Safe creation process',
|
|
||||||
tx,
|
|
||||||
network,
|
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<Page align="center">
|
<Page align="center">
|
||||||
<Paragraph className={classes.page} color="secondary" size="xxl" weight="bolder" align="center">
|
<Paragraph className={classes.page} color="secondary" size="xxl" weight="bolder" align="center">
|
||||||
|
@ -68,13 +65,25 @@ const Opening = ({
|
||||||
</Block>
|
</Block>
|
||||||
<Block margin="md">
|
<Block margin="md">
|
||||||
<Paragraph size="md" align="center" weight="light" noMargin>
|
<Paragraph size="md" align="center" weight="light" noMargin>
|
||||||
This process should take a couple of minutes. <br />
|
This process should take a couple of minutes.
|
||||||
|
{' '}
|
||||||
|
<br />
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
{ tx &&
|
{tx && (
|
||||||
<Paragraph className={classes.follow} size="md" align="center" weight="light" noMargin>
|
<Paragraph className={classes.follow} size="md" align="center" weight="light" noMargin>
|
||||||
Follow progress on <a href={openTxInEtherScan(tx, network)} target="_blank" rel="noopener noreferrer" className={classes.etherscan}>Etherscan.io<OpenInNew className={classes.icon} /></a>
|
Follow progress on
|
||||||
|
{' '}
|
||||||
|
<a
|
||||||
|
href={openTxInEtherScan(tx, network)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={classes.etherscan}
|
||||||
|
>
|
||||||
|
Etherscan.io
|
||||||
|
<OpenInNew className={classes.icon} />
|
||||||
|
</a>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
}
|
)}
|
||||||
</Block>
|
</Block>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
// @flow
|
|
||||||
|
|
||||||
import AssetTableCell from './AssetTableCell'
|
|
||||||
|
|
||||||
export default AssetTableCell
|
|
|
@ -23,6 +23,7 @@ const styles = () => ({
|
||||||
padding: `${sm} ${lg}`,
|
padding: `${sm} ${lg}`,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
maxHeight: '75px',
|
maxHeight: '75px',
|
||||||
|
boxSizing: 'border-box',
|
||||||
},
|
},
|
||||||
manage: {
|
manage: {
|
||||||
fontSize: '24px',
|
fontSize: '24px',
|
||||||
|
@ -93,7 +94,14 @@ const Receive = ({
|
||||||
</Block>
|
</Block>
|
||||||
<Block align="center" className={classes.addressContainer}>
|
<Block align="center" className={classes.addressContainer}>
|
||||||
<Identicon address={safeAddress} diameter={32} />
|
<Identicon address={safeAddress} diameter={32} />
|
||||||
<Paragraph onClick={copyToClipboard} className={classes.address}>{safeAddress}</Paragraph>
|
<Paragraph
|
||||||
|
onClick={() => {
|
||||||
|
copyToClipboard(safeAddress)
|
||||||
|
}}
|
||||||
|
className={classes.address}
|
||||||
|
>
|
||||||
|
{safeAddress}
|
||||||
|
</Paragraph>
|
||||||
<Link className={classes.open} to={etherScanLink} target="_blank">
|
<Link className={classes.open} to={etherScanLink} target="_blank">
|
||||||
<OpenInNew style={openIconStyle} />
|
<OpenInNew style={openIconStyle} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react'
|
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
|
||||||
import Close from '@material-ui/icons/Close'
|
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
|
||||||
import Row from '~/components/layout/Row'
|
|
||||||
import { lg, md } from '~/theme/variables'
|
|
||||||
|
|
||||||
const styles = () => ({
|
|
||||||
heading: {
|
|
||||||
padding: `${md} ${lg}`,
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
manage: {
|
|
||||||
fontSize: '24px',
|
|
||||||
},
|
|
||||||
close: {
|
|
||||||
height: '35px',
|
|
||||||
width: '35px',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onClose: () => void,
|
|
||||||
classes: Object,
|
|
||||||
}
|
|
||||||
|
|
||||||
const Send = ({ classes, onClose }: Props) => (
|
|
||||||
<React.Fragment>
|
|
||||||
<Row align="center" grow className={classes.heading}>
|
|
||||||
<Paragraph className={classes.manage} noMargin>Send Funds</Paragraph>
|
|
||||||
<IconButton onClick={onClose} disableRipple>
|
|
||||||
<Close className={classes.close} />
|
|
||||||
</IconButton>
|
|
||||||
</Row>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default withStyles(styles)(Send)
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Link from '~/components/layout/Link'
|
||||||
|
import Bold from '~/components/layout/Bold'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Identicon from '~/components/Identicon'
|
||||||
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
|
import { secondary, xs } from '~/theme/variables'
|
||||||
|
|
||||||
|
const openIconStyle = {
|
||||||
|
height: '16px',
|
||||||
|
color: secondary,
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = () => ({
|
||||||
|
balanceContainer: {
|
||||||
|
fontSize: '12px',
|
||||||
|
lineHeight: 1.08,
|
||||||
|
letterSpacing: -0.5,
|
||||||
|
backgroundColor: '#eae9ef',
|
||||||
|
width: 'fit-content',
|
||||||
|
padding: '6px',
|
||||||
|
marginTop: xs,
|
||||||
|
borderRadius: '3px',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
classes: Object,
|
||||||
|
safeAddress: string,
|
||||||
|
etherScanLink: string,
|
||||||
|
safeName: string,
|
||||||
|
ethBalance: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SafeInfo = (props: Props) => {
|
||||||
|
const {
|
||||||
|
safeAddress, safeName, etherScanLink, ethBalance, classes,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={1}>
|
||||||
|
<Identicon address={safeAddress} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11} layout="column">
|
||||||
|
<Paragraph weight="bolder" noMargin style={{ lineHeight: 1 }}>
|
||||||
|
{safeName}
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin>
|
||||||
|
{safeAddress}
|
||||||
|
<Link to={etherScanLink} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Paragraph>
|
||||||
|
<Block className={classes.balanceContainer}>
|
||||||
|
<Paragraph noMargin>
|
||||||
|
Balance:
|
||||||
|
{' '}
|
||||||
|
<Bold>
|
||||||
|
{ethBalance}
|
||||||
|
{' '}
|
||||||
|
ETH
|
||||||
|
</Bold>
|
||||||
|
</Paragraph>
|
||||||
|
</Block>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(SafeInfo)
|
|
@ -0,0 +1,111 @@
|
||||||
|
// @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 Modal from '~/components/Modal'
|
||||||
|
import ChooseTxType from './screens/ChooseTxType'
|
||||||
|
import SendFunds from './screens/SendFunds'
|
||||||
|
import ReviewTx from './screens/ReviewTx'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
classes: Object,
|
||||||
|
isOpen: boolean,
|
||||||
|
safeAddress: string,
|
||||||
|
etherScanLink: string,
|
||||||
|
safeName: string,
|
||||||
|
ethBalance: string,
|
||||||
|
tokens: List<Token>,
|
||||||
|
selectedToken: string,
|
||||||
|
createTransaction: Function,
|
||||||
|
}
|
||||||
|
type ActiveScreen = 'chooseTxType' | 'sendFunds' | 'reviewTx'
|
||||||
|
|
||||||
|
type TxStateType =
|
||||||
|
| {
|
||||||
|
token: Token,
|
||||||
|
recipientAddress: string,
|
||||||
|
amount: string,
|
||||||
|
}
|
||||||
|
| Object
|
||||||
|
|
||||||
|
const styles = () => ({
|
||||||
|
smallerModalWindow: {
|
||||||
|
height: 'auto',
|
||||||
|
position: 'static',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Send = ({
|
||||||
|
onClose,
|
||||||
|
isOpen,
|
||||||
|
classes,
|
||||||
|
safeAddress,
|
||||||
|
etherScanLink,
|
||||||
|
safeName,
|
||||||
|
ethBalance,
|
||||||
|
tokens,
|
||||||
|
selectedToken,
|
||||||
|
createTransaction,
|
||||||
|
}: Props) => {
|
||||||
|
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('sendFunds')
|
||||||
|
const [tx, setTx] = useState<TxStateType>({})
|
||||||
|
const smallerModalSize = activeScreen === 'chooseTxType'
|
||||||
|
const handleTxCreation = (txInfo) => {
|
||||||
|
setActiveScreen('reviewTx')
|
||||||
|
setTx(txInfo)
|
||||||
|
}
|
||||||
|
const onClickBack = () => setActiveScreen('sendFunds')
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
setActiveScreen('sendFunds')
|
||||||
|
setTx({})
|
||||||
|
},
|
||||||
|
[isOpen],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Send Tokens"
|
||||||
|
description="Send Tokens Form"
|
||||||
|
handleClose={onClose}
|
||||||
|
open={isOpen}
|
||||||
|
paperClassName={cn(smallerModalSize && classes.smallerModalWindow)}
|
||||||
|
>
|
||||||
|
<React.Fragment>
|
||||||
|
{activeScreen === 'chooseTxType' && <ChooseTxType onClose={onClose} setActiveScreen={setActiveScreen} />}
|
||||||
|
{activeScreen === 'sendFunds' && (
|
||||||
|
<SendFunds
|
||||||
|
onClose={onClose}
|
||||||
|
setActiveScreen={setActiveScreen}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
safeName={safeName}
|
||||||
|
ethBalance={ethBalance}
|
||||||
|
tokens={tokens}
|
||||||
|
selectedToken={selectedToken}
|
||||||
|
onSubmit={handleTxCreation}
|
||||||
|
initialValues={tx}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeScreen === 'reviewTx' && (
|
||||||
|
<ReviewTx
|
||||||
|
tx={tx}
|
||||||
|
onClose={onClose}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
safeName={safeName}
|
||||||
|
ethBalance={ethBalance}
|
||||||
|
onClickBack={onClickBack}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(Send)
|
|
@ -0,0 +1,78 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import { lg, sm } from '~/theme/variables'
|
||||||
|
|
||||||
|
const styles = () => ({
|
||||||
|
heading: {
|
||||||
|
padding: `${sm} ${lg}`,
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '75px',
|
||||||
|
},
|
||||||
|
manage: {
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
closeIcon: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
buttonColumn: {
|
||||||
|
padding: '52px 0',
|
||||||
|
},
|
||||||
|
secondButton: {
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
classes: Object,
|
||||||
|
setActiveScreen: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChooseTxType = ({ classes, onClose, setActiveScreen }: Props) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
|
Send
|
||||||
|
</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center">
|
||||||
|
<Col layout="column" middle="xs" className={classes.buttonColumn}>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
minWidth={260}
|
||||||
|
minHeight={52}
|
||||||
|
onClick={() => setActiveScreen('sendFunds')}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
SEND FUNDS
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
className={classes.secondButton}
|
||||||
|
minWidth={260}
|
||||||
|
minHeight={52}
|
||||||
|
onClick={onClose}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
SEND CUSTOM TRANSACTION
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withStyles(styles)(ChooseTxType)
|
|
@ -0,0 +1,136 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import { SharedSnackbarConsumer } from '~/components/SharedSnackBar/Context'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import Link from '~/components/layout/Link'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Img from '~/components/layout/Img'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Identicon from '~/components/Identicon'
|
||||||
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
|
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||||
|
import ArrowDown from '../assets/arrow-down.svg'
|
||||||
|
import { secondary } from '~/theme/variables'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
classes: Object,
|
||||||
|
safeAddress: string,
|
||||||
|
etherScanLink: string,
|
||||||
|
safeName: string,
|
||||||
|
onClickBack: Function,
|
||||||
|
ethBalance: string,
|
||||||
|
tx: Object,
|
||||||
|
createTransaction: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
const openIconStyle = {
|
||||||
|
height: '16px',
|
||||||
|
color: secondary,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReviewTx = ({
|
||||||
|
onClose,
|
||||||
|
classes,
|
||||||
|
safeAddress,
|
||||||
|
etherScanLink,
|
||||||
|
safeName,
|
||||||
|
ethBalance,
|
||||||
|
tx,
|
||||||
|
onClickBack,
|
||||||
|
createTransaction,
|
||||||
|
}: Props) => (
|
||||||
|
<SharedSnackbarConsumer>
|
||||||
|
{({ openSnackbar }) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.headingText} noMargin>
|
||||||
|
Send Funds
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Block className={classes.container}>
|
||||||
|
<SafeInfo
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
safeName={safeName}
|
||||||
|
ethBalance={ethBalance}
|
||||||
|
/>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={1}>
|
||||||
|
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11} center="xs" layout="column">
|
||||||
|
<Hairline />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="xs">
|
||||||
|
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
|
||||||
|
Recipient
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md" align="center">
|
||||||
|
<Col xs={1}>
|
||||||
|
<Identicon address={tx.recipientAddress} diameter={32} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11} layout="column">
|
||||||
|
<Paragraph weight="bolder" onClick={copyToClipboard} noMargin>
|
||||||
|
{tx.recipientAddress}
|
||||||
|
<Link to={etherScanLink} target="_blank">
|
||||||
|
<OpenInNew style={openIconStyle} />
|
||||||
|
</Link>
|
||||||
|
</Paragraph>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="xs">
|
||||||
|
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
|
||||||
|
Amount
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md" align="center">
|
||||||
|
<Img src={tx.token.logoUri} height={28} alt={tx.token.name} onError={setImageToPlaceholder} />
|
||||||
|
<Paragraph size="md" noMargin className={classes.amount}>
|
||||||
|
{tx.amount}
|
||||||
|
{' '}
|
||||||
|
{tx.token.symbol}
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
</Block>
|
||||||
|
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClickBack}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={classes.button}
|
||||||
|
onClick={() => {
|
||||||
|
createTransaction(safeAddress, tx.recipientAddress, tx.amount, tx.token, openSnackbar)
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
minWidth={140}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
SUBMIT
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</SharedSnackbarConsumer>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withStyles(styles)(ReviewTx)
|
|
@ -0,0 +1,37 @@
|
||||||
|
// @flow
|
||||||
|
import { lg, md, sm } from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
heading: {
|
||||||
|
padding: `${sm} ${lg}`,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '75px',
|
||||||
|
},
|
||||||
|
annotation: {
|
||||||
|
letterSpacing: '-1px',
|
||||||
|
color: '#a2a8ba',
|
||||||
|
marginRight: 'auto',
|
||||||
|
marginLeft: '20px',
|
||||||
|
},
|
||||||
|
headingText: {
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
closeIcon: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
marginLeft: sm,
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,83 @@
|
||||||
|
// @flow
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import MenuItem from '@material-ui/core/MenuItem'
|
||||||
|
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||||
|
import ListItemText from '@material-ui/core/ListItemText'
|
||||||
|
import Field from '~/components/forms/Field'
|
||||||
|
import Img from '~/components/layout/Img'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import SelectField from '~/components/forms/SelectField'
|
||||||
|
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||||
|
import { required } from '~/components/forms/validator'
|
||||||
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
|
import { selectedTokenStyles, selectStyles } from './style'
|
||||||
|
|
||||||
|
type SelectFieldProps = {
|
||||||
|
tokens: List<Token>,
|
||||||
|
classes: Object,
|
||||||
|
initialValue: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectedTokenProps = {
|
||||||
|
token?: Token,
|
||||||
|
classes: Object,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectedToken = ({ token, classes }: SelectedTokenProps) => (
|
||||||
|
<MenuItem className={classes.container}>
|
||||||
|
{token ? (
|
||||||
|
<>
|
||||||
|
<ListItemIcon className={classes.tokenImage}>
|
||||||
|
<Img src={token.logoUri} height={28} alt={token.name} onError={setImageToPlaceholder} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
className={classes.tokenData}
|
||||||
|
primary={token.name}
|
||||||
|
secondary={`${token.balance} ${token.symbol}`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Paragraph color="disabled" size="lg" weight="light" style={{ opacity: 0.5 }}>
|
||||||
|
Select an asset*
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
|
||||||
|
const SelectedTokenStyled = withStyles(selectedTokenStyles)(SelectedToken)
|
||||||
|
|
||||||
|
type InitialTokenType = Token | string
|
||||||
|
|
||||||
|
const TokenSelectField = ({ tokens, classes, initialValue }: SelectFieldProps) => {
|
||||||
|
const [initialToken, setInitialToken] = useState<InitialTokenType>('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selectedToken = tokens.find(token => token.name === initialValue)
|
||||||
|
setInitialToken(selectedToken || '')
|
||||||
|
}, [initialValue])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
name="token"
|
||||||
|
component={SelectField}
|
||||||
|
classes={{ selectMenu: classes.selectMenu }}
|
||||||
|
validate={required}
|
||||||
|
renderValue={token => <SelectedTokenStyled token={token} />}
|
||||||
|
initialValue={initialToken}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
{tokens.map(token => (
|
||||||
|
<MenuItem key={token.address} value={token}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Img src={token.logoUri} height={28} alt={token.name} onError={setImageToPlaceholder} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={token.name} secondary={`${token.balance} ${token.symbol}`} />
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Field>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(selectStyles)(TokenSelectField)
|
|
@ -0,0 +1,24 @@
|
||||||
|
// @flow
|
||||||
|
import { sm } from '~/theme/variables'
|
||||||
|
|
||||||
|
export const selectedTokenStyles = () => ({
|
||||||
|
container: {
|
||||||
|
minHeight: '55px',
|
||||||
|
padding: 0,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
tokenData: {
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
lineHeight: '14px',
|
||||||
|
},
|
||||||
|
tokenImage: {
|
||||||
|
marginRight: sm,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const selectStyles = () => ({
|
||||||
|
selectMenu: {
|
||||||
|
paddingRight: 0,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,177 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react'
|
||||||
|
import { List } from 'immutable'
|
||||||
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
|
import { OnChange } from 'react-final-form-listeners'
|
||||||
|
import Close from '@material-ui/icons/Close'
|
||||||
|
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
|
import GnoForm from '~/components/forms/GnoForm'
|
||||||
|
import Col from '~/components/layout/Col'
|
||||||
|
import Button from '~/components/layout/Button'
|
||||||
|
import Block from '~/components/layout/Block'
|
||||||
|
import Hairline from '~/components/layout/Hairline'
|
||||||
|
import ButtonLink from '~/components/layout/ButtonLink'
|
||||||
|
import Field from '~/components/forms/Field'
|
||||||
|
import TextField from '~/components/forms/TextField'
|
||||||
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
|
import {
|
||||||
|
composeValidators,
|
||||||
|
required,
|
||||||
|
mustBeEthereumAddress,
|
||||||
|
mustBeFloat,
|
||||||
|
maxValue,
|
||||||
|
greaterThan,
|
||||||
|
} from '~/components/forms/validator'
|
||||||
|
import TokenSelectField from '~/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField'
|
||||||
|
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
|
import ArrowDown from '../assets/arrow-down.svg'
|
||||||
|
import { styles } from './style'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void,
|
||||||
|
classes: Object,
|
||||||
|
safeAddress: string,
|
||||||
|
etherScanLink: string,
|
||||||
|
safeName: string,
|
||||||
|
ethBalance: string,
|
||||||
|
selectedToken: string,
|
||||||
|
tokens: List<Token>,
|
||||||
|
onSubmit: Function,
|
||||||
|
initialValues: Object,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SendFunds = ({
|
||||||
|
classes,
|
||||||
|
onClose,
|
||||||
|
safeAddress,
|
||||||
|
etherScanLink,
|
||||||
|
safeName,
|
||||||
|
ethBalance,
|
||||||
|
tokens,
|
||||||
|
selectedToken,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
}: Props) => {
|
||||||
|
const handleSubmit = (values) => {
|
||||||
|
onSubmit(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formMutators = {
|
||||||
|
setMax: (args, state, utils) => {
|
||||||
|
const { token } = state.formState.values
|
||||||
|
|
||||||
|
utils.changeValue(state, 'amount', () => token && token.balance)
|
||||||
|
},
|
||||||
|
onTokenChange: (args, state, utils) => {
|
||||||
|
utils.changeValue(state, 'amount', () => '')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row align="center" grow className={classes.heading}>
|
||||||
|
<Paragraph weight="bolder" className={classes.manage} noMargin>
|
||||||
|
Send Funds
|
||||||
|
</Paragraph>
|
||||||
|
<Paragraph className={classes.annotation}>1 of 2</Paragraph>
|
||||||
|
<IconButton onClick={onClose} disableRipple>
|
||||||
|
<Close className={classes.closeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Block className={classes.formContainer}>
|
||||||
|
<SafeInfo safeAddress={safeAddress} etherScanLink={etherScanLink} safeName={safeName} ethBalance={ethBalance} />
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={1}>
|
||||||
|
<img src={ArrowDown} alt="Arrow Down" style={{ marginLeft: '8px' }} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={11} center="xs" layout="column">
|
||||||
|
<Hairline />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<GnoForm onSubmit={handleSubmit} formMutators={formMutators} initialValues={initialValues}>
|
||||||
|
{(...args) => {
|
||||||
|
const formState = args[2]
|
||||||
|
const mutators = args[3]
|
||||||
|
const { token } = formState.values
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col xs={12}>
|
||||||
|
<Field
|
||||||
|
name="recipientAddress"
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(required, mustBeEthereumAddress)}
|
||||||
|
placeholder="Recipient*"
|
||||||
|
text="Recipient*"
|
||||||
|
className={classes.addressInput}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="sm">
|
||||||
|
<Col>
|
||||||
|
<TokenSelectField tokens={tokens} initialValue={selectedToken} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="xs">
|
||||||
|
<Col between="lg">
|
||||||
|
<Paragraph size="md" color="disabled" style={{ letterSpacing: '-0.5px' }} noMargin>
|
||||||
|
Amount
|
||||||
|
</Paragraph>
|
||||||
|
<ButtonLink weight="bold" onClick={mutators.setMax}>
|
||||||
|
Send max
|
||||||
|
</ButtonLink>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row margin="md">
|
||||||
|
<Col>
|
||||||
|
<Field
|
||||||
|
name="amount"
|
||||||
|
component={TextField}
|
||||||
|
type="text"
|
||||||
|
validate={composeValidators(
|
||||||
|
required,
|
||||||
|
mustBeFloat,
|
||||||
|
greaterThan(0),
|
||||||
|
maxValue(token && token.balance),
|
||||||
|
)}
|
||||||
|
placeholder="Amount*"
|
||||||
|
text="Amount*"
|
||||||
|
className={classes.addressInput}
|
||||||
|
inputAdornment={
|
||||||
|
token && {
|
||||||
|
endAdornment: <InputAdornment position="end">{token.symbol}</InputAdornment>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<OnChange name="token">
|
||||||
|
{() => {
|
||||||
|
mutators.onTokenChange()
|
||||||
|
}}
|
||||||
|
</OnChange>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Hairline />
|
||||||
|
<Row align="center" className={classes.buttonRow}>
|
||||||
|
<Button className={classes.button} minWidth={140} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" className={classes.button} variant="contained" minWidth={140} color="primary">
|
||||||
|
Review
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</GnoForm>
|
||||||
|
</Block>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(SendFunds)
|
|
@ -0,0 +1,31 @@
|
||||||
|
// @flow
|
||||||
|
import { lg, md, sm } from '~/theme/variables'
|
||||||
|
|
||||||
|
export const styles = () => ({
|
||||||
|
heading: {
|
||||||
|
padding: `${sm} ${lg}`,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
maxHeight: '75px',
|
||||||
|
},
|
||||||
|
annotation: {
|
||||||
|
letterSpacing: '-1px',
|
||||||
|
color: '#a2a8ba',
|
||||||
|
marginRight: 'auto',
|
||||||
|
marginLeft: '20px',
|
||||||
|
},
|
||||||
|
manage: {
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
closeIcon: {
|
||||||
|
height: '35px',
|
||||||
|
width: '35px',
|
||||||
|
},
|
||||||
|
formContainer: {
|
||||||
|
padding: `${md} ${lg}`,
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
height: '84px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="21" viewBox="0 0 13 21">
|
||||||
|
<path fill="#A2A8BA" fill-rule="evenodd" d="M8.7 11.266V0H4.27v11.266H0l6.484 9.172 6.493-9.172z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 195 B |
|
@ -21,9 +21,10 @@ type Props = Actions & {
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
activeTokens: List<Token>,
|
activeTokens: List<Token>,
|
||||||
}
|
}
|
||||||
|
type ActiveScreen = 'tokenList' | 'addCustomToken'
|
||||||
|
|
||||||
const Tokens = (props: Props) => {
|
const Tokens = (props: Props) => {
|
||||||
const [activeScreen, setActiveScreen] = useState<string>('tokenList')
|
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('tokenList')
|
||||||
const {
|
const {
|
||||||
onClose,
|
onClose,
|
||||||
classes,
|
classes,
|
||||||
|
|
|
@ -158,7 +158,7 @@ class Tokens extends React.Component<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem key={token.address} className={classes.token}>
|
<ListItem key={token.address} className={classes.token}>
|
||||||
<ListItemIcon>
|
<ListItemIcon className={classes.tokenIcon}>
|
||||||
<Img src={token.logoUri} height={28} alt={token.name} onError={setImageToPlaceholder} />
|
<Img src={token.logoUri} height={28} alt={token.name} onError={setImageToPlaceholder} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={token.symbol} secondary={token.name} />
|
<ListItemText primary={token.symbol} secondary={token.name} />
|
||||||
|
|
|
@ -47,6 +47,9 @@ export const styles = () => ({
|
||||||
letterSpacing: '-0.5px',
|
letterSpacing: '-0.5px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
tokenIcon: {
|
||||||
|
marginRight: md,
|
||||||
|
},
|
||||||
progressContainer: {
|
progressContainer: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from './dataFetcher'
|
} from './dataFetcher'
|
||||||
import AssetTableCell from './AssetTableCell'
|
import AssetTableCell from './AssetTableCell'
|
||||||
import Tokens from './Tokens'
|
import Tokens from './Tokens'
|
||||||
import Send from './Send'
|
import SendModal from './SendModal'
|
||||||
import Receive from './Receive'
|
import Receive from './Receive'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ type State = {
|
||||||
hideZero: boolean,
|
hideZero: boolean,
|
||||||
showToken: boolean,
|
showToken: boolean,
|
||||||
showReceive: boolean,
|
showReceive: boolean,
|
||||||
showSend: boolean,
|
sendFunds: Object,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -40,6 +40,8 @@ type Props = {
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
safeName: string,
|
safeName: string,
|
||||||
etherScanLink: string,
|
etherScanLink: string,
|
||||||
|
ethBalance: string,
|
||||||
|
createTransaction: Function,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = 'Token' | 'Send' | 'Receive'
|
type Action = 'Token' | 'Send' | 'Receive'
|
||||||
|
@ -48,7 +50,10 @@ class Balances extends React.Component<Props, State> {
|
||||||
state = {
|
state = {
|
||||||
hideZero: false,
|
hideZero: false,
|
||||||
showToken: false,
|
showToken: false,
|
||||||
showSend: false,
|
sendFunds: {
|
||||||
|
isOpen: false,
|
||||||
|
selectedToken: undefined,
|
||||||
|
},
|
||||||
showReceive: false,
|
showReceive: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +65,24 @@ class Balances extends React.Component<Props, State> {
|
||||||
this.setState(() => ({ [`show${action}`]: false }))
|
this.setState(() => ({ [`show${action}`]: false }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showSendFunds = (token: Token) => {
|
||||||
|
this.setState({
|
||||||
|
sendFunds: {
|
||||||
|
isOpen: true,
|
||||||
|
selectedToken: token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSendFunds = () => {
|
||||||
|
this.setState({
|
||||||
|
sendFunds: {
|
||||||
|
isOpen: false,
|
||||||
|
selectedToken: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
handleChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
|
handleChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
const { checked } = e.target
|
const { checked } = e.target
|
||||||
|
|
||||||
|
@ -68,10 +91,18 @@ class Balances extends React.Component<Props, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
hideZero, showToken, showReceive, showSend,
|
hideZero, showToken, showReceive, sendFunds,
|
||||||
} = this.state
|
} = this.state
|
||||||
const {
|
const {
|
||||||
classes, granted, tokens, safeAddress, activeTokens, safeName, etherScanLink,
|
classes,
|
||||||
|
granted,
|
||||||
|
tokens,
|
||||||
|
safeAddress,
|
||||||
|
activeTokens,
|
||||||
|
safeName,
|
||||||
|
etherScanLink,
|
||||||
|
ethBalance,
|
||||||
|
createTransaction,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const columns = generateColumns()
|
const columns = generateColumns()
|
||||||
|
@ -137,7 +168,7 @@ class Balances extends React.Component<Props, State> {
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className={classes.send}
|
className={classes.send}
|
||||||
onClick={this.onShow('Send')}
|
onClick={() => this.showSendFunds(row.asset.name)}
|
||||||
>
|
>
|
||||||
<CallMade className={classNames(classes.leftIcon, classes.iconSmall)} />
|
<CallMade className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||||
Send
|
Send
|
||||||
|
@ -159,9 +190,17 @@ class Balances extends React.Component<Props, State> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</Table>
|
</Table>
|
||||||
<Modal title="Send Tokens" description="Send Tokens Form" handleClose={this.onHide('Send')} open={showSend}>
|
<SendModal
|
||||||
<Send onClose={this.onHide('Send')} />
|
onClose={this.hideSendFunds}
|
||||||
</Modal>
|
isOpen={sendFunds.isOpen}
|
||||||
|
etherScanLink={etherScanLink}
|
||||||
|
safeAddress={safeAddress}
|
||||||
|
safeName={safeName}
|
||||||
|
ethBalance={ethBalance}
|
||||||
|
tokens={activeTokens}
|
||||||
|
selectedToken={sendFunds.selectedToken}
|
||||||
|
createTransaction={createTransaction}
|
||||||
|
/>
|
||||||
<Modal
|
<Modal
|
||||||
title="Receive Tokens"
|
title="Receive Tokens"
|
||||||
description="Receive Tokens Form"
|
description="Receive Tokens Form"
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const styles = (theme: Object) => ({
|
||||||
margin: `${sm} 0`,
|
margin: `${sm} 0`,
|
||||||
},
|
},
|
||||||
actionIcon: {
|
actionIcon: {
|
||||||
marginRight: theme.spacing.unit,
|
marginRight: theme.spacing(1),
|
||||||
},
|
},
|
||||||
iconSmall: {
|
iconSmall: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|
|
@ -23,6 +23,7 @@ import Balances from './Balances'
|
||||||
type Props = SelectorProps & {
|
type Props = SelectorProps & {
|
||||||
classes: Object,
|
classes: Object,
|
||||||
granted: boolean,
|
granted: boolean,
|
||||||
|
createTransaction: Function,
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
@ -88,7 +89,7 @@ class Layout extends React.Component<Props, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
safe, provider, network, classes, granted, tokens, activeTokens,
|
safe, provider, network, classes, granted, tokens, activeTokens, createTransaction,
|
||||||
} = this.props
|
} = this.props
|
||||||
const { tabIndex } = this.state
|
const { tabIndex } = this.state
|
||||||
|
|
||||||
|
@ -137,6 +138,7 @@ class Layout extends React.Component<Props, State> {
|
||||||
safeAddress={address}
|
safeAddress={address}
|
||||||
safeName={name}
|
safeName={name}
|
||||||
etherScanLink={etherScanLink}
|
etherScanLink={etherScanLink}
|
||||||
|
createTransaction={createTransaction}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -14,7 +14,7 @@ type FormProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
symbol: string
|
symbol: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const spinnerStyle = {
|
const spinnerStyle = {
|
||||||
|
@ -25,14 +25,14 @@ const ReviewTx = ({ symbol }: Props) => (controls: React$Node, { values, submitt
|
||||||
<OpenPaper controls={controls}>
|
<OpenPaper controls={controls}>
|
||||||
<Heading tag="h2">Review the move token funds</Heading>
|
<Heading tag="h2">Review the move token funds</Heading>
|
||||||
<Paragraph align="left">
|
<Paragraph align="left">
|
||||||
<Bold>Destination: </Bold> {values[TKN_DESTINATION_PARAM]}
|
<Bold>Destination: </Bold>
|
||||||
|
{' '}
|
||||||
|
{values[TKN_DESTINATION_PARAM]}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph align="left">
|
<Paragraph align="left">
|
||||||
<Bold>{`Amount to transfer: ${values[TKN_VALUE_PARAM]} ${symbol}`}</Bold>
|
<Bold>{`Amount to transfer: ${values[TKN_VALUE_PARAM]} ${symbol}`}</Bold>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Block style={spinnerStyle}>
|
<Block style={spinnerStyle}>{submitting && <CircularProgress size={50} />}</Block>
|
||||||
{ submitting && <CircularProgress size={50} /> }
|
|
||||||
</Block>
|
|
||||||
</OpenPaper>
|
</OpenPaper>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
// @flow
|
// @flow
|
||||||
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
|
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
|
||||||
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||||
|
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||||
|
|
||||||
export type Actions = {
|
export type Actions = {
|
||||||
fetchSafe: typeof fetchSafe,
|
fetchSafe: typeof fetchSafe,
|
||||||
fetchTokenBalances: typeof fetchTokenBalances,
|
fetchTokenBalances: typeof fetchTokenBalances,
|
||||||
|
createTransaction: typeof createTransaction,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fetchSafe,
|
fetchSafe,
|
||||||
fetchTokenBalances,
|
fetchTokenBalances,
|
||||||
|
createTransaction,
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ class SafeView extends React.Component<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
safe, provider, activeTokens, granted, userAddress, network, tokens,
|
safe, provider, activeTokens, granted, userAddress, network, tokens, createTransaction,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -65,6 +65,7 @@ class SafeView extends React.Component<Props> {
|
||||||
userAddress={userAddress}
|
userAddress={userAddress}
|
||||||
network={network}
|
network={network}
|
||||||
granted={granted}
|
granted={granted}
|
||||||
|
createTransaction={createTransaction}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,7 +31,7 @@ const saveSafe = (
|
||||||
threshold: number,
|
threshold: number,
|
||||||
ownersName: string[],
|
ownersName: string[],
|
||||||
ownersAddress: string[],
|
ownersAddress: string[],
|
||||||
) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
) => (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
const owners: List<Owner> = buildOwnersFrom(ownersName, ownersAddress)
|
const owners: List<Owner> = buildOwnersFrom(ownersName, ownersAddress)
|
||||||
|
|
||||||
const safe: Safe = SafeRecord({
|
const safe: Safe = SafeRecord({
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
// @flow
|
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
|
|
||||||
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
|
|
||||||
|
|
||||||
export default createAction<string, *>(ADD_TRANSACTIONS)
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
// @flow
|
||||||
|
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
||||||
|
import { createAction } from 'redux-actions'
|
||||||
|
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
|
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||||
|
import { type GlobalState } from '~/store'
|
||||||
|
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
|
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
|
import { executeTransaction, CALL } from '~/logic/safe/transactions'
|
||||||
|
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
|
|
||||||
|
export const ADD_TRANSACTIONS = 'ADD_TRANSACTIONS'
|
||||||
|
export const addTransactions = createAction<string, *>(ADD_TRANSACTIONS)
|
||||||
|
|
||||||
|
const createTransaction = (
|
||||||
|
safeAddress: string,
|
||||||
|
to: string,
|
||||||
|
valueInEth: string,
|
||||||
|
token: Token,
|
||||||
|
openSnackbar: Function,
|
||||||
|
) => async (dispatch: ReduxDispatch<GlobalState>, getState: GetState<GlobalState>) => {
|
||||||
|
const isSendingETH = isEther(token.symbol)
|
||||||
|
const state: GlobalState = getState()
|
||||||
|
|
||||||
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
|
const web3 = getWeb3()
|
||||||
|
const from = userAccountSelector(state)
|
||||||
|
const threshold = await safeInstance.getThreshold()
|
||||||
|
const nonce = await safeInstance.nonce()
|
||||||
|
const txRecipient = isSendingETH ? to : token.address
|
||||||
|
const valueInWei = web3.utils.toWei(valueInEth, 'ether')
|
||||||
|
let txAmount = valueInWei
|
||||||
|
const isExecution = threshold.toNumber() === 1
|
||||||
|
|
||||||
|
let txData = EMPTY_DATA
|
||||||
|
if (!isSendingETH) {
|
||||||
|
const StandardToken = await getStandardTokenContract()
|
||||||
|
const sendToken = await StandardToken.at(token.address)
|
||||||
|
|
||||||
|
txData = sendToken.contract.methods.transfer(to, valueInWei).encodeABI()
|
||||||
|
// txAmount should be 0 if we send tokens
|
||||||
|
// the real value is encoded in txData and will be used by the contract
|
||||||
|
// if txAmount > 0 it would send ETH from the safe
|
||||||
|
txAmount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let txHash
|
||||||
|
if (isExecution) {
|
||||||
|
openSnackbar('Transaction has been submitted', 'success')
|
||||||
|
txHash = await executeTransaction(safeInstance, txRecipient, txAmount, txData, CALL, nonce, from)
|
||||||
|
openSnackbar('Transaction has been confirmed', 'success')
|
||||||
|
} else {
|
||||||
|
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
|
||||||
|
}
|
||||||
|
// dispatch(addTransactions(txHash))
|
||||||
|
|
||||||
|
return txHash
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createTransaction
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
import { handleActions, type ActionType } from 'redux-actions'
|
import { handleActions, type ActionType } from 'redux-actions'
|
||||||
import addTransactions, { ADD_TRANSACTIONS } from '~/routes/safe/store/actions/addTransactions'
|
import { ADD_TRANSACTIONS } from '~/routes/safe/store/actions/createTransaction'
|
||||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||||
|
|
||||||
export const TRANSACTIONS_REDUCER_ID = 'transactions'
|
export const TRANSACTIONS_REDUCER_ID = 'transactions'
|
||||||
|
@ -10,7 +10,7 @@ export type State = Map<string, List<Transaction>>
|
||||||
|
|
||||||
export default handleActions<State, *>(
|
export default handleActions<State, *>(
|
||||||
{
|
{
|
||||||
[ADD_TRANSACTIONS]: (state: State, action: ActionType<typeof addTransactions>): State => action.payload,
|
[ADD_TRANSACTIONS]: (state: State, action: ActionType<Function>): State => action.payload,
|
||||||
},
|
},
|
||||||
Map(),
|
Map(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
bolderFont,
|
bolderFont,
|
||||||
boldFont,
|
boldFont,
|
||||||
buttonLargeFontSize,
|
buttonLargeFontSize,
|
||||||
|
xs,
|
||||||
} from './variables'
|
} from './variables'
|
||||||
|
|
||||||
export type WithStyles = {
|
export type WithStyles = {
|
||||||
|
@ -100,6 +101,9 @@ export default createMuiTheme({
|
||||||
letterSpacing: '-0.5px',
|
letterSpacing: '-0.5px',
|
||||||
fontSize: mediumFontSize,
|
fontSize: mediumFontSize,
|
||||||
},
|
},
|
||||||
|
body2: {
|
||||||
|
fontFamily: 'Roboto Mono, monospace',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
MuiFormHelperText: {
|
MuiFormHelperText: {
|
||||||
root: {
|
root: {
|
||||||
|
@ -111,7 +115,7 @@ export default createMuiTheme({
|
||||||
color: secondary,
|
color: secondary,
|
||||||
order: 0,
|
order: 0,
|
||||||
marginTop: '0px',
|
marginTop: '0px',
|
||||||
backgroundColor: 'EAE9EF',
|
backgroundColor: '#EAE9EF',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MuiInput: {
|
MuiInput: {
|
||||||
|
@ -123,6 +127,7 @@ export default createMuiTheme({
|
||||||
order: 1,
|
order: 1,
|
||||||
padding: `0 ${md}`,
|
padding: `0 ${md}`,
|
||||||
backgroundColor: '#EAE9EF',
|
backgroundColor: '#EAE9EF',
|
||||||
|
borderRadius: '5px',
|
||||||
'&:$disabled': {
|
'&:$disabled': {
|
||||||
color: '#0000ff',
|
color: '#0000ff',
|
||||||
},
|
},
|
||||||
|
@ -131,6 +136,7 @@ export default createMuiTheme({
|
||||||
padding: 0,
|
padding: 0,
|
||||||
letterSpacing: '0.5px',
|
letterSpacing: '0.5px',
|
||||||
color: primary,
|
color: primary,
|
||||||
|
height: 'auto',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
'&::-webkit-input-placeholder': {
|
'&::-webkit-input-placeholder': {
|
||||||
|
@ -213,6 +219,8 @@ export default createMuiTheme({
|
||||||
color: primary,
|
color: primary,
|
||||||
letterSpacing: '-0.5px',
|
letterSpacing: '-0.5px',
|
||||||
fontWeight: 'normal',
|
fontWeight: 'normal',
|
||||||
|
paddingTop: xs,
|
||||||
|
paddingBottom: xs,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MuiBackdrop: {
|
MuiBackdrop: {
|
||||||
|
@ -221,6 +229,16 @@ export default createMuiTheme({
|
||||||
backgroundColor: 'rgba(228, 232, 241, 0.75)',
|
backgroundColor: 'rgba(228, 232, 241, 0.75)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiMenuItem: {
|
||||||
|
root: {
|
||||||
|
fontFamily: 'Roboto Mono, monospace',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiListItemIcon: {
|
||||||
|
root: {
|
||||||
|
minWidth: 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
MuiListItemText: {
|
MuiListItemText: {
|
||||||
primary: {
|
primary: {
|
||||||
fontFamily: 'Roboto Mono, monospace',
|
fontFamily: 'Roboto Mono, monospace',
|
||||||
|
|
Loading…
Reference in New Issue