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
This commit is contained in:
HenryNguyen5 2017-10-17 00:01:28 -04:00 committed by Daniel Ternyak
parent 2f8e0fe272
commit efccac79ad
31 changed files with 1292 additions and 152 deletions

View File

@ -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<Props, {}> {
<Route path="/help" component={Help} />
<Route path="/swap" component={Swap} />
<Route path="/send-transaction" component={SendTransaction} />
<Route path="/contracts" component={Contracts} />
<Route path="/ens" component={ENS} />
</div>
</Router>

View File

@ -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
};
}

View File

@ -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;

View File

@ -1,4 +0,0 @@
export enum TypeKeys {
ACCESS_CONTRACT = 'ACCESS_CONTRACT',
SET_INTERACTIVE_CONTRACT = 'SET_INTERACTIVE_CONTRACT'
}

View File

@ -1,3 +0,0 @@
export * from './constants';
export * from './actionTypes';
export * from './actionCreators';

View File

@ -21,6 +21,10 @@ const tabs = [
name: 'NAV_ViewWallet'
// to: 'view-wallet'
},
{
name: 'NAV_Contracts',
to: 'contracts'
},
{
name: 'NAV_ENS',
to: 'ens'

View File

@ -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;
}
}

View File

@ -0,0 +1,10 @@
import React, { Component } from 'react';
import './Code.scss';
const Code = ({ children }) => (
<pre>
<code>{children}</code>
</pre>
);
export default Code;

View File

@ -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<Props, State> {
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<HTMLTextAreaElement | HTMLInputElement>
): 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 <PassedComponent {...props} />;
}
private displayCompareTx = (): React.ReactElement<TTxCompare> => {
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 <TxCompare {...props} />;
};
private displayDeployModal = (): React.ReactElement<TTxModal> => {
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 <TxModal {...props} />;
};
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);
};

View File

@ -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
};

View File

@ -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<TTxCompare> | null;
displayModal: boolean;
deployModal: React.ReactElement<TTxModal> | null;
handleInput(
input: string
): (ev: React.FormEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
handleSignTx(): Promise<void>;
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 (
<div className="Deploy">
<section>
<label className="Deploy-field form-group">
<h4 className="Deploy-field-label">
{translate('CONTRACT_ByteCode')}
</h4>
<textarea
name="byteCode"
placeholder="0x8f87a973e..."
rows={6}
onChange={handleInput('data')}
className={classnames('Deploy-field-input', 'form-control', {
'is-invalid': !validByteCode
})}
value={byteCode || ''}
/>
</label>
<label className="Deploy-field form-group">
<h4 className="Deploy-field-label">Gas Limit</h4>
<input
name="gasLimit"
value={gasLimit || ''}
onChange={handleInput('gasLimit')}
className={classnames('Deploy-field-input', 'form-control', {
'is-invalid': !validGasLimit
})}
/>
</label>
{walletExists ? (
<button
className="Sign-submit btn btn-primary"
disabled={!showSignTxButton}
{...addProperties(showSignTxButton, { onClick: handleSignTx })}
>
{translate('DEP_signtx')}
</button>
) : (
<WalletDecrypt />
)}
{txCompare ? (
<section>
{txCompare}
<button
className="Deploy-submit btn btn-primary"
onClick={handleDeploy}
>
{translate('NAV_DeployContract')}
</button>
</section>
) : null}
{displayModal && deployModal}
</section>
</div>
);
};
export default deployHOC(Deploy);

View File

@ -0,0 +1,32 @@
@import 'common/sass/variables';
.InteractExplorer {
&-title {
&-address {
margin-left: 6px;
font-weight: 300;
opacity: 0.6;
}
}
&-func {
&-in,
&-out {
&-label {
&-type {
margin-left: 5px;
font-weight: 300;
opacity: 0.6;
}
}
}
&-in {
margin-right: 2rem;
}
&-out {
margin-left: 2rem;
}
}
}

View File

@ -0,0 +1,277 @@
import React, { Component } from 'react';
import translate from 'translations';
import './InteractExplorer.scss';
import Contract from 'libs/contracts';
import { TTxModal } from 'containers/Tabs/Contracts/components/TxModal';
import { TTxCompare } from 'containers/Tabs/Contracts/components/TxCompare';
import WalletDecrypt from 'components/WalletDecrypt';
import { TShowNotification } from 'actions/notifications';
import classnames from 'classnames';
import { isValidGasPrice, isValidValue } from 'libs/validators';
import { addProperties } from 'utils/helpers';
export interface Props {
contractFunctions: any;
walletDecrypted: boolean;
address: Contract['address'];
gasLimit: string;
value: string;
txGenerated: boolean;
txModal: React.ReactElement<TTxModal> | null;
txCompare: React.ReactElement<TTxCompare> | null;
displayModal: boolean;
showNotification: TShowNotification;
toggleModal(): void;
handleInput(name: string): (ev) => void;
handleFunctionSend(selectedFunction, inputs): () => void;
}
interface State {
inputs: {
[key: string]: { rawData: string; parsedData: string[] | string };
};
outputs;
selectedFunction: null | any;
selectedFunctionName: string;
}
export default class InteractExplorer extends Component<Props, State> {
public static defaultProps: Partial<Props> = {
contractFunctions: {}
};
public state: State = {
selectedFunction: null,
selectedFunctionName: '',
inputs: {},
outputs: {}
};
public render() {
const {
inputs,
outputs,
selectedFunction,
selectedFunctionName
} = this.state;
const {
address,
displayModal,
handleInput,
handleFunctionSend,
gasLimit,
txGenerated,
txCompare,
txModal,
toggleModal,
value,
walletDecrypted
} = this.props;
const validValue = isValidValue(value);
const validGasLimit = isValidGasPrice(gasLimit);
const showContractWrite = validValue && validGasLimit;
return (
<div className="InteractExplorer">
<h3 className="InteractExplorer-title">
{translate('CONTRACT_Interact_Title')}
<span className="InteractExplorer-title-address">{address}</span>
</h3>
<select
value={selectedFunction ? selectedFunction.name : ''}
className="InteractExplorer-fnselect form-control"
onChange={this.handleFunctionSelect}
>
<option>{translate('CONTRACT_Interact_CTA', true)}</option>
{this.contractOptions()}
</select>
{selectedFunction && (
<div key={selectedFunctionName} className="InteractExplorer-func">
{/* TODO: Use reusable components with validation */}
{selectedFunction.inputs.map(input => {
const { type, name } = input;
return (
<label
key={name}
className="InteractExplorer-func-in form-group"
>
<h4 className="InteractExplorer-func-in-label">
{name}
<span className="InteractExplorer-func-in-label-type">
{type}
</span>
</h4>
<input
className="InteractExplorer-func-in-input form-control"
name={name}
value={(inputs[name] && inputs[name].rawData) || ''}
onChange={this.handleInputChange}
/>
</label>
);
})}
{selectedFunction.outputs.map((output, index) => {
const { type, name } = output;
const parsedName = name === '' ? index : name;
return (
<label
key={parsedName}
className="InteractExplorer-func-out form-group"
>
<h4 className="InteractExplorer-func-out-label">
{name}
<span className="InteractExplorer-func-out-label-type">
{type}
</span>
</h4>
<input
className="InteractExplorer-func-out-input form-control"
value={outputs[parsedName] || ''}
disabled={true}
/>
</label>
);
})}
{selectedFunction.constant ? (
<button
className="InteractExplorer-func-submit btn btn-primary"
onClick={this.handleFunctionCall}
>
{translate('CONTRACT_Read')}
</button>
) : walletDecrypted ? (
!txGenerated ? (
<Aux>
<label className="InteractExplorer-field form-group">
<h4 className="InteractExplorer-field-label">Gas Limit</h4>
<input
name="gasLimit"
value={gasLimit}
onChange={handleInput('gasLimit')}
className={classnames(
'InteractExplorer-field-input',
'form-control',
{ 'is-invalid': !validGasLimit }
)}
/>
</label>
<label className="InteractExplorer-field form-group">
<h4 className="InteractExplorer-field-label">Value</h4>
<input
name="value"
value={value}
onChange={handleInput('value')}
placeholder="0"
className={classnames(
'InteractExplorer-field-input',
'form-control',
{ 'is-invalid': !validValue }
)}
/>
</label>
<button
className="InteractExplorer-func-submit btn btn-primary"
disabled={!showContractWrite}
{...addProperties(showContractWrite, {
onClick: handleFunctionSend(selectedFunction, inputs)
})}
>
{translate('CONTRACT_Write')}
</button>
</Aux>
) : (
<Aux>
{txCompare}
<button
className="Deploy-submit btn btn-primary"
onClick={toggleModal}
>
{translate('SEND_trans')}
</button>
</Aux>
)
) : (
<WalletDecrypt />
)}
</div>
)}
{displayModal && txModal}
</div>
);
}
private contractOptions = () => {
const { contractFunctions } = this.props;
return Object.keys(contractFunctions).map(name => {
return (
<option key={name} value={name}>
{name}
</option>
);
});
};
private handleFunctionCall = async (_: any) => {
try {
const { selectedFunction, inputs } = this.state;
const parsedInputs = Object.keys(inputs).reduce(
(accu, key) => ({ ...accu, [key]: inputs[key].parsedData }),
{}
);
const results = await selectedFunction.call(parsedInputs);
this.setState({ outputs: results });
} catch (e) {
this.props.showNotification(
'warning',
`Function call error: ${(e as Error).message}` ||
'Invalid input parameters',
5000
);
}
};
private handleFunctionSelect = (ev: any) => {
const { contractFunctions } = this.props;
const selectedFunctionName = ev.target.value;
const selectedFunction = contractFunctions[selectedFunctionName];
this.setState({
selectedFunction,
selectedFunctionName,
outputs: {},
inputs: {}
});
};
private tryParseJSON(input: string) {
try {
return JSON.parse(input);
} catch {
return input;
}
}
private handleInputChange = (ev: any) => {
const rawValue: string = ev.target.value;
const isArr = rawValue.startsWith('[') && rawValue.endsWith(']');
const value = {
rawData: rawValue,
parsedData: isArr ? this.tryParseJSON(rawValue) : rawValue
};
this.setState({
inputs: {
...this.state.inputs,
[ev.target.name]: value
}
});
};
}
const Aux = ({ children }) => children;

View File

@ -0,0 +1,14 @@
.InteractForm {
&-address {
display: flex;
> * {
flex: 1;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
}

View File

@ -0,0 +1,158 @@
import React, { Component } from 'react';
import translate from 'translations';
import './InteractForm.scss';
import { NetworkContract } from 'config/data';
import { getNetworkContracts } from 'selectors/config';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { isValidETHAddress, isValidAbiJson } from 'libs/validators';
import { addProperties } from 'utils/helpers';
import classnames from 'classnames';
interface Props {
contracts: NetworkContract[];
accessContract(abiJson: string, address: string): (ev) => void;
resetState(): void;
}
interface State {
address: string;
abiJson: string;
}
class InteractForm extends Component<Props, State> {
public state = {
address: '',
abiJson: ''
};
private abiJsonPlaceholder = '[{ "type":"contructor", "inputs":\
[{ "name":"param1","type":"uint256", "indexed":true }],\
"name":"Event" }, { "type":"function", "inputs": [{"nam\
e":"a", "type":"uint256"}], "name":"foo", "outputs": [] }]';
public render() {
const { contracts, accessContract } = this.props;
const { address, abiJson } = this.state;
const validEthAddress = isValidETHAddress(address);
const validAbiJson = isValidAbiJson(abiJson);
const showContractAccessButton = validEthAddress && validAbiJson;
let contractOptions;
if (contracts && contracts.length) {
contractOptions = [
{
name: 'Select a contract...',
value: null
}
];
contractOptions = contractOptions.concat(
contracts.map(contract => {
return {
name: `${contract.name} (${contract.address.substr(0, 10)}...)`,
value: contract.address
};
})
);
} else {
contractOptions = [
{
name: 'No contracts available',
value: null
}
];
}
// TODO: Use common components for address, abi json
return (
<div className="InteractForm">
<div className="InteractForm-address">
<label className="InteractForm-address-field form-group">
<h4>{translate('CONTRACT_Title')}</h4>
<input
placeholder="mewtopia.eth or 0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8"
name="contract_address"
autoComplete="off"
value={address}
className={classnames(
'InteractForm-address-field-input',
'form-control',
{ 'is-invalid': !validEthAddress }
)}
onChange={this.handleInput('address')}
/>
</label>
<label className="InteractForm-address-contract form-group">
<h4>{translate('CONTRACT_Title_2')}</h4>
<select
className="InteractForm-address-field-input form-control"
onChange={this.handleSelectContract}
disabled={!contracts || !contracts.length}
>
{contractOptions.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.name}
</option>
))}
</select>
</label>
</div>
<div className="InteractForm-interface">
<label className="InteractForm-interface-field form-group">
<h4 className="InteractForm-interface-field-label">
{translate('CONTRACT_Json')}
</h4>
<textarea
placeholder={this.abiJsonPlaceholder}
name="abiJson"
className={classnames(
'InteractForm-interface-field-input',
'form-control',
{ 'is-invalid': !validAbiJson }
)}
onChange={this.handleInput('abiJson')}
value={abiJson}
rows={6}
/>
</label>
</div>
<button
className="InteractForm-submit btn btn-primary"
disabled={!showContractAccessButton}
{...addProperties(showContractAccessButton, {
onClick: accessContract(abiJson, address)
})}
>
{translate('x_Access')}
</button>
</div>
);
}
private handleInput = name => (ev: any) => {
this.props.resetState();
this.setState({ [name]: ev.target.value });
};
private handleSelectContract = (ev: any) => {
this.props.resetState();
const addr = ev.target.value;
const contract = this.props.contracts.reduce((prev, currContract) => {
return currContract.address === addr ? currContract : prev;
});
this.setState({
address: contract.address,
abiJson: contract.abi
});
};
}
const mapStateToProps = (state: AppState) => ({
contracts: getNetworkContracts(state)
});
export default connect(mapStateToProps)(InteractForm);

View File

@ -0,0 +1,205 @@
import React, { Component } from 'react';
import InteractForm from './components/InteractForm';
import InteractExplorer from './components//InteractExplorer';
import Contract from 'libs/contracts';
import { withTx, IWithTx } from '../withTx';
import {
TxModal,
Props as DMProps,
TTxModal
} from 'containers/Tabs/Contracts/components/TxModal';
import { IUserSendParams } from 'libs/contracts/ABIFunction';
import Big from 'bignumber.js';
import {
TxCompare,
Props as TCProps,
TTxCompare
} from 'containers/Tabs/Contracts/components/TxCompare';
interface State {
currentContract: Contract | null;
showExplorer: boolean;
address: string | null;
signedTx: string | null;
rawTx: any | null;
gasLimit: string;
value: string;
displayModal: boolean;
}
class Interact extends Component<IWithTx, State> {
public initialState: State = {
currentContract: null,
showExplorer: false,
address: null,
signedTx: null,
rawTx: null,
gasLimit: '30000',
value: '0',
displayModal: false
};
public state: State = this.initialState;
public componentWillReceiveProps(nextProps: IWithTx) {
if (nextProps.wallet && this.state.currentContract) {
Contract.setConfigForTx(this.state.currentContract, nextProps);
}
}
public accessContract = (contractAbi: string, address: string) => () => {
try {
const parsedAbi = JSON.parse(contractAbi);
const contractInstance = new Contract(parsedAbi);
contractInstance.at(address);
contractInstance.setNode(this.props.nodeLib);
this.setState({
currentContract: contractInstance,
showExplorer: true,
address
});
} catch (e) {
this.props.showNotification(
'danger',
`Contract Access Error: ${(e as Error).message ||
'Can not parse contract'}`
);
this.resetState();
}
};
public render() {
const {
showExplorer,
currentContract,
gasLimit,
value,
signedTx,
displayModal
} = this.state;
const { wallet, showNotification } = this.props;
const txGenerated = !!signedTx;
return (
<div className="Interact">
<InteractForm
accessContract={this.accessContract}
resetState={this.resetState}
/>
<hr />
{showExplorer &&
currentContract && (
<InteractExplorer
{...{
address: currentContract.address,
walletDecrypted: !!wallet,
handleInput: this.handleInput,
contractFunctions: Contract.getFunctions(currentContract),
gasLimit,
value,
handleFunctionSend: this.handleFunctionSend,
txGenerated,
txModal: txGenerated ? this.makeModal() : null,
txCompare: txGenerated ? this.makeCompareTx() : null,
toggleModal: this.toggleModal,
displayModal,
showNotification
}}
/>
)}
</div>
);
}
private makeCompareTx = (): React.ReactElement<TTxCompare> => {
const { nonce, gasLimit, data, value, to, gasPrice } = this.state.rawTx;
const { signedTx } = this.state;
const { 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 <TxCompare {...props} />;
};
private makeModal = (): React.ReactElement<TTxModal> => {
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: 'send a contract state modifying transaction',
networkName,
network,
service,
handleBroadcastTx: this.handleBroadcastTx,
onClose: this.resetState
};
return <TxModal {...props} />;
};
private toggleModal = () => this.setState({ displayModal: true });
private resetState = () => this.setState(this.initialState);
private handleBroadcastTx = () => {
const { signedTx } = this.state;
if (!signedTx) {
return null;
}
this.props.broadcastTx(signedTx);
this.resetState();
};
private handleFunctionSend = (selectedFunction, inputs) => async () => {
try {
const { address, gasLimit, value } = this.state;
if (!address) {
return null;
}
const parsedInputs = Object.keys(inputs).reduce(
(accu, key) => ({ ...accu, [key]: inputs[key].parsedData }),
{}
);
const userInputs: IUserSendParams = {
input: parsedInputs,
to: address,
gasLimit: new Big(gasLimit),
value
};
const { signedTx, rawTx } = await selectedFunction.send(userInputs);
this.setState({ signedTx, rawTx });
} catch (e) {
this.props.showNotification(
'danger',
`Function send error: ${(e as Error).message}` ||
'Invalid input parameters',
5000
);
}
};
private handleInput = name => ev =>
this.setState({ [name]: ev.target.value });
}
export default withTx(Interact);

View File

@ -0,0 +1,47 @@
import React from 'react';
import { Wei } from 'libs/units';
import translate from 'translations';
import Code from 'components/ui/Code';
export interface Props {
nonce: string;
gasPrice: Wei;
gasLimit: string;
to: string;
value: string;
data: string;
chainId: number;
signedTx: string;
}
export const TxCompare = (props: Props) => {
if (!props.signedTx) {
return null;
}
const { signedTx, ...rawTx } = props;
const Left = () => (
<div className="form-group">
<h4>{translate('SEND_raw')}</h4>
<Code>
{JSON.stringify(
{ ...rawTx, gasPrice: rawTx.gasPrice.toString(16) },
null,
2
)}
</Code>
</div>
);
const Right = () => (
<div className="form-group">
<h4> {translate('SEND_signed')} </h4>
<Code>{signedTx}</Code>
</div>
);
return (
<section>
<Left />
<Right />
</section>
);
};
export type TTxCompare = typeof TxCompare;

View File

@ -0,0 +1,65 @@
import React from 'react';
import translate from 'translations';
import Modal, { IButton } from 'components/ui/Modal';
export interface Props {
networkName: string;
network: string;
service: string;
action: string;
handleBroadcastTx(): void;
onClose(): void;
}
export type TTxModal = typeof TxModal;
export const TxModal = (props: Props) => {
const {
networkName,
network,
service,
handleBroadcastTx,
onClose,
action
} = props;
const buttons: IButton[] = [
{
text: translate('SENDModal_Yes', true) as string,
type: 'primary',
onClick: handleBroadcastTx
},
{
text: translate('SENDModal_No', true) as string,
type: 'default',
onClick: onClose
}
];
return (
<Modal
title="Confirm Your Transaction"
buttons={buttons}
handleClose={onClose}
isOpen={true}
>
<div className="modal-body">
<h2 className="modal-title text-danger">
{translate('SENDModal_Title')}
</h2>
<p>
You are about to <strong>{action}</strong> on the{' '}
<strong>{networkName}</strong> chain.
</p>
<p>
The <strong>{network}</strong> node you are sending through is
provided by <strong>{service}</strong>.
</p>
<h4>{translate('SENDModal_Content_3')}</h4>
</div>
</Modal>
);
};

View File

@ -0,0 +1,37 @@
import * as configSelectors from 'selectors/config';
import { AppState } from 'reducers';
import { GWei, Wei, Ether } from 'libs/units';
import { connect } from 'react-redux';
import { showNotification, TShowNotification } from 'actions/notifications';
import { broadcastTx, TBroadcastTx } from 'actions/wallet';
import { IWallet } from 'libs/wallet/IWallet';
import { RPCNode } from 'libs/nodes';
import { NodeConfig, NetworkConfig } from 'config/data';
export interface IWithTx {
wallet: IWallet;
balance: Ether;
node: NodeConfig;
nodeLib: RPCNode;
chainId: NetworkConfig['chainId'];
networkName: NetworkConfig['name'];
gasPrice: Wei;
broadcastTx: TBroadcastTx;
showNotification: TShowNotification;
}
const mapStateToProps = (state: AppState) => ({
wallet: state.wallet.inst,
balance: state.wallet.balance,
node: configSelectors.getNodeConfig(state),
nodeLib: configSelectors.getNodeLib(state),
chainId: configSelectors.getNetworkConfig(state).chainId,
networkName: configSelectors.getNetworkConfig(state).name,
gasPrice: new GWei(configSelectors.getGasPriceGwei(state)).toWei()
});
export const withTx = passedComponent =>
connect(mapStateToProps, {
showNotification,
broadcastTx
})(passedComponent);

View File

@ -0,0 +1,30 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
.Contracts {
&-header {
margin: 0;
text-align: center;
&-tab {
@include reset-button;
color: $ether-blue;
&:hover,
&:active {
opacity: 0.8;
}
&.is-active {
&,
&:hover,
&:active {
color: $text-color;
cursor: default;
opacity: 1;
font-weight: 500;
}
}
}
}
}

View File

@ -0,0 +1,61 @@
import React, { Component } from 'react';
import translate from 'translations';
import Interact from './components/Interact';
import Deploy from './components/Deploy';
import './index.scss';
import TabSection from 'containers/TabSection';
interface State {
activeTab: string;
}
export default class Contracts extends Component<{}, State> {
public state: State = {
activeTab: 'interact'
};
public changeTab = activeTab => () => this.setState({ activeTab });
public render() {
const { activeTab } = this.state;
let content;
let interactActive = '';
let deployActive = '';
if (activeTab === 'interact') {
content = <Interact />;
interactActive = 'is-active';
} else {
content = <Deploy />;
deployActive = 'is-active';
}
return (
<TabSection>
<section className="Tab-content Contracts">
<div className="Tab-content-pane">
<h1 className="Contracts-header">
<button
className={`Contracts-header-tab ${interactActive}`}
onClick={this.changeTab('interact')}
>
{translate('NAV_InteractContract')}
</button>{' '}
<span>or</span>{' '}
<button
className={`Contracts-header-tab ${deployActive}`}
onClick={this.changeTab('deploy')}
>
{translate('NAV_DeployContract')}
</button>
</h1>
</div>
<main className="Tab-content-pane" role="main">
<div className="Contracts-content">{content}</div>
</main>
</section>
</TabSection>
);
}
}

View File

@ -601,6 +601,7 @@ export class SendTransaction extends React.Component<Props, State> {
bigGasLimit,
chainId,
transactionInput,
false,
nonce,
offline
);

View File

@ -1,12 +1,12 @@
// Application styles must come first in order, to allow for overrides
import 'assets/styles/etherwallet-master.less';
import 'font-awesome/scss/font-awesome.scss';
import 'sass/styles.scss';
import React from 'react';
import { render } from 'react-dom';
import Root from './Root';
import createHistory from 'history/createBrowserHistory';
import { configuredStore } from './store';
import 'sass/styles.scss';
const history = createHistory();

View File

@ -77,7 +77,8 @@ EncodedCall:${data}`);
userInputs.gasPrice,
gasLimit,
chainId,
transactionInput
transactionInput,
false
);
return { signedTx, rawTx: JSON.parse(rawTx) };
};

View File

@ -129,10 +129,11 @@ function generateTxValidation(
token: Token | null | undefined,
data: string,
gasLimit: BigNumber | string,
gasPrice: Wei | string
gasPrice: Wei | string,
skipEthAddressValidation: boolean
) {
// Reject bad addresses
if (!isValidETHAddress(to)) {
if (!isValidETHAddress(to) && !skipEthAddressValidation) {
throw new Error(translateRaw('ERROR_5'));
}
// Reject token transactions without data
@ -166,11 +167,12 @@ export async function generateCompleteTransactionFromRawTransaction(
tx: ExtendedRawTransaction,
wallet: IWallet,
token: Token | null | undefined,
skipValidation: boolean,
offline?: boolean
): Promise<CompleteTransaction> {
const { to, data, gasLimit, gasPrice, chainId, nonce } = tx;
// validation
generateTxValidation(to, token, data, gasLimit, gasPrice);
generateTxValidation(to, token, data, gasLimit, gasPrice, skipValidation);
// duplicated from generateTxValidation -- typescript bug
if (typeof gasLimit === 'string' || typeof gasPrice === 'string') {
throw Error('Gas Limit and Gas Price should be of type bignumber');
@ -240,6 +242,7 @@ export async function generateCompleteTransaction(
gasLimit: BigNumber,
chainId: number,
transactionInput: TransactionInput,
skipValidation: boolean,
nonce?: number | null,
offline?: boolean
): Promise<CompleteTransaction> {
@ -263,6 +266,7 @@ export async function generateCompleteTransaction(
transaction,
wallet,
token,
skipValidation,
offline
);
}

View File

@ -28,7 +28,7 @@ export function isValidHex(str: string): boolean {
str.substring(0, 2) === '0x'
? str.substring(2).toUpperCase()
: str.toUpperCase();
const re = /^[0-9A-F]+$/g;
const re = /^[0-9A-F]*$/g; // Match 0 -> unlimited times, 0 being "0x" case
return re.test(str);
}
@ -150,9 +150,6 @@ export function isValidRawTx(rawTx: RawTransaction): boolean {
}
}
if (!isValidETHAddress(rawTx.to)) {
return false;
}
if (Object.keys(rawTx).length !== propReqs.length) {
return false;
}
@ -168,3 +165,15 @@ export function isValidPath(dPath: string) {
const len = dPath.split("'/").length;
return len === 3 || len === 4;
}
export const isValidValue = (value: string) =>
!!(value && isFinite(parseFloat(value)) && parseFloat(value) >= 0);
export const isValidGasPrice = (gasLimit: string) =>
!!(gasLimit && isFinite(parseFloat(gasLimit)) && parseFloat(gasLimit) > 0);
export const isValidByteCode = (byteCode: string) =>
byteCode && byteCode.length > 0 && byteCode.length % 2 === 0;
export const isValidAbiJson = (abiJson: string) =>
abiJson && abiJson.startsWith('[') && abiJson.endsWith(']');

View File

@ -1,39 +0,0 @@
import {
AccessContractAction,
SetInteractiveContractAction
} from 'actions/contracts';
import { TypeKeys } from 'actions/contracts/constants';
export interface State {
selectedAddress?: string | null;
selectedABIJson?: string | null;
selectedABIFunctions?: any[] | null;
}
export const initialState: State = {
// Interact
selectedAddress: null,
selectedABIJson: null,
selectedABIFunctions: null
};
type Action = AccessContractAction | SetInteractiveContractAction;
export function contracts(state: State = initialState, action: Action) {
switch (action.type) {
case TypeKeys.ACCESS_CONTRACT:
return {
...state,
selectedAddress: action.address,
selectedABIJson: action.abiJson
};
case TypeKeys.SET_INTERACTIVE_CONTRACT:
return {
...state,
selectedABIFunctions: action.functions
};
default:
return state;
}
}

View File

@ -2,7 +2,6 @@ import { routerReducer } from 'react-router-redux';
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { config, State as ConfigState } from './config';
import { contracts, State as ContractsState } from './contracts';
import { customTokens, State as CustomTokensState } from './customTokens';
import {
deterministicWallets,
@ -23,7 +22,6 @@ export interface AppState {
wallet: WalletState;
customTokens: CustomTokensState;
rates: RatesState;
contracts: ContractsState;
deterministicWallets: DeterministicWalletsState;
// Third party reducers (TODO: Fill these out)
form: any;
@ -40,7 +38,6 @@ export default combineReducers({
wallet,
customTokens,
rates,
contracts,
deterministicWallets,
form: formReducer,
routing: routerReducer

View File

@ -1,39 +0,0 @@
import {
AccessContractAction,
setInteractiveContract
} from 'actions/contracts';
import { showNotification } from 'actions/notifications';
import { isValidETHAddress } from 'libs/validators';
import { SagaIterator } from 'redux-saga';
import { put, takeEvery } from 'redux-saga/effects';
import translate from 'translations';
function* handleAccessContract(action: AccessContractAction): SagaIterator {
const contractFunctions: any[] = [];
if (!action.address || !isValidETHAddress(action.address)) {
yield put(showNotification('danger', translate('ERROR_5'), 5000));
return;
}
try {
const abi = JSON.parse(action.abiJson);
if (abi.constructor !== Array) {
throw new Error('ABI JSON was not an array!');
}
abi.forEach(instruction => {
if (instruction.type === 'function') {
contractFunctions.push(instruction);
}
});
yield put(setInteractiveContract(contractFunctions));
} catch (err) {
yield put(showNotification('danger', translate('ERROR_26'), 5000));
}
}
export default function* contractsSaga(): SagaIterator {
yield takeEvery('ACCESS_CONTRACT', handleAccessContract);
}

View File

@ -1,5 +1,4 @@
import configSaga from './config';
import contracts from './contracts';
import deterministicWallets from './deterministicWallets';
import notifications from './notifications';
import {
@ -16,7 +15,6 @@ export default {
postBityOrderSaga,
pollBityOrderStatusSaga,
getBityRatesSaga,
contracts,
notifications,
wallet,
deterministicWallets

View File

@ -2,6 +2,13 @@ export function getKeyByValue(object, value) {
return Object.keys(object).find(key => object[key] === value);
}
interface IKeyedObj {
[key: string]: any;
}
export const addProperties = (
truthy,
propertiesToAdd: IKeyedObj
): {} | IKeyedObj => (truthy ? propertiesToAdd : {});
export function getParam(query: { [key: string]: string }, key: string) {
const keys = Object.keys(query);
const index = keys.findIndex(k => k.toLowerCase() === key.toLowerCase());