aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan J Miller <danjm.com@gmail.com>2019-08-17 02:54:10 +0800
committerWhymarrh Whitby <whymarrh.whitby@gmail.com>2019-08-17 02:54:10 +0800
commit821529622e4baf095dc34c309b878d09f945da9b (patch)
tree2d68d862cd7e24c3e0b9fcf56046e01ec546d521
parent2f5d7ac8c304260bf9f6aef487140c04741bd17c (diff)
downloadtangerine-wallet-browser-821529622e4baf095dc34c309b878d09f945da9b.tar.gz
tangerine-wallet-browser-821529622e4baf095dc34c309b878d09f945da9b.tar.zst
tangerine-wallet-browser-821529622e4baf095dc34c309b878d09f945da9b.zip
Fetch & display received transactions (#6996)
-rw-r--r--app/scripts/controllers/incoming-transactions.js222
-rw-r--r--app/scripts/metamask-controller.js10
-rw-r--r--development/states/confirm-sig-requests.json1
-rw-r--r--development/states/currency-localization.json1
-rw-r--r--development/states/send-new-ui.json1
-rw-r--r--development/states/tx-list-items.json1
-rw-r--r--test/data/mock-state.json1
-rw-r--r--test/unit/app/controllers/incoming-transactions-test.js638
-rw-r--r--ui/app/components/app/transaction-list-item/transaction-list-item.component.js5
-rw-r--r--ui/app/components/app/transaction-list-item/transaction-list-item.container.js12
-rw-r--r--ui/app/components/app/transaction-list/index.scss33
-rw-r--r--ui/app/helpers/constants/transactions.js1
-rw-r--r--ui/app/helpers/utils/transactions.util.js5
-rw-r--r--ui/app/selectors/transactions.js37
14 files changed, 942 insertions, 26 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))
}
diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json
index 16199f48f..ae7f3454d 100644
--- a/development/states/confirm-sig-requests.json
+++ b/development/states/confirm-sig-requests.json
@@ -63,6 +63,7 @@
],
"tokens": [],
"transactions": {},
+ "incomingTransactions": {},
"selectedAddressTxList": [],
"unapprovedTxs": {},
"unapprovedMsgs": {
diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json
index 9d5f771c2..dff527f5a 100644
--- a/development/states/currency-localization.json
+++ b/development/states/currency-localization.json
@@ -64,6 +64,7 @@
],
"tokens": [],
"transactions": {},
+ "incomingTransactions": {},
"selectedAddressTxList": [],
"unapprovedMsgs": {},
"unapprovedMsgCount": 0,
diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json
index bcfc76221..69b4b0568 100644
--- a/development/states/send-new-ui.json
+++ b/development/states/send-new-ui.json
@@ -28,6 +28,7 @@
"conversionRate": 1200.88200327,
"conversionDate": 1489013762,
"noActiveNotices": true,
+ "incomingTransactions": {},
"frequentRpcList": [],
"network": "3",
"accounts": {
diff --git a/development/states/tx-list-items.json b/development/states/tx-list-items.json
index fd60003ba..08d1cf263 100644
--- a/development/states/tx-list-items.json
+++ b/development/states/tx-list-items.json
@@ -64,6 +64,7 @@
],
"tokens": [],
"transactions": {},
+ "incomingTransactions": {},
"selectedAddressTxList": [
{
"err": {
diff --git a/test/data/mock-state.json b/test/data/mock-state.json
index 122945ec1..ed25b0abe 100644
--- a/test/data/mock-state.json
+++ b/test/data/mock-state.json
@@ -12,6 +12,7 @@
}
},
"cachedBalances": {},
+ "incomingTransactions": {},
"unapprovedTxs": {
"8393540981007587": {
"id": 8393540981007587,
diff --git a/test/unit/app/controllers/incoming-transactions-test.js b/test/unit/app/controllers/incoming-transactions-test.js
new file mode 100644
index 000000000..923da7de9
--- /dev/null
+++ b/test/unit/app/controllers/incoming-transactions-test.js
@@ -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',
+ })
+ })
+ })
+})
diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js
index 8bdb6a313..f4f8d97b2 100644
--- a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js
+++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js
@@ -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 ? '' : '-'}
/>
)
}
diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js
index 1675958aa..27b9e2608 100644
--- a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js
+++ b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js
@@ -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,
diff --git a/ui/app/components/app/transaction-list/index.scss b/ui/app/components/app/transaction-list/index.scss
index 42eddd31e..7535137e2 100644
--- a/ui/app/components/app/transaction-list/index.scss
+++ b/ui/app/components/app/transaction-list/index.scss
@@ -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;
}
}
diff --git a/ui/app/helpers/constants/transactions.js b/ui/app/helpers/constants/transactions.js
index d0a819b9b..e91e56ddc 100644
--- a/ui/app/helpers/constants/transactions.js
+++ b/ui/app/helpers/constants/transactions.js
@@ -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'
diff --git a/ui/app/helpers/utils/transactions.util.js b/ui/app/helpers/utils/transactions.util.js
index b65bda5b2..cb347ffaa 100644
--- a/ui/app/helpers/utils/transactions.util.js
+++ b/ui/app/helpers/utils/transactions.util.js
@@ -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
}
diff --git a/ui/app/selectors/transactions.js b/ui/app/selectors/transactions.js
index b1d27b333..5450978a6 100644
--- a/ui/app/selectors/transactions.js
+++ b/ui/app/selectors/transactions.js
@@ -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)
}
)