Fix tokens and badges (#6725)

* Update new token fetching

* Working Certifications Monitoring

* Update on Certification / Revoke

* Fix none-fetched tokens value display

* Fix tests
This commit is contained in:
Nicolas Gotchac 2017-10-12 20:09:44 +02:00 committed by Jaco Greeff
parent edbbd42d80
commit 3e7b775961
18 changed files with 635 additions and 316 deletions

12
js/package-lock.json generated
View File

@ -428,7 +428,7 @@
"glob": { "glob": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true, "dev": true,
"requires": { "requires": {
"fs.realpath": "1.0.0", "fs.realpath": "1.0.0",
@ -716,7 +716,7 @@
"glob": { "glob": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true, "dev": true,
"requires": { "requires": {
"fs.realpath": "1.0.0", "fs.realpath": "1.0.0",
@ -4140,7 +4140,7 @@
"glob": { "glob": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true, "dev": true,
"requires": { "requires": {
"fs.realpath": "1.0.0", "fs.realpath": "1.0.0",
@ -11654,7 +11654,7 @@
"string-width": { "string-width": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=",
"dev": true, "dev": true,
"requires": { "requires": {
"is-fullwidth-code-point": "2.0.0", "is-fullwidth-code-point": "2.0.0",
@ -12611,7 +12611,7 @@
"async": { "async": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz",
"integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", "integrity": "sha1-hDGQ/WtzV6C54clW7d3V7IRitU0=",
"dev": true, "dev": true,
"requires": { "requires": {
"lodash": "4.17.2" "lodash": "4.17.2"
@ -13016,7 +13016,7 @@
"commander": { "commander": {
"version": "2.11.0", "version": "2.11.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" "integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM="
}, },
"detect-indent": { "detect-indent": {
"version": "5.0.0", "version": "5.0.0",

View File

@ -105,7 +105,7 @@ export default class BadgeReg {
]); ]);
}) })
.then(([ title, icon ]) => { .then(([ title, icon ]) => {
title = bytesToHex(title); title = bytesToHex(title).replace(/(00)+$/, '');
title = title === ZERO32 ? null : hexToAscii(title); title = title === ZERO32 ? null : hexToAscii(title);
if (bytesToHex(icon) === ZERO32) { if (bytesToHex(icon) === ZERO32) {

View File

@ -29,6 +29,7 @@
left: 0; left: 0;
opacity: 1; opacity: 1;
padding: 1.5em; padding: 1.5em;
cursor: pointer;
position: fixed; position: fixed;
right: 50%; right: 50%;
z-index: 100; z-index: 100;

View File

@ -43,8 +43,13 @@ export default class Application extends Component {
contract: PropTypes.object contract: PropTypes.object
}; };
state = {
hideWarning: false
};
render () { render () {
const { isLoading, contract } = this.props; const { isLoading, contract } = this.props;
const { hideWarning } = this.state;
if (isLoading) { if (isLoading) {
return ( return (
@ -62,9 +67,15 @@ export default class Application extends Component {
<Actions /> <Actions />
<Tokens /> <Tokens />
<div className={ styles.warning }> {
WARNING: The token registry is experimental. Please ensure that you understand the steps, risks, benefits & consequences of registering a token before doing so. A non-refundable fee of { api.util.fromWei(contract.fee).toFormat(3) }<small>ETH</small> is required for all registrations. hideWarning
</div> ? null
: (
<div className={ styles.warning } onClick={ this.handleHideWarning }>
WARNING: The token registry is experimental. Please ensure that you understand the steps, risks, benefits & consequences of registering a token before doing so. A non-refundable fee of { api.util.fromWei(contract.fee).toFormat(3) }<small>ETH</small> is required for all registrations.
</div>
)
}
</div> </div>
); );
} }
@ -74,4 +85,8 @@ export default class Application extends Component {
muiTheme muiTheme
}; };
} }
handleHideWarning = () => {
this.setState({ hideWarning: true });
}
} }

View File

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

View File

@ -0,0 +1,342 @@
// Copyright 2015-2017 Parity Technologies (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 { range } from 'lodash';
import { addCertification, removeCertification } from './actions';
import { getLogger, LOG_KEYS } from '~/config';
import Contract from '~/api/contract';
import { bytesToHex, hexToAscii } from '~/api/util/format';
import Contracts from '~/contracts';
import CertifierABI from '~/contracts/abi/certifier.json';
import { querier } from './enhanced-querier';
const log = getLogger(LOG_KEYS.CertificationsMiddleware);
let self = null;
export default class CertifiersMonitor {
constructor (api, store) {
this._api = api;
this._name = 'Certifiers';
this._store = store;
this._contract = new Contract(this.api, CertifierABI);
this._contractEvents = [ 'Confirmed', 'Revoked' ]
.map((name) => this.contract.events.find((e) => e.name === name));
this.certifiers = {};
this.fetchedAccounts = {};
this.load();
}
static get () {
if (self) {
return self;
}
self = new CertifiersMonitor();
return self;
}
static init (api, store) {
if (!self) {
self = new CertifiersMonitor(api, store);
}
}
get api () {
return this._api;
}
get contract () {
return this._contract;
}
get contractEvents () {
return this._contractEvents;
}
get name () {
return this._name;
}
get store () {
return this._store;
}
get registry () {
return this._registry;
}
get registryEvents () {
return this._registryEvents;
}
checkFilters () {
this.checkCertifiersFilter();
this.checkRegistryFilter();
}
checkCertifiersFilter () {
if (!this.certifiersFilter) {
return;
}
this.api.eth.getFilterChanges(this.certifiersFilter)
.then((logs) => {
if (logs.length === 0) {
return;
}
const parsedLogs = this.contract.parseEventLogs(logs).filter((log) => log.params);
log.debug('received certifiers logs', parsedLogs);
const promises = parsedLogs.map((log) => {
const account = log.params.who.value;
const certifier = Object.values(this.certifiers).find((c) => c.address === log.address);
if (!certifier) {
log.warn('could not find the certifier', { certifiers: this.certifiers, log });
return Promise.resolve();
}
return this.fetchAccount(account, { ids: [ certifier.id ] });
});
return Promise.all(promises);
})
.catch((error) => {
console.error(error);
});
}
checkRegistryFilter () {
if (!this.registryFilter) {
return;
}
this.api.eth.getFilterChanges(this.registryFilter)
.then((logs) => {
if (logs.length === 0) {
return;
}
const parsedLogs = this.contract.parseEventLogs(logs).filter((log) => log.params);
const indexes = parsedLogs.map((log) => log.params && log.params.id.value.toNumber());
log.debug('received registry logs', parsedLogs);
return this.fetchElements(indexes);
})
.catch((error) => {
console.error(error);
});
}
/**
* Initial load of the Monitor.
* Fetch the contract from the Registry, and
* load the elements addresses
*/
load () {
const badgeReg = Contracts.get().badgeReg;
log.debug(`loading the ${this.name} monitor...`);
return badgeReg.getContract()
.then((registryContract) => {
this._registry = registryContract;
this._registryEvents = [ 'Registered', 'Unregistered', 'MetaChanged', 'AddressChanged' ]
.map((name) => this.registry.events.find((e) => e.name === name));
return this.registry.instance.badgeCount.call({});
})
.then((count) => {
log.debug(`found ${count.toFormat()} registered contracts for ${this.name}`);
return this.fetchElements(range(count.toNumber()));
})
.then(() => {
return this.setRegistryFilter();
})
.then(() => {
// Listen for new blocks
return this.api.subscribe('eth_blockNumber', (err) => {
if (err) {
return;
}
this.checkFilters();
});
})
.then(() => {
log.debug(`loaded the ${this.name} monitor!`, this.certifiers);
})
.catch((error) => {
log.error(error);
});
}
/**
* Fetch the given registered element
*/
fetchElements (indexes) {
const badgeReg = Contracts.get().badgeReg;
const { instance } = this.registry;
const sorted = indexes.sort();
const from = sorted[0];
const last = sorted[sorted.length - 1];
const limit = last - from + 1;
// Fetch the address, name and owner in one batch
return querier(this.api, { address: instance.address, from, limit }, instance.badge)
.then((results) => {
this.certifiers = results
.map(([ address, name, owner ], index) => ({
address, owner,
id: index + from,
name: hexToAscii(bytesToHex(name).replace(/(00)+$/, ''))
}))
.reduce((certifiers, certifier) => {
const { id } = certifier;
if (!/^(0x)?0+$/.test(certifier.address)) {
certifiers[id] = certifier;
} else if (certifiers[id]) {
delete certifiers[id];
}
return certifiers;
}, {});
// Fetch the meta-data in serie
return Object.values(this.certifiers).reduce((promise, certifier) => {
return promise.then(() => badgeReg.fetchMeta(certifier.id))
.then((meta) => {
this.certifiers[certifier.id] = { ...certifier, ...meta };
});
}, Promise.resolve());
})
.then(() => log.debug('fetched certifiers', { certifiers: this.certifiers }))
// Fetch the know accounts in case it's an update of the certifiers
.then(() => this.fetchAccounts(Object.keys(this.fetchedAccounts), { ids: indexes, force: true }));
}
fetchAccounts (addresses, { ids = null, force = false } = {}) {
const newAddresses = force
? addresses
: addresses.filter((address) => !this.fetchedAccounts[address]);
if (newAddresses.length === 0) {
return Promise.resolve();
}
log.debug(`fetching values for "${addresses.join(' ; ')}" in ${this.name}...`);
return newAddresses
.reduce((promise, address) => {
return promise.then(() => this.fetchAccount(address, { ids }));
}, Promise.resolve())
.then(() => {
log.debug(`fetched values for "${addresses.join(' ; ')}" in ${this.name}!`);
})
.then(() => this.setCertifiersFilter());
}
fetchAccount (address, { ids = null } = {}) {
let certifiers = Object.values(this.certifiers);
// Only fetch values for the givens ids, if any
if (ids) {
certifiers = certifiers.filter((certifier) => ids.includes(certifier.id));
}
certifiers
.reduce((promise, certifier) => {
return promise
.then(() => {
return this.contract.at(certifier.address).instance.certified.call({}, [ address ]);
})
.then((certified) => {
const { id, title, icon, name } = certifier;
this.fetchedAccounts[address] = true;
if (!certified) {
return this.store.dispatch(removeCertification(address, id));
}
log.debug('seen as certified', { address, id, name, icon });
this.store.dispatch(addCertification(address, id, name, title, icon));
});
}, Promise.resolve());
}
setCertifiersFilter () {
const accounts = Object.keys(this.fetchedAccounts);
const addresses = Object.values(this.certifiers).map((c) => c.address);
// The events have as first indexed data the account address
const topics = [
this.contractEvents.map((event) => '0x' + event.signature),
accounts
];
if (accounts.length === 0 || addresses.length === 0) {
return;
}
const promise = this.certifiersFilter
? this.api.eth.uninstallFilter(this.certifiersFilter)
: Promise.resolve();
log.debug('setting up registry filter', { topics, accounts, addresses });
return promise
.then(() => this.api.eth.newFilter({
fromBlock: 'latest',
toBlock: 'latest',
address: addresses,
topics
}))
.then((filterId) => {
this.certifiersFilter = filterId;
})
.catch((error) => {
console.error(error);
});
}
setRegistryFilter () {
const { address } = this.registry.instance;
const topics = [ this.registryEvents.map((event) => '0x' + event.signature) ];
log.debug('setting up registry filter', { topics, address });
return this.api.eth
.newFilter({
fromBlock: 'latest',
toBlock: 'latest',
address, topics
})
.then((filterId) => {
this.registryFilter = filterId;
})
.catch((error) => {
console.error(error);
});
}
}

View File

@ -0,0 +1,96 @@
// Copyright 2015-2017 Parity Technologies (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 { padRight, padLeft } from '~/api/util/format';
/**
* Bytecode of this contract:
*
*
pragma solidity ^0.4.10;
contract Querier {
function Querier
(address addr, bytes32 sign, uint out_size, uint from, uint limit)
public
{
// The size is 32 bytes for each
// value, plus 32 bytes for the count
uint m_size = out_size * limit + 32;
bytes32 p_return;
uint p_in;
uint p_out;
assembly {
p_return := mload(0x40)
mstore(0x40, add(p_return, m_size))
mstore(p_return, limit)
p_in := mload(0x40)
mstore(0x40, add(p_in, 0x24))
mstore(p_in, sign)
p_out := add(p_return, 0x20)
}
for (uint i = from; i < from + limit; i++) {
assembly {
mstore(add(p_in, 0x4), i)
call(gas, addr, 0x0, p_in, 0x24, p_out, out_size)
p_out := add(p_out, out_size)
pop
}
}
assembly {
return (p_return, m_size)
}
}
}
*/
export const bytecode = '0x60606040523415600e57600080fd5b60405160a0806099833981016040528080519190602001805191906020018051919060200180519190602001805191505082810260200160008080806040519350848401604052858452604051602481016040528981529250505060208201855b858701811015609457806004840152878260248560008e5af15090870190600101606f565b8484f300';
export const querier = (api, { address, from, limit }, method) => {
const { outputs, signature } = method;
const outLength = 32 * outputs.length;
const callargs = [
padLeft(address, 32),
padRight(signature, 32),
padLeft(outLength, 32),
padLeft(from, 32),
padLeft(limit, 32)
].map((v) => v.slice(2)).join('');
const calldata = bytecode + callargs;
return api.eth.call({ data: calldata })
.then((result) => {
const data = result.slice(2);
const results = [];
for (let i = 0; i < limit; i++) {
const datum = data.substr(2 * (32 + i * outLength), 2 * outLength);
const decoded = method.decodeOutput('0x' + datum).map((t) => t.value);
results.push(decoded);
}
return results;
});
};

View File

@ -14,222 +14,22 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { uniq, range, debounce } from 'lodash';
import { addCertification, removeCertification } from './actions';
import { getLogger, LOG_KEYS } from '~/config';
import Contract from '~/api/contract';
import Contracts from '~/contracts'; import Contracts from '~/contracts';
import CertifierABI from '~/contracts/abi/certifier.json'; import Monitor from './certifiers.monitor';
const log = getLogger(LOG_KEYS.CertificationsMiddleware);
// TODO: move this to a more general place
const updatableFilter = (api, onFilter) => {
let filter = null;
const update = (address, topics) => {
if (filter) {
filter = filter.then((filterId) => {
api.eth.uninstallFilter(filterId);
});
}
filter = (filter || Promise.resolve())
.then(() => api.eth.newFilter({
fromBlock: 'latest',
toBlock: 'latest',
address,
topics
}))
.then((filterId) => {
onFilter(filterId);
return filterId;
})
.catch((err) => {
console.error('Failed to create certifications filter:', err);
});
return filter;
};
return update;
};
export default class CertificationsMiddleware { export default class CertificationsMiddleware {
toMiddleware () { toMiddleware () {
const api = Contracts.get()._api; const api = Contracts.get()._api;
const badgeReg = Contracts.get().badgeReg;
const contract = new Contract(api, CertifierABI);
const Confirmed = contract.events.find((e) => e.name === 'Confirmed');
const Revoked = contract.events.find((e) => e.name === 'Revoked');
return (store) => { return (store) => {
let certifiers = []; Monitor.init(api, store);
let addresses = [];
let filterChanged = false;
let filter = null;
let badgeRegFilter = null;
let fetchCertifiersPromise = null;
const updateFilter = updatableFilter(api, (filterId) => {
filterChanged = true;
filter = filterId;
});
const badgeRegUpdateFilter = updatableFilter(api, (filterId) => {
filterChanged = true;
badgeRegFilter = filterId;
});
badgeReg
.getContract()
.then((badgeRegContract) => {
return badgeRegUpdateFilter(badgeRegContract.address, [ [
badgeRegContract.instance.Registered.signature,
badgeRegContract.instance.Unregistered.signature,
badgeRegContract.instance.MetaChanged.signature,
badgeRegContract.instance.AddressChanged.signature
] ]);
})
.then(() => {
shortFetchChanges();
api.subscribe('eth_blockNumber', (err) => {
if (err) {
return;
}
fetchChanges();
});
});
function onLogs (logs) {
logs = contract.parseEventLogs(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') {
store.dispatch(removeCertification(log.params.who.value, id));
} else {
store.dispatch(addCertification(log.params.who.value, id, name, title, icon));
}
});
}
function onBadgeRegLogs (logs) {
return badgeReg.getContract()
.then((badgeRegContract) => {
logs = badgeRegContract.parseEventLogs(logs);
const ids = logs.map((log) => log.params && log.params.id.value.toNumber());
return fetchCertifiers(uniq(ids));
});
}
function _fetchChanges () {
const method = filterChanged
? 'getFilterLogs'
: 'getFilterChanges';
filterChanged = false;
api.eth[method](badgeRegFilter)
.then(onBadgeRegLogs)
.catch((err) => {
console.error('Failed to fetch badge reg events:', err);
})
.then(() => api.eth[method](filter))
.then(onLogs)
.catch((err) => {
console.error('Failed to fetch new certifier events:', err);
});
}
const shortFetchChanges = debounce(_fetchChanges, 0.5 * 1000, { leading: true });
const fetchChanges = debounce(shortFetchChanges, 10 * 1000, { leading: true });
function fetchConfirmedEvents () {
return updateFilter(certifiers.map((c) => c.address), [
[ Confirmed.signature, Revoked.signature ],
addresses
]).then(() => shortFetchChanges());
}
function fetchCertifiers (ids = []) {
if (fetchCertifiersPromise) {
return fetchCertifiersPromise;
}
let fetchEvents = false;
const idsPromise = (certifiers.length === 0)
? badgeReg.certifierCount().then((count) => {
return range(count);
})
: Promise.resolve(ids);
fetchCertifiersPromise = idsPromise
.then((ids) => {
const promises = ids.map((id) => {
return badgeReg.fetchCertifier(id)
.then((cert) => {
if (!certifiers.some((c) => c.id === cert.id)) {
certifiers = certifiers.concat(cert);
fetchEvents = true;
}
})
.catch((err) => {
if (/does not exist/.test(err.toString())) {
return log.info(err.toString());
}
log.warn(`Could not fetch certifier ${id}:`, err);
});
});
return Promise
.all(promises)
.then(() => {
fetchCertifiersPromise = null;
if (fetchEvents) {
return fetchConfirmedEvents();
}
});
});
return fetchCertifiersPromise;
}
return (next) => (action) => { return (next) => (action) => {
switch (action.type) { switch (action.type) {
case 'fetchCertifiers':
fetchConfirmedEvents();
break;
case 'fetchCertifications':
const { address } = action;
if (!addresses.includes(address)) {
addresses = addresses.concat(address);
fetchConfirmedEvents();
}
break;
case 'setVisibleAccounts': case 'setVisibleAccounts':
const _addresses = action.addresses || []; const { addresses = [] } = action;
addresses = uniq(addresses.concat(_addresses)); Monitor.get().fetchAccounts(addresses);
fetchConfirmedEvents();
next(action); next(action);
break; break;

View File

@ -20,24 +20,32 @@ export default (state = initialState, action) => {
if (action.type === 'addCertification') { if (action.type === 'addCertification') {
const { address, id, name, icon, title } = action; const { address, id, name, icon, title } = action;
const certifications = state[address] || []; const certifications = state[address] || [];
const certifierIndex = certifications.findIndex((c) => c.id === id);
const data = { id, name, icon, title };
const nextCertifications = certifications.slice();
if (certifications.some((c) => c.id === id)) { if (certifierIndex >= 0) {
return state; nextCertifications[certifierIndex] = data;
} else {
nextCertifications.push(data);
} }
const newCertifications = certifications.concat({ return { ...state, [address]: nextCertifications };
id, name, icon, title
});
return { ...state, [address]: newCertifications };
} }
if (action.type === 'removeCertification') { if (action.type === 'removeCertification') {
const { address, id } = action; const { address, id } = action;
const certifications = state[address] || []; const certifications = state[address] || [];
const certifierIndex = certifications.findIndex((c) => c.id === id);
const newCertifications = certifications.filter((c) => c.id !== id); // Don't remove if not there
if (certifierIndex < 0) {
return state;
}
const newCertifications = certifications.slice();
newCertifications.splice(certifierIndex, 1);
return { ...state, [address]: newCertifications }; return { ...state, [address]: newCertifications };
} }

View File

@ -54,13 +54,24 @@ export const watchRequest = (request) => (dispatch, getState) => {
dispatch(trackRequest(requestId, request)); dispatch(trackRequest(requestId, request));
}; };
export const trackRequest = (requestId, { transactionHash = null } = {}) => (dispatch, getState) => { export const trackRequest = (requestId, { transactionHash = null, retries = 0 } = {}) => (dispatch, getState) => {
const { api } = getState(); const { api } = getState();
trackRequestUtil(api, { requestId, transactionHash }, (error, _data = {}) => { trackRequestUtil(api, { requestId, transactionHash }, (error, _data = {}) => {
const data = { ..._data }; const data = { ..._data };
if (error) { if (error) {
// Retry in 500ms if request not found, max 5 times
if (error.type === 'REQUEST_NOT_FOUND') {
if (retries > 5) {
return dispatch(deleteRequest(requestId));
}
return setTimeout(() => {
trackRequest(requestId, { transactionHash, retries: retries + 1 })(dispatch, getState);
}, 500);
}
console.error(error); console.error(error);
return dispatch(setRequest(requestId, { error })); return dispatch(setRequest(requestId, { error }));
} }

View File

@ -115,9 +115,11 @@ export function loadTokensBasics (_tokenIndexes, options) {
const prevTokensIndexes = Object.values(tokens).map((t) => t.index); const prevTokensIndexes = Object.values(tokens).map((t) => t.index);
// Only fetch tokens we don't have yet // Only fetch tokens we don't have yet
const tokenIndexes = _tokenIndexes.filter((tokenIndex) => { const tokenIndexes = _tokenIndexes
return !prevTokensIndexes.includes(tokenIndex); .filter((tokenIndex) => {
}); return !prevTokensIndexes.includes(tokenIndex);
})
.sort();
const count = tokenIndexes.length; const count = tokenIndexes.length;
@ -130,10 +132,15 @@ export function loadTokensBasics (_tokenIndexes, options) {
return tokenReg.getContract() return tokenReg.getContract()
.then((tokenRegContract) => { .then((tokenRegContract) => {
let promise = Promise.resolve(); let promise = Promise.resolve();
const first = tokenIndexes[0];
const last = tokenIndexes[tokenIndexes.length - 1];
for (let from = first; from <= last; from += limit) {
// No need to fetch `limit` elements
const lowerLimit = Math.min(limit, last - from + 1);
for (let start = 0; start < count; start += limit) {
promise = promise promise = promise
.then(() => fetchTokensBasics(api, tokenRegContract, start, limit)) .then(() => fetchTokensBasics(api, tokenRegContract, from, lowerLimit))
.then((results) => { .then((results) => {
results results
.forEach((token) => { .forEach((token) => {

View File

@ -20,7 +20,6 @@
background-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.8);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 100%;
overflow: hidden; overflow: hidden;
transition: transform ease-out 0.1s; transition: transform ease-out 0.1s;
transform: scale(1); transform: scale(1);

View File

@ -15,11 +15,12 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { pick } from 'lodash';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import TokenImage from '~/ui/TokenImage'; import TokenValue from './tokenValue';
import styles from './balance.css'; import styles from './balance.css';
@ -69,58 +70,19 @@ export class Balance extends Component {
const balanceValue = balance[tokenId]; const balanceValue = balance[tokenId];
const isEthToken = token.native; const isEthToken = token.native;
const isFullToken = !showOnlyEth || isEthToken;
const hasBalance = (balanceValue instanceof BigNumber) && balanceValue.gt(0); const hasBalance = (balanceValue instanceof BigNumber) && balanceValue.gt(0);
if (!hasBalance && !isEthToken) { if (!hasBalance && !isEthToken) {
return null; return null;
} }
const bnf = new BigNumber(token.format || 1);
let decimals = 0;
if (bnf.gte(1000)) {
decimals = 3;
} else if (bnf.gte(100)) {
decimals = 2;
} else if (bnf.gte(10)) {
decimals = 1;
}
const rawValue = new BigNumber(balanceValue).div(bnf);
const value = rawValue.toFormat(decimals);
const classNames = [styles.balance];
let details = null;
if (isFullToken) {
classNames.push(styles.full);
details = [
<div
className={ styles.value }
key='value'
>
<span title={ `${rawValue.toFormat()} ${token.tag}` }>
{ value }
</span>
</div>,
<div
className={ styles.tag }
key='tag'
>
{ token.tag }
</div>
];
}
return ( return (
<div <TokenValue
className={ classNames.join(' ') }
key={ tokenId } key={ tokenId }
> showOnlyEth={ showOnlyEth }
<TokenImage token={ token } /> token={ token }
{ details } value={ balanceValue }
</div> />
); );
}) })
.filter((node) => node); .filter((node) => node);
@ -155,11 +117,15 @@ export class Balance extends Component {
} }
function mapStateToProps (state, props) { function mapStateToProps (state, props) {
const { balances, tokens } = state; const { balances, tokens: allTokens } = state;
const { address } = props; const { address } = props;
const balance = balances[address] || props.balance || {};
const tokenIds = Object.keys(balance);
const tokens = pick(allTokens, tokenIds);
return { return {
balance: balances[address] || props.balance || {}, balance,
tokens tokens
}; };
} }

View File

@ -84,13 +84,13 @@ describe('ui/Balance', () => {
}); });
it('renders all the non-zero balances', () => { it('renders all the non-zero balances', () => {
expect(component.find('Connect(TokenImage)')).to.have.length(2); expect(component.find('Connect(TokenValue)')).to.have.length(2);
}); });
describe('render specifiers', () => { describe('render specifiers', () => {
it('renders all the tokens with showZeroValues', () => { it('renders all the tokens with showZeroValues', () => {
render({ showZeroValues: true }); render({ showZeroValues: true });
expect(component.find('Connect(TokenImage)')).to.have.length(2); expect(component.find('Connect(TokenValue)')).to.have.length(2);
}); });
}); });
}); });

View File

@ -0,0 +1,109 @@
// Copyright 2015-2017 Parity Technologies (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, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { fetchTokens } from '~/redux/providers/tokensActions';
import TokenImage from '~/ui/TokenImage';
import styles from './balance.css';
class TokenValue extends Component {
static propTypes = {
token: PropTypes.object.isRequired,
value: PropTypes.object.isRequired,
// Redux injection
fetchTokens: PropTypes.func.isRequired,
showOnlyEth: PropTypes.bool
};
componentWillMount () {
const { token } = this.props;
if (token.native) {
return;
}
if (!token.fetched) {
if (!Number.isFinite(token.index)) {
return console.warn('no token index', token);
}
this.props.fetchTokens([ token.index ]);
}
}
render () {
const { token, showOnlyEth, value } = this.props;
const isEthToken = token.native;
const isFullToken = !showOnlyEth || isEthToken;
const bnf = new BigNumber(token.format || 1);
let decimals = 0;
if (bnf.gte(1000)) {
decimals = 3;
} else if (bnf.gte(100)) {
decimals = 2;
} else if (bnf.gte(10)) {
decimals = 1;
}
const rawValue = new BigNumber(value).div(bnf);
const classNames = [styles.balance];
if (isFullToken) {
classNames.push(styles.full);
}
return (
<div className={ classNames.join(' ') }>
<TokenImage token={ token } />
{
isFullToken
? [
<div className={ styles.value } key='value'>
<span title={ `${rawValue.toFormat()} ${token.tag}` }>
{ rawValue.toFormat(decimals) }
</span>
</div>,
<div className={ styles.tag } key='tag'>
{ token.tag }
</div>
]
: null
}
</div>
);
}
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
fetchTokens
}, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(TokenValue);

View File

@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { fetchTokens } from '../../redux/providers/tokensActions'; import { fetchTokens } from '~/redux/providers/tokensActions';
import styles from './methodDecoding.css'; import styles from './methodDecoding.css';
class TokenValue extends Component { class TokenValue extends Component {

View File

@ -26,7 +26,6 @@ import HardwareStore from '~/mobx/hardwareStore';
import ExportStore from '~/modals/ExportAccount/exportStore'; import ExportStore from '~/modals/ExportAccount/exportStore';
import { DeleteAccount, EditMeta, Faucet, PasswordManager, Shapeshift, Transfer, Verification } from '~/modals'; import { DeleteAccount, EditMeta, Faucet, PasswordManager, Shapeshift, Transfer, Verification } from '~/modals';
import { setVisibleAccounts } from '~/redux/providers/personalActions'; import { setVisibleAccounts } from '~/redux/providers/personalActions';
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
import { Actionbar, Button, ConfirmDialog, Input, Page, Portal } from '~/ui'; import { Actionbar, Button, ConfirmDialog, Input, Page, Portal } from '~/ui';
import { DeleteIcon, DialIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon, FileDownloadIcon } from '~/ui/Icons'; import { DeleteIcon, DialIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon, FileDownloadIcon } from '~/ui/Icons';
@ -45,8 +44,6 @@ class Account extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
fetchCertifiers: PropTypes.func.isRequired,
fetchCertifications: PropTypes.func.isRequired,
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
account: PropTypes.object, account: PropTypes.object,
@ -67,7 +64,6 @@ class Account extends Component {
} }
componentDidMount () { componentDidMount () {
this.props.fetchCertifiers();
this.setVisibleAccounts(); this.setVisibleAccounts();
} }
@ -90,11 +86,10 @@ class Account extends Component {
} }
setVisibleAccounts (props = this.props) { setVisibleAccounts (props = this.props) {
const { params, setVisibleAccounts, fetchCertifications } = props; const { params, setVisibleAccounts } = props;
const addresses = [params.address]; const addresses = [params.address];
setVisibleAccounts(addresses); setVisibleAccounts(addresses);
fetchCertifications(params.address);
} }
render () { render () {
@ -524,8 +519,6 @@ function mapStateToProps (state, props) {
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return bindActionCreators({ return bindActionCreators({
fetchCertifiers,
fetchCertifications,
newError, newError,
setVisibleAccounts setVisibleAccounts
}, dispatch); }, dispatch);

View File

@ -17,10 +17,8 @@
import { pick } from 'lodash'; import { pick } from 'lodash';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Container, SectionList } from '~/ui'; import { Container, SectionList } from '~/ui';
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
import { ETH_TOKEN } from '~/util/tokens'; import { ETH_TOKEN } from '~/util/tokens';
import Summary from '../Summary'; import Summary from '../Summary';
@ -38,20 +36,9 @@ class List extends Component {
orderFallback: PropTypes.string, orderFallback: PropTypes.string,
search: PropTypes.array, search: PropTypes.array,
fetchCertifiers: PropTypes.func.isRequired,
fetchCertifications: PropTypes.func.isRequired,
handleAddSearchToken: PropTypes.func handleAddSearchToken: PropTypes.func
}; };
componentWillMount () {
const { accounts, fetchCertifiers, fetchCertifications } = this.props;
fetchCertifiers();
for (let address in accounts) {
fetchCertifications(address);
}
}
render () { render () {
const { accounts, disabled, empty } = this.props; const { accounts, disabled, empty } = this.props;
@ -264,14 +251,7 @@ function mapStateToProps (state, props) {
return { balances, certifications }; return { balances, certifications };
} }
function mapDispatchToProps (dispatch) {
return bindActionCreators({
fetchCertifiers,
fetchCertifications
}, dispatch);
}
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps null
)(List); )(List);