* 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
335 lines
10 KiB
JavaScript
335 lines
10 KiB
JavaScript
// 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 { difference, uniq } from 'lodash';
|
|
import { push } from 'react-router-redux';
|
|
|
|
import { notifyTransaction } from '~/util/notifications';
|
|
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 = {
|
|
tokenAddresses: [],
|
|
addresses: []
|
|
};
|
|
|
|
function _setBalances (balances) {
|
|
return {
|
|
type: 'setBalances',
|
|
balances
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {Object} _balances - In the shape:
|
|
* {
|
|
* [ who ]: { [ tokenId ]: BigNumber } // The balances of `who`
|
|
* }
|
|
* @param {Boolean} skipNotifications [description]
|
|
*/
|
|
function setBalances (updates, skipNotifications = false) {
|
|
return (dispatch, getState) => {
|
|
const { tokens, balances } = getState();
|
|
|
|
const prevBalances = balances;
|
|
const nextBalances = { ...prevBalances };
|
|
|
|
Object.keys(updates)
|
|
.forEach((who) => {
|
|
const accountUpdates = updates[who];
|
|
|
|
Object.keys(accountUpdates)
|
|
.forEach((tokenId) => {
|
|
const token = tokens[tokenId];
|
|
const prevTokenValue = (prevBalances[who] || {})[tokenId];
|
|
const nextTokenValue = accountUpdates[tokenId];
|
|
|
|
if (prevTokenValue && prevTokenValue.lt(nextTokenValue)) {
|
|
dispatch(notifyBalanceChange(who, prevTokenValue, nextTokenValue, token));
|
|
}
|
|
|
|
nextBalances[who] = {
|
|
...(nextBalances[who] || {}),
|
|
[tokenId]: nextTokenValue
|
|
};
|
|
});
|
|
});
|
|
|
|
return dispatch(_setBalances(nextBalances));
|
|
};
|
|
}
|
|
|
|
function notifyBalanceChange (who, fromValue, toValue, token) {
|
|
return (dispatch, getState) => {
|
|
const account = getState().personal.accounts[who];
|
|
|
|
if (account) {
|
|
const txValue = toValue.minus(fromValue);
|
|
|
|
const redirectToAccount = () => {
|
|
const basePath = account.wallet
|
|
? 'wallet'
|
|
: 'accounts';
|
|
|
|
const route = `/${basePath}/${account.address}`;
|
|
|
|
dispatch(push(route));
|
|
};
|
|
|
|
notifyTransaction(account, token, txValue, redirectToAccount);
|
|
}
|
|
};
|
|
}
|
|
|
|
// TODO: fetch txCount when needed
|
|
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 (options = {}) {
|
|
return (dispatch, getState) => {
|
|
const { api, personal, tokens } = getState();
|
|
const { visibleAccounts, accounts } = personal;
|
|
|
|
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 tokenAddresses = tokensToUpdate
|
|
.map((t) => t.address)
|
|
.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) {
|
|
// If no new addresses and the same tokens, don't change the filter
|
|
if (newTokenAddresses.length === 0 && newAddresses.length === 0) {
|
|
log.debug('no need to update token filter', addresses, tokenAddresses, tokensFilter);
|
|
return;
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
if (tokensFilter.filterToId) {
|
|
promises.push(api.eth.uninstallFilter(tokensFilter.filterToId));
|
|
}
|
|
|
|
return Promise.all(promises)
|
|
.then(([ filterFromId, filterToId ]) => {
|
|
const nextTokensFilter = {
|
|
filterFromId, filterToId,
|
|
addresses, tokenAddresses
|
|
};
|
|
|
|
tokensFilter = nextTokensFilter;
|
|
})
|
|
.then(() => fetchTokensBalances(updates)(dispatch, getState))
|
|
.catch((error) => {
|
|
console.warn('balances::updateTokensFilter', error);
|
|
});
|
|
};
|
|
}
|
|
|
|
export function queryTokensFilter () {
|
|
return (dispatch, getState) => {
|
|
const { api } = getState();
|
|
|
|
Promise
|
|
.all([
|
|
api.eth.getFilterChanges(tokensFilter.filterFromId),
|
|
api.eth.getFilterChanges(tokensFilter.filterToId)
|
|
])
|
|
.then(([ logsFrom, logsTo ]) => {
|
|
const logs = [].concat(logsFrom, logsTo);
|
|
|
|
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, index) => {
|
|
const tokenAddress = log.address.toLowerCase();
|
|
const token = lcTokensMap[tokenAddress];
|
|
|
|
// logs = [ ...logsFrom, ...logsTo ]
|
|
const topicIdx = index < logsFrom.length ? 1 : 2;
|
|
const address = ('0x' + log.topics[topicIdx].slice(-40)).toLowerCase();
|
|
const addressIndex = lcAddresses.indexOf(address);
|
|
|
|
if (addressIndex > -1) {
|
|
const who = addressesToFetch[addressIndex];
|
|
|
|
updates[who] = [].concat(updates[who] || [], token.id);
|
|
}
|
|
});
|
|
|
|
// No accounts to update
|
|
if (Object.keys(updates).length === 0) {
|
|
return;
|
|
}
|
|
|
|
Object.keys(updates).forEach((who) => {
|
|
// Keep non-empty token addresses
|
|
updates[who] = uniq(updates[who]);
|
|
});
|
|
|
|
fetchTokensBalances(updates)(dispatch, getState);
|
|
});
|
|
};
|
|
}
|
|
|
|
export function fetchTokensBalances (updates, skipNotifications = false) {
|
|
return (dispatch, getState) => {
|
|
const { api, personal, tokens } = getState();
|
|
const allTokens = Object.values(tokens);
|
|
|
|
if (!updates) {
|
|
const { visibleAccounts, accounts } = personal;
|
|
const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts)));
|
|
|
|
updates = addressesToFetch.reduce((updates, who) => {
|
|
updates[who] = allTokens.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));
|
|
})
|
|
.catch((error) => {
|
|
console.warn('balances::fetchTokensBalances', error);
|
|
});
|
|
};
|
|
}
|