diff --git a/js/src/contracts/badgereg.js b/js/src/contracts/badgereg.js index 6cf3d8bc9..8075f456e 100644 --- a/js/src/contracts/badgereg.js +++ b/js/src/contracts/badgereg.js @@ -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 }; }); } diff --git a/js/src/redux/providers/certifications/actions.js b/js/src/redux/providers/certifications/actions.js index 03bb93fe9..10f4ce9fd 100644 --- a/js/src/redux/providers/certifications/actions.js +++ b/js/src/redux/providers/certifications/actions.js @@ -14,10 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +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 }); diff --git a/js/src/redux/providers/certifications/middleware.js b/js/src/redux/providers/certifications/middleware.js index 500fe39b3..6e4b898d0 100644 --- a/js/src/redux/providers/certifications/middleware.js +++ b/js/src/redux/providers/certifications/middleware.js @@ -14,38 +14,90 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -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); + } }; } } diff --git a/js/src/redux/providers/certifications/reducer.js b/js/src/redux/providers/certifications/reducer.js index f9195b1df..bb2681cf6 100644 --- a/js/src/redux/providers/certifications/reducer.js +++ b/js/src/redux/providers/certifications/reducer.js @@ -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; }; diff --git a/js/src/ui/Certifications/certifications.js b/js/src/ui/Certifications/certifications.js index edf8be10a..bafd06f35 100644 --- a/js/src/ui/Certifications/certifications.js +++ b/js/src/ui/Certifications/certifications.js @@ -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); diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index b2e0a9c28..058c20db3 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -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 } /> { children } diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index 38c712622..82b2e6b71 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -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); } diff --git a/js/src/views/Accounts/List/list.js b/js/src/views/Accounts/List/list.js index f08c9fdb0..d5bdd9662 100644 --- a/js/src/views/Accounts/List/list.js +++ b/js/src/views/Accounts/List/list.js @@ -15,22 +15,29 @@ // along with Parity. If not, see . 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 + /> ); }); @@ -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); diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index 98d4642fd..3b1d64d18 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -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() } ); } @@ -181,4 +185,15 @@ export default class Summary extends Component { ); } + + renderCertifications () { + const { showCertifications, account } = this.props; + if (!showCertifications) { + return null; + } + + return ( + + ); + } }