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
|
||||
},
|
||||
"dependencies": {
|
||||
"@gnosis.pm/util-contracts": "2.0.1",
|
||||
"@gnosis.pm/safe-contracts": "^1.0.0",
|
||||
"@gnosis.pm/util-contracts": "^2.0.0",
|
||||
"@material-ui/core": "^3.9.3",
|
||||
"@material-ui/icons": "^3.0.1",
|
||||
"@material-ui/core": "4.0.0",
|
||||
"@material-ui/icons": "4.0.0",
|
||||
"@welldone-software/why-did-you-render": "^3.0.9",
|
||||
"axios": "^0.18.0",
|
||||
"bignumber.js": "^8.1.1",
|
||||
"connected-react-router": "^6.3.1",
|
||||
"final-form": "^4.2.1",
|
||||
"final-form": "4.13.0",
|
||||
"history": "^4.7.2",
|
||||
"immortal-db": "^1.0.2",
|
||||
"immutable": "^4.0.0-rc.9",
|
||||
|
@ -81,10 +82,11 @@
|
|||
"qrcode.react": "^0.9.3",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-final-form": "^4.1.0",
|
||||
"react-hot-loader": "4.8.4",
|
||||
"react-final-form": "6.0.0",
|
||||
"react-final-form-listeners": "^1.0.2",
|
||||
"react-hot-loader": "4.8.8",
|
||||
"react-infinite-scroll-component": "^4.5.2",
|
||||
"react-redux": "7.0.2",
|
||||
"react-redux": "7.0.3",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"recompose": "^0.30.0",
|
||||
"redux": "^4.0.1",
|
||||
|
@ -94,10 +96,10 @@
|
|||
"web3": "1.0.0-beta.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0-beta.40",
|
||||
"@babel/core": "^7.4.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.4.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.0",
|
||||
"@babel/cli": "7.4.4",
|
||||
"@babel/core": "7.4.5",
|
||||
"@babel/plugin-proposal-class-properties": "7.4.4",
|
||||
"@babel/plugin-proposal-decorators": "7.4.4",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.0.0",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.0.0",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
|
||||
|
@ -105,7 +107,7 @@
|
|||
"@babel/plugin-proposal-function-sent": "^7.0.0",
|
||||
"@babel/plugin-proposal-json-strings": "^7.0.0",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.0.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.0.0",
|
||||
|
@ -114,20 +116,20 @@
|
|||
"@babel/plugin-syntax-import-meta": "^7.0.0",
|
||||
"@babel/plugin-transform-member-expression-literals": "^7.2.0",
|
||||
"@babel/plugin-transform-property-literals": "^7.2.0",
|
||||
"@babel/polyfill": "^7.4.0",
|
||||
"@babel/preset-env": "^7.4.2",
|
||||
"@babel/polyfill": "7.4.4",
|
||||
"@babel/preset-env": "7.4.5",
|
||||
"@babel/preset-flow": "^7.0.0-beta.40",
|
||||
"@babel/preset-react": "^7.0.0-beta.40",
|
||||
"@sambego/storybook-state": "^1.0.7",
|
||||
"@storybook/addon-actions": "5.0.10",
|
||||
"@storybook/addon-knobs": "5.0.10",
|
||||
"@storybook/addon-links": "5.0.10",
|
||||
"@storybook/react": "5.0.10",
|
||||
"@storybook/addon-actions": "5.0.11",
|
||||
"@storybook/addon-knobs": "5.0.11",
|
||||
"@storybook/addon-links": "5.0.11",
|
||||
"@storybook/react": "5.0.11",
|
||||
"autoprefixer": "9.5.1",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^24.1.0",
|
||||
"babel-loader": "^8.0.0-beta.0",
|
||||
"babel-jest": "24.8.0",
|
||||
"babel-loader": "8.0.6",
|
||||
"babel-plugin-dynamic-import-node": "^2.2.0",
|
||||
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
|
||||
"babel-plugin-transform-es3-property-literals": "^6.22.0",
|
||||
|
@ -136,19 +138,19 @@
|
|||
"detect-port": "^1.2.2",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-airbnb": "^17.1.0",
|
||||
"eslint-plugin-flowtype": "3.6.1",
|
||||
"eslint-plugin-import": "2.17.2",
|
||||
"eslint-plugin-jest": "22.5.1",
|
||||
"eslint-plugin-flowtype": "3.9.1",
|
||||
"eslint-plugin-import": "2.17.3",
|
||||
"eslint-plugin-jest": "22.6.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"eslint-plugin-react": "7.13.0",
|
||||
"ethereumjs-abi": "^0.6.7",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"file-loader": "^3.0.1",
|
||||
"flow-bin": "0.97.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"flow-bin": "0.98.1",
|
||||
"fs-extra": "8.0.1",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.0.4",
|
||||
"jest": "^24.1.0",
|
||||
"jest": "24.8.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"mini-css-extract-plugin": "0.6.0",
|
||||
"postcss-loader": "^3.0.0",
|
||||
|
@ -160,14 +162,14 @@
|
|||
"storybook-host": "^5.0.3",
|
||||
"storybook-router": "^0.3.3",
|
||||
"style-loader": "^0.23.1",
|
||||
"truffle": "5.0.14",
|
||||
"truffle-contract": "4.0.13",
|
||||
"truffle-solidity-loader": "0.1.14",
|
||||
"uglifyjs-webpack-plugin": "^2.1.2",
|
||||
"webpack": "^4.1.1",
|
||||
"truffle": "5.0.19",
|
||||
"truffle-contract": "4.0.17",
|
||||
"truffle-solidity-loader": "0.1.18",
|
||||
"uglifyjs-webpack-plugin": "2.1.3",
|
||||
"webpack": "4.32.2",
|
||||
"webpack-bundle-analyzer": "3.3.2",
|
||||
"webpack-cli": "3.3.1",
|
||||
"webpack-dev-server": "3.3.1",
|
||||
"webpack-cli": "3.3.2",
|
||||
"webpack-dev-server": "3.4.1",
|
||||
"webpack-manifest-plugin": "^2.0.0-rc.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,15 @@ const buildDotStyleFrom = (size: number, top: number, right: number, mode: Mode)
|
|||
})
|
||||
|
||||
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) => {
|
||||
const keyStyle = buildKeyStyleFrom(circleSize, center, dotSize)
|
||||
const dotStyle = buildDotStyleFrom(dotSize, dotTop, dotRight, mode)
|
||||
|
@ -80,7 +88,7 @@ const KeyRing = ({
|
|||
className={isWarning ? classes.warning : undefined}
|
||||
/>
|
||||
</Block>
|
||||
{ !hideDot && <Dot className={classes.dot} style={dotStyle} /> }
|
||||
{!hideDot && <Dot className={classes.dot} style={dotStyle} />}
|
||||
</Block>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
|
|
@ -55,16 +55,13 @@ class Provider extends React.Component<Props> {
|
|||
<React.Fragment>
|
||||
<div ref={this.myRef} className={classes.root}>
|
||||
<Col end="sm" middle="xs" className={classes.provider} onClick={toggle}>
|
||||
{ info }
|
||||
<IconButton
|
||||
disableRipple
|
||||
className={classes.expand}
|
||||
>
|
||||
{ open ? <ExpandLess /> : <ExpandMore />}
|
||||
{info}
|
||||
<IconButton disableRipple className={classes.expand}>
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
</Col>
|
||||
</div>
|
||||
{ children(this.myRef) }
|
||||
{children(this.myRef)}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -54,18 +54,20 @@ const ProviderInfo = ({
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ connected &&
|
||||
{connected && (
|
||||
<React.Fragment>
|
||||
<Identicon address={identiconAddress} diameter={30} />
|
||||
<Dot className={classes.logo} />
|
||||
</React.Fragment>
|
||||
}
|
||||
{ !connected &&
|
||||
<CircleDot keySize={14} circleSize={35} dotSize={16} dotTop={24} dotRight={11} mode="warning" />
|
||||
}
|
||||
)}
|
||||
{!connected && <CircleDot keySize={14} circleSize={35} dotSize={16} dotTop={24} dotRight={11} mode="warning" />}
|
||||
<Col start="sm" layout="column" className={classes.account}>
|
||||
<Paragraph size="sm" transform="capitalize" className={classes.network} noMargin weight="bolder">{providerText}</Paragraph>
|
||||
<Paragraph size="sm" className={classes.address} noMargin color={color}>{cutAddress}</Paragraph>
|
||||
<Paragraph size="sm" transform="capitalize" className={classes.network} noMargin weight="bolder">
|
||||
{providerText}
|
||||
</Paragraph>
|
||||
<Paragraph size="sm" className={classes.address} noMargin color={color}>
|
||||
{cutAddress}
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
|
|
@ -33,8 +33,12 @@ const ProviderDesconnected = ({ classes }: Props) => (
|
|||
<React.Fragment>
|
||||
<CircleDot keySize={17} circleSize={35} dotSize={16} dotTop={24} dotRight={11} mode="error" />
|
||||
<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" color="fancy" className={classes.connect} noMargin>Connect Wallet</Paragraph>
|
||||
<Paragraph size="sm" transform="capitalize" className={classes.network} noMargin weight="bold">
|
||||
Not Connected
|
||||
</Paragraph>
|
||||
<Paragraph size="sm" color="fancy" className={classes.connect} noMargin>
|
||||
Connect Wallet
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
|
|
@ -12,9 +12,10 @@ import Layout from './component/Layout'
|
|||
import actions, { type Actions } from './actions'
|
||||
import selector, { type SelectorProps } from './selector'
|
||||
|
||||
type Props = Actions & SelectorProps & {
|
||||
openSnackbar: (message: string, variant: Variant) => void,
|
||||
}
|
||||
type Props = Actions &
|
||||
SelectorProps & {
|
||||
openSnackbar: (message: string, variant: Variant) => void,
|
||||
}
|
||||
|
||||
type State = {
|
||||
hasError: boolean,
|
||||
|
@ -26,22 +27,19 @@ class HeaderComponent extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchProvider(this.props.openSnackbar)
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: Info) {
|
||||
this.setState({ hasError: true })
|
||||
this.props.openSnackbar(WALLET_ERROR_MSG, 'error')
|
||||
|
||||
logComponentStack(error, info)
|
||||
this.onConnect()
|
||||
}
|
||||
|
||||
onDisconnect = () => {
|
||||
this.props.removeProvider(this.props.openSnackbar)
|
||||
const { removeProvider, openSnackbar } = this.props
|
||||
|
||||
removeProvider(openSnackbar)
|
||||
}
|
||||
|
||||
onConnect = () => {
|
||||
this.props.fetchProvider(this.props.openSnackbar)
|
||||
const { fetchProvider, openSnackbar } = this.props
|
||||
|
||||
fetchProvider(openSnackbar)
|
||||
}
|
||||
|
||||
getProviderInfoBased = () => {
|
||||
|
@ -67,13 +65,23 @@ class HeaderComponent extends React.PureComponent<Props, State> {
|
|||
return <ConnectDetails onConnect={this.onConnect} />
|
||||
}
|
||||
|
||||
return (<UserDetails
|
||||
provider={provider}
|
||||
network={network}
|
||||
userAddress={userAddress}
|
||||
connected={available}
|
||||
onDisconnect={this.onDisconnect}
|
||||
/>)
|
||||
return (
|
||||
<UserDetails
|
||||
provider={provider}
|
||||
network={network}
|
||||
userAddress={userAddress}
|
||||
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() {
|
||||
|
@ -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 = () => (
|
||||
<SharedSnackbarConsumer>
|
||||
{({ openSnackbar }) => (
|
||||
<Header openSnackbar={openSnackbar} />
|
||||
)}
|
||||
</SharedSnackbarConsumer>
|
||||
<SharedSnackbarConsumer>{({ openSnackbar }) => <Header openSnackbar={openSnackbar} />}</SharedSnackbarConsumer>
|
||||
)
|
||||
|
||||
export default HeaderSnack
|
||||
|
|
|
@ -16,7 +16,7 @@ export type SelectorProps = {
|
|||
available: boolean,
|
||||
}
|
||||
|
||||
export default createStructuredSelector({
|
||||
export default createStructuredSelector<Object, *>({
|
||||
provider: providerNameSelector,
|
||||
userAddress: userAccountSelector,
|
||||
network: networkSelector,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import cn from 'classnames'
|
||||
import Modal from '@material-ui/core/Modal'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
|
||||
|
@ -10,6 +11,8 @@ type Props = {
|
|||
handleClose: Function,
|
||||
children: React$Node,
|
||||
classes: Object,
|
||||
modalClassName: ?string,
|
||||
paperClassName: ?string,
|
||||
}
|
||||
|
||||
const styles = () => ({
|
||||
|
@ -35,18 +38,16 @@ const styles = () => ({
|
|||
})
|
||||
|
||||
const GnoModal = ({
|
||||
title, description, open, children, handleClose, classes,
|
||||
title, description, open, children, handleClose, modalClassName, classes, paperClassName,
|
||||
}: Props) => (
|
||||
<Modal
|
||||
aria-labelledby={title}
|
||||
aria-describedby={description}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
className={classes.root}
|
||||
className={cn(classes.root, modalClassName)}
|
||||
>
|
||||
<div className={classes.paper}>
|
||||
{ children }
|
||||
</div>
|
||||
<div className={cn(classes.paper, paperClassName)}>{children}</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ const styles = theme => ({
|
|||
},
|
||||
iconVariant: {
|
||||
opacity: 0.9,
|
||||
marginRight: theme.spacing.unit,
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
message: {
|
||||
display: 'flex',
|
||||
|
|
|
@ -15,6 +15,7 @@ type Props = {
|
|||
padding?: number,
|
||||
validation?: (values: Object) => Object | Promise<Object>,
|
||||
initialValues?: Object,
|
||||
formMutators?: Object,
|
||||
}
|
||||
|
||||
const stylesBasedOn = (padding: number): $Shape<CSSStyleDeclaration> => ({
|
||||
|
@ -24,15 +25,16 @@ const stylesBasedOn = (padding: number): $Shape<CSSStyleDeclaration> => ({
|
|||
})
|
||||
|
||||
const GnoForm = ({
|
||||
onSubmit, validation, initialValues, children, padding = 0,
|
||||
onSubmit, validation, initialValues, children, padding = 0, formMutators,
|
||||
}: Props) => (
|
||||
<Form
|
||||
validate={validation}
|
||||
onSubmit={onSubmit}
|
||||
initialValues={initialValues}
|
||||
mutators={formMutators}
|
||||
render={({ handleSubmit, ...rest }) => (
|
||||
<form onSubmit={handleSubmit} style={stylesBasedOn(padding)}>
|
||||
{children(rest.submitting, rest.validating, rest)}
|
||||
{children(rest.submitting, rest.validating, rest, rest.form.mutators)}
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -16,32 +16,30 @@ const SelectInput = ({
|
|||
meta,
|
||||
label,
|
||||
formControlProps,
|
||||
classes,
|
||||
renderValue,
|
||||
...rest
|
||||
}: SelectFieldProps) => {
|
||||
const showError = ((meta.submitError && !meta.dirtySinceLastSubmit) || meta.error) && meta.touched
|
||||
const inputProps = { ...restInput, name }
|
||||
const inputProps = {
|
||||
...restInput,
|
||||
name,
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
{...formControlProps}
|
||||
error={showError}
|
||||
style={style}
|
||||
>
|
||||
<FormControl {...formControlProps} error={showError} style={style}>
|
||||
<InputLabel htmlFor={name}>{label}</InputLabel>
|
||||
<Select
|
||||
{...rest}
|
||||
classes={classes}
|
||||
onChange={onChange}
|
||||
renderValue={renderValue}
|
||||
inputProps={inputProps}
|
||||
value={value}
|
||||
{...rest}
|
||||
/>
|
||||
{ showError &&
|
||||
<FormHelperText>
|
||||
{meta.error || meta.submitError}
|
||||
</FormHelperText>
|
||||
}
|
||||
{showError && <FormHelperText>{meta.error || meta.submitError}</FormHelperText>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default SelectInput
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import MuiTextField, { TextFieldProps } from '@material-ui/core/TextField'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { lg } from '~/theme/variables'
|
||||
|
@ -17,9 +16,6 @@ const styles = () => ({
|
|||
paddingBottom: '12px',
|
||||
lineHeight: 0,
|
||||
},
|
||||
input: {
|
||||
borderRadius: '5px',
|
||||
},
|
||||
})
|
||||
|
||||
class TextField extends React.PureComponent<TextFieldProps> {
|
||||
|
@ -41,7 +37,7 @@ class TextField extends React.PureComponent<TextFieldProps> {
|
|||
|
||||
const inputRoot = helperText ? classes.root : undefined
|
||||
const inputProps = { ...restInput, autoComplete: 'off' }
|
||||
const inputRootProps = { ...inputAdornment, disableUnderline: !underline, className: cn(inputRoot, classes.input) }
|
||||
const inputRootProps = { ...inputAdornment, disableUnderline: !underline, className: inputRoot }
|
||||
|
||||
return (
|
||||
<MuiTextField
|
||||
|
|
|
@ -11,14 +11,16 @@ const styles = {
|
|||
|
||||
type Props = {
|
||||
minWidth?: number,
|
||||
minHeight?: number,
|
||||
}
|
||||
|
||||
const calculateStyleBased = minWidth => ({
|
||||
minWidth: `${minWidth}px`,
|
||||
const calculateStyleBased = (minWidth, minHeight) => ({
|
||||
minWidth: minWidth && `${minWidth}px`,
|
||||
minHeight: minHeight && `${minHeight}px`,
|
||||
})
|
||||
|
||||
const GnoButton = ({ minWidth, ...props }: Props) => {
|
||||
const style = minWidth ? calculateStyleBased(minWidth) : undefined
|
||||
const GnoButton = ({ minWidth, minHeight, ...props }: Props) => {
|
||||
const style = calculateStyleBased(minWidth, minHeight)
|
||||
|
||||
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;
|
||||
}
|
||||
.top$(size) {
|
||||
align-items: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.middle$(size) {
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
.bottom$(size) {
|
||||
align-items: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.around$(size) {
|
||||
justify-content: space-around;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.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) => ({
|
||||
width: '100%',
|
||||
minHeight: '1px',
|
||||
height: '1px',
|
||||
backgroundColor: color || border,
|
||||
margin: `${getSize(margin)} 0px`,
|
||||
|
@ -13,12 +14,14 @@ const calculateStyleFrom = (color?: string, margin?: Size) => ({
|
|||
type Props = {
|
||||
margin?: Size,
|
||||
color?: string,
|
||||
style?: Object
|
||||
}
|
||||
|
||||
const Hairline = ({ margin, color }: Props) => {
|
||||
const style = calculateStyleFrom(color, margin)
|
||||
const Hairline = ({ margin, color, style }: Props) => {
|
||||
const calculatedStyles = calculateStyleFrom(color, margin)
|
||||
const mergedStyles = { ...calculatedStyles, ...(style || {}) }
|
||||
|
||||
return <div style={style} />
|
||||
return <div style={mergedStyles} />
|
||||
}
|
||||
|
||||
export default Hairline
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import classNames from 'classnames/bind'
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { capitalize } from '~/utils/css'
|
||||
import styles from './index.scss'
|
||||
|
@ -13,20 +13,35 @@ type Props = {
|
|||
children: React$Node,
|
||||
color?: 'regular' | 'white',
|
||||
className?: string,
|
||||
innerRef: React.ElementRef<any>,
|
||||
}
|
||||
|
||||
const GnosisLink = ({
|
||||
to, children, color, className, padding, ...props
|
||||
to, children, color, className, padding, innerRef, ...props
|
||||
}: Props) => {
|
||||
const internal = /^\/(?!\/)/.test(to)
|
||||
const classes = cx(styles.link, color || 'regular', padding ? capitalize(padding, 'padding') : undefined, className)
|
||||
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 (
|
||||
<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}
|
||||
</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 = {
|
||||
className?: string,
|
||||
children: React$Node,
|
||||
margin?: 'sm' | 'md' | 'lg' | 'xl',
|
||||
margin?: 'xs' | 'sm' | 'md' | 'lg' | 'xl',
|
||||
align?: 'center' | 'end' | 'start',
|
||||
grow?: boolean,
|
||||
}
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
.grow {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.marginXs {
|
||||
margin-bottom: $xs;
|
||||
}
|
||||
|
||||
.marginSm {
|
||||
margin-bottom: $sm;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ const devConfig = {
|
|||
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||
[SIGNATURES_VIA_METAMASK]: false,
|
||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
|
||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
||||
}
|
||||
|
||||
export default devConfig
|
||||
|
|
|
@ -10,7 +10,7 @@ const prodConfig = {
|
|||
[TX_SERVICE_HOST]: 'https://safe-transaction-history.dev.gnosisdev.com/api/v1/',
|
||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||
[SIGNATURES_VIA_METAMASK]: false,
|
||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
|
||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
||||
}
|
||||
|
||||
export default prodConfig
|
||||
|
|
|
@ -10,7 +10,7 @@ const testConfig = {
|
|||
[TX_SERVICE_HOST]: 'http://localhost:8000/api/v1/',
|
||||
[ENABLED_TX_SERVICE_REMOVAL_SENDER]: false,
|
||||
[SIGNATURES_VIA_METAMASK]: false,
|
||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1/',
|
||||
[RELAY_API_URL]: 'https://safe-relay.staging.gnosisdev.com/api/v1',
|
||||
}
|
||||
|
||||
export default testConfig
|
||||
|
|
|
@ -8,6 +8,12 @@ import { store } from '~/store'
|
|||
import loadSafesFromStorage from '~/routes/safe/store/actions/loadSafesFromStorage'
|
||||
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(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
|
||||
import { List } from 'immutable'
|
||||
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 { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
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
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
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 (
|
||||
safe: any,
|
||||
|
@ -167,7 +89,7 @@ export const generateMetamaskSignature = async (
|
|||
// To change once Metamask fixes their status
|
||||
// https://github.com/MetaMask/metamask-extension/pull/5368
|
||||
// https://github.com/MetaMask/metamask-extension/issues/5366
|
||||
params: [jsonTypedData, sender],
|
||||
params: [sender, jsonTypedData],
|
||||
from: sender,
|
||||
}
|
||||
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 = ({
|
||||
classes,
|
||||
name = 'Safe creation process',
|
||||
tx,
|
||||
network,
|
||||
classes, name = 'Safe creation process', tx, network,
|
||||
}: Props) => (
|
||||
<Page align="center">
|
||||
<Paragraph className={classes.page} color="secondary" size="xxl" weight="bolder" align="center">
|
||||
|
@ -68,13 +65,25 @@ const Opening = ({
|
|||
</Block>
|
||||
<Block margin="md">
|
||||
<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>
|
||||
{ tx &&
|
||||
{tx && (
|
||||
<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>
|
||||
}
|
||||
)}
|
||||
</Block>
|
||||
</Page>
|
||||
)
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import AssetTableCell from './AssetTableCell'
|
||||
|
||||
export default AssetTableCell
|
|
@ -23,6 +23,7 @@ const styles = () => ({
|
|||
padding: `${sm} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
maxHeight: '75px',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
manage: {
|
||||
fontSize: '24px',
|
||||
|
@ -93,7 +94,14 @@ const Receive = ({
|
|||
</Block>
|
||||
<Block align="center" className={classes.addressContainer}>
|
||||
<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">
|
||||
<OpenInNew style={openIconStyle} />
|
||||
</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,
|
||||
activeTokens: List<Token>,
|
||||
}
|
||||
type ActiveScreen = 'tokenList' | 'addCustomToken'
|
||||
|
||||
const Tokens = (props: Props) => {
|
||||
const [activeScreen, setActiveScreen] = useState<string>('tokenList')
|
||||
const [activeScreen, setActiveScreen] = useState<ActiveScreen>('tokenList')
|
||||
const {
|
||||
onClose,
|
||||
classes,
|
||||
|
|
|
@ -158,7 +158,7 @@ class Tokens extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<ListItem key={token.address} className={classes.token}>
|
||||
<ListItemIcon>
|
||||
<ListItemIcon className={classes.tokenIcon}>
|
||||
<Img src={token.logoUri} height={28} alt={token.name} onError={setImageToPlaceholder} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={token.symbol} secondary={token.name} />
|
||||
|
|
|
@ -47,6 +47,9 @@ export const styles = () => ({
|
|||
letterSpacing: '-0.5px',
|
||||
},
|
||||
},
|
||||
tokenIcon: {
|
||||
marginRight: md,
|
||||
},
|
||||
progressContainer: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from './dataFetcher'
|
||||
import AssetTableCell from './AssetTableCell'
|
||||
import Tokens from './Tokens'
|
||||
import Send from './Send'
|
||||
import SendModal from './SendModal'
|
||||
import Receive from './Receive'
|
||||
import { styles } from './style'
|
||||
|
||||
|
@ -29,7 +29,7 @@ type State = {
|
|||
hideZero: boolean,
|
||||
showToken: boolean,
|
||||
showReceive: boolean,
|
||||
showSend: boolean,
|
||||
sendFunds: Object,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
|
@ -40,6 +40,8 @@ type Props = {
|
|||
safeAddress: string,
|
||||
safeName: string,
|
||||
etherScanLink: string,
|
||||
ethBalance: string,
|
||||
createTransaction: Function,
|
||||
}
|
||||
|
||||
type Action = 'Token' | 'Send' | 'Receive'
|
||||
|
@ -48,7 +50,10 @@ class Balances extends React.Component<Props, State> {
|
|||
state = {
|
||||
hideZero: false,
|
||||
showToken: false,
|
||||
showSend: false,
|
||||
sendFunds: {
|
||||
isOpen: false,
|
||||
selectedToken: undefined,
|
||||
},
|
||||
showReceive: false,
|
||||
}
|
||||
|
||||
|
@ -60,6 +65,24 @@ class Balances extends React.Component<Props, State> {
|
|||
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>) => {
|
||||
const { checked } = e.target
|
||||
|
||||
|
@ -68,10 +91,18 @@ class Balances extends React.Component<Props, State> {
|
|||
|
||||
render() {
|
||||
const {
|
||||
hideZero, showToken, showReceive, showSend,
|
||||
hideZero, showToken, showReceive, sendFunds,
|
||||
} = this.state
|
||||
const {
|
||||
classes, granted, tokens, safeAddress, activeTokens, safeName, etherScanLink,
|
||||
classes,
|
||||
granted,
|
||||
tokens,
|
||||
safeAddress,
|
||||
activeTokens,
|
||||
safeName,
|
||||
etherScanLink,
|
||||
ethBalance,
|
||||
createTransaction,
|
||||
} = this.props
|
||||
|
||||
const columns = generateColumns()
|
||||
|
@ -137,7 +168,7 @@ class Balances extends React.Component<Props, State> {
|
|||
size="small"
|
||||
color="secondary"
|
||||
className={classes.send}
|
||||
onClick={this.onShow('Send')}
|
||||
onClick={() => this.showSendFunds(row.asset.name)}
|
||||
>
|
||||
<CallMade className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||
Send
|
||||
|
@ -159,9 +190,17 @@ class Balances extends React.Component<Props, State> {
|
|||
))
|
||||
}
|
||||
</Table>
|
||||
<Modal title="Send Tokens" description="Send Tokens Form" handleClose={this.onHide('Send')} open={showSend}>
|
||||
<Send onClose={this.onHide('Send')} />
|
||||
</Modal>
|
||||
<SendModal
|
||||
onClose={this.hideSendFunds}
|
||||
isOpen={sendFunds.isOpen}
|
||||
etherScanLink={etherScanLink}
|
||||
safeAddress={safeAddress}
|
||||
safeName={safeName}
|
||||
ethBalance={ethBalance}
|
||||
tokens={activeTokens}
|
||||
selectedToken={sendFunds.selectedToken}
|
||||
createTransaction={createTransaction}
|
||||
/>
|
||||
<Modal
|
||||
title="Receive Tokens"
|
||||
description="Receive Tokens Form"
|
||||
|
|
|
@ -13,7 +13,7 @@ export const styles = (theme: Object) => ({
|
|||
margin: `${sm} 0`,
|
||||
},
|
||||
actionIcon: {
|
||||
marginRight: theme.spacing.unit,
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
iconSmall: {
|
||||
fontSize: 16,
|
||||
|
|
|
@ -23,6 +23,7 @@ import Balances from './Balances'
|
|||
type Props = SelectorProps & {
|
||||
classes: Object,
|
||||
granted: boolean,
|
||||
createTransaction: Function,
|
||||
}
|
||||
|
||||
type State = {
|
||||
|
@ -88,7 +89,7 @@ class Layout extends React.Component<Props, State> {
|
|||
|
||||
render() {
|
||||
const {
|
||||
safe, provider, network, classes, granted, tokens, activeTokens,
|
||||
safe, provider, network, classes, granted, tokens, activeTokens, createTransaction,
|
||||
} = this.props
|
||||
const { tabIndex } = this.state
|
||||
|
||||
|
@ -137,6 +138,7 @@ class Layout extends React.Component<Props, State> {
|
|||
safeAddress={address}
|
||||
safeName={name}
|
||||
etherScanLink={etherScanLink}
|
||||
createTransaction={createTransaction}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
|
|
@ -14,7 +14,7 @@ type FormProps = {
|
|||
}
|
||||
|
||||
type Props = {
|
||||
symbol: string
|
||||
symbol: string,
|
||||
}
|
||||
|
||||
const spinnerStyle = {
|
||||
|
@ -25,14 +25,14 @@ const ReviewTx = ({ symbol }: Props) => (controls: React$Node, { values, submitt
|
|||
<OpenPaper controls={controls}>
|
||||
<Heading tag="h2">Review the move token funds</Heading>
|
||||
<Paragraph align="left">
|
||||
<Bold>Destination: </Bold> {values[TKN_DESTINATION_PARAM]}
|
||||
<Bold>Destination: </Bold>
|
||||
{' '}
|
||||
{values[TKN_DESTINATION_PARAM]}
|
||||
</Paragraph>
|
||||
<Paragraph align="left">
|
||||
<Bold>{`Amount to transfer: ${values[TKN_VALUE_PARAM]} ${symbol}`}</Bold>
|
||||
</Paragraph>
|
||||
<Block style={spinnerStyle}>
|
||||
{ submitting && <CircularProgress size={50} /> }
|
||||
</Block>
|
||||
<Block style={spinnerStyle}>{submitting && <CircularProgress size={50} />}</Block>
|
||||
</OpenPaper>
|
||||
)
|
||||
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// @flow
|
||||
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
|
||||
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||
|
||||
export type Actions = {
|
||||
fetchSafe: typeof fetchSafe,
|
||||
fetchTokenBalances: typeof fetchTokenBalances,
|
||||
createTransaction: typeof createTransaction,
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchSafe,
|
||||
fetchTokenBalances,
|
||||
createTransaction,
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ class SafeView extends React.Component<Props> {
|
|||
|
||||
render() {
|
||||
const {
|
||||
safe, provider, activeTokens, granted, userAddress, network, tokens,
|
||||
safe, provider, activeTokens, granted, userAddress, network, tokens, createTransaction,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
|
@ -65,6 +65,7 @@ class SafeView extends React.Component<Props> {
|
|||
userAddress={userAddress}
|
||||
network={network}
|
||||
granted={granted}
|
||||
createTransaction={createTransaction}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
|
|
|
@ -31,7 +31,7 @@ const saveSafe = (
|
|||
threshold: number,
|
||||
ownersName: string[],
|
||||
ownersAddress: string[],
|
||||
) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
) => (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
const owners: List<Owner> = buildOwnersFrom(ownersName, ownersAddress)
|
||||
|
||||
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
|
||||
import { List, Map } from 'immutable'
|
||||
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'
|
||||
|
||||
export const TRANSACTIONS_REDUCER_ID = 'transactions'
|
||||
|
@ -10,7 +10,7 @@ export type State = Map<string, List<Transaction>>
|
|||
|
||||
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(),
|
||||
)
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
bolderFont,
|
||||
boldFont,
|
||||
buttonLargeFontSize,
|
||||
xs,
|
||||
} from './variables'
|
||||
|
||||
export type WithStyles = {
|
||||
|
@ -100,6 +101,9 @@ export default createMuiTheme({
|
|||
letterSpacing: '-0.5px',
|
||||
fontSize: mediumFontSize,
|
||||
},
|
||||
body2: {
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
},
|
||||
},
|
||||
MuiFormHelperText: {
|
||||
root: {
|
||||
|
@ -111,7 +115,7 @@ export default createMuiTheme({
|
|||
color: secondary,
|
||||
order: 0,
|
||||
marginTop: '0px',
|
||||
backgroundColor: 'EAE9EF',
|
||||
backgroundColor: '#EAE9EF',
|
||||
},
|
||||
},
|
||||
MuiInput: {
|
||||
|
@ -123,6 +127,7 @@ export default createMuiTheme({
|
|||
order: 1,
|
||||
padding: `0 ${md}`,
|
||||
backgroundColor: '#EAE9EF',
|
||||
borderRadius: '5px',
|
||||
'&:$disabled': {
|
||||
color: '#0000ff',
|
||||
},
|
||||
|
@ -131,6 +136,7 @@ export default createMuiTheme({
|
|||
padding: 0,
|
||||
letterSpacing: '0.5px',
|
||||
color: primary,
|
||||
height: 'auto',
|
||||
textOverflow: 'ellipsis',
|
||||
display: 'flex',
|
||||
'&::-webkit-input-placeholder': {
|
||||
|
@ -213,6 +219,8 @@ export default createMuiTheme({
|
|||
color: primary,
|
||||
letterSpacing: '-0.5px',
|
||||
fontWeight: 'normal',
|
||||
paddingTop: xs,
|
||||
paddingBottom: xs,
|
||||
},
|
||||
},
|
||||
MuiBackdrop: {
|
||||
|
@ -221,6 +229,16 @@ export default createMuiTheme({
|
|||
backgroundColor: 'rgba(228, 232, 241, 0.75)',
|
||||
},
|
||||
},
|
||||
MuiMenuItem: {
|
||||
root: {
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
},
|
||||
},
|
||||
MuiListItemIcon: {
|
||||
root: {
|
||||
minWidth: 'auto',
|
||||
},
|
||||
},
|
||||
MuiListItemText: {
|
||||
primary: {
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
|
|
Loading…
Reference in New Issue