diff --git a/js/package-lock.json b/js/package-lock.json
index 881c20f1a..41d8fa26e 100644
--- a/js/package-lock.json
+++ b/js/package-lock.json
@@ -428,7 +428,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
- "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@@ -810,7 +810,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
- "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@@ -4414,7 +4414,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
- "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@@ -11958,7 +11958,7 @@
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
- "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=",
"dev": true,
"requires": {
"is-fullwidth-code-point": "2.0.0",
@@ -12923,7 +12923,7 @@
"async": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz",
- "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==",
+ "integrity": "sha1-hDGQ/WtzV6C54clW7d3V7IRitU0=",
"dev": true,
"requires": {
"lodash": "4.17.2"
@@ -13328,7 +13328,7 @@
"commander": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
- "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ=="
+ "integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM="
},
"detect-indent": {
"version": "5.0.0",
diff --git a/js/src/contracts/badgereg.js b/js/src/contracts/badgereg.js
index a1782cb30..07e740e6d 100644
--- a/js/src/contracts/badgereg.js
+++ b/js/src/contracts/badgereg.js
@@ -105,7 +105,7 @@ export default class BadgeReg {
]);
})
.then(([ title, icon ]) => {
- title = bytesToHex(title);
+ title = bytesToHex(title).replace(/(00)+$/, '');
title = title === ZERO32 ? null : hexToAscii(title);
if (bytesToHex(icon) === ZERO32) {
diff --git a/js/src/dapps/tokenreg/Application/application.css b/js/src/dapps/tokenreg/Application/application.css
index b1eef22e4..d855b6388 100644
--- a/js/src/dapps/tokenreg/Application/application.css
+++ b/js/src/dapps/tokenreg/Application/application.css
@@ -29,6 +29,7 @@
left: 0;
opacity: 1;
padding: 1.5em;
+ cursor: pointer;
position: fixed;
right: 50%;
z-index: 100;
diff --git a/js/src/dapps/tokenreg/Application/application.js b/js/src/dapps/tokenreg/Application/application.js
index 71081c7ee..df4a53ff1 100644
--- a/js/src/dapps/tokenreg/Application/application.js
+++ b/js/src/dapps/tokenreg/Application/application.js
@@ -43,8 +43,13 @@ export default class Application extends Component {
contract: PropTypes.object
};
+ state = {
+ hideWarning: false
+ };
+
render () {
const { isLoading, contract } = this.props;
+ const { hideWarning } = this.state;
if (isLoading) {
return (
@@ -62,9 +67,15 @@ export default class Application extends Component {
-
- 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) }ETH is required for all registrations.
-
+ {
+ hideWarning
+ ? null
+ : (
+
+ 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) }ETH is required for all registrations.
+
+ )
+ }
);
}
@@ -74,4 +85,8 @@ export default class Application extends Component {
muiTheme
};
}
+
+ handleHideWarning = () => {
+ this.setState({ hideWarning: true });
+ }
}
diff --git a/js/src/redux/providers/certifications/actions.js b/js/src/redux/providers/certifications/actions.js
index 8dede1c53..1eed6caea 100644
--- a/js/src/redux/providers/certifications/actions.js
+++ b/js/src/redux/providers/certifications/actions.js
@@ -14,14 +14,6 @@
// 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, id, name, title, icon) => ({
type: 'addCertification', address, id, name, title, icon
});
diff --git a/js/src/redux/providers/certifications/certifiers.monitor.js b/js/src/redux/providers/certifications/certifiers.monitor.js
new file mode 100644
index 000000000..208b728b3
--- /dev/null
+++ b/js/src/redux/providers/certifications/certifiers.monitor.js
@@ -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 .
+
+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);
+ });
+ }
+}
diff --git a/js/src/redux/providers/certifications/enhanced-querier.js b/js/src/redux/providers/certifications/enhanced-querier.js
new file mode 100644
index 000000000..9da42f1bf
--- /dev/null
+++ b/js/src/redux/providers/certifications/enhanced-querier.js
@@ -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 .
+
+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;
+ });
+};
diff --git a/js/src/redux/providers/certifications/middleware.js b/js/src/redux/providers/certifications/middleware.js
index 5965ec679..73178882a 100644
--- a/js/src/redux/providers/certifications/middleware.js
+++ b/js/src/redux/providers/certifications/middleware.js
@@ -14,222 +14,22 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-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 CertifierABI from '~/contracts/abi/certifier.json';
-
-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;
-};
+import Monitor from './certifiers.monitor';
export default class CertificationsMiddleware {
toMiddleware () {
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) => {
- let certifiers = [];
- 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;
- }
+ Monitor.init(api, store);
return (next) => (action) => {
switch (action.type) {
- case 'fetchCertifiers':
- fetchConfirmedEvents();
-
- break;
- case 'fetchCertifications':
- const { address } = action;
-
- if (!addresses.includes(address)) {
- addresses = addresses.concat(address);
- fetchConfirmedEvents();
- }
-
- break;
case 'setVisibleAccounts':
- const _addresses = action.addresses || [];
+ const { addresses = [] } = action;
- addresses = uniq(addresses.concat(_addresses));
- fetchConfirmedEvents();
+ Monitor.get().fetchAccounts(addresses);
next(action);
break;
diff --git a/js/src/redux/providers/certifications/reducer.js b/js/src/redux/providers/certifications/reducer.js
index 0f94239c6..7be0fb15b 100644
--- a/js/src/redux/providers/certifications/reducer.js
+++ b/js/src/redux/providers/certifications/reducer.js
@@ -20,24 +20,32 @@ export default (state = initialState, action) => {
if (action.type === 'addCertification') {
const { address, id, name, icon, title } = action;
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)) {
- return state;
+ if (certifierIndex >= 0) {
+ nextCertifications[certifierIndex] = data;
+ } else {
+ nextCertifications.push(data);
}
- const newCertifications = certifications.concat({
- id, name, icon, title
- });
-
- return { ...state, [address]: newCertifications };
+ return { ...state, [address]: nextCertifications };
}
if (action.type === 'removeCertification') {
const { address, id } = action;
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 };
}
diff --git a/js/src/redux/providers/requestsActions.js b/js/src/redux/providers/requestsActions.js
index dfcbcf4a3..3eb28ea4b 100644
--- a/js/src/redux/providers/requestsActions.js
+++ b/js/src/redux/providers/requestsActions.js
@@ -54,13 +54,24 @@ export const watchRequest = (request) => (dispatch, getState) => {
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();
trackRequestUtil(api, { requestId, transactionHash }, (error, _data = {}) => {
const data = { ..._data };
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);
return dispatch(setRequest(requestId, { error }));
}
diff --git a/js/src/redux/providers/tokensActions.js b/js/src/redux/providers/tokensActions.js
index 59245b27a..4083adf08 100644
--- a/js/src/redux/providers/tokensActions.js
+++ b/js/src/redux/providers/tokensActions.js
@@ -115,9 +115,11 @@ export function loadTokensBasics (_tokenIndexes, options) {
const prevTokensIndexes = Object.values(tokens).map((t) => t.index);
// Only fetch tokens we don't have yet
- const tokenIndexes = _tokenIndexes.filter((tokenIndex) => {
- return !prevTokensIndexes.includes(tokenIndex);
- });
+ const tokenIndexes = _tokenIndexes
+ .filter((tokenIndex) => {
+ return !prevTokensIndexes.includes(tokenIndex);
+ })
+ .sort();
const count = tokenIndexes.length;
@@ -130,10 +132,15 @@ export function loadTokensBasics (_tokenIndexes, options) {
return tokenReg.getContract()
.then((tokenRegContract) => {
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
- .then(() => fetchTokensBasics(api, tokenRegContract, start, limit))
+ .then(() => fetchTokensBasics(api, tokenRegContract, from, lowerLimit))
.then((results) => {
results
.forEach((token) => {
diff --git a/js/src/ui/AccountCard/accountCard.css b/js/src/ui/AccountCard/accountCard.css
index e6cafe656..d260b78b7 100644
--- a/js/src/ui/AccountCard/accountCard.css
+++ b/js/src/ui/AccountCard/accountCard.css
@@ -20,7 +20,6 @@
background-color: rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: row;
- height: 100%;
overflow: hidden;
transition: transform ease-out 0.1s;
transform: scale(1);
diff --git a/js/src/ui/Balance/balance.js b/js/src/ui/Balance/balance.js
index 72ccd0f14..4116f6eb9 100644
--- a/js/src/ui/Balance/balance.js
+++ b/js/src/ui/Balance/balance.js
@@ -15,11 +15,12 @@
// along with Parity. If not, see .
import BigNumber from 'bignumber.js';
+import { pick } from 'lodash';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
-import TokenImage from '~/ui/TokenImage';
+import TokenValue from './tokenValue';
import styles from './balance.css';
@@ -69,58 +70,19 @@ export class Balance extends Component {
const balanceValue = balance[tokenId];
const isEthToken = token.native;
- const isFullToken = !showOnlyEth || isEthToken;
const hasBalance = (balanceValue instanceof BigNumber) && balanceValue.gt(0);
if (!hasBalance && !isEthToken) {
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 = [
-
-
- { value }
-
-
,
-
- { token.tag }
-
- ];
- }
-
return (
-
-
- { details }
-
+ showOnlyEth={ showOnlyEth }
+ token={ token }
+ value={ balanceValue }
+ />
);
})
.filter((node) => node);
@@ -155,11 +117,15 @@ export class Balance extends Component {
}
function mapStateToProps (state, props) {
- const { balances, tokens } = state;
+ const { balances, tokens: allTokens } = state;
const { address } = props;
+ const balance = balances[address] || props.balance || {};
+
+ const tokenIds = Object.keys(balance);
+ const tokens = pick(allTokens, tokenIds);
return {
- balance: balances[address] || props.balance || {},
+ balance,
tokens
};
}
diff --git a/js/src/ui/Balance/balance.spec.js b/js/src/ui/Balance/balance.spec.js
index d5601a489..c1637329b 100644
--- a/js/src/ui/Balance/balance.spec.js
+++ b/js/src/ui/Balance/balance.spec.js
@@ -84,13 +84,13 @@ describe('ui/Balance', () => {
});
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', () => {
it('renders all the tokens with showZeroValues', () => {
render({ showZeroValues: true });
- expect(component.find('Connect(TokenImage)')).to.have.length(2);
+ expect(component.find('Connect(TokenValue)')).to.have.length(2);
});
});
});
diff --git a/js/src/ui/Balance/tokenValue.js b/js/src/ui/Balance/tokenValue.js
new file mode 100644
index 000000000..12f750ef5
--- /dev/null
+++ b/js/src/ui/Balance/tokenValue.js
@@ -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 .
+
+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 (
+
+
+ {
+ isFullToken
+ ? [
+
+
+ { rawValue.toFormat(decimals) }
+
+
,
+
+ { token.tag }
+
+ ]
+ : null
+ }
+
+ );
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return bindActionCreators({
+ fetchTokens
+ }, dispatch);
+}
+
+export default connect(
+ null,
+ mapDispatchToProps
+)(TokenValue);
diff --git a/js/src/ui/DappVouchFor/store.js b/js/src/ui/DappVouchFor/store.js
index cee44749d..60c615eb8 100644
--- a/js/src/ui/DappVouchFor/store.js
+++ b/js/src/ui/DappVouchFor/store.js
@@ -20,55 +20,68 @@ import { uniq } from 'lodash';
import Contracts from '~/contracts';
import { vouchfor as vouchForAbi } from '~/contracts/abi';
+let contractPromise = null;
+
export default class Store {
@observable vouchers = [];
constructor (api, app) {
this._api = api;
- const { contentHash } = app;
+ this.findVouchers(app);
+ }
- if (contentHash) {
- this.lookupVouchers(contentHash);
+ async attachContract () {
+ const address = await Contracts.get().registry.lookupAddress('vouchfor');
+
+ if (!address || /^0x0*$/.test(address)) {
+ return null;
}
+
+ const contract = await this._api.newContract(vouchForAbi, address);
+
+ return contract;
}
- lookupVouchers (contentHash) {
- Contracts
- .get().registry
- .lookupAddress('vouchfor')
- .then((address) => {
- if (!address || /^0x0*$/.test(address)) {
- return;
- }
+ async findVouchers ({ contentHash, id }) {
+ if (!contentHash) {
+ return;
+ }
- return this._api.newContract(vouchForAbi, address);
- })
- .then(async (contract) => {
- if (!contract) {
- return;
- }
+ if (!contractPromise) {
+ contractPromise = this.attachContract();
+ }
- let lastItem = false;
+ const contract = await contractPromise;
- for (let index = 0; !lastItem; index++) {
- const voucher = await contract.instance.vouched.call({}, [`0x${contentHash}`, index]);
+ if (!contract) {
+ return;
+ }
- if (/^0x0*$/.test(voucher)) {
- lastItem = true;
- } else {
- this.addVoucher(voucher);
- }
- }
- })
- .catch((error) => {
- console.error('vouchFor', error);
+ const vouchHash = await this.lookupHash(contract, `0x${contentHash}`);
+ const vouchId = await this.lookupHash(contract, id);
- return;
- });
+ this.addVouchers(vouchHash, vouchId);
}
- @action addVoucher = (voucher) => {
- this.vouchers = uniq([].concat(this.vouchers.peek(), [voucher]));
+ async lookupHash (contract, hash) {
+ const vouchers = [];
+ let lastItem = false;
+
+ for (let index = 0; !lastItem; index++) {
+ const voucher = await contract.instance.vouched.call({}, [hash, index]);
+
+ if (/^0x0*$/.test(voucher)) {
+ lastItem = true;
+ } else {
+ vouchers.push(voucher);
+ }
+ }
+
+ return vouchers;
+ }
+
+ @action addVouchers = (vouchHash, vouchId) => {
+ this.vouchers = uniq([].concat(this.vouchers.peek(), vouchHash, vouchId));
}
}
diff --git a/js/src/ui/MethodDecoding/methodDecoding.js b/js/src/ui/MethodDecoding/methodDecoding.js
index eb42a8bff..711e51ad9 100644
--- a/js/src/ui/MethodDecoding/methodDecoding.js
+++ b/js/src/ui/MethodDecoding/methodDecoding.js
@@ -23,6 +23,7 @@ import { connect } from 'react-redux';
import { TypedInput, InputAddress } from '../Form';
import MethodDecodingStore from './methodDecodingStore';
+import TokenValue from './tokenValue';
import styles from './methodDecoding.css';
@@ -602,9 +603,10 @@ class MethodDecoding extends Component {
const { token } = this.props;
return (
-
- { value.div(token.format).toFormat(5) } { token.tag }
-
+
);
}
diff --git a/js/src/ui/MethodDecoding/tokenValue.js b/js/src/ui/MethodDecoding/tokenValue.js
new file mode 100644
index 000000000..03ceb82cc
--- /dev/null
+++ b/js/src/ui/MethodDecoding/tokenValue.js
@@ -0,0 +1,102 @@
+// 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 .
+
+import React, { Component, PropTypes } from 'react';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+
+import { fetchTokens } from '~/redux/providers/tokensActions';
+import styles from './methodDecoding.css';
+
+class TokenValue extends Component {
+ static propTypes = {
+ id: PropTypes.string.isRequired,
+ value: PropTypes.object.isRequired,
+
+ fetchTokens: PropTypes.func,
+ token: PropTypes.object
+ };
+
+ componentWillMount () {
+ const { token } = this.props;
+
+ if (!token.fetched) {
+ this.props.fetchTokens([ token.index ]);
+ }
+ }
+
+ render () {
+ const { token, value } = this.props;
+
+ if (!token.format) {
+ console.warn('token with no format', token);
+ }
+
+ const format = token.format
+ ? token.format
+ : 1;
+
+ const precision = token.format
+ ? 5
+ : 0;
+
+ const tag = token.format
+ ? token.tag
+ : 'TOKENS';
+
+ return (
+
+ { value.div(format).toFormat(precision) } { tag }
+
+ );
+ }
+}
+
+function mapStateToProps (initState, initProps) {
+ const { id } = initProps;
+ let token = Object.assign({}, initState.tokens[id]);
+
+ if (token.fetched) {
+ return () => ({ token });
+ }
+
+ let update = true;
+
+ return (state) => {
+ if (update) {
+ const { tokens } = state;
+ const nextToken = tokens[id];
+
+ if (nextToken.fetched) {
+ token = Object.assign({}, nextToken);
+ update = false;
+ }
+ }
+
+ return { token };
+ };
+}
+
+function mapDispatchToProps (dispatch) {
+ return bindActionCreators({
+ fetchTokens
+ }, dispatch);
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(TokenValue);
diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js
index fdccac793..03c7d439a 100644
--- a/js/src/views/Account/account.js
+++ b/js/src/views/Account/account.js
@@ -26,7 +26,6 @@ import HardwareStore from '~/mobx/hardwareStore';
import ExportStore from '~/modals/ExportAccount/exportStore';
import { DeleteAccount, EditMeta, Faucet, PasswordManager, Shapeshift, Transfer, Verification } from '~/modals';
import { setVisibleAccounts } from '~/redux/providers/personalActions';
-import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
import { Actionbar, Button, ConfirmDialog, Input, Page, Portal } from '~/ui';
import { DeleteIcon, DialIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon, FileDownloadIcon } from '~/ui/Icons';
@@ -45,8 +44,6 @@ class Account extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
- fetchCertifiers: PropTypes.func.isRequired,
- fetchCertifications: PropTypes.func.isRequired,
setVisibleAccounts: PropTypes.func.isRequired,
account: PropTypes.object,
@@ -67,7 +64,6 @@ class Account extends Component {
}
componentDidMount () {
- this.props.fetchCertifiers();
this.setVisibleAccounts();
}
@@ -90,11 +86,10 @@ class Account extends Component {
}
setVisibleAccounts (props = this.props) {
- const { params, setVisibleAccounts, fetchCertifications } = props;
+ const { params, setVisibleAccounts } = props;
const addresses = [params.address];
setVisibleAccounts(addresses);
- fetchCertifications(params.address);
}
render () {
@@ -524,8 +519,6 @@ function mapStateToProps (state, props) {
function mapDispatchToProps (dispatch) {
return bindActionCreators({
- fetchCertifiers,
- fetchCertifications,
newError,
setVisibleAccounts
}, dispatch);
diff --git a/js/src/views/Accounts/List/list.js b/js/src/views/Accounts/List/list.js
index 2fa3be70b..7ff04ab30 100644
--- a/js/src/views/Accounts/List/list.js
+++ b/js/src/views/Accounts/List/list.js
@@ -17,10 +17,8 @@
import { pick } from 'lodash';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
import { Container, SectionList } from '~/ui';
-import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
import { ETH_TOKEN } from '~/util/tokens';
import Summary from '../Summary';
@@ -38,20 +36,9 @@ class List extends Component {
orderFallback: PropTypes.string,
search: PropTypes.array,
- fetchCertifiers: PropTypes.func.isRequired,
- fetchCertifications: PropTypes.func.isRequired,
handleAddSearchToken: PropTypes.func
};
- componentWillMount () {
- const { accounts, fetchCertifiers, fetchCertifications } = this.props;
-
- fetchCertifiers();
- for (let address in accounts) {
- fetchCertifications(address);
- }
- }
-
render () {
const { accounts, disabled, empty } = this.props;
@@ -264,14 +251,7 @@ function mapStateToProps (state, props) {
return { balances, certifications };
}
-function mapDispatchToProps (dispatch) {
- return bindActionCreators({
- fetchCertifiers,
- fetchCertifications
- }, dispatch);
-}
-
export default connect(
mapStateToProps,
- mapDispatchToProps
+ null
)(List);