diff --git a/js/src/contracts/verification.js b/js/src/contracts/verification.js index 05b7ea35f..3940e0e18 100644 --- a/js/src/contracts/verification.js +++ b/js/src/contracts/verification.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import subscribeToEvent from '../util/subscribe-to-event'; +import subscribeToEvents from '../util/subscribe-to-events'; export const checkIfVerified = (contract, account) => { return contract.instance.certified.call({}, [account]); @@ -72,7 +72,7 @@ export const awaitPuzzle = (api, contract, account) => { return blockNumber(api) .then((block) => { return new Promise((resolve, reject) => { - const subscription = subscribeToEvent(contract, 'Puzzled', { + const subscription = subscribeToEvents(contract, ['Puzzled'], { from: block.toNumber(), filter: (log) => log.params.who.value === account }); diff --git a/js/src/modals/ExecuteContract/executeContract.test.js b/js/src/modals/ExecuteContract/executeContract.test.js index ce196408d..8d9e4ccca 100644 --- a/js/src/modals/ExecuteContract/executeContract.test.js +++ b/js/src/modals/ExecuteContract/executeContract.test.js @@ -59,6 +59,9 @@ const STORE = { }, settings: { backgroundSeed: '' + }, + registry: { + reverse: {} } }; } diff --git a/js/src/redux/middleware.js b/js/src/redux/middleware.js index b62b4e5f2..bffeddc98 100644 --- a/js/src/redux/middleware.js +++ b/js/src/redux/middleware.js @@ -23,6 +23,7 @@ import SignerMiddleware from './providers/signerMiddleware'; import statusMiddleware from '~/views/Status/middleware'; import CertificationsMiddleware from './providers/certifications/middleware'; import ChainMiddleware from './providers/chainMiddleware'; +import RegistryMiddleware from './providers/registry/middleware'; export default function (api, browserHistory) { const errors = new ErrorsMiddleware(); @@ -32,13 +33,15 @@ export default function (api, browserHistory) { const certifications = new CertificationsMiddleware(); const routeMiddleware = routerMiddleware(browserHistory); const chain = new ChainMiddleware(); + const registry = new RegistryMiddleware(api); const middleware = [ settings.toMiddleware(), signer.toMiddleware(), errors.toMiddleware(), certifications.toMiddleware(), - chain.toMiddleware() + chain.toMiddleware(), + registry ]; return middleware.concat(status, routeMiddleware, thunk); diff --git a/js/src/redux/providers/registry/actions.js b/js/src/redux/providers/registry/actions.js new file mode 100644 index 000000000..eeeb47f51 --- /dev/null +++ b/js/src/redux/providers/registry/actions.js @@ -0,0 +1,28 @@ +// Copyright 2015, 2016 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 . + +export const setReverse = (address, reverse) => ({ + type: 'setReverse', + address, reverse +}); + +export const startCachingReverses = () => ({ + type: 'startCachingReverses' +}); + +export const stopCachingReverses = () => ({ + type: 'stopCachingReverses' +}); diff --git a/js/src/redux/providers/registry/middleware.js b/js/src/redux/providers/registry/middleware.js new file mode 100644 index 000000000..136a9eb08 --- /dev/null +++ b/js/src/redux/providers/registry/middleware.js @@ -0,0 +1,99 @@ +// Copyright 2015, 2016 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 Contracts from '~/contracts'; +import subscribeToEvents from '~/util/subscribe-to-events'; + +import registryABI from '~/contracts/abi/registry.json'; + +import { setReverse, startCachingReverses } from './actions'; + +export default (api) => (store) => { + let contract, subscription, timeout, interval; + + let addressesToCheck = {}; + + const onLog = (log) => { + switch (log.event) { + case 'ReverseConfirmed': + addressesToCheck[log.params.reverse.value] = true; + + break; + case 'ReverseRemoved': + delete addressesToCheck[log.params.reverse.value]; + + break; + } + }; + + const checkReverses = () => { + Object + .keys(addressesToCheck) + .forEach((address) => { + contract + .instance + .reverse + .call({}, [ address ]) + .then((reverse) => store.dispatch(setReverse(address, reverse))); + }); + + addressesToCheck = {}; + }; + + return (next) => (action) => { + switch (action.type) { + case 'initAll': + next(action); + store.dispatch(startCachingReverses()); + + break; + case 'startCachingReverses': + const { registry } = Contracts.get(); + + registry.getInstance() + .then((instance) => api.newContract(registryABI, instance.address)) + .then((_contract) => { + contract = _contract; + + subscription = subscribeToEvents(_contract, ['ReverseConfirmed', 'ReverseRemoved']); + subscription.on('log', onLog); + + timeout = setTimeout(checkReverses, 5000); + interval = setInterval(checkReverses, 20000); + }) + .catch((err) => { + console.error('Failed to start caching reverses:', err); + throw err; + }); + + break; + case 'stopCachingReverses': + if (subscription) { + subscription.unsubscribe(); + } + if (interval) { + clearInterval(interval); + } + if (timeout) { + clearTimeout(timeout); + } + + break; + default: + next(action); + } + }; +}; diff --git a/js/src/redux/providers/registry/reducer.js b/js/src/redux/providers/registry/reducer.js new file mode 100644 index 000000000..5c267d822 --- /dev/null +++ b/js/src/redux/providers/registry/reducer.js @@ -0,0 +1,33 @@ +// Copyright 2015, 2016 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 . + +const initialState = { + reverse: {} // cache for reverse lookup +}; + +export default (state = initialState, action) => { + if (action.type === 'setReverse') { + if (state.reverse[action.address] === action.reverse) { + return state; + } + + return { ...state, reverse: { + ...state.reverse, [ action.address ]: action.reverse + } }; + } + + return state; +}; diff --git a/js/src/redux/reducers.js b/js/src/redux/reducers.js index 1156d1836..45408de92 100644 --- a/js/src/redux/reducers.js +++ b/js/src/redux/reducers.js @@ -24,6 +24,7 @@ import { snackbarReducer, walletReducer } from './providers'; import certificationsReducer from './providers/certifications/reducer'; +import registryReducer from './providers/registry/reducer'; import errorReducer from '~/ui/Errors/reducers'; import settingsReducer from '~/views/Settings/reducers'; @@ -43,6 +44,7 @@ export default function () { images: imagesReducer, nodeStatus: nodeStatusReducer, personal: personalReducer, + registry: registryReducer, signer: signerReducer, snackbar: snackbarReducer, wallet: walletReducer, diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index 692ff4285..7b09b5b07 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -56,6 +56,7 @@ class AddressSelect extends Component { contacts: PropTypes.object, contracts: PropTypes.object, tokens: PropTypes.object, + reverse: PropTypes.object, // Optional props allowCopy: PropTypes.bool, @@ -584,10 +585,12 @@ class AddressSelect extends Component { function mapStateToProps (state) { const { accountsInfo } = state.personal; const { balances } = state.balances; + const { reverse } = state.registry; return { accountsInfo, - balances + balances, + reverse }; } diff --git a/js/src/ui/Form/AddressSelect/addressSelectStore.js b/js/src/ui/Form/AddressSelect/addressSelectStore.js index 26f9fe80e..bdb7c1fb2 100644 --- a/js/src/ui/Form/AddressSelect/addressSelectStore.js +++ b/js/src/ui/Form/AddressSelect/addressSelectStore.js @@ -16,7 +16,7 @@ import React from 'react'; import { observable, action } from 'mobx'; -import { flatMap } from 'lodash'; +import { flatMap, uniqBy } from 'lodash'; import { FormattedMessage } from 'react-intl'; import Contracts from '~/contracts'; @@ -30,7 +30,48 @@ export default class AddressSelectStore { @observable registryValues = []; initValues = []; - regLookups = []; + regLookups = [ + (query) => { + query = query.toLowerCase().trim(); + if (query.length === 0 || query === '0x') { + return null; + } + const startsWithQuery = (s) => new RegExp('^' + query, 'i').test(s); + + let address; + let name = this.reverse[query]; + + if (!name) { + const addr = Object + .keys(this.reverse) + .find((addr) => { + const name = this.reverse[addr]; + return startsWithQuery(addr) || (name && startsWithQuery(name)); + }); + + if (addr) { + address = addr; + name = this.reverse[addr]; + } else { + return null; + } + } + + return { + address, + name, + description: ( + + ) + }; + } + ]; constructor (api) { this.api = api; @@ -114,7 +155,8 @@ export default class AddressSelectStore { } @action setValues (props) { - const { accounts = {}, contracts = {}, contacts = {} } = props; + const { accounts = {}, contracts = {}, contacts = {}, reverse = {} } = props; + this.reverse = reverse; const accountsN = Object.keys(accounts).length; const contractsN = Object.keys(contracts).length; @@ -194,6 +236,8 @@ export default class AddressSelectStore { .filter((result) => result && !ZERO.test(result.address)); }) .then((results) => { + results = uniqBy(results, (result) => result.address); + this.registryValues = results .map((result) => { const lowercaseAddress = result.address.toLowerCase(); diff --git a/js/src/util/subscribe-to-event.js b/js/src/util/subscribe-to-event.js deleted file mode 100644 index 36d1f6d55..000000000 --- a/js/src/util/subscribe-to-event.js +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2015, 2016 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 EventEmitter from 'eventemitter3'; - -const defaults = { - from: 0, // TODO - to: 'latest', - timeout: null, - filter: () => true -}; - -const subscribeToEvent = (contract, name, opt = {}) => { - opt = Object.assign({}, defaults, opt); - - let subscription = null; - let timeout = null; - - const unsubscribe = () => { - if (subscription) { - contract.unsubscribe(subscription); - subscription = null; - } - if (timeout) { - clearTimeout(timeout); - timeout = null; - } - }; - - const emitter = new EventEmitter(); - emitter.unsubscribe = unsubscribe; - - if (typeof opt.timeout === 'number') { - timeout = setTimeout(() => { - unsubscribe(); - emitter.emit('timeout'); - }, opt.timeout); - } - - const callback = (err, logs) => { - if (err) { - return emitter.emit('error', err); - } - for (let log of logs) { - if (opt.filter(log)) { - emitter.emit('log', log); - } - } - }; - - contract.subscribe(name, { - fromBlock: opt.from, toBlock: opt.to - }, callback) - .then((_subscription) => { - subscription = _subscription; - }) - .catch((err) => { - emitter.emit('error', err); - }); - - return emitter; -}; - -export default subscribeToEvent; diff --git a/js/src/util/subscribe-to-events.js b/js/src/util/subscribe-to-events.js new file mode 100644 index 000000000..48c990277 --- /dev/null +++ b/js/src/util/subscribe-to-events.js @@ -0,0 +1,97 @@ +// Copyright 2015, 2016 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 EventEmitter from 'eventemitter3'; + +const defaults = { + from: 0, + to: 'latest', + interval: 5000, + filter: () => true +}; + +const subscribeToEvents = (contract, events, opt = {}) => { + const { api } = contract; + opt = Object.assign({}, defaults, opt); + + let filter = null; + let interval = null; + + const unsubscribe = () => { + if (filter) { + filter + .then((filterId) => { + return api.eth.uninstallFilter(filterId); + }) + .catch((err) => { + emitter.emit('error', err); + }); + filter = null; + } + if (interval) { + clearInterval(interval); + interval = null; + } + }; + + const emitter = new EventEmitter(); + emitter.unsubscribe = unsubscribe; + + const fetcher = (method, filterId) => () => { + api + .eth[method](filterId) + .then((logs) => { + logs = contract.parseEventLogs(logs); + + for (let log of logs) { + if (opt.filter(log)) { + emitter.emit('log', log); + emitter.emit(log.event, log); + } + } + }) + .catch((err) => { + emitter.emit('error', err); + }); + }; + + const signatures = events + .filter((event) => contract.instance[event]) + .map((event) => contract.instance[event].signature); + + filter = api.eth + .newFilter({ + fromBlock: opt.from, toBlock: opt.to, + address: contract.address, + topics: [signatures] + }) + .then((filterId) => { + fetcher('getFilterLogs', filterId)(); // fetch immediately + + const fetchChanges = fetcher('getFilterChanges', filterId); + interval = setInterval(fetchChanges, opt.interval); + + return filterId; + }) + .catch((err) => { + emitter.emit('error', err); + throw err; // reject Promise + }); + + return emitter; +}; + +export default subscribeToEvents;