Skip to content
This repository has been archived by the owner on Nov 6, 2020. It is now read-only.

get certifications from BadgeReg, show them in accounts overview #3768

Merged
merged 16 commits into from
Dec 15, 2016
Merged
66 changes: 46 additions & 20 deletions js/src/contracts/badgereg.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,66 @@ 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) {
this._api = api;
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 };
});
}

Expand Down
12 changes: 10 additions & 2 deletions js/src/redux/providers/certifications/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
102 changes: 77 additions & 25 deletions js/src/redux/providers/certifications/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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);
let certifiers = [];
let accounts = []; // these are addresses

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') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really picky, but would prefer a line before this block, just indicates a break between assigning and doing. (Not a crisis, just aids readability once again)

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);
}
};
}
}
26 changes: 18 additions & 8 deletions js/src/redux/providers/certifications/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,27 @@
const initialState = {};

export default (state = initialState, action) => {
if (action.type !== 'addCertification') {
return state;
if (action.type === 'addCertification') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch statement would be good here as well, since we are only checking the type, default is the state return.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing it to a switch statement in this case means that the variable declarations aren't as local anymore. The linter will complain. Moving them up is ugly because we're assigning stuff we may not need.

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;
};
21 changes: 5 additions & 16 deletions js/src/ui/Certifications/certifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 () {
Expand Down Expand Up @@ -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 };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Less squashing, break would be good before the return line.

};
}

export default connect(
mapStateToProps,
mapDispatchToProps
null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could remove completely

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually like null here because null !== undefined and because connect is commonly known as the function that accepts two params, one function for each direction.

)(Certifications);
6 changes: 0 additions & 6 deletions js/src/views/Account/Header/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -85,7 +80,6 @@ export default class Header extends Component {
balance={ balance } />
<Certifications
account={ account.address }
dappsUrl={ api.dappsUrl }
/>
</div>
{ children }
Expand Down
11 changes: 9 additions & 2 deletions js/src/views/Account/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -63,6 +66,7 @@ class Account extends Component {
}

componentDidMount () {
this.props.fetchCertifiers();
this.setVisibleAccounts();
}

Expand All @@ -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 () {
Expand Down Expand Up @@ -353,7 +358,9 @@ function mapStateToProps (state) {

function mapDispatchToProps (dispatch) {
return bindActionCreators({
setVisibleAccounts
setVisibleAccounts,
fetchCertifiers,
fetchCertifications
}, dispatch);
}

Expand Down
Loading