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 ( +
+
+