Contracts Tab Scaffolding (#70)
* Empty component, routes setup. * Shared components for all Contracts inputs. Dont do anything yet. * Check in reducer work so far. Still WIP. * Header styling * Check in input work so far, splitting to new branch. * Strip down contracts inputs. Split out into form and explorer * Contract selector * Constantized config actions to use in contract saga. * Interact explorer UI, no functionality * Convert to constants, hook up errors * Deploy and style cleanup. * Remove unnecessary class. * Fix flow errors with css modules * Attempt at fixing all newly introduced flow errors in the contracts branch. * Removed unused validator. * Remove action constants, fix flow specificity in reducers * Fix unit tests * Move network contracts out of redux / sagas, and read directly from state with a selector in mapStateToProps. * Fix initialState -> INITIAL_STATE rename * foreach push -> concat
This commit is contained in:
parent
96405157f0
commit
e3505fd958
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
|
||||
/***** Access Contract *****/
|
||||
export type AccessContractAction = {
|
||||
type: 'ACCESS_CONTRACT',
|
||||
address: string,
|
||||
abiJson: string
|
||||
};
|
||||
|
||||
export function accessContract(
|
||||
address: string,
|
||||
abiJson: string
|
||||
): AccessContractAction {
|
||||
return {
|
||||
type: 'ACCESS_CONTRACT',
|
||||
address,
|
||||
abiJson
|
||||
};
|
||||
}
|
||||
|
||||
/***** Set Interactive Contract *****/
|
||||
export type ABIFunctionField = {
|
||||
name: string,
|
||||
type: string
|
||||
};
|
||||
|
||||
export type ABIFunction = {
|
||||
name: string,
|
||||
type: string,
|
||||
constant: boolean,
|
||||
inputs: Array<ABIFunctionField>,
|
||||
outputs: Array<ABIFunctionField>
|
||||
};
|
||||
|
||||
export type SetInteractiveContractAction = {
|
||||
type: 'SET_INTERACTIVE_CONTRACT',
|
||||
functions: Array<ABIFunction>
|
||||
};
|
||||
|
||||
export function setInteractiveContract(
|
||||
functions: Array<ABIFunction>
|
||||
): SetInteractiveContractAction {
|
||||
return {
|
||||
type: 'SET_INTERACTIVE_CONTRACT',
|
||||
functions
|
||||
};
|
||||
}
|
|
@ -22,7 +22,8 @@ const tabs = [
|
|||
name: 'NAV_Offline'
|
||||
},
|
||||
{
|
||||
name: 'NAV_Contracts'
|
||||
name: 'NAV_Contracts',
|
||||
link: 'contracts'
|
||||
},
|
||||
{
|
||||
name: 'NAV_ViewWallet',
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,11 @@
|
|||
import ETC from './ETC.json';
|
||||
import ETH from './ETH.json';
|
||||
import Rinkeby from './Rinkeby.json';
|
||||
import Ropsten from './Ropsten.json';
|
||||
|
||||
export default {
|
||||
ETC,
|
||||
ETH,
|
||||
Rinkeby,
|
||||
Ropsten
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"name": "ENS - Eth Registrar (Auction)",
|
||||
"address": "0x21397c1a1f4acd9132fe36df011610564b87e24b",
|
||||
"abi": "[{\"constant\":true,\"inputs\":[],\"name\":\"ens\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"expiryTimes\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"subnode\",\"type\":\"bytes32\"},{\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"register\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"rootNode\",\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\"}],\"payable\":false,\"type\":\"function\"},{\"inputs\":[{\"name\":\"ensAddr\",\"type\":\"address\"},{\"name\":\"node\",\"type\":\"bytes32\"}],\"type\":\"constructor\"}]"
|
||||
},
|
||||
{
|
||||
"name": "ENS - Registry",
|
||||
"address": "0xe7410170f87102df0055eb195163a03b7f2bff4a",
|
||||
"abi": "[{\"constant\":true,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"}],\"name\":\"resolver\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"}],\"name\":\"owner\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"},{\"name\":\"label\",\"type\":\"bytes32\"},{\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"setSubnodeOwner\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"},{\"name\":\"ttl\",\"type\":\"uint64\"}],\"name\":\"setTTL\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"}],\"name\":\"ttl\",\"outputs\":[{\"name\":\"\",\"type\":\"uint64\"}],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"},{\"name\":\"resolver\",\"type\":\"address\"}],\"name\":\"setResolver\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"node\",\"type\":\"bytes32\"},{\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"setOwner\",\"outputs\":[],\"payable\":false,\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"node\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"node\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"label\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"NewOwner\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"node\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"resolver\",\"type\":\"address\"}],\"name\":\"NewResolver\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"node\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"ttl\",\"type\":\"uint64\"}],\"name\":\"NewTTL\",\"type\":\"event\"}]"
|
||||
}
|
||||
]
|
File diff suppressed because one or more lines are too long
|
@ -117,6 +117,12 @@ export type Token = {
|
|||
decimal: number
|
||||
};
|
||||
|
||||
export type NetworkContract = {
|
||||
name: string,
|
||||
address: string,
|
||||
abi: string
|
||||
};
|
||||
|
||||
export type NetworkConfig = {
|
||||
name: string,
|
||||
// unit: string,
|
||||
|
@ -130,7 +136,8 @@ export type NetworkConfig = {
|
|||
address: string
|
||||
},
|
||||
chainId: number,
|
||||
tokens: Token[]
|
||||
tokens: Token[],
|
||||
contracts: ?Array<NetworkContract>
|
||||
};
|
||||
|
||||
export const NETWORKS: { [key: string]: NetworkConfig } = {
|
||||
|
@ -147,8 +154,8 @@ export const NETWORKS: { [key: string]: NetworkConfig } = {
|
|||
name: 'Ethplorer.io',
|
||||
address: 'https://ethplorer.io/address/[[address]]'
|
||||
},
|
||||
tokens: require('./tokens/eth').default
|
||||
// 'abiList': require('./abiDefinitions/ethAbi.json'),
|
||||
tokens: require('./tokens/eth').default,
|
||||
contracts: require('./contracts/eth.json')
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
|
||||
type Props = {};
|
||||
|
||||
export default class Deploy extends Component {
|
||||
props: Props;
|
||||
|
||||
state = {
|
||||
byteCode: '',
|
||||
gasLimit: ''
|
||||
};
|
||||
|
||||
_handleInput = (ev: SyntheticInputEvent) => {
|
||||
this.setState({ [ev.target.name]: ev.target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { byteCode, gasLimit } = this.state;
|
||||
|
||||
// TODO: Use common components for byte code / gas price
|
||||
return (
|
||||
<div className="Deploy">
|
||||
<label className="Deploy-field form-group">
|
||||
<h4 className="Deploy-field-label">
|
||||
{translate('CONTRACT_ByteCode')}
|
||||
</h4>
|
||||
<textarea
|
||||
name="byteCode"
|
||||
placeholder="0x8f87a973e..."
|
||||
rows={6}
|
||||
onChange={this._handleInput}
|
||||
className="Deploy-field-input form-control"
|
||||
value={byteCode}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="Deploy-field form-group">
|
||||
<h4 className="Deploy-field-label">
|
||||
{translate('CONTRACT_ByteCode')}
|
||||
</h4>
|
||||
<input
|
||||
name="gasLimit"
|
||||
value={gasLimit}
|
||||
onChange={this._handleInput}
|
||||
className="Deploy-field-input form-control"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button className="Deploy-submit btn btn-primary">Implement Me</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import InteractForm from './InteractForm';
|
||||
import InteractExplorer from './InteractExplorer';
|
||||
import type { ABIFunction } from 'actions/contracts';
|
||||
import type { NetworkContract } from 'config/data';
|
||||
|
||||
type Props = {
|
||||
NetworkContracts: Array<NetworkContract>,
|
||||
selectedAddress: ?string,
|
||||
selectedABIJson: ?string,
|
||||
selectedABIFunctions: ?Array<ABIFunction>,
|
||||
accessContract: Function
|
||||
};
|
||||
|
||||
export default class Interact extends Component {
|
||||
props: Props;
|
||||
|
||||
render() {
|
||||
const {
|
||||
NetworkContracts,
|
||||
selectedAddress,
|
||||
selectedABIJson,
|
||||
selectedABIFunctions,
|
||||
accessContract
|
||||
} = this.props;
|
||||
|
||||
// TODO: Use common components for address, abi json
|
||||
return (
|
||||
<div className="Interact">
|
||||
<InteractForm
|
||||
contracts={NetworkContracts}
|
||||
address={selectedAddress}
|
||||
abiJson={selectedABIJson}
|
||||
accessContract={accessContract}
|
||||
/>
|
||||
<hr />
|
||||
<InteractExplorer
|
||||
address={selectedAddress}
|
||||
functions={selectedABIFunctions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import type { ABIFunction } from 'actions/contracts';
|
||||
import './InteractExplorer.scss';
|
||||
|
||||
type Props = {
|
||||
address: ?string,
|
||||
functions: ?Array<ABIFunction>
|
||||
};
|
||||
|
||||
type State = {
|
||||
selectedFunction: ?ABIFunction,
|
||||
inputs: Object,
|
||||
outputs: Object
|
||||
};
|
||||
|
||||
export default class InteractExplorer extends Component {
|
||||
props: Props;
|
||||
|
||||
state: State = {
|
||||
selectedFunction: null,
|
||||
inputs: {},
|
||||
outputs: {}
|
||||
};
|
||||
|
||||
_handleFunctionSelect = (ev: SyntheticInputEvent) => {
|
||||
const { functions } = this.props;
|
||||
|
||||
if (!functions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedFunction = functions.reduce((prev, fn) => {
|
||||
return ev.target.value === fn.name ? fn : prev;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
selectedFunction,
|
||||
inputs: {}
|
||||
});
|
||||
};
|
||||
|
||||
_handleInputChange = (ev: SyntheticInputEvent) => {
|
||||
this.setState({
|
||||
inputs: {
|
||||
...this.state.inputs,
|
||||
[ev.target.name]: ev.target.value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { address, functions } = this.props;
|
||||
const { selectedFunction, inputs, outputs } = this.state;
|
||||
|
||||
if (!functions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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')}
|
||||
</option>
|
||||
{functions.map(fn =>
|
||||
<option key={fn.name} value={fn.name}>
|
||||
{fn.name}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
|
||||
{selectedFunction &&
|
||||
<div key={selectedFunction.name} className="InteractExplorer-func">
|
||||
{/* TODO: Use reusable components with validation */}
|
||||
{selectedFunction.inputs.map(input =>
|
||||
<label
|
||||
key={input.name}
|
||||
className="InteractExplorer-func-in form-group"
|
||||
>
|
||||
<h4 className="InteractExplorer-func-in-label">
|
||||
{input.name}
|
||||
<span className="InteractExplorer-func-in-label-type">
|
||||
{input.type}
|
||||
</span>
|
||||
</h4>
|
||||
<input
|
||||
className="InteractExplorer-func-in-input form-control"
|
||||
name={input.name}
|
||||
value={inputs[input.name]}
|
||||
onChange={this._handleInputChange}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{selectedFunction.outputs.map(output =>
|
||||
<label
|
||||
key={output.name}
|
||||
className="InteractExplorer-func-out form-group"
|
||||
>
|
||||
<h4 className="InteractExplorer-func-out-label">
|
||||
↳ {output.name}
|
||||
<span className="InteractExplorer-func-out-label-type">
|
||||
{output.type}
|
||||
</span>
|
||||
</h4>
|
||||
<input
|
||||
className="InteractExplorer-func-out-input form-control"
|
||||
value={outputs[output.name]}
|
||||
disabled
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{selectedFunction.constant
|
||||
? <button className="InteractExplorer-func-submit btn btn-primary">
|
||||
{/* translate('CONTRACT_Read') */}
|
||||
Implement Me
|
||||
</button>
|
||||
: <button className="InteractExplorer-func-submit btn btn-primary">
|
||||
{/* translate('CONTRACT_Write') */}
|
||||
Implement Me
|
||||
</button>}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,130 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import './InteractForm.scss';
|
||||
import type { NetworkContract } from 'config/data';
|
||||
|
||||
type Props = {
|
||||
contracts: Array<NetworkContract>,
|
||||
address: ?string,
|
||||
abiJson: ?string,
|
||||
accessContract: Function
|
||||
};
|
||||
|
||||
export default class InteractForm extends Component {
|
||||
props: Props;
|
||||
|
||||
state = {
|
||||
address: '',
|
||||
abiJson: ''
|
||||
};
|
||||
|
||||
_handleInput = (ev: SyntheticInputEvent) => {
|
||||
this.setState({ [ev.target.name]: ev.target.value });
|
||||
};
|
||||
|
||||
_handleSelectContract = (ev: SyntheticInputEvent) => {
|
||||
const addr = ev.target.value;
|
||||
const contract = this.props.contracts.reduce((prev, contract) => {
|
||||
return contract.address === addr ? contract : prev;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
address: contract.address,
|
||||
abiJson: contract.abi
|
||||
});
|
||||
};
|
||||
|
||||
_accessContract = () => {
|
||||
this.props.accessContract(this.state.address, this.state.abiJson);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { contracts } = this.props;
|
||||
const { address, abiJson } = this.state;
|
||||
|
||||
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_2')}
|
||||
</h4>
|
||||
<input
|
||||
name="address"
|
||||
value={address}
|
||||
className="InteractForm-address-field-input form-control"
|
||||
onChange={this._handleInput}
|
||||
/>
|
||||
</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
|
||||
name="abiJson"
|
||||
className="InteractForm-interface-field-input form-control"
|
||||
onChange={this._handleInput}
|
||||
value={abiJson}
|
||||
rows={6}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="InteractForm-submit btn btn-primary"
|
||||
onClick={this._accessContract}
|
||||
>
|
||||
{translate('x_Access')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
.InteractForm {
|
||||
&-address {
|
||||
display: flex;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { accessContract } from 'actions/contracts';
|
||||
import type { ABIFunction } from 'actions/contracts';
|
||||
import { getNetworkContracts } from 'selectors/config';
|
||||
import type { NetworkContract } from 'config/data';
|
||||
import { State } from 'reducers/contracts';
|
||||
import translate from 'translations';
|
||||
import Interact from './components/Interact';
|
||||
import Deploy from './components/Deploy';
|
||||
import './index.scss';
|
||||
|
||||
type Props = {
|
||||
NetworkContracts: Array<NetworkContract>,
|
||||
selectedAddress: ?string,
|
||||
selectedABIJson: ?string,
|
||||
selectedABIFunctions: ?Array<ABIFunction>,
|
||||
accessContract: Function
|
||||
};
|
||||
|
||||
class Contracts extends Component {
|
||||
props: Props;
|
||||
|
||||
state = {
|
||||
activeTab: 'interact'
|
||||
};
|
||||
|
||||
changeTab(activeTab) {
|
||||
this.setState({ activeTab });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
NetworkContracts,
|
||||
selectedAddress,
|
||||
selectedABIJson,
|
||||
selectedABIFunctions,
|
||||
accessContract
|
||||
} = this.props;
|
||||
const { activeTab } = this.state;
|
||||
let content = '';
|
||||
let interactActive = '';
|
||||
let deployActive = '';
|
||||
|
||||
if (activeTab === 'interact') {
|
||||
content = (
|
||||
<Interact
|
||||
NetworkContracts={NetworkContracts}
|
||||
selectedAddress={selectedAddress}
|
||||
selectedABIJson={selectedABIJson}
|
||||
selectedABIFunctions={selectedABIFunctions}
|
||||
accessContract={accessContract}
|
||||
/>
|
||||
);
|
||||
interactActive = 'is-active';
|
||||
} else {
|
||||
content = <Deploy />;
|
||||
deployActive = 'is-active';
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="container" style={{ minHeight: '50%' }}>
|
||||
<div className="tab-content">
|
||||
<main className="tab-pane active" role="main">
|
||||
<div className="Contracts">
|
||||
<h1 className="Contracts-header">
|
||||
<button
|
||||
className={`Contracts-header-tab ${interactActive}`}
|
||||
onClick={this.changeTab.bind(this, 'interact')}
|
||||
>
|
||||
{translate('NAV_InteractContract')}
|
||||
</button>{' '}
|
||||
<span>or</span>{' '}
|
||||
<button
|
||||
className={`Contracts-header-tab ${deployActive}`}
|
||||
onClick={this.changeTab.bind(this, 'deploy')}
|
||||
>
|
||||
{translate('NAV_DeployContract')}
|
||||
</button>
|
||||
</h1>
|
||||
|
||||
<div className="Contracts-content">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: State) {
|
||||
return {
|
||||
NetworkContracts: getNetworkContracts(state),
|
||||
selectedAddress: state.contracts.selectedAddress,
|
||||
selectedABIJson: state.contracts.selectedABIJson,
|
||||
selectedABIFunctions: state.contracts.selectedABIFunctions
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { accessContract })(Contracts);
|
|
@ -0,0 +1,30 @@
|
|||
@import "common/sass/variables";
|
||||
@import "common/sass/mixins";
|
||||
|
||||
.Contracts {
|
||||
&-header {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
|
||||
&-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
// Application styles must come first in order, to allow for overrides
|
||||
import 'assets/styles/etherwallet-master.less';
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { syncHistoryWithStore } from 'react-router-redux';
|
||||
|
@ -5,8 +8,6 @@ import { syncHistoryWithStore } from 'react-router-redux';
|
|||
import { Root } from 'components';
|
||||
import { Routing, history } from './routing';
|
||||
import { store } from './store';
|
||||
// application styles
|
||||
import 'assets/styles/etherwallet-master.less';
|
||||
|
||||
const renderRoot = Root => {
|
||||
let syncedHistory = syncHistoryWithStore(history, store);
|
||||
|
|
|
@ -20,9 +20,10 @@ export function isValidHex(str: string): boolean {
|
|||
return false;
|
||||
}
|
||||
if (str === '') return true;
|
||||
str = str.substring(0, 2) == '0x'
|
||||
? str.substring(2).toUpperCase()
|
||||
: str.toUpperCase();
|
||||
str =
|
||||
str.substring(0, 2) == '0x'
|
||||
? str.substring(2).toUpperCase()
|
||||
: str.toUpperCase();
|
||||
var re = /^[0-9A-F]+$/g;
|
||||
return re.test(str);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import type {
|
||||
AccessContractAction,
|
||||
SetInteractiveContractAction
|
||||
} from 'actions/contracts';
|
||||
|
||||
export type State = {
|
||||
selectedAddress: ?string,
|
||||
selectedABIJson: ?string,
|
||||
selectedABIFunctions: ?Array
|
||||
};
|
||||
|
||||
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 'ACCESS_CONTRACT':
|
||||
return {
|
||||
...state,
|
||||
selectedAddress: action.address,
|
||||
selectedABIJson: action.abiJson
|
||||
};
|
||||
|
||||
case 'SET_INTERACTIVE_CONTRACT':
|
||||
return {
|
||||
...state,
|
||||
selectedABIFunctions: action.functions
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -22,6 +22,9 @@ import type { State as CustomTokensState } from './customTokens';
|
|||
import * as rates from './rates';
|
||||
import type { State as RatesState } from './rates';
|
||||
|
||||
import * as contracts from './contracts';
|
||||
import type { State as ContractsState } from './contracts';
|
||||
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
import { combineReducers } from 'redux';
|
||||
import { routerReducer } from 'react-router-redux';
|
||||
|
@ -35,6 +38,7 @@ export type State = {
|
|||
wallet: WalletState,
|
||||
customTokens: CustomTokensState,
|
||||
rates: RatesState,
|
||||
contracts: ContractsState,
|
||||
// Third party reducers (TODO: Fill these out)
|
||||
form: Object,
|
||||
routing: Object
|
||||
|
@ -49,6 +53,7 @@ export default combineReducers({
|
|||
...wallet,
|
||||
...customTokens,
|
||||
...rates,
|
||||
...contracts,
|
||||
form: formReducer,
|
||||
routing: routerReducer
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ import ViewWallet from 'containers/Tabs/ViewWallet';
|
|||
import Help from 'containers/Tabs/Help';
|
||||
import Swap from 'containers/Tabs/Swap';
|
||||
import SendTransaction from 'containers/Tabs/SendTransaction';
|
||||
import Contracts from 'containers/Tabs/Contracts';
|
||||
export const history = getHistory();
|
||||
|
||||
export const Routing = () =>
|
||||
|
@ -16,6 +17,7 @@ export const Routing = () =>
|
|||
<Route name="Help" path="/help" component={Help} />
|
||||
<Route name="Swap" path="/swap" component={Swap} />
|
||||
<Route name="Send" path="/send-transaction" component={SendTransaction} />
|
||||
<Route name="Contracts" path="/contracts" component={Contracts} />
|
||||
|
||||
<Redirect from="/*" to="/" />
|
||||
</Route>;
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { takeEvery, put } from 'redux-saga/effects';
|
||||
import type { Effect } from 'redux-saga/effects';
|
||||
import translate from 'translations';
|
||||
|
||||
import {
|
||||
AccessContractAction,
|
||||
setInteractiveContract
|
||||
} from 'actions/contracts';
|
||||
import { showNotification } from 'actions/notifications';
|
||||
import { isValidETHAddress } from 'libs/validators';
|
||||
|
||||
function* handleAccessContract(action: AccessContractAction) {
|
||||
const contractFunctions = [];
|
||||
|
||||
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) {
|
||||
console.error('Error parsing contract ABI JSON', err);
|
||||
yield put(showNotification('danger', translate('ERROR_26'), 5000));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* contractsSaga(): Generator<Effect, void, any> {
|
||||
yield takeEvery('ACCESS_CONTRACT', handleAccessContract);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import bity from './bity';
|
||||
import contracts from './contracts';
|
||||
import ens from './ens';
|
||||
import notifications from './notifications';
|
||||
import rates from './rates';
|
||||
import wallet from './wallet';
|
||||
|
||||
export default { bity, contracts, ens, notifications, rates, wallet };
|
|
@ -2,7 +2,11 @@
|
|||
import type { State } from 'reducers';
|
||||
import { BaseNode } from 'libs/nodes';
|
||||
import { NODES, NETWORKS } from 'config/data';
|
||||
import type { NetworkConfig } from 'config/data';
|
||||
import type { NetworkConfig, NetworkContract } from 'config/data';
|
||||
|
||||
export function getNode(state: State): string {
|
||||
return state.config.nodeSelection;
|
||||
}
|
||||
|
||||
export function getNodeLib(state: State): BaseNode {
|
||||
return NODES[state.config.nodeSelection].lib;
|
||||
|
@ -11,3 +15,7 @@ export function getNodeLib(state: State): BaseNode {
|
|||
export function getNetworkConfig(state: State): NetworkConfig {
|
||||
return NETWORKS[NODES[state.config.nodeSelection].network];
|
||||
}
|
||||
|
||||
export function getNetworkContracts(state: State): ?Array<NetworkContract> {
|
||||
return getNetworkConfig(state).contracts;
|
||||
}
|
||||
|
|
|
@ -5,11 +5,7 @@ import {
|
|||
} from 'utils/localStorage';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import notificationsSaga from './sagas/notifications';
|
||||
import ensSaga from './sagas/ens';
|
||||
import walletSaga from './sagas/wallet';
|
||||
import bitySaga from './sagas/bity';
|
||||
import ratesSaga from './sagas/rates';
|
||||
import sagas from './sagas';
|
||||
import { INITIAL_STATE as configInitialState } from 'reducers/config';
|
||||
import { INITIAL_STATE as customTokensInitialState } from 'reducers/customTokens';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
@ -45,11 +41,11 @@ const configureStore = () => {
|
|||
};
|
||||
|
||||
store = createStore(RootReducer, persistedInitialState, middleware);
|
||||
sagaMiddleware.run(notificationsSaga);
|
||||
sagaMiddleware.run(ensSaga);
|
||||
sagaMiddleware.run(walletSaga);
|
||||
sagaMiddleware.run(bitySaga);
|
||||
sagaMiddleware.run(ratesSaga);
|
||||
|
||||
// Add all of the sagas to the middleware
|
||||
Object.keys(sagas).forEach(saga => {
|
||||
sagaMiddleware.run(sagas[saga]);
|
||||
});
|
||||
|
||||
store.subscribe(
|
||||
throttle(() => {
|
||||
|
|
Loading…
Reference in New Issue