diff options
Diffstat (limited to 'app/scripts')
-rw-r--r-- | app/scripts/controllers/incoming-transactions.js | 222 | ||||
-rw-r--r-- | app/scripts/metamask-controller.js | 10 |
2 files changed, 232 insertions, 0 deletions
diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js new file mode 100644 index 000000000..4b4314427 --- /dev/null +++ b/app/scripts/controllers/incoming-transactions.js @@ -0,0 +1,222 @@ +const ObservableStore = require('obs-store') +const log = require('loglevel') +const BN = require('bn.js') +const createId = require('../lib/random-id') +const { bnToHex } = require('../lib/util') +const { + MAINNET_CODE, + ROPSTEN_CODE, + RINKEYBY_CODE, + KOVAN_CODE, + ROPSTEN, + RINKEBY, + KOVAN, + MAINNET, +} = require('./network/enums') +const networkTypeToIdMap = { + [ROPSTEN]: ROPSTEN_CODE, + [RINKEBY]: RINKEYBY_CODE, + [KOVAN]: KOVAN_CODE, + [MAINNET]: MAINNET_CODE, +} + +class IncomingTransactionsController { + + constructor (opts = {}) { + const { + blockTracker, + networkController, + preferencesController, + } = opts + this.blockTracker = blockTracker + this.networkController = networkController + this.preferencesController = preferencesController + this.getCurrentNetwork = () => networkController.getProviderConfig().type + + const initState = Object.assign({ + incomingTransactions: {}, + incomingTxLastFetchedBlocksByNetwork: { + [ROPSTEN]: null, + [RINKEBY]: null, + [KOVAN]: null, + [MAINNET]: null, + }, + }, opts.initState) + this.store = new ObservableStore(initState) + + this.networkController.on('networkDidChange', async (newType) => { + const address = this.preferencesController.getSelectedAddress() + await this._update({ + address, + networkType: newType, + }) + }) + this.blockTracker.on('latest', async (newBlockNumberHex) => { + const address = this.preferencesController.getSelectedAddress() + await this._update({ + address, + newBlockNumberDec: parseInt(newBlockNumberHex, 16), + }) + }) + this.preferencesController.store.subscribe(async ({ selectedAddress }) => { + await this._update({ + address: selectedAddress, + }) + }) + } + + async _update ({ address, newBlockNumberDec, networkType } = {}) { + try { + const dataForUpdate = await this._getDataForUpdate({ address, newBlockNumberDec, networkType }) + await this._updateStateWithNewTxData(dataForUpdate) + } catch (err) { + log.error(err) + } + } + + async _getDataForUpdate ({ address, newBlockNumberDec, networkType } = {}) { + const { + incomingTransactions: currentIncomingTxs, + incomingTxLastFetchedBlocksByNetwork: currentBlocksByNetwork, + } = this.store.getState() + + const network = networkType || this.getCurrentNetwork() + const lastFetchBlockByCurrentNetwork = currentBlocksByNetwork[network] + let blockToFetchFrom = lastFetchBlockByCurrentNetwork || newBlockNumberDec + if (blockToFetchFrom === undefined) { + blockToFetchFrom = parseInt(this.blockTracker.getCurrentBlock(), 16) + } + + const { latestIncomingTxBlockNumber, txs: newTxs } = await this._fetchAll(address, blockToFetchFrom, network) + + return { + latestIncomingTxBlockNumber, + newTxs, + currentIncomingTxs, + currentBlocksByNetwork, + fetchedBlockNumber: blockToFetchFrom, + network, + } + } + + async _updateStateWithNewTxData ({ + latestIncomingTxBlockNumber, + newTxs, + currentIncomingTxs, + currentBlocksByNetwork, + fetchedBlockNumber, + network, + }) { + const newLatestBlockHashByNetwork = latestIncomingTxBlockNumber + ? parseInt(latestIncomingTxBlockNumber, 10) + 1 + : fetchedBlockNumber + 1 + const newIncomingTransactions = { + ...currentIncomingTxs, + } + newTxs.forEach(tx => { newIncomingTransactions[tx.hash] = tx }) + + this.store.updateState({ + incomingTxLastFetchedBlocksByNetwork: { + ...currentBlocksByNetwork, + [network]: newLatestBlockHashByNetwork, + }, + incomingTransactions: newIncomingTransactions, + }) + } + + async _fetchAll (address, fromBlock, networkType) { + try { + const fetchedTxResponse = await this._fetchTxs(address, fromBlock, networkType) + return this._processTxFetchResponse(fetchedTxResponse) + } catch (err) { + log.error(err) + } + } + + async _fetchTxs (address, fromBlock, networkType) { + let etherscanSubdomain = 'api' + const currentNetworkID = networkTypeToIdMap[networkType] + const supportedNetworkTypes = [ROPSTEN, RINKEBY, KOVAN, MAINNET] + + if (supportedNetworkTypes.indexOf(networkType) === -1) { + return {} + } + + if (networkType !== MAINNET) { + etherscanSubdomain = `api-${networkType}` + } + const apiUrl = `https://${etherscanSubdomain}.etherscan.io` + let url = `${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1` + + if (fromBlock) { + url += `&startBlock=${parseInt(fromBlock, 10)}` + } + const response = await fetch(url) + const parsedResponse = await response.json() + + return { + ...parsedResponse, + address, + currentNetworkID, + } + } + + _processTxFetchResponse ({ status, result, address, currentNetworkID }) { + if (status !== '0' && result.length > 0) { + const remoteTxList = {} + const remoteTxs = [] + result.forEach((tx) => { + if (!remoteTxList[tx.hash]) { + remoteTxs.push(this._normalizeTxFromEtherscan(tx, currentNetworkID)) + remoteTxList[tx.hash] = 1 + } + }) + + const incomingTxs = remoteTxs.filter(tx => tx.txParams.to && tx.txParams.to.toLowerCase() === address.toLowerCase()) + incomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1)) + + let latestIncomingTxBlockNumber = null + incomingTxs.forEach((tx) => { + if ( + tx.blockNumber && + (!latestIncomingTxBlockNumber || + parseInt(latestIncomingTxBlockNumber, 10) < parseInt(tx.blockNumber, 10)) + ) { + latestIncomingTxBlockNumber = tx.blockNumber + } + }) + return { + latestIncomingTxBlockNumber, + txs: incomingTxs, + } + } + return { + latestIncomingTxBlockNumber: null, + txs: [], + } + } + + _normalizeTxFromEtherscan (txMeta, currentNetworkID) { + const time = parseInt(txMeta.timeStamp, 10) * 1000 + const status = txMeta.isError === '0' ? 'confirmed' : 'failed' + return { + blockNumber: txMeta.blockNumber, + id: createId(), + metamaskNetworkId: currentNetworkID, + status, + time, + txParams: { + from: txMeta.from, + gas: bnToHex(new BN(txMeta.gas)), + gasPrice: bnToHex(new BN(txMeta.gasPrice)), + nonce: bnToHex(new BN(txMeta.nonce)), + to: txMeta.to, + value: bnToHex(new BN(txMeta.value)), + }, + hash: txMeta.hash, + transactionCategory: 'incoming', + } + } +} + +module.exports = IncomingTransactionsController diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index e9299d5f8..14fa143f4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -30,6 +30,7 @@ const InfuraController = require('./controllers/infura') const CachedBalancesController = require('./controllers/cached-balances') const OnboardingController = require('./controllers/onboarding') const RecentBlocksController = require('./controllers/recent-blocks') +const IncomingTransactionsController = require('./controllers/incoming-transactions') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') const TypedMessageManager = require('./lib/typed-message-manager') @@ -137,6 +138,13 @@ module.exports = class MetamaskController extends EventEmitter { networkController: this.networkController, }) + this.incomingTransactionsController = new IncomingTransactionsController({ + blockTracker: this.blockTracker, + networkController: this.networkController, + preferencesController: this.preferencesController, + initState: initState.IncomingTransactionsController, + }) + // account tracker watches balances, nonces, and any code at their address. this.accountTracker = new AccountTracker({ provider: this.provider, @@ -270,6 +278,7 @@ module.exports = class MetamaskController extends EventEmitter { CachedBalancesController: this.cachedBalancesController.store, OnboardingController: this.onboardingController.store, ProviderApprovalController: this.providerApprovalController.store, + IncomingTransactionsController: this.incomingTransactionsController.store, }) this.memStore = new ComposableObservableStore(null, { @@ -294,6 +303,7 @@ module.exports = class MetamaskController extends EventEmitter { // ProviderApprovalController ProviderApprovalController: this.providerApprovalController.store, ProviderApprovalControllerMemStore: this.providerApprovalController.memStore, + IncomingTransactionsController: this.incomingTransactionsController.store, }) this.memStore.subscribe(this.sendUpdate.bind(this)) } |