Merge remote-tracking branch 'origin/master' into consistent-id

This commit is contained in:
Gav Wood
2016-12-10 13:36:30 +01:00
91 changed files with 2514 additions and 525 deletions

View File

@@ -17,6 +17,15 @@
},
"development": {
"plugins": ["react-hot-loader/babel"]
},
"test": {
"plugins": [
[
"babel-plugin-webpack-alias", {
"config": "webpack/test.js"
}
]
]
}
}
}

View File

@@ -40,9 +40,9 @@
"coveralls": "npm run testCoverage && coveralls < coverage/lcov.info",
"lint": "eslint --ignore-path .gitignore ./src/",
"lint:cached": "eslint --cache --ignore-path .gitignore ./src/",
"test": "mocha 'src/**/*.spec.js'",
"test:coverage": "istanbul cover _mocha -- 'src/**/*.spec.js'",
"test:e2e": "mocha 'src/**/*.e2e.js'",
"test": "NODE_ENV=test mocha 'src/**/*.spec.js'",
"test:coverage": "NODE_ENV=test istanbul cover _mocha -- 'src/**/*.spec.js'",
"test:e2e": "NODE_ENV=test mocha 'src/**/*.e2e.js'",
"test:npm": "(cd .npmjs && npm i) && node test/npmLibrary && (rm -rf .npmjs/node_modules)",
"prepush": "npm run lint:cached"
},
@@ -57,6 +57,7 @@
"babel-plugin-transform-object-rest-spread": "6.20.2",
"babel-plugin-transform-react-remove-prop-types": "0.2.11",
"babel-plugin-transform-runtime": "6.15.0",
"babel-plugin-webpack-alias": "2.1.2",
"babel-polyfill": "6.20.0",
"babel-preset-es2015": "6.18.0",
"babel-preset-es2016": "6.16.0",
@@ -66,6 +67,7 @@
"babel-register": "6.18.0",
"babel-runtime": "6.20.0",
"chai": "3.5.0",
"chai-as-promised": "6.0.0",
"chai-enzyme": "0.6.1",
"circular-dependency-plugin": "2.0.0",
"copy-webpack-plugin": "4.0.1",
@@ -99,8 +101,8 @@
"mock-local-storage": "1.0.2",
"mock-socket": "6.0.3",
"nock": "9.0.2",
"postcss-import": "8.1.0",
"postcss-loader": "1.1.1",
"postcss-import": "9.0.0",
"postcss-loader": "1.2.0",
"postcss-nested": "1.0.0",
"postcss-simple-vars": "3.0.0",
"progress": "1.1.8",
@@ -137,7 +139,7 @@
"js-sha3": "0.5.5",
"lodash": "4.17.2",
"marked": "0.3.6",
"material-ui": "0.16.4",
"material-ui": "0.16.5",
"material-ui-chip-input": "0.11.1",
"mobx": "2.6.4",
"mobx-react": "4.0.3",

View File

@@ -19,7 +19,10 @@ import * as abis from './abi';
export default class Registry {
constructor (api) {
this._api = api;
this._contracts = [];
this._contracts = {};
this._pendingContracts = {};
this._instance = null;
this._fetching = false;
this._queue = [];
@@ -59,20 +62,25 @@ export default class Registry {
getContract (_name) {
const name = _name.toLowerCase();
return new Promise((resolve, reject) => {
if (this._contracts[name]) {
resolve(this._contracts[name]);
return;
}
if (this._contracts[name]) {
return Promise.resolve(this._contracts[name]);
}
this
.lookupAddress(name)
.then((address) => {
this._contracts[name] = this._api.newContract(abis[name], address);
resolve(this._contracts[name]);
})
.catch(reject);
});
if (this._pendingContracts[name]) {
return this._pendingContracts[name];
}
const promise = this
.lookupAddress(name)
.then((address) => {
this._contracts[name] = this._api.newContract(abis[name], address);
delete this._pendingContracts[name];
return this._contracts[name];
});
this._pendingContracts[name] = promise;
return promise;
}
getContractInstance (_name) {
@@ -89,7 +97,7 @@ export default class Registry {
return instance.getAddress.call({}, [sha3, 'A']);
})
.then((address) => {
console.log('lookupAddress', name, sha3, address);
console.log('[lookupAddress]', `(${sha3}) ${name}: ${address}`);
return address;
});
}

View File

@@ -21,7 +21,7 @@ import '../../../environment/tests';
import Application from './application';
describe('localtx/Application', () => {
describe('dapps/localtx/Application', () => {
describe('rendering', () => {
it('renders without crashing', () => {
const rendered = shallow(<Application />);

View File

@@ -29,7 +29,7 @@ Api.api = {
import BigNumber from 'bignumber.js';
import { Transaction, LocalTransaction } from './transaction';
describe('localtx/Transaction', () => {
describe('dapps/localtx/Transaction', () => {
describe('rendering', () => {
it('renders without crashing', () => {
const transaction = {
@@ -51,7 +51,7 @@ describe('localtx/Transaction', () => {
});
});
describe('localtx/LocalTransaction', () => {
describe('dapps/localtx/LocalTransaction', () => {
describe('rendering', () => {
it('renders without crashing', () => {
const rendered = shallow(

View File

@@ -23,6 +23,7 @@ import { wallet as walletAbi } from '~/contracts/abi';
import { wallet as walletCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet';
import { validateUint, validateAddress, validateName } from '~/util/validation';
import { toWei } from '~/api/util/wei';
import WalletsUtils from '~/util/wallets';
const STEPS = {
@@ -47,7 +48,7 @@ export default class CreateWalletStore {
address: '',
owners: [],
required: 1,
daylimit: 0,
daylimit: toWei(1),
name: '',
description: ''

View File

@@ -107,10 +107,9 @@ export default class TransferStore {
constructor (api, props) {
this.api = api;
const { account, balance, gasLimit, senders, onClose, newError, sendersBalances } = props;
const { account, balance, gasLimit, senders, newError, sendersBalances } = props;
this.account = account;
this.balance = balance;
this.onClose = onClose;
this.isWallet = account && account.wallet;
this.newError = newError;
@@ -136,8 +135,7 @@ export default class TransferStore {
this.stage -= 1;
}
@action onClose = () => {
this.onClose && this.onClose();
@action handleClose = () => {
this.stage = 0;
}

View File

@@ -208,7 +208,7 @@ class Transfer extends Component {
<Button
icon={ <ContentClear /> }
label='Cancel'
onClick={ this.store.onClose } />
onClick={ this.handleClose } />
);
const nextBtn = (
<Button
@@ -234,7 +234,7 @@ class Transfer extends Component {
<Button
icon={ <ActionDoneAll /> }
label='Close'
onClick={ this.store.onClose } />
onClick={ this.handleClose } />
);
switch (stage) {
@@ -264,6 +264,13 @@ class Transfer extends Component {
</div>
);
}
handleClose = () => {
const { onClose } = this.props;
this.store.handleClose();
typeof onClose === 'function' && onClose();
}
}
function mapStateToProps (initState, initProps) {

View File

@@ -36,9 +36,6 @@ export default class Personal {
}
this._store.dispatch(personalAccountsInfo(accountsInfo));
})
.then((subscriptionId) => {
console.log('personal._subscribeAccountsInfo', 'subscriptionId', subscriptionId);
});
}

View File

@@ -34,9 +34,6 @@ export default class Signer {
}
this._store.dispatch(signerRequestsToConfirm(pending || []));
})
.then((subscriptionId) => {
console.log('signer._subscribeRequestsToConfirm', 'subscriptionId', subscriptionId);
});
}
}

View File

@@ -59,9 +59,6 @@ export default class Status {
.catch((error) => {
console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error);
});
})
.then((subscriptionId) => {
console.log('status._subscribeBlockNumber', 'subscriptionId', subscriptionId);
});
}

View File

@@ -0,0 +1,38 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { shallow } from 'enzyme';
import Actionbar from './actionbar';
function renderShallow (props) {
return shallow(
<Actionbar { ...props } />
);
}
describe('ui/Actionbar', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
});
});
});

View File

@@ -0,0 +1,38 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { shallow } from 'enzyme';
import Badge from './badge';
function renderShallow (props) {
return shallow(
<Badge { ...props } />
);
}
describe('ui/Badge', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
});
});
});

View File

@@ -17,16 +17,15 @@
import React, { Component, PropTypes } from 'react';
import { FlatButton } from 'material-ui';
import { nodeOrStringProptype } from '~/util/proptypes';
export default class Button extends Component {
static propTypes = {
backgroundColor: PropTypes.string,
className: PropTypes.string,
disabled: PropTypes.bool,
icon: PropTypes.node,
label: PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.object
]),
label: nodeOrStringProptype(),
onClick: PropTypes.func,
primary: PropTypes.bool
}

View File

@@ -0,0 +1,38 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { shallow } from 'enzyme';
import Button from './button';
function renderShallow (props) {
return shallow(
<Button { ...props } />
);
}
describe('ui/Button', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
});
});
});

View File

@@ -27,10 +27,6 @@ export default class Title extends Component {
byline: nodeOrStringProptype()
}
state = {
name: 'Unnamed'
}
render () {
const { className, title, byline } = this.props;

View File

@@ -0,0 +1,52 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { mount, shallow } from 'enzyme';
import Title from './title';
function renderShallow (props) {
return shallow(
<Title { ...props } />
);
}
function renderMount (props) {
return mount(
<Title { ...props } />
);
}
describe('ui/Container/Title', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
});
it('renders the specified title', () => {
expect(renderMount({ title: 'titleText' })).to.contain.text('titleText');
});
it('renders the specified byline', () => {
expect(renderMount({ byline: 'bylineText' })).to.contain.text('bylineText');
});
});
});

View File

@@ -0,0 +1,38 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { shallow } from 'enzyme';
import Container from './container';
function renderShallow (props) {
return shallow(
<Container { ...props } />
);
}
describe('ui/Container', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
it('renders with the specified className', () => {
expect(renderShallow({ className: 'testClass' })).to.have.className('testClass');
});
});
});

View File

@@ -113,32 +113,38 @@ export default class Input extends Component {
<TextField
autoComplete='off'
className={ className }
style={ textFieldStyle }
readOnly={ readOnly }
errorText={ error }
floatingLabelFixed
floatingLabelText={ label }
fullWidth
hintText={ hint }
id={ NAME_ID }
inputStyle={ inputStyle }
fullWidth
max={ max }
min={ min }
multiLine={ multiLine }
name={ NAME_ID }
id={ NAME_ID }
rows={ rows }
type={ type || 'text' }
underlineDisabledStyle={ UNDERLINE_DISABLED }
underlineStyle={ readOnly ? UNDERLINE_READONLY : UNDERLINE_NORMAL }
underlineFocusStyle={ readOnly ? { display: 'none' } : null }
underlineShow={ !hideUnderline }
value={ value }
onBlur={ this.onBlur }
onChange={ this.onChange }
onKeyDown={ this.onKeyDown }
onPaste={ this.onPaste }
inputStyle={ inputStyle }
min={ min }
max={ max }
readOnly={ readOnly }
rows={ rows }
style={ textFieldStyle }
type={ type || 'text' }
underlineDisabledStyle={ UNDERLINE_DISABLED }
underlineStyle={ readOnly ? UNDERLINE_READONLY : UNDERLINE_NORMAL }
underlineFocusStyle={ readOnly ? { display: 'none' } : null }
underlineShow={ !hideUnderline }
value={ value }
>
{ children }
</TextField>

View File

@@ -53,13 +53,13 @@ export default class TypedInput extends Component {
};
state = {
isEth: true,
isEth: false,
ethValue: 0
};
componentDidMount () {
componentWillMount () {
if (this.props.isEth && this.props.value) {
this.setState({ ethValue: fromWei(this.props.value) });
this.setState({ isEth: true, ethValue: fromWei(this.props.value) });
}
}
@@ -164,28 +164,32 @@ export default class TypedInput extends Component {
}
if (type === ABI_TYPES.INT) {
return this.renderNumber();
return this.renderEth();
}
if (type === ABI_TYPES.FIXED) {
return this.renderNumber();
return this.renderFloat();
}
return this.renderDefault();
}
renderEth () {
const { ethValue } = this.state;
const { ethValue, isEth } = this.state;
const value = ethValue && typeof ethValue.toNumber === 'function'
? ethValue.toNumber()
: ethValue;
const input = isEth
? this.renderFloat(value, this.onEthValueChange)
: this.renderInteger(value, this.onEthValueChange);
return (
<div className={ styles.ethInput }>
<div className={ styles.input }>
{ this.renderNumber(value, this.onEthValueChange) }
{ this.state.isEth ? (<div className={ styles.label }>ETH</div>) : null }
{ input }
{ isEth ? (<div className={ styles.label }>ETH</div>) : null }
</div>
<div className={ styles.toggle }>
<Toggle
@@ -198,8 +202,9 @@ export default class TypedInput extends Component {
);
}
renderNumber (value = this.props.value, onChange = this.onChange) {
renderInteger (value = this.props.value, onChange = this.onChange) {
const { label, error, param, hint, min, max } = this.props;
const realValue = value && typeof value.toNumber === 'function'
? value.toNumber()
: value;
@@ -212,6 +217,35 @@ export default class TypedInput extends Component {
error={ error }
onChange={ onChange }
type='number'
step={ 1 }
min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : null }
/>
);
}
/**
* Decimal numbers have to be input via text field
* because of some react issues with input number fields.
* Once the issue is fixed, this could be a number again.
*
* @see https://github.com/facebook/react/issues/1549
*/
renderFloat (value = this.props.value, onChange = this.onChange) {
const { label, error, param, hint, min, max } = this.props;
const realValue = value && typeof value.toNumber === 'function'
? value.toNumber()
: value;
return (
<Input
label={ label }
hint={ hint }
value={ realValue }
error={ error }
onChange={ onChange }
type='text'
min={ min !== null ? min : (param.signed ? null : 0) }
max={ max !== null ? max : null }
/>

View File

@@ -0,0 +1,76 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { mount } from 'enzyme';
import sinon from 'sinon';
import IdentityName from './identityName';
const ADDR_A = '0x123456789abcdef0123456789A';
const ADDR_B = '0x123456789abcdef0123456789B';
const ADDR_C = '0x123456789abcdef0123456789C';
const STORE = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
balances: {
tokens: {}
},
personal: {
accountsInfo: {
[ADDR_A]: { name: 'testing' },
[ADDR_B]: {}
}
}
};
}
};
function render (props) {
return mount(
<IdentityName
store={ STORE }
{ ...props } />
);
}
describe('ui/IdentityName', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
describe('account not found', () => {
it('renders null with empty', () => {
expect(render({ address: ADDR_C, empty: true }).html()).to.be.null;
});
it('renders address without empty', () => {
expect(render({ address: ADDR_C }).text()).to.equal(ADDR_C);
});
it('renders short address with shorten', () => {
expect(render({ address: ADDR_C, shorten: true }).text()).to.equal('123456…56789c');
});
it('renders unknown with flag', () => {
expect(render({ address: ADDR_C, unknown: true }).text()).to.equal('UNNAMED');
});
});
});
});

View File

@@ -17,8 +17,8 @@
.layout {
padding: 0.25em 0.25em 1em 0.25em;
}
.layout>div {
padding-bottom: 0.75em;
> * {
margin-bottom: 0.75em;
}
}

View File

@@ -0,0 +1,55 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
const muiTheme = getMuiTheme(lightBaseTheme);
import theme from './theme';
describe('ui/Theme', () => {
it('is MUI-based', () => {
expect(Object.keys(theme)).to.deep.equal(Object.keys(muiTheme).concat('parity'));
});
it('allows setting of Parity backgrounds', () => {
expect(typeof theme.parity.setBackgroundSeed === 'function').to.be.true;
expect(typeof theme.parity.getBackgroundStyle === 'function').to.be.true;
});
describe('parity', () => {
describe('setBackgroundSeed', () => {
const SEED = 'testseed';
beforeEach(() => {
theme.parity.setBackgroundSeed(SEED);
});
it('sets the correct theme values', () => {
expect(theme.parity.backgroundSeed).to.equal(SEED);
});
});
describe('getBackgroundStyle', () => {
it('generates a style containing background', () => {
const style = theme.parity.getBackgroundStyle();
expect(style).to.have.property('background');
});
});
});
});

View File

@@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './txRow';

View File

@@ -0,0 +1,133 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { txLink, addressLink } from '~/3rdparty/etherscan/links';
import IdentityIcon from '../../IdentityIcon';
import IdentityName from '../../IdentityName';
import MethodDecoding from '../../MethodDecoding';
import styles from '../txList.css';
export default class TxRow extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
tx: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
block: PropTypes.object,
historic: PropTypes.bool,
className: PropTypes.string
};
static defaultProps = {
historic: true
};
render () {
const { tx, address, isTest, historic, className } = this.props;
return (
<tr className={ className || '' }>
{ this.renderBlockNumber(tx.blockNumber) }
{ this.renderAddress(tx.from) }
<td className={ styles.transaction }>
{ this.renderEtherValue(tx.value) }
<div></div>
<div>
<a
className={ styles.link }
href={ txLink(tx.hash, isTest) }
target='_blank'>
{ `${tx.hash.substr(2, 6)}...${tx.hash.slice(-6)}` }
</a>
</div>
</td>
{ this.renderAddress(tx.to) }
<td className={ styles.method }>
<MethodDecoding
historic={ historic }
address={ address }
transaction={ tx } />
</td>
</tr>
);
}
renderAddress (address) {
const { isTest } = this.props;
let esLink = null;
if (address) {
esLink = (
<a
href={ addressLink(address, isTest) }
target='_blank'
className={ styles.link }>
<IdentityName address={ address } shorten />
</a>
);
}
return (
<td className={ styles.address }>
<div className={ styles.center }>
<IdentityIcon
center
className={ styles.icon }
address={ address } />
</div>
<div className={ styles.center }>
{ esLink || 'DEPLOY' }
</div>
</td>
);
}
renderEtherValue (_value) {
const { api } = this.context;
const value = api.util.fromWei(_value);
if (value.eq(0)) {
return <div className={ styles.value }>{ ' ' }</div>;
}
return (
<div className={ styles.value }>
{ value.toFormat(5) }<small>ETH</small>
</div>
);
}
renderBlockNumber (_blockNumber) {
const { block } = this.props;
const blockNumber = _blockNumber.toNumber();
return (
<td className={ styles.timestamp }>
<div>{ blockNumber && block ? moment(block.timestamp).fromNow() : null }</div>
<div>{ blockNumber ? _blockNumber.toFormat() : 'Pending' }</div>
</td>
);
}
}

View File

@@ -0,0 +1,51 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import Api from '~/api';
import TxRow from './txRow';
const api = new Api({ execute: sinon.stub() });
function renderShallow (props) {
return shallow(
<TxRow
{ ...props } />,
{ context: { api } }
);
}
describe('ui/TxRow', () => {
describe('rendering', () => {
it('renders defaults', () => {
const block = {
timestamp: new Date()
};
const tx = {
blockNumber: new BigNumber(123),
hash: '0x123456789abcdef0123456789abcdef0123456789abcdef',
value: new BigNumber(1)
};
expect(renderShallow({ block, tx })).to.be.ok;
});
});
});

View File

@@ -45,6 +45,8 @@ export default class Store {
if (bnB.eq(0)) {
return bnB.eq(bnA) ? 0 : 1;
} else if (bnA.eq(0)) {
return -1;
}
return bnB.comparedTo(bnA);

View File

@@ -0,0 +1,90 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import sinon from 'sinon';
import Store from './store';
const SUBID = 123;
const BLOCKS = {
1: { blockhash: '0x1' },
2: { blockhash: '0x2' }
};
const TRANSACTIONS = {
'0x123': { blockNumber: new BigNumber(1) },
'0x234': { blockNumber: new BigNumber(0) },
'0x345': { blockNumber: new BigNumber(2) },
'0x456': { blockNumber: new BigNumber(0) }
};
describe('ui/TxList/store', () => {
let api;
let store;
beforeEach(() => {
api = {
subscribe: sinon.stub().resolves(SUBID),
eth: {
getBlockByNumber: (blockNumber) => {
return Promise.resolve(BLOCKS[blockNumber]);
}
}
};
store = new Store(api);
});
describe('create', () => {
it('has empty storage', () => {
expect(store.blocks).to.deep.equal({});
expect(store.sortedHashes.peek()).to.deep.equal([]);
expect(store.transactions).to.deep.equal({});
});
it('subscribes to eth_blockNumber', () => {
expect(api.subscribe).to.have.been.calledWith('eth_blockNumber');
expect(store._subscriptionId).to.equal(SUBID);
});
});
describe('addBlocks', () => {
beforeEach(() => {
store.addBlocks(BLOCKS);
});
it('adds the blocks to the list', () => {
expect(store.blocks).to.deep.equal(BLOCKS);
});
});
describe('addTransactions', () => {
beforeEach(() => {
store.addTransactions(TRANSACTIONS);
});
it('adds all transactions to the list', () => {
expect(store.transactions).to.deep.equal(TRANSACTIONS);
});
it('sorts transactions based on blockNumber', () => {
expect(store.sortedHashes.peek()).to.deep.equal(['0x234', '0x456', '0x345', '0x123']);
});
it('adds pending transactions to the pending queue', () => {
expect(store._pendingHashes).to.deep.equal(['0x234', '0x456']);
});
});
});

View File

@@ -14,128 +14,16 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react';
import { txLink, addressLink } from '~/3rdparty/etherscan/links';
import IdentityIcon from '../IdentityIcon';
import IdentityName from '../IdentityName';
import MethodDecoding from '../MethodDecoding';
import Store from './store';
import TxRow from './TxRow';
import styles from './txList.css';
export class TxRow extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
tx: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
isTest: PropTypes.bool.isRequired,
block: PropTypes.object,
historic: PropTypes.bool,
className: PropTypes.string
};
static defaultProps = {
historic: true
};
render () {
const { tx, address, isTest, historic, className } = this.props;
return (
<tr className={ className || '' }>
{ this.renderBlockNumber(tx.blockNumber) }
{ this.renderAddress(tx.from) }
<td className={ styles.transaction }>
{ this.renderEtherValue(tx.value) }
<div></div>
<div>
<a
className={ styles.link }
href={ txLink(tx.hash, isTest) }
target='_blank'>
{ `${tx.hash.substr(2, 6)}...${tx.hash.slice(-6)}` }
</a>
</div>
</td>
{ this.renderAddress(tx.to) }
<td className={ styles.method }>
<MethodDecoding
historic={ historic }
address={ address }
transaction={ tx } />
</td>
</tr>
);
}
renderAddress (address) {
const { isTest } = this.props;
let esLink = null;
if (address) {
esLink = (
<a
href={ addressLink(address, isTest) }
target='_blank'
className={ styles.link }>
<IdentityName address={ address } shorten />
</a>
);
}
return (
<td className={ styles.address }>
<div className={ styles.center }>
<IdentityIcon
center
className={ styles.icon }
address={ address } />
</div>
<div className={ styles.center }>
{ esLink || 'DEPLOY' }
</div>
</td>
);
}
renderEtherValue (_value) {
const { api } = this.context;
const value = api.util.fromWei(_value);
if (value.eq(0)) {
return <div className={ styles.value }>{ ' ' }</div>;
}
return (
<div className={ styles.value }>
{ value.toFormat(5) }<small>ETH</small>
</div>
);
}
renderBlockNumber (_blockNumber) {
const { block } = this.props;
const blockNumber = _blockNumber.toNumber();
return (
<td className={ styles.timestamp }>
<div>{ blockNumber && block ? moment(block.timestamp).fromNow() : null }</div>
<div>{ blockNumber ? _blockNumber.toFormat() : 'Pending' }</div>
</td>
);
}
}
@observer
class TxList extends Component {
static contextTypes = {

View File

@@ -0,0 +1,54 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import Api from '~/api';
import TxList from './txList';
const api = new Api({ execute: sinon.stub() });
const STORE = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
nodeStatus: {
isTest: true
}
};
}
};
function renderShallow (props) {
return shallow(
<TxList
store={ STORE }
{ ...props } />,
{ context: { api } }
);
}
describe('ui/TxList', () => {
describe('rendering', () => {
it('renders defaults', () => {
expect(renderShallow()).to.be.ok;
});
});
});

View File

@@ -14,9 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { range } from 'lodash';
import { range, uniq } from 'lodash';
import { bytesToHex, toHex } from '~/api/util/format';
import { validateAddress } from '~/util/validation';
export default class WalletsUtils {
@@ -26,10 +27,82 @@ export default class WalletsUtils {
static fetchOwners (walletContract) {
const walletInstance = walletContract.instance;
return walletInstance
.m_numOwners.call()
.then((mNumOwners) => {
return Promise.all(range(mNumOwners.toNumber()).map((idx) => walletInstance.getOwner.call({}, [ idx ])));
const promises = range(mNumOwners.toNumber())
.map((idx) => walletInstance.getOwner.call({}, [ idx ]));
return Promise
.all(promises)
.then((owners) => {
const uniqOwners = uniq(owners);
// If all owners are the zero account : must be Mist wallet contract
if (uniqOwners.length === 1 && /^(0x)?0*$/.test(owners[0])) {
return WalletsUtils.fetchMistOwners(walletContract, mNumOwners.toNumber());
}
return owners;
});
});
}
static fetchMistOwners (walletContract, mNumOwners) {
const walletAddress = walletContract.address;
return WalletsUtils
.getMistOwnersOffset(walletContract)
.then((result) => {
if (!result || result.offset === -1) {
return [];
}
const owners = [ result.address ];
if (mNumOwners === 1) {
return owners;
}
const initOffset = result.offset + 1;
let promise = Promise.resolve();
range(initOffset, initOffset + mNumOwners - 1).forEach((offset) => {
promise = promise
.then(() => {
return walletContract.api.eth.getStorageAt(walletAddress, offset);
})
.then((result) => {
const resultAddress = '0x' + (result || '').slice(-40);
const { address } = validateAddress(resultAddress);
owners.push(address);
});
});
return promise.then(() => owners);
});
}
static getMistOwnersOffset (walletContract, offset = 3) {
return walletContract.api.eth
.getStorageAt(walletContract.address, offset)
.then((result) => {
if (result && !/^(0x)?0*$/.test(result)) {
const resultAddress = '0x' + result.slice(-40);
const { address, addressError } = validateAddress(resultAddress);
if (!addressError) {
return { offset, address };
}
}
if (offset >= 100) {
return { offset: -1 };
}
return WalletsUtils.getMistOwnersOffset(walletContract, offset + 1);
});
}

View File

@@ -31,12 +31,14 @@ export default class Header extends Component {
account: PropTypes.object,
balance: PropTypes.object,
className: PropTypes.string,
children: PropTypes.node
children: PropTypes.node,
isContract: PropTypes.bool
};
static defaultProps = {
className: '',
children: null
children: null,
isContract: false
};
render () {
@@ -88,9 +90,9 @@ export default class Header extends Component {
}
renderTxCount () {
const { balance } = this.props;
const { balance, isContract } = this.props;
if (!balance) {
if (!balance || isContract) {
return null;
}

View File

@@ -75,6 +75,13 @@ export default class Summary extends Component {
return true;
}
const prevOwners = this.props.owners;
const nextOwners = nextProps.owners;
if (!isEqual(prevOwners, nextOwners)) {
return true;
}
return false;
}
@@ -123,8 +130,8 @@ export default class Summary extends Component {
return (
<div className={ styles.owners }>
{
ownersValid.map((owner) => (
<div key={ owner.address }>
ownersValid.map((owner, index) => (
<div key={ `${index}_${owner.address}` }>
<div
data-tip
data-for={ `owner_${owner.address}` }

View File

@@ -188,7 +188,7 @@ class TabBar extends Component {
return (
<ToolbarGroup>
<div className={ styles.logo }>
<img src={ imagesEthcoreBlock } />
<img src={ imagesEthcoreBlock } height={ 28 } />
</div>
</ToolbarGroup>
);

View File

@@ -17,7 +17,7 @@
import React, { Component, PropTypes } from 'react';
import { uniq } from 'lodash';
import { Container } from '~/ui';
import { Container, Loading } from '~/ui';
import Event from './Event';
import styles from '../contract.css';
@@ -25,18 +25,38 @@ import styles from '../contract.css';
export default class Events extends Component {
static contextTypes = {
api: PropTypes.object
}
};
static propTypes = {
events: PropTypes.array,
isTest: PropTypes.bool.isRequired
}
isTest: PropTypes.bool.isRequired,
isLoading: PropTypes.bool,
events: PropTypes.array
};
static defaultProps = {
isLoading: false,
events: []
};
render () {
const { events, isTest } = this.props;
const { events, isTest, isLoading } = this.props;
if (isLoading) {
return (
<Container title='events'>
<div>
<Loading size={ 2 } />
</div>
</Container>
);
}
if (!events || !events.length) {
return null;
return (
<Container title='events'>
<p>No events has been sent from this contract.</p>
</Container>
);
}
const eventsKey = uniq(events.map((e) => e.key));

View File

@@ -54,6 +54,10 @@ export default class Queries extends Component {
.filter((fn) => fn.inputs.length > 0)
.map((fn) => this.renderInputQuery(fn));
if (queries.length + noInputQueries.length + withInputQueries.length === 0) {
return null;
}
return (
<Container title='queries'>
<div className={ styles.methods }>

View File

@@ -40,7 +40,7 @@ import styles from './contract.css';
class Contract extends Component {
static contextTypes = {
api: React.PropTypes.object.isRequired
}
};
static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired,
@@ -50,7 +50,7 @@ class Contract extends Component {
contracts: PropTypes.object,
isTest: PropTypes.bool,
params: PropTypes.object
}
};
state = {
contract: null,
@@ -64,8 +64,9 @@ class Contract extends Component {
allEvents: [],
minedEvents: [],
pendingEvents: [],
queryValues: {}
}
queryValues: {},
loadingEvents: true
};
componentDidMount () {
const { api } = this.context;
@@ -115,7 +116,7 @@ class Contract extends Component {
render () {
const { balances, contracts, params, isTest } = this.props;
const { allEvents, contract, queryValues } = this.state;
const { allEvents, contract, queryValues, loadingEvents } = this.state;
const account = contracts[params.address];
const balance = balances[params.address];
@@ -133,13 +134,19 @@ class Contract extends Component {
<Header
account={ account }
balance={ balance }
isContract
/>
<Queries
contract={ contract }
values={ queryValues } />
values={ queryValues }
/>
<Events
isTest={ isTest }
events={ allEvents } />
isLoading={ loadingEvents }
events={ allEvents }
/>
{ this.renderDetails(account) }
</Page>
@@ -358,6 +365,10 @@ class Contract extends Component {
}
_receiveEvents = (error, logs) => {
if (this.state.loadingEvents) {
this.setState({ loadingEvents: false });
}
if (error) {
console.error('_receiveEvents', error);
return;

View File

@@ -17,7 +17,7 @@
import BigNumber from 'bignumber.js';
import { getShortData, getFee, getTotalValue } from './transaction';
describe('util/transaction', () => {
describe('views/Signer/components/util/transaction', () => {
describe('getEstimatedMiningTime', () => {
it('should return estimated mining time', () => {
});

View File

@@ -21,7 +21,7 @@ import getMuiTheme from 'material-ui/styles/getMuiTheme';
import WrappedAutoComplete from './AutoComplete';
describe('components/AutoComplete', () => {
describe('views/Status/components/AutoComplete', () => {
describe('rendering', () => {
let rendered;

View File

@@ -19,7 +19,7 @@ import { shallow } from 'enzyme';
import Box from './Box';
describe('components/Box', () => {
describe('views/Status/components/Box', () => {
describe('rendering', () => {
const title = 'test title';
let rendered;

View File

@@ -22,7 +22,7 @@ import '../../../../environment/tests';
import Call from './Call';
describe('components/Call', () => {
describe('views/Status/components/Call', () => {
const call = { callIdx: 123, callNo: 456, name: 'eth_call', params: [{ name: '123' }], response: '' };
const element = 'dummyElement';

View File

@@ -21,7 +21,7 @@ import '../../../../environment/tests';
import Calls from './Calls';
describe('components/Calls', () => {
describe('views/Status/components/Calls', () => {
describe('rendering (no calls)', () => {
let rendered;

View File

@@ -22,7 +22,7 @@ import '../../../../environment/tests';
import CallsToolbar from './CallsToolbar';
describe('components/CallsToolbar', () => {
describe('views/Status/components/CallsToolbar', () => {
const callEl = { offsetTop: 0 };
const containerEl = { scrollTop: 0, clientHeight: 0, scrollHeight: 999 };

View File

@@ -16,7 +16,7 @@
import { decodeExtraData } from './decodeExtraData';
describe('MINING SETTINGS', () => {
describe('views/Status/components/MiningSettings/decodeExtraData', () => {
describe('EXTRA DATA', () => {
const str = 'parity/1.0.0/1.0.0-beta2';
const encoded = '0xd783010000867061726974798b312e302e302d6265746132';

View File

@@ -16,7 +16,7 @@
import { numberFromString } from './numberFromString';
describe('NUMBER FROM STRING', () => {
describe('views/Status/components/MiningSettings/numberFromString', () => {
it('should convert string to number', () => {
expect(numberFromString('12345'), 12345);
});

View File

@@ -21,7 +21,7 @@ import '../../../../environment/tests';
import Response from './Response';
describe('components/Response', () => {
describe('views/Status/components/Response', () => {
describe('rendering', () => {
it('renders non-arrays/non-objects exactly as received', () => {
const TEST = '1234567890';

View File

@@ -21,7 +21,7 @@ import { syncRpcStateFromLocalStorage } from '../actions/localstorage';
import rpcData from '../data/rpc.json';
import LocalStorageMiddleware from './localstorage';
describe('MIDDLEWARE: LOCAL STORAGE', () => {
describe('views/Status/middleware/localstorage', () => {
let cut, state;
beforeEach('mock cut', () => {

View File

@@ -17,7 +17,7 @@
import sinon from 'sinon';
import * as ErrorUtil from './error';
describe('util/error', () => {
describe('views/Status/util/error', () => {
beforeEach('spy on isError', () => {
sinon.spy(ErrorUtil, 'isError');
});

View File

@@ -16,7 +16,7 @@
import { toPromise, identity } from './';
describe('util', () => {
describe('views/Status/util', () => {
describe('toPromise', () => {
it('rejects on error result', () => {
const ERROR = new Error();

View File

@@ -23,7 +23,7 @@ import { bindActionCreators } from 'redux';
import { confirmOperation, revokeOperation } from '~/redux/providers/walletActions';
import { bytesToHex } from '~/api/util/format';
import { Container, InputAddress, Button, IdentityIcon } from '~/ui';
import { TxRow } from '~/ui/TxList/txList';
import TxRow from '~/ui/TxList/TxRow';
import styles from '../wallet.css';
import txListStyles from '~/ui/TxList/txList.css';

View File

@@ -55,9 +55,9 @@ export default class WalletDetails extends Component {
return null;
}
const ownersList = owners.map((address) => (
const ownersList = owners.map((address, idx) => (
<InputAddress
key={ address }
key={ `${idx}_${address}` }
value={ address }
disabled
text

View File

@@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { bytesToHex } from '~/api/util/format';
import { Container } from '~/ui';
import { TxRow } from '~/ui/TxList/txList';
import TxRow from '~/ui/TxList/TxRow';
import txListStyles from '~/ui/TxList/txList.css';

View File

@@ -127,6 +127,7 @@ class Wallet extends Component {
className={ styles.header }
account={ wallet }
balance={ balance }
isContract
>
{ this.renderInfos() }
</Header>
@@ -152,7 +153,13 @@ class Wallet extends Component {
return null;
}
const limit = api.util.fromWei(dailylimit.limit).toFormat(3);
const _limit = api.util.fromWei(dailylimit.limit);
if (_limit.equals(0)) {
return null;
}
const limit = _limit.toFormat(3);
const spent = api.util.fromWei(dailylimit.spent).toFormat(3);
const date = moment(dailylimit.last.toNumber() * 24 * 3600 * 1000);

View File

@@ -22,11 +22,13 @@ es6Promise.polyfill();
import 'mock-local-storage';
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import chaiEnzyme from 'chai-enzyme';
import sinonChai from 'sinon-chai';
import { WebSocket } from 'mock-socket';
import jsdom from 'jsdom';
chai.use(chaiAsPromised);
chai.use(chaiEnzyme());
chai.use(sinonChai);

26
js/webpack/test.js Normal file
View File

@@ -0,0 +1,26 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
const path = require('path');
module.exports = {
context: path.join(__dirname, '../src'),
resolve: {
alias: {
'~': path.resolve(__dirname, '../src')
}
}
};