Update v1 Wallet Dapp (#6935)

* Start removing duplicated functionality (v1 inside v2)

* Update compilation targets

* Update locks

* Fix js-old build

* Update with removed extra references

* Adapt dev.{parity,web3}.html for extra debug info

* Update dependencies

* Remove Tooltips

* Update dependencies

* Only inject window.ethereum once

* Fix versions to 2.0.x for @parity libraries

* Update to @parity/api 2.1.x

* Update for @parity/api 2.1.x

* Freeze signer plugin dependency hashes

* Fix lint

* Move local account handling from API

* Update for 2.2.x @parity/{shared,ui}

* Update API references for middleware

* Install updated dependencies

* Update for build

* Always do local builds for development

* Remove unused hasAccounts property

* Fix Windows build for js-old

* Adjust inclusing rules to be Windows friendly

* Explicitly add --config option to webpack

* Add process.env.EMBED flag for Windows compatability

* Revert embed flag

* Fix build

* Merge changes from beta

* Update packages after merge

* Update Accounts from beta

* Update with beta

* Remove upgrade check

* Fix CI build script execution

* Make rm -rf commands cross-platform

* Remove ability to deploy wallets (only watch)

* Update path references for js-old (Windows)

* Render local dapps first

* Cleanup dependencies
This commit is contained in:
Jaco Greeff
2017-11-13 09:31:08 +01:00
committed by GitHub
parent bcdfc50a0b
commit ce1609726f
97 changed files with 8062 additions and 14290 deletions

View File

@@ -33,6 +33,7 @@
<div id="container">
<div class="loading">Loading</div>
</div>
<script src="/parity-utils/inject.js"></script>
<script src="vendor.js"></script>
</body>
</html>

View File

@@ -25,9 +25,9 @@ import { AppContainer } from 'react-hot-loader';
import injectTapEventPlugin from 'react-tap-event-plugin';
import { hashHistory } from 'react-router';
import qs from 'querystring';
import SecureApi from './secureApi';
import Api from '@parity/api';
import ContractInstances from '~/contracts';
import { initStore } from './redux';
@@ -45,23 +45,7 @@ import '../assets/fonts/RobotoMono/font.css';
injectTapEventPlugin();
if (process.env.NODE_ENV === 'development') {
// Expose the React Performance Tools on the`window` object
const Perf = require('react-addons-perf');
window.Perf = Perf;
}
const AUTH_HASH = '#/auth?';
let token = null;
if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) {
token = qs.parse(window.location.hash.substr(AUTH_HASH.length)).token;
}
const uiUrl = window.location.host;
const api = new SecureApi(uiUrl, token);
const api = new Api(window.ethereum);
patchApi(api);
loadSender(api);
@@ -72,8 +56,6 @@ const store = initStore(api, hashHistory);
store.dispatch({ type: 'initAll', api });
store.dispatch(setApi(api));
window.secureApi = api;
ReactDOM.render(
<AppContainer>
<ContextProvider api={ api } muiTheme={ muiTheme } store={ store }>

View File

@@ -40,10 +40,10 @@ impl WebApp for App {
fn info(&self) -> Info {
Info {
name: "Parity Wallet v1",
name: "Parity Wallet",
version: env!("CARGO_PKG_VERSION"),
author: "Parity <admin@parity.io>",
description: "Deprecated version of Parity Wallet.",
description: "Parity Wallet and Account management tools",
icon_url: "icon.png",
}
}

7
js-old/src/manifest.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "Parity Wallet",
"version": "development",
"author": "Parity <admin@parity.io>",
"description": "Parity Wallet and Account management tools",
"icon_url": "icon.png",
}

View File

@@ -17,35 +17,35 @@
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { walletSourceURL } from '~/contracts/code/wallet';
// import { walletSourceURL } from '~/contracts/code/wallet';
import { RadioButtons } from '~/ui';
const TYPES = [
{
label: (
<FormattedMessage
id='createWallet.type.multisig.label'
defaultMessage='Multi-Sig wallet'
/>
),
key: 'MULTISIG',
description: (
<FormattedMessage
id='createWallet.type.multisig.description'
defaultMessage='Create/Deploy a {link} Wallet'
values={ {
link: (
<a href={ walletSourceURL } target='_blank'>
<FormattedMessage
id='createWallet.type.multisig.link'
defaultMessage='standard multi-signature'
/>
</a>
)
} }
/>
)
},
// {
// label: (
// <FormattedMessage
// id='createWallet.type.multisig.label'
// defaultMessage='Multi-Sig wallet'
// />
// ),
// key: 'MULTISIG',
// description: (
// <FormattedMessage
// id='createWallet.type.multisig.description'
// defaultMessage='Create/Deploy a {link} Wallet'
// values={ {
// link: (
// <a href={ walletSourceURL } target='_blank'>
// <FormattedMessage
// id='createWallet.type.multisig.link'
// defaultMessage='standard multi-signature'
// />
// </a>
// )
// } }
// />
// )
// },
{
label: (
<FormattedMessage
@@ -57,7 +57,7 @@ const TYPES = [
description: (
<FormattedMessage
id='createWallet.type.watch.description'
defaultMessage='Add an existing wallet to your accounts'
defaultMessage='Add an existing multisig wallet to your accounts'
/>
)
}

View File

@@ -59,7 +59,7 @@ const STEPS = {
export default class CreateWalletStore {
@observable step = null;
@observable txhash = null;
@observable walletType = 'MULTISIG';
@observable walletType = 'WATCH'; // 'MULTISIG';
@observable wallet = {
account: '',

View File

@@ -133,8 +133,8 @@ export default class TransferStore {
}
@action handleClose = () => {
this.stage = 0;
this.onClose();
this.stage = 0;
}
@action onUpdateDetails = (type, value) => {
@@ -169,7 +169,6 @@ export default class TransferStore {
}
@action onSend = () => {
this.onNext();
this.sending = true;
this

View File

@@ -16,7 +16,7 @@
import { handleActions } from 'redux-actions';
const initialState = {};
const initialState = null;
export default handleActions({
setApi (state, action) {

View File

@@ -16,12 +16,11 @@
import { throttle } from 'lodash';
import { fetchBalances, fetchTokensBalances, queryTokensFilter } from './balancesActions';
import { loadTokens, fetchTokens } from './tokensActions';
import { padRight } from '~/api/util/format';
import { LOG_KEYS, getLogger } from '~/config';
import Contracts from '~/contracts';
import { fetchBalances, queryTokensFilter, updateTokensFilter } from './balancesActions';
const log = getLogger(LOG_KEYS.Balances);
let instance = null;
export default class Balances {
@@ -29,40 +28,20 @@ export default class Balances {
this._api = api;
this._store = store;
this._tokenreg = null;
this._tokenregSID = null;
this._tokenMetaSID = null;
this._apiSubs = [];
this._blockNumberSID = null;
this._accountsInfoSID = null;
// Throtthled load tokens (no more than once
// every minute)
this.loadTokens = throttle(
this._loadTokens,
60 * 1000,
{ leading: true, trailing: true }
);
// Throttled `_fetchBalances` function
// Throttled `_fetchEthBalances` function
// that gets called max once every 40s
this.longThrottledFetch = throttle(
this._fetchBalances,
this._fetchEthBalances,
40 * 1000,
{ leading: false, trailing: true }
{ leading: true, trailing: false }
);
this.shortThrottledFetch = throttle(
this._fetchBalances,
this._fetchEthBalances,
2 * 1000,
{ leading: false, trailing: true }
);
// Fetch all tokens every 2 minutes
this.throttledTokensFetch = throttle(
this._fetchTokens,
2 * 60 * 1000,
{ leading: false, trailing: true }
{ leading: true, trailing: false }
);
// Unsubscribe previous instance if it exists
@@ -71,17 +50,19 @@ export default class Balances {
}
}
static get (store = {}) {
static get (store) {
if (!instance && store) {
const { api } = store.getState();
return Balances.instantiate(store, api);
return Balances.init(store);
} else if (!instance) {
throw new Error('The Balances Provider has not been initialized yet');
}
return instance;
}
static instantiate (store, api) {
static init (store) {
const { api } = store.getState();
if (!instance) {
instance = new Balances(store, api);
}
@@ -91,15 +72,13 @@ export default class Balances {
static start () {
if (!instance) {
return Promise.reject('BalancesProvider has not been intiated yet');
return Promise.reject('BalancesProvider has not been initiated yet');
}
const self = instance;
// Unsubscribe from previous subscriptions
return Balances
.stop()
.then(() => self.loadTokens())
return Balances.stop()
.then(() => {
const promises = [
self.subscribeBlockNumber(),
@@ -107,7 +86,8 @@ export default class Balances {
];
return Promise.all(promises);
});
})
.then(() => self.fetchEthBalances());
}
static stop () {
@@ -116,71 +96,35 @@ export default class Balances {
}
const self = instance;
const promises = [];
const promises = self._apiSubs.map((subId) => self._api.unsubscribe(subId));
if (self._blockNumberSID) {
const p = self._api
.unsubscribe(self._blockNumberSID)
.then(() => {
self._blockNumberSID = null;
});
promises.push(p);
}
if (self._accountsInfoSID) {
const p = self._api
.unsubscribe(self._accountsInfoSID)
.then(() => {
self._accountsInfoSID = null;
});
promises.push(p);
}
// Unsubscribe without adding the promises
// to the result, since it would have to wait for a
// reconnection to resolve if the Node is disconnected
if (self._tokenreg) {
if (self._tokenregSID) {
const tokenregSID = self._tokenregSID;
self._tokenreg
.unsubscribe(tokenregSID)
.then(() => {
if (self._tokenregSID === tokenregSID) {
self._tokenregSID = null;
}
});
}
if (self._tokenMetaSID) {
const tokenMetaSID = self._tokenMetaSID;
self._tokenreg
.unsubscribe(tokenMetaSID)
.then(() => {
if (self._tokenMetaSID === tokenMetaSID) {
self._tokenMetaSID = null;
}
});
}
}
return Promise.all(promises);
return Promise.all(promises)
.then(() => {
self._apiSubs = [];
});
}
subscribeAccountsInfo () {
// Don't trigger the balances updates on first call (when the
// subscriptions are setup)
let firstcall = true;
return this._api
.subscribe('parity_allAccountsInfo', (error, accountsInfo) => {
if (error) {
return console.warn('balances::subscribeAccountsInfo', error);
}
if (firstcall) {
firstcall = false;
return;
}
this.fetchAllBalances();
this._store.dispatch(updateTokensFilter());
this.fetchEthBalances();
})
.then((accountsInfoSID) => {
this._accountsInfoSID = accountsInfoSID;
.then((subId) => {
this._apiSubs.push(subId);
})
.catch((error) => {
console.warn('_subscribeAccountsInfo', error);
@@ -188,161 +132,57 @@ export default class Balances {
}
subscribeBlockNumber () {
// Don't trigger the balances updates on first call (when the
// subscriptions are setup)
let firstcall = true;
return this._api
.subscribe('eth_blockNumber', (error) => {
.subscribe('eth_blockNumber', (error, block) => {
if (error) {
return console.warn('_subscribeBlockNumber', error);
return console.warn('balances::subscribeBlockNumber', error);
}
if (firstcall) {
firstcall = false;
return;
}
this._store.dispatch(queryTokensFilter());
return this.fetchAllBalances();
return this.fetchEthBalances();
})
.then((blockNumberSID) => {
this._blockNumberSID = blockNumberSID;
.then((subId) => {
this._apiSubs.push(subId);
})
.catch((error) => {
console.warn('_subscribeBlockNumber', error);
});
}
fetchAllBalances (options = {}) {
// If it's a network change, reload the tokens
// ( and then fetch the tokens balances ) and fetch
// the accounts balances
if (options.changedNetwork) {
this.loadTokens({ skipNotifications: true });
this.loadTokens.flush();
fetchEthBalances (options = {}) {
log.debug('fetching eth balances (throttled)...');
this.fetchBalances({
force: true,
skipNotifications: true
});
return;
}
this.fetchTokensBalances(options);
this.fetchBalances(options);
}
fetchTokensBalances (options) {
const { skipNotifications = false, force = false } = options;
this.throttledTokensFetch(skipNotifications);
if (force) {
this.throttledTokensFetch.flush();
}
}
fetchBalances (options) {
const { skipNotifications = false, force = false } = options;
const { syncing } = this._store.getState().nodeStatus;
if (options.force) {
return this._fetchEthBalances();
}
// If syncing, only retrieve balances once every
// few seconds
if (syncing || syncing === null) {
this.shortThrottledFetch.cancel();
this.longThrottledFetch(skipNotifications);
if (force) {
this.longThrottledFetch.flush();
}
return;
return this.longThrottledFetch();
}
this.longThrottledFetch.cancel();
this.shortThrottledFetch(skipNotifications);
if (force) {
this.shortThrottledFetch.flush();
}
return this.shortThrottledFetch();
}
_fetchBalances (skipNotifications = false) {
this._store.dispatch(fetchBalances(null, skipNotifications));
}
_fetchEthBalances (skipNotifications = false) {
log.debug('fetching eth balances (real)...');
_fetchTokens (skipNotifications = false) {
this._store.dispatch(fetchTokensBalances(null, null, skipNotifications));
}
const { dispatch, getState } = this._store;
getTokenRegistry () {
return Contracts.get().tokenReg.getContract();
}
_loadTokens (options = {}) {
return this
.getTokenRegistry()
.then((tokenreg) => {
this._tokenreg = tokenreg;
this._store.dispatch(loadTokens(options));
return this.attachToTokens(tokenreg);
})
.catch((error) => {
console.warn('balances::loadTokens', error);
});
}
attachToTokens (tokenreg) {
return Promise
.all([
this.attachToTokenMetaChange(tokenreg),
this.attachToNewToken(tokenreg)
]);
}
attachToNewToken (tokenreg) {
if (this._tokenregSID) {
return Promise.resolve();
}
return tokenreg.instance.Registered
.subscribe({
fromBlock: 0,
toBlock: 'latest',
skipInitFetch: true
}, (error, logs) => {
if (error) {
return console.error('balances::attachToNewToken', 'failed to attach to tokenreg Registered', error.toString(), error.stack);
}
this.handleTokensLogs(logs);
})
.then((tokenregSID) => {
this._tokenregSID = tokenregSID;
});
}
attachToTokenMetaChange (tokenreg) {
if (this._tokenMetaSID) {
return Promise.resolve();
}
return tokenreg.instance.MetaChanged
.subscribe({
fromBlock: 0,
toBlock: 'latest',
topics: [ null, padRight(this._api.util.asciiToHex('IMG'), 32) ],
skipInitFetch: true
}, (error, logs) => {
if (error) {
return console.error('balances::attachToTokenMetaChange', 'failed to attach to tokenreg MetaChanged', error.toString(), error.stack);
}
this.handleTokensLogs(logs);
})
.then((tokenMetaSID) => {
this._tokenMetaSID = tokenMetaSID;
});
}
handleTokensLogs (logs) {
const tokenIds = logs.map((log) => log.params.id.value.toNumber());
this._store.dispatch(fetchTokens(tokenIds));
return fetchBalances(null, skipNotifications)(dispatch, getState);
}
}

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { uniq, isEqual } from 'lodash';
import { difference, uniq } from 'lodash';
import { push } from 'react-router-redux';
import { notifyTransaction } from '~/util/notifications';
@@ -22,11 +22,16 @@ import { ETH_TOKEN, fetchAccountsBalances } from '~/util/tokens';
import { LOG_KEYS, getLogger } from '~/config';
import { sha3 } from '~/api/util/sha3';
import { fetchTokens } from './tokensActions';
const TRANSFER_SIGNATURE = sha3('Transfer(address,address,uint256)');
const log = getLogger(LOG_KEYS.Balances);
let tokensFilter = {};
let tokensFilter = {
tokenAddresses: [],
addresses: []
};
function _setBalances (balances) {
return {
@@ -63,13 +68,10 @@ function setBalances (updates, skipNotifications = false) {
dispatch(notifyBalanceChange(who, prevTokenValue, nextTokenValue, token));
}
// Add the token if it's native ETH or if it has a value
if (token.native || nextTokenValue.gt(0)) {
nextBalances[who] = {
...(nextBalances[who] || {}),
[tokenId]: nextTokenValue
};
}
nextBalances[who] = {
...(nextBalances[who] || {}),
[tokenId]: nextTokenValue
};
});
});
@@ -100,41 +102,92 @@ function notifyBalanceChange (who, fromValue, toValue, token) {
}
// TODO: fetch txCount when needed
export function fetchBalances (_addresses, skipNotifications = false) {
return fetchTokensBalances(_addresses, [ ETH_TOKEN ], skipNotifications);
export function fetchBalances (addresses, skipNotifications = false) {
return (dispatch, getState) => {
const { personal } = getState();
const { visibleAccounts, accounts } = personal;
const addressesToFetch = addresses || uniq(visibleAccounts.concat(Object.keys(accounts)));
const updates = addressesToFetch.reduce((updates, who) => {
updates[who] = [ ETH_TOKEN.id ];
return updates;
}, {});
return fetchTokensBalances(updates, skipNotifications)(dispatch, getState);
};
}
export function updateTokensFilter (_addresses, _tokens, options = {}) {
export function updateTokensFilter (options = {}) {
return (dispatch, getState) => {
const { api, personal, tokens } = getState();
const { visibleAccounts, accounts } = personal;
const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts)));
const addresses = uniq(_addresses || addressesToFetch || []).sort();
const addresses = uniq(visibleAccounts.concat(Object.keys(accounts)));
const tokensToUpdate = Object.values(tokens);
const tokensAddressMap = Object.values(tokens).reduce((map, token) => {
map[token.address] = token;
return map;
}, {});
const tokensToUpdate = _tokens || Object.values(tokens);
const tokenAddresses = tokensToUpdate
.map((t) => t.address)
.filter((address) => address)
.sort();
.filter((address) => address && !/^(0x)?0*$/.test(address));
// Token Addresses that are not in the current filter
const newTokenAddresses = difference(tokenAddresses, tokensFilter.tokenAddresses);
// Addresses that are not in the current filter (omit those
// that the filter includes)
const newAddresses = difference(addresses, tokensFilter.addresses);
if (tokensFilter.filterFromId || tokensFilter.filterToId) {
// Has the tokens addresses changed (eg. a network change)
const sameTokens = isEqual(tokenAddresses, tokensFilter.tokenAddresses);
// Addresses that are not in the current filter (omit those
// that the filter includes)
const newAddresses = addresses.filter((address) => !tokensFilter.addresses.includes(address));
// If no new addresses and the same tokens, don't change the filter
if (sameTokens && newAddresses.length === 0) {
if (newTokenAddresses.length === 0 && newAddresses.length === 0) {
log.debug('no need to update token filter', addresses, tokenAddresses, tokensFilter);
return queryTokensFilter(tokensFilter)(dispatch, getState);
return;
}
}
log.debug('updating the token filter', addresses, tokenAddresses);
const promises = [];
const updates = {};
const allTokenIds = tokensToUpdate.map((token) => token.id);
const newTokenIds = newTokenAddresses.map((address) => tokensAddressMap[address].id);
newAddresses.forEach((newAddress) => {
updates[newAddress] = allTokenIds;
});
difference(addresses, newAddresses).forEach((oldAddress) => {
updates[oldAddress] = newTokenIds;
});
log.debug('updating the token filter', addresses, tokenAddresses);
const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ];
const topicsTo = [ TRANSFER_SIGNATURE, null, addresses ];
const filterOptions = {
fromBlock: 'latest',
toBlock: 'latest',
address: tokenAddresses
};
const optionsFrom = {
...filterOptions,
topics: topicsFrom
};
const optionsTo = {
...filterOptions,
topics: topicsTo
};
promises.push(
api.eth.newFilter(optionsFrom),
api.eth.newFilter(optionsTo)
);
if (tokensFilter.filterFromId) {
promises.push(api.eth.uninstallFilter(tokensFilter.filterFromId));
@@ -144,48 +197,16 @@ export function updateTokensFilter (_addresses, _tokens, options = {}) {
promises.push(api.eth.uninstallFilter(tokensFilter.filterToId));
}
Promise
.all([
api.eth.blockNumber()
].concat(promises))
.then(([ block ]) => {
const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ];
const topicsTo = [ TRANSFER_SIGNATURE, null, addresses ];
const filterOptions = {
fromBlock: block,
toBlock: 'pending',
address: tokenAddresses
};
const optionsFrom = {
...filterOptions,
topics: topicsFrom
};
const optionsTo = {
...filterOptions,
topics: topicsTo
};
const newFilters = Promise.all([
api.eth.newFilter(optionsFrom),
api.eth.newFilter(optionsTo)
]);
return newFilters;
})
return Promise.all(promises)
.then(([ filterFromId, filterToId ]) => {
const nextTokensFilter = {
filterFromId, filterToId,
addresses, tokenAddresses
};
const { skipNotifications } = options;
tokensFilter = nextTokensFilter;
fetchTokensBalances(addresses, tokensToUpdate, skipNotifications)(dispatch, getState);
})
.then(() => fetchTokensBalances(updates)(dispatch, getState))
.catch((error) => {
console.warn('balances::updateTokensFilter', error);
});
@@ -194,12 +215,7 @@ export function updateTokensFilter (_addresses, _tokens, options = {}) {
export function queryTokensFilter () {
return (dispatch, getState) => {
const { api, personal, tokens } = getState();
const { visibleAccounts, accounts } = personal;
const allAddresses = visibleAccounts.concat(Object.keys(accounts));
const addressesToFetch = uniq(allAddresses);
const lcAddresses = addressesToFetch.map((a) => a.toLowerCase());
const { api } = getState();
Promise
.all([
@@ -207,67 +223,107 @@ export function queryTokensFilter () {
api.eth.getFilterChanges(tokensFilter.filterToId)
])
.then(([ logsFrom, logsTo ]) => {
const addresses = [];
const tokenAddresses = [];
const logs = logsFrom.concat(logsTo);
const logs = [].concat(logsFrom, logsTo);
if (logs.length > 0) {
if (logs.length === 0) {
return;
} else {
log.debug('got tokens filter logs', logs);
}
const { personal, tokens } = getState();
const { visibleAccounts, accounts } = personal;
const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts)));
const lcAddresses = addressesToFetch.map((a) => a.toLowerCase());
const lcTokensMap = Object.values(tokens).reduce((map, token) => {
map[token.address.toLowerCase()] = token;
return map;
});
// The keys are the account addresses,
// and the value is an Array of the tokens addresses
// to update
const updates = {};
logs
.forEach((log) => {
const tokenAddress = log.address;
.forEach((log, index) => {
const tokenAddress = log.address.toLowerCase();
const token = lcTokensMap[tokenAddress];
const fromAddress = '0x' + log.topics[1].slice(-40);
const toAddress = '0x' + log.topics[2].slice(-40);
// logs = [ ...logsFrom, ...logsTo ]
const topicIdx = index < logsFrom.length ? 1 : 2;
const address = ('0x' + log.topics[topicIdx].slice(-40)).toLowerCase();
const addressIndex = lcAddresses.indexOf(address);
const fromAddressIndex = lcAddresses.indexOf(fromAddress);
const toAddressIndex = lcAddresses.indexOf(toAddress);
if (addressIndex > -1) {
const who = addressesToFetch[addressIndex];
if (fromAddressIndex > -1) {
addresses.push(addressesToFetch[fromAddressIndex]);
updates[who] = [].concat(updates[who] || [], token.id);
}
if (toAddressIndex > -1) {
addresses.push(addressesToFetch[toAddressIndex]);
}
tokenAddresses.push(tokenAddress);
});
if (addresses.length === 0) {
// No accounts to update
if (Object.keys(updates).length === 0) {
return;
}
const tokensToUpdate = Object.values(tokens)
.filter((t) => tokenAddresses.includes(t.address));
Object.keys(updates).forEach((who) => {
// Keep non-empty token addresses
updates[who] = uniq(updates[who]);
});
fetchTokensBalances(uniq(addresses), tokensToUpdate)(dispatch, getState);
fetchTokensBalances(updates)(dispatch, getState);
});
};
}
export function fetchTokensBalances (_addresses = null, _tokens = null, skipNotifications = false) {
export function fetchTokensBalances (updates, skipNotifications = false) {
return (dispatch, getState) => {
const { api, personal, tokens } = getState();
const { visibleAccounts, accounts } = personal;
const allTokens = Object.values(tokens);
const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts)));
const addresses = _addresses || addressesToFetch;
const tokensToUpdate = _tokens || allTokens;
if (!updates) {
const { visibleAccounts, accounts } = personal;
const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts)));
if (addresses.length === 0) {
return Promise.resolve();
updates = addressesToFetch.reduce((updates, who) => {
updates[who] = allTokens.map((token) => token.id);
return updates;
}, {});
}
const updates = addresses.reduce((updates, who) => {
updates[who] = tokensToUpdate.map((token) => token.id);
return updates;
}, {});
let start = Date.now();
return fetchAccountsBalances(api, allTokens, updates)
.then((balances) => {
log.debug('got tokens balances', balances, updates, `(took ${Date.now() - start}ms)`);
// Tokens info might not be fetched yet (to not load
// tokens we don't care about)
const tokenIdsToFetch = Object.values(balances)
.reduce((tokenIds, balance) => {
const nextTokenIds = Object.keys(balance)
.filter((tokenId) => balance[tokenId].gt(0));
return tokenIds.concat(nextTokenIds);
}, []);
const tokenIndexesToFetch = uniq(tokenIdsToFetch)
.filter((tokenId) => tokens[tokenId] && tokens[tokenId].index && !tokens[tokenId].fetched)
.map((tokenId) => tokens[tokenId].index);
if (tokenIndexesToFetch.length === 0) {
return balances;
}
start = Date.now();
return fetchTokens(tokenIndexesToFetch)(dispatch, getState)
.then(() => log.debug('token indexes fetched', tokenIndexesToFetch, `(took ${Date.now() - start}ms)`))
.then(() => balances);
})
.then((balances) => {
dispatch(setBalances(balances, skipNotifications));
})

View File

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

View File

@@ -0,0 +1,343 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { range } from 'lodash';
import { addCertification, removeCertification } from './actions';
import { getLogger, LOG_KEYS } from '~/config';
import Contract from '~/api/contract';
import { bytesToHex, hexToAscii } from '~/api/util/format';
import Contracts from '~/contracts';
import CertifierABI from '~/contracts/abi/certifier.json';
import { querier } from './enhanced-querier';
const log = getLogger(LOG_KEYS.CertificationsMiddleware);
let self = null;
export default class CertifiersMonitor {
constructor (api, store) {
this._api = api;
this._name = 'Certifiers';
this._store = store;
this._contract = new Contract(this.api, CertifierABI);
this._contractEvents = [ 'Confirmed', 'Revoked' ]
.map((name) => this.contract.events.find((e) => e.name === name));
this.certifiers = {};
this.fetchedAccounts = {};
this.load();
}
static get () {
if (self) {
return self;
}
self = new CertifiersMonitor();
return self;
}
static init (api, store) {
if (!self) {
self = new CertifiersMonitor(api, store);
}
}
get api () {
return this._api;
}
get contract () {
return this._contract;
}
get contractEvents () {
return this._contractEvents;
}
get name () {
return this._name;
}
get store () {
return this._store;
}
get registry () {
return this._registry;
}
get registryEvents () {
return this._registryEvents;
}
checkFilters () {
this.checkCertifiersFilter();
this.checkRegistryFilter();
}
checkCertifiersFilter () {
if (!this.certifiersFilter) {
return;
}
this.api.eth.getFilterChanges(this.certifiersFilter)
.then((logs) => {
if (logs.length === 0) {
return;
}
const parsedLogs = this.contract.parseEventLogs(logs).filter((log) => log.params);
log.debug('received certifiers logs', parsedLogs);
const promises = parsedLogs.map((log) => {
const account = log.params.who.value;
const certifier = Object.values(this.certifiers).find((c) => c.address === log.address);
if (!certifier) {
log.warn('could not find the certifier', { certifiers: this.certifiers, log });
return Promise.resolve();
}
return this.fetchAccount(account, { ids: [ certifier.id ] });
});
return Promise.all(promises);
})
.catch((error) => {
console.error(error);
});
}
checkRegistryFilter () {
if (!this.registryFilter) {
return;
}
this.api.eth.getFilterChanges(this.registryFilter)
.then((logs) => {
if (logs.length === 0) {
return;
}
const parsedLogs = this.contract.parseEventLogs(logs).filter((log) => log.params);
const indexes = parsedLogs.map((log) => log.params && log.params.id.value.toNumber());
log.debug('received registry logs', parsedLogs);
return this.fetchElements(indexes);
})
.catch((error) => {
console.error(error);
});
}
/**
* Initial load of the Monitor.
* Fetch the contract from the Registry, and
* load the elements addresses
*/
load () {
const badgeReg = Contracts.get().badgeReg;
log.debug(`loading the ${this.name} monitor...`);
return badgeReg.getContract()
.then((registryContract) => {
this._registry = registryContract;
this._registryEvents = [ 'Registered', 'Unregistered', 'MetaChanged', 'AddressChanged' ]
.map((name) => this.registry.events.find((e) => e.name === name));
return this.registry.instance.badgeCount.call({});
})
.then((count) => {
log.debug(`found ${count.toFormat()} registered contracts for ${this.name}`);
return this.fetchElements(range(count.toNumber()));
})
.then(() => {
return this.setRegistryFilter();
})
.then(() => {
// Listen for new blocks
return this.api.subscribe('eth_blockNumber', (err) => {
if (err) {
return;
}
this.checkFilters();
});
})
.then(() => {
log.debug(`loaded the ${this.name} monitor!`, this.certifiers);
})
.catch((error) => {
log.error(error);
});
}
/**
* Fetch the given registered element
*/
fetchElements (indexes) {
const badgeReg = Contracts.get().badgeReg;
const { instance } = this.registry;
const sorted = indexes.sort();
const from = sorted[0];
const last = sorted[sorted.length - 1];
const limit = last - from + 1;
// Fetch the address, name and owner in one batch
return querier(this.api, { address: instance.address, from, limit }, instance.badge)
.then((results) => {
const 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(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;
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())
.then(() => {
this.fetchedAccounts[address] = true;
});
}
setCertifiersFilter () {
const accounts = Object.keys(this.fetchedAccounts);
const addresses = Object.values(this.certifiers).map((c) => c.address);
// The events have as first indexed data the account address
const topics = [
this.contractEvents.map((event) => '0x' + event.signature),
accounts
];
if (accounts.length === 0 || addresses.length === 0) {
return;
}
const promise = this.certifiersFilter
? this.api.eth.uninstallFilter(this.certifiersFilter)
: Promise.resolve();
log.debug('setting up registry filter', { topics, accounts, addresses });
return promise
.then(() => this.api.eth.newFilter({
fromBlock: 'latest',
toBlock: 'latest',
address: addresses,
topics
}))
.then((filterId) => {
this.certifiersFilter = filterId;
})
.catch((error) => {
console.error(error);
});
}
setRegistryFilter () {
const { address } = this.registry.instance;
const topics = [ this.registryEvents.map((event) => '0x' + event.signature) ];
log.debug('setting up registry filter', { topics, address });
return this.api.eth
.newFilter({
fromBlock: 'latest',
toBlock: 'latest',
address, topics
})
.then((filterId) => {
this.registryFilter = filterId;
})
.catch((error) => {
console.error(error);
});
}
}

View File

@@ -0,0 +1,96 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { padRight, padLeft } from '~/api/util/format';
/**
* Bytecode of this contract:
*
*
pragma solidity ^0.4.10;
contract Querier {
function Querier
(address addr, bytes32 sign, uint out_size, uint from, uint limit)
public
{
// The size is 32 bytes for each
// value, plus 32 bytes for the count
uint m_size = out_size * limit + 32;
bytes32 p_return;
uint p_in;
uint p_out;
assembly {
p_return := mload(0x40)
mstore(0x40, add(p_return, m_size))
mstore(p_return, limit)
p_in := mload(0x40)
mstore(0x40, add(p_in, 0x24))
mstore(p_in, sign)
p_out := add(p_return, 0x20)
}
for (uint i = from; i < from + limit; i++) {
assembly {
mstore(add(p_in, 0x4), i)
call(gas, addr, 0x0, p_in, 0x24, p_out, out_size)
p_out := add(p_out, out_size)
pop
}
}
assembly {
return (p_return, m_size)
}
}
}
*/
export const bytecode = '0x60606040523415600e57600080fd5b60405160a0806099833981016040528080519190602001805191906020018051919060200180519190602001805191505082810260200160008080806040519350848401604052858452604051602481016040528981529250505060208201855b858701811015609457806004840152878260248560008e5af15090870190600101606f565b8484f300';
export const querier = (api, { address, from, limit }, method) => {
const { outputs, signature } = method;
const outLength = 32 * outputs.length;
const callargs = [
padLeft(address, 32),
padRight(signature, 32),
padLeft(outLength, 32),
padLeft(from, 32),
padLeft(limit, 32)
].map((v) => v.slice(2)).join('');
const calldata = bytecode + callargs;
return api.eth.call({ data: calldata })
.then((result) => {
const data = result.slice(2);
const results = [];
for (let i = 0; i < limit; i++) {
const datum = data.substr(2 * (32 + i * outLength), 2 * outLength);
const decoded = method.decodeOutput('0x' + datum).map((t) => t.value);
results.push(decoded);
}
return results;
});
};

View File

@@ -14,222 +14,22 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { uniq, range, debounce } from 'lodash';
import { addCertification, removeCertification } from './actions';
import { getLogger, LOG_KEYS } from '~/config';
import Contract from '~/api/contract';
import Contracts from '~/contracts';
import 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: 0,
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;

View File

@@ -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 };
}

View File

@@ -18,6 +18,7 @@ export Balances from './balances';
export Personal from './personal';
export Signer from './signer';
export Status from './status';
export Tokens from './tokens';
export apiReducer from './apiReducer';
export balancesReducer from './balancesReducer';

View File

@@ -16,37 +16,117 @@
import { personalAccountsInfo } from './personalActions';
let instance;
export default class Personal {
constructor (store, api) {
this._api = api;
this._store = store;
}
start () {
this._removeDeleted();
this._subscribeAccountsInfo();
static get (store) {
if (!instance && store) {
return Personal.init(store);
}
return instance;
}
static init (store) {
const { api } = store.getState();
if (!instance) {
instance = new Personal(store, api);
} else if (!instance) {
throw new Error('The Personal Provider has not been initialized yet');
}
return instance;
}
static start () {
const self = instance;
return Personal.stop()
.then(() => Promise.all([
self._removeDeleted(),
self._subscribeAccountsInfo()
]));
}
static stop () {
if (!instance) {
return Promise.resolve();
}
const self = instance;
return self._unsubscribeAccountsInfo();
}
_subscribeAccountsInfo () {
this._api
.subscribe('parity_allAccountsInfo', (error, accountsInfo) => {
if (error) {
console.error('parity_allAccountsInfo', error);
return;
}
let resolved = false;
// Add the address to each accounts
Object.keys(accountsInfo)
.forEach((address) => {
accountsInfo[address].address = address;
});
// The Promise will be resolved when the first
// accounts are loaded
return new Promise((resolve, reject) => {
this._api
.subscribe('parity_allAccountsInfo', (error, accountsInfo) => {
if (error) {
console.error('parity_allAccountsInfo', error);
this._store.dispatch(personalAccountsInfo(accountsInfo));
});
if (!resolved) {
resolved = true;
return reject(error);
}
return;
}
// Add the address to each accounts
Object.keys(accountsInfo)
.forEach((address) => {
accountsInfo[address].address = address;
});
const { dispatch, getState } = this._store;
personalAccountsInfo(accountsInfo)(dispatch, getState)
.then(() => {
if (!resolved) {
resolved = true;
return resolve();
}
})
.catch((error) => {
if (!resolved) {
resolved = true;
return reject(error);
}
});
})
.then((subId) => {
this.subscriptionId = subId;
});
});
}
_unsubscribeAccountsInfo () {
// Unsubscribe to any previous
// subscriptions
if (this.subscriptionId) {
return this._api
.unsubscribe(this.subscriptionId)
.then(() => {
this.subscriptionId = null;
});
}
return Promise.resolve();
}
_removeDeleted () {
this._api.parity
return this._api.parity
.allAccountsInfo()
.then((accountsInfo) => {
return Promise.all(

View File

@@ -17,6 +17,7 @@
import { isEqual, intersection } from 'lodash';
import BalancesProvider from './balances';
import TokensProvider from './tokens';
import { updateTokensFilter } from './balancesActions';
import { attachWallets } from './walletActions';
@@ -70,7 +71,7 @@ export function personalAccountsInfo (accountsInfo) {
return WalletsUtils.fetchOwners(walletContract.at(wallet.address));
});
Promise
return Promise
.all(_fetchOwners)
.then((walletsOwners) => {
return Object
@@ -135,10 +136,6 @@ export function personalAccountsInfo (accountsInfo) {
hardware
}));
dispatch(attachWallets(wallets));
BalancesProvider.get().fetchAllBalances({
force: true
});
})
.catch((error) => {
console.warn('personalAccountsInfo', error);
@@ -176,12 +173,17 @@ export function setVisibleAccounts (addresses) {
return;
}
// Update the Tokens filter to take into account the new
// addresses
dispatch(updateTokensFilter());
const promises = [];
BalancesProvider.get().fetchBalances({
force: true
});
// Update the Tokens filter to take into account the new
// addresses if it is not loading (it fetches the
// balances automatically after loading)
if (!TokensProvider.get().loading) {
promises.push(updateTokensFilter()(dispatch, getState));
}
promises.push(BalancesProvider.get().fetchEthBalances({ force: true }));
return Promise.all(promises);
};
}

View File

@@ -23,12 +23,19 @@ import SavedRequests from '~/views/Application/Requests/savedRequests';
const savedRequests = new SavedRequests();
export const init = (api) => (dispatch) => {
api.subscribe('parity_postTransaction', (error, request) => {
api.subscribe('signer_requestsToConfirm', (error, pending) => {
if (error) {
return console.error(error);
return;
}
dispatch(watchRequest(request));
const requests = pending
.filter((p) => p.payload && p.payload.sendTransaction)
.map((p) => ({
requestId: '0x' + p.id.toString(16),
transaction: p.payload.sendTransaction
}));
requests.forEach((request) => dispatch(watchRequest(request)));
});
api.once('connected', () => {
@@ -47,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 }));
}
@@ -65,8 +83,9 @@ export const trackRequest = (requestId, { transactionHash = null } = {}) => (dis
const requestData = requests[requestId];
let blockSubscriptionId = -1;
// Set the block height to 0 at the beggining
data.blockHeight = new BigNumber(0);
// Set the block height to 1 at the beginning (transaction mined,
// thus one confirmation)
data.blockHeight = new BigNumber(1);
// If the request was a contract deployment,
// then add the contract with the saved metadata to the account

View File

@@ -48,6 +48,12 @@ let store;
function createApi () {
api = {
transport: {
on: sinon.stub()
},
pubsub: {
subscribeAndGetResult: sinon.stub().returns(Promise.reject(new Error('not connected')))
},
net: {
version: sinon.stub().resolves('2')
},

View File

@@ -14,12 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { isEqual } from 'lodash';
import { isEqual, debounce } from 'lodash';
import { LOG_KEYS, getLogger } from '~/config';
import UpgradeStore from '~/modals/UpgradeParity/store';
// import UpgradeStore from '~/modals/UpgradeParity/store';
import BalancesProvider from './balances';
import { statusBlockNumber, statusCollection } from './statusActions';
const log = getLogger(LOG_KEYS.Signer);
@@ -31,7 +30,6 @@ const STATUS_BAD = 'bad';
export default class Status {
_apiStatus = {};
_status = {};
_longStatus = {};
_minerSettings = {};
_timeoutIds = {};
@@ -41,21 +39,14 @@ export default class Status {
constructor (store, api) {
this._api = api;
this._store = store;
this._upgradeStore = UpgradeStore.get(api);
// On connecting, stop all subscriptions
api.on('connecting', this.stop, this);
// On connected, start the subscriptions
api.on('connected', this.start, this);
// On disconnected, stop all subscriptions
api.on('disconnected', this.stop, this);
// this._upgradeStore = UpgradeStore.get(api);
this.updateApiStatus();
}
static instantiate (store, api) {
static init (store) {
const { api } = store.getState();
if (!instance) {
instance = new Status(store, api);
}
@@ -63,59 +54,61 @@ export default class Status {
return instance;
}
static get () {
if (!instance) {
static get (store) {
if (!instance && store) {
return Status.init(store);
} else if (!instance) {
throw new Error('The Status Provider has not been initialized yet');
}
return instance;
}
start () {
static start () {
const self = instance;
log.debug('status::start');
Promise
.all([
this._subscribeBlockNumber(),
const promises = [
self._subscribeBlockNumber(),
self._subscribeNetPeers(),
self._subscribeEthSyncing(),
self._subscribeNodeHealth(),
self._pollLongStatus(),
self._pollApiStatus()
];
this._pollLongStatus(),
this._pollStatus()
])
.then(() => {
return BalancesProvider.start();
});
return Status.stop()
.then(() => Promise.all(promises));
}
stop () {
log.debug('status::stop');
const promises = [];
if (this._blockNumberSubscriptionId) {
const promise = this._api
.unsubscribe(this._blockNumberSubscriptionId)
.then(() => {
this._blockNumberSubscriptionId = null;
});
promises.push(promise);
static stop () {
if (!instance) {
return Promise.resolve();
}
Object.values(this._timeoutIds).forEach((timeoutId) => {
clearTimeout(timeoutId);
});
const self = instance;
const promise = BalancesProvider.stop();
log.debug('status::stop');
promises.push(promise);
self._clearTimeouts();
return Promise.all(promises)
.then(() => true)
return self._unsubscribeBlockNumber()
.catch((error) => {
console.error('status::stop', error);
return true;
})
.then(() => this.updateApiStatus());
.then(() => self.updateApiStatus());
}
getApiStatus = () => {
const { isConnected, isConnecting, needsToken, secureToken } = this._api;
return {
isConnected,
isConnecting,
needsToken,
secureToken
};
}
updateApiStatus () {
@@ -129,6 +122,33 @@ export default class Status {
}
}
_clearTimeouts () {
Object.values(this._timeoutIds).forEach((timeoutId) => {
clearTimeout(timeoutId);
});
}
_overallStatus (health) {
const allWithTime = [health.peers, health.sync, health.time].filter(x => x);
const all = [health.peers, health.sync].filter(x => x);
const statuses = all.map(x => x.status);
const bad = statuses.find(x => x === STATUS_BAD);
const needsAttention = statuses.find(x => x === STATUS_WARN);
const message = allWithTime.map(x => x.message).filter(x => x);
if (all.length) {
return {
status: bad || needsAttention || STATUS_OK,
message
};
}
return {
status: STATUS_BAD,
message: ['Unable to fetch node health.']
};
}
_subscribeBlockNumber = () => {
return this._api
.subscribe('eth_blockNumber', (error, blockNumber) => {
@@ -159,92 +179,81 @@ export default class Status {
});
}
_pollTraceMode = () => {
return this._api.trace
.block()
.then(blockTraces => {
// Assumes not in Trace Mode if no transactions
// in latest block...
return blockTraces.length > 0;
})
.catch(() => false);
_updateStatus = debounce(status => {
this._store.dispatch(statusCollection(status));
}, 2500, {
maxWait: 5000
});
_subscribeEthSyncing = () => {
return this._api.pubsub
.eth
.syncing((error, syncing) => {
if (error) {
return;
}
this._updateStatus({ syncing });
});
}
getApiStatus = () => {
const { isConnected, isConnecting, needsToken, secureToken } = this._api;
_subscribeNetPeers = () => {
return this._api.pubsub
.parity
.netPeers((error, netPeers) => {
if (error || !netPeers) {
return;
}
return {
isConnected,
isConnecting,
needsToken,
secureToken
};
this._store.dispatch(statusCollection({ netPeers }));
});
}
_pollStatus = () => {
const nextTimeout = (timeout = 1000) => {
if (this._timeoutIds.status) {
clearTimeout(this._timeoutIds.status);
}
this._timeoutIds.status = setTimeout(() => this._pollStatus(), timeout);
};
this.updateApiStatus();
if (!this._api.isConnected) {
nextTimeout(250);
return Promise.resolve();
}
const statusPromises = [
this._api.eth.syncing(),
this._api.parity.netPeers(),
this._api.parity.nodeHealth()
];
return Promise
.all(statusPromises)
.then(([ syncing, netPeers, health ]) => {
const status = { netPeers, syncing, health };
_subscribeNodeHealth = () => {
return this._api.pubsub
.parity
.nodeHealth((error, health) => {
if (error || !health) {
return;
}
health.overall = this._overallStatus(health);
health.peers = health.peers || {};
health.sync = health.sync || {};
health.time = health.time || {};
if (!isEqual(status, this._status)) {
this._store.dispatch(statusCollection(status));
this._status = status;
}
})
.catch((error) => {
console.error('_pollStatus', error);
})
.then(() => {
nextTimeout();
this._store.dispatch(statusCollection({ health }));
});
}
_overallStatus = (health) => {
const allWithTime = [health.peers, health.sync, health.time].filter(x => x);
const all = [health.peers, health.sync].filter(x => x);
const statuses = all.map(x => x.status);
const bad = statuses.find(x => x === STATUS_BAD);
const needsAttention = statuses.find(x => x === STATUS_WARN);
const message = allWithTime.map(x => x.message).filter(x => x);
if (all.length) {
return {
status: bad || needsAttention || STATUS_OK,
message
};
_unsubscribeBlockNumber () {
if (this._blockNumberSubscriptionId) {
return this._api
.unsubscribe(this._blockNumberSubscriptionId)
.then(() => {
this._blockNumberSubscriptionId = null;
});
}
return {
status: STATUS_BAD,
message: ['Unable to fetch node health.']
return Promise.resolve();
}
_pollApiStatus = () => {
const nextTimeout = (timeout = 1000) => {
if (this._timeoutIds.status) {
clearTimeout(this._timeoutIds.status);
}
this._timeoutIds.status = setTimeout(() => this._pollApiStatus(), timeout);
};
this.updateApiStatus();
if (!this._api.isConnected) {
nextTimeout(250);
} else {
nextTimeout();
}
}
/**
@@ -259,7 +268,7 @@ export default class Status {
}
const { nodeKindFull } = this._store.getState().nodeStatus;
const defaultTimeout = (nodeKindFull === false ? 240 : 30) * 1000;
const defaultTimeout = (nodeKindFull === false ? 240 : 60) * 1000;
const nextTimeout = (timeout = defaultTimeout) => {
if (this._timeoutIds.longStatus) {
@@ -271,19 +280,18 @@ export default class Status {
const statusPromises = [
this._api.parity.nodeKind(),
this._api.parity.netPeers(),
this._api.web3.clientVersion(),
this._api.net.version(),
this._api.parity.netChain()
];
if (nodeKindFull) {
statusPromises.push(this._upgradeStore.checkUpgrade());
}
// if (nodeKindFull) {
// statusPromises.push(this._upgradeStore.checkUpgrade());
// }
return Promise
.all(statusPromises)
.then(([nodeKind, netPeers, clientVersion, netVersion, netChain]) => {
.then(([nodeKind, clientVersion, netVersion, netChain]) => {
const isTest = [
'2', // morden
'3', // ropsten,
@@ -298,7 +306,6 @@ export default class Status {
const longStatus = {
nodeKind,
nodeKindFull,
netPeers,
clientVersion,
netChain,
netVersion,
@@ -310,11 +317,12 @@ export default class Status {
this._longStatus = longStatus;
}
})
.then(() => {
nextTimeout();
})
.catch((error) => {
console.error('_pollLongStatus', error);
})
.then(() => {
nextTimeout(60000);
nextTimeout(30000);
});
}
}

View File

@@ -0,0 +1,166 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { updateTokensFilter } from './balancesActions';
import { loadTokens, fetchTokens } from './tokensActions';
import { padRight } from '~/api/util/format';
import { LOG_KEYS, getLogger } from '~/config';
import Contracts from '~/contracts';
const log = getLogger(LOG_KEYS.Balances);
let instance = null;
export default class Tokens {
constructor (store, api) {
this._api = api;
this._store = store;
this._tokenreg = null;
this._tokenregSubs = [];
this._loading = false;
}
get loading () {
return this._loading;
}
static get (store) {
if (!instance && store) {
return Tokens.init(store);
} else if (!instance) {
throw new Error('The Tokens Provider has not been initialized yet');
}
return instance;
}
static init (store) {
const { api } = store.getState();
if (!instance) {
instance = new Tokens(store, api);
}
return instance;
}
static start () {
if (!instance) {
return Promise.reject('Tokens Provider has not been initiated yet');
}
const self = instance;
self._loading = true;
// Unsubscribe from previous subscriptions
return Tokens.stop()
.then(() => self.loadTokens())
.then(() => {
self._loading = false;
});
}
static stop () {
if (!instance) {
return Promise.resolve();
}
const self = instance;
// Unsubscribe without adding the promises
// to the result, since it would have to wait for a
// reconnection to resolve if the Node is disconnected
if (self._tokenreg) {
const tokenregPromises = self._tokenregSubs
.map((tokenregSID) => self._tokenreg.unsubscribe(tokenregSID));
Promise.all(tokenregPromises)
.then(() => {
self._tokenregSubs = [];
});
}
return Promise.resolve();
}
attachToTokensEvents (tokenreg) {
const metaTopics = [ null, padRight(this._api.util.asciiToHex('IMG'), 32) ];
return Promise
.all([
this._attachToTokenregEvents(tokenreg, 'Registered'),
this._attachToTokenregEvents(tokenreg, 'MetaChanged', metaTopics)
]);
}
getTokenRegistry () {
return Contracts.get().tokenReg.getContract();
}
loadTokens (options = {}) {
const { dispatch, getState } = this._store;
return this
.getTokenRegistry()
.then((tokenreg) => {
this._tokenreg = tokenreg;
return loadTokens(options)(dispatch, getState);
})
.then(() => updateTokensFilter()(dispatch, getState))
.then(() => this.attachToTokensEvents(this._tokenreg))
.catch((error) => {
console.warn('balances::loadTokens', error);
});
}
_attachToTokenregEvents (tokenreg, event, topics = []) {
if (this._tokenregSID) {
return Promise.resolve();
}
return tokenreg.instance[event]
.subscribe({
fromBlock: 'latest',
toBlock: 'latest',
topics: topics,
skipInitFetch: true
}, (error, logs) => {
if (error) {
return console.error('balances::attachToNewToken', 'failed to attach to tokenreg Registered', error.toString(), error.stack);
}
this._handleTokensLogs(logs);
})
.then((tokenregSID) => {
this._tokenregSubs.push(tokenregSID);
});
}
_handleTokensLogs (logs) {
const { dispatch, getState } = this._store;
const tokenIds = logs.map((log) => log.params.id.value.toNumber());
log.debug('got TokenRegistry logs', logs, tokenIds);
return fetchTokens(tokenIds)(dispatch, getState)
.then(() => updateTokensFilter()(dispatch, getState));
}
}

View File

@@ -14,56 +14,223 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { uniq } from 'lodash';
import { chunk, uniq } from 'lodash';
import store from 'store';
import Contracts from '~/contracts';
import { LOG_KEYS, getLogger } from '~/config';
import { fetchTokenIds, fetchTokenInfo } from '~/util/tokens';
import { fetchTokenIds, fetchTokensBasics, fetchTokensInfo, fetchTokensImages } from '~/util/tokens';
import { updateTokensFilter } from './balancesActions';
import { setAddressImage } from './imagesActions';
const TOKENS_CACHE_LS_KEY_PREFIX = '_parity::tokens::';
const log = getLogger(LOG_KEYS.Balances);
export function setTokens (tokens) {
function _setTokens (tokens) {
return {
type: 'setTokens',
tokens
};
}
export function setTokens (nextTokens) {
return (dispatch, getState) => {
const { nodeStatus, tokens: prevTokens } = getState();
const { tokenReg } = Contracts.get();
const tokens = {
...prevTokens,
...nextTokens
};
return tokenReg.getContract()
.then((tokenRegContract) => {
const lsKey = TOKENS_CACHE_LS_KEY_PREFIX + nodeStatus.netChain;
store.set(lsKey, {
tokenreg: tokenRegContract.address,
tokens
});
})
.catch((error) => {
console.error(error);
})
.then(() => {
dispatch(_setTokens(nextTokens));
});
};
}
function loadCachedTokens (tokenRegContract) {
return (dispatch, getState) => {
const { nodeStatus } = getState();
const lsKey = TOKENS_CACHE_LS_KEY_PREFIX + nodeStatus.netChain;
const cached = store.get(lsKey);
if (cached) {
// Check if we have data from the right contract
if (cached.tokenreg === tokenRegContract.address && cached.tokens) {
log.debug('found cached tokens', cached.tokens);
// Fetch all the tokens images on load
// (it's the only thing that might have changed)
const tokenIndexes = Object.values(cached.tokens)
.filter((t) => t && t.fetched)
.map((t) => t.index);
fetchTokensData(tokenRegContract, tokenIndexes)(dispatch, getState);
} else {
store.remove(lsKey);
}
}
};
}
export function loadTokens (options = {}) {
log.debug('loading tokens', Object.keys(options).length ? options : '');
return (dispatch, getState) => {
const { tokenReg } = Contracts.get();
tokenReg.getInstance()
.then((tokenRegInstance) => {
return fetchTokenIds(tokenRegInstance);
return tokenReg.getContract()
.then((tokenRegContract) => {
loadCachedTokens(tokenRegContract)(dispatch, getState);
return fetchTokenIds(tokenRegContract.instance);
})
.then((tokenIndexes) => dispatch(fetchTokens(tokenIndexes, options)))
.then((tokenIndexes) => loadTokensBasics(tokenIndexes, options)(dispatch, getState))
.catch((error) => {
console.warn('tokens::loadTokens', error);
});
};
}
export function fetchTokens (_tokenIndexes, options = {}) {
const tokenIndexes = uniq(_tokenIndexes || []);
export function loadTokensBasics (tokenIndexes, options) {
const limit = 64;
return (dispatch, getState) => {
const { api } = getState();
const { tokenReg } = Contracts.get();
const nextTokens = {};
const count = tokenIndexes.length;
log.debug('loading basic tokens', tokenIndexes);
if (count === 0) {
return Promise.resolve();
}
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);
promise = promise
.then(() => fetchTokensBasics(api, tokenRegContract, from, lowerLimit))
.then((results) => {
results
.forEach((token) => {
nextTokens[token.id] = token;
});
});
}
return promise;
})
.then(() => {
log.debug('fetched tokens basic info', nextTokens);
dispatch(setTokens(nextTokens));
})
.catch((error) => {
console.warn('tokens::fetchTokens', error);
});
};
}
export function fetchTokens (_tokenIndexes) {
const tokenIndexes = uniq(_tokenIndexes || []);
const tokenChunks = chunk(tokenIndexes, 64);
return (dispatch, getState) => {
const { api, images } = getState();
const { tokenReg } = Contracts.get();
return tokenReg.getInstance()
.then((tokenRegInstance) => {
const promises = tokenIndexes.map((id) => fetchTokenInfo(api, tokenRegInstance, id));
return tokenReg.getContract()
.then((tokenRegContract) => {
let promise = Promise.resolve();
return Promise.all(promises);
tokenChunks.forEach((tokenChunk) => {
promise = promise
.then(() => fetchTokensData(tokenRegContract, tokenChunk)(dispatch, getState));
});
return promise;
})
.then((results) => {
const tokens = results
.then(() => {
log.debug('fetched token', getState().tokens);
})
.catch((error) => {
console.warn('tokens::fetchTokens', error);
});
};
}
/**
* Split the given token indexes between those for whom
* we already have some info, and thus just need to fetch
* the image, and those for whom we don't have anything and
* need to fetch all the info.
*/
function fetchTokensData (tokenRegContract, tokenIndexes) {
return (dispatch, getState) => {
const { api, tokens, images } = getState();
const allTokens = Object.values(tokens);
const tokensIndexesMap = allTokens
.reduce((map, token) => {
map[token.index] = token;
return map;
}, {});
const fetchedTokenIndexes = allTokens
.filter((token) => token.fetched)
.map((token) => token.index);
const fullIndexes = [];
const partialIndexes = [];
tokenIndexes.forEach((tokenIndex) => {
if (fetchedTokenIndexes.includes(tokenIndex)) {
partialIndexes.push(tokenIndex);
} else {
fullIndexes.push(tokenIndex);
}
});
log.debug('need to fully fetch', fullIndexes);
log.debug('need to partially fetch', partialIndexes);
const fullPromise = fetchTokensInfo(api, tokenRegContract, fullIndexes);
const partialPromise = fetchTokensImages(api, tokenRegContract, partialIndexes)
.then((imagesResult) => {
return imagesResult.map((image, index) => {
const tokenIndex = partialIndexes[index];
const token = tokensIndexesMap[tokenIndex];
return { ...token, image };
});
});
return Promise.all([ fullPromise, partialPromise ])
.then(([ fullResults, partialResults ]) => {
log.debug('fetched', { fullResults, partialResults });
return [].concat(fullResults, partialResults)
.filter(({ address }) => !/0x0*$/.test(address))
.reduce((tokens, token) => {
const { id, image, address } = token;
@@ -75,14 +242,9 @@ export function fetchTokens (_tokenIndexes, options = {}) {
tokens[id] = token;
return tokens;
}, {});
log.debug('fetched token', tokens);
dispatch(setTokens(tokens));
dispatch(updateTokensFilter(null, null, options));
})
.catch((error) => {
console.warn('tokens::fetchTokens', error);
.then((tokens) => {
dispatch(setTokens(tokens));
});
};
}

View File

@@ -25,10 +25,15 @@ const initialState = {
export default handleActions({
setTokens (state, action) {
const { tokens } = action;
const nextTokens = { ...state };
return {
...state,
...tokens
};
Object.keys(tokens).forEach((tokenId) => {
nextTokens[tokenId] = {
...(nextTokens[tokenId]),
...tokens[tokenId]
};
});
return nextTokens;
}
}, initialState);

View File

@@ -22,12 +22,14 @@ import initReducers from './reducers';
import { load as loadWallet } from './providers/walletActions';
import { init as initRequests } from './providers/requestsActions';
import { setupWorker } from './providers/workerWrapper';
import { setApi } from './providers/apiActions';
import {
Balances as BalancesProvider,
Personal as PersonalProvider,
Signer as SignerProvider,
Status as StatusProvider
Status as StatusProvider,
Tokens as TokensProvider
} from './providers';
const storeCreation = window.devToolsExtension
@@ -39,14 +41,59 @@ export default function (api, browserHistory, forEmbed = false) {
const middleware = initMiddleware(api, browserHistory, forEmbed);
const store = applyMiddleware(...middleware)(storeCreation)(reducers);
BalancesProvider.instantiate(store, api);
StatusProvider.instantiate(store, api);
new PersonalProvider(store, api).start();
// Add the `api` to the Redux Store
store.dispatch({ type: 'initAll', api });
store.dispatch(setApi(api));
// Initialise the Store Providers
BalancesProvider.init(store);
PersonalProvider.init(store);
StatusProvider.init(store);
TokensProvider.init(store);
new SignerProvider(store, api).start();
store.dispatch(loadWallet(api));
store.dispatch(initRequests(api));
setupWorker(store);
const start = () => {
return Promise
.resolve()
.then(() => console.log('v1: starting Status Provider...'))
.then(() => StatusProvider.start())
.then(() => console.log('v1: started Status Provider'))
.then(() => console.log('v1: starting Personal Provider...'))
.then(() => PersonalProvider.start())
.then(() => console.log('v1: started Personal Provider'))
.then(() => console.log('v1: starting Balances Provider...'))
.then(() => BalancesProvider.start())
.then(() => console.log('v1: started Balances Provider'))
.then(() => console.log('v1: starting Tokens Provider...'))
.then(() => TokensProvider.start())
.then(() => console.log('v1: started Tokens Provider'));
};
const stop = () => {
return StatusProvider
.stop()
.then(() => PersonalProvider.stop())
.then(() => TokensProvider.stop())
.then(() => BalancesProvider.stop());
};
// On connected, start the subscriptions
api.on('connected', start);
// On disconnected, stop all subscriptions
api.on('disconnected', stop);
if (api.isConnected) {
start();
}
return store;
}

View File

@@ -17,12 +17,12 @@
import Push from 'push.js';
import BigNumber from 'bignumber.js';
import unkownIcon from '~/../assets/images/contracts/unknown-64x64.png';
import unknownIcon from '~/../assets/images/contracts/unknown-64x64.png';
export function notifyTransaction (account, token, _value, onClick) {
const name = account.name || account.address;
const value = _value.div(new BigNumber(token.format || 1));
const icon = token.image || unkownIcon;
const icon = token.image || unknownIcon;
let _notification = null;

View File

@@ -1,133 +0,0 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { range } from 'lodash';
import BigNumber from 'bignumber.js';
import { hashToImageUrl } from '~/redux/util';
import { sha3 } from '~/api/util/sha3';
import imagesEthereum from '~/../assets/images/contracts/ethereum-black-64x64.png';
const BALANCEOF_SIGNATURE = sha3('balanceOf(address)');
const ADDRESS_PADDING = range(24).map(() => '0').join('');
export const ETH_TOKEN = {
address: '',
format: new BigNumber(10).pow(18),
id: sha3('eth_native_token').slice(0, 10),
image: imagesEthereum,
name: 'Ethereum',
native: true,
tag: 'ETH'
};
export function fetchTokenIds (tokenregInstance) {
return tokenregInstance.tokenCount
.call()
.then((numTokens) => {
const tokenIndexes = range(numTokens.toNumber());
return tokenIndexes;
});
}
export function fetchTokenInfo (api, tokenregInstace, tokenIndex) {
return Promise
.all([
tokenregInstace.token.call({}, [tokenIndex]),
tokenregInstace.meta.call({}, [tokenIndex, 'IMG'])
])
.then(([ tokenData, image ]) => {
const [ address, tag, format, name ] = tokenData;
const token = {
format: format.toString(),
index: tokenIndex,
image: hashToImageUrl(image),
id: sha3(address + tokenIndex).slice(0, 10),
address,
name,
tag
};
return token;
});
}
/**
* `updates` should be in the shape:
* {
* [ who ]: [ tokenId ] // Array of tokens to updates
* }
*
* Returns a Promise resolved witht the balances in the shape:
* {
* [ who ]: { [ tokenId ]: BigNumber } // The balances of `who`
* }
*/
export function fetchAccountsBalances (api, tokens, updates) {
const addresses = Object.keys(updates);
const promises = addresses
.map((who) => {
const tokensIds = updates[who];
const tokensToUpdate = tokensIds.map((tokenId) => tokens.find((t) => t.id === tokenId));
return fetchAccountBalances(api, tokensToUpdate, who);
});
return Promise.all(promises)
.then((results) => {
return results.reduce((balances, accountBalances, index) => {
balances[addresses[index]] = accountBalances;
return balances;
}, {});
});
}
/**
* Returns a Promise resolved with the balances in the shape:
* {
* [ tokenId ]: BigNumber // Token balance value
* }
*/
export function fetchAccountBalances (api, tokens, who) {
const calldata = '0x' + BALANCEOF_SIGNATURE.slice(2, 10) + ADDRESS_PADDING + who.slice(2);
const promises = tokens.map((token) => fetchTokenBalance(api, token, { who, calldata }));
return Promise.all(promises)
.then((results) => {
return results.reduce((balances, value, index) => {
const token = tokens[index];
balances[token.id] = value;
return balances;
}, {});
});
}
export function fetchTokenBalance (api, token, { who, calldata }) {
if (token.native) {
return api.eth.getBalance(who);
}
return api.eth
.call({ data: calldata, to: token.address })
.then((result) => {
const cleanResult = result.replace(/^0x/, '');
return new BigNumber(`0x${cleanResult || 0}`);
});
}

View File

@@ -0,0 +1,23 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
// build from : https://raw.githubusercontent.com/paritytech/contracts/4c8501e908166aab7ff4d2ebb05db61b5d017024/TokenCalls.sol
// metadata (include build version and options):
// {"compiler":{"version":"0.4.16+commit.d7661dd9"},"language":"Solidity","output":{"abi":[{"inputs":[{"name":"tokenRegAddress","type":"address"},{"name":"start","type":"uint256"},{"name":"limit","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}],"devdoc":{"methods":{}},"userdoc":{"methods":{}}},"settings":{"compilationTarget":{"":"Tokens"},"libraries":{},"optimizer":{"enabled":true,"runs":200},"remappings":[]},"sources":{"":{"keccak256":"0x4790e490f418d1a5884c27ffe9684914dab2d55bd1d23b99cff7aa2ca289e2d3","urls":["bzzr://bb200beae6849f1f5bb97b36c57cd493be52877ec0b55ee9969fa5f8159cf37b"]}},"version":1}
// {"compiler":{"version":"0.4.16+commit.d7661dd9"},"language":"Solidity","output":{"abi":[{"inputs":[{"name":"who","type":"address[]"},{"name":"tokens","type":"address[]"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}],"devdoc":{"methods":{}},"userdoc":{"methods":{}}},"settings":{"compilationTarget":{"":"TokensBalances"},"libraries":{},"optimizer":{"enabled":true,"runs":200},"remappings":[]},"sources":{"":{"keccak256":"0x4790e490f418d1a5884c27ffe9684914dab2d55bd1d23b99cff7aa2ca289e2d3","urls":["bzzr://bb200beae6849f1f5bb97b36c57cd493be52877ec0b55ee9969fa5f8159cf37b"]}},"version":1}
export const tokenAddresses = '0x6060604052341561000f57600080fd5b6040516060806102528339810160405280805191906020018051919060200180519150505b6000806000806100426101fc565b600088955085600160a060020a0316639f181b5e6000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b15156100a657600080fd5b6102c65a03f115156100b757600080fd5b50505060405180519550508688018890116100ce57fe5b8785116100de57600093506100f6565b8487890111156100f25787850393506100f6565b8693505b5b83602002602001925060405191508282016040528382528790505b8388018110156101ea5785600160a060020a031663044215c682600060405160a001526040517c010000000000000000000000000000000000000000000000000000000063ffffffff8416028152600481019190915260240160a060405180830381600087803b151561018457600080fd5b6102c65a03f1151561019557600080fd5b50505060405180519060200180519060200180519060200180519060200180515086935050508a84039050815181106101ca57fe5b600160a060020a039092166020928302909101909101525b600101610112565b8282f35b50505050505050505061020e565b60206040519081016040526000815290565b60368061021c6000396000f30060606040525b600080fd00a165627a7a72305820a9a09f013393cf3c6398ce0f8175073fe363b6f594f9bd569261d0bb94aa84d40029';
export const tokensBalances = '0x6060604052341561000f57600080fd5b60405161018b38038061018b8339810160405280805182019190602001805190910190505b6000806000610041610135565b60008060008060008060008c518c51029a506020808c020199507f70a0823100000000000000000000000000000000000000000000000000000000985060405197508988016040528a8852604051965060248701604052888752879550866004019450600093505b8c5184101561011f57600092505b8b51831015610113578c84815181106100cc57fe5b9060200190602002015191508b83815181106100e457fe5b90602001906020020151905060208601955081855260208660248960008561fffff1505b6001909201916100b7565b5b6001909301926100a9565b8988f35b50505050505050505050505050610147565b60206040519081016040526000815290565b6036806101556000396000f30060606040525b600080fd00a165627a7a723058203cfc17c394936aa87b7db79e4f082a7cfdcefef54acd3124d17525b56c92e7950029';

View File

@@ -0,0 +1,299 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { range } from 'lodash';
import BigNumber from 'bignumber.js';
import { hashToImageUrl } from '~/redux/util';
import { sha3 } from '~/api/util/sha3';
import imagesEthereum from '~/../assets/images/contracts/ethereum-black-64x64.png';
import {
tokenAddresses as tokenAddressesBytcode,
tokensBalances as tokensBalancesBytecode
} from './bytecodes';
export const ETH_TOKEN = {
address: '',
format: new BigNumber(10).pow(18),
id: getTokenId('eth_native_token'),
image: imagesEthereum,
name: 'Ethereum',
native: true,
tag: 'ETH'
};
export function fetchTokenIds (tokenregInstance) {
return tokenregInstance.tokenCount
.call()
.then((numTokens) => {
const tokenIndexes = range(numTokens.toNumber());
return tokenIndexes;
});
}
export function fetchTokensBasics (api, tokenReg, start = 0, limit = 100) {
const tokenAddressesCallData = encode(
api,
[ 'address', 'uint', 'uint' ],
[ tokenReg.address, start, limit ]
);
return api.eth
.call({ data: tokenAddressesBytcode + tokenAddressesCallData })
.then((result) => {
return decodeArray(api, 'address[]', result);
})
.then((tokenAddresses) => {
return tokenAddresses.map((tokenAddress, index) => {
if (/^0x0*$/.test(tokenAddress)) {
return null;
}
const tokenIndex = start + index;
return {
address: tokenAddress,
id: getTokenId(tokenAddress, tokenIndex),
index: tokenIndex,
fetched: false
};
});
})
.then((tokens) => tokens.filter((token) => token))
.then((tokens) => {
const randomAddress = sha3(`${Date.now()}`).substr(0, 42);
return fetchTokensBalances(api, tokens, [randomAddress])
.then((_balances) => {
const balances = _balances[randomAddress];
return tokens.filter(({ id }) => balances[id].eq(0));
});
});
}
export function fetchTokensInfo (api, tokenReg, tokenIndexes) {
const requests = tokenIndexes.map((tokenIndex) => {
const tokenCalldata = tokenReg.getCallData(tokenReg.instance.token, {}, [tokenIndex]);
return { to: tokenReg.address, data: tokenCalldata };
});
const calls = requests.map((req) => api.eth.call(req));
const imagesPromise = fetchTokensImages(api, tokenReg, tokenIndexes);
return Promise.all(calls)
.then((results) => {
return imagesPromise.then((images) => [ results, images ]);
})
.then(([ results, images ]) => {
return results.map((rawTokenData, index) => {
const tokenIndex = tokenIndexes[index];
const tokenData = tokenReg.instance.token
.decodeOutput(rawTokenData)
.map((t) => t.value);
const [ address, tag, format, name ] = tokenData;
const image = images[index];
const token = {
address,
id: getTokenId(address, tokenIndex),
index: tokenIndex,
format: format.toString(),
image,
name,
tag,
fetched: true
};
return token;
});
});
}
export function fetchTokensImages (api, tokenReg, tokenIndexes) {
const requests = tokenIndexes.map((tokenIndex) => {
const metaCalldata = tokenReg.getCallData(tokenReg.instance.meta, {}, [tokenIndex, 'IMG']);
return { to: tokenReg.address, data: metaCalldata };
});
const calls = requests.map((req) => api.eth.call(req));
return Promise.all(calls)
.then((results) => {
return results.map((rawImage) => {
const image = tokenReg.instance.meta.decodeOutput(rawImage)[0].value;
return hashToImageUrl(image);
});
});
}
/**
* `updates` should be in the shape:
* {
* [ who ]: [ tokenId ] // Array of tokens to updates
* }
*
* Returns a Promise resolved with the balances in the shape:
* {
* [ who ]: { [ tokenId ]: BigNumber } // The balances of `who`
* }
*/
export function fetchAccountsBalances (api, tokens, updates) {
const accountAddresses = Object.keys(updates);
// Updates for the ETH balances
const ethUpdates = accountAddresses
.filter((accountAddress) => {
return updates[accountAddress].find((tokenId) => tokenId === ETH_TOKEN.id);
})
.reduce((nextUpdates, accountAddress) => {
nextUpdates[accountAddress] = [ETH_TOKEN.id];
return nextUpdates;
}, {});
// Updates for Tokens balances
const tokenUpdates = Object.keys(updates)
.reduce((nextUpdates, accountAddress) => {
const tokenIds = updates[accountAddress].filter((tokenId) => tokenId !== ETH_TOKEN.id);
if (tokenIds.length > 0) {
nextUpdates[accountAddress] = tokenIds;
}
return nextUpdates;
}, {});
let ethBalances = {};
let tokensBalances = {};
const ethPromise = fetchEthBalances(api, Object.keys(ethUpdates))
.then((_ethBalances) => {
ethBalances = _ethBalances;
});
const tokenPromise = Object.keys(tokenUpdates)
.reduce((tokenPromise, accountAddress) => {
const tokenIds = tokenUpdates[accountAddress];
const updateTokens = tokens
.filter((t) => tokenIds.includes(t.id));
return tokenPromise
.then(() => fetchTokensBalances(api, updateTokens, [ accountAddress ]))
.then((balances) => {
tokensBalances[accountAddress] = balances[accountAddress];
});
}, Promise.resolve());
return Promise.all([ ethPromise, tokenPromise ])
.then(() => {
const balances = Object.assign({}, tokensBalances);
Object.keys(ethBalances).forEach((accountAddress) => {
if (!balances[accountAddress]) {
balances[accountAddress] = {};
}
balances[accountAddress] = Object.assign(
{},
balances[accountAddress],
ethBalances[accountAddress]
);
});
return balances;
});
}
function fetchEthBalances (api, accountAddresses) {
const promises = accountAddresses
.map((accountAddress) => api.eth.getBalance(accountAddress));
return Promise.all(promises)
.then((balancesArray) => {
return balancesArray.reduce((balances, balance, index) => {
balances[accountAddresses[index]] = {
[ETH_TOKEN.id]: balance
};
return balances;
}, {});
});
}
function fetchTokensBalances (api, tokens, accountAddresses) {
const tokenAddresses = tokens.map((t) => t.address);
const tokensBalancesCallData = encode(
api,
[ 'address[]', 'address[]' ],
[ accountAddresses, tokenAddresses ]
);
return api.eth
.call({ data: tokensBalancesBytecode + tokensBalancesCallData })
.then((result) => {
const rawBalances = decodeArray(api, 'uint[]', result);
const balances = {};
accountAddresses.forEach((accountAddress, accountIndex) => {
const balance = {};
const preIndex = accountIndex * tokenAddresses.length;
tokenAddresses.forEach((tokenAddress, tokenIndex) => {
const index = preIndex + tokenIndex;
const token = tokens[tokenIndex];
balance[token.id] = rawBalances[index];
});
balances[accountAddress] = balance;
});
return balances;
});
}
function getTokenId (...args) {
return sha3(args.join('')).slice(0, 10);
}
function encode (api, types, values) {
return api.util.abiEncode(
null,
types,
values
).replace('0x', '');
}
function decodeArray (api, type, data) {
return api.util
.abiDecode(
[type],
[
'0x',
(32).toString(16).padStart(64, 0),
data.replace('0x', '')
].join('')
)[0]
.map((t) => t.value);
}

View File

@@ -81,12 +81,11 @@ export function getTxOptions (api, func, _options, values = []) {
options.to = options.to || func.contract.address;
}
if (!address) {
return Promise.resolve({ func, options, values });
}
const promise = (!address)
? Promise.resolve(false)
: WalletsUtils.isWallet(api, address);
return WalletsUtils
.isWallet(api, address)
return promise
.then((isWallet) => {
if (!isWallet) {
return { func, options, values };

View File

@@ -78,13 +78,39 @@ export default class WalletsUtils {
.delegateCall(api, walletContract.address, 'fetchTransactions', [ walletContract ])
.then((transactions) => {
return transactions.sort((txA, txB) => {
const comp = txB.blockNumber.comparedTo(txA.blockNumber);
const bnA = txA.blockNumber;
const bnB = txB.blockNumber;
if (!bnA) {
console.warn('could not find block number in transaction', txA);
return 1;
}
if (!bnB) {
console.warn('could not find block number in transaction', txB);
return -1;
}
const comp = bnA.comparedTo(bnB);
if (comp !== 0) {
return comp;
}
return txB.transactionIndex.comparedTo(txA.transactionIndex);
const txIdxA = txA.transactionIndex;
const txIdxB = txB.transactionIndex;
if (!txIdxA) {
console.warn('could not find transaction index in transaction', txA);
return 1;
}
if (!txIdxB) {
console.warn('could not find transaction index in transaction', txB);
return -1;
}
return txIdxA.comparedTo(txIdxB);
});
});
}

View File

@@ -212,6 +212,7 @@ export default class ConsensysWalletUtils {
const transaction = {
transactionHash: log.transactionHash,
transactionIndex: log.transactionIndex,
blockNumber: log.blockNumber
};

View File

@@ -130,27 +130,67 @@ export default class FoundationWalletUtils {
.ConfirmationNeeded
.getAllLogs()
.then((logs) => {
return logs.map((log) => ({
initiator: log.params.initiator.value,
to: log.params.to.value,
data: log.params.data.value,
value: log.params.value.value,
operation: bytesToHex(log.params.operation.value),
transactionIndex: log.transactionIndex,
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
confirmedBy: []
}));
return logs
.filter((log) => {
if (!log.blockNumber) {
console.warn('got a log without blockNumber', log);
return false;
}
if (!log.transactionIndex) {
console.warn('got a log without transactionIndex', log);
return false;
}
return true;
})
.map((log) => ({
initiator: log.params.initiator.value,
to: log.params.to.value,
data: log.params.data.value,
value: log.params.value.value,
operation: bytesToHex(log.params.operation.value),
transactionIndex: log.transactionIndex,
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
confirmedBy: []
}));
})
.then((logs) => {
return logs.sort((logA, logB) => {
const comp = logA.blockNumber.comparedTo(logB.blockNumber);
const bnA = logA.blockNumber;
const bnB = logA.blockNumber;
if (!bnA) {
console.warn('could not find block number in log', logA);
return 1;
}
if (!bnB) {
console.warn('could not find block number in log', logB);
return -1;
}
const comp = bnA.comparedTo(bnB);
if (comp !== 0) {
return comp;
}
return logA.transactionIndex.comparedTo(logB.transactionIndex);
const txIdxA = logA.transactionIndex;
const txIdxB = logB.transactionIndex;
if (!txIdxA) {
console.warn('could not find transaction index in log', logA);
return 1;
}
if (!txIdxB) {
console.warn('could not find transaction index in log', logB);
return -1;
}
return txIdxA.comparedTo(txIdxB);
});
})
.then((pendingTxs) => {
@@ -205,40 +245,48 @@ export default class FoundationWalletUtils {
] ]
})
.then((logs) => {
const transactions = logs.map((log) => {
const signature = toHex(log.topics[0]);
const transactions = logs
.map((log) => {
const signature = toHex(log.topics[0]);
const value = log.params.value.value;
const from = signature === WalletSignatures.Deposit
? log.params['_from'].value
: walletContract.address;
const value = log.params.value.value;
const from = signature === WalletSignatures.Deposit
? log.params['_from'].value
: walletContract.address;
const to = signature === WalletSignatures.Deposit
? walletContract.address
: log.params.to.value;
const to = signature === WalletSignatures.Deposit
? walletContract.address
: log.params.to.value;
const transaction = {
transactionHash: log.transactionHash,
blockNumber: log.blockNumber,
from, to, value
};
const transaction = {
transactionHash: log.transactionHash,
transactionIndex: log.transactionIndex,
blockNumber: log.blockNumber,
from, to, value
};
if (log.params.created && log.params.created.value && !/^(0x)?0*$/.test(log.params.created.value)) {
transaction.creates = log.params.created.value;
delete transaction.to;
}
if (!transaction.blockNumber) {
console.warn('log without block number', log);
return null;
}
if (log.params.operation) {
transaction.operation = bytesToHex(log.params.operation.value);
checkPendingOperation(api, log, transaction.operation);
}
if (log.params.created && log.params.created.value && !/^(0x)?0*$/.test(log.params.created.value)) {
transaction.creates = log.params.created.value;
delete transaction.to;
}
if (log.params.data) {
transaction.data = log.params.data.value;
}
if (log.params.operation) {
transaction.operation = bytesToHex(log.params.operation.value);
checkPendingOperation(api, log, transaction.operation);
}
return transaction;
});
if (log.params.data) {
transaction.data = log.params.data.value;
}
return transaction;
})
.filter((tx) => tx);
return transactions;
});

View File

@@ -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 () {
@@ -370,14 +365,14 @@ class Account extends Component {
onDeny={ this.exportClose }
title={
<FormattedMessage
id='export.account.title'
id='account.export.title'
defaultMessage='Export Account'
/>
}
>
<div className={ styles.textbox }>
<FormattedMessage
id='export.account.info'
id='account.export.info'
defaultMessage='Export your account as a JSON file. Please enter the password linked with this account.'
/>
</div>
@@ -388,13 +383,13 @@ class Account extends Component {
type='password'
hint={
<FormattedMessage
id='export.account.password.hint'
id='account.export.password.hint'
defaultMessage='The password specified when creating this account'
/>
}
label={
<FormattedMessage
id='export.account.password.label'
id='account.export.password.label'
defaultMessage='Account password'
/>
}
@@ -524,8 +519,6 @@ function mapStateToProps (state, props) {
function mapDispatchToProps (dispatch) {
return bindActionCreators({
fetchCertifiers,
fetchCertifications,
newError,
setVisibleAccounts
}, dispatch);

View File

@@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
import { shallow } from 'enzyme';
import React from 'react';
@@ -34,7 +35,15 @@ function render (props) {
/>,
{
context: {
store: createRedux()
store: createRedux(),
api: {
transport: {
on: sinon.stub()
},
pubsub: {
subscribeAndGetResult: sinon.stub()
}
}
}
}
).find('Account').shallow();

View File

@@ -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);

View File

@@ -27,7 +27,11 @@ let instance;
let redux;
function createApi () {
api = {};
api = {
pubsub: {
subscribeAndGetResult: sinon.stub().returns(Promise.reject(new Error('uninitialized')))
}
};
return api;
}

View File

@@ -16,8 +16,7 @@
import React, { Component, PropTypes } from 'react';
import { FirstRun, UpgradeParity } from '~/modals';
import { Errors, ParityBackground, Tooltips } from '~/ui';
import { Errors, ParityBackground } from '~/ui';
import styles from '../application.css';
@@ -25,24 +24,17 @@ export default class Container extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
onCloseFirstRun: PropTypes.func,
showFirstRun: PropTypes.bool,
upgradeStore: PropTypes.object.isRequired
showFirstRun: PropTypes.bool
};
render () {
const { children, onCloseFirstRun, showFirstRun, upgradeStore } = this.props;
const { children } = this.props;
return (
<ParityBackground
attachDocument
className={ styles.container }
>
<FirstRun
onClose={ onCloseFirstRun }
visible={ showFirstRun }
/>
<Tooltips />
<UpgradeParity store={ upgradeStore } />
<Errors />
{ children }
</ParityBackground>

View File

@@ -15,13 +15,12 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar';
import { isEqual } from 'lodash';
import { Tooltip, StatusIndicator } from '~/ui';
import { StatusIndicator } from '~/ui';
import Tab from './Tab';
import styles from './tabBar.css';
@@ -66,15 +65,6 @@ class TabBar extends Component {
</div>
</Link>
{ this.renderTabItems() }
<Tooltip
className={ styles.tabbarTooltip }
text={
<FormattedMessage
id='tabBar.tooltip.overview'
defaultMessage='navigate between the different parts and views of the application, switching between an account view, token view and decentralized application view'
/>
}
/>
</div>
<ToolbarGroup className={ styles.last }>
<div />

View File

@@ -18,27 +18,14 @@ import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import UpgradeStore from '~/modals/UpgradeParity/store';
import Connection from '../Connection';
import ParityBar from '../ParityBar';
import SyncWarning, { showSyncWarning } from '../SyncWarning';
import Snackbar from './Snackbar';
import Container from './Container';
import DappContainer from './DappContainer';
import Extension from './Extension';
import FrameError from './FrameError';
import Status from './Status';
import Store from './store';
import TabBar from './TabBar';
import Requests from './Requests';
import styles from './application.css';
const inFrame = window.parent !== window && window.parent.frames.length !== 0;
const doShowSyncWarning = showSyncWarning();
@observer
class Application extends Component {
static contextTypes = {
@@ -53,11 +40,9 @@ class Application extends Component {
}
store = new Store(this.context.api);
upgradeStore = UpgradeStore.get(this.context.api);
render () {
const [root] = (window.location.hash || '').replace('#/', '').split('/');
const isMinimized = root === 'app' || root === 'web';
if (process.env.NODE_ENV !== 'production' && root === 'playground') {
return (
@@ -69,47 +54,20 @@ class Application extends Component {
return (
<div>
{
inFrame
? <FrameError />
: null
}
{
isMinimized
? this.renderMinimized()
: this.renderApp()
}
{
doShowSyncWarning
? (<SyncWarning />)
: null
}
<Connection />
<Requests />
<ParityBar dapp={ isMinimized } />
{ this.renderApp() }
</div>
);
}
renderApp () {
const { blockNumber, children, pending } = this.props;
const { children, pending } = this.props;
return (
<Container
upgradeStore={ this.upgradeStore }
onCloseFirstRun={ this.store.closeFirstrun }
showFirstRun={ this.store.firstrunVisible }
>
<Container>
<TabBar pending={ pending } />
<div className={ styles.content }>
{ children }
</div>
{
blockNumber
? <Status upgradeStore={ this.upgradeStore } />
: null
}
<Extension />
<Snackbar />
</Container>
);

View File

@@ -17,7 +17,7 @@
import React from 'react';
import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg';
import { AccountsIcon, AddressesIcon, AppsIcon, ContactsIcon, FingerprintIcon, SettingsIcon } from '~/ui/Icons';
import { AccountsIcon, AddressesIcon, ContactsIcon, FingerprintIcon, SettingsIcon } from '~/ui/Icons';
import styles from './views.css';
@@ -50,13 +50,6 @@ const defaultViews = {
value: 'address'
},
apps: {
active: true,
icon: <AppsIcon />,
route: '/apps',
value: 'app'
},
contracts: {
active: false,
onlyPersonal: true,

View File

@@ -91,17 +91,6 @@ class Views extends Component {
/>
)
}
{
this.renderView('apps',
<FormattedMessage
id='settings.views.apps.label'
/>,
<FormattedMessage
id='settings.views.apps.description'
defaultMessage='Decentralized applications that interact with the underlying network. Add applications, manage you application portfolio and interact with application from around the network.'
/>
)
}
{
this.renderView('contracts',
<FormattedMessage