diff --git a/js/package-lock.json b/js/package-lock.json index 2d3ffc25e..ee461ca2a 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", @@ -716,7 +716,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", @@ -4140,7 +4140,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", @@ -11654,7 +11654,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", @@ -12611,7 +12611,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" @@ -13016,7 +13016,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 fe3be2e43..b968b1c06 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 a9de46667..df1374dd7 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/MethodDecoding/tokenValue.js b/js/src/ui/MethodDecoding/tokenValue.js index 166773389..03ceb82cc 100644 --- a/js/src/ui/MethodDecoding/tokenValue.js +++ b/js/src/ui/MethodDecoding/tokenValue.js @@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { fetchTokens } from '../../redux/providers/tokensActions'; +import { fetchTokens } from '~/redux/providers/tokensActions'; import styles from './methodDecoding.css'; class TokenValue extends Component { diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index a475e3a6b..50ad70164 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);