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 (
+
+ );
+ }
}