[beta] UI backports (#4855)

* Added React Hot Reload to dapps + TokenDeplpoy fix (#4846)

* Fix method decoding (#4845)

* Fix contract deployment method decoding in Signer

* Linting

* Fix TxViewer when no `to` (contract deployment) (#4847)

* Added React Hot Reload to dapps + TokenDeplpoy fix

* Fixes to the LocalTx dapp

* Don't send the nonce for mined transactions

* Don't encode empty to values for options

* Pull steps from actual available steps (#4848)

* Wait for the value to have changed in the input (#4844)

* Backport Regsirty changes from #4589

* Test fixes for #4589
This commit is contained in:
Jaco Greeff 2017-03-10 14:03:10 +01:00 committed by Arkadiy Paronyan
parent 34e81101d7
commit ab236690df
17 changed files with 266 additions and 61 deletions

View File

@ -143,8 +143,15 @@ export function inOptions (options) {
if (options) { if (options) {
Object.keys(options).forEach((key) => { Object.keys(options).forEach((key) => {
switch (key) { switch (key) {
case 'from':
case 'to': case 'to':
// Don't encode the `to` option if it's empty
// (eg. contract deployments)
if (options[key]) {
options[key] = inAddress(options[key]);
}
break;
case 'from':
options[key] = inAddress(options[key]); options[key] = inAddress(options[key]);
break; break;

View File

@ -208,6 +208,13 @@ describe('api/format/input', () => {
}); });
}); });
it('does not encode an empty `to` value', () => {
const options = { to: '' };
const formatted = inOptions(options);
expect(formatted.to).to.equal('');
});
['gas', 'gasPrice', 'value', 'minBlock', 'nonce'].forEach((input) => { ['gas', 'gasPrice', 'value', 'minBlock', 'nonce'].forEach((input) => {
it(`formats ${input} number as hexnumber`, () => { it(`formats ${input} number as hexnumber`, () => {
const block = {}; const block = {};

View File

@ -16,7 +16,14 @@
import * as abis from './abi'; import * as abis from './abi';
const REGISTRY_V1_HASHES = [
'0x34f7c51bbb1b1902fbdabfdf04811100f5c9f998f26dd535d2f6f977492c748e', // ropsten
'0x64c3ee34851517a9faecd995c102b339f03e564ad6772dc43a26f993238b20ec' // homestead
];
export default class Registry { export default class Registry {
_registryContract = null;
constructor (api) { constructor (api) {
this._api = api; this._api = api;
@ -43,11 +50,10 @@ export default class Registry {
this._fetching = true; this._fetching = true;
return this._api.parity return this.fetchContract()
.registryAddress() .then((contract) => {
.then((address) => {
this._fetching = false; this._fetching = false;
this._instance = this._api.newContract(abis.registry, address).instance; this._instance = contract.instance;
this._queue.forEach((queued) => { this._queue.forEach((queued) => {
queued.resolve(this._instance); queued.resolve(this._instance);
@ -89,6 +95,47 @@ export default class Registry {
.then((contract) => contract.instance); .then((contract) => contract.instance);
} }
fetchContract () {
if (this._registryContract) {
return Promise.resolve(this._registryContract);
}
return this._api.parity
.registryAddress()
.then((address) => Promise.all([ address, this._api.eth.getCode(address) ]))
.then(([ address, code ]) => {
const codeHash = this._api.util.sha3(code);
const version = REGISTRY_V1_HASHES.includes(codeHash)
? 1
: 2;
const abi = version === 1
? abis.registry
: abis.registry2;
const contract = this._api.newContract(abi, address);
// Add support for previous `set` and `get` methods
if (!contract.instance.get && contract.instance.getData) {
contract.instance.get = contract.instance.getData;
}
if (contract.instance.get && !contract.instance.getData) {
contract.instance.getData = contract.instance.get;
}
if (!contract.instance.set && contract.instance.setData) {
contract.instance.set = contract.instance.setData;
}
if (contract.instance.set && !contract.instance.setData) {
contract.instance.setData = contract.instance.set;
}
console.log(`registry at ${address}, code ${codeHash}, version ${version}`);
this._registryContract = contract;
return this._registryContract;
});
}
_createGetParams (_name, key) { _createGetParams (_name, key) {
const name = _name.toLowerCase(); const name = _name.toLowerCase();
const sha3 = this._api.util.sha3.text(name); const sha3 = this._api.util.sha3.text(name);

View File

@ -35,6 +35,9 @@ function create () {
} }
}; };
api = { api = {
eth: {
getCode: sinon.stub().resolves(0)
},
parity: { parity: {
registryAddress: sinon.stub().resolves('testRegistryAddress') registryAddress: sinon.stub().resolves('testRegistryAddress')
}, },

View File

@ -16,6 +16,7 @@
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import React from 'react'; import React from 'react';
import { AppContainer } from 'react-hot-loader';
import injectTapEventPlugin from 'react-tap-event-plugin'; import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin(); injectTapEventPlugin();
@ -27,6 +28,21 @@ import '../../assets/fonts/RobotoMono/font.css';
import './style.css'; import './style.css';
ReactDOM.render( ReactDOM.render(
<Application />, <AppContainer>
<Application />
</AppContainer>,
document.querySelector('#container') document.querySelector('#container')
); );
if (module.hot) {
module.hot.accept('./githubhint/Application/index.js', () => {
require('./githubhint/Application/index.js');
ReactDOM.render(
<AppContainer>
<Application />
</AppContainer>,
document.querySelector('#container')
);
});
}

View File

@ -16,6 +16,7 @@
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import React from 'react'; import React from 'react';
import { AppContainer } from 'react-hot-loader';
import injectTapEventPlugin from 'react-tap-event-plugin'; import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin(); injectTapEventPlugin();
@ -27,6 +28,21 @@ import '../../assets/fonts/RobotoMono/font.css';
import './style.css'; import './style.css';
ReactDOM.render( ReactDOM.render(
<Application />, <AppContainer>
<Application />
</AppContainer>,
document.querySelector('#container') document.querySelector('#container')
); );
if (module.hot) {
module.hot.accept('./localtx/Application/index.js', () => {
require('./localtx/Application/index.js');
ReactDOM.render(
<AppContainer>
<Application />
</AppContainer>,
document.querySelector('#container')
);
});
}

View File

@ -15,5 +15,29 @@
th { th {
text-align: center; text-align: center;
} }
td {
text-align: center;
}
}
button {
background-color: rgba(0, 136, 170, 1);
border: none;
border-radius: 5px;
color: white;
font-size: 1rem;
padding: 0.5em 1em;
width: 100%;
&:hover {
background-color: rgba(0, 136, 170, 0.8);
cursor: pointer;
}
}
input {
font-size: 1rem;
padding: 0.5em 1em;
} }
} }

View File

@ -70,7 +70,6 @@ export default class Application extends Component {
local[tx.hash].transaction = tx; local[tx.hash].transaction = tx;
local[tx.hash].stats = data.stats; local[tx.hash].stats = data.stats;
}); });
// Convert local transactions to array // Convert local transactions to array
const localTransactions = Object.keys(local).map(hash => { const localTransactions = Object.keys(local).map(hash => {
const data = local[hash]; const data = local[hash];

View File

@ -6,6 +6,14 @@
} }
} }
.txhash {
display: inline-block;
overflow: hidden;
padding-right: 3ch;
text-overflow: ellipsis;
width: 10ch;
}
.transaction { .transaction {
td { td {
padding: 7px 15px; padding: 7px 15px;

View File

@ -31,8 +31,8 @@ class BaseTransaction extends Component {
renderHash (hash) { renderHash (hash) {
return ( return (
<code title={ hash }> <code title={ hash } className={ styles.txhash }>
{ this.shortHash(hash) } { hash }
</code> </code>
); );
} }
@ -206,7 +206,10 @@ export class LocalTransaction extends BaseTransaction {
From From
</th> </th>
<th> <th>
Gas Price / Gas Gas Price
</th>
<th>
Gas
</th> </th>
<th> <th>
Status Status
@ -224,18 +227,18 @@ export class LocalTransaction extends BaseTransaction {
toggleResubmit = () => { toggleResubmit = () => {
const { transaction } = this.props; const { transaction } = this.props;
const { isResubmitting, gasPrice } = this.state; const { isResubmitting } = this.state;
this.setState({ const nextState = {
isResubmitting: !isResubmitting isResubmitting: !isResubmitting
}); };
if (gasPrice === null) { if (!isResubmitting) {
this.setState({ nextState.gasPrice = api.util.fromWei(transaction.gasPrice, 'shannon').toNumber();
gasPrice: `0x${transaction.gasPrice.toString(16)}`, nextState.gas = transaction.gas.div(1000000).toNumber();
gas: `0x${transaction.gas.toString(16)}`
});
} }
this.setState(nextState);
}; };
setGasPrice = el => { setGasPrice = el => {
@ -251,16 +254,15 @@ export class LocalTransaction extends BaseTransaction {
}; };
sendTransaction = () => { sendTransaction = () => {
const { transaction } = this.props; const { transaction, status } = this.props;
const { gasPrice, gas } = this.state; const { gasPrice, gas } = this.state;
const newTransaction = { const newTransaction = {
from: transaction.from, from: transaction.from,
to: transaction.to,
nonce: transaction.nonce,
value: transaction.value, value: transaction.value,
data: transaction.input, data: transaction.input,
gasPrice, gas gasPrice: api.util.toWei(gasPrice, 'shannon'),
gas: new BigNumber(gas).mul(1000000)
}; };
this.setState({ this.setState({
@ -268,11 +270,21 @@ export class LocalTransaction extends BaseTransaction {
isSending: true isSending: true
}); });
const closeSending = () => this.setState({ const closeSending = () => {
this.setState({
isSending: false, isSending: false,
gasPrice: null, gasPrice: null,
gas: null gas: null
}); });
};
if (transaction.to) {
newTransaction.to = transaction.to;
}
if (!['mined', 'replaced'].includes(status)) {
newTransaction.nonce = transaction.nonce;
}
api.eth.sendTransaction(newTransaction) api.eth.sendTransaction(newTransaction)
.then(closeSending) .then(closeSending)
@ -290,9 +302,9 @@ export class LocalTransaction extends BaseTransaction {
const resubmit = isSending ? ( const resubmit = isSending ? (
'sending...' 'sending...'
) : ( ) : (
<a href='javascript:void' onClick={ this.toggleResubmit }> <button onClick={ this.toggleResubmit }>
resubmit resubmit
</a> </button>
); );
return ( return (
@ -308,7 +320,8 @@ export class LocalTransaction extends BaseTransaction {
</td> </td>
<td> <td>
{ this.renderGasPrice(transaction) } { this.renderGasPrice(transaction) }
<br /> </td>
<td>
{ this.renderGas(transaction) } { this.renderGas(transaction) }
</td> </td>
<td> <td>
@ -345,9 +358,9 @@ export class LocalTransaction extends BaseTransaction {
return ( return (
<tr className={ styles.transaction }> <tr className={ styles.transaction }>
<td> <td>
<a href='javascript:void' onClick={ this.toggleResubmit }> <button onClick={ this.toggleResubmit }>
cancel cancel
</a> </button>
</td> </td>
<td> <td>
{ this.renderHash(transaction.hash) } { this.renderHash(transaction.hash) }
@ -357,20 +370,24 @@ export class LocalTransaction extends BaseTransaction {
</td> </td>
<td className={ styles.edit }> <td className={ styles.edit }>
<input <input
type='text' type='number'
value={ gasPrice } value={ gasPrice }
onChange={ this.setGasPrice } onChange={ this.setGasPrice }
/> />
<span>shannon</span>
</td>
<td className={ styles.edit }>
<input <input
type='text' type='number'
value={ gas } value={ gas }
onChange={ this.setGas } onChange={ this.setGas }
/> />
<span>MGas</span>
</td> </td>
<td colSpan='2'> <td colSpan='2'>
<a href='javascript:void' onClick={ this.sendTransaction }> <button onClick={ this.sendTransaction }>
Send Send
</a> </button>
</td> </td>
</tr> </tr>
); );

View File

@ -16,6 +16,7 @@
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import React from 'react'; import React from 'react';
import { AppContainer } from 'react-hot-loader';
import injectTapEventPlugin from 'react-tap-event-plugin'; import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin(); injectTapEventPlugin();
@ -27,6 +28,21 @@ import '../../assets/fonts/RobotoMono/font.css';
import './style.css'; import './style.css';
ReactDOM.render( ReactDOM.render(
<Application />, <AppContainer>
<Application />
</AppContainer>,
document.querySelector('#container') document.querySelector('#container')
); );
if (module.hot) {
module.hot.accept('./signaturereg/Application/index.js', () => {
require('./signaturereg/Application/index.js');
ReactDOM.render(
<AppContainer>
<Application />
</AppContainer>,
document.querySelector('#container')
);
});
}

View File

@ -17,6 +17,7 @@
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import React from 'react'; import React from 'react';
import { Redirect, Router, Route, hashHistory } from 'react-router'; import { Redirect, Router, Route, hashHistory } from 'react-router';
import { AppContainer } from 'react-hot-loader';
import injectTapEventPlugin from 'react-tap-event-plugin'; import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin(); injectTapEventPlugin();
@ -31,6 +32,7 @@ import '../../assets/fonts/RobotoMono/font.css';
import './style.css'; import './style.css';
ReactDOM.render( ReactDOM.render(
<AppContainer>
<Router history={ hashHistory }> <Router history={ hashHistory }>
<Redirect from='/' to='/overview' /> <Redirect from='/' to='/overview' />
<Route path='/' component={ Application }> <Route path='/' component={ Application }>
@ -38,6 +40,29 @@ ReactDOM.render(
<Route path='overview' component={ Overview } /> <Route path='overview' component={ Overview } />
<Route path='transfer' component={ Transfer } /> <Route path='transfer' component={ Transfer } />
</Route> </Route>
</Router>, </Router>
</AppContainer>,
document.querySelector('#container') document.querySelector('#container')
); );
if (module.hot) {
module.hot.accept('./tokendeploy/Application/index.js', () => {
require('./tokendeploy/Application/index.js');
require('./tokendeploy/Overview/index.js');
require('./tokendeploy/Transfer/index.js');
ReactDOM.render(
<AppContainer>
<Router history={ hashHistory }>
<Redirect from='/' to='/overview' />
<Route path='/' component={ Application }>
<Route path='deploy' component={ Deploy } />
<Route path='overview' component={ Overview } />
<Route path='transfer' component={ Transfer } />
</Route>
</Router>
</AppContainer>,
document.querySelector('#container')
);
});
}

View File

@ -119,7 +119,7 @@ export function attachInstances () {
.all([ .all([
api.parity.registryAddress(), api.parity.registryAddress(),
api.parity.netChain(), api.parity.netChain(),
api.partiy.netVersion() api.net.version()
]) ])
.then(([registryAddress, netChain, _netVersion]) => { .then(([registryAddress, netChain, _netVersion]) => {
const registry = api.newContract(abis.registry, registryAddress).instance; const registry = api.newContract(abis.registry, registryAddress).instance;

View File

@ -17,6 +17,7 @@
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { AppContainer } from 'react-hot-loader';
import injectTapEventPlugin from 'react-tap-event-plugin'; import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin(); injectTapEventPlugin();
@ -29,10 +30,25 @@ import '../../assets/fonts/RobotoMono/font.css';
import './style.css'; import './style.css';
ReactDOM.render( ReactDOM.render(
( <AppContainer>
<Provider store={ store }> <Provider store={ store }>
<Container /> <Container />
</Provider> </Provider>
), </AppContainer>,
document.querySelector('#container') document.querySelector('#container')
); );
if (module.hot) {
module.hot.accept('./tokenreg/Container.js', () => {
require('./tokenreg/Container.js');
ReactDOM.render(
<AppContainer>
<Provider store={ store }>
<Container />
</Provider>
</AppContainer>,
document.querySelector('#container')
);
});
}

View File

@ -142,12 +142,13 @@ class DeployContract extends Component {
render () { render () {
const { step, deployError, rejected, inputs } = this.state; const { step, deployError, rejected, inputs } = this.state;
const realStep = Object.keys(STEPS).findIndex((k) => k === step); const realStepKeys = deployError || rejected
const realSteps = deployError || rejected ? []
? null : Object.keys(STEPS).filter((k) => k !== 'CONTRACT_PARAMETERS' || inputs.length > 0);
: Object.keys(STEPS) const realStep = realStepKeys.findIndex((k) => k === step);
.filter((k) => k !== 'CONTRACT_PARAMETERS' || inputs.length > 0) const realSteps = realStepKeys.length
.map((k) => STEPS[k]); ? realStepKeys.map((k) => STEPS[k])
: null;
const title = realSteps const title = realSteps
? null ? null

View File

@ -214,6 +214,7 @@ export default class Input extends Component {
onChange = (event, value) => { onChange = (event, value) => {
event.persist(); event.persist();
this.setValue(value, () => { this.setValue(value, () => {
this.props.onChange && this.props.onChange(event, value); this.props.onChange && this.props.onChange(event, value);
}); });
@ -231,12 +232,10 @@ export default class Input extends Component {
} }
onPaste = (event) => { onPaste = (event) => {
const { value } = event.target; // Wait for the onChange handler to be called
const pasted = event.clipboardData.getData('Text');
window.setTimeout(() => { window.setTimeout(() => {
this.onSubmit(value + pasted); this.onSubmit(this.state.value);
}, 0); }, 200);
} }
onKeyDown = (event) => { onKeyDown = (event) => {

View File

@ -138,6 +138,10 @@ export default class MethodDecodingStore {
return Promise.resolve(result); return Promise.resolve(result);
} }
if (!transaction.to) {
return this.decodeContractCreation(result);
}
let signature; let signature;
try { try {
@ -206,7 +210,7 @@ export default class MethodDecodingStore {
}); });
} }
decodeContractCreation (data, contractAddress) { decodeContractCreation (data, contractAddress = '') {
const result = { const result = {
...data, ...data,
contract: true, contract: true,