Skip to content

Commit

Permalink
Fetch & display received transactions (#6996)
Browse files Browse the repository at this point in the history
  • Loading branch information
danjm authored and whymarrh committed Aug 16, 2019
1 parent 2f5d7ac commit 8215296
Show file tree
Hide file tree
Showing 14 changed files with 942 additions and 26 deletions.
222 changes: 222 additions & 0 deletions app/scripts/controllers/incoming-transactions.js
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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, {
Expand All @@ -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))
}
Expand Down
1 change: 1 addition & 0 deletions development/states/confirm-sig-requests.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
],
"tokens": [],
"transactions": {},
"incomingTransactions": {},
"selectedAddressTxList": [],
"unapprovedTxs": {},
"unapprovedMsgs": {
Expand Down
1 change: 1 addition & 0 deletions development/states/currency-localization.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
],
"tokens": [],
"transactions": {},
"incomingTransactions": {},
"selectedAddressTxList": [],
"unapprovedMsgs": {},
"unapprovedMsgCount": 0,
Expand Down
1 change: 1 addition & 0 deletions development/states/send-new-ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"conversionRate": 1200.88200327,
"conversionDate": 1489013762,
"noActiveNotices": true,
"incomingTransactions": {},
"frequentRpcList": [],
"network": "3",
"accounts": {
Expand Down
1 change: 1 addition & 0 deletions development/states/tx-list-items.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
],
"tokens": [],
"transactions": {},
"incomingTransactions": {},
"selectedAddressTxList": [
{
"err": {
Expand Down
1 change: 1 addition & 0 deletions test/data/mock-state.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
}
},
"cachedBalances": {},
"incomingTransactions": {},
"unapprovedTxs": {
"8393540981007587": {
"id": 8393540981007587,
Expand Down
Loading

0 comments on commit 8215296

Please sign in to comment.