From 13ebb0b455bc775a53b6bb30e675a39d02d8f6f5 Mon Sep 17 00:00:00 2001 From: tmashuang Date: Mon, 21 May 2018 05:59:26 -0700 Subject: Moved loose some loose test files to sub folders --- .../controllers/transactions/nonce-tracker-test.js | 239 +++++++++++ .../controllers/transactions/pending-tx-test.js | 402 ++++++++++++++++++ .../controllers/transactions/tx-controller-test.js | 463 +++++++++++++++++++++ .../controllers/transactions/tx-gas-util-test.js | 77 ++++ .../app/controllers/transactions/tx-helper-test.js | 17 + .../transactions/tx-state-history-helper-test.js | 129 ++++++ .../transactions/tx-state-manager-test.js | 291 +++++++++++++ .../app/controllers/transactions/tx-utils-test.js | 98 +++++ 8 files changed, 1716 insertions(+) create mode 100644 test/unit/app/controllers/transactions/nonce-tracker-test.js create mode 100644 test/unit/app/controllers/transactions/pending-tx-test.js create mode 100644 test/unit/app/controllers/transactions/tx-controller-test.js create mode 100644 test/unit/app/controllers/transactions/tx-gas-util-test.js create mode 100644 test/unit/app/controllers/transactions/tx-helper-test.js create mode 100644 test/unit/app/controllers/transactions/tx-state-history-helper-test.js create mode 100644 test/unit/app/controllers/transactions/tx-state-manager-test.js create mode 100644 test/unit/app/controllers/transactions/tx-utils-test.js (limited to 'test/unit/app/controllers/transactions') diff --git a/test/unit/app/controllers/transactions/nonce-tracker-test.js b/test/unit/app/controllers/transactions/nonce-tracker-test.js new file mode 100644 index 000000000..fc852458c --- /dev/null +++ b/test/unit/app/controllers/transactions/nonce-tracker-test.js @@ -0,0 +1,239 @@ +const assert = require('assert') +const NonceTracker = require('../../../../../app/scripts/controllers/transactions/nonce-tracker') +const MockTxGen = require('../../../../lib/mock-tx-gen') +let providerResultStub = {} + +describe('Nonce Tracker', function () { + let nonceTracker, provider + let getPendingTransactions, pendingTxs + let getConfirmedTransactions, confirmedTxs + + describe('#getNonceLock', function () { + + describe('with 3 confirmed and 1 pending', function () { + beforeEach(function () { + const txGen = new MockTxGen() + confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 }) + pendingTxs = txGen.generate({ status: 'submitted' }, { count: 1 }) + nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x1') + }) + + it('should return 4', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '4', `nonce should be 4 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + + it('should use localNonce if network returns a nonce lower then a confirmed tx in state', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '4', 'nonce should be 4') + await nonceLock.releaseLock() + }) + }) + + describe('sentry issue 476304902', function () { + beforeEach(function () { + const txGen = new MockTxGen() + pendingTxs = txGen.generate({ status: 'submitted' }, { + fromNonce: 3, + count: 29, + }) + nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x3') + }) + + it('should return 9', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '32', `nonce should be 32 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) + + describe('issue 3670', function () { + beforeEach(function () { + const txGen = new MockTxGen() + pendingTxs = txGen.generate({ status: 'submitted' }, { + fromNonce: 6, + count: 3, + }) + nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x6') + }) + + it('should return 9', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '9', `nonce should be 9 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) + + describe('with no previous txs', function () { + beforeEach(function () { + nonceTracker = generateNonceTrackerWith([], []) + }) + + it('should return 0', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 returned ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) + + describe('with multiple previous txs with same nonce', function () { + beforeEach(function () { + const txGen = new MockTxGen() + confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 1 }) + pendingTxs = txGen.generate({ + status: 'submitted', + txParams: { nonce: '0x01' }, + }, { count: 5 }) + + nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x0') + }) + + it('should return nonce after those', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) + + describe('when local confirmed count is higher than network nonce', function () { + beforeEach(function () { + const txGen = new MockTxGen() + confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 }) + nonceTracker = generateNonceTrackerWith([], confirmedTxs, '0x1') + }) + + it('should return nonce after those', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '3', `nonce should be 3 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) + + describe('when local pending count is higher than other metrics', function () { + beforeEach(function () { + const txGen = new MockTxGen() + pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 }) + nonceTracker = generateNonceTrackerWith(pendingTxs, []) + }) + + it('should return nonce after those', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) + + describe('when provider nonce is higher than other metrics', function () { + beforeEach(function () { + const txGen = new MockTxGen() + pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 }) + nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x05') + }) + + it('should return nonce after those', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) + + describe('when there are some pending nonces below the remote one and some over.', function () { + beforeEach(function () { + const txGen = new MockTxGen() + pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 }) + nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x03') + }) + + it('should return nonce after those', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) + + describe('when there are pending nonces non sequentially over the network nonce.', function () { + beforeEach(function () { + const txGen = new MockTxGen() + txGen.generate({ status: 'submitted' }, { count: 5 }) + // 5 over that number + pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 }) + nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x00') + }) + + it('should return nonce after network nonce', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) + + describe('When all three return different values', function () { + beforeEach(function () { + const txGen = new MockTxGen() + const confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 10 }) + const pendingTxs = txGen.generate({ + status: 'submitted', + nonce: 100, + }, { count: 1 }) + // 0x32 is 50 in hex: + nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x32') + }) + + it('should return nonce after network nonce', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '50', `nonce should be 50 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) + + describe('Faq issue 67', function () { + beforeEach(function () { + const txGen = new MockTxGen() + const confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 64 }) + const pendingTxs = txGen.generate({ + status: 'submitted', + }, { count: 10 }) + // 0x40 is 64 in hex: + nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x40') + }) + + it('should return nonce after network nonce', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '74', `nonce should be 74 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) + }) +}) + +function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') { + const getPendingTransactions = () => pending + const getConfirmedTransactions = () => confirmed + providerResultStub.result = providerStub + const provider = { + sendAsync: (_, cb) => { cb(undefined, providerResultStub) }, + _blockTracker: { + getCurrentBlock: () => '0x11b568', + }, + } + return new NonceTracker({ + provider, + getPendingTransactions, + getConfirmedTransactions, + }) +} + diff --git a/test/unit/app/controllers/transactions/pending-tx-test.js b/test/unit/app/controllers/transactions/pending-tx-test.js new file mode 100644 index 000000000..e7705c594 --- /dev/null +++ b/test/unit/app/controllers/transactions/pending-tx-test.js @@ -0,0 +1,402 @@ +const assert = require('assert') +const ethUtil = require('ethereumjs-util') +const EthTx = require('ethereumjs-tx') +const ObservableStore = require('obs-store') +const clone = require('clone') +const { createTestProviderTools } = require('../../../../stub/provider') +const PendingTransactionTracker = require('../../../../../app/scripts/controllers/transactions/pending-tx-tracker') +const MockTxGen = require('../../../../lib/mock-tx-gen') +const sinon = require('sinon') +const noop = () => true +const currentNetworkId = 42 +const otherNetworkId = 36 +const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') + + +describe('PendingTransactionTracker', function () { + let pendingTxTracker, txMeta, txMetaNoHash, txMetaNoRawTx, providerResultStub, + provider, txMeta3, txList, knownErrors + this.timeout(10000) + beforeEach(function () { + txMeta = { + id: 1, + hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb', + status: 'signed', + txParams: { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + nonce: '0x1', + value: '0xfffff', + }, + rawTx: '0xf86c808504a817c800827b0d940c62bb85faa3311a998d3aba8098c1235c564966880de0b6b3a7640000802aa08ff665feb887a25d4099e40e11f0fef93ee9608f404bd3f853dd9e84ed3317a6a02ec9d3d1d6e176d4d2593dd760e74ccac753e6a0ea0d00cc9789d0d7ff1f471d', + } + txMetaNoHash = { + id: 2, + status: 'signed', + txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, + } + txMetaNoRawTx = { + hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb', + status: 'signed', + txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, + } + providerResultStub = {} + provider = createTestProviderTools({ scaffold: providerResultStub }).provider + + pendingTxTracker = new PendingTransactionTracker({ + provider, + nonceTracker: { + getGlobalLock: async () => { + return { releaseLock: () => {} } + } + }, + getPendingTransactions: () => {return []}, + getCompletedTransactions: () => {return []}, + publishTransaction: () => {}, + }) + }) + + describe('_checkPendingTx state management', function () { + let stub + + afterEach(function () { + if (stub) { + stub.restore() + } + }) + + it('should become failed if another tx with the same nonce succeeds', async function () { + + // SETUP + const txGen = new MockTxGen() + + txGen.generate({ + id: '456', + value: '0x01', + hash: '0xbad', + status: 'confirmed', + nonce: '0x01', + }, { count: 1 }) + + const pending = txGen.generate({ + id: '123', + value: '0x02', + hash: '0xfad', + status: 'submitted', + nonce: '0x01', + }, { count: 1 })[0] + + stub = sinon.stub(pendingTxTracker, 'getCompletedTransactions') + .returns(txGen.txs) + + // THE EXPECTATION + const spy = sinon.spy() + pendingTxTracker.on('tx:failed', (txId, err) => { + assert.equal(txId, pending.id, 'should fail the pending tx') + assert.equal(err.name, 'NonceTakenErr', 'should emit a nonce taken error.') + spy(txId, err) + }) + + // THE METHOD + await pendingTxTracker._checkPendingTx(pending) + + // THE ASSERTION + assert.ok(spy.calledWith(pending.id), 'tx failed should be emitted') + }) + }) + + describe('#checkForTxInBlock', function () { + it('should return if no pending transactions', function () { + // throw a type error if it trys to do anything on the block + // thus failing the test + const block = Proxy.revocable({}, {}).revoke() + pendingTxTracker.checkForTxInBlock(block) + }) + it('should emit \'tx:failed\' if the txMeta does not have a hash', function (done) { + const block = Proxy.revocable({}, {}).revoke() + pendingTxTracker.getPendingTransactions = () => [txMetaNoHash] + pendingTxTracker.once('tx:failed', (txId, err) => { + assert(txId, txMetaNoHash.id, 'should pass txId') + done() + }) + pendingTxTracker.checkForTxInBlock(block) + }) + it('should emit \'txConfirmed\' if the tx is in the block', function (done) { + const block = { transactions: [txMeta]} + pendingTxTracker.getPendingTransactions = () => [txMeta] + pendingTxTracker.once('tx:confirmed', (txId) => { + assert(txId, txMeta.id, 'should pass txId') + done() + }) + pendingTxTracker.once('tx:failed', (_, err) => { done(err) }) + pendingTxTracker.checkForTxInBlock(block) + }) + }) + describe('#queryPendingTxs', function () { + it('should call #_checkPendingTxs if their is no oldBlock', function (done) { + let newBlock, oldBlock + newBlock = { number: '0x01' } + pendingTxTracker._checkPendingTxs = done + pendingTxTracker.queryPendingTxs({ oldBlock, newBlock }) + }) + it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) { + let newBlock, oldBlock + oldBlock = { number: '0x01' } + newBlock = { number: '0x03' } + pendingTxTracker._checkPendingTxs = done + pendingTxTracker.queryPendingTxs({ oldBlock, newBlock }) + }) + it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) { + let newBlock, oldBlock + oldBlock = { number: '0x1' } + newBlock = { number: '0x2' } + pendingTxTracker._checkPendingTxs = () => { + const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less') + done(err) + } + pendingTxTracker.queryPendingTxs({ oldBlock, newBlock }) + done() + }) + }) + + describe('#_checkPendingTx', function () { + it('should emit \'tx:failed\' if the txMeta does not have a hash', function (done) { + pendingTxTracker.once('tx:failed', (txId, err) => { + assert(txId, txMetaNoHash.id, 'should pass txId') + done() + }) + pendingTxTracker._checkPendingTx(txMetaNoHash) + }) + + it('should should return if query does not return txParams', function () { + providerResultStub.eth_getTransactionByHash = null + pendingTxTracker._checkPendingTx(txMeta) + }) + + it('should emit \'txConfirmed\'', function (done) { + providerResultStub.eth_getTransactionByHash = {blockNumber: '0x01'} + pendingTxTracker.once('tx:confirmed', (txId) => { + assert(txId, txMeta.id, 'should pass txId') + done() + }) + pendingTxTracker.once('tx:failed', (_, err) => { done(err) }) + pendingTxTracker._checkPendingTx(txMeta) + }) + }) + + describe('#_checkPendingTxs', function () { + beforeEach(function () { + const txMeta2 = txMeta3 = txMeta + txMeta2.id = 2 + txMeta3.id = 3 + txList = [txMeta, txMeta2, txMeta3].map((tx) => { + tx.processed = new Promise ((resolve) => { tx.resolve = resolve }) + return tx + }) + }) + + it('should warp all txMeta\'s in #_checkPendingTx', function (done) { + pendingTxTracker.getPendingTransactions = () => txList + pendingTxTracker._checkPendingTx = (tx) => { tx.resolve(tx) } + const list = txList.map + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + + pendingTxTracker._checkPendingTxs() + }) + }) + + describe('#resubmitPendingTxs', function () { + const blockStub = { number: '0x0' }; + beforeEach(function () { + const txMeta2 = txMeta3 = txMeta + txList = [txMeta, txMeta2, txMeta3].map((tx) => { + tx.processed = new Promise ((resolve) => { tx.resolve = resolve }) + return tx + }) + }) + + it('should return if no pending transactions', function () { + pendingTxTracker.resubmitPendingTxs() + }) + it('should call #_resubmitTx for all pending tx\'s', function (done) { + pendingTxTracker.getPendingTransactions = () => txList + pendingTxTracker._resubmitTx = async (tx) => { tx.resolve(tx) } + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + pendingTxTracker.resubmitPendingTxs(blockStub) + }) + it('should not emit \'tx:failed\' if the txMeta throws a known txError', function (done) { + knownErrors =[ + // geth + ' Replacement transaction Underpriced ', + ' known transaction', + // parity + 'Gas price too low to replace ', + ' transaction with the sAme hash was already imported', + // other + ' gateway timeout', + ' noncE too low ', + ] + const enoughForAllErrors = txList.concat(txList) + + pendingTxTracker.on('tx:failed', (_, err) => done(err)) + + pendingTxTracker.getPendingTransactions = () => enoughForAllErrors + pendingTxTracker._resubmitTx = async (tx) => { + tx.resolve() + throw new Error(knownErrors.pop()) + } + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + + pendingTxTracker.resubmitPendingTxs(blockStub) + }) + it('should emit \'tx:warning\' if it encountered a real error', function (done) { + pendingTxTracker.once('tx:warning', (txMeta, err) => { + if (err.message === 'im some real error') { + const matchingTx = txList.find(tx => tx.id === txMeta.id) + matchingTx.resolve() + } else { + done(err) + } + }) + + pendingTxTracker.getPendingTransactions = () => txList + pendingTxTracker._resubmitTx = async (tx) => { throw new TypeError('im some real error') } + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + + pendingTxTracker.resubmitPendingTxs(blockStub) + }) + }) + describe('#_resubmitTx', function () { + const mockFirstRetryBlockNumber = '0x1' + let txMetaToTestExponentialBackoff + + beforeEach(() => { + pendingTxTracker.getBalance = (address) => { + assert.equal(address, txMeta.txParams.from, 'Should pass the address') + return enoughBalance + } + pendingTxTracker.publishTransaction = async (rawTx) => { + assert.equal(rawTx, txMeta.rawTx, 'Should pass the rawTx') + } + sinon.spy(pendingTxTracker, 'publishTransaction') + + txMetaToTestExponentialBackoff = Object.assign({}, txMeta, { + retryCount: 4, + firstRetryBlockNumber: mockFirstRetryBlockNumber, + }) + }) + + afterEach(() => { + pendingTxTracker.publishTransaction.restore() + }) + + it('should publish the transaction', function (done) { + const enoughBalance = '0x100000' + + // Stubbing out current account state: + // Adding the fake tx: + pendingTxTracker._resubmitTx(txMeta) + .then(() => done()) + .catch((err) => { + assert.ifError(err, 'should not throw an error') + done(err) + }) + + assert.equal(pendingTxTracker.publishTransaction.callCount, 1, 'Should call publish transaction') + }) + + it('should not publish the transaction if the limit of retries has been exceeded', function (done) { + const enoughBalance = '0x100000' + const mockLatestBlockNumber = '0x5' + + pendingTxTracker._resubmitTx(txMetaToTestExponentialBackoff, mockLatestBlockNumber) + .then(() => done()) + .catch((err) => { + assert.ifError(err, 'should not throw an error') + done(err) + }) + + assert.equal(pendingTxTracker.publishTransaction.callCount, 0, 'Should NOT call publish transaction') + }) + + it('should publish the transaction if the number of blocks since last retry exceeds the last set limit', function (done) { + const enoughBalance = '0x100000' + const mockLatestBlockNumber = '0x11' + + pendingTxTracker._resubmitTx(txMetaToTestExponentialBackoff, mockLatestBlockNumber) + .then(() => done()) + .catch((err) => { + assert.ifError(err, 'should not throw an error') + done(err) + }) + + assert.equal(pendingTxTracker.publishTransaction.callCount, 1, 'Should call publish transaction') + }) + }) + + describe('#_checkIfNonceIsTaken', function () { + beforeEach ( function () { + let confirmedTxList = [{ + id: 1, + hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb', + status: 'confirmed', + txParams: { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + nonce: '0x1', + value: '0xfffff', + }, + rawTx: '0xf86c808504a817c800827b0d940c62bb85faa3311a998d3aba8098c1235c564966880de0b6b3a7640000802aa08ff665feb887a25d4099e40e11f0fef93ee9608f404bd3f853dd9e84ed3317a6a02ec9d3d1d6e176d4d2593dd760e74ccac753e6a0ea0d00cc9789d0d7ff1f471d', + }, { + id: 2, + hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb', + status: 'confirmed', + txParams: { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + nonce: '0x2', + value: '0xfffff', + }, + rawTx: '0xf86c808504a817c800827b0d940c62bb85faa3311a998d3aba8098c1235c564966880de0b6b3a7640000802aa08ff665feb887a25d4099e40e11f0fef93ee9608f404bd3f853dd9e84ed3317a6a02ec9d3d1d6e176d4d2593dd760e74ccac753e6a0ea0d00cc9789d0d7ff1f471d', + }] + pendingTxTracker.getCompletedTransactions = (address) => { + if (!address) throw new Error('unless behavior has changed #_checkIfNonceIsTaken needs a filtered list of transactions to see if the nonce is taken') + return confirmedTxList + } + }) + + it('should return false if nonce has not been taken', function (done) { + pendingTxTracker._checkIfNonceIsTaken({ + txParams: { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + nonce: '0x3', + value: '0xfffff', + }, + }) + .then((taken) => { + assert.ok(!taken) + done() + }) + .catch(done) + }) + + it('should return true if nonce has been taken', function (done) { + pendingTxTracker._checkIfNonceIsTaken({ + txParams: { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + nonce: '0x2', + value: '0xfffff', + }, + }).then((taken) => { + assert.ok(taken) + done() + }) + .catch(done) + }) + }) +}) diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js new file mode 100644 index 000000000..f1d67f64e --- /dev/null +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -0,0 +1,463 @@ +const assert = require('assert') +const ethUtil = require('ethereumjs-util') +const EthTx = require('ethereumjs-tx') +const EthjsQuery = require('ethjs-query') +const ObservableStore = require('obs-store') +const sinon = require('sinon') +const TransactionController = require('../../../../../app/scripts/controllers/transactions') +const TxGasUtils = require('../../../../../app/scripts/controllers/transactions/tx-gas-utils') +const { createTestProviderTools, getTestAccounts } = require('../../../../stub/provider') + +const noop = () => true +const currentNetworkId = 42 +const otherNetworkId = 36 + + +describe('Transaction Controller', function () { + let txController, provider, providerResultStub, query, fromAccount + + beforeEach(function () { + providerResultStub = { + // 1 gwei + eth_gasPrice: '0x0de0b6b3a7640000', + // by default, all accounts are external accounts (not contracts) + eth_getCode: '0x', + } + provider = createTestProviderTools({ scaffold: providerResultStub }).provider + query = new EthjsQuery(provider) + fromAccount = getTestAccounts()[0] + + txController = new TransactionController({ + provider, + networkStore: new ObservableStore(currentNetworkId), + txHistoryLimit: 10, + blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, + signTransaction: (ethTx) => new Promise((resolve) => { + ethTx.sign(fromAccount.key) + resolve() + }), + }) + txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) + }) + + describe('#isNonceTaken', function () { + it('should return true', function (done) { + txController.txStateManager._saveTxList([ + { id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} }, + { id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} }, + { id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} }, + ]) + txController.isNonceTaken({txParams: {nonce:0, from:'0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'}}) + .then((isNonceTaken) => { + assert(isNonceTaken) + done() + }).catch(done) + + }) + it('should return false', function (done) { + txController.txStateManager._saveTxList([ + { id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} }, + { id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} }, + { id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} }, + ]) + + txController.isNonceTaken({txParams: {nonce:0, from:'0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'}}) + .then((isNonceTaken) => { + assert(!isNonceTaken) + done() + }).catch(done) + + }) + }) + + describe('#getState', function () { + it('should return a state object with the right keys and datat types', function () { + const exposedState = txController.getState() + assert('unapprovedTxs' in exposedState, 'state should have the key unapprovedTxs') + assert('selectedAddressTxList' in exposedState, 'state should have the key selectedAddressTxList') + assert(typeof exposedState.unapprovedTxs === 'object', 'should be an object') + assert(Array.isArray(exposedState.selectedAddressTxList), 'should be an array') + }) + }) + + describe('#getUnapprovedTxCount', function () { + it('should return the number of unapproved txs', function () { + txController.txStateManager._saveTxList([ + { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 2, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, + ]) + const unapprovedTxCount = txController.getUnapprovedTxCount() + assert.equal(unapprovedTxCount, 3, 'should be 3') + }) + }) + + describe('#getPendingTxCount', function () { + it('should return the number of pending txs', function () { + txController.txStateManager._saveTxList([ + { id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} }, + ]) + const pendingTxCount = txController.getPendingTxCount() + assert.equal(pendingTxCount, 3, 'should be 3') + }) + }) + + describe('#getConfirmedTransactions', function () { + let address + beforeEach(function () { + address = '0xc684832530fcbddae4b4230a47e991ddcec2831d' + const txParams = { + 'from': address, + 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', + } + txController.txStateManager._saveTxList([ + {id: 0, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, + {id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, + {id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams}, + {id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams}, + {id: 4, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams}, + {id: 5, status: 'approved', metamaskNetworkId: currentNetworkId, txParams}, + {id: 6, status: 'signed', metamaskNetworkId: currentNetworkId, txParams}, + {id: 7, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams}, + {id: 8, status: 'failed', metamaskNetworkId: currentNetworkId, txParams}, + ]) + }) + + it('should return the number of confirmed txs', function () { + assert.equal(txController.nonceTracker.getConfirmedTransactions(address).length, 3) + }) + }) + + + describe('#newUnapprovedTransaction', function () { + let stub, txMeta, txParams + beforeEach(function () { + txParams = { + 'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d', + 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', + } + txMeta = { + status: 'unapproved', + id: 1, + metamaskNetworkId: currentNetworkId, + txParams, + history: [], + } + txController.txStateManager._saveTxList([txMeta]) + stub = sinon.stub(txController, 'addUnapprovedTransaction').callsFake(() => { + txController.emit('newUnapprovedTx', txMeta) + return Promise.resolve(txController.txStateManager.addTx(txMeta)) + }) + + afterEach(function () { + txController.txStateManager._saveTxList([]) + stub.restore() + }) + }) + + it('should resolve when finished and status is submitted and resolve with the hash', function (done) { + txController.once('newUnapprovedTx', (txMetaFromEmit) => { + setTimeout(() => { + txController.setTxHash(txMetaFromEmit.id, '0x0') + txController.txStateManager.setTxStatusSubmitted(txMetaFromEmit.id) + }, 10) + }) + + txController.newUnapprovedTransaction(txParams) + .then((hash) => { + assert(hash, 'newUnapprovedTransaction needs to return the hash') + done() + }) + .catch(done) + }) + + it('should reject when finished and status is rejected', function (done) { + txController.once('newUnapprovedTx', (txMetaFromEmit) => { + setTimeout(() => { + txController.txStateManager.setTxStatusRejected(txMetaFromEmit.id) + }, 10) + }) + + txController.newUnapprovedTransaction(txParams) + .catch((err) => { + if (err.message === 'MetaMask Tx Signature: User denied transaction signature.') done() + else done(err) + }) + }) + }) + + describe('#addUnapprovedTransaction', function () { + + it('should add an unapproved transaction and return a valid txMeta', function (done) { + txController.addUnapprovedTransaction({ from: '0x1678a085c290ebd122dc42cba69373b5953b831d' }) + .then((txMeta) => { + assert(('id' in txMeta), 'should have a id') + assert(('time' in txMeta), 'should have a time stamp') + assert(('metamaskNetworkId' in txMeta), 'should have a metamaskNetworkId') + assert(('txParams' in txMeta), 'should have a txParams') + assert(('history' in txMeta), 'should have a history') + + const memTxMeta = txController.txStateManager.getTx(txMeta.id) + assert.deepEqual(txMeta, memTxMeta, `txMeta should be stored in txController after adding it\n expected: ${txMeta} \n got: ${memTxMeta}`) + done() + }).catch(done) + }) + + it('should emit newUnapprovedTx event and pass txMeta as the first argument', function (done) { + providerResultStub.eth_gasPrice = '4a817c800' + txController.once('newUnapprovedTx', (txMetaFromEmit) => { + assert(txMetaFromEmit, 'txMeta is falsey') + done() + }) + txController.addUnapprovedTransaction({ from: '0x1678a085c290ebd122dc42cba69373b5953b831d' }) + .catch(done) + }) + + }) + + describe('#addTxGasDefaults', function () { + it('should add the tx defaults if their are none', function (done) { + const txMeta = { + 'txParams': { + 'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d', + 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', + }, + } + providerResultStub.eth_gasPrice = '4a817c800' + providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' } + providerResultStub.eth_estimateGas = '5209' + txController.addTxGasDefaults(txMeta) + .then((txMetaWithDefaults) => { + assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value') + assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price') + assert(txMetaWithDefaults.txParams.gas, 'should have added the gas field') + done() + }) + .catch(done) + }) + }) + + describe('#addTx', function () { + it('should emit updates', function (done) { + const txMeta = { + id: '1', + status: 'unapproved', + metamaskNetworkId: currentNetworkId, + txParams: {}, + } + + const eventNames = ['update:badge', '1:unapproved'] + const listeners = [] + eventNames.forEach((eventName) => { + listeners.push(new Promise((resolve) => { + txController.once(eventName, (arg) => { + resolve(arg) + }) + })) + }) + Promise.all(listeners) + .then((returnValues) => { + assert.deepEqual(returnValues.pop(), txMeta, 'last event 1:unapproved should return txMeta') + done() + }) + .catch(done) + txController.addTx(txMeta) + }) + }) + + describe('#approveTransaction', function () { + let txMeta, originalValue + + beforeEach(function () { + originalValue = '0x01' + txMeta = { + id: '1', + status: 'unapproved', + metamaskNetworkId: currentNetworkId, + txParams: { + nonce: originalValue, + gas: originalValue, + gasPrice: originalValue, + }, + } + }) + + + it('does not overwrite set values', function (done) { + this.timeout(15000) + const wrongValue = '0x05' + + txController.addTx(txMeta) + providerResultStub.eth_gasPrice = wrongValue + providerResultStub.eth_estimateGas = '0x5209' + + const signStub = sinon.stub(txController, 'signTransaction').callsFake(() => Promise.resolve()) + + const pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => { + txController.setTxHash('1', originalValue) + txController.txStateManager.setTxStatusSubmitted('1') + }) + + txController.approveTransaction(txMeta.id).then(() => { + const result = txController.txStateManager.getTx(txMeta.id) + const params = result.txParams + + assert.equal(params.gas, originalValue, 'gas unmodified') + assert.equal(params.gasPrice, originalValue, 'gas price unmodified') + assert.equal(result.hash, originalValue, `hash was set \n got: ${result.hash} \n expected: ${originalValue}`) + signStub.restore() + pubStub.restore() + done() + }).catch(done) + }) + }) + + describe('#sign replay-protected tx', function () { + it('prepares a tx with the chainId set', function (done) { + txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txController.signTransaction('1').then((rawTx) => { + const ethTx = new EthTx(ethUtil.toBuffer(rawTx)) + assert.equal(ethTx.getChainId(), currentNetworkId) + done() + }).catch(done) + }) + }) + + describe('#updateAndApproveTransaction', function () { + let txMeta + beforeEach(() => { + txMeta = { + id: 1, + status: 'unapproved', + txParams: { + from: fromAccount.address, + to: '0x1678a085c290ebd122dc42cba69373b5953b831d', + gasPrice: '0x77359400', + gas: '0x7b0d', + nonce: '0x4b', + }, + metamaskNetworkId: currentNetworkId, + } + }) + it('should update and approve transactions', async () => { + txController.txStateManager.addTx(txMeta) + const approvalPromise = txController.updateAndApproveTransaction(txMeta) + const tx = txController.txStateManager.getTx(1) + assert.equal(tx.status, 'approved') + await approvalPromise + }) + }) + + describe('#getChainId', function () { + it('returns 0 when the chainId is NaN', function () { + txController.networkStore = new ObservableStore(NaN) + assert.equal(txController.getChainId(), 0) + }) + }) + + describe('#cancelTransaction', function () { + beforeEach(function () { + txController.txStateManager._saveTxList([ + { id: 0, status: 'unapproved', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 1, status: 'rejected', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 2, status: 'approved', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 3, status: 'signed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 4, status: 'submitted', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 5, status: 'confirmed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + { id: 6, status: 'failed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] }, + ]) + }) + + it('should set the transaction to rejected from unapproved', async function () { + await txController.cancelTransaction(0) + assert.equal(txController.txStateManager.getTx(0).status, 'rejected') + }) + + }) + + describe('#publishTransaction', function () { + let hash, txMeta + beforeEach(function () { + hash = '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8' + txMeta = { + id: 1, + status: 'unapproved', + txParams: {}, + metamaskNetworkId: currentNetworkId, + } + providerResultStub.eth_sendRawTransaction = hash + }) + + it('should publish a tx, updates the rawTx when provided a one', async function () { + txController.txStateManager.addTx(txMeta) + await txController.publishTransaction(txMeta.id) + const publishedTx = txController.txStateManager.getTx(1) + assert.equal(publishedTx.hash, hash) + assert.equal(publishedTx.status, 'submitted') + }) + }) + + describe('#retryTransaction', function () { + it('should create a new txMeta with the same txParams as the original one', function (done) { + let txParams = { + nonce: '0x00', + from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4', + to: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4', + data: '0x0', + } + txController.txStateManager._saveTxList([ + { id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams }, + ]) + txController.retryTransaction(1) + .then((txMeta) => { + assert.equal(txMeta.txParams.nonce, txParams.nonce, 'nonce should be the same') + assert.equal(txMeta.txParams.from, txParams.from, 'from should be the same') + assert.equal(txMeta.txParams.to, txParams.to, 'to should be the same') + assert.equal(txMeta.txParams.data, txParams.data, 'data should be the same') + assert.ok(('lastGasPrice' in txMeta), 'should have the key `lastGasPrice`') + assert.equal(txController.txStateManager.getTxList().length, 2) + done() + }).catch(done) + }) + }) + + describe('#_markNonceDuplicatesDropped', function () { + it('should mark all nonce duplicates as dropped without marking the confirmed transaction as dropped', function () { + txController.txStateManager._saveTxList([ + { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } }, + { id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } }, + { id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } }, + { id: 4, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } }, + { id: 5, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } }, + { id: 6, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } }, + { id: 7, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } }, + ]) + txController._markNonceDuplicatesDropped(1) + const confirmedTx = txController.txStateManager.getTx(1) + const droppedTxs = txController.txStateManager.getFilteredTxList({ nonce: '0x01', status: 'dropped' }) + assert.equal(confirmedTx.status, 'confirmed', 'the confirmedTx should remain confirmed') + assert.equal(droppedTxs.length, 6, 'their should be 6 dropped txs') + + }) + }) + + describe('#getPendingTransactions', function () { + beforeEach(function () { + txController.txStateManager._saveTxList([ + { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 2, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 3, status: 'approved', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 4, status: 'signed', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 5, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 6, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, + { id: 7, status: 'failed', metamaskNetworkId: currentNetworkId, txParams: {} }, + ]) + }) + it('should show only submitted transactions as pending transasction', function () { + assert(txController.pendingTxTracker.getPendingTransactions().length, 1) + assert(txController.pendingTxTracker.getPendingTransactions()[0].status, 'submitted') + }) + }) +}) diff --git a/test/unit/app/controllers/transactions/tx-gas-util-test.js b/test/unit/app/controllers/transactions/tx-gas-util-test.js new file mode 100644 index 000000000..d1ee86033 --- /dev/null +++ b/test/unit/app/controllers/transactions/tx-gas-util-test.js @@ -0,0 +1,77 @@ +const assert = require('assert') +const Transaction = require('ethereumjs-tx') +const BN = require('bn.js') + + +const { hexToBn, bnToHex } = require('../../../../../app/scripts/lib/util') +const TxUtils = require('../../../../../app/scripts/controllers/transactions/tx-gas-utils') + + +describe('txUtils', function () { + let txUtils + + before(function () { + txUtils = new TxUtils(new Proxy({}, { + get: (obj, name) => { + return () => {} + }, + })) + }) + + describe('chain Id', function () { + it('prepares a transaction with the provided chainId', function () { + const txParams = { + to: '0x70ad465e0bab6504002ad58c744ed89c7da38524', + from: '0x69ad465e0bab6504002ad58c744ed89c7da38525', + value: '0x0', + gas: '0x7b0c', + gasPrice: '0x199c82cc00', + data: '0x', + nonce: '0x3', + chainId: 42, + } + const ethTx = new Transaction(txParams) + assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params') + }) + }) + + describe('addGasBuffer', function () { + it('multiplies by 1.5, when within block gas limit', function () { + // naive estimatedGas: 0x16e360 (1.5 mil) + const inputHex = '0x16e360' + // dummy gas limit: 0x3d4c52 (4 mil) + const blockGasLimitHex = '0x3d4c52' + const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex) + const inputBn = hexToBn(inputHex) + const outputBn = hexToBn(output) + const expectedBn = inputBn.muln(1.5) + assert(outputBn.eq(expectedBn), 'returns 1.5 the input value') + }) + + it('uses original estimatedGas, when above block gas limit', function () { + // naive estimatedGas: 0x16e360 (1.5 mil) + const inputHex = '0x16e360' + // dummy gas limit: 0x0f4240 (1 mil) + const blockGasLimitHex = '0x0f4240' + const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex) + // const inputBn = hexToBn(inputHex) + const outputBn = hexToBn(output) + const expectedBn = hexToBn(inputHex) + assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value') + }) + + it('buffers up to recommend gas limit recommended ceiling', function () { + // naive estimatedGas: 0x16e360 (1.5 mil) + const inputHex = '0x16e360' + // dummy gas limit: 0x1e8480 (2 mil) + const blockGasLimitHex = '0x1e8480' + const blockGasLimitBn = hexToBn(blockGasLimitHex) + const ceilGasLimitBn = blockGasLimitBn.muln(0.9) + const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex) + // const inputBn = hexToBn(inputHex) + // const outputBn = hexToBn(output) + const expectedHex = bnToHex(ceilGasLimitBn) + assert.equal(output, expectedHex, 'returns the gas limit recommended ceiling value') + }) + }) +}) diff --git a/test/unit/app/controllers/transactions/tx-helper-test.js b/test/unit/app/controllers/transactions/tx-helper-test.js new file mode 100644 index 000000000..ce54ef483 --- /dev/null +++ b/test/unit/app/controllers/transactions/tx-helper-test.js @@ -0,0 +1,17 @@ +const assert = require('assert') +const txHelper = require('../../../../../ui/lib/tx-helper') + +describe('txHelper', function () { + it('always shows the oldest tx first', function () { + const metamaskNetworkId = 1 + const txs = { + a: { metamaskNetworkId, time: 3 }, + b: { metamaskNetworkId, time: 1 }, + c: { metamaskNetworkId, time: 2 }, + } + + const sorted = txHelper(txs, null, null, metamaskNetworkId) + assert.equal(sorted[0].time, 1, 'oldest tx first') + assert.equal(sorted[2].time, 3, 'newest tx last') + }) +}) diff --git a/test/unit/app/controllers/transactions/tx-state-history-helper-test.js b/test/unit/app/controllers/transactions/tx-state-history-helper-test.js new file mode 100644 index 000000000..f4c3a6be1 --- /dev/null +++ b/test/unit/app/controllers/transactions/tx-state-history-helper-test.js @@ -0,0 +1,129 @@ +const assert = require('assert') +const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper') +const testVault = require('../../../../data/v17-long-history.json') + +describe ('Transaction state history helper', function () { + + describe('#snapshotFromTxMeta', function () { + it('should clone deep', function () { + const input = { + foo: { + bar: { + bam: 'baz' + } + } + } + const output = txStateHistoryHelper.snapshotFromTxMeta(input) + assert('foo' in output, 'has a foo key') + assert('bar' in output.foo, 'has a bar key') + assert('bam' in output.foo.bar, 'has a bar key') + assert.equal(output.foo.bar.bam, 'baz', 'has a baz value') + }) + + it('should remove the history key', function () { + const input = { foo: 'bar', history: 'remembered' } + const output = txStateHistoryHelper.snapshotFromTxMeta(input) + assert(typeof output.history, 'undefined', 'should remove history') + }) + }) + + describe('#migrateFromSnapshotsToDiffs', function () { + it('migrates history to diffs and can recover original values', function () { + testVault.data.TransactionController.transactions.forEach((tx, index) => { + const newHistory = txStateHistoryHelper.migrateFromSnapshotsToDiffs(tx.history) + newHistory.forEach((newEntry, index) => { + if (index === 0) { + assert.equal(Array.isArray(newEntry), false, 'initial history item IS NOT a json patch obj') + } else { + assert.equal(Array.isArray(newEntry), true, 'non-initial history entry IS a json patch obj') + } + const oldEntry = tx.history[index] + const historySubset = newHistory.slice(0, index + 1) + const reconstructedValue = txStateHistoryHelper.replayHistory(historySubset) + assert.deepEqual(oldEntry, reconstructedValue, 'was able to reconstruct old entry from diffs') + }) + }) + }) + }) + + describe('#replayHistory', function () { + it('replaying history does not mutate the original obj', function () { + const initialState = { test: true, message: 'hello', value: 1 } + const diff1 = [{ + "op": "replace", + "path": "/message", + "value": "haay", + }] + const diff2 = [{ + "op": "replace", + "path": "/value", + "value": 2, + }] + const history = [initialState, diff1, diff2] + + const beforeStateSnapshot = JSON.stringify(initialState) + const latestState = txStateHistoryHelper.replayHistory(history) + const afterStateSnapshot = JSON.stringify(initialState) + + assert.notEqual(initialState, latestState, 'initial state is not the same obj as the latest state') + assert.equal(beforeStateSnapshot, afterStateSnapshot, 'initial state is not modified during run') + }) + }) + + describe('#generateHistoryEntry', function () { + + function generateHistoryEntryTest(note) { + + const prevState = { + someValue: 'value 1', + foo: { + bar: { + bam: 'baz' + } + } + } + + const nextState = { + newPropRoot: 'new property - root', + someValue: 'value 2', + foo: { + newPropFirstLevel: 'new property - first level', + bar: { + bam: 'baz' + } + } + } + + const before = new Date().getTime() + const result = txStateHistoryHelper.generateHistoryEntry(prevState, nextState, note) + const after = new Date().getTime() + + assert.ok(Array.isArray(result)) + assert.equal(result.length, 3) + + const expectedEntry1 = { op: 'add', path: '/foo/newPropFirstLevel', value: 'new property - first level' } + assert.equal(result[0].op, expectedEntry1.op) + assert.equal(result[0].path, expectedEntry1.path) + assert.equal(result[0].value, expectedEntry1.value) + assert.equal(result[0].value, expectedEntry1.value) + if (note) + assert.equal(result[0].note, note) + + assert.ok(result[0].timestamp >= before && result[0].timestamp <= after) + + const expectedEntry2 = { op: 'replace', path: '/someValue', value: 'value 2' } + assert.deepEqual(result[1], expectedEntry2) + + const expectedEntry3 = { op: 'add', path: '/newPropRoot', value: 'new property - root' } + assert.deepEqual(result[2], expectedEntry3) + } + + it('should generate history entries', function () { + generateHistoryEntryTest() + }) + + it('should add note to first entry', function () { + generateHistoryEntryTest('custom note') + }) + }) +}) \ No newline at end of file diff --git a/test/unit/app/controllers/transactions/tx-state-manager-test.js b/test/unit/app/controllers/transactions/tx-state-manager-test.js new file mode 100644 index 000000000..20bc08b94 --- /dev/null +++ b/test/unit/app/controllers/transactions/tx-state-manager-test.js @@ -0,0 +1,291 @@ +const assert = require('assert') +const clone = require('clone') +const ObservableStore = require('obs-store') +const TxStateManager = require('../../../../../app/scripts/controllers/transactions/tx-state-manager') +const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper') +const noop = () => true + +describe('TransactionStateManager', function () { + let txStateManager + const currentNetworkId = 42 + const otherNetworkId = 2 + + beforeEach(function () { + txStateManager = new TxStateManager({ + initState: { + transactions: [], + }, + txHistoryLimit: 10, + getNetwork: () => currentNetworkId + }) + }) + + describe('#setTxStatusSigned', function () { + it('sets the tx status to signed', function () { + let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txStateManager.addTx(tx, noop) + txStateManager.setTxStatusSigned(1) + let result = txStateManager.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].status, 'signed') + }) + + it('should emit a signed event to signal the exciton of callback', (done) => { + let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + const noop = function () { + assert(true, 'event listener has been triggered and noop executed') + done() + } + txStateManager.addTx(tx) + txStateManager.on('1:signed', noop) + txStateManager.setTxStatusSigned(1) + + }) + }) + + describe('#setTxStatusRejected', function () { + it('sets the tx status to rejected', function () { + let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txStateManager.addTx(tx) + txStateManager.setTxStatusRejected(1) + let result = txStateManager.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].status, 'rejected') + }) + + it('should emit a rejected event to signal the exciton of callback', (done) => { + let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txStateManager.addTx(tx) + const noop = function (err, txId) { + assert(true, 'event listener has been triggered and noop executed') + done() + } + txStateManager.on('1:rejected', noop) + txStateManager.setTxStatusRejected(1) + }) + }) + + describe('#getFullTxList', function () { + it('when new should return empty array', function () { + let result = txStateManager.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 0) + }) + }) + + describe('#getTxList', function () { + it('when new should return empty array', function () { + let result = txStateManager.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 0) + }) + }) + + describe('#addTx', function () { + it('adds a tx returned in getTxList', function () { + let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + txStateManager.addTx(tx, noop) + let result = txStateManager.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].id, 1) + }) + + it('does not override txs from other networks', function () { + let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + let tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} } + txStateManager.addTx(tx, noop) + txStateManager.addTx(tx2, noop) + let result = txStateManager.getFullTxList() + let result2 = txStateManager.getTxList() + assert.equal(result.length, 2, 'txs were deleted') + assert.equal(result2.length, 1, 'incorrect number of txs on network.') + }) + + it('cuts off early txs beyond a limit', function () { + const limit = txStateManager.txHistoryLimit + for (let i = 0; i < limit + 1; i++) { + const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + txStateManager.addTx(tx, noop) + } + let result = txStateManager.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 1, 'early txs truncted') + }) + + it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () { + const limit = txStateManager.txHistoryLimit + for (let i = 0; i < limit + 1; i++) { + const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} } + txStateManager.addTx(tx, noop) + } + let result = txStateManager.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 1, 'early txs truncted') + }) + + it('cuts off early txs beyond a limit but does not cut unapproved txs', function () { + let unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } + txStateManager.addTx(unconfirmedTx, noop) + const limit = txStateManager.txHistoryLimit + for (let i = 1; i < limit + 1; i++) { + const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } + txStateManager.addTx(tx, noop) + } + let result = txStateManager.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 0, 'first tx should still be there') + assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved') + assert.equal(result[1].id, 2, 'early txs truncted') + }) + }) + + describe('#updateTx', function () { + it('replaces the tx with the same id', function () { + txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + const txMeta = txStateManager.getTx('1') + txMeta.hash = 'foo' + txStateManager.updateTx(txMeta) + let result = txStateManager.getTx('1') + assert.equal(result.hash, 'foo') + }) + + it('updates gas price and adds history items', function () { + const originalGasPrice = '0x01' + const desiredGasPrice = '0x02' + + const txMeta = { + id: '1', + status: 'unapproved', + metamaskNetworkId: currentNetworkId, + txParams: { + gasPrice: originalGasPrice, + }, + } + + const updatedMeta = clone(txMeta) + + txStateManager.addTx(txMeta) + const updatedTx = txStateManager.getTx('1') + // verify tx was initialized correctly + assert.equal(updatedTx.history.length, 1, 'one history item (initial)') + assert.equal(Array.isArray(updatedTx.history[0]), false, 'first history item is initial state') + assert.deepEqual(updatedTx.history[0], txStateHistoryHelper.snapshotFromTxMeta(updatedTx), 'first history item is initial state') + // modify value and updateTx + updatedTx.txParams.gasPrice = desiredGasPrice + const before = new Date().getTime() + txStateManager.updateTx(updatedTx) + const after = new Date().getTime() + // check updated value + const result = txStateManager.getTx('1') + assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated') + // validate history was updated + assert.equal(result.history.length, 2, 'two history items (initial + diff)') + assert.equal(result.history[1].length, 1, 'two history state items (initial + diff)') + + const expectedEntry = { op: 'replace', path: '/txParams/gasPrice', value: desiredGasPrice } + assert.deepEqual(result.history[1][0].op, expectedEntry.op, 'two history items (initial + diff) operation') + assert.deepEqual(result.history[1][0].path, expectedEntry.path, 'two history items (initial + diff) path') + assert.deepEqual(result.history[1][0].value, expectedEntry.value, 'two history items (initial + diff) value') + assert.ok(result.history[1][0].timestamp >= before && result.history[1][0].timestamp <= after) + }) + }) + + describe('#getUnapprovedTxList', function () { + it('returns unapproved txs in a hash', function () { + txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + const result = txStateManager.getUnapprovedTxList() + assert.equal(typeof result, 'object') + assert.equal(result['1'].status, 'unapproved') + assert.equal(result['2'], undefined) + }) + }) + + describe('#getTx', function () { + it('returns a tx with the requested id', function () { + txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) + assert.equal(txStateManager.getTx('1').status, 'unapproved') + assert.equal(txStateManager.getTx('2').status, 'confirmed') + }) + }) + + describe('#getFilteredTxList', function () { + it('returns a tx with the requested data', function () { + const txMetas = [ + { id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, + { id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + { id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, + ] + txMetas.forEach((txMeta) => txStateManager.addTx(txMeta, noop)) + let filterParams + + filterParams = { status: 'unapproved', from: '0xaa' } + assert.equal(txStateManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { status: 'unapproved', to: '0xaa' } + assert.equal(txStateManager.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { status: 'confirmed', from: '0xbb' } + assert.equal(txStateManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { status: 'confirmed' } + assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { from: '0xaa' } + assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + filterParams = { to: '0xaa' } + assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) + }) + }) + + describe('#wipeTransactions', function () { + + const specificAddress = '0xaa' + const otherAddress = '0xbb' + + it('should remove only the transactions from a specific address', function () { + + const txMetas = [ + { id: 0, status: 'unapproved', txParams: { from: specificAddress, to: otherAddress }, metamaskNetworkId: currentNetworkId }, + { id: 1, status: 'confirmed', txParams: { from: otherAddress, to: specificAddress }, metamaskNetworkId: currentNetworkId }, + { id: 2, status: 'confirmed', txParams: { from: otherAddress, to: specificAddress }, metamaskNetworkId: currentNetworkId }, + ] + txMetas.forEach((txMeta) => txStateManager.addTx(txMeta, noop)) + + txStateManager.wipeTransactions(specificAddress) + + const transactionsFromCurrentAddress = txStateManager.getTxList().filter((txMeta) => txMeta.txParams.from === specificAddress) + const transactionsFromOtherAddresses = txStateManager.getTxList().filter((txMeta) => txMeta.txParams.from !== specificAddress) + + assert.equal(transactionsFromCurrentAddress.length, 0) + assert.equal(transactionsFromOtherAddresses.length, 2) + }) + + it('should not remove the transactions from other networks', function () { + const txMetas = [ + { id: 0, status: 'unapproved', txParams: { from: specificAddress, to: otherAddress }, metamaskNetworkId: currentNetworkId }, + { id: 1, status: 'confirmed', txParams: { from: specificAddress, to: otherAddress }, metamaskNetworkId: otherNetworkId }, + { id: 2, status: 'confirmed', txParams: { from: specificAddress, to: otherAddress }, metamaskNetworkId: otherNetworkId }, + ] + + txMetas.forEach((txMeta) => txStateManager.addTx(txMeta, noop)) + + txStateManager.wipeTransactions(specificAddress) + + const txsFromCurrentNetworkAndAddress = txStateManager.getTxList().filter((txMeta) => txMeta.txParams.from === specificAddress) + const txFromOtherNetworks = txStateManager.getFullTxList().filter((txMeta) => txMeta.metamaskNetworkId === otherNetworkId) + + assert.equal(txsFromCurrentNetworkAndAddress.length, 0) + assert.equal(txFromOtherNetworks.length, 2) + + }) + }) +}) diff --git a/test/unit/app/controllers/transactions/tx-utils-test.js b/test/unit/app/controllers/transactions/tx-utils-test.js new file mode 100644 index 000000000..115127f85 --- /dev/null +++ b/test/unit/app/controllers/transactions/tx-utils-test.js @@ -0,0 +1,98 @@ +const assert = require('assert') +const txUtils = require('../../../../../app/scripts/controllers/transactions/lib/util') + + +describe('txUtils', function () { + describe('#validateTxParams', function () { + it('does not throw for positive values', function () { + var sample = { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + value: '0x01', + } + txUtils.validateTxParams(sample) + }) + + it('returns error for negative values', function () { + var sample = { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + value: '-0x01', + } + try { + txUtils.validateTxParams(sample) + } catch (err) { + assert.ok(err, 'error') + } + }) + }) + + describe('#normalizeTxParams', () => { + it('should normalize txParams', () => { + let txParams = { + chainId: '0x1', + from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402', + to: null, + data: '68656c6c6f20776f726c64', + random: 'hello world', + } + + let normalizedTxParams = txUtils.normalizeTxParams(txParams) + + assert(!normalizedTxParams.chainId, 'their should be no chainId') + assert(!normalizedTxParams.to, 'their should be no to address if null') + assert.equal(normalizedTxParams.from.slice(0, 2), '0x', 'from should be hexPrefixd') + assert.equal(normalizedTxParams.data.slice(0, 2), '0x', 'data should be hexPrefixd') + assert(!('random' in normalizedTxParams), 'their should be no random key in normalizedTxParams') + + txParams.to = 'a7df1beDBF813f57096dF77FCd515f0B3900e402' + normalizedTxParams = txUtils.normalizeTxParams(txParams) + assert.equal(normalizedTxParams.to.slice(0, 2), '0x', 'to should be hexPrefixd') + + }) + }) + + describe('#validateRecipient', () => { + it('removes recipient for txParams with 0x when contract data is provided', function () { + const zeroRecipientandDataTxParams = { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + to: '0x', + data: 'bytecode', + } + const sanitizedTxParams = txUtils.validateRecipient(zeroRecipientandDataTxParams) + assert.deepEqual(sanitizedTxParams, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', data: 'bytecode' }, 'no recipient with 0x') + }) + + it('should error when recipient is 0x', function () { + const zeroRecipientTxParams = { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + to: '0x', + } + assert.throws(() => { txUtils.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address') + }) + }) + + + describe('#validateFrom', () => { + it('should error when from is not a hex string', function () { + + // where from is undefined + const txParams = {} + assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) + + // where from is array + txParams.from = [] + assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) + + // where from is a object + txParams.from = {} + assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) + + // where from is a invalid address + txParams.from = 'im going to fail' + assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address`) + + // should run + txParams.from ='0x1678a085c290ebd122dc42cba69373b5953b831d' + txUtils.validateFrom(txParams) + }) + }) +}) -- cgit