1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Fetch & display received transactions (#6996)

This commit is contained in:
Dan J Miller 2019-08-16 16:24:10 -02:30 committed by Whymarrh Whitby
parent 2f5d7ac8c3
commit 821529622e
14 changed files with 942 additions and 26 deletions

View File

@ -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

View File

@ -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))
}

View File

@ -63,6 +63,7 @@
],
"tokens": [],
"transactions": {},
"incomingTransactions": {},
"selectedAddressTxList": [],
"unapprovedTxs": {},
"unapprovedMsgs": {

View File

@ -64,6 +64,7 @@
],
"tokens": [],
"transactions": {},
"incomingTransactions": {},
"selectedAddressTxList": [],
"unapprovedMsgs": {},
"unapprovedMsgCount": 0,

View File

@ -28,6 +28,7 @@
"conversionRate": 1200.88200327,
"conversionDate": 1489013762,
"noActiveNotices": true,
"incomingTransactions": {},
"frequentRpcList": [],
"network": "3",
"accounts": {

View File

@ -64,6 +64,7 @@
],
"tokens": [],
"transactions": {},
"incomingTransactions": {},
"selectedAddressTxList": [
{
"err": {

View File

@ -12,6 +12,7 @@
}
},
"cachedBalances": {},
"incomingTransactions": {},
"unapprovedTxs": {
"8393540981007587": {
"id": 8393540981007587,

View File

@ -0,0 +1,638 @@
const assert = require('assert')
const sinon = require('sinon')
const proxyquire = require('proxyquire')
const IncomingTransactionsController = proxyquire('../../../../app/scripts/controllers/incoming-transactions', {
'../lib/random-id': () => 54321,
})
const {
ROPSTEN,
RINKEBY,
KOVAN,
MAINNET,
} = require('../../../../app/scripts/controllers/network/enums')
describe('IncomingTransactionsController', () => {
const EMPTY_INIT_STATE = {
incomingTransactions: {},
incomingTxLastFetchedBlocksByNetwork: {
[ROPSTEN]: null,
[RINKEBY]: null,
[KOVAN]: null,
[MAINNET]: null,
},
}
const NON_EMPTY_INIT_STATE = {
incomingTransactions: {
'0x123456': { id: 777 },
},
incomingTxLastFetchedBlocksByNetwork: {
[ROPSTEN]: 1,
[RINKEBY]: 2,
[KOVAN]: 3,
[MAINNET]: 4,
},
}
const NON_EMPTY_INIT_STATE_WITH_FAKE_NETWORK_STATE = {
incomingTransactions: {
'0x123456': { id: 777 },
},
incomingTxLastFetchedBlocksByNetwork: {
[ROPSTEN]: 1,
[RINKEBY]: 2,
[KOVAN]: 3,
[MAINNET]: 4,
FAKE_NETWORK: 1111,
},
}
const MOCK_BLOCKTRACKER = {
on: sinon.spy(),
testProperty: 'fakeBlockTracker',
getCurrentBlock: () => '0xa',
}
const MOCK_NETWORK_CONTROLLER = {
getProviderConfig: () => ({ type: 'FAKE_NETWORK' }),
on: sinon.spy(),
}
const MOCK_PREFERENCES_CONTROLLER = {
getSelectedAddress: sinon.stub().returns('0x0101'),
store: {
subscribe: sinon.spy(),
},
}
describe('constructor', () => {
it('should set up correct store, listeners and properties in the constructor', () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: {},
})
sinon.spy(incomingTransactionsController, '_update')
assert.deepEqual(incomingTransactionsController.blockTracker, MOCK_BLOCKTRACKER)
assert.deepEqual(incomingTransactionsController.networkController, MOCK_NETWORK_CONTROLLER)
assert.equal(incomingTransactionsController.preferencesController, MOCK_PREFERENCES_CONTROLLER)
assert.equal(incomingTransactionsController.getCurrentNetwork(), 'FAKE_NETWORK')
assert.deepEqual(incomingTransactionsController.store.getState(), EMPTY_INIT_STATE)
assert(incomingTransactionsController.networkController.on.calledOnce)
assert.equal(incomingTransactionsController.networkController.on.getCall(0).args[0], 'networkDidChange')
const networkControllerListenerCallback = incomingTransactionsController.networkController.on.getCall(0).args[1]
assert.equal(incomingTransactionsController._update.callCount, 0)
networkControllerListenerCallback('testNetworkType')
assert.equal(incomingTransactionsController._update.callCount, 1)
assert.deepEqual(incomingTransactionsController._update.getCall(0).args[0], {
address: '0x0101',
networkType: 'testNetworkType',
})
incomingTransactionsController._update.resetHistory()
assert(incomingTransactionsController.blockTracker.on.calledOnce)
assert.equal(incomingTransactionsController.blockTracker.on.getCall(0).args[0], 'latest')
const blockTrackerListenerCallback = incomingTransactionsController.blockTracker.on.getCall(0).args[1]
assert.equal(incomingTransactionsController._update.callCount, 0)
blockTrackerListenerCallback('0xabc')
assert.equal(incomingTransactionsController._update.callCount, 1)
assert.deepEqual(incomingTransactionsController._update.getCall(0).args[0], {
address: '0x0101',
newBlockNumberDec: 2748,
})
})
it('should set the store to a provided initial state', () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
assert.deepEqual(incomingTransactionsController.store.getState(), NON_EMPTY_INIT_STATE)
})
})
describe('_getDataForUpdate', () => {
it('should call fetchAll with the correct params when passed a new block number and the current network has no stored block', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
incomingTransactionsController._fetchAll = sinon.stub().returns({})
await incomingTransactionsController._getDataForUpdate({ address: 'fakeAddress', newBlockNumberDec: 999 })
assert(incomingTransactionsController._fetchAll.calledOnce)
assert.deepEqual(incomingTransactionsController._fetchAll.getCall(0).args, [
'fakeAddress', 999, 'FAKE_NETWORK',
])
})
it('should call fetchAll with the correct params when passed a new block number but the current network has a stored block', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE_WITH_FAKE_NETWORK_STATE,
})
incomingTransactionsController._fetchAll = sinon.stub().returns({})
await incomingTransactionsController._getDataForUpdate({ address: 'fakeAddress', newBlockNumberDec: 999 })
assert(incomingTransactionsController._fetchAll.calledOnce)
assert.deepEqual(incomingTransactionsController._fetchAll.getCall(0).args, [
'fakeAddress', 1111, 'FAKE_NETWORK',
])
})
it('should call fetchAll with the correct params when passed a new network type but no block info exists', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE_WITH_FAKE_NETWORK_STATE,
})
incomingTransactionsController._fetchAll = sinon.stub().returns({})
await incomingTransactionsController._getDataForUpdate({
address: 'fakeAddress',
networkType: 'NEW_FAKE_NETWORK',
})
assert(incomingTransactionsController._fetchAll.calledOnce)
assert.deepEqual(incomingTransactionsController._fetchAll.getCall(0).args, [
'fakeAddress', 10, 'NEW_FAKE_NETWORK',
])
})
it('should call fetchAll with the correct params when passed a new block number but the current network has a stored block', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE_WITH_FAKE_NETWORK_STATE,
})
incomingTransactionsController._fetchAll = sinon.stub().returns({})
await incomingTransactionsController._getDataForUpdate({ address: 'fakeAddress', newBlockNumberDec: 999 })
assert(incomingTransactionsController._fetchAll.calledOnce)
assert.deepEqual(incomingTransactionsController._fetchAll.getCall(0).args, [
'fakeAddress', 1111, 'FAKE_NETWORK',
])
})
it('should return the expected data', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE_WITH_FAKE_NETWORK_STATE,
})
incomingTransactionsController._fetchAll = sinon.stub().returns({
latestIncomingTxBlockNumber: 444,
txs: [{ id: 555 }],
})
const result = await incomingTransactionsController._getDataForUpdate({
address: 'fakeAddress',
networkType: 'FAKE_NETWORK',
})
assert.deepEqual(result, {
latestIncomingTxBlockNumber: 444,
newTxs: [{ id: 555 }],
currentIncomingTxs: {
'0x123456': { id: 777 },
},
currentBlocksByNetwork: {
[ROPSTEN]: 1,
[RINKEBY]: 2,
[KOVAN]: 3,
[MAINNET]: 4,
FAKE_NETWORK: 1111,
},
fetchedBlockNumber: 1111,
network: 'FAKE_NETWORK',
})
})
})
describe('_updateStateWithNewTxData', () => {
const MOCK_INPUT_WITHOUT_LASTEST = {
newTxs: [{ id: 555, hash: '0xfff' }],
currentIncomingTxs: {
'0x123456': { id: 777, hash: '0x123456' },
},
currentBlocksByNetwork: {
[ROPSTEN]: 1,
[RINKEBY]: 2,
[KOVAN]: 3,
[MAINNET]: 4,
FAKE_NETWORK: 1111,
},
fetchedBlockNumber: 1111,
network: 'FAKE_NETWORK',
}
const MOCK_INPUT_WITH_LASTEST = {
...MOCK_INPUT_WITHOUT_LASTEST,
latestIncomingTxBlockNumber: 444,
}
it('should update state with correct blockhash and transactions when passed a truthy latestIncomingTxBlockNumber', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
sinon.spy(incomingTransactionsController.store, 'updateState')
await incomingTransactionsController._updateStateWithNewTxData(MOCK_INPUT_WITH_LASTEST)
assert(incomingTransactionsController.store.updateState.calledOnce)
assert.deepEqual(incomingTransactionsController.store.updateState.getCall(0).args[0], {
incomingTxLastFetchedBlocksByNetwork: {
...MOCK_INPUT_WITH_LASTEST.currentBlocksByNetwork,
'FAKE_NETWORK': 445,
},
incomingTransactions: {
'0x123456': { id: 777, hash: '0x123456' },
'0xfff': { id: 555, hash: '0xfff' },
},
})
})
it('should update state with correct blockhash and transactions when passed a falsy latestIncomingTxBlockNumber', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
sinon.spy(incomingTransactionsController.store, 'updateState')
await incomingTransactionsController._updateStateWithNewTxData(MOCK_INPUT_WITHOUT_LASTEST)
assert(incomingTransactionsController.store.updateState.calledOnce)
assert.deepEqual(incomingTransactionsController.store.updateState.getCall(0).args[0], {
incomingTxLastFetchedBlocksByNetwork: {
...MOCK_INPUT_WITH_LASTEST.currentBlocksByNetwork,
'FAKE_NETWORK': 1112,
},
incomingTransactions: {
'0x123456': { id: 777, hash: '0x123456' },
'0xfff': { id: 555, hash: '0xfff' },
},
})
})
})
describe('_fetchTxs', () => {
const mockFetch = sinon.stub().returns(Promise.resolve({
json: () => Promise.resolve({ someKey: 'someValue' }),
}))
let tempFetch
beforeEach(() => {
tempFetch = global.fetch
global.fetch = mockFetch
})
afterEach(() => {
global.fetch = tempFetch
mockFetch.resetHistory()
})
it('should call fetch with the expected url when passed an address, block number and supported network', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
await incomingTransactionsController._fetchTxs('0xfakeaddress', '789', ROPSTEN)
assert(mockFetch.calledOnce)
assert.equal(mockFetch.getCall(0).args[0], `https://api-${ROPSTEN}.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1&startBlock=789`)
})
it('should call fetch with the expected url when passed an address, block number and MAINNET', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
await incomingTransactionsController._fetchTxs('0xfakeaddress', '789', MAINNET)
assert(mockFetch.calledOnce)
assert.equal(mockFetch.getCall(0).args[0], `https://api.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1&startBlock=789`)
})
it('should call fetch with the expected url when passed an address and supported network, but a falsy block number', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
await incomingTransactionsController._fetchTxs('0xfakeaddress', null, ROPSTEN)
assert(mockFetch.calledOnce)
assert.equal(mockFetch.getCall(0).args[0], `https://api-${ROPSTEN}.etherscan.io/api?module=account&action=txlist&address=0xfakeaddress&tag=latest&page=1`)
})
it('should not fetch and return an empty object when passed an unsported network', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
const result = await incomingTransactionsController._fetchTxs('0xfakeaddress', null, 'UNSUPPORTED_NETWORK')
assert(mockFetch.notCalled)
assert.deepEqual(result, {})
})
it('should return the results from the fetch call, plus the address and currentNetworkID, when passed an address, block number and supported network', async () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
const result = await incomingTransactionsController._fetchTxs('0xfakeaddress', '789', ROPSTEN)
assert(mockFetch.calledOnce)
assert.deepEqual(result, {
someKey: 'someValue',
address: '0xfakeaddress',
currentNetworkID: 3,
})
})
})
describe('_processTxFetchResponse', () => {
it('should return a null block number and empty tx array if status is 0', () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
const result = incomingTransactionsController._processTxFetchResponse({
status: '0',
result: [{ id: 1 }],
address: '0xfakeaddress',
})
assert.deepEqual(result, {
latestIncomingTxBlockNumber: null,
txs: [],
})
})
it('should return a null block number and empty tx array if the passed result array is empty', () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
const result = incomingTransactionsController._processTxFetchResponse({
status: '1',
result: [],
address: '0xfakeaddress',
})
assert.deepEqual(result, {
latestIncomingTxBlockNumber: null,
txs: [],
})
})
it('should return the expected block number and tx list when passed data from a successful fetch', () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
incomingTransactionsController._normalizeTxFromEtherscan = (tx, currentNetworkID) => ({
...tx,
currentNetworkID,
normalized: true,
})
const result = incomingTransactionsController._processTxFetchResponse({
status: '1',
address: '0xfakeaddress',
currentNetworkID: 'FAKE_NETWORK',
result: [
{
hash: '0xabc123',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5000,
time: 10,
},
{
hash: '0xabc123',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5000,
time: 10,
},
{
hash: '0xabc1234',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5000,
time: 9,
},
{
hash: '0xabc12345',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5001,
time: 11,
},
{
hash: '0xabc123456',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5001,
time: 12,
},
{
hash: '0xabc1234567',
txParams: {
to: '0xanotherFakeaddress',
},
blockNumber: 5002,
time: 13,
},
],
})
assert.deepEqual(result, {
latestIncomingTxBlockNumber: 5001,
txs: [
{
hash: '0xabc1234',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5000,
time: 9,
normalized: true,
currentNetworkID: 'FAKE_NETWORK',
},
{
hash: '0xabc123',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5000,
time: 10,
normalized: true,
currentNetworkID: 'FAKE_NETWORK',
},
{
hash: '0xabc12345',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5001,
time: 11,
normalized: true,
currentNetworkID: 'FAKE_NETWORK',
},
{
hash: '0xabc123456',
txParams: {
to: '0xfakeaddress',
},
blockNumber: 5001,
time: 12,
normalized: true,
currentNetworkID: 'FAKE_NETWORK',
},
],
})
})
})
describe('_normalizeTxFromEtherscan', () => {
it('should return the expected data when the tx is in error', () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
const result = incomingTransactionsController._normalizeTxFromEtherscan({
timeStamp: '4444',
isError: '1',
blockNumber: 333,
from: '0xa',
gas: '11',
gasPrice: '12',
nonce: '13',
to: '0xe',
value: '15',
hash: '0xg',
}, 'FAKE_NETWORK')
assert.deepEqual(result, {
blockNumber: 333,
id: 54321,
metamaskNetworkId: 'FAKE_NETWORK',
status: 'failed',
time: 4444000,
txParams: {
from: '0xa',
gas: '0xb',
gasPrice: '0xc',
nonce: '0xd',
to: '0xe',
value: '0xf',
},
hash: '0xg',
transactionCategory: 'incoming',
})
})
it('should return the expected data when the tx is not in error', () => {
const incomingTransactionsController = new IncomingTransactionsController({
blockTracker: MOCK_BLOCKTRACKER,
networkController: MOCK_NETWORK_CONTROLLER,
preferencesController: MOCK_PREFERENCES_CONTROLLER,
initState: NON_EMPTY_INIT_STATE,
})
const result = incomingTransactionsController._normalizeTxFromEtherscan({
timeStamp: '4444',
isError: '0',
blockNumber: 333,
from: '0xa',
gas: '11',
gasPrice: '12',
nonce: '13',
to: '0xe',
value: '15',
hash: '0xg',
}, 'FAKE_NETWORK')
assert.deepEqual(result, {
blockNumber: 333,
id: 54321,
metamaskNetworkId: 'FAKE_NETWORK',
status: 'confirmed',
time: 4444000,
txParams: {
from: '0xa',
gas: '0xb',
gasPrice: '0xc',
nonce: '0xd',
to: '0xe',
value: '0xf',
},
hash: '0xg',
transactionCategory: 'incoming',
})
})
})
})

View File

@ -36,6 +36,7 @@ export default class TransactionListItem extends PureComponent {
rpcPrefs: PropTypes.object,
data: PropTypes.string,
getContractMethodData: PropTypes.func,
isDeposit: PropTypes.bool,
}
static defaultProps = {
@ -117,7 +118,7 @@ export default class TransactionListItem extends PureComponent {
}
renderPrimaryCurrency () {
const { token, primaryTransaction: { txParams: { data } = {} } = {}, value } = this.props
const { token, primaryTransaction: { txParams: { data } = {} } = {}, value, isDeposit } = this.props
return token
? (
@ -132,7 +133,7 @@ export default class TransactionListItem extends PureComponent {
className="transaction-list-item__amount transaction-list-item__amount--primary"
value={value}
type={PRIMARY}
prefix="-"
prefix={isDeposit ? '' : '-'}
/>
)
}

View File

@ -21,8 +21,10 @@ const mapStateToProps = (state, ownProps) => {
const { showFiatInTestnets } = preferencesSelector(state)
const isMainnet = getIsMainnet(state)
const { transactionGroup: { primaryTransaction } = {} } = ownProps
const { txParams: { gas: gasLimit, gasPrice, data } = {} } = primaryTransaction
const selectedAccountBalance = accounts[getSelectedAddress(state)].balance
const { txParams: { gas: gasLimit, gasPrice, data, to } = {} } = primaryTransaction
const selectedAddress = getSelectedAddress(state)
const selectedAccountBalance = accounts[selectedAddress].balance
const isDeposit = selectedAddress === to
const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget)
const { rpcPrefs } = selectRpcInfo || {}
@ -42,6 +44,7 @@ const mapStateToProps = (state, ownProps) => {
selectedAccountBalance,
hasEnoughCancelGas,
rpcPrefs,
isDeposit,
}
}
@ -68,12 +71,13 @@ const mapDispatchToProps = dispatch => {
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { transactionGroup: { primaryTransaction, initialTransaction } = {} } = ownProps
const { isDeposit } = stateProps
const { retryTransaction, ...restDispatchProps } = dispatchProps
const { txParams: { nonce, data } = {}, time } = initialTransaction
const { txParams: { nonce, data } = {}, time = 0 } = initialTransaction
const { txParams: { value } = {} } = primaryTransaction
const tokenData = data && getTokenData(data)
const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
const nonceAndDate = nonce && !isDeposit ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
return {
...stateProps,

View File

@ -11,15 +11,34 @@
}
&__header {
flex: 0 0 auto;
font-size: 14px;
line-height: 20px;
color: $Grey-400;
border-bottom: 1px solid $Grey-100;
padding: 8px 0 8px 20px;
@media screen and (max-width: $break-small) {
padding: 8px 0 8px 16px;
&__tabs {
display: flex;
}
&__tab,
&__tab--selected {
flex: 0 0 auto;
font-size: 14px;
line-height: 20px;
color: $Grey-400;
padding: 8px 0 8px 20px;
cursor: pointer;
&:hover {
font-weight: bold;
}
@media screen and (max-width: $break-small) {
padding: 8px 0 8px 16px;
}
}
&__tab--selected {
font-weight: bold;
color: $Blue-400;
cursor: auto;
}
}

View File

@ -20,5 +20,6 @@ export const TRANSFER_FROM_ACTION_KEY = 'transferFrom'
export const SIGNATURE_REQUEST_KEY = 'signatureRequest'
export const CONTRACT_INTERACTION_KEY = 'contractInteraction'
export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt'
export const DEPOSIT_TRANSACTION_KEY = 'deposit'
export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift'

View File

@ -21,6 +21,7 @@ import {
SIGNATURE_REQUEST_KEY,
CONTRACT_INTERACTION_KEY,
CANCEL_ATTEMPT_ACTION_KEY,
DEPOSIT_TRANSACTION_KEY,
} from '../constants/transactions'
import log from 'loglevel'
@ -124,6 +125,10 @@ export function isTokenMethodAction (transactionCategory) {
export function getTransactionActionKey (transaction) {
const { msgParams, type, transactionCategory } = transaction
if (transactionCategory === 'incoming') {
return DEPOSIT_TRANSACTION_KEY
}
if (type === 'cancel') {
return CANCEL_ATTEMPT_ACTION_KEY
}

View File

@ -10,11 +10,16 @@ import {
TRANSACTION_TYPE_RETRY,
} from '../../../app/scripts/controllers/transactions/enums'
import { hexToDecimal } from '../helpers/utils/conversions.util'
import { selectedTokenAddressSelector } from './tokens'
import txHelper from '../../lib/tx-helper'
export const shapeShiftTxListSelector = state => state.metamask.shapeShiftTxList
export const incomingTxListSelector = state => {
const selectedAddress = state.metamask.selectedAddress
return Object.values(state.metamask.incomingTransactions)
.filter(({ txParams }) => txParams.to === selectedAddress)
}
export const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs
export const selectedAddressTxListSelector = state => state.metamask.selectedAddressTxList
export const unapprovedPersonalMsgsSelector = state => state.metamask.unapprovedPersonalMsgs
@ -55,9 +60,10 @@ export const transactionsSelector = createSelector(
selectedTokenAddressSelector,
unapprovedMessagesSelector,
shapeShiftTxListSelector,
incomingTxListSelector,
selectedAddressTxListSelector,
(selectedTokenAddress, unapprovedMessages = [], shapeShiftTxList = [], transactions = []) => {
const txsToRender = transactions.concat(unapprovedMessages, shapeShiftTxList)
(selectedTokenAddress, unapprovedMessages = [], shapeShiftTxList = [], incomingTxList = [], transactions = []) => {
const txsToRender = transactions.concat(unapprovedMessages, shapeShiftTxList, incomingTxList)
return selectedTokenAddress
? txsToRender
@ -158,17 +164,18 @@ const insertTransactionGroupByTime = (transactionGroups, transactionGroup) => {
}
/**
* @name mergeShapeshiftTransactionGroups
* @name mergeNonNonceTransactionGroups
* @private
* @description Inserts (mutates) shapeshift transactionGroups into an array of nonce-ordered
* transactionGroups by time. Shapeshift transactionGroups need to be sorted by time within the list
* of transactions as they do not have nonces.
* @description Inserts (mutates) transactionGroups that are not to be ordered by nonce into an array
* of nonce-ordered transactionGroups by time. Shapeshift transactionGroups need to be sorted by time
* within the list of transactions as they do not have nonces.
* @param {transactionGroup[]} orderedTransactionGroups - Array of transactionGroups ordered by
* nonce.
* @param {transactionGroup[]} shapeshiftTransactionGroups - Array of shapeshift transactionGroups
* @param {transactionGroup[]} nonNonceTransactionGroups - Array of transactionGroups not intended to be ordered by nonce,
* but intended to be ordered by timestamp
*/
const mergeShapeshiftTransactionGroups = (orderedTransactionGroups, shapeshiftTransactionGroups) => {
shapeshiftTransactionGroups.forEach(shapeshiftGroup => {
const mergeNonNonceTransactionGroups = (orderedTransactionGroups, nonNonceTransactionGroups) => {
nonNonceTransactionGroups.forEach(shapeshiftGroup => {
insertTransactionGroupByTime(orderedTransactionGroups, shapeshiftGroup)
})
}
@ -183,13 +190,14 @@ export const nonceSortedTransactionsSelector = createSelector(
(transactions = []) => {
const unapprovedTransactionGroups = []
const shapeshiftTransactionGroups = []
const incomingTransactionGroups = []
const orderedNonces = []
const nonceToTransactionsMap = {}
transactions.forEach(transaction => {
const { txParams: { nonce } = {}, status, type, time: txTime, key } = transaction
const { txParams: { nonce } = {}, status, type, time: txTime, key, transactionCategory } = transaction
if (typeof nonce === 'undefined') {
if (typeof nonce === 'undefined' || transactionCategory === 'incoming') {
const transactionGroup = {
transactions: [transaction],
initialTransaction: transaction,
@ -200,6 +208,8 @@ export const nonceSortedTransactionsSelector = createSelector(
if (key === 'shapeshift') {
shapeshiftTransactionGroups.push(transactionGroup)
} else if (transactionCategory === 'incoming') {
incomingTransactionGroups.push(transactionGroup)
} else {
insertTransactionGroupByTime(unapprovedTransactionGroups, transactionGroup)
}
@ -245,7 +255,8 @@ export const nonceSortedTransactionsSelector = createSelector(
})
const orderedTransactionGroups = orderedNonces.map(nonce => nonceToTransactionsMap[nonce])
mergeShapeshiftTransactionGroups(orderedTransactionGroups, shapeshiftTransactionGroups)
mergeNonNonceTransactionGroups(orderedTransactionGroups, shapeshiftTransactionGroups)
mergeNonNonceTransactionGroups(orderedTransactionGroups, incomingTransactionGroups)
return unapprovedTransactionGroups.concat(orderedTransactionGroups)
}
)