Merge remote-tracking branch 'origin/master' into check-updates

This commit is contained in:
Gav Wood
2016-12-15 15:12:09 +01:00
43 changed files with 729 additions and 238 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "parity.js",
"version": "0.2.122",
"version": "0.2.124",
"main": "release/index.js",
"jsnext:main": "src/index.js",
"author": "Parity Team <admin@parity.io>",

View File

@@ -18,7 +18,8 @@ import { bytesToHex, hex2Ascii } from '~/api/util/format';
import ABI from './abi/certifier.json';
const ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000';
const ZERO20 = '0x0000000000000000000000000000000000000000';
const ZERO32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
export default class BadgeReg {
constructor (api, registry) {
@@ -26,32 +27,57 @@ export default class BadgeReg {
this._registry = registry;
registry.getContract('badgereg');
this.certifiers = {}; // by name
this.certifiers = []; // by id
this.contracts = {}; // by name
}
fetchCertifier (name) {
if (this.certifiers[name]) {
return Promise.resolve(this.certifiers[name]);
certifierCount () {
return this._registry.getContract('badgereg')
.then((badgeReg) => {
return badgeReg.instance.badgeCount.call({}, [])
.then((count) => count.valueOf());
});
}
fetchCertifier (id) {
if (this.certifiers[id]) {
return Promise.resolve(this.certifiers[id]);
}
return this._registry.getContract('badgereg')
.then((badgeReg) => {
return badgeReg.instance.fromName.call({}, [name])
.then(([ id, address ]) => {
return Promise.all([
badgeReg.instance.meta.call({}, [id, 'TITLE']),
badgeReg.instance.meta.call({}, [id, 'IMG'])
])
.then(([ title, img ]) => {
title = bytesToHex(title);
title = title === ZERO ? null : hex2Ascii(title);
if (bytesToHex(img) === ZERO) img = null;
return badgeReg.instance.badge.call({}, [ id ]);
})
.then(([ address, name ]) => {
if (address === ZERO20) {
throw new Error(`Certifier ${id} does not exist.`);
}
const data = { address, name, title, icon: img };
this.certifiers[name] = data;
return data;
});
});
name = bytesToHex(name);
name = name === ZERO32
? null
: hex2Ascii(name);
return this.fetchMeta(id)
.then(({ title, icon }) => {
const data = { address, id, name, title, icon };
this.certifiers[id] = data;
return data;
});
});
}
fetchMeta (id) {
return this._registry.getContract('badgereg')
.then((badgeReg) => {
return Promise.all([
badgeReg.instance.meta.call({}, [id, 'TITLE']),
badgeReg.instance.meta.call({}, [id, 'IMG'])
]);
})
.then(([ title, icon ]) => {
title = bytesToHex(title);
title = title === ZERO32 ? null : hex2Ascii(title);
if (bytesToHex(icon) === ZERO32) icon = null;
return { title, icon };
});
}

View File

@@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import ActionDone from 'material-ui/svg-icons/action/done';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
@@ -35,14 +36,15 @@ import ParityLogo from '../../../assets/images/parity-logo-black-no-text.svg';
const STAGE_NAMES = ['welcome', 'terms', 'new account', 'recovery', 'completed'];
export default class FirstRun extends Component {
class FirstRun extends Component {
static contextTypes = {
api: PropTypes.object.isRequired,
store: PropTypes.object.isRequired
}
static propTypes = {
visible: PropTypes.bool,
hasAccounts: PropTypes.bool.isRequired,
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired
}
@@ -109,6 +111,7 @@ export default class FirstRun extends Component {
}
renderDialogActions () {
const { hasAccounts } = this.props;
const { canCreate, stage, hasAcceptedTnc } = this.state;
switch (stage) {
@@ -130,13 +133,26 @@ export default class FirstRun extends Component {
);
case 2:
return (
const buttons = [
<Button
icon={ <ActionDone /> }
label='Create'
key='create'
disabled={ !canCreate }
onClick={ this.onCreate } />
);
onClick={ this.onCreate }
/>
];
if (hasAccounts) {
buttons.unshift(
<Button
icon={ <NavigationArrowForward /> }
label='Skip'
key='skip'
onClick={ this.skipAccountCreation }
/>
);
}
return buttons;
case 3:
return [
@@ -219,6 +235,10 @@ export default class FirstRun extends Component {
});
}
skipAccountCreation = () => {
this.setState({ stage: this.state.stage + 2 });
}
newError = (error) => {
const { store } = this.context;
@@ -232,3 +252,9 @@ export default class FirstRun extends Component {
print(recoveryPage({ phrase, name, identity, address, logo: ParityLogo }));
}
}
function mapStateToProps (state) {
return { hasAccounts: state.personal.hasAccounts };
}
export default connect(mapStateToProps, null)(FirstRun);

View File

@@ -14,10 +14,18 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export const fetchCertifiers = () => ({
type: 'fetchCertifiers'
});
export const fetchCertifications = (address) => ({
type: 'fetchCertifications', address
});
export const addCertification = (address, name, title, icon) => ({
type: 'addCertification', address, name, title, icon
export const addCertification = (address, id, name, title, icon) => ({
type: 'addCertification', address, id, name, title, icon
});
export const removeCertification = (address, id) => ({
type: 'removeCertification', address, id
});

View File

@@ -14,38 +14,90 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import Contracts from '~/contracts';
import { addCertification } from './actions';
import { uniq } from 'lodash';
const knownCertifiers = [ 'smsverification' ];
import ABI from '~/contracts/abi/certifier.json';
import Contract from '~/api/contract';
import Contracts from '~/contracts';
import { addCertification, removeCertification } from './actions';
export default class CertificationsMiddleware {
toMiddleware () {
return (store) => (next) => (action) => {
if (action.type !== 'fetchCertifications') {
return next(action);
}
const api = Contracts.get()._api;
const badgeReg = Contracts.get().badgeReg;
const contract = new Contract(api, ABI);
const Confirmed = contract.events.find((e) => e.name === 'Confirmed');
const Revoked = contract.events.find((e) => e.name === 'Revoked');
const { address } = action;
const badgeReg = Contracts.get().badgeReg;
let certifiers = [];
let accounts = []; // these are addresses
knownCertifiers.forEach((name) => {
badgeReg.fetchCertifier(name)
.then((cert) => {
return badgeReg.checkIfCertified(cert.address, address)
.then((isCertified) => {
if (isCertified) {
const { name, title, icon } = cert;
store.dispatch(addCertification(address, name, title, icon));
}
});
})
.catch((err) => {
if (err) {
console.error(`Failed to check if ${address} certified by ${name}:`, err);
const fetchConfirmedEvents = (dispatch) => {
if (certifiers.length === 0 || accounts.length === 0) return;
api.eth.getLogs({
fromBlock: 0,
toBlock: 'latest',
address: certifiers.map((c) => c.address),
topics: [ [ Confirmed.signature, Revoked.signature ], accounts ]
})
.then((logs) => contract.parseEventLogs(logs))
.then((logs) => {
logs.forEach((log) => {
const certifier = certifiers.find((c) => c.address === log.address);
if (!certifier) {
throw new Error(`Could not find certifier at ${log.address}.`);
}
const { id, name, title, icon } = certifier;
if (log.event === 'Revoked') {
dispatch(removeCertification(log.params.who.value, id));
} else {
dispatch(addCertification(log.params.who.value, id, name, title, icon));
}
});
});
})
.catch((err) => {
console.error('Failed to fetch Confirmed events:', err);
});
};
return (store) => (next) => (action) => {
switch (action.type) {
case 'fetchCertifiers':
badgeReg.certifierCount().then((count) => {
new Array(+count).fill(null).forEach((_, id) => {
badgeReg.fetchCertifier(id)
.then((cert) => {
if (!certifiers.some((c) => c.id === cert.id)) {
certifiers = certifiers.concat(cert);
fetchConfirmedEvents(store.dispatch);
}
})
.catch((err) => {
console.warn(`Could not fetch certifier ${id}:`, err);
});
});
});
break;
case 'fetchCertifications':
const { address } = action;
if (!accounts.includes(address)) {
accounts = accounts.concat(address);
fetchConfirmedEvents(store.dispatch);
}
break;
case 'setVisibleAccounts':
const { addresses } = action;
accounts = uniq(accounts.concat(addresses));
fetchConfirmedEvents(store.dispatch);
break;
default:
next(action);
}
};
}
}

View File

@@ -17,17 +17,27 @@
const initialState = {};
export default (state = initialState, action) => {
if (action.type !== 'addCertification') {
return state;
if (action.type === 'addCertification') {
const { address, id, name, icon, title } = action;
const certifications = state[address] || [];
if (certifications.some((c) => c.id === id)) {
return state;
}
const newCertifications = certifications.concat({
id, name, icon, title
});
return { ...state, [address]: newCertifications };
}
const { address, name, icon, title } = action;
const certifications = state[address] || [];
if (action.type === 'removeCertification') {
const { address, id } = action;
const certifications = state[address] || [];
if (certifications.some((c) => c.name === name)) {
return state;
const newCertifications = certifications.filter((c) => c.id !== id);
return { ...state, [address]: newCertifications };
}
const newCertifications = certifications.concat({ name, icon, title });
return { ...state, [address]: newCertifications };
return state;
};

View File

@@ -16,10 +16,8 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { hashToImageUrl } from '~/redux/providers/imagesReducer';
import { fetchCertifications } from '~/redux/providers/certifications/actions';
import defaultIcon from '../../../assets/images/certifications/unknown.svg';
@@ -29,14 +27,7 @@ class Certifications extends Component {
static propTypes = {
account: PropTypes.string.isRequired,
certifications: PropTypes.array.isRequired,
dappsUrl: PropTypes.string.isRequired,
fetchCertifications: PropTypes.func.isRequired
}
componentWillMount () {
const { account, fetchCertifications } = this.props;
fetchCertifications(account);
dappsUrl: PropTypes.string.isRequired
}
render () {
@@ -73,15 +64,13 @@ function mapStateToProps (_, initProps) {
return (state) => {
const certifications = state.certifications[account] || [];
return { certifications };
};
}
const dappsUrl = state.api.dappsUrl;
function mapDispatchToProps (dispatch) {
return bindActionCreators({ fetchCertifications }, dispatch);
return { certifications, dappsUrl };
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
null
)(Certifications);

View File

@@ -23,10 +23,6 @@ import Certifications from '~/ui/Certifications';
import styles from './header.css';
export default class Header extends Component {
static contextTypes = {
api: PropTypes.object
};
static propTypes = {
account: PropTypes.object,
balance: PropTypes.object,
@@ -44,7 +40,6 @@ export default class Header extends Component {
};
render () {
const { api } = this.context;
const { account, balance, className, children, hideName } = this.props;
const { address, meta, uuid } = account;
@@ -85,7 +80,6 @@ export default class Header extends Component {
balance={ balance } />
<Certifications
account={ account.address }
dappsUrl={ api.dappsUrl }
/>
</div>
{ children }

View File

@@ -31,6 +31,7 @@ import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
import Header from './Header';
import Transactions from './Transactions';
import { setVisibleAccounts } from '~/redux/providers/personalActions';
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
import SMSVerificationStore from '~/modals/Verification/sms-store';
import EmailVerificationStore from '~/modals/Verification/email-store';
@@ -44,6 +45,8 @@ class Account extends Component {
static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired,
fetchCertifiers: PropTypes.func.isRequired,
fetchCertifications: PropTypes.func.isRequired,
images: PropTypes.object.isRequired,
params: PropTypes.object,
@@ -63,6 +66,7 @@ class Account extends Component {
}
componentDidMount () {
this.props.fetchCertifiers();
this.setVisibleAccounts();
}
@@ -80,9 +84,10 @@ class Account extends Component {
}
setVisibleAccounts (props = this.props) {
const { params, setVisibleAccounts } = props;
const { params, setVisibleAccounts, fetchCertifications } = props;
const addresses = [ params.address ];
setVisibleAccounts(addresses);
fetchCertifications(params.address);
}
render () {
@@ -353,7 +358,9 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return bindActionCreators({
setVisibleAccounts
setVisibleAccounts,
fetchCertifiers,
fetchCertifications
}, dispatch);
}

View File

@@ -15,22 +15,29 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Container } from '~/ui';
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
import Summary from '../Summary';
import styles from './list.css';
export default class List extends Component {
class List extends Component {
static propTypes = {
accounts: PropTypes.object,
walletsOwners: PropTypes.object,
balances: PropTypes.object,
link: PropTypes.string,
search: PropTypes.array,
certifications: PropTypes.object.isRequired,
empty: PropTypes.bool,
link: PropTypes.string,
order: PropTypes.string,
orderFallback: PropTypes.string,
search: PropTypes.array,
walletsOwners: PropTypes.object,
fetchCertifiers: PropTypes.func.isRequired,
fetchCertifications: PropTypes.func.isRequired,
handleAddSearchToken: PropTypes.func
};
@@ -42,8 +49,16 @@ export default class List extends Component {
);
}
componentWillMount () {
const { accounts, fetchCertifiers, fetchCertifications } = this.props;
fetchCertifiers();
for (let address in accounts) {
fetchCertifications(address);
}
}
renderAccounts () {
const { accounts, balances, link, empty, handleAddSearchToken, walletsOwners } = this.props;
const { accounts, balances, empty, link, walletsOwners, handleAddSearchToken } = this.props;
if (empty) {
return (
@@ -72,7 +87,9 @@ export default class List extends Component {
account={ account }
balance={ balance }
owners={ owners }
handleAddSearchToken={ handleAddSearchToken } />
handleAddSearchToken={ handleAddSearchToken }
showCertifications
/>
</div>
);
});
@@ -207,3 +224,20 @@ export default class List extends Component {
});
}
}
function mapStateToProps (state) {
const { certifications } = state;
return { certifications };
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
fetchCertifiers,
fetchCertifications
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(List);

View File

@@ -21,6 +21,7 @@ import { isEqual } from 'lodash';
import ReactTooltip from 'react-tooltip';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '~/ui';
import Certifications from '~/ui/Certifications';
import { nullableProptype } from '~/util/proptypes';
import styles from '../accounts.css';
@@ -36,12 +37,14 @@ export default class Summary extends Component {
link: PropTypes.string,
name: PropTypes.string,
noLink: PropTypes.bool,
showCertifications: PropTypes.bool,
handleAddSearchToken: PropTypes.func,
owners: nullableProptype(PropTypes.array)
};
static defaultProps = {
noLink: false
noLink: false,
showCertifications: false
};
shouldComponentUpdate (nextProps) {
@@ -115,6 +118,7 @@ export default class Summary extends Component {
{ this.renderOwners() }
{ this.renderBalance() }
{ this.renderCertifications() }
</Container>
);
}
@@ -181,4 +185,15 @@ export default class Summary extends Component {
<Balance balance={ balance } />
);
}
renderCertifications () {
const { showCertifications, account } = this.props;
if (!showCertifications) {
return null;
}
return (
<Certifications account={ account.address } />
);
}
}

View File

@@ -16,14 +16,17 @@
import { action, observable } from 'mobx';
const showFirstRun = window.localStorage.getItem('showFirstRun') !== '0';
export default class Store {
@observable firstrunVisible = showFirstRun;
@observable firstrunVisible = false;
constructor (api) {
this._api = api;
const value = window.localStorage.getItem('showFirstRun');
if (value) {
this.firstrunVisible = JSON.parse(value);
}
this._checkAccounts();
}
@@ -33,7 +36,7 @@ export default class Store {
@action toggleFirstrun = (visible = false) => {
this.firstrunVisible = visible;
window.localStorage.setItem('showFirstRun', visible ? '1' : '0');
window.localStorage.setItem('showFirstRun', JSON.stringify(!!visible));
}
_checkAccounts () {