Merge branch 'development' of github.com:gnosis/safe-react into 982-fiatbalances-dont-show
This commit is contained in:
commit
6d56286c02
|
@ -0,0 +1,4 @@
|
||||||
|
owner: gnosis
|
||||||
|
repo: safe-react
|
||||||
|
provider: github
|
||||||
|
updaterCacheDirName: safe-react-updater
|
12
package.json
12
package.json
|
@ -167,10 +167,11 @@
|
||||||
"date-fns": "2.14.0",
|
"date-fns": "2.14.0",
|
||||||
"electron-is-dev": "^1.1.0",
|
"electron-is-dev": "^1.1.0",
|
||||||
"electron-log": "4.2.1",
|
"electron-log": "4.2.1",
|
||||||
|
"electron-settings": "^4.0.0",
|
||||||
"electron-updater": "4.3.1",
|
"electron-updater": "4.3.1",
|
||||||
"eth-sig-util": "^2.5.3",
|
"eth-sig-util": "^2.5.3",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"final-form": "4.20.0",
|
"final-form": "^4.20.0",
|
||||||
"final-form-calculate": "^1.3.1",
|
"final-form-calculate": "^1.3.1",
|
||||||
"history": "4.10.1",
|
"history": "4.10.1",
|
||||||
"immortal-db": "^1.0.2",
|
"immortal-db": "^1.0.2",
|
||||||
|
@ -185,7 +186,7 @@
|
||||||
"query-string": "6.12.1",
|
"query-string": "6.12.1",
|
||||||
"react": "16.13.1",
|
"react": "16.13.1",
|
||||||
"react-dom": "16.13.1",
|
"react-dom": "16.13.1",
|
||||||
"react-final-form": "6.5.0",
|
"react-final-form": "^6.5.0",
|
||||||
"react-final-form-listeners": "^1.0.2",
|
"react-final-form-listeners": "^1.0.2",
|
||||||
"react-ga": "^2.7.0",
|
"react-ga": "^2.7.0",
|
||||||
"react-hot-loader": "4.12.21",
|
"react-hot-loader": "4.12.21",
|
||||||
|
@ -234,8 +235,9 @@
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"prettier": "2.0.5",
|
"prettier": "2.0.5",
|
||||||
"react-app-rewired": "^2.1.6",
|
"react-app-rewired": "^2.1.6",
|
||||||
"truffle": "5.1.28",
|
"truffle": "5.1.23",
|
||||||
"typescript": "3.9.3",
|
"typescript": "~3.7.2",
|
||||||
"wait-on": "5.0.0"
|
"wait-on": "5.0.0",
|
||||||
|
"web3-utils": "^1.2.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const fetch = require('node-fetch');
|
const { dialog } = require('electron');
|
||||||
const { dialog, app } = require('electron');
|
|
||||||
const log = require('electron-log');
|
const log = require('electron-log');
|
||||||
const isDev = require("electron-is-dev");
|
const settings = require('electron-settings').default;
|
||||||
|
|
||||||
const { autoUpdater } = require("electron-updater");
|
const { autoUpdater } = require("electron-updater");
|
||||||
|
|
||||||
// This logging setup is not required for auto-updates to work,
|
// This logging setup is not required for auto-updates to work,
|
||||||
|
@ -19,7 +19,7 @@ let downloadProgress = 0;
|
||||||
|
|
||||||
function init(mainWindow) {
|
function init(mainWindow) {
|
||||||
|
|
||||||
if(initialized || isDev) return;
|
if(initialized) return;
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
|
@ -27,24 +27,32 @@ function init(mainWindow) {
|
||||||
log.error(error == null ? "unknown" : (error.stack || error).toString());
|
log.error(error == null ? "unknown" : (error.stack || error).toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('update-available', () => {
|
autoUpdater.on('update-available', (info) => {
|
||||||
|
if(info.version === settings.get('release.version')) {
|
||||||
|
log.info(`Skipped version ${info.version}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
dialog.showMessageBox({
|
dialog.showMessageBox({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Found Updates',
|
title: 'Found Updates',
|
||||||
message: 'There is a newer version of this app available. Do you want to update now?',
|
message: 'There is a newer version of this app available. Do you want to update now?',
|
||||||
buttons: ['Yes', 'Remind me later'],
|
detail: info.releaseNotes.replace(/(<([^>]+)>)/g, ""),
|
||||||
|
buttons: ['Install Update', 'Remind me later','Skip this version'],
|
||||||
cancelId:1,
|
cancelId:1,
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
if(result.response === 0){
|
if(result.response === 0){
|
||||||
autoUpdater.downloadUpdate();
|
autoUpdater.downloadUpdate();
|
||||||
}
|
}
|
||||||
|
if(result.response === 2) {
|
||||||
|
settings.set('release', {version: info.version });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => {
|
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => {
|
||||||
autoUpdater.logger.info("Update Downloaded...");
|
autoUpdater.logger.info("Update Downloaded...");
|
||||||
dialog.showMessageBox({
|
dialog.showMessageBox({
|
||||||
title: 'Install Updates',
|
title: 'Install Updates',
|
||||||
message: process.platform === 'win32' ? releaseNotes : releaseName,
|
message: releaseName,
|
||||||
detail: 'A new version has been downloaded. Restart the application to apply the updates.',
|
detail: 'A new version has been downloaded. Restart the application to apply the updates.',
|
||||||
buttons: ['Restart', 'Cancel'],
|
buttons: ['Restart', 'Cancel'],
|
||||||
cancelId:1,
|
cancelId:1,
|
||||||
|
|
|
@ -57,7 +57,7 @@ function getOpenedWindow(url,options) {
|
||||||
height:700,
|
height:700,
|
||||||
x: width - 1300,
|
x: width - 1300,
|
||||||
parent:mainWindow,
|
parent:mainWindow,
|
||||||
y: height - 200,
|
y: height - (process.platform === 'win32' ? 750 : 200),
|
||||||
webContents: options.webContents, // use existing webContents if provided
|
webContents: options.webContents, // use existing webContents if provided
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
show: false,
|
show: false,
|
||||||
|
@ -140,9 +140,7 @@ process.on('uncaughtException',function(error){
|
||||||
log.error(error);
|
log.error(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.userAgentFallback = process.platform ==='win32' ?
|
app.userAgentFallback = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) old-airport-include/1.0.0 Chrome Electron/7.1.7 Safari/537.36';
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.100 Safari/537.36' :
|
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) old-airport-include/1.0.0 Chrome Electron/7.1.7 Safari/537.36';
|
|
||||||
|
|
||||||
app.commandLine.appendSwitch('ignore-certificate-errors');
|
app.commandLine.appendSwitch('ignore-certificate-errors');
|
||||||
app.on("ready", () =>{
|
app.on("ready", () =>{
|
||||||
|
|
|
@ -33,13 +33,15 @@ class TextField extends React.PureComponent<any> {
|
||||||
} = this.props
|
} = this.props
|
||||||
const helperText = value ? text : undefined
|
const helperText = value ? text : undefined
|
||||||
const showError = (meta.touched || !meta.pristine) && !meta.valid
|
const showError = (meta.touched || !meta.pristine) && !meta.valid
|
||||||
|
const hasError = !!meta.error || (!meta.modifiedSinceLastSubmit && !!meta.submitError)
|
||||||
|
const errorMessage = meta.error || meta.submitError
|
||||||
const isInactiveAndPristineOrUntouched = !meta.active && (meta.pristine || !meta.touched)
|
const isInactiveAndPristineOrUntouched = !meta.active && (meta.pristine || !meta.touched)
|
||||||
const isInvalidAndUntouched = typeof meta.error === 'undefined' ? true : !meta.touched
|
const isInvalidAndUntouched = typeof meta.error === 'undefined' ? true : !meta.touched
|
||||||
|
|
||||||
const disableUnderline = isInactiveAndPristineOrUntouched && isInvalidAndUntouched
|
const disableUnderline = isInactiveAndPristineOrUntouched && isInvalidAndUntouched
|
||||||
|
|
||||||
const inputRoot = helperText ? classes.root : ''
|
const inputRoot = helperText ? classes.root : ''
|
||||||
const statusClasses = meta.valid ? 'isValid' : meta.error && (meta.dirty || meta.touched) ? 'isInvalid' : ''
|
const statusClasses = meta.valid ? 'isValid' : hasError && showError ? 'isInvalid' : ''
|
||||||
const inputProps = {
|
const inputProps = {
|
||||||
...restInput,
|
...restInput,
|
||||||
autoComplete: 'off',
|
autoComplete: 'off',
|
||||||
|
@ -53,8 +55,8 @@ class TextField extends React.PureComponent<any> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MuiTextField
|
<MuiTextField
|
||||||
error={meta.error && (meta.touched || !meta.pristine)}
|
error={hasError && showError}
|
||||||
helperText={showError ? meta.error : helperText || ' '}
|
helperText={hasError && showError ? errorMessage : helperText || ' '}
|
||||||
inputProps={inputProps} // blank in order to force to have helper text
|
inputProps={inputProps} // blank in order to force to have helper text
|
||||||
InputProps={inputRootProps}
|
InputProps={inputRootProps}
|
||||||
multiline={multiline}
|
multiline={multiline}
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
|
||||||
|
|
||||||
class ABIService {
|
|
||||||
static extractUsefulMethods(abi) {
|
|
||||||
return abi
|
|
||||||
.filter(({ constant, name, type }) => type === 'function' && !!name && typeof constant === 'boolean')
|
|
||||||
.map((method) => ({
|
|
||||||
action: method.constant ? 'read' : 'write',
|
|
||||||
...ABIService.getMethodSignatureAndSignatureHash(method),
|
|
||||||
...method,
|
|
||||||
}))
|
|
||||||
.sort(({ name: a }, { name: b }) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1))
|
|
||||||
}
|
|
||||||
|
|
||||||
static getMethodHash(method) {
|
|
||||||
const signature = ABIService.getMethodSignature(method)
|
|
||||||
return ABIService.getSignatureHash(signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
static getMethodSignatureAndSignatureHash(method) {
|
|
||||||
const signature = ABIService.getMethodSignature(method)
|
|
||||||
const signatureHash = ABIService.getSignatureHash(signature)
|
|
||||||
return { signature, signatureHash }
|
|
||||||
}
|
|
||||||
|
|
||||||
static getMethodSignature({ inputs, name }) {
|
|
||||||
const params = inputs.map((x) => x.type).join(',')
|
|
||||||
return `${name}(${params})`
|
|
||||||
}
|
|
||||||
|
|
||||||
static getSignatureHash(signature) {
|
|
||||||
const web3 = getWeb3()
|
|
||||||
return web3.utils.keccak256(signature).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
static isPayable(method) {
|
|
||||||
return method.payable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ABIService
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
|
||||||
|
import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3'
|
||||||
|
|
||||||
|
export interface AbiItemExtended extends AbiItem {
|
||||||
|
action: string
|
||||||
|
methodSignature: string
|
||||||
|
signatureHash: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMethodSignature = ({ inputs, name }: AbiItem) => {
|
||||||
|
const params = inputs.map((x) => x.type).join(',')
|
||||||
|
return `${name}(${params})`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSignatureHash = (signature: string): string => {
|
||||||
|
return web3.utils.keccak256(signature).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMethodHash = (method: AbiItem): string => {
|
||||||
|
const signature = getMethodSignature(method)
|
||||||
|
return getSignatureHash(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMethodSignatureAndSignatureHash = (
|
||||||
|
method: AbiItem,
|
||||||
|
): { methodSignature: string; signatureHash: string } => {
|
||||||
|
const methodSignature = getMethodSignature(method)
|
||||||
|
const signatureHash = getSignatureHash(methodSignature)
|
||||||
|
return { methodSignature, signatureHash }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isAllowedMethod = ({ name, type }: AbiItem): boolean => {
|
||||||
|
return type === 'function' && !!name
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMethodAction = ({ stateMutability }: AbiItem): 'read' | 'write' => {
|
||||||
|
return ['view', 'pure'].includes(stateMutability) ? 'read' : 'write'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractUsefulMethods = (abi: AbiItem[]): AbiItemExtended[] => {
|
||||||
|
return abi
|
||||||
|
.filter(isAllowedMethod)
|
||||||
|
.map(
|
||||||
|
(method): AbiItemExtended => ({
|
||||||
|
action: getMethodAction(method),
|
||||||
|
...getMethodSignatureAndSignatureHash(method),
|
||||||
|
...method,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.sort(({ name: a }, { name: b }) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isPayable = (method: AbiItem | AbiItemExtended): boolean => {
|
||||||
|
return method.payable
|
||||||
|
}
|
|
@ -1,11 +1,10 @@
|
||||||
import { RateLimit } from 'async-sema'
|
import { RateLimit } from 'async-sema'
|
||||||
import memoize from 'lodash.memoize'
|
import memoize from 'lodash.memoize'
|
||||||
|
|
||||||
import ABIService from 'src/logic/contractInteraction/sources/ABIService'
|
|
||||||
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
|
import { ETHEREUM_NETWORK } from 'src/logic/wallets/getWeb3'
|
||||||
import { ETHERSCAN_API_KEY } from 'src/utils/constants'
|
import { ETHERSCAN_API_KEY } from 'src/utils/constants'
|
||||||
|
|
||||||
class EtherscanService extends ABIService {
|
class EtherscanService {
|
||||||
_rateLimit = async () => {}
|
_rateLimit = async () => {}
|
||||||
|
|
||||||
_endpointsUrls = {
|
_endpointsUrls = {
|
||||||
|
@ -38,7 +37,6 @@ class EtherscanService extends ABIService {
|
||||||
)
|
)
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super()
|
|
||||||
this._rateLimit = RateLimit(options.rps)
|
this._rateLimit = RateLimit(options.rps)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,14 +36,14 @@ export const ContinueFooter = ({
|
||||||
onContinue,
|
onContinue,
|
||||||
}: {
|
}: {
|
||||||
continueButtonDisabled: boolean
|
continueButtonDisabled: boolean
|
||||||
onContinue: Function
|
onContinue: () => void
|
||||||
}) => (
|
}) => (
|
||||||
<Button color="primary" disabled={continueButtonDisabled} onClick={onContinue} variant="contained">
|
<Button color="primary" disabled={continueButtonDisabled} onClick={onContinue} variant="contained">
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
||||||
export const ErrorFooter = ({ onCancel, onRetry }: { onCancel: Function; onRetry: Function }) => (
|
export const ErrorFooter = ({ onCancel, onRetry }: { onCancel: () => void; onRetry: () => void }) => (
|
||||||
<>
|
<>
|
||||||
<ButtonWithMargin onClick={onCancel} variant="contained">
|
<ButtonWithMargin onClick={onCancel} variant="contained">
|
||||||
Cancel
|
Cancel
|
||||||
|
|
|
@ -5,57 +5,45 @@ import { useField, useFormState } from 'react-final-form'
|
||||||
import Button from 'src/components/layout/Button'
|
import Button from 'src/components/layout/Button'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||||
import { createTxObject } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
import { isReadMethod } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles as any)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const Buttons = ({ onCallSubmit, onClose }) => {
|
export interface ButtonProps {
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Buttons = ({ onClose }: ButtonProps) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const {
|
const {
|
||||||
input: { value: method },
|
input: { value: method },
|
||||||
} = useField('selectedMethod', { value: true })
|
} = useField('selectedMethod', { subscription: { value: true } })
|
||||||
const {
|
const { modifiedSinceLastSubmit, submitError, submitting, valid, validating } = useFormState({
|
||||||
input: { value: contractAddress },
|
subscription: {
|
||||||
} = useField('contractAddress', { valid: true } as any)
|
modifiedSinceLastSubmit: true,
|
||||||
const { submitting, valid, validating, values } = useFormState({
|
submitError: true,
|
||||||
subscription: { submitting: true, valid: true, values: true, validating: true },
|
submitting: true,
|
||||||
|
valid: true,
|
||||||
|
validating: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleCallSubmit = async () => {
|
|
||||||
const results = await createTxObject(method, contractAddress, values).call()
|
|
||||||
onCallSubmit(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row align="center" className={classes.buttonRow}>
|
<Row align="center" className={classes.buttonRow}>
|
||||||
<Button minWidth={140} onClick={onClose}>
|
<Button minWidth={140} onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{method && (method as any).action === 'read' ? (
|
|
||||||
<Button
|
<Button
|
||||||
className={classes.submitButton}
|
className={classes.submitButton}
|
||||||
color="primary"
|
color="primary"
|
||||||
data-testid="review-tx-btn"
|
data-testid={`${isReadMethod(method) ? 'call' : 'review'}-tx-btn`}
|
||||||
disabled={validating || !valid}
|
disabled={submitting || validating || ((!valid || !!submitError) && !modifiedSinceLastSubmit) || !method}
|
||||||
minWidth={140}
|
|
||||||
onClick={handleCallSubmit}
|
|
||||||
variant="contained"
|
|
||||||
>
|
|
||||||
Call
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
className={classes.submitButton}
|
|
||||||
color="primary"
|
|
||||||
data-testid="review-tx-btn"
|
|
||||||
disabled={submitting || validating || !valid || !method || (method as any).action === 'read'}
|
|
||||||
minWidth={140}
|
minWidth={140}
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
Review
|
{isReadMethod(method) ? 'Call' : 'Review'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,19 +3,19 @@ import React from 'react'
|
||||||
import TextareaField from 'src/components/forms/TextareaField'
|
import TextareaField from 'src/components/forms/TextareaField'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import EtherscanService from 'src/logic/contractInteraction/sources/EtherscanService'
|
import { extractUsefulMethods } from 'src/logic/contractInteraction/sources/ABIService'
|
||||||
|
|
||||||
export const NO_DATA = 'no data'
|
export const NO_DATA = 'no data'
|
||||||
|
|
||||||
const mustBeValidABI = (abi) => {
|
const mustBeValidABI = (abi: string): undefined | string => {
|
||||||
try {
|
try {
|
||||||
const parsedABI = EtherscanService.extractUsefulMethods(JSON.parse(abi))
|
const parsedABI = extractUsefulMethods(JSON.parse(abi))
|
||||||
|
|
||||||
if (parsedABI.length === 0) {
|
if (parsedABI.length === 0) {
|
||||||
return NO_DATA
|
return NO_DATA
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return []
|
return NO_DATA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,17 @@ import Col from 'src/components/layout/Col'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles as any)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const EthAddressInput = ({ isContract = true, isRequired = true, name, onScannedValue, text }) => {
|
export interface EthAddressProps {
|
||||||
|
isContract?: boolean
|
||||||
|
isRequired?: boolean
|
||||||
|
name: string
|
||||||
|
onScannedValue: (scannedValue: string) => void
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EthAddressInput = ({ isContract = true, isRequired = true, name, onScannedValue, text }: EthAddressProps) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress]
|
const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress]
|
||||||
const validate = composeValidators(...validatorsList.filter((_) => _))
|
const validate = composeValidators(...validatorsList.filter((_) => _))
|
||||||
|
|
|
@ -11,21 +11,24 @@ import ButtonLink from 'src/components/layout/ButtonLink'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import ABIService from 'src/logic/contractInteraction/sources/ABIService'
|
import { isPayable } from 'src/logic/contractInteraction/sources/ABIService'
|
||||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||||
import { safeSelector } from 'src/routes/safe/store/selectors'
|
import { safeSelector } from 'src/routes/safe/store/selectors'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles as any)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const EthValue = ({ onSetMax }) => {
|
interface EthValueProps {
|
||||||
|
onSetMax: (ethBalance: string) => void
|
||||||
|
}
|
||||||
|
const EthValue = ({ onSetMax }: EthValueProps) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const { ethBalance } = useSelector(safeSelector)
|
const { ethBalance } = useSelector(safeSelector)
|
||||||
const {
|
const {
|
||||||
input: { value: method },
|
input: { value: method },
|
||||||
} = useField('selectedMethod', { value: true })
|
} = useField('selectedMethod', { subscription: { value: true } })
|
||||||
const disabled = !ABIService.isPayable(method)
|
const disabled = !isPayable(method)
|
||||||
|
|
||||||
return (
|
return disabled ? null : (
|
||||||
<>
|
<>
|
||||||
<Row className={classes.fullWidth} margin="xs">
|
<Row className={classes.fullWidth} margin="xs">
|
||||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||||
|
@ -42,7 +45,6 @@ const EthValue = ({ onSetMax }) => {
|
||||||
<Row margin="md">
|
<Row margin="md">
|
||||||
<Col>
|
<Col>
|
||||||
<Field
|
<Field
|
||||||
className={classes.addressInput}
|
|
||||||
component={TextField}
|
component={TextField}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
inputAdornment={{
|
inputAdornment={{
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import React from 'react'
|
||||||
|
import { useFormState } from 'react-final-form'
|
||||||
|
|
||||||
|
import Row from 'src/components/layout/Row'
|
||||||
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
|
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const FormErrorMessage = () => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const { modifiedSinceLastSubmit, submitError } = useFormState({
|
||||||
|
subscription: { modifiedSinceLastSubmit: true, submitError: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasNewSubmitError = !!submitError && !modifiedSinceLastSubmit
|
||||||
|
return hasNewSubmitError ? (
|
||||||
|
<Row align="center" className={classes.fullWidth} margin="xs">
|
||||||
|
<Paragraph color="error" noMargin size="md" style={{ letterSpacing: '-0.5px', overflowWrap: 'anywhere' }}>
|
||||||
|
{submitError}
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormErrorMessage
|
|
@ -7,14 +7,20 @@ import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles as any)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const Header = ({ onClose, subTitle, title }) => {
|
interface HeaderProps {
|
||||||
|
onClose: () => void
|
||||||
|
subTitle: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = ({ onClose, subTitle, title }: HeaderProps) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row align="center" className={classes.heading} grow>
|
<Row align="center" className={classes.heading} grow>
|
||||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||||
{title}
|
{title}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Paragraph className={classes.annotation}>{subTitle}</Paragraph>
|
<Paragraph className={classes.annotation}>{subTitle}</Paragraph>
|
||||||
|
|
|
@ -8,23 +8,28 @@ import SearchIcon from '@material-ui/icons/Search'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useField, useFormState } from 'react-final-form'
|
import { useField, useFormState } from 'react-final-form'
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
import EtherscanService from 'src/logic/contractInteraction/sources/EtherscanService'
|
|
||||||
import { NO_CONTRACT } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
import { NO_CONTRACT } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
||||||
import CheckIcon from 'src/routes/safe/components/CurrencyDropdown/img/check.svg'
|
import CheckIcon from 'src/routes/safe/components/CurrencyDropdown/img/check.svg'
|
||||||
import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style'
|
import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style'
|
||||||
import { DropdownListTheme } from 'src/theme/mui'
|
import { DropdownListTheme } from 'src/theme/mui'
|
||||||
|
import { extractUsefulMethods } from 'src/logic/contractInteraction/sources/ABIService'
|
||||||
|
|
||||||
const MENU_WIDTH = '452px'
|
const MENU_WIDTH = '452px'
|
||||||
|
|
||||||
const MethodsDropdown = ({ onChange }) => {
|
interface MethodsDropdownProps {
|
||||||
|
onChange: (method: AbiItem) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MethodsDropdown = ({ onChange }: MethodsDropdownProps) => {
|
||||||
const classes = useDropdownStyles({ buttonWidth: MENU_WIDTH })
|
const classes = useDropdownStyles({ buttonWidth: MENU_WIDTH })
|
||||||
const {
|
const {
|
||||||
input: { value: abi },
|
input: { value: abi },
|
||||||
meta: { valid },
|
meta: { valid },
|
||||||
} = useField('abi', { value: true, valid: true } as any)
|
} = useField('abi', { subscription: { value: true, valid: true } })
|
||||||
const {
|
const {
|
||||||
initialValues: { selectedMethod: selectedMethodByDefault },
|
initialValues: { selectedMethod: selectedMethodByDefault },
|
||||||
} = useFormState({ subscription: { initialValues: true } })
|
} = useFormState({ subscription: { initialValues: true } })
|
||||||
|
@ -37,14 +42,14 @@ const MethodsDropdown = ({ onChange }) => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (abi) {
|
if (abi) {
|
||||||
try {
|
try {
|
||||||
setMethodsList(EtherscanService.extractUsefulMethods(JSON.parse(abi)))
|
setMethodsList(extractUsefulMethods(JSON.parse(abi)))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMethodsList([])
|
setMethodsList([])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [abi])
|
}, [abi])
|
||||||
|
|
||||||
React.useMemo(() => {
|
React.useEffect(() => {
|
||||||
setMethodsListFiltered(methodsList.filter(({ name }) => name.toLowerCase().includes(searchParams.toLowerCase())))
|
setMethodsListFiltered(methodsList.filter(({ name }) => name.toLowerCase().includes(searchParams.toLowerCase())))
|
||||||
}, [methodsList, searchParams])
|
}, [methodsList, searchParams])
|
||||||
|
|
||||||
|
@ -56,7 +61,7 @@ const MethodsDropdown = ({ onChange }) => {
|
||||||
setAnchorEl(null)
|
setAnchorEl(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMethodSelectedChanged = (chosenMethod) => {
|
const onMethodSelectedChanged = (chosenMethod: AbiItem) => {
|
||||||
setSelectedMethod(chosenMethod)
|
setSelectedMethod(chosenMethod)
|
||||||
onChange(chosenMethod)
|
onChange(chosenMethod)
|
||||||
handleClose()
|
handleClose()
|
||||||
|
|
|
@ -8,10 +8,10 @@ import InputComponent from './InputComponent'
|
||||||
const RenderInputParams = () => {
|
const RenderInputParams = () => {
|
||||||
const {
|
const {
|
||||||
meta: { valid: validABI },
|
meta: { valid: validABI },
|
||||||
} = useField('abi', { value: true })
|
} = useField('abi', { subscription: { valid: true, value: true } })
|
||||||
const {
|
const {
|
||||||
input: { value: method },
|
input: { value: method },
|
||||||
}: any = useField('selectedMethod', { value: true })
|
}: any = useField('selectedMethod', { subscription: { value: true } })
|
||||||
const renderInputs = validABI && !!method && method.inputs.length
|
const renderInputs = validABI && !!method && method.inputs.length
|
||||||
|
|
||||||
return !renderInputs
|
return !renderInputs
|
||||||
|
|
|
@ -3,19 +3,26 @@ import { useField } from 'react-final-form'
|
||||||
|
|
||||||
import TextField from 'src/components/forms/TextField'
|
import TextField from 'src/components/forms/TextField'
|
||||||
import Col from 'src/components/layout/Col'
|
import Col from 'src/components/layout/Col'
|
||||||
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
|
|
||||||
const RenderOutputParams = () => {
|
const RenderOutputParams = () => {
|
||||||
const {
|
const {
|
||||||
input: { value: method },
|
input: { value: method },
|
||||||
}: any = useField('selectedMethod', { value: true })
|
}: any = useField('selectedMethod', { subscription: { value: true } })
|
||||||
const {
|
const {
|
||||||
input: { value: results },
|
input: { value: results },
|
||||||
}: any = useField('callResults', { value: true })
|
}: any = useField('callResults', { subscription: { value: true } })
|
||||||
const multipleResults = !!method && method.outputs.length > 1
|
const multipleResults = !!method && method.outputs.length > 1
|
||||||
|
|
||||||
return results
|
return results ? (
|
||||||
? method.outputs.map(({ name, type }, index) => {
|
<>
|
||||||
|
<Row align="left" margin="xs">
|
||||||
|
<Paragraph color="primary" size="lg" style={{ letterSpacing: '-0.5px' }}>
|
||||||
|
Call result:
|
||||||
|
</Paragraph>
|
||||||
|
</Row>
|
||||||
|
{method.outputs.map(({ name, type }, index) => {
|
||||||
const placeholder = name ? `${name} (${type})` : type
|
const placeholder = name ? `${name} (${type})` : type
|
||||||
const key = `methodCallResult-${method.name}_${index}_${type}`
|
const key = `methodCallResult-${method.name}_${index}_${type}`
|
||||||
const value = multipleResults ? results[index] : results
|
const value = multipleResults ? results[index] : results
|
||||||
|
@ -33,8 +40,9 @@ const RenderOutputParams = () => {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
})
|
})}
|
||||||
: null
|
</>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RenderOutputParams
|
export default RenderOutputParams
|
||||||
|
|
|
@ -3,8 +3,6 @@ import { useSnackbar } from 'notistack'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { styles } from './style'
|
|
||||||
|
|
||||||
import AddressInfo from 'src/components/AddressInfo'
|
import AddressInfo from 'src/components/AddressInfo'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Button from 'src/components/layout/Button'
|
import Button from 'src/components/layout/Button'
|
||||||
|
@ -13,11 +11,13 @@ import Hairline from 'src/components/layout/Hairline'
|
||||||
import Img from 'src/components/layout/Img'
|
import Img from 'src/components/layout/Img'
|
||||||
import Paragraph from 'src/components/layout/Paragraph'
|
import Paragraph from 'src/components/layout/Paragraph'
|
||||||
import Row from 'src/components/layout/Row'
|
import Row from 'src/components/layout/Row'
|
||||||
|
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||||
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gasNew'
|
import { estimateTxGasCosts } from 'src/logic/safe/transactions/gasNew'
|
||||||
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
|
||||||
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
|
||||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
|
import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/style'
|
||||||
import Header from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
import Header from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
||||||
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils'
|
||||||
import createTransaction from 'src/routes/safe/store/actions/createTransaction'
|
import createTransaction from 'src/routes/safe/store/actions/createTransaction'
|
||||||
|
@ -31,14 +31,7 @@ export type TransactionReviewType = {
|
||||||
contractAddress?: string
|
contractAddress?: string
|
||||||
data?: string
|
data?: string
|
||||||
value?: string
|
value?: string
|
||||||
selectedMethod?: {
|
selectedMethod?: AbiItemExtended
|
||||||
action: string
|
|
||||||
signature: string
|
|
||||||
signatureHash: string
|
|
||||||
constant: boolean
|
|
||||||
inputs: []
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -102,7 +95,7 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props) => {
|
||||||
<>
|
<>
|
||||||
<Header onClose={onClose} subTitle="2 of 2" title="Contract Interaction" />
|
<Header onClose={onClose} subTitle="2 of 2" title="Contract Interaction" />
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Block className={classes.container}>
|
<Block className={classes.formContainer}>
|
||||||
<Row margin="xs">
|
<Row margin="xs">
|
||||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||||
Contract Address
|
Contract Address
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { border, lg, md, secondaryText, sm } from 'src/theme/variables'
|
|
||||||
import { createStyles } from '@material-ui/core'
|
|
||||||
|
|
||||||
export const styles = createStyles({
|
|
||||||
heading: {
|
|
||||||
padding: `${md} ${lg}`,
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
maxHeight: '75px',
|
|
||||||
},
|
|
||||||
annotation: {
|
|
||||||
letterSpacing: '-1px',
|
|
||||||
color: secondaryText,
|
|
||||||
marginRight: 'auto',
|
|
||||||
marginLeft: '20px',
|
|
||||||
},
|
|
||||||
headingText: {
|
|
||||||
fontSize: lg,
|
|
||||||
},
|
|
||||||
closeIcon: {
|
|
||||||
height: '35px',
|
|
||||||
width: '35px',
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
padding: `${md} ${lg}`,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
marginLeft: sm,
|
|
||||||
},
|
|
||||||
outerData: {
|
|
||||||
borderRadius: '5px',
|
|
||||||
border: `1px solid ${border}`,
|
|
||||||
padding: '11px',
|
|
||||||
minHeight: '21px',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
wordBreak: 'break-all',
|
|
||||||
overflow: 'auto',
|
|
||||||
fontSize: '14px',
|
|
||||||
fontFamily: 'Averta',
|
|
||||||
maxHeight: '100px',
|
|
||||||
letterSpacing: 'normal',
|
|
||||||
fontStretch: 'normal',
|
|
||||||
lineHeight: '1.43',
|
|
||||||
},
|
|
||||||
buttonRow: {
|
|
||||||
height: '84px',
|
|
||||||
justifyContent: 'center',
|
|
||||||
'& > button': {
|
|
||||||
fontFamily: 'Averta',
|
|
||||||
fontSize: md,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
submitButton: {
|
|
||||||
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
|
||||||
marginLeft: '15px',
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,31 +1,45 @@
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
import GnoForm from 'src/components/forms/GnoForm'
|
import GnoForm from 'src/components/forms/GnoForm'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Hairline from 'src/components/layout/Hairline'
|
import Hairline from 'src/components/layout/Hairline'
|
||||||
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||||
import Buttons from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Buttons'
|
import { safeSelector } from 'src/routes/safe/store/selectors'
|
||||||
import ContractABI from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ContractABI'
|
import Buttons from './Buttons'
|
||||||
import EthAddressInput from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthAddressInput'
|
import ContractABI from './ContractABI'
|
||||||
import EthValue from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/EthValue'
|
import EthAddressInput from './EthAddressInput'
|
||||||
import FormDivisor from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/FormDivisor'
|
import EthValue from './EthValue'
|
||||||
import Header from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Header'
|
import FormDivisor from './FormDivisor'
|
||||||
import MethodsDropdown from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/MethodsDropdown'
|
import FormErrorMessage from './FormErrorMessage'
|
||||||
import RenderInputParams from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderInputParams'
|
import Header from './Header'
|
||||||
import RenderOutputParams from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/RenderOutputParams'
|
import MethodsDropdown from './MethodsDropdown'
|
||||||
import {
|
import RenderInputParams from './RenderInputParams'
|
||||||
abiExtractor,
|
import RenderOutputParams from './RenderOutputParams'
|
||||||
createTxObject,
|
import { abiExtractor, createTxObject, formMutators, handleSubmitError, isReadMethod } from './utils'
|
||||||
formMutators,
|
|
||||||
} from 'src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/utils'
|
|
||||||
|
|
||||||
const useStyles = makeStyles(styles as any)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }) => {
|
export interface CreatedTx {
|
||||||
|
contractAddress: string
|
||||||
|
data: string
|
||||||
|
selectedMethod: any
|
||||||
|
value: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContractInteractionProps {
|
||||||
|
contractAddress: string
|
||||||
|
initialValues: { contractAddress?: string }
|
||||||
|
onClose: () => void
|
||||||
|
onNext: (tx: CreatedTx) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }: ContractInteractionProps) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
const { address: safeAddress = '' } = useSelector(safeSelector)
|
||||||
|
let setCallResults
|
||||||
|
|
||||||
React.useMemo(() => {
|
React.useMemo(() => {
|
||||||
if (contractAddress) {
|
if (contractAddress) {
|
||||||
|
@ -35,8 +49,22 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
||||||
|
|
||||||
const handleSubmit = async ({ contractAddress, selectedMethod, value, ...values }) => {
|
const handleSubmit = async ({ contractAddress, selectedMethod, value, ...values }) => {
|
||||||
if (value || (contractAddress && selectedMethod)) {
|
if (value || (contractAddress && selectedMethod)) {
|
||||||
const data = await createTxObject(selectedMethod, contractAddress, values).encodeABI()
|
try {
|
||||||
|
const txObject = createTxObject(selectedMethod, contractAddress, values)
|
||||||
|
const data = txObject.encodeABI()
|
||||||
|
|
||||||
|
if (isReadMethod(selectedMethod)) {
|
||||||
|
const result = await txObject.call({ from: safeAddress })
|
||||||
|
setCallResults(result)
|
||||||
|
|
||||||
|
// this was a read method, so we won't go to the 'review' screen
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
onNext({ ...values, contractAddress, data, selectedMethod, value })
|
onNext({ ...values, contractAddress, data, selectedMethod, value })
|
||||||
|
} catch (error) {
|
||||||
|
return handleSubmitError(error, values)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +80,8 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
||||||
subscription={{ submitting: true, pristine: true }}
|
subscription={{ submitting: true, pristine: true }}
|
||||||
>
|
>
|
||||||
{(submitting, validating, rest, mutators) => {
|
{(submitting, validating, rest, mutators) => {
|
||||||
|
setCallResults = mutators.setCallResults
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Block className={classes.formContainer}>
|
<Block className={classes.formContainer}>
|
||||||
|
@ -62,14 +92,15 @@ const ContractInteraction = ({ contractAddress, initialValues, onClose, onNext }
|
||||||
onScannedValue={mutators.setContractAddress}
|
onScannedValue={mutators.setContractAddress}
|
||||||
text="Contract Address*"
|
text="Contract Address*"
|
||||||
/>
|
/>
|
||||||
<EthValue onSetMax={mutators.setMax} />
|
|
||||||
<ContractABI />
|
<ContractABI />
|
||||||
<MethodsDropdown onChange={mutators.setSelectedMethod} />
|
<MethodsDropdown onChange={mutators.setSelectedMethod} />
|
||||||
|
<EthValue onSetMax={mutators.setMax} />
|
||||||
<RenderInputParams />
|
<RenderInputParams />
|
||||||
<RenderOutputParams />
|
<RenderOutputParams />
|
||||||
|
<FormErrorMessage />
|
||||||
</Block>
|
</Block>
|
||||||
<Hairline />
|
<Hairline />
|
||||||
<Buttons onCallSubmit={mutators.setCallResults} onClose={onClose} />
|
<Buttons onClose={onClose} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { lg, md } from 'src/theme/variables'
|
import { lg, md, secondaryText, sm, border } from 'src/theme/variables'
|
||||||
|
import { createStyles } from '@material-ui/core'
|
||||||
|
|
||||||
export const styles = () => ({
|
export const styles = createStyles({
|
||||||
heading: {
|
heading: {
|
||||||
padding: `${md} ${lg}`,
|
padding: `${md} ${lg}`,
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
|
@ -9,11 +10,11 @@ export const styles = () => ({
|
||||||
},
|
},
|
||||||
annotation: {
|
annotation: {
|
||||||
letterSpacing: '-1px',
|
letterSpacing: '-1px',
|
||||||
color: '#a2a8ba',
|
color: secondaryText,
|
||||||
marginRight: 'auto',
|
marginRight: 'auto',
|
||||||
marginLeft: '20px',
|
marginLeft: '20px',
|
||||||
},
|
},
|
||||||
manage: {
|
headingText: {
|
||||||
fontSize: lg,
|
fontSize: lg,
|
||||||
},
|
},
|
||||||
closeIcon: {
|
closeIcon: {
|
||||||
|
@ -26,6 +27,25 @@ export const styles = () => ({
|
||||||
formContainer: {
|
formContainer: {
|
||||||
padding: `${md} ${lg}`,
|
padding: `${md} ${lg}`,
|
||||||
},
|
},
|
||||||
|
value: {
|
||||||
|
marginLeft: sm,
|
||||||
|
},
|
||||||
|
outerData: {
|
||||||
|
borderRadius: '5px',
|
||||||
|
border: `1px solid ${border}`,
|
||||||
|
padding: '11px',
|
||||||
|
minHeight: '21px',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
overflow: 'auto',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'Averta',
|
||||||
|
maxHeight: '100px',
|
||||||
|
letterSpacing: 'normal',
|
||||||
|
fontStretch: 'normal',
|
||||||
|
lineHeight: '1.43',
|
||||||
|
},
|
||||||
buttonRow: {
|
buttonRow: {
|
||||||
height: '84px',
|
height: '84px',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
import { FORM_ERROR } from 'final-form'
|
||||||
import createDecorator from 'final-form-calculate'
|
import createDecorator from 'final-form-calculate'
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
|
||||||
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator'
|
||||||
import { getNetwork } from 'src/config'
|
import { getNetwork } from 'src/config'
|
||||||
import { getConfiguredSource } from 'src/logic/contractInteraction/sources'
|
import { getConfiguredSource } from 'src/logic/contractInteraction/sources'
|
||||||
|
import { AbiItemExtended } from 'src/logic/contractInteraction/sources/ABIService'
|
||||||
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
import { getWeb3 } from 'src/logic/wallets/getWeb3'
|
||||||
import { TransactionReviewType } from '../Review'
|
import { TransactionReviewType } from '../Review'
|
||||||
|
|
||||||
|
@ -49,7 +52,18 @@ export const formMutators = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createTxObject = (method, contractAddress, values) => {
|
export const handleSubmitError = (error, values) => {
|
||||||
|
for (const key in values) {
|
||||||
|
if (values.hasOwnProperty(key) && values[key] === error.value) {
|
||||||
|
return { [key]: error.reason }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// .call() failed and we're logging a generic error
|
||||||
|
return { [FORM_ERROR]: error.message }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createTxObject = (method: AbiItem, contractAddress: string, values) => {
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
const contract: any = new web3.eth.Contract([method], contractAddress)
|
const contract: any = new web3.eth.Contract([method], contractAddress)
|
||||||
const { inputs, name } = method
|
const { inputs, name } = method
|
||||||
|
@ -58,6 +72,8 @@ export const createTxObject = (method, contractAddress, values) => {
|
||||||
return contract.methods[name](...args)
|
return contract.methods[name](...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isReadMethod = (method: AbiItemExtended): boolean => method && method.action === 'read'
|
||||||
|
|
||||||
export const getValueFromTxInputs = (key: string, type: string, tx: TransactionReviewType): string => {
|
export const getValueFromTxInputs = (key: string, type: string, tx: TransactionReviewType): string => {
|
||||||
let value = tx[key]
|
let value = tx[key]
|
||||||
if (type === 'bool') {
|
if (type === 'bool') {
|
||||||
|
|
|
@ -351,7 +351,7 @@ export function generateSafeTxHash(safeAddress: string, txArgs: TxArgs): string
|
||||||
{ type: 'uint256', name: 'nonce' },
|
{ type: 'uint256', name: 'nonce' },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
const primaryType: 'SafeTx' = 'SafeTx'
|
const primaryType = 'SafeTx' as const
|
||||||
|
|
||||||
const typedData = {
|
const typedData = {
|
||||||
types: messageTypes,
|
types: messageTypes,
|
||||||
|
|
Loading…
Reference in New Issue