diff options
author | Dan J Miller <danjm.com@gmail.com> | 2019-08-17 02:54:10 +0800 |
---|---|---|
committer | Whymarrh Whitby <whymarrh.whitby@gmail.com> | 2019-08-17 02:54:10 +0800 |
commit | 821529622e4baf095dc34c309b878d09f945da9b (patch) | |
tree | 2d68d862cd7e24c3e0b9fcf56046e01ec546d521 /app/scripts/controllers | |
parent | 2f5d7ac8c304260bf9f6aef487140c04741bd17c (diff) | |
download | tangerine-wallet-browser-821529622e4baf095dc34c309b878d09f945da9b.tar.gz tangerine-wallet-browser-821529622e4baf095dc34c309b878d09f945da9b.tar.zst tangerine-wallet-browser-821529622e4baf095dc34c309b878d09f945da9b.zip |
Fetch & display received transactions (#6996)
Diffstat (limited to 'app/scripts/controllers')
-rw-r--r-- | app/scripts/controllers/incoming-transactions.js | 222 |
1 files changed, 222 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 |