mirror of https://github.com/embarklabs/embark.git
refactor(embark-ui): improve cockpit's contracts explorer
Rename contract "Transactions" tab to "Log". Display and allow filtering of all contract methods. Disable debug button for pure/view functions and the constructor. Revise the filtering logic so that filters are combined together. Make the status filter a drop down menu like the others. Revise styling for consistent row height, alignment of text, and button sizes; use a monospaced font in some cases to achieve the effect. Handle enter/return correctly in forms within a contract's Interact tab. Remove event rows from a contract's Interact tab. Track pure/view calls in the blockchain proxy so they can be logged server-side by console listener and reported in Cockpit within a contract's Log tab. Eliminate double logging in the contracts manager. Ensure contracts deployed on a fresh `embark run` have an `address` / `deployedAddress` property.
This commit is contained in:
parent
b15467f64a
commit
9afdbd9848
|
@ -14,4 +14,4 @@ contract SimpleStorage {
|
|||
function get() public view returns (uint retVal) {
|
||||
return storedData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,4 +31,3 @@ ContractDetail.propTypes = {
|
|||
};
|
||||
|
||||
export default ContractDetail;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { TabContent, TabPane, Nav, NavItem, NavLink, Card, CardBody, CardTitle }
|
|||
import classnames from 'classnames';
|
||||
|
||||
import ContractDetail from '../components/ContractDetail';
|
||||
import ContractTransactionsContainer from '../containers/ContractTransactionsContainer';
|
||||
import ContractLogContainer from '../containers/ContractLogContainer';
|
||||
import ContractOverviewContainer from '../containers/ContractOverviewContainer';
|
||||
|
||||
class ContractLayout extends React.Component {
|
||||
|
@ -53,7 +53,7 @@ class ContractLayout extends React.Component {
|
|||
className={classnames({ active: this.state.activeTab === '3' })}
|
||||
onClick={() => { this.toggle('3'); }}
|
||||
>
|
||||
<FontAwesomeIcon className="mr-2" name="list-alt" />Transactions
|
||||
<FontAwesomeIcon className="mr-2" name="list-alt" />Log
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
|
@ -65,13 +65,13 @@ class ContractLayout extends React.Component {
|
|||
<ContractDetail contract={this.props.contract} />
|
||||
</TabPane>
|
||||
<TabPane tabId="3">
|
||||
<ContractTransactionsContainer contract={this.props.contract} />
|
||||
<ContractLogContainer contract={this.props.contract} />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
import PropTypes from "prop-types";
|
||||
import React from 'react';
|
||||
import {Row, Col, Table, FormGroup, Label, Input, Form} from 'reactstrap';
|
||||
import DebugButton from './DebugButton';
|
||||
|
||||
import "./ContractLog.scss";
|
||||
|
||||
const TX_STATES = {Any: '', Success: '0x1', Fail: '0x0'};
|
||||
const EVENT = 'event';
|
||||
const FUNCTION = 'function';
|
||||
|
||||
class ContractLog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {method: '', event: '', status: TX_STATES['Any']};
|
||||
}
|
||||
|
||||
getMethods() {
|
||||
if (!this.props.contract.abiDefinition) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.props.contract.abiDefinition.filter(method => method.type === FUNCTION);
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
if (!this.props.contract.abiDefinition) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.props.contract.abiDefinition.filter(method => method.type === EVENT);
|
||||
}
|
||||
|
||||
updateState(key, value) {
|
||||
this.setState({[key]: value});
|
||||
}
|
||||
|
||||
dataToDisplay() {
|
||||
return this.props.contractLogs.map(contractLog => {
|
||||
const events = this.props.contractEvents
|
||||
.filter(contractEvent => contractEvent.transactionHash === contractLog.transactionHash)
|
||||
.map(contractEvent => contractEvent.event);
|
||||
contractLog.events = events;
|
||||
return contractLog;
|
||||
}).filter(contractLog => {
|
||||
if (this.state.status && contractLog.status !== this.state.status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.method && this.state.event) {
|
||||
return this.state.method === contractLog.functionName &&
|
||||
contractLog.events.includes(this.state.event);
|
||||
}
|
||||
|
||||
if (this.state.method) {
|
||||
return this.state.method === contractLog.functionName;
|
||||
}
|
||||
|
||||
if (this.state.event) {
|
||||
return contractLog.events.includes(this.state.event);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Form>
|
||||
<Row>
|
||||
<Col md={4}>
|
||||
<FormGroup>
|
||||
<Label htmlFor="functions">Functions</Label>
|
||||
<Input type="select"
|
||||
name="functions"
|
||||
id="functions"
|
||||
onChange={(event) => this.updateState('method', event.target.value)}
|
||||
value={this.state.method}>
|
||||
<option value="">(all)</option>
|
||||
{this.getMethods().map((method, index) => (
|
||||
<option value={method.name} key={index}>
|
||||
{method.name}
|
||||
</option>))}
|
||||
<option value="constructor">constructor</option>
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<FormGroup>
|
||||
<Label htmlFor="events">Events</Label>
|
||||
<Input type="select"
|
||||
name="events"
|
||||
id="events"
|
||||
onChange={(event) => this.updateState('event', event.target.value)}
|
||||
value={this.state.event}>
|
||||
<option value="">(all)</option>
|
||||
{this.getEvents().map((event, index) => (
|
||||
<option value={event.name} key={index}>
|
||||
{event.name}
|
||||
</option>))}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<FormGroup>
|
||||
<Label htmlFor="events">Status</Label>
|
||||
<Input type="select"
|
||||
name="status"
|
||||
id="status"
|
||||
onChange={(event) => this.updateState('status', event.target.value)}
|
||||
value={this.state.status}>
|
||||
{Object.keys(TX_STATES).map((key, index) => (
|
||||
<option value={TX_STATES[key]} key={index}>
|
||||
{key === 'Any' ? '(any)' : key}
|
||||
</option>
|
||||
))}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
<Row>
|
||||
<Col className="overflow-auto">
|
||||
<Table className="contract-log">
|
||||
<thead>
|
||||
<tr>
|
||||
<th/>
|
||||
<th>Invocation</th>
|
||||
<th>Events</th>
|
||||
<th>Gas</th>
|
||||
<th>Block</th>
|
||||
<th>Status</th>
|
||||
<th>Transaction</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
this.dataToDisplay().map((log, index) => {
|
||||
return (
|
||||
<tr key={'log-' + index}>
|
||||
<td><DebugButton contracts={[this.props.contract]}
|
||||
transaction={
|
||||
{...log,
|
||||
hash: log.transactionHash,
|
||||
isCall: log.kind === 'call',
|
||||
isConstructor: log.functionName === 'constructor'}}/></td>
|
||||
<td>{`${log.name}.${log.functionName}(${log.paramString})`}</td>
|
||||
<td>{log.events.join(', ')}</td>
|
||||
<td>{log.gasUsed}</td>
|
||||
<td>{log.blockNumber}</td>
|
||||
<td>{log.status}</td>
|
||||
<td>{log.transactionHash}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ContractLog.propTypes = {
|
||||
contractLogs: PropTypes.array,
|
||||
contractEvents: PropTypes.array,
|
||||
contract: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default ContractLog;
|
|
@ -0,0 +1,6 @@
|
|||
table.contract-log {
|
||||
td, th {
|
||||
height: 4.2em;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
.contract-function-container .gas-price-form #gasPrice {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.contract-function-container .gas-price-form button {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border: 0;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.contract-function-container .card-header.closed {
|
||||
border-bottom: none;
|
||||
border-radius: 0.25rem;
|
||||
}
|
|
@ -23,7 +23,7 @@ import {formatContractForDisplay} from '../utils/presentation';
|
|||
import FontAwesome from 'react-fontawesome';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import "./ContractOverview.css";
|
||||
import "./ContractOverview.scss";
|
||||
|
||||
class ContractFunction extends Component {
|
||||
constructor(props) {
|
||||
|
@ -45,7 +45,7 @@ class ContractFunction extends Component {
|
|||
return 'Deploy';
|
||||
}
|
||||
|
||||
return ContractFunction.isPureCall(method) ? 'Call' : 'Send';
|
||||
return ContractFunction.isPureCall(method) ? 'call' : 'send';
|
||||
}
|
||||
|
||||
inputsAsArray() {
|
||||
|
@ -70,7 +70,22 @@ class ContractFunction extends Component {
|
|||
|
||||
handleCall(e) {
|
||||
e.preventDefault();
|
||||
this.props.postContractFunction(this.props.contractName, this.props.method.name, this.inputsAsArray(), this.state.inputs.gasPrice * 1000000000);
|
||||
this.props.postContractFunction(
|
||||
this.props.contractName,
|
||||
this.props.method.name,
|
||||
this.inputsAsArray(),
|
||||
this.state.inputs.gasPrice * 1000000000
|
||||
);
|
||||
}
|
||||
|
||||
handleKeyPress(e) {
|
||||
if (e.key === 'Enter') {
|
||||
if (this.callDisabled()) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
this.handleCall(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callDisabled() {
|
||||
|
@ -95,38 +110,62 @@ class ContractFunction extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
makeBadge(color, codeColor, text) {
|
||||
const badgeDark = this.state.functionCollapse;
|
||||
const _codeColor = badgeDark ? 'white' : codeColor;
|
||||
return (
|
||||
<Badge color={color} className={classnames({
|
||||
'badge-dark': badgeDark,
|
||||
'contract-function-badge': true,
|
||||
'float-right': true,
|
||||
'p-2': true
|
||||
})}>
|
||||
<code className={classnames({
|
||||
[`code-${_codeColor}`]: true,
|
||||
})}>
|
||||
{text}
|
||||
</code>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (ContractFunction.isEvent(this.props.method)) {
|
||||
return <React.Fragment/>;
|
||||
}
|
||||
return (
|
||||
<Card className="contract-function-container">
|
||||
<CardHeader
|
||||
className={classnames({
|
||||
collapsable: !ContractFunction.isEvent(this.props.method),
|
||||
'border-bottom-0': !this.state.functionCollapse,
|
||||
'rounded': !this.state.functionCollapse
|
||||
})}
|
||||
onClick={() => this.toggleFunction()}>
|
||||
<CardTitle>
|
||||
{ContractFunction.isPureCall(this.props.method) && Boolean(this.props.method.inputs.length) &&
|
||||
<Badge color="warning" className="float-right p-2">call</Badge>
|
||||
}
|
||||
{ContractFunction.isPureCall(this.props.method) && !this.props.method.inputs.length &&
|
||||
<Button color="warning" size="sm" className="float-right" onClick={(e) => this.handleCall(e)}>call</Button>
|
||||
}
|
||||
{ContractFunction.isEvent(this.props.method) &&
|
||||
<Badge color="info" className="float-right p-2">event</Badge>
|
||||
}
|
||||
{this.props.method.name}({this.props.method.inputs.map(input => input.name).join(', ')})
|
||||
<span className="contract-function-signature">
|
||||
{`${this.props.method.name}` +
|
||||
`(${this.props.method.inputs.map(i => i.name).join(', ')})`}
|
||||
</span>
|
||||
<div>
|
||||
{(ContractFunction.isPureCall(this.props.method) &&
|
||||
this.makeBadge('success', 'white', 'call')) ||
|
||||
this.makeBadge('warning', 'black', 'send')}
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
{!ContractFunction.isEvent(this.props.method) &&
|
||||
<Collapse isOpen={this.state.functionCollapse} className="relative">
|
||||
<CardBody>
|
||||
<Form method="post" inline>
|
||||
<Form inline>
|
||||
{this.props.method.inputs.map(input => (
|
||||
<FormGroup key={input.name}>
|
||||
<Label for={input.name} className="mr-2 font-weight-bold">{input.name}</Label>
|
||||
<Input name={input.name} id={input.name} placeholder={input.type}
|
||||
onChange={(e) => this.handleChange(e, input.name)}/>
|
||||
<Label for={input.name} className="mr-2 font-weight-bold contract-function-input">
|
||||
{input.name}
|
||||
</Label>
|
||||
<Input name={input.name}
|
||||
id={input.name}
|
||||
placeholder={input.type}
|
||||
onChange={(e) => this.handleChange(e, input.name)}
|
||||
onKeyPress={(e) => this.handleKeyPress(e)}/>
|
||||
</FormGroup>
|
||||
))}
|
||||
</Form>
|
||||
|
@ -140,12 +179,15 @@ class ContractFunction extends Component {
|
|||
</Row>
|
||||
<Row>
|
||||
<Collapse isOpen={this.state.optionsCollapse} className="pl-3">
|
||||
<Form method="post" inline className="gas-price-form ">
|
||||
<Form inline className="gas-price-form">
|
||||
<FormGroup key="gasPrice">
|
||||
<Label for="gasPrice" className="mr-2">Gas Price (in GWei)(optional)</Label>
|
||||
<Input name="gasPrice" id="gasPrice" placeholder="uint256"
|
||||
<Label for="gasPrice" className="mr-2">Gas Price (in GWei) (optional)</Label>
|
||||
<Input name="gasPrice"
|
||||
id="gasPrice"
|
||||
placeholder="uint256"
|
||||
value={this.state.inputs.gasPrice || ''}
|
||||
onChange={(e) => this.handleChange(e, 'gasPrice')}/>
|
||||
onChange={(e) => this.handleChange(e, 'gasPrice')}
|
||||
onKeyPress={(e) => this.handleKeyPress(e)}/>
|
||||
<Button onClick={(e) => this.autoSetGasPrice(e)}
|
||||
title="Automatically set the gas price to what is currently in the estimator (default: safe low)">
|
||||
Auto-set
|
||||
|
@ -165,24 +207,40 @@ class ContractFunction extends Component {
|
|||
</Row>
|
||||
</Col>
|
||||
}
|
||||
<Button className="contract-function-button float-right" color="primary" disabled={this.callDisabled()}
|
||||
onClick={(e) => this.handleCall(e)}>
|
||||
<Button
|
||||
className={classnames({
|
||||
'btn-sm': true,
|
||||
'contract-function-button': true,
|
||||
'contract-function-button-with-margin-top': this.state.gasPriceCollapse,
|
||||
'float-right': true})}
|
||||
color="primary"
|
||||
disabled={this.callDisabled()}
|
||||
onClick={(e) => this.handleCall(e)}>
|
||||
{this.buttonTitle()}
|
||||
</Button>
|
||||
<div className="clearfix"/>
|
||||
</CardBody>
|
||||
{this.props.contractFunctions && this.props.contractFunctions.length > 0 && <CardFooter>
|
||||
<ListGroup>
|
||||
{this.props.contractFunctions.map(contractFunction => (
|
||||
<ListGroupItem key={contractFunction.result}>
|
||||
{contractFunction.inputs.length > 0 && <p>Input(s): {contractFunction.inputs.join(', ')}</p>}
|
||||
<strong>Result: {JSON.stringify(contractFunction.result)}</strong>
|
||||
{this.props.contractFunctions.map((contractFunction, idx) => (
|
||||
<ListGroupItem key={idx}>
|
||||
{contractFunction.inputs.length > 0 &&
|
||||
<p>Input(s):
|
||||
<span className="contract-function-input-values">
|
||||
{contractFunction.inputs.join(', ')}
|
||||
</span>
|
||||
</p>}
|
||||
Result:
|
||||
<strong>
|
||||
<span className="contract-function-result">
|
||||
{JSON.stringify(contractFunction.result).slice(1, -1)}
|
||||
</span>
|
||||
</strong>
|
||||
</ListGroupItem>
|
||||
))}
|
||||
</ListGroup>
|
||||
</CardFooter>}
|
||||
</Collapse>}
|
||||
|
||||
</Collapse>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
@ -217,7 +275,8 @@ const ContractOverview = (props) => {
|
|||
.filter((method) => {
|
||||
return props.onlyConstructor ? method.type === 'constructor' : method.type !== 'constructor';
|
||||
})
|
||||
.map(method => <ContractFunction key={method.name} contractName={contract.className}
|
||||
.map(method => <ContractFunction key={method.name}
|
||||
contractName={contract.className}
|
||||
method={method}
|
||||
contractFunctions={filterContractFunctions(props.contractFunctions, contract.className, method.name)}
|
||||
postContractFunction={props.postContractFunction}/>)}
|
||||
|
@ -237,4 +296,3 @@ ContractOverview.defaultProps = {
|
|||
};
|
||||
|
||||
export default ContractOverview;
|
||||
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
.contract-function-badge {
|
||||
cursor: default;
|
||||
|
||||
code {
|
||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
font-size: 13.1333px !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.code-black {
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.code-white {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.contract-function-button {
|
||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
font-size: 13.1333px !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.contract-function-button-with-margin-top {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.contract-function-container {
|
||||
.card-header.closed {
|
||||
border-bottom: none;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.card-title div {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.gas-price-form {
|
||||
#gasPrice {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border: 0;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contract-function-input {
|
||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.contract-function-input-values, .contract-function-result, .contract-function-signature {
|
||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
import PropTypes from "prop-types";
|
||||
import React from 'react';
|
||||
import {Row, Col, Table, FormGroup, Label, Input, Form} from 'reactstrap';
|
||||
|
||||
import DebugButton from './DebugButton'
|
||||
|
||||
const TX_STATES = {Success: '0x1', Fail: '0x0', Any: ''};
|
||||
const EVENT = 'event';
|
||||
const FUNCTION = 'function';
|
||||
const CONSTRUCTOR = 'constructor';
|
||||
const PURE = 'pure';
|
||||
const VIEW = 'view';
|
||||
|
||||
class ContractTransactions extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {method: '', event: '', status: TX_STATES['Any']};
|
||||
}
|
||||
|
||||
getMethods() {
|
||||
if (!this.props.contract.abiDefinition) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.props.contract.abiDefinition.filter(method => method.mutability !== VIEW && method.mutability !== PURE && method.constant !== true && (method.type === FUNCTION || method.type === CONSTRUCTOR));
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
if (!this.props.contract.abiDefinition) {
|
||||
return [];
|
||||
}
|
||||
return this.props.contract.abiDefinition.filter(method => method.type === EVENT);
|
||||
}
|
||||
|
||||
updateState(key, value) {
|
||||
this.setState({[key]: value});
|
||||
}
|
||||
|
||||
dataToDisplay() {
|
||||
return this.props.contractLogs.map(contractLog => {
|
||||
const events = this.props.contractEvents
|
||||
.filter(contractEvent => contractEvent.transactionHash === contractLog.transactionHash)
|
||||
.map(contractEvent => contractEvent.event);
|
||||
contractLog.events = events;
|
||||
return contractLog;
|
||||
}).filter(contractLog => {
|
||||
if (this.state.status && contractLog.status !== this.state.status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.method || this.state.event) {
|
||||
return this.state.method === contractLog.functionName || contractLog.events.includes(this.state.event);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Form>
|
||||
<Row>
|
||||
<Col md={6}>
|
||||
<FormGroup>
|
||||
<Label htmlFor="functions">Functions</Label>
|
||||
<Input type="select" name="functions" id="functions" onChange={(event) => this.updateState('method', event.target.value)} value={this.state.method}>
|
||||
<option value=""/>
|
||||
{this.getMethods().map((method, index) => <option value={method.name} key={index}>{method.type === CONSTRUCTOR ? CONSTRUCTOR : method.name}</option>)}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<FormGroup>
|
||||
<Label htmlFor="events">Events</Label>
|
||||
<Input type="select" name="events" id="events" onChange={(event) => this.updateState('event', event.target.value)} value={this.state.event}>
|
||||
<option value=""/>
|
||||
{this.getEvents().map((event, index) => <option value={event.name} key={index}>{event.name}</option>)}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col>
|
||||
<FormGroup row>
|
||||
<Col md="3">
|
||||
<Label>Tx Status</Label>
|
||||
</Col>
|
||||
<Col md="9">
|
||||
{Object.keys(TX_STATES).map(key => (
|
||||
<FormGroup key={key} check inline>
|
||||
<Input className="form-check-input"
|
||||
type="radio"
|
||||
id={key}
|
||||
name={key}
|
||||
value={TX_STATES[key]}
|
||||
onChange={(event) => this.updateState('status', event.target.value)}
|
||||
checked={TX_STATES[key] === this.state.status} />
|
||||
<Label check className="form-check-label" htmlFor={key}>{key}</Label>
|
||||
</FormGroup>
|
||||
))}
|
||||
</Col>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
<Row>
|
||||
<Col className="overflow-auto">
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th/>
|
||||
<th>Call</th>
|
||||
<th>Events</th>
|
||||
<th>Gas Used</th>
|
||||
<th>Block number</th>
|
||||
<th>Status</th>
|
||||
<th>Transaction hash</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
this.dataToDisplay().map((log, index) => {
|
||||
return (
|
||||
<tr key={'log-' + index}>
|
||||
<td><DebugButton forceDebuggable transaction={{hash: log.transactionHash}}/></td>
|
||||
<td>{`${log.name}.${log.functionName}(${log.paramString})`}</td>
|
||||
<td>{log.events.join(', ')}</td>
|
||||
<td>{log.gasUsed}</td>
|
||||
<td>{log.blockNumber}</td>
|
||||
<td>{log.status}</td>
|
||||
<td>{log.transactionHash}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ContractTransactions.propTypes = {
|
||||
contractLogs: PropTypes.array,
|
||||
contractEvents: PropTypes.array,
|
||||
contract: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default ContractTransactions;
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button} from "reactstrap";
|
||||
|
@ -13,12 +12,20 @@ class DebugButton extends React.Component {
|
|||
|
||||
isDebuggable() {
|
||||
return this.props.forceDebuggable ||
|
||||
(this.props.contracts && this.props.contracts.find(contract => contract.address === this.props.transaction.to));
|
||||
(!this.props.transaction.isCall &&
|
||||
!this.props.transaction.isConstructor &&
|
||||
this.props.contracts &&
|
||||
this.props.contracts.find(contract => {
|
||||
const address = this.props.transaction.to || this.props.transaction.address;
|
||||
return contract.address &&
|
||||
address &&
|
||||
(contract.address.toLowerCase() === address.toLowerCase());
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isDebuggable()) {
|
||||
return <React.Fragment />
|
||||
return <React.Fragment/>;
|
||||
}
|
||||
return (
|
||||
<Button color="primary" onClick={() => this.onClick()}>
|
||||
|
@ -42,4 +49,4 @@ DebugButton.propTypes = {
|
|||
contracts: PropTypes.arrayOf(PropTypes.object)
|
||||
};
|
||||
|
||||
export default withRouter(DebugButton);
|
||||
export default withRouter(DebugButton);
|
||||
|
|
|
@ -21,7 +21,7 @@ export const TextEditorToolbarTabs = {
|
|||
Interact: { label: 'Interact', icon: 'bolt' },
|
||||
Details: { label: 'Details', icon: 'info-circle' },
|
||||
Debugger: { label: 'Debugger', icon: 'bug' },
|
||||
Transactions: { label: 'Transactions', icon: 'list-alt' },
|
||||
Log: { label: 'Log', icon: 'list-alt' },
|
||||
Browser: { label: 'Browser', icon: 'eye' }
|
||||
};
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@ import {connect} from 'react-redux';
|
|||
import PropTypes from 'prop-types';
|
||||
import {contractEvents as contractEventsAction, contractLogs as contractLogsAction, listenToContractLogs, listenToContractEvents} from '../actions';
|
||||
|
||||
import ContractTransactions from '../components/ContractTransactions';
|
||||
import ContractLog from '../components/ContractLog';
|
||||
import DataWrapper from "../components/DataWrapper";
|
||||
import {getContractLogsByContract, getContractEventsByContract} from "../reducers/selectors";
|
||||
|
||||
class ContractTransactionsContainer extends Component {
|
||||
class ContractLogContainer extends Component {
|
||||
componentDidMount() {
|
||||
if (this.props.contractLogs.length === 0) {
|
||||
this.props.listenToContractLogs();
|
||||
|
@ -23,9 +23,9 @@ class ContractTransactionsContainer extends Component {
|
|||
render() {
|
||||
return (
|
||||
<DataWrapper shouldRender={this.props.contractLogs !== undefined } {...this.props} render={() => (
|
||||
<ContractTransactions contractLogs={this.props.contractLogs}
|
||||
contractEvents={this.props.contractEvents}
|
||||
contract={this.props.contract}/>
|
||||
<ContractLog contractLogs={this.props.contractLogs}
|
||||
contractEvents={this.props.contractEvents}
|
||||
contract={this.props.contract}/>
|
||||
)} />
|
||||
);
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ function mapStateToProps(state, props) {
|
|||
};
|
||||
}
|
||||
|
||||
ContractTransactionsContainer.propTypes = {
|
||||
ContractLogContainer.propTypes = {
|
||||
contract: PropTypes.object,
|
||||
contractLogs: PropTypes.array,
|
||||
contractEvents: PropTypes.array,
|
||||
|
@ -57,4 +57,4 @@ export default connect(
|
|||
fetchContractEvents: contractEventsAction.request,
|
||||
listenToContractEvents: listenToContractEvents
|
||||
}
|
||||
)(ContractTransactionsContainer);
|
||||
)(ContractLogContainer);
|
|
@ -261,4 +261,3 @@ export default withRouter(connect(
|
|||
fetchContracts: contractsAction.request
|
||||
},
|
||||
)(EditorContainer));
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import classNames from 'classnames';
|
|||
import Preview from '../components/Preview';
|
||||
import {getContractsByPath, getPreviewUrl} from "../reducers/selectors";
|
||||
import ContractDetail from '../components/ContractDetail';
|
||||
import ContractTransactionsContainer from './ContractTransactionsContainer';
|
||||
import ContractLogContainer from './ContractLogContainer';
|
||||
import ContractOverviewContainer from '../containers/ContractOverviewContainer';
|
||||
import ContractDebuggerContainer from '../containers/ContractDebuggerContainer';
|
||||
import { TextEditorToolbarTabs } from '../components/TextEditorToolbar';
|
||||
|
@ -25,11 +25,11 @@ class TextEditorAsideContainer extends Component {
|
|||
<ContractDetail key={index} contract={contract}/>
|
||||
</React.Fragment>
|
||||
);
|
||||
case TextEditorToolbarTabs.Transactions.label:
|
||||
case TextEditorToolbarTabs.Log.label:
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h2>{contract.className} - Transactions</h2>
|
||||
<ContractTransactionsContainer key={index} contract={contract}/>
|
||||
<h2>{contract.className} - Log</h2>
|
||||
<ContractLogContainer key={index} contract={contract}/>
|
||||
</React.Fragment>
|
||||
);
|
||||
case TextEditorToolbarTabs.Interact.label:
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"webpackDone": "webpackDone"
|
||||
},
|
||||
"blockchain": {
|
||||
"call": "eth_call",
|
||||
"clients": {
|
||||
"geth": "geth",
|
||||
"parity": "parity"
|
||||
|
|
|
@ -66,6 +66,14 @@ class Proxy {
|
|||
if (Object.values(METHODS_TO_MODIFY).includes(req.method)) {
|
||||
this.toModifyPayloads[req.id] = req.method;
|
||||
}
|
||||
if (req.method === constants.blockchain.call) {
|
||||
this.commList[req.id] = {
|
||||
kind: 'call',
|
||||
type: 'contract-log',
|
||||
address: req.params[0].to,
|
||||
data: req.params[0].data
|
||||
};
|
||||
}
|
||||
if (req.method === constants.blockchain.transactionMethods.eth_sendTransaction) {
|
||||
this.commList[req.id] = {
|
||||
type: 'contract-log',
|
||||
|
@ -97,10 +105,16 @@ class Proxy {
|
|||
if (!res) return;
|
||||
try {
|
||||
if (this.commList[res.id]) {
|
||||
this.commList[res.id].transactionHash = res.result;
|
||||
this.transactions[res.result] = {
|
||||
commListId: res.id
|
||||
};
|
||||
if (this.commList[res.id].kind === 'call') {
|
||||
this.commList[res.id].result = res.result;
|
||||
this.sendIpcMessage(this.commList[res.id]);
|
||||
delete this.commList[res.id];
|
||||
} else {
|
||||
this.commList[res.id].transactionHash = res.result;
|
||||
this.transactions[res.result] = {
|
||||
commListId: res.id
|
||||
};
|
||||
}
|
||||
} else if (this.receipts[res.id] && res.result && res.result.blockNumber) {
|
||||
// TODO find out why commList[receipts[res.id]] is sometimes not defined
|
||||
if (!this.commList[this.receipts[res.id]]) {
|
||||
|
@ -109,17 +123,7 @@ class Proxy {
|
|||
this.commList[this.receipts[res.id]].blockNumber = res.result.blockNumber;
|
||||
this.commList[this.receipts[res.id]].gasUsed = res.result.gasUsed;
|
||||
this.commList[this.receipts[res.id]].status = res.result.status;
|
||||
|
||||
if (this.ipc.connected && !this.ipc.connecting) {
|
||||
this.ipc.request('log', this.commList[this.receipts[res.id]]);
|
||||
} else {
|
||||
const message = this.commList[this.receipts[res.id]];
|
||||
this.ipc.connecting = true;
|
||||
this.ipc.connect(() => {
|
||||
this.ipc.connecting = false;
|
||||
this.ipc.request('log', message);
|
||||
});
|
||||
}
|
||||
this.sendIpcMessage(this.commList[this.receipts[res.id]]);
|
||||
delete this.transactions[this.commList[this.receipts[res.id]].transactionHash];
|
||||
delete this.commList[this.receipts[res.id]];
|
||||
delete this.receipts[res.id];
|
||||
|
@ -131,6 +135,18 @@ class Proxy {
|
|||
}
|
||||
}
|
||||
|
||||
sendIpcMessage(message) {
|
||||
if (this.ipc.connected && !this.ipc.connecting) {
|
||||
this.ipc.request('log', message);
|
||||
} else {
|
||||
this.ipc.connecting = true;
|
||||
this.ipc.connect(() => {
|
||||
this.ipc.connecting = false;
|
||||
this.ipc.request('log', message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async serve(host, port, ws, origin, accounts, certOptions={}) {
|
||||
const start = Date.now();
|
||||
await (function waitOnTarget() {
|
||||
|
|
|
@ -65,7 +65,7 @@ class ConsoleListener {
|
|||
functionName: 'constructor',
|
||||
paramString: '',
|
||||
address: receipt.contractAddress,
|
||||
status: receipt.status,
|
||||
status: receipt.status ? '0x1' : '0x0',
|
||||
gasUsed: receipt.gasUsed,
|
||||
blockNumber: receipt.blockNumber,
|
||||
transactionHash: receipt.transactionHash
|
||||
|
@ -78,14 +78,13 @@ class ConsoleListener {
|
|||
}
|
||||
|
||||
_onIpcLogRequest(request) {
|
||||
|
||||
if (request.type !== 'contract-log') {
|
||||
return this.logger.info(JSON.stringify(request));
|
||||
}
|
||||
|
||||
if (!this.contractsDeployed) return;
|
||||
|
||||
let {address, data, transactionHash, blockNumber, gasUsed, status} = request;
|
||||
const {address, data} = request;
|
||||
const contract = this.addressToContract[address];
|
||||
|
||||
if (!contract) {
|
||||
|
@ -94,16 +93,25 @@ class ConsoleListener {
|
|||
this.addressToContract = getAddressToContract(contractsList, this.addressToContract);
|
||||
});
|
||||
}
|
||||
|
||||
const {name, silent} = contract;
|
||||
if (silent && !this.outputDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {functionName, paramString} = getTransactionParams(contract, data);
|
||||
|
||||
if (request.kind === 'call') {
|
||||
const log = Object.assign({}, request, {name, functionName, paramString});
|
||||
log.status = '0x1';
|
||||
return this.events.emit('contracts:log', log);
|
||||
}
|
||||
|
||||
let {transactionHash, blockNumber, gasUsed, status} = request;
|
||||
gasUsed = utils.hexToNumber(gasUsed);
|
||||
blockNumber = utils.hexToNumber(blockNumber);
|
||||
|
||||
const log = Object.assign({}, request, {name, functionName, paramString, gasUsed, blockNumber});
|
||||
|
||||
this.events.emit('contracts:log', log);
|
||||
this.logger.info(`Blockchain>`.underline + ` ${name}.${functionName}(${paramString})`.bold + ` | ${transactionHash} | gas:${gasUsed} | blk:${blockNumber} | status:${status}`);
|
||||
this.events.emit('blockchain:tx', {name: name, functionName: functionName, paramString: paramString, transactionHash: transactionHash, gasUsed: gasUsed, blockNumber: blockNumber, status: status});
|
||||
|
|
|
@ -61,6 +61,18 @@ class ContractsManager {
|
|||
});
|
||||
|
||||
self.events.on("deploy:contract:deployed", (_contract) => {
|
||||
const contract = self.contracts[_contract.className];
|
||||
if (contract) {
|
||||
if (!_contract.address && _contract.deployedAddress) {
|
||||
_contract.address = _contract.deployedAddress;
|
||||
}
|
||||
if (!contract.address && _contract.address) {
|
||||
contract.address = _contract.address;
|
||||
}
|
||||
if (!contract.deployedAddress && _contract.deployedAddress) {
|
||||
contract.deployedAddress = _contract.deployedAddress;
|
||||
}
|
||||
}
|
||||
self.events.emit('contractsState', self.contractsState());
|
||||
});
|
||||
|
||||
|
@ -140,24 +152,10 @@ class ContractsManager {
|
|||
|
||||
if(funcCall === 'call') {
|
||||
contractLog.status = '0x1';
|
||||
self.events.emit('contracts:log', contractLog);
|
||||
return res.send({result});
|
||||
}
|
||||
|
||||
self.events.request("blockchain:get", web3 => {
|
||||
web3.eth.getTransaction(result, (err, tx) => {
|
||||
contractLog = Object.assign(contractLog, {
|
||||
data: tx.input,
|
||||
status: '0x1',
|
||||
gasUsed: tx.gas,
|
||||
blockNumber: tx.blockNumber,
|
||||
transactionHash: tx.hash
|
||||
});
|
||||
|
||||
self.events.emit('contracts:log', contractLog);
|
||||
res.send({result});
|
||||
});
|
||||
});
|
||||
res.send({result});
|
||||
});
|
||||
} catch (e) {
|
||||
if (funcCall === 'call' && e.message === constants.blockchain.gasAllowanceError) {
|
||||
|
|
Loading…
Reference in New Issue