diff --git a/js/src/contracts/dappreg.js b/js/src/contracts/dappreg.js index b9ee15764..d82c76a5a 100644 --- a/js/src/contracts/dappreg.js +++ b/js/src/contracts/dappreg.js @@ -22,8 +22,12 @@ export default class DappReg { this.getInstance(); } + getContract () { + return this._registry.getContract('dappreg'); + } + getInstance () { - return this._registry.getContractInstance('dappreg'); + return this.getContract().then((contract) => contract.instance); } count () { diff --git a/js/src/redux/providers/signerReducer.js b/js/src/redux/providers/signerReducer.js index cc10b3fd1..c6d55f140 100644 --- a/js/src/redux/providers/signerReducer.js +++ b/js/src/redux/providers/signerReducer.js @@ -62,7 +62,7 @@ export default handleActions({ signerSuccessConfirmRequest (state, action) { const { id, txHash } = action.payload; const confirmed = Object.assign( - state.pending.find(p => p.id === id), + state.pending.find(p => p.id === id) || { id }, { result: txHash, status: 'confirmed' } ); diff --git a/js/src/util/dapps.js b/js/src/util/dapps.js new file mode 100644 index 000000000..89f17274e --- /dev/null +++ b/js/src/util/dapps.js @@ -0,0 +1,208 @@ +// 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 BigNumber from 'bignumber.js'; +import { pick, range, uniq } from 'lodash'; + +import Contracts from '~/contracts'; +import { hashToImageUrl } from '~/redux/util'; +import { bytesToHex } from '~/api/util/format'; + +import builtinApps from '~/views/Dapps/builtin.json'; + +function getHost (api) { + const host = process.env.DAPPS_URL || + ( + process.env.NODE_ENV === 'production' + ? api.dappsUrl + : '' + ); + + if (host === '/') { + return ''; + } + + return host; +} + +export function subscribeToChanges (api, dappReg, callback) { + return dappReg + .getContract() + .then((dappRegContract) => { + const dappRegInstance = dappRegContract.instance; + + const signatures = ['MetaChanged', 'OwnerChanged', 'Registered'] + .map((event) => dappRegInstance[event].signature); + + return api.eth + .newFilter({ + fromBlock: '0', + toBlock: 'latest', + address: dappRegInstance.address, + topics: [ signatures ] + }) + .then((filterId) => { + return api + .subscribe('eth_blockNumber', () => { + if (filterId > -1) { + api.eth + .getFilterChanges(filterId) + .then((logs) => { + return dappRegContract.parseEventLogs(logs); + }) + .then((events) => { + if (events.length === 0) { + return []; + } + + // Return uniq IDs which changed meta-data + const ids = uniq(events.map((event) => bytesToHex(event.params.id.value))); + callback(ids); + }); + } + }) + .then((blockSubId) => { + return { + block: blockSubId, + filter: filterId + }; + }); + }); + }); +} + +export function fetchBuiltinApps () { + const { dappReg } = Contracts.get(); + + return Promise + .all(builtinApps.map((app) => dappReg.getImage(app.id))) + .then((imageIds) => { + return builtinApps.map((app, index) => { + app.type = 'builtin'; + app.image = hashToImageUrl(imageIds[index]); + return app; + }); + }) + .catch((error) => { + console.warn('DappsStore:fetchBuiltinApps', error); + }); +} + +export function fetchLocalApps (api) { + return fetch(`${getHost(api)}/api/apps`) + .then((response) => { + return response.ok + ? response.json() + : []; + }) + .then((apps) => { + return apps + .map((app) => { + app.type = 'local'; + app.visible = true; + return app; + }) + .filter((app) => app.id && !['ui'].includes(app.id)); + }) + .catch((error) => { + console.warn('DappsStore:fetchLocal', error); + }); +} + +export function fetchRegistryAppIds () { + const { dappReg } = Contracts.get(); + + return dappReg + .count() + .then((count) => { + const promises = range(0, count.toNumber()).map((index) => dappReg.at(index)); + return Promise.all(promises); + }) + .then((appsInfo) => { + const appIds = appsInfo + .map(([appId, owner]) => bytesToHex(appId)) + .filter((appId) => { + return (new BigNumber(appId)).gt(0) && !builtinApps.find((app) => app.id === appId); + }); + + return appIds; + }) + .catch((error) => { + console.warn('DappsStore:fetchRegistryAppIds', error); + }); +} + +export function fetchRegistryApp (api, dappReg, appId) { + return Promise + .all([ + dappReg.getImage(appId), + dappReg.getContent(appId), + dappReg.getManifest(appId) + ]) + .then(([ imageId, contentId, manifestId ]) => { + const app = { + id: appId, + image: hashToImageUrl(imageId), + contentHash: bytesToHex(contentId).substr(2), + manifestHash: bytesToHex(manifestId).substr(2), + type: 'network', + visible: true + }; + + return fetchManifest(api, app.manifestHash) + .then((manifest) => { + if (manifest) { + app.manifestHash = null; + + // Add usefull manifest fields to app + Object.assign(app, pick(manifest, ['author', 'description', 'name', 'version'])); + } + + return app; + }); + }) + .then((app) => { + // Keep dapps that has a Manifest File and an Id + const dapp = (app.manifestHash || !app.id) ? null : app; + return dapp; + }) + .catch((error) => { + console.warn('DappsStore:fetchRegistryApp', error); + }); +} + +export function fetchManifest (api, manifestHash) { + if (/^(0x)?0+/.test(manifestHash)) { + return Promise.resolve(null); + } + + return fetch( + `${getHost(api)}/api/content/${manifestHash}/`, + { redirect: 'follow', mode: 'cors' } + ) + .then((response) => { + return response.ok + ? response.json() + : null; + }) + .then((manifest) => { + return manifest; + }) + .catch((error) => { + console.warn('DappsStore:fetchManifest', error); + return null; + }); +} diff --git a/js/src/views/Dapp/dapp.js b/js/src/views/Dapp/dapp.js index 16a842964..87245ca72 100644 --- a/js/src/views/Dapp/dapp.js +++ b/js/src/views/Dapp/dapp.js @@ -31,7 +31,7 @@ export default class Dapp extends Component { params: PropTypes.object }; - store = new DappsStore(this.context.api); + store = DappsStore.get(this.context.api); render () { const { dappsUrl } = this.context.api; diff --git a/js/src/views/Dapps/dapps.js b/js/src/views/Dapps/dapps.js index 07bdcd758..9760382c2 100644 --- a/js/src/views/Dapps/dapps.js +++ b/js/src/views/Dapps/dapps.js @@ -36,7 +36,7 @@ export default class Dapps extends Component { api: PropTypes.object.isRequired } - store = new DappsStore(this.context.api); + store = DappsStore.get(this.context.api); render () { let externalOverlay = null; diff --git a/js/src/views/Dapps/dappsStore.js b/js/src/views/Dapps/dappsStore.js index 1841f4c7c..e167331a4 100644 --- a/js/src/views/Dapps/dappsStore.js +++ b/js/src/views/Dapps/dappsStore.js @@ -14,17 +14,21 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import BigNumber from 'bignumber.js'; import { action, computed, observable, transaction } from 'mobx'; import store from 'store'; import Contracts from '~/contracts'; -import { hashToImageUrl } from '~/redux/util'; - -import builtinApps from './builtin.json'; +import { + fetchBuiltinApps, fetchLocalApps, + fetchRegistryAppIds, fetchRegistryApp, + subscribeToChanges +} from '~/util/dapps'; const LS_KEY_DISPLAY = 'displayApps'; const LS_KEY_EXTERNAL_ACCEPT = 'acceptExternal'; +const BUILTIN_APPS_KEY = 'BUILTIN_APPS_KEY'; + +let instance = null; export default class DappsStore { @observable apps = []; @@ -32,23 +36,138 @@ export default class DappsStore { @observable modalOpen = false; @observable externalOverlayVisible = true; + _api = null; + _subscriptions = {}; + + _cachedApps = {}; _manifests = {}; + _registryAppsIds = null; constructor (api) { this._api = api; this.loadExternalOverlay(); - this.readDisplayApps(); + this.loadApps(); + this.subscribeToChanges(); + } + + loadApps () { + const { dappReg } = Contracts.get(); Promise .all([ - this._fetchBuiltinApps(), - this._fetchLocalApps(), - this._fetchRegistryApps() + this.fetchBuiltinApps().then((apps) => this.addApps(apps)), + this.fetchLocalApps().then((apps) => this.addApps(apps)), + this.fetchRegistryApps(dappReg).then((apps) => this.addApps(apps)) ]) .then(this.writeDisplayApps); } + static get (api) { + if (!instance) { + instance = new DappsStore(api); + } else { + instance.loadApps(); + } + + return instance; + } + + subscribeToChanges () { + const { dappReg } = Contracts.get(); + + // Unsubscribe from previous subscriptions, if any + if (this._subscriptions.block) { + this._api.unsubscribe(this._subscriptions.block); + } + + if (this._subscriptions.filter) { + this._api.eth.uninstallFilter(this._subscriptions.filter); + } + + // Subscribe to dapps reg changes + subscribeToChanges(this._api, dappReg, (appIds) => { + const updates = appIds.map((appId) => { + return this.fetchRegistryApp(dappReg, appId, true); + }); + + Promise + .all(updates) + .then((apps) => { + this.addApps(apps); + }); + }).then((subscriptions) => { + this._subscriptions = subscriptions; + }); + } + + fetchBuiltinApps (force = false) { + if (!force && this._cachedApps[BUILTIN_APPS_KEY] !== undefined) { + return Promise.resolve(this._cachedApps[BUILTIN_APPS_KEY]); + } + + this._cachedApps[BUILTIN_APPS_KEY] = fetchBuiltinApps() + .then((apps) => { + this._cachedApps[BUILTIN_APPS_KEY] = apps; + return apps; + }); + + return Promise.resolve(this._cachedApps[BUILTIN_APPS_KEY]); + } + + fetchLocalApps () { + return fetchLocalApps(this._api); + } + + fetchRegistryAppIds (force = false) { + if (!force && this._registryAppsIds) { + return Promise.resolve(this._registryAppsIds); + } + + this._registryAppsIds = fetchRegistryAppIds() + .then((appIds) => { + this._registryAppsIds = appIds; + return this._registryAppsIds; + }); + + return Promise.resolve(this._registryAppsIds); + } + + fetchRegistryApp (dappReg, appId, force = false) { + if (!force && this._cachedApps[appId] !== undefined) { + return Promise.resolve(this._cachedApps[appId]); + } + + this._cachedApps[appId] = fetchRegistryApp(this._api, dappReg, appId) + .then((dapp) => { + this._cachedApps[appId] = dapp; + return dapp; + }); + + return Promise.resolve(this._cachedApps[appId]); + } + + fetchRegistryApps (dappReg) { + return this + .fetchRegistryAppIds() + .then((appIds) => { + const promises = appIds.map((appId) => { + // Fetch the Dapp and display it ASAP + return this + .fetchRegistryApp(dappReg, appId) + .then((app) => { + if (app) { + this.addApps([ app ]); + } + + return app; + }); + }); + + return Promise.all(promises); + }); + } + @computed get sortedBuiltin () { return this.apps.filter((app) => app.type === 'builtin'); } @@ -112,9 +231,17 @@ export default class DappsStore { store.set(LS_KEY_DISPLAY, this.displayApps); } - @action addApps = (apps) => { + @action addApps = (_apps) => { transaction(() => { + const apps = _apps.filter((app) => app); + + // Get new apps IDs if available + const newAppsIds = apps + .map((app) => app.id) + .filter((id) => id); + this.apps = this.apps + .filter((app) => !app.id || !newAppsIds.includes(app.id)) .concat(apps || []) .sort((a, b) => a.name.localeCompare(b.name)); @@ -128,159 +255,4 @@ export default class DappsStore { this.displayApps = Object.assign({}, this.displayApps, visibility); }); } - - _getHost (api) { - const host = process.env.DAPPS_URL || (process.env.NODE_ENV === 'production' - ? this._api.dappsUrl - : ''); - - if (host === '/') { - return ''; - } - - return host; - } - - _fetchBuiltinApps () { - const { dappReg } = Contracts.get(); - - return Promise - .all(builtinApps.map((app) => dappReg.getImage(app.id))) - .then((imageIds) => { - this.addApps( - builtinApps.map((app, index) => { - app.type = 'builtin'; - app.image = hashToImageUrl(imageIds[index]); - return app; - }) - ); - }) - .catch((error) => { - console.warn('DappsStore:fetchBuiltinApps', error); - }); - } - - _fetchLocalApps () { - return fetch(`${this._getHost()}/api/apps`) - .then((response) => { - return response.ok - ? response.json() - : []; - }) - .then((apps) => { - return apps - .map((app) => { - app.type = 'local'; - app.visible = true; - return app; - }) - .filter((app) => app.id && !['ui'].includes(app.id)); - }) - .then(this.addApps) - .catch((error) => { - console.warn('DappsStore:fetchLocal', error); - }); - } - - _fetchRegistryApps () { - const { dappReg } = Contracts.get(); - - return dappReg - .count() - .then((_count) => { - const count = _count.toNumber(); - const promises = []; - - for (let index = 0; index < count; index++) { - promises.push(dappReg.at(index)); - } - - return Promise.all(promises); - }) - .then((appsInfo) => { - const appIds = appsInfo - .map(([appId, owner]) => this._api.util.bytesToHex(appId)) - .filter((appId) => { - return (new BigNumber(appId)).gt(0) && !builtinApps.find((app) => app.id === appId); - }); - - return Promise - .all([ - Promise.all(appIds.map((appId) => dappReg.getImage(appId))), - Promise.all(appIds.map((appId) => dappReg.getContent(appId))), - Promise.all(appIds.map((appId) => dappReg.getManifest(appId))) - ]) - .then(([imageIds, contentIds, manifestIds]) => { - return appIds.map((appId, index) => { - const app = { - id: appId, - image: hashToImageUrl(imageIds[index]), - contentHash: this._api.util.bytesToHex(contentIds[index]).substr(2), - manifestHash: this._api.util.bytesToHex(manifestIds[index]).substr(2), - type: 'network', - visible: true - }; - - return app; - }); - }); - }) - .then((apps) => { - return Promise - .all(apps.map((app) => this._fetchManifest(app.manifestHash))) - .then((manifests) => { - return apps.map((app, index) => { - const manifest = manifests[index]; - - if (manifest) { - app.manifestHash = null; - Object.keys(manifest) - .filter((key) => ['author', 'description', 'name', 'version'].includes(key)) - .forEach((key) => { - app[key] = manifest[key]; - }); - } - - return app; - }); - }) - .then((apps) => { - return apps.filter((app) => { - return !app.manifestHash && app.id; - }); - }); - }) - .then(this.addApps) - .catch((error) => { - console.warn('DappsStore:fetchRegistry', error); - }); - } - - _fetchManifest (manifestHash) { - if (/^(0x)?0+/.test(manifestHash)) { - return Promise.resolve(null); - } - - if (this._manifests[manifestHash]) { - return Promise.resolve(this._manifests[manifestHash]); - } - - return fetch(`${this._getHost()}/api/content/${manifestHash}/`, { redirect: 'follow', mode: 'cors' }) - .then((response) => { - return response.ok - ? response.json() - : null; - }) - .then((manifest) => { - if (manifest) { - this._manifests[manifestHash] = manifest; - } - - return manifest; - }) - .catch((error) => { - console.warn('DappsStore:fetchManifest', error); - return null; - }); - } }