Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Received transactions #6996

Merged
merged 15 commits into from
Aug 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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