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:
parent
2f8e0fe272
commit
efccac79ad
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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;
|
|
@ -1,4 +0,0 @@
|
|||
export enum TypeKeys {
|
||||
ACCESS_CONTRACT = 'ACCESS_CONTRACT',
|
||||
SET_INTERACTIVE_CONTRACT = 'SET_INTERACTIVE_CONTRACT'
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export * from './constants';
|
||||
export * from './actionTypes';
|
||||
export * from './actionCreators';
|
|
@ -21,6 +21,10 @@ const tabs = [
|
|||
name: 'NAV_ViewWallet'
|
||||
// to: 'view-wallet'
|
||||
},
|
||||
{
|
||||
name: 'NAV_Contracts',
|
||||
to: 'contracts'
|
||||
},
|
||||
{
|
||||
name: 'NAV_ENS',
|
||||
to: 'ens'
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import React, { Component } from 'react';
|
||||
import './Code.scss';
|
||||
|
||||
const Code = ({ children }) => (
|
||||
<pre>
|
||||
<code>{children}</code>
|
||||
</pre>
|
||||
);
|
||||
|
||||
export default Code;
|
|
@ -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);
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,14 @@
|
|||
.InteractForm {
|
||||
&-address {
|
||||
display: flex;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -601,6 +601,7 @@ export class SendTransaction extends React.Component<Props, State> {
|
|||
bigGasLimit,
|
||||
chainId,
|
||||
transactionInput,
|
||||
false,
|
||||
nonce,
|
||||
offline
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -77,7 +77,8 @@ EncodedCall:${data}`);
|
|||
userInputs.gasPrice,
|
||||
gasLimit,
|
||||
chainId,
|
||||
transactionInput
|
||||
transactionInput,
|
||||
false
|
||||
);
|
||||
return { signedTx, rawTx: JSON.parse(rawTx) };
|
||||
};
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(']');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in New Issue