From efccac79ad818112b7f5f4a1c4989ffa1d43296f Mon Sep 17 00:00:00 2001 From: HenryNguyen5 Date: Tue, 17 Oct 2017 00:01:28 -0400 Subject: [PATCH] Contracts UI (#277) * Refactor BaseNode to be an interface INode * Initial contract commit * Remove redundant fallback ABI function * First working iteration of Contract generator to be used in ENS branch * Hide abi to clean up logging output * Strip 0x prefix from output decode * Handle unnamed output params * Implement ability to supply output mappings to ABI functions * Fix null case in outputMapping * Add flow typing * Add .call method to functions * Partial commit for type refactor * Temp contract type fix -- waiting for NPM modularization * Misc. Optimizations to tsconfig + webpack * Convert Contracts to TS * Remove nested prop passing from contracts, get rid of contract reducers / sagas / redux state * Add disclaimer modal to footer * Remove duplicate code & unnecessary styles * Add contracts to nav * Wrap Contracts in App * Add ether/hex validation override for contract creation calls * First iteration of working deploy contract * Delete routing file that shouldnt exist * Revert "Misc. Optimizations to tsconfig + webpack" This reverts commit 70cba3a07f4255153a9e277b3c41032a4b661c94. * Cleanup HOC code * Fix formatting noise * remove un-used css style * Remove deterministic contract address computation * Remove empty files * Cleanup contract * Add call request to node interface * Fix output mapping types * Revert destructuring overboard * Add sendCallRequest to rpcNode class and add typing * Use enum for selecting ABI methods * Fix tslint error & add media query for modals * Nest Media Query * Fix contracts to include new router fixes * Add transaction capability to contracts * Get ABI parsing + contract calls almost fully integrated using dynamic contract parser lib * Refactor contract deploy to have a reusable HOC for contract interact * Move modal and tx comparasion up file tree * Include ABI outputs in display * Cleanup privaite/public members * Remove broadcasting step from a contract transaction * Update TX contract components to inter-op with interact and deploy * Finish contracts-interact functionality * Add transaction capability to contracts * Cleanup privaite/public members * Remove broadcasting step from a contract transaction * Apply James's CSS fix * Cleanup uneeded types * Remove unecessary class * Add UI side validation and helper utils, addresess PR comments * Fix spacing + remove unused imports / types * Fix spacing + remove unused imports / types * Address PR comments * Actually address PR comments * Actually address PR comments --- common/Root.tsx | 2 + common/actions/contracts/actionCreators.ts | 22 -- common/actions/contracts/actionTypes.ts | 31 -- common/actions/contracts/constants.ts | 4 - common/actions/contracts/index.ts | 3 - .../Header/components/Navigation.tsx | 4 + common/components/ui/Code.scss | 14 + common/components/ui/Code.tsx | 10 + .../Deploy/components/DeployHoc/index.tsx | 162 ++++++++++ .../Deploy/components/DeployHoc/types.ts | 42 +++ .../Contracts/components/Deploy/index.tsx | 101 +++++++ .../InteractExplorer/InteractExplorer.scss | 32 ++ .../components/InteractExplorer/index.tsx | 277 ++++++++++++++++++ .../components/InteractForm/InteractForm.scss | 14 + .../components/InteractForm/index.tsx | 158 ++++++++++ .../Contracts/components/Interact/index.tsx | 205 +++++++++++++ .../Tabs/Contracts/components/TxCompare.tsx | 47 +++ .../Tabs/Contracts/components/TxModal.tsx | 65 ++++ .../Tabs/Contracts/components/withTx.tsx | 37 +++ common/containers/Tabs/Contracts/index.scss | 30 ++ common/containers/Tabs/Contracts/index.tsx | 61 ++++ .../containers/Tabs/SendTransaction/index.tsx | 1 + common/index.tsx | 2 +- common/libs/contracts/ABIFunction.ts | 3 +- common/libs/transaction.ts | 10 +- common/libs/validators.ts | 17 +- common/reducers/contracts.ts | 39 --- common/reducers/index.ts | 3 - common/sagas/contracts.ts | 39 --- common/sagas/index.ts | 2 - common/utils/helpers.ts | 7 + 31 files changed, 1292 insertions(+), 152 deletions(-) delete mode 100644 common/actions/contracts/actionCreators.ts delete mode 100644 common/actions/contracts/actionTypes.ts delete mode 100644 common/actions/contracts/constants.ts delete mode 100644 common/actions/contracts/index.ts create mode 100644 common/components/ui/Code.scss create mode 100644 common/components/ui/Code.tsx create mode 100644 common/containers/Tabs/Contracts/components/Deploy/components/DeployHoc/index.tsx create mode 100644 common/containers/Tabs/Contracts/components/Deploy/components/DeployHoc/types.ts create mode 100644 common/containers/Tabs/Contracts/components/Deploy/index.tsx create mode 100644 common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/InteractExplorer.scss create mode 100644 common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/index.tsx create mode 100644 common/containers/Tabs/Contracts/components/Interact/components/InteractForm/InteractForm.scss create mode 100644 common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx create mode 100644 common/containers/Tabs/Contracts/components/Interact/index.tsx create mode 100644 common/containers/Tabs/Contracts/components/TxCompare.tsx create mode 100644 common/containers/Tabs/Contracts/components/TxModal.tsx create mode 100644 common/containers/Tabs/Contracts/components/withTx.tsx create mode 100644 common/containers/Tabs/Contracts/index.scss create mode 100644 common/containers/Tabs/Contracts/index.tsx delete mode 100644 common/reducers/contracts.ts delete mode 100644 common/sagas/contracts.ts diff --git a/common/Root.tsx b/common/Root.tsx index 54916a36..34e362a5 100644 --- a/common/Root.tsx +++ b/common/Root.tsx @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import { Provider } from 'react-redux'; import { Router, Route } from 'react-router-dom'; // Components +import Contracts from 'containers/Tabs/Contracts'; import ENS from 'containers/Tabs/ENS'; import GenerateWallet from 'containers/Tabs/GenerateWallet'; import Help from 'containers/Tabs/Help'; @@ -28,6 +29,7 @@ export default class Root extends Component { + diff --git a/common/actions/contracts/actionCreators.ts b/common/actions/contracts/actionCreators.ts deleted file mode 100644 index 3ef77e59..00000000 --- a/common/actions/contracts/actionCreators.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as interfaces from './actionTypes'; -import { TypeKeys } from './constants'; - -export function accessContract( - address: string, - abiJson: string -): interfaces.AccessContractAction { - return { - type: TypeKeys.ACCESS_CONTRACT, - address, - abiJson - }; -} - -export function setInteractiveContract( - functions: interfaces.ABIFunction[] -): interfaces.SetInteractiveContractAction { - return { - type: TypeKeys.SET_INTERACTIVE_CONTRACT, - functions - }; -} diff --git a/common/actions/contracts/actionTypes.ts b/common/actions/contracts/actionTypes.ts deleted file mode 100644 index 7ae50608..00000000 --- a/common/actions/contracts/actionTypes.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TypeKeys } from './constants'; -/***** Set Interactive Contract *****/ -export interface ABIFunctionField { - name: string; - type: string; -} - -export interface ABIFunction { - name: string; - type: string; - constant: boolean; - inputs: ABIFunctionField[]; - outputs: ABIFunctionField[]; -} - -export interface SetInteractiveContractAction { - type: TypeKeys.SET_INTERACTIVE_CONTRACT; - functions: ABIFunction[]; -} - -/***** Access Contract *****/ -export interface AccessContractAction { - type: TypeKeys.ACCESS_CONTRACT; - address: string; - abiJson: string; -} - -/*** Union Type ***/ -export type ContractsAction = - | SetInteractiveContractAction - | AccessContractAction; diff --git a/common/actions/contracts/constants.ts b/common/actions/contracts/constants.ts deleted file mode 100644 index 3a23e1ef..00000000 --- a/common/actions/contracts/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum TypeKeys { - ACCESS_CONTRACT = 'ACCESS_CONTRACT', - SET_INTERACTIVE_CONTRACT = 'SET_INTERACTIVE_CONTRACT' -} diff --git a/common/actions/contracts/index.ts b/common/actions/contracts/index.ts deleted file mode 100644 index fee14683..00000000 --- a/common/actions/contracts/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './constants'; -export * from './actionTypes'; -export * from './actionCreators'; diff --git a/common/components/Header/components/Navigation.tsx b/common/components/Header/components/Navigation.tsx index 15cda5cd..339b1cb5 100644 --- a/common/components/Header/components/Navigation.tsx +++ b/common/components/Header/components/Navigation.tsx @@ -21,6 +21,10 @@ const tabs = [ name: 'NAV_ViewWallet' // to: 'view-wallet' }, + { + name: 'NAV_Contracts', + to: 'contracts' + }, { name: 'NAV_ENS', to: 'ens' diff --git a/common/components/ui/Code.scss b/common/components/ui/Code.scss new file mode 100644 index 00000000..3612e36c --- /dev/null +++ b/common/components/ui/Code.scss @@ -0,0 +1,14 @@ +pre { + color: #333; + background-color: #fafafa; + border: 1px solid #ececec; + border-radius: 0px; + padding: 8px; + code { + font-size: 14px; + line-height: 20px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + } +} diff --git a/common/components/ui/Code.tsx b/common/components/ui/Code.tsx new file mode 100644 index 00000000..41f1dfae --- /dev/null +++ b/common/components/ui/Code.tsx @@ -0,0 +1,10 @@ +import React, { Component } from 'react'; +import './Code.scss'; + +const Code = ({ children }) => ( +
+    {children}
+  
+); + +export default Code; diff --git a/common/containers/Tabs/Contracts/components/Deploy/components/DeployHoc/index.tsx b/common/containers/Tabs/Contracts/components/Deploy/components/DeployHoc/index.tsx new file mode 100644 index 00000000..4c43ef92 --- /dev/null +++ b/common/containers/Tabs/Contracts/components/Deploy/components/DeployHoc/index.tsx @@ -0,0 +1,162 @@ +import Big from 'bignumber.js'; +import React, { Component } from 'react'; +import { + generateCompleteTransaction as makeAndSignTx, + TransactionInput +} from 'libs/transaction'; +import { Props, State, initialState } from './types'; +import { + TxModal, + Props as DMProps, + TTxModal +} from 'containers/Tabs/Contracts/components/TxModal'; +import { + TxCompare, + Props as TCProps, + TTxCompare +} from 'containers/Tabs/Contracts/components/TxCompare'; +import { withTx } from 'containers/Tabs/Contracts/components//withTx'; +import { Props as DProps } from '../../'; + +export const deployHOC = PassedComponent => { + class WrappedComponent extends Component { + public state: State = initialState; + + public asyncSetState = value => + new Promise(resolve => this.setState(value, resolve)); + + public resetState = () => this.setState(initialState); + + public handleSignTx = async () => { + const { props, state } = this; + + if (state.data === '') { + return; + } + + try { + await this.getAddressAndNonce(); + await this.makeSignedTxFromState(); + } catch (e) { + props.showNotification( + 'danger', + e.message || 'Error during contract tx generation', + 5000 + ); + + return this.resetState(); + } + }; + + public handleInput = inputName => ( + ev: React.FormEvent + ): void => { + if (this.state.signedTx) { + this.resetState(); + } + + this.setState({ + [inputName]: ev.currentTarget.value + }); + }; + + public handleDeploy = () => this.setState({ displayModal: true }); + + public render() { + const { data: byteCode, gasLimit, signedTx, displayModal } = this.state; + + const props: DProps = { + handleInput: this.handleInput, + handleSignTx: this.handleSignTx, + handleDeploy: this.handleDeploy, + byteCode, + gasLimit, + displayModal, + walletExists: !!this.props.wallet, + txCompare: signedTx ? this.displayCompareTx() : null, + deployModal: signedTx ? this.displayDeployModal() : null + }; + + return ; + } + + private displayCompareTx = (): React.ReactElement => { + const { nonce, gasLimit, data, value, signedTx, to } = this.state; + const { gasPrice, chainId } = this.props; + + if (!nonce || !signedTx) { + throw Error('Can not display raw tx, nonce empty or no signed tx'); + } + + const props: TCProps = { + nonce, + gasPrice, + chainId, + data, + gasLimit, + to, + value, + signedTx + }; + + return ; + }; + + private displayDeployModal = (): React.ReactElement => { + const { networkName, node: { network, service } } = this.props; + const { signedTx } = this.state; + + if (!signedTx) { + throw Error('Can not deploy contract, no signed tx'); + } + + const props: DMProps = { + action: 'deploy a contract', + networkName, + network, + service, + handleBroadcastTx: this.handleBroadcastTx, + onClose: this.resetState + }; + + return ; + }; + + private handleBroadcastTx = () => { + if (!this.state.signedTx) { + throw Error('Can not broadcast tx, signed tx does not exist'); + } + this.props.broadcastTx(this.state.signedTx); + this.resetState(); + }; + + private makeSignedTxFromState = () => { + const { props, state: { data, gasLimit, value, to } } = this; + const transactionInput: TransactionInput = { + unit: 'ether', + to, + data, + value + }; + + return makeAndSignTx( + props.wallet, + props.nodeLib, + props.gasPrice, + new Big(gasLimit), + props.chainId, + transactionInput, + true + ).then(({ signedTx }) => this.asyncSetState({ signedTx })); + }; + + private getAddressAndNonce = async () => { + const address = await this.props.wallet.getAddress(); + const nonce = await this.props.nodeLib + .getTransactionCount(address) + .then(n => new Big(n).toString()); + return this.asyncSetState({ nonce, address }); + }; + } + return withTx(WrappedComponent); +}; diff --git a/common/containers/Tabs/Contracts/components/Deploy/components/DeployHoc/types.ts b/common/containers/Tabs/Contracts/components/Deploy/components/DeployHoc/types.ts new file mode 100644 index 00000000..67e3ea4c --- /dev/null +++ b/common/containers/Tabs/Contracts/components/Deploy/components/DeployHoc/types.ts @@ -0,0 +1,42 @@ +import { Wei, Ether } from 'libs/units'; +import { IWallet } from 'libs/wallet/IWallet'; +import { RPCNode } from 'libs/nodes'; +import { NodeConfig, NetworkConfig } from 'config/data'; +import { TBroadcastTx } from 'actions/wallet'; +import { TShowNotification } from 'actions/notifications'; + +export interface Props { + wallet: IWallet; + balance: Ether; + node: NodeConfig; + nodeLib: RPCNode; + chainId: NetworkConfig['chainId']; + networkName: NetworkConfig['name']; + gasPrice: Wei; + broadcastTx: TBroadcastTx; + showNotification: TShowNotification; +} + +export interface State { + data: string; + gasLimit: string; + determinedContractAddress: string; + signedTx: null | string; + nonce: null | string; + address: null | string; + value: string; + to: string; + displayModal: boolean; +} + +export const initialState: State = { + data: '', + gasLimit: '300000', + determinedContractAddress: '', + signedTx: null, + nonce: null, + address: null, + to: '0x', + value: '0x0', + displayModal: false +}; diff --git a/common/containers/Tabs/Contracts/components/Deploy/index.tsx b/common/containers/Tabs/Contracts/components/Deploy/index.tsx new file mode 100644 index 00000000..d3954dd4 --- /dev/null +++ b/common/containers/Tabs/Contracts/components/Deploy/index.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import translate from 'translations'; +import WalletDecrypt from 'components/WalletDecrypt'; +import { deployHOC } from './components/DeployHoc'; +import { TTxCompare } from '../TxCompare'; +import { TTxModal } from '../TxModal'; +import classnames from 'classnames'; +import { addProperties } from 'utils/helpers'; +import { isValidGasPrice, isValidByteCode } from 'libs/validators'; + +export interface Props { + byteCode: string; + gasLimit: string; + walletExists: boolean; + txCompare: React.ReactElement | null; + displayModal: boolean; + deployModal: React.ReactElement | null; + handleInput( + input: string + ): (ev: React.FormEvent) => void; + handleSignTx(): Promise; + handleDeploy(): void; +} + +const Deploy = (props: Props) => { + const { + handleSignTx, + handleInput, + handleDeploy, + byteCode, + gasLimit, + walletExists, + deployModal, + displayModal, + txCompare + } = props; + const validByteCode = isValidByteCode(byteCode); + const validGasLimit = isValidGasPrice(gasLimit); + const showSignTxButton = validByteCode && validGasLimit; + return ( +
+
+