From 2d7c3c2b00a698b19ac015624154c3c1cd2619b2 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 6 Apr 2018 11:07:20 -0700 Subject: [PATCH 1/9] meta - transactions - create a transactions dir in controller and move relevant files into it --- .../index.js} | 68 +-------- .../lib/tx-state-history-helper.js | 0 .../controllers/transactions/lib/util.js | 66 ++++++++ .../transactions}/nonce-tracker.js | 0 .../transactions}/pending-tx-tracker.js | 0 .../transactions}/tx-gas-utils.js | 2 +- .../transactions}/tx-state-manager.js | 24 ++- app/scripts/migrations/018.js | 2 +- test/unit/nonce-tracker-test.js | 2 +- test/unit/pending-tx-test.js | 2 +- test/unit/tx-controller-test.js | 95 +----------- test/unit/tx-gas-util-test.js | 83 ++++++++-- test/unit/tx-state-history-helper-test.js | 2 +- test/unit/tx-state-history-helper.js | 2 +- test/unit/tx-state-manager-test.js | 4 +- test/unit/tx-utils-test.js | 143 ++++++++++-------- 16 files changed, 255 insertions(+), 240 deletions(-) rename app/scripts/controllers/{transactions.js => transactions/index.js} (84%) rename app/scripts/{ => controllers/transactions}/lib/tx-state-history-helper.js (100%) create mode 100644 app/scripts/controllers/transactions/lib/util.js rename app/scripts/{lib => controllers/transactions}/nonce-tracker.js (100%) rename app/scripts/{lib => controllers/transactions}/pending-tx-tracker.js (100%) rename app/scripts/{lib => controllers/transactions}/tx-gas-utils.js (99%) rename app/scripts/{lib => controllers/transactions}/tx-state-manager.js (93%) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions/index.js similarity index 84% rename from app/scripts/controllers/transactions.js rename to app/scripts/controllers/transactions/index.js index 336b0d8f7..6f66e3a1e 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions/index.js @@ -3,11 +3,11 @@ const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') const EthQuery = require('ethjs-query') -const TransactionStateManager = require('../lib/tx-state-manager') -const TxGasUtil = require('../lib/tx-gas-utils') -const PendingTransactionTracker = require('../lib/pending-tx-tracker') -const NonceTracker = require('../lib/nonce-tracker') - +const TransactionStateManager = require('./tx-state-manager') +const TxGasUtil = require('./tx-gas-utils') +const PendingTransactionTracker = require('./pending-tx-tracker') +const NonceTracker = require('./nonce-tracker') +const txUtils = require('./lib/util') /* Transaction Controller is an aggregate of sub-controllers and trackers composing them in a way to be exposed to the metamask controller @@ -185,8 +185,8 @@ module.exports = class TransactionController extends EventEmitter { async addUnapprovedTransaction (txParams) { // validate - const normalizedTxParams = this._normalizeTxParams(txParams) - this._validateTxParams(normalizedTxParams) + const normalizedTxParams = txUtils.normalizeTxParams(txParams) + txUtils.validateTxParams(normalizedTxParams) // construct txMeta let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams }) this.addTx(txMeta) @@ -314,60 +314,6 @@ module.exports = class TransactionController extends EventEmitter { // PRIVATE METHODS // - _normalizeTxParams (txParams) { - // functions that handle normalizing of that key in txParams - const whiteList = { - from: from => ethUtil.addHexPrefix(from).toLowerCase(), - to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(), - nonce: nonce => ethUtil.addHexPrefix(nonce), - value: value => ethUtil.addHexPrefix(value), - data: data => ethUtil.addHexPrefix(data), - gas: gas => ethUtil.addHexPrefix(gas), - gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice), - } - - // apply only keys in the whiteList - const normalizedTxParams = {} - Object.keys(whiteList).forEach((key) => { - if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key]) - }) - - return normalizedTxParams - } - - _validateTxParams (txParams) { - this._validateFrom(txParams) - this._validateRecipient(txParams) - if ('value' in txParams) { - const value = txParams.value.toString() - if (value.includes('-')) { - throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) - } - - if (value.includes('.')) { - throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`) - } - } - } - - _validateFrom (txParams) { - if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`) - if (!ethUtil.isValidAddress(txParams.from)) throw new Error('Invalid from address') - } - - _validateRecipient (txParams) { - if (txParams.to === '0x' || txParams.to === null ) { - if (txParams.data) { - delete txParams.to - } else { - throw new Error('Invalid recipient address') - } - } else if ( txParams.to !== undefined && !ethUtil.isValidAddress(txParams.to) ) { - throw new Error('Invalid recipient address') - } - return txParams - } - _markNonceDuplicatesDropped (txId) { this.txStateManager.setTxStatusConfirmed(txId) // get the confirmed transactions nonce and from address diff --git a/app/scripts/lib/tx-state-history-helper.js b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js similarity index 100% rename from app/scripts/lib/tx-state-history-helper.js rename to app/scripts/controllers/transactions/lib/tx-state-history-helper.js diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js new file mode 100644 index 000000000..f403b0758 --- /dev/null +++ b/app/scripts/controllers/transactions/lib/util.js @@ -0,0 +1,66 @@ +const { + addHexPrefix, + isValidAddress, +} = require('ethereumjs-util') + +module.exports = { + normalizeTxParams, + validateTxParams, + validateFrom, + validateRecipient +} + + +function normalizeTxParams (txParams) { + // functions that handle normalizing of that key in txParams + const whiteList = { + from: from => addHexPrefix(from).toLowerCase(), + to: to => addHexPrefix(txParams.to).toLowerCase(), + nonce: nonce => addHexPrefix(nonce), + value: value => addHexPrefix(value), + data: data => addHexPrefix(data), + gas: gas => addHexPrefix(gas), + gasPrice: gasPrice => addHexPrefix(gasPrice), + } + + // apply only keys in the whiteList + const normalizedTxParams = {} + Object.keys(whiteList).forEach((key) => { + if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key]) + }) + + return normalizedTxParams +} + +function validateTxParams (txParams) { + validateFrom(txParams) + validateRecipient(txParams) + if ('value' in txParams) { + const value = txParams.value.toString() + if (value.includes('-')) { + throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) + } + + if (value.includes('.')) { + throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`) + } + } +} + +function validateFrom (txParams) { + if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`) + if (!isValidAddress(txParams.from)) throw new Error('Invalid from address') +} + +function validateRecipient (txParams) { + if (txParams.to === '0x' || txParams.to === null ) { + if (txParams.data) { + delete txParams.to + } else { + throw new Error('Invalid recipient address') + } + } else if ( txParams.to !== undefined && !isValidAddress(txParams.to) ) { + throw new Error('Invalid recipient address') + } + return txParams +} \ No newline at end of file diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js similarity index 100% rename from app/scripts/lib/nonce-tracker.js rename to app/scripts/controllers/transactions/nonce-tracker.js diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js similarity index 100% rename from app/scripts/lib/pending-tx-tracker.js rename to app/scripts/controllers/transactions/pending-tx-tracker.js diff --git a/app/scripts/lib/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js similarity index 99% rename from app/scripts/lib/tx-gas-utils.js rename to app/scripts/controllers/transactions/tx-gas-utils.js index c579e462a..f40542603 100644 --- a/app/scripts/lib/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -3,7 +3,7 @@ const { hexToBn, BnMultiplyByFraction, bnToHex, -} = require('./util') +} = require('../../lib/util') const { addHexPrefix } = require('ethereumjs-util') const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js similarity index 93% rename from app/scripts/lib/tx-state-manager.js rename to app/scripts/controllers/transactions/tx-state-manager.js index c6d10ee62..cb24b8c99 100644 --- a/app/scripts/lib/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -1,9 +1,9 @@ const extend = require('xtend') const EventEmitter = require('events') const ObservableStore = require('obs-store') -const createId = require('./random-id') +const createId = require('../../lib/random-id') const ethUtil = require('ethereumjs-util') -const txStateHistoryHelper = require('./tx-state-history-helper') +const txStateHistoryHelper = require('./lib/tx-state-history-helper') // STATUS METHODS // statuses: @@ -92,7 +92,9 @@ module.exports = class TransactionStateManager extends EventEmitter { // or rejected tx's. // not tx's that are pending or unapproved if (txCount > txHistoryLimit - 1) { - let index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') + let index = transactions.findIndex((metaTx) => { + return this.getFinalStates().includes(metaTx.status) + }) if (index !== -1) { transactions.splice(index, 1) } @@ -258,6 +260,16 @@ module.exports = class TransactionStateManager extends EventEmitter { this._setTxStatus(txId, 'failed') } + // returns an array of states that can be considered final + getFinalStates () { + return [ + 'rejected', // the user has responded no! + 'confirmed', // the tx has been included in a block. + 'failed', // the tx failed for some reason, included on tx data. + 'dropped', // the tx nonce was already used + ] + } + wipeTransactions (address) { // network only tx const txs = this.getFullTxList() @@ -273,9 +285,8 @@ module.exports = class TransactionStateManager extends EventEmitter { // PRIVATE METHODS // - // Should find the tx in the tx list and - // update it. - // should set the status in txData + // STATUS METHODS + // statuses: // - `'unapproved'` the user has not responded // - `'rejected'` the user has responded no! // - `'approved'` the user has approved the tx @@ -283,6 +294,7 @@ module.exports = class TransactionStateManager extends EventEmitter { // - `'submitted'` the tx is sent to a server // - `'confirmed'` the tx has been included in a block. // - `'failed'` the tx failed for some reason, included on tx data. + // - `'dropped'` the tx nonce was already used _setTxStatus (txId, status) { const txMeta = this.getTx(txId) txMeta.status = status diff --git a/app/scripts/migrations/018.js b/app/scripts/migrations/018.js index bea1fe3da..ffbf24a4b 100644 --- a/app/scripts/migrations/018.js +++ b/app/scripts/migrations/018.js @@ -7,7 +7,7 @@ This migration updates "transaction state history" to diffs style */ const clone = require('clone') -const txStateHistoryHelper = require('../lib/tx-state-history-helper') +const txStateHistoryHelper = require('../controllers/transactions/lib/tx-state-history-helper') module.exports = { diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js index 5a27882ef..cf26945d3 100644 --- a/test/unit/nonce-tracker-test.js +++ b/test/unit/nonce-tracker-test.js @@ -1,5 +1,5 @@ const assert = require('assert') -const NonceTracker = require('../../app/scripts/lib/nonce-tracker') +const NonceTracker = require('../../app/scripts/controllers/transactions/nonce-tracker') const MockTxGen = require('../lib/mock-tx-gen') let providerResultStub = {} diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js index f0b4e3bfc..001b86dd1 100644 --- a/test/unit/pending-tx-test.js +++ b/test/unit/pending-tx-test.js @@ -4,7 +4,7 @@ const EthTx = require('ethereumjs-tx') const ObservableStore = require('obs-store') const clone = require('clone') const { createTestProviderTools } = require('../stub/provider') -const PendingTransactionTracker = require('../../app/scripts/lib/pending-tx-tracker') +const PendingTransactionTracker = require('../../app/scripts/controllers/transactions/pending-tx-tracker') const MockTxGen = require('../lib/mock-tx-gen') const sinon = require('sinon') const noop = () => true diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 824574ff2..08f16d83b 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -5,7 +5,7 @@ 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/lib/tx-gas-utils') +const TxGasUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils') const { createTestProviderTools } = require('../stub/provider') const noop = () => true @@ -210,99 +210,6 @@ describe('Transaction Controller', function () { }) }) - describe('#_validateTxParams', function () { - it('does not throw for positive values', function () { - var sample = { - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - value: '0x01', - } - txController._validateTxParams(sample) - }) - - it('returns error for negative values', function () { - var sample = { - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - value: '-0x01', - } - try { - txController._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 = txController._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 = txController._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 = txController._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(() => { txController._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(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) - - // where from is array - txParams.from = [] - assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) - - // where from is a object - txParams.from = {} - assert.throws(() => { txController._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(() => { txController._validateFrom(txParams) }, Error, `Invalid from address`) - - // should run - txParams.from ='0x1678a085c290ebd122dc42cba69373b5953b831d' - txController._validateFrom(txParams) - }) - }) - describe('#addTx', function () { it('should emit updates', function (done) { const txMeta = { diff --git a/test/unit/tx-gas-util-test.js b/test/unit/tx-gas-util-test.js index 40ea8a7d6..c1d5966da 100644 --- a/test/unit/tx-gas-util-test.js +++ b/test/unit/tx-gas-util-test.js @@ -1,14 +1,77 @@ const assert = require('assert') -const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils') -const { createTestProviderTools } = require('../stub/provider') - -describe('Tx Gas Util', function () { - let txGasUtil, provider, providerResultStub - beforeEach(function () { - providerResultStub = {} - provider = createTestProviderTools({ scaffold: providerResultStub }).provider - txGasUtil = new TxGasUtils({ - provider, +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/tx-state-history-helper-test.js b/test/unit/tx-state-history-helper-test.js index 90cb10713..35e9ef188 100644 --- a/test/unit/tx-state-history-helper-test.js +++ b/test/unit/tx-state-history-helper-test.js @@ -1,6 +1,6 @@ const assert = require('assert') const clone = require('clone') -const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') +const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') describe('deepCloneFromTxMeta', function () { it('should clone deep', function () { diff --git a/test/unit/tx-state-history-helper.js b/test/unit/tx-state-history-helper.js index 79ee26d6e..35f7dac57 100644 --- a/test/unit/tx-state-history-helper.js +++ b/test/unit/tx-state-history-helper.js @@ -1,5 +1,5 @@ const assert = require('assert') -const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') +const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') const testVault = require('../data/v17-long-history.json') diff --git a/test/unit/tx-state-manager-test.js b/test/unit/tx-state-manager-test.js index a5ac13664..e5fe68d0b 100644 --- a/test/unit/tx-state-manager-test.js +++ b/test/unit/tx-state-manager-test.js @@ -1,8 +1,8 @@ const assert = require('assert') const clone = require('clone') const ObservableStore = require('obs-store') -const TxStateManager = require('../../app/scripts/lib/tx-state-manager') -const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') +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 () { diff --git a/test/unit/tx-utils-test.js b/test/unit/tx-utils-test.js index 8ca13412e..ae7afd285 100644 --- a/test/unit/tx-utils-test.js +++ b/test/unit/tx-utils-test.js @@ -1,77 +1,98 @@ 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/lib/tx-gas-utils') +const txUtils = require('../../app/scripts/controllers/transactions/lib/util') describe('txUtils', function () { - let txUtils - - before(function () { - txUtils = new TxUtils(new Proxy({}, { - get: (obj, name) => { - return () => {} - }, - })) - }) + describe('#validateTxParams', function () { + it('does not throw for positive values', function () { + var sample = { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + value: '0x01', + } + txUtils.validateTxParams(sample) + }) - 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, + it('returns error for negative values', function () { + var sample = { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + value: '-0x01', + } + try { + txUtils.validateTxParams(sample) + } catch (err) { + assert.ok(err, 'error') } - 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') + 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') + }) + }) - 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') + 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('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') + 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) + }) + }) }) \ No newline at end of file From 5494aa4f9c34353158dc0c6d07d48abec247ccd8 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Tue, 10 Apr 2018 14:53:40 -0700 Subject: [PATCH 2/9] transactions - lint fixes --- app/scripts/controllers/transactions/lib/util.js | 10 +++++----- app/scripts/controllers/transactions/tx-gas-utils.js | 2 +- .../controllers/transactions/tx-state-manager.js | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js index f403b0758..5d5e63c59 100644 --- a/app/scripts/controllers/transactions/lib/util.js +++ b/app/scripts/controllers/transactions/lib/util.js @@ -7,7 +7,7 @@ module.exports = { normalizeTxParams, validateTxParams, validateFrom, - validateRecipient + validateRecipient, } @@ -48,19 +48,19 @@ function validateTxParams (txParams) { } function validateFrom (txParams) { - if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`) + if (!(typeof txParams.from === 'string')) throw new Error(`Invalid from address ${txParams.from} not a string`) if (!isValidAddress(txParams.from)) throw new Error('Invalid from address') } function validateRecipient (txParams) { - if (txParams.to === '0x' || txParams.to === null ) { + if (txParams.to === '0x' || txParams.to === null) { if (txParams.data) { delete txParams.to } else { throw new Error('Invalid recipient address') } - } else if ( txParams.to !== undefined && !isValidAddress(txParams.to) ) { + } else if (txParams.to !== undefined && !isValidAddress(txParams.to)) { throw new Error('Invalid recipient address') } return txParams -} \ No newline at end of file +} diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index f40542603..1a7ff5b54 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -100,4 +100,4 @@ module.exports = class TxGasUtil { // otherwise use blockGasLimit return bnToHex(upperGasLimitBn) } -} \ No newline at end of file +} diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index cb24b8c99..f898cc44a 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -92,7 +92,7 @@ module.exports = class TransactionStateManager extends EventEmitter { // or rejected tx's. // not tx's that are pending or unapproved if (txCount > txHistoryLimit - 1) { - let index = transactions.findIndex((metaTx) => { + const index = transactions.findIndex((metaTx) => { return this.getFinalStates().includes(metaTx.status) }) if (index !== -1) { @@ -145,7 +145,7 @@ module.exports = class TransactionStateManager extends EventEmitter { } // validates txParams members by type - validateTxParams(txParams) { + validateTxParams (txParams) { Object.keys(txParams).forEach((key) => { const value = txParams[key] // validate types @@ -263,10 +263,10 @@ module.exports = class TransactionStateManager extends EventEmitter { // returns an array of states that can be considered final getFinalStates () { return [ - 'rejected', // the user has responded no! - 'confirmed', // the tx has been included in a block. - 'failed', // the tx failed for some reason, included on tx data. - 'dropped', // the tx nonce was already used + 'rejected', // the user has responded no! + 'confirmed', // the tx has been included in a block. + 'failed', // the tx failed for some reason, included on tx data. + 'dropped', // the tx nonce was already used ] } From 3aaa28531e1b05a93a68d8016d1648191cdc9696 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 12 Apr 2018 12:24:16 -0700 Subject: [PATCH 3/9] transactions - code cleanup --- app/scripts/controllers/transactions/index.js | 113 +++++++++--------- .../transactions/pending-tx-tracker.js | 4 +- 2 files changed, 59 insertions(+), 58 deletions(-) diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 6f66e3a1e..ca6c3923e 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -43,39 +43,13 @@ module.exports = class TransactionController extends EventEmitter { getNetwork: this.getNetwork.bind(this), }) - this.txStateManager.getFilteredTxList({ - status: 'unapproved', - loadingDefaults: true, - }).forEach((tx) => { - this.addTxDefaults(tx) - .then((txMeta) => { - txMeta.loadingDefaults = false - this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot') - }).catch((error) => { - this.txStateManager.setTxStatusFailed(tx.id, error) - }) - }) - - this.txStateManager.getFilteredTxList({ - status: 'approved', - }).forEach((txMeta) => { - const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing') - this.txStateManager.setTxStatusFailed(txMeta.id, txSignError) - }) - + this._onBootCleanUp() this.store = this.txStateManager.store - this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) this.nonceTracker = new NonceTracker({ provider: this.provider, getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), - getConfirmedTransactions: (address) => { - return this.txStateManager.getFilteredTxList({ - from: address, - status: 'confirmed', - err: undefined, - }) - }, + getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), }) this.pendingTxTracker = new PendingTransactionTracker({ @@ -87,29 +61,7 @@ module.exports = class TransactionController extends EventEmitter { }) this.txStateManager.store.subscribe(() => this.emit('update:badge')) - - this.pendingTxTracker.on('tx:warning', (txMeta) => { - this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning') - }) - this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId)) - this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) - this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { - if (!txMeta.firstRetryBlockNumber) { - txMeta.firstRetryBlockNumber = latestBlockNumber - this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update') - } - }) - this.pendingTxTracker.on('tx:retry', (txMeta) => { - if (!('retryCount' in txMeta)) txMeta.retryCount = 0 - txMeta.retryCount++ - this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry') - }) - - this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) - // this is a little messy but until ethstore has been either - // removed or redone this is to guard against the race condition - this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) - this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) + this._setupListners() // memstore is computed from a few different stores this._updateMemstore() this.txStateManager.store.subscribe(() => this._updateMemstore()) @@ -151,16 +103,16 @@ module.exports = class TransactionController extends EventEmitter { } } - wipeTransactions (address) { - this.txStateManager.wipeTransactions(address) - } - - // Adds a tx to the txlist +// Adds a tx to the txlist addTx (txMeta) { this.txStateManager.addTx(txMeta) this.emit(`${txMeta.id}:unapproved`, txMeta) } + wipeTransactions (address) { + this.txStateManager.wipeTransactions(address) + } + async newUnapprovedTransaction (txParams, opts = {}) { log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) const initialTxMeta = await this.addUnapprovedTransaction(txParams) @@ -314,6 +266,55 @@ module.exports = class TransactionController extends EventEmitter { // PRIVATE METHODS // + _onBootCleanUp () { + this.txStateManager.getFilteredTxList({ + status: 'unapproved', + loadingDefaults: true, + }).forEach((tx) => { + this.addTxDefaults(tx) + .then((txMeta) => { + txMeta.loadingDefaults = false + this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot') + }).catch((error) => { + this.txStateManager.setTxStatusFailed(tx.id, error) + }) + }) + + this.txStateManager.getFilteredTxList({ + status: 'approved', + }).forEach((txMeta) => { + const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing') + this.txStateManager.setTxStatusFailed(txMeta.id, txSignError) + }) + } + + _setupListners () { + this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) + this.pendingTxTracker.on('tx:warning', (txMeta) => { + this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning') + }) + this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId)) + this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { + if (!txMeta.firstRetryBlockNumber) { + txMeta.firstRetryBlockNumber = latestBlockNumber + this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update') + } + }) + this.pendingTxTracker.on('tx:retry', (txMeta) => { + if (!('retryCount' in txMeta)) txMeta.retryCount = 0 + txMeta.retryCount++ + this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry') + }) + + this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) + // this is a little messy but until ethstore has been either + // removed or redone this is to guard against the race condition + this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) + this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) + + } + _markNonceDuplicatesDropped (txId) { this.txStateManager.setTxStatusConfirmed(txId) // get the confirmed transactions nonce and from address diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index e8869e6b8..f2259fb96 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -171,8 +171,8 @@ module.exports = class PendingTransactionTracker extends EventEmitter { try { await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) } catch (err) { - console.error('PendingTransactionWatcher - Error updating pending transactions') - console.error(err) + log.error('PendingTransactionWatcher - Error updating pending transactions') + log.error(err) } nonceGlobalLock.releaseLock() } From 88f4212363601b2bb3778f4090235a0a0740b4c9 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 13 Apr 2018 12:38:07 -0700 Subject: [PATCH 4/9] meta - transactions - code clean up and jsDoc --- app/scripts/controllers/transactions/index.js | 150 +++++++++++++----- test/unit/tx-controller-test.js | 4 +- 2 files changed, 111 insertions(+), 43 deletions(-) diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index ca6c3923e..ca83941fc 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -8,22 +8,37 @@ const TxGasUtil = require('./tx-gas-utils') const PendingTransactionTracker = require('./pending-tx-tracker') const NonceTracker = require('./nonce-tracker') const txUtils = require('./lib/util') -/* + +module.exports = TransactionController + +/** Transaction Controller is an aggregate of sub-controllers and trackers composing them in a way to be exposed to the metamask controller - - txStateManager +
- txStateManager responsible for the state of a transaction and storing the transaction - - pendingTxTracker +
- pendingTxTracker watching blocks for transactions to be include and emitting confirmed events - - txGasUtil +
- txGasUtil gas calculations and safety buffering - - nonceTracker +
- nonceTracker calculating nonces + + +@param {object} opts - + - initState, initial transaction list default is an empty array
+ - networkStore, an observable store for network number
+ - blockTracker,
+ - provider,
+ - signTransaction, function the signs an ethereumjs-tx
+ - getGasPrice, optional gas price calculator
+ - txHistoryLimit, number *optional* for limiting how many transactions are in state
+ - preferencesStore, +@class */ -module.exports = class TransactionController extends EventEmitter { +class TransactionController extends EventEmitter { constructor (opts) { super() this.networkStore = opts.networkStore || new ObservableStore({}) @@ -42,7 +57,7 @@ module.exports = class TransactionController extends EventEmitter { txHistoryLimit: opts.txHistoryLimit, getNetwork: this.getNetwork.bind(this), }) - + this._mapMethods() this._onBootCleanUp() this.store = this.txStateManager.store @@ -68,31 +83,7 @@ module.exports = class TransactionController extends EventEmitter { this.networkStore.subscribe(() => this._updateMemstore()) this.preferencesStore.subscribe(() => this._updateMemstore()) } - - getState () { - return this.memStore.getState() - } - - getNetwork () { - return this.networkStore.getState() - } - - getSelectedAddress () { - return this.preferencesStore.getState().selectedAddress - } - - getUnapprovedTxCount () { - return Object.keys(this.txStateManager.getUnapprovedTxList()).length - } - - getPendingTxCount (account) { - return this.txStateManager.getPendingTransactions(account).length - } - - getFilteredTxList (opts) { - return this.txStateManager.getFilteredTxList(opts) - } - + /** @returns {number} the chainId*/ getChainId () { const networkState = this.networkStore.getState() const getChainId = parseInt(networkState) @@ -103,16 +94,27 @@ module.exports = class TransactionController extends EventEmitter { } } -// Adds a tx to the txlist +/** Adds a tx to the txlist */ addTx (txMeta) { this.txStateManager.addTx(txMeta) this.emit(`${txMeta.id}:unapproved`, txMeta) } +/** + wipes the transactions for a given account + @param address {string} - hex string of the from address for txs being removed +*/ wipeTransactions (address) { this.txStateManager.wipeTransactions(address) } +/** +add a new unapproved transaction to the pipeline +@returns {promise} +@param txParams {object} - txParams for the transaction +@param opts {object} - with the key origin to put the origin on the txMeta + +*/ async newUnapprovedTransaction (txParams, opts = {}) { log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) const initialTxMeta = await this.addUnapprovedTransaction(txParams) @@ -135,6 +137,13 @@ module.exports = class TransactionController extends EventEmitter { }) } + /** + validates and generates a txMeta with defaults and puts it in txStateManager + store + + @returns {txMeta} + */ + async addUnapprovedTransaction (txParams) { // validate const normalizedTxParams = txUtils.normalizeTxParams(txParams) @@ -145,7 +154,7 @@ module.exports = class TransactionController extends EventEmitter { this.emit('newUnapprovedTx', txMeta) // add default tx params try { - txMeta = await this.addTxDefaults(txMeta) + txMeta = await this.addTxGasDefaults(txMeta) } catch (error) { console.log(error) this.txStateManager.setTxStatusFailed(txMeta.id, error) @@ -157,8 +166,12 @@ module.exports = class TransactionController extends EventEmitter { return txMeta } - - async addTxDefaults (txMeta) { +/** + adds the tx gas defaults: gas && gasPrice + @param txMeta {object} - the txMeta object + @returns {promise} resolves with txMeta +*/ + async addTxGasDefaults (txMeta) { const txParams = txMeta.txParams // ensure value txMeta.gasPriceSpecified = Boolean(txParams.gasPrice) @@ -167,11 +180,18 @@ module.exports = class TransactionController extends EventEmitter { gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice() } txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) - txParams.value = txParams.value || '0x0' // set gasLimit return await this.txGasUtil.analyzeGasUsage(txMeta) } + /** + creates a new txMeta with the same txParams as the original + to allow the user to resign the transaction with a higher gas values + @param originalTxId {number} - the id of the txMeta that + you want to attempt to retry + @return {txMeta} + */ + async retryTransaction (originalTxId) { const originalTxMeta = this.txStateManager.getTx(originalTxId) const lastGasPrice = originalTxMeta.txParams.gasPrice @@ -185,15 +205,31 @@ module.exports = class TransactionController extends EventEmitter { return txMeta } + /** + updates the txMeta in the txStateManager + @param txMeta {object} - the updated txMeta + */ async updateTransaction (txMeta) { this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction') } + /** + updates and approves the transaction + @param txMeta {object} + */ async updateAndApproveTransaction (txMeta) { this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction') await this.approveTransaction(txMeta.id) } + /** + sets the tx status to approved + auto fills the nonce + signs the transaction + publishes the transaction + if any of these steps fails the tx status will be set to failed + @param txId {number} - the tx's Id + */ async approveTransaction (txId) { let nonceLock try { @@ -225,7 +261,11 @@ module.exports = class TransactionController extends EventEmitter { throw err } } - + /** + adds the chain id and signs the transaction and set the status to signed + @param txId {number} - the tx's Id + @returns - rawTx {string} + */ async signTransaction (txId) { const txMeta = this.txStateManager.getTx(txId) // add network/chain id @@ -241,6 +281,11 @@ module.exports = class TransactionController extends EventEmitter { return rawTx } + /** + publishes the raw tx and sets the txMeta to submitted + @param txId {number} - the tx's Id + @param rawTx {string} - the hex string of the serialized signed transaction + */ async publishTransaction (txId, rawTx) { const txMeta = this.txStateManager.getTx(txId) txMeta.rawTx = rawTx @@ -250,11 +295,19 @@ module.exports = class TransactionController extends EventEmitter { this.txStateManager.setTxStatusSubmitted(txId) } + /** + convenience method for the ui thats sets the transaction to rejected + @param txId {number} - the tx's Id + */ async cancelTransaction (txId) { this.txStateManager.setTxStatusRejected(txId) } - // receives a txHash records the tx as signed + /** + sets the txHas on the txMeta + @param txId {number} - the tx's Id + @param txHash {string} - the hash for the txMeta + */ setTxHash (txId, txHash) { // Add the tx hash to the persisted meta-tx object const txMeta = this.txStateManager.getTx(txId) @@ -265,13 +318,28 @@ module.exports = class TransactionController extends EventEmitter { // // PRIVATE METHODS // + /** maps methods for convenience*/ + _mapMethods () { + /** Returns the state in transaction controller */ + this.getState = () => this.memStore.getState() + /** Returns the network number stored in networkStore */ + this.getNetwork = () => this.networkStore.getState() + /** Returns the user selected address */ + this.getSelectedAddress = () => this.preferencesStore.getState().selectedAddress + /** Returns an array of transactions whos status is unapproved */ + this.getUnapprovedTxCount = () => Object.keys(this.txStateManager.getUnapprovedTxList()).length + /** Returns a number that represents how many transactions have the status submitted*/ + this.getPendingTxCount = (account) => this.txStateManager.getPendingTransactions(account).length + /** see txStateManager */ + this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts) + } _onBootCleanUp () { this.txStateManager.getFilteredTxList({ status: 'unapproved', loadingDefaults: true, }).forEach((tx) => { - this.addTxDefaults(tx) + this.addTxGasDefaults(tx) .then((txMeta) => { txMeta.loadingDefaults = false this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot') @@ -339,4 +407,4 @@ module.exports = class TransactionController extends EventEmitter { }) this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } -} +} \ No newline at end of file diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 08f16d83b..20d6f8573 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -188,7 +188,7 @@ describe('Transaction Controller', function () { }) - describe('#addTxDefaults', function () { + describe('#addTxGasDefaults', function () { it('should add the tx defaults if their are none', function (done) { const txMeta = { 'txParams': { @@ -199,7 +199,7 @@ describe('Transaction Controller', function () { providerResultStub.eth_gasPrice = '4a817c800' providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' } providerResultStub.eth_estimateGas = '5209' - txController.addTxDefaults(txMeta) + 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') From 943eea043cc40ea42ffe757a7115ccbc5585b37b Mon Sep 17 00:00:00 2001 From: frankiebee Date: Fri, 13 Apr 2018 13:18:45 -0700 Subject: [PATCH 5/9] fix up - more docs --- app/scripts/controllers/transactions/index.js | 11 ++- .../controllers/transactions/lib/util.js | 32 ++++----- .../transactions/pending-tx-tracker.js | 72 ++++++++++++++----- 3 files changed, 77 insertions(+), 38 deletions(-) diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index ca83941fc..c81251cd2 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -9,8 +9,6 @@ const PendingTransactionTracker = require('./pending-tx-tracker') const NonceTracker = require('./nonce-tracker') const txUtils = require('./lib/util') -module.exports = TransactionController - /** Transaction Controller is an aggregate of sub-controllers and trackers composing them in a way to be exposed to the metamask controller @@ -356,6 +354,11 @@ add a new unapproved transaction to the pipeline }) } + /** + is called in constructor applies the listeners for pendingTxTracker txStateManager + and blockTracker +
+ */ _setupListners () { this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) this.pendingTxTracker.on('tx:warning', (txMeta) => { @@ -407,4 +410,6 @@ add a new unapproved transaction to the pipeline }) this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } -} \ No newline at end of file +} + +module.exports = TransactionController \ No newline at end of file diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js index 5d5e63c59..b18283997 100644 --- a/app/scripts/controllers/transactions/lib/util.js +++ b/app/scripts/controllers/transactions/lib/util.js @@ -11,24 +11,24 @@ module.exports = { } +// functions that handle normalizing of that key in txParams +const normalizers = { + from: from => addHexPrefix(from).toLowerCase(), + to: to => addHexPrefix(to).toLowerCase(), + nonce: nonce => addHexPrefix(nonce), + value: value => value ? addHexPrefix(value) : '0x0', + data: data => addHexPrefix(data), + gas: gas => addHexPrefix(gas), + gasPrice: gasPrice => addHexPrefix(gasPrice), +} + /** + */ function normalizeTxParams (txParams) { - // functions that handle normalizing of that key in txParams - const whiteList = { - from: from => addHexPrefix(from).toLowerCase(), - to: to => addHexPrefix(txParams.to).toLowerCase(), - nonce: nonce => addHexPrefix(nonce), - value: value => addHexPrefix(value), - data: data => addHexPrefix(data), - gas: gas => addHexPrefix(gas), - gasPrice: gasPrice => addHexPrefix(gasPrice), - } - - // apply only keys in the whiteList + // apply only keys in the normalizers const normalizedTxParams = {} - Object.keys(whiteList).forEach((key) => { - if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key]) - }) - + for (let key in normalizers) { + if (txParams[key]) normalizedTxParams[key] = normalizers[key](txParams[key]) + } return normalizedTxParams } diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index f2259fb96..503343e22 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -1,23 +1,23 @@ const EventEmitter = require('events') const EthQuery = require('ethjs-query') -/* - - Utility class for tracking the transactions as they - go from a pending state to a confirmed (mined in a block) state +/** + Event emitter utility class for tracking the transactions as they
+ go from a pending state to a confirmed (mined in a block) state
+
As well as continues broadcast while in the pending state +
+@param config {object} - non optional configuration object consists of: +
provider +
nonceTracker: see nonce tracker +
getPendingTransactions: a function for getting an array of transactions, +
publishTransaction: a async function for publishing raw transactions, - ~config is not optional~ - requires a: { - provider: //, - nonceTracker: //see nonce tracker, - getPendingTransactions: //() a function for getting an array of transactions, - publishTransaction: //(rawTx) a async function for publishing raw transactions, - } +@class */ -module.exports = class PendingTransactionTracker extends EventEmitter { +class PendingTransactionTracker extends EventEmitter { constructor (config) { super() this.query = new EthQuery(config.provider) @@ -29,8 +29,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { this._checkPendingTxs() } - // checks if a signed tx is in a block and - // if included sets the tx status as 'confirmed' + /** + checks if a signed tx is in a block and + if it is included emits tx status as 'confirmed' + @param block {object}, a full block + @emits tx:confirmed + @emits tx:failed + */ checkForTxInBlock (block) { const signedTxList = this.getPendingTransactions() if (!signedTxList.length) return @@ -52,6 +57,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter { }) } + /** + asks the network for the transaction to see if a block number is included on it + if we have skipped/missed blocks + @param object - oldBlock newBlock + */ queryPendingTxs ({ oldBlock, newBlock }) { // check pending transactions on start if (!oldBlock) { @@ -63,7 +73,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter { if (diff > 1) this._checkPendingTxs() } - + /** + Will resubmit any transactions who have not been confirmed in a block + @param block {object} - a block object + @emits tx:warning + */ resubmitPendingTxs (block) { const pending = this.getPendingTransactions() // only try resubmitting if their are transactions to resubmit @@ -100,6 +114,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { })) } + /** + resubmits the individual txMeta used in resubmitPendingTxs + @param txMeta {object} - txMeta object + @param latestBlockNumber {string} - hex string for the latest block number + @emits tx:retry + @returns txHash {string} + */ async _resubmitTx (txMeta, latestBlockNumber) { if (!txMeta.firstRetryBlockNumber) { this.emit('tx:block-update', txMeta, latestBlockNumber) @@ -123,7 +144,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { this.emit('tx:retry', txMeta) return txHash } - + /** + Ask the network for the transaction to see if it has been include in a block + @param txMeta {object} - the txMeta object + @emits tx:failed + @emits tx:confirmed + @emits tx:warning + */ async _checkPendingTx (txMeta) { const txHash = txMeta.hash const txId = txMeta.id @@ -162,8 +189,9 @@ module.exports = class PendingTransactionTracker extends EventEmitter { } } - // checks the network for signed txs and - // if confirmed sets the tx status as 'confirmed' + /** + checks the network for signed txs and releases the nonce global lock if it is + */ async _checkPendingTxs () { const signedTxList = this.getPendingTransactions() // in order to keep the nonceTracker accurate we block it while updating pending transactions @@ -177,6 +205,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter { nonceGlobalLock.releaseLock() } + /** + checks to see if a confirmed txMeta has the same nonce + @param txMeta {object} - txMeta object + @returns {boolean} + */ async _checkIfNonceIsTaken (txMeta) { const address = txMeta.txParams.from const completed = this.getCompletedTransactions(address) @@ -185,5 +218,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter { }) return sameNonce.length > 0 } - } + +module.exports = PendingTransactionTracker \ No newline at end of file From eeb9390de81ce6fc92247d5c499e991dce8330bd Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 19 Apr 2018 11:29:26 -0700 Subject: [PATCH 6/9] meta - transactions - docs yo! --- .../controllers/transactions/README.md | 92 ++++++++ app/scripts/controllers/transactions/index.js | 27 ++- .../lib/tx-state-history-helper.js | 27 ++- .../controllers/transactions/lib/util.js | 37 +++- .../controllers/transactions/nonce-tracker.js | 28 ++- .../transactions/pending-tx-tracker.js | 10 +- .../controllers/transactions/tx-gas-utils.js | 1 + .../transactions/tx-state-manager.js | 202 +++++++++++++----- docs/transaction-flow.png | Bin 0 -> 141125 bytes 9 files changed, 351 insertions(+), 73 deletions(-) create mode 100644 app/scripts/controllers/transactions/README.md create mode 100644 docs/transaction-flow.png diff --git a/app/scripts/controllers/transactions/README.md b/app/scripts/controllers/transactions/README.md new file mode 100644 index 000000000..ea38b5ae6 --- /dev/null +++ b/app/scripts/controllers/transactions/README.md @@ -0,0 +1,92 @@ +# Transaction Controller + +Transaction Controller is an aggregate of sub-controllers and trackers +composing them in a way to be exposed to the metamask controller + +- txStateManager + responsible for the state of a transaction and + storing the transaction +- pendingTxTracker + watching blocks for transactions to be include + and emitting confirmed events +- txGasUtil + gas calculations and safety buffering +- nonceTracker + calculating nonces + +## flow digram of processing a transaction + +![transaction-flow](../../../../docs/transaction-flow.png) + +## txMeta's && txParams + +A txMeta is the "meta" object it has all the random bits of info we need about a transaction on it. txParams are sacred every thing on txParams gets signed so it must +be a valid key and be hex prefixed except for the network number. Extra stuff must go on the txMeta! + +Here is a txMeta too look at: + +```js +txMeta = { + "id": 2828415030114568, // unique id for this txMeta used for look ups + "time": 1524094064821, // time of creation + "status": "confirmed", + "metamaskNetworkId": "1524091532133", //the network id for the transaction + "loadingDefaults": false, // used to tell the ui when we are done calculatyig gass defaults + "txParams": { // the txParams object + "from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675", + "to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675", + "value": "0x0", + "gasPrice": "0x3b9aca00", + "gas": "0x7b0c", + "nonce": "0x0" + }, + "history": [{ //debug + "id": 2828415030114568, + "time": 1524094064821, + "status": "unapproved", + "metamaskNetworkId": "1524091532133", + "loadingDefaults": true, + "txParams": { + "from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675", + "to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675", + "value": "0x0" + } + }, + [ + { + "op": "add", + "path": "/txParams/gasPrice", + "value": "0x3b9aca00" + }, + ...], // I've removed most of history for this + "gasPriceSpecified": false, //weather or not the user/dapp has specified gasPrice + "gasLimitSpecified": false, //weather or not the user/dapp has specified gas + "estimatedGas": "5208", + "origin": "MetaMask", //debug + "nonceDetails": { + "params": { + "highestLocallyConfirmed": 0, + "highestSuggested": 0, + "nextNetworkNonce": 0 + }, + "local": { + "name": "local", + "nonce": 0, + "details": { + "startPoint": 0, + "highest": 0 + } + }, + "network": { + "name": "network", + "nonce": 0, + "details": { + "baseCount": 0 + } + } + }, + "rawTx": "0xf86980843b9aca00827b0c948acce2391c0d510a6c5e5d8f819a678f79b7e67580808602c5b5de66eea05c01a320b96ac730cb210ca56d2cb71fa360e1fc2c21fa5cf333687d18eb323fa02ed05987a6e5fd0f2459fcff80710b76b83b296454ad9a37594a0ccb4643ea90", // used for rebroadcast + "hash": "0xa45ba834b97c15e6ff4ed09badd04ecd5ce884b455eb60192cdc73bcc583972a", + "submittedTime": 1524094077902 // time of the attempt to submit the raw tx to the network, used in the ui to show the retry button +} +``` diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index c81251cd2..d7287450b 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -25,14 +25,15 @@ const txUtils = require('./lib/util') @param {object} opts - - - initState, initial transaction list default is an empty array
- - networkStore, an observable store for network number
- - blockTracker,
- - provider,
- - signTransaction, function the signs an ethereumjs-tx
- - getGasPrice, optional gas price calculator
- - txHistoryLimit, number *optional* for limiting how many transactions are in state
- - preferencesStore, + @property {object} opts.initState initial transaction list default is an empty array + @property {Object} opts.networkStore an observable store for network number + @property {Object} opts.blockTracker + @property {Object} opts.provider + @property {Object} opts.signTransaction function the signs an ethereumjs-tx + @property {function} opts.getGasPrice optional gas price calculator + @property {function} opts.signTransaction ethTx signer that returns a rawTx + @property {number} opts.txHistoryLimit number *optional* for limiting how many transactions are in state + @property {Object} opts.preferencesStore @class */ @@ -50,12 +51,12 @@ class TransactionController extends EventEmitter { this.query = new EthQuery(this.provider) this.txGasUtil = new TxGasUtil(this.provider) + this._mapMethods() this.txStateManager = new TransactionStateManager({ initState: opts.initState, txHistoryLimit: opts.txHistoryLimit, getNetwork: this.getNetwork.bind(this), }) - this._mapMethods() this._onBootCleanUp() this.store = this.txStateManager.store @@ -92,7 +93,10 @@ class TransactionController extends EventEmitter { } } -/** Adds a tx to the txlist */ +/** + Adds a tx to the txlist + @emits ${txMeta.id}:unapproved +*/ addTx (txMeta) { this.txStateManager.addTx(txMeta) this.emit(`${txMeta.id}:unapproved`, txMeta) @@ -172,6 +176,7 @@ add a new unapproved transaction to the pipeline async addTxGasDefaults (txMeta) { const txParams = txMeta.txParams // ensure value + txParams.value = txParams.value ? ethUtil.addHexPrefix(value) : '0x0', txMeta.gasPriceSpecified = Boolean(txParams.gasPrice) let gasPrice = txParams.gasPrice if (!gasPrice) { @@ -412,4 +417,4 @@ add a new unapproved transaction to the pipeline } } -module.exports = TransactionController \ No newline at end of file +module.exports = TransactionController diff --git a/app/scripts/controllers/transactions/lib/tx-state-history-helper.js b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js index 94c7b6792..7a57e3cb5 100644 --- a/app/scripts/controllers/transactions/lib/tx-state-history-helper.js +++ b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js @@ -1,6 +1,6 @@ const jsonDiffer = require('fast-json-patch') const clone = require('clone') - +/** @module*/ module.exports = { generateHistoryEntry, replayHistory, @@ -8,7 +8,11 @@ module.exports = { migrateFromSnapshotsToDiffs, } - +/** + converts non-initial history entries into diffs + @param longHistory {array} + @returns {array} +*/ function migrateFromSnapshotsToDiffs (longHistory) { return ( longHistory @@ -20,6 +24,17 @@ function migrateFromSnapshotsToDiffs (longHistory) { ) } +/** + generates an array of history objects sense the previous state. + The object has the keys opp(the operation preformed), + path(the key and if a nested object then each key will be seperated with a `/`) + value + with the first entry having the note + @param previousState {object} - the previous state of the object + @param newState {object} - the update object + @param note {string} - a optional note for the state change + @reurns {array} +*/ function generateHistoryEntry (previousState, newState, note) { const entry = jsonDiffer.compare(previousState, newState) // Add a note to the first op, since it breaks if we append it to the entry @@ -27,11 +42,19 @@ function generateHistoryEntry (previousState, newState, note) { return entry } +/** + Recovers previous txMeta state obj + @return {object} +*/ function replayHistory (_shortHistory) { const shortHistory = clone(_shortHistory) return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument) } +/** + @param txMeta {object} + @returns {object} a clone object of the txMeta with out history +*/ function snapshotFromTxMeta (txMeta) { // create txMeta snapshot for history const snapshot = clone(txMeta) diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js index b18283997..84f7592a0 100644 --- a/app/scripts/controllers/transactions/lib/util.js +++ b/app/scripts/controllers/transactions/lib/util.js @@ -3,11 +3,15 @@ const { isValidAddress, } = require('ethereumjs-util') +/** +@module +*/ module.exports = { normalizeTxParams, validateTxParams, validateFrom, validateRecipient, + getFinalStates, } @@ -16,22 +20,30 @@ const normalizers = { from: from => addHexPrefix(from).toLowerCase(), to: to => addHexPrefix(to).toLowerCase(), nonce: nonce => addHexPrefix(nonce), - value: value => value ? addHexPrefix(value) : '0x0', + value: value => addHexPrefix(value), data: data => addHexPrefix(data), gas: gas => addHexPrefix(gas), gasPrice: gasPrice => addHexPrefix(gasPrice), } + /** + normalizes txParams + @param txParams {object} + @returns {object} normalized txParams */ function normalizeTxParams (txParams) { // apply only keys in the normalizers const normalizedTxParams = {} - for (let key in normalizers) { + for (const key in normalizers) { if (txParams[key]) normalizedTxParams[key] = normalizers[key](txParams[key]) } return normalizedTxParams } + /** + validates txParams + @param txParams {object} + */ function validateTxParams (txParams) { validateFrom(txParams) validateRecipient(txParams) @@ -47,11 +59,19 @@ function validateTxParams (txParams) { } } + /** + validates the from field in txParams + @param txParams {object} + */ function validateFrom (txParams) { if (!(typeof txParams.from === 'string')) throw new Error(`Invalid from address ${txParams.from} not a string`) if (!isValidAddress(txParams.from)) throw new Error('Invalid from address') } + /** + validates the to field in txParams + @param txParams {object} + */ function validateRecipient (txParams) { if (txParams.to === '0x' || txParams.to === null) { if (txParams.data) { @@ -64,3 +84,16 @@ function validateRecipient (txParams) { } return txParams } + + /** + @returns an {array} of states that can be considered final + */ +function getFinalStates () { + return [ + 'rejected', // the user has responded no! + 'confirmed', // the tx has been included in a block. + 'failed', // the tx failed for some reason, included on tx data. + 'dropped', // the tx nonce was already used + ] +} + diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js index 5b1cd7f43..e0f4d0fe3 100644 --- a/app/scripts/controllers/transactions/nonce-tracker.js +++ b/app/scripts/controllers/transactions/nonce-tracker.js @@ -1,7 +1,15 @@ const EthQuery = require('ethjs-query') const assert = require('assert') const Mutex = require('await-semaphore').Mutex - +/** + @param opts {object} - + @property {Object} opts.provider a ethereum provider + @property {function} opts.getPendingTransactions a function that returns an array of txMeta + whos status is `submitted` + @property {function} opts.getConfirmedTransactions a function that returns an array of txMeta + whos status is `confirmed` + @class +*/ class NonceTracker { constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) { @@ -12,6 +20,9 @@ class NonceTracker { this.lockMap = {} } + /** + @returns {object} with the key releaseLock (the gloabl mutex) + */ async getGlobalLock () { const globalMutex = this._lookupMutex('global') // await global mutex free @@ -19,8 +30,19 @@ class NonceTracker { return { releaseLock } } - // releaseLock must be called - // releaseLock must be called after adding signed tx to pending transactions (or discarding) + /** + this will return an object with the `nextNonce` `nonceDetails` which is an + object with: + highestLocallyConfirmed (nonce), + highestSuggested (either the network nonce or the highestLocallyConfirmed nonce), + nextNetworkNonce (the nonce suggested by the network), + and the releaseLock +
note: releaseLock must be called after adding signed tx to pending transactions + (or discarding)
+ + @param address {string} the hex string for the address whos nonce we are calculating + @returns {object} + */ async getNonceLock (address) { // await global mutex free await this._globalMutexFree() diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index 503343e22..98b3d2b08 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -8,10 +8,10 @@ const EthQuery = require('ethjs-query') As well as continues broadcast while in the pending state
@param config {object} - non optional configuration object consists of: -
provider -
nonceTracker: see nonce tracker -
getPendingTransactions: a function for getting an array of transactions, -
publishTransaction: a async function for publishing raw transactions, + @property {Object} config.provider + @property {Object} config.nonceTracker see nonce tracker + @property {function} config.getPendingTransactions a function for getting an array of transactions, + @property {function} config.publishTransaction a async function for publishing raw transactions, @class @@ -220,4 +220,4 @@ class PendingTransactionTracker extends EventEmitter { } } -module.exports = PendingTransactionTracker \ No newline at end of file +module.exports = PendingTransactionTracker diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 1a7ff5b54..31a5bfcf4 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -11,6 +11,7 @@ const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. tx-utils are utility methods for Transaction manager its passed ethquery and used to do things like calculate gas of a tx. +@param provider {object} */ module.exports = class TxGasUtil { diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index f898cc44a..328024925 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -1,22 +1,33 @@ const extend = require('xtend') const EventEmitter = require('events') const ObservableStore = require('obs-store') -const createId = require('../../lib/random-id') const ethUtil = require('ethereumjs-util') const txStateHistoryHelper = require('./lib/tx-state-history-helper') - -// STATUS METHODS - // statuses: - // - `'unapproved'` the user has not responded - // - `'rejected'` the user has responded no! - // - `'approved'` the user has approved the tx - // - `'signed'` the tx is signed - // - `'submitted'` the tx is sent to a server - // - `'confirmed'` the tx has been included in a block. - // - `'failed'` the tx failed for some reason, included on tx data. - // - `'dropped'` the tx nonce was already used - -module.exports = class TransactionStateManager extends EventEmitter { +const createId = require('../../lib/random-id') +const { getFinalStates } = require('./lib/util') +/** + TransactionStateManager is responsible for the state of a transaction and + storing the transaction + it also has some convenience methods for finding subsets of transactions + * + *STATUS METHODS +
statuses: +
- `'unapproved'` the user has not responded +
- `'rejected'` the user has responded no! +
- `'approved'` the user has approved the tx +
- `'signed'` the tx is signed +
- `'submitted'` the tx is sent to a server +
- `'confirmed'` the tx has been included in a block. +
- `'failed'` the tx failed for some reason, included on tx data. +
- `'dropped'` the tx nonce was already used + @param opts {object} - + @property {object} opts.initState with the key transaction {array} + @property {number} opts.txHistoryLimit limit for how many finished + transactions can hang around in state + @property {function} opts.getNetwork return network number + @class +*/ +class TransactionStateManager extends EventEmitter { constructor ({ initState, txHistoryLimit, getNetwork }) { super() @@ -28,6 +39,10 @@ module.exports = class TransactionStateManager extends EventEmitter { this.getNetwork = getNetwork } + /** + @param opts {object} - the object to use when overwriting defaults + @returns {txMeta} the default txMeta object + */ generateTxMeta (opts) { return extend({ id: createId(), @@ -38,17 +53,25 @@ module.exports = class TransactionStateManager extends EventEmitter { }, opts) } + /** + @returns {array} of txMetas that have been filtered for only the current network + */ getTxList () { const network = this.getNetwork() const fullTxList = this.getFullTxList() return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network) } + /** + @returns {array} of all the txMetas in store + */ getFullTxList () { return this.store.getState().transactions } - // Returns the tx list + /** + @returns {array} the tx list whos status is unapproved + */ getUnapprovedTxList () { const txList = this.getTxsByMetaData('status', 'unapproved') return txList.reduce((result, tx) => { @@ -57,18 +80,35 @@ module.exports = class TransactionStateManager extends EventEmitter { }, {}) } + /** + @param address {string} - hex prefixed address to sort the txMetas for [optional] + @returns {array} the tx list whos status is submitted + */ getPendingTransactions (address) { const opts = { status: 'submitted' } if (address) opts.from = address return this.getFilteredTxList(opts) } + /** + @param address {string} - hex prefixed address to sort the txMetas for [optional] + @returns {array} the tx list whos status is confirmed + */ getConfirmedTransactions (address) { const opts = { status: 'confirmed' } if (address) opts.from = address return this.getFilteredTxList(opts) } + /** + Adds the txMeta to the list of transactions in the store. + if the list is over txHistoryLimit it will remove a transaction that + is in its final state + it will allso add the key `history` to the txMeta with the snap shot of the original + object + @param txMeta {object} + @returns {object} the txMeta + */ addTx (txMeta) { this.once(`${txMeta.id}:signed`, function (txId) { this.removeAllListeners(`${txMeta.id}:rejected`) @@ -93,7 +133,7 @@ module.exports = class TransactionStateManager extends EventEmitter { // not tx's that are pending or unapproved if (txCount > txHistoryLimit - 1) { const index = transactions.findIndex((metaTx) => { - return this.getFinalStates().includes(metaTx.status) + return getFinalStates().includes(metaTx.status) }) if (index !== -1) { transactions.splice(index, 1) @@ -103,12 +143,21 @@ module.exports = class TransactionStateManager extends EventEmitter { this._saveTxList(transactions) return txMeta } - // gets tx by Id and returns it + /** + @param txId {number} + @returns {object} the txMeta who matches the given id if none found + for the network returns undefined + */ getTx (txId) { const txMeta = this.getTxsByMetaData('id', txId)[0] return txMeta } + /** + updates the txMeta in the list and adds a history entry + @param txMeta {object} - the txMeta to update + @param note {string} - a not about the update for history + */ updateTx (txMeta, note) { // validate txParams if (txMeta.txParams) { @@ -136,15 +185,22 @@ module.exports = class TransactionStateManager extends EventEmitter { } - // merges txParams obj onto txData.txParams - // use extend to ensure that all fields are filled + /** + merges txParams obj onto txMeta.txParams + use extend to ensure that all fields are filled + @param txId {number} - the id of the txMeta + @param txParams {object} - the updated txParams + */ updateTxParams (txId, txParams) { const txMeta = this.getTx(txId) txMeta.txParams = extend(txMeta.txParams, txParams) this.updateTx(txMeta, `txStateManager#updateTxParams`) } - // validates txParams members by type + /** + validates txParams members by type + @param txParams {object} - txParams to validate + */ validateTxParams (txParams) { Object.keys(txParams).forEach((key) => { const value = txParams[key] @@ -161,17 +217,18 @@ module.exports = class TransactionStateManager extends EventEmitter { }) } -/* - Takes an object of fields to search for eg: - let thingsToLookFor = { - to: '0x0..', - from: '0x0..', - status: 'signed', - err: undefined, - } - and returns a list of tx with all +/** + @param opts {object} - an object of fields to search for eg:
+ let thingsToLookFor = {
+ to: '0x0..',
+ from: '0x0..',
+ status: 'signed',
+ err: undefined,
+ }
+ @returns a {array} of txMeta with all options matching - + */ + /* ****************HINT**************** | `err: undefined` is like looking | | for a tx with no err | @@ -192,7 +249,14 @@ module.exports = class TransactionStateManager extends EventEmitter { }) return filteredTxList } + /** + @param key {string} - the key to check + @param value - the value your looking for + @param txList {array} - [optional] the list to search. default is the txList + from txStateManager#getTxList + @returns {array} a list of txMetas who matches the search params + */ getTxsByMetaData (key, value, txList = this.getTxList()) { return txList.filter((txMeta) => { if (txMeta.txParams[key]) { @@ -205,33 +269,51 @@ module.exports = class TransactionStateManager extends EventEmitter { // get::set status - // should return the status of the tx. + /** + @param txId {number} - the txMeta Id + @return {string} the status of the tx. + */ getTxStatus (txId) { const txMeta = this.getTx(txId) return txMeta.status } - // should update the status of the tx to 'rejected'. + /** + should update the status of the tx to 'rejected'. + @param txId {number} - the txMeta Id + */ setTxStatusRejected (txId) { this._setTxStatus(txId, 'rejected') } - // should update the status of the tx to 'unapproved'. + /** + should update the status of the tx to 'unapproved'. + @param txId {number} - the txMeta Id + */ setTxStatusUnapproved (txId) { this._setTxStatus(txId, 'unapproved') } - // should update the status of the tx to 'approved'. + /** + should update the status of the tx to 'approved'. + @param txId {number} - the txMeta Id + */ setTxStatusApproved (txId) { this._setTxStatus(txId, 'approved') } - // should update the status of the tx to 'signed'. + /** + should update the status of the tx to 'signed'. + @param txId {number} - the txMeta Id + */ setTxStatusSigned (txId) { this._setTxStatus(txId, 'signed') } - // should update the status of the tx to 'submitted'. - // and add a time stamp for when it was called + /** + should update the status of the tx to 'submitted'. + and add a time stamp for when it was called + @param txId {number} - the txMeta Id + */ setTxStatusSubmitted (txId) { const txMeta = this.getTx(txId) txMeta.submittedTime = (new Date()).getTime() @@ -239,17 +321,29 @@ module.exports = class TransactionStateManager extends EventEmitter { this._setTxStatus(txId, 'submitted') } - // should update the status of the tx to 'confirmed'. + /** + should update the status of the tx to 'confirmed'. + @param txId {number} - the txMeta Id + */ setTxStatusConfirmed (txId) { this._setTxStatus(txId, 'confirmed') } - // should update the status dropped + /** + should update the status of the tx to 'dropped'. + @param txId {number} - the txMeta Id + */ setTxStatusDropped (txId) { this._setTxStatus(txId, 'dropped') } + /** + should update the status of the tx to 'failed'. + and put the error on the txMeta + @param txId {number} - the txMeta Id + @param err {erroObject} - error object + */ setTxStatusFailed (txId, err) { const txMeta = this.getTx(txId) txMeta.err = { @@ -260,16 +354,11 @@ module.exports = class TransactionStateManager extends EventEmitter { this._setTxStatus(txId, 'failed') } - // returns an array of states that can be considered final - getFinalStates () { - return [ - 'rejected', // the user has responded no! - 'confirmed', // the tx has been included in a block. - 'failed', // the tx failed for some reason, included on tx data. - 'dropped', // the tx nonce was already used - ] - } - + /** + Removes transaction from the given address for the current network + from the txList + @param address {string} - hex string of the from address on the txParams to remove + */ wipeTransactions (address) { // network only tx const txs = this.getFullTxList() @@ -295,6 +384,14 @@ module.exports = class TransactionStateManager extends EventEmitter { // - `'confirmed'` the tx has been included in a block. // - `'failed'` the tx failed for some reason, included on tx data. // - `'dropped'` the tx nonce was already used + + /** + @param txId {number} - the txMeta Id + @param status {string} - the status to set on the txMeta + @emits tx:status-update - passes txId and status + @emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta + @emits update:badge + */ _setTxStatus (txId, status) { const txMeta = this.getTx(txId) txMeta.status = status @@ -307,9 +404,14 @@ module.exports = class TransactionStateManager extends EventEmitter { this.emit('update:badge') } - // Saves the new/updated txList. + /** + Saves the new/updated txList. + @param transactions {array} - the list of transactions to save + */ // Function is intended only for internal use _saveTxList (transactions) { this.store.updateState({ transactions }) } } + +module.exports = TransactionStateManager diff --git a/docs/transaction-flow.png b/docs/transaction-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..1059b60d8bae07e4a9c683ab67a702982635226a GIT binary patch literal 141125 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¥bRY0IIi;RD_>l)<(aVz*_;Kf%Tv6-R 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 literal 0 HcmV?d00001 From e862a5091e271e8065e9da3c25540612f98a8d2a Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 19 Apr 2018 11:56:34 -0700 Subject: [PATCH 7/9] transactions - fix refernces --- app/scripts/controllers/transactions/index.js | 2 +- app/scripts/controllers/transactions/pending-tx-tracker.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 6e47816f8..fc6340bd6 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -177,7 +177,7 @@ add a new unapproved transaction to the pipeline async addTxGasDefaults (txMeta) { const txParams = txMeta.txParams // ensure value - txParams.value = txParams.value ? ethUtil.addHexPrefix(value) : '0x0', + txParams.value = txParams.value ? ethUtil.addHexPrefix(txParams.value) : '0x0' txMeta.gasPriceSpecified = Boolean(txParams.gasPrice) let gasPrice = txParams.gasPrice if (!gasPrice) { diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index 98b3d2b08..f7c9993b3 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -1,4 +1,5 @@ const EventEmitter = require('events') +const log = require('loglevel') const EthQuery = require('ethjs-query') /** From 621e9334bc7009f7ccd7c1536df00ff6f8240bd3 Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Mon, 23 Apr 2018 09:43:18 -0700 Subject: [PATCH 8/9] Cleaned up some typos and JSDocs in Transactions Nonce tracker is not fully documented yet. Have not yet touched: - tx-state-manager - tx-state-history-helper - util - tx-gas-utils - pending-tx-tracker --- .../controllers/transactions/README.md | 10 +++--- app/scripts/controllers/transactions/index.js | 33 ++++++++--------- .../controllers/transactions/nonce-tracker.js | 35 ++++++++++--------- 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/app/scripts/controllers/transactions/README.md b/app/scripts/controllers/transactions/README.md index ea38b5ae6..b414762dc 100644 --- a/app/scripts/controllers/transactions/README.md +++ b/app/scripts/controllers/transactions/README.md @@ -1,7 +1,7 @@ # Transaction Controller Transaction Controller is an aggregate of sub-controllers and trackers -composing them in a way to be exposed to the metamask controller +exposed to the MetaMask controller. - txStateManager responsible for the state of a transaction and @@ -14,11 +14,11 @@ composing them in a way to be exposed to the metamask controller - nonceTracker calculating nonces -## flow digram of processing a transaction +## Flow diagram of processing a transaction ![transaction-flow](../../../../docs/transaction-flow.png) -## txMeta's && txParams +## txMeta's & txParams A txMeta is the "meta" object it has all the random bits of info we need about a transaction on it. txParams are sacred every thing on txParams gets signed so it must be a valid key and be hex prefixed except for the network number. Extra stuff must go on the txMeta! @@ -59,8 +59,8 @@ txMeta = { "value": "0x3b9aca00" }, ...], // I've removed most of history for this - "gasPriceSpecified": false, //weather or not the user/dapp has specified gasPrice - "gasLimitSpecified": false, //weather or not the user/dapp has specified gas + "gasPriceSpecified": false, //whether or not the user/dapp has specified gasPrice + "gasLimitSpecified": false, //whether or not the user/dapp has specified gas "estimatedGas": "5208", "origin": "MetaMask", //debug "nonceDetails": { diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index fc6340bd6..321438598 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -25,17 +25,18 @@ const log = require('loglevel') calculating nonces -@param {object} opts - - @property {object} opts.initState initial transaction list default is an empty array + @class + @param {Object} opts + @property {Object} opts.initState initial transaction list default is an empty array @property {Object} opts.networkStore an observable store for network number - @property {Object} opts.blockTracker + @param {Object} opts.blockTracker - An instance of eth-blocktracker @property {Object} opts.provider + @param {Object} opts.provider - A network provider. @property {Object} opts.signTransaction function the signs an ethereumjs-tx @property {function} opts.getGasPrice optional gas price calculator @property {function} opts.signTransaction ethTx signer that returns a rawTx @property {number} opts.txHistoryLimit number *optional* for limiting how many transactions are in state @property {Object} opts.preferencesStore -@class */ class TransactionController extends EventEmitter { @@ -103,21 +104,21 @@ class TransactionController extends EventEmitter { this.emit(`${txMeta.id}:unapproved`, txMeta) } -/** + /** wipes the transactions for a given account - @param address {string} - hex string of the from address for txs being removed -*/ + @param {string} address - hex string of the from address for txs being removed + */ wipeTransactions (address) { this.txStateManager.wipeTransactions(address) } -/** -add a new unapproved transaction to the pipeline -@returns {promise} -@param txParams {object} - txParams for the transaction -@param opts {object} - with the key origin to put the origin on the txMeta + /** + add a new unapproved transaction to the pipeline -*/ + @returns {promise} + @param txParams {Object} - txParams for the transaction + @param opts {Object} - with the key origin to put the origin on the txMeta + */ async newUnapprovedTransaction (txParams, opts = {}) { log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) const initialTxMeta = await this.addUnapprovedTransaction(txParams) @@ -171,7 +172,7 @@ add a new unapproved transaction to the pipeline } /** adds the tx gas defaults: gas && gasPrice - @param txMeta {object} - the txMeta object + @param txMeta {Object} - the txMeta object @returns {promise} resolves with txMeta */ async addTxGasDefaults (txMeta) { @@ -211,7 +212,7 @@ add a new unapproved transaction to the pipeline /** updates the txMeta in the txStateManager - @param txMeta {object} - the updated txMeta + @param txMeta {Object} - the updated txMeta */ async updateTransaction (txMeta) { this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction') @@ -219,7 +220,7 @@ add a new unapproved transaction to the pipeline /** updates and approves the transaction - @param txMeta {object} + @param txMeta {Object} */ async updateAndApproveTransaction (txMeta) { this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction') diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js index e0f4d0fe3..e2c5dadef 100644 --- a/app/scripts/controllers/transactions/nonce-tracker.js +++ b/app/scripts/controllers/transactions/nonce-tracker.js @@ -2,12 +2,12 @@ const EthQuery = require('ethjs-query') const assert = require('assert') const Mutex = require('await-semaphore').Mutex /** - @param opts {object} - + @param opts {Object} - @property {Object} opts.provider a ethereum provider - @property {function} opts.getPendingTransactions a function that returns an array of txMeta - whos status is `submitted` - @property {function} opts.getConfirmedTransactions a function that returns an array of txMeta - whos status is `confirmed` + @property {Function} opts.getPendingTransactions a function that returns an array of txMeta + whosee status is `submitted` + @property {Function} opts.getConfirmedTransactions a function that returns an array of txMeta + whose status is `confirmed` @class */ class NonceTracker { @@ -21,7 +21,7 @@ class NonceTracker { } /** - @returns {object} with the key releaseLock (the gloabl mutex) + @returns {Promise} with the key releaseLock (the gloabl mutex) */ async getGlobalLock () { const globalMutex = this._lookupMutex('global') @@ -31,17 +31,18 @@ class NonceTracker { } /** - this will return an object with the `nextNonce` `nonceDetails` which is an - object with: - highestLocallyConfirmed (nonce), - highestSuggested (either the network nonce or the highestLocallyConfirmed nonce), - nextNetworkNonce (the nonce suggested by the network), - and the releaseLock -
note: releaseLock must be called after adding signed tx to pending transactions - (or discarding)
- - @param address {string} the hex string for the address whos nonce we are calculating - @returns {object} + * @typedef NonceDetails + * @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction. + * @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method. + * @property {number} highetSuggested - The maximum between the other two, the number returned. + */ + + /** + this will return an object with the `nextNonce` `nonceDetails` of type NonceDetails, and the releaseLock + Note: releaseLock must be called after adding a signed tx to pending transactions (or discarding). + + @param address {string} the hex string for the address whose nonce we are calculating + @returns {Promise} */ async getNonceLock (address) { // await global mutex free From 8ffce8b59dc95ffa1af72293f40c21f78fd103bb Mon Sep 17 00:00:00 2001 From: frankiebee Date: Wed, 25 Apr 2018 11:13:51 -0700 Subject: [PATCH 9/9] transactions - more docs and clean ups --- app/scripts/controllers/transactions/index.js | 70 ++++++++++++------- .../lib/tx-state-history-helper.js | 2 +- .../controllers/transactions/nonce-tracker.js | 25 +++++-- .../transactions/pending-tx-tracker.js | 14 ++-- .../controllers/transactions/tx-gas-utils.js | 33 +++++++-- .../transactions/tx-state-manager.js | 27 +++---- 6 files changed, 116 insertions(+), 55 deletions(-) diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 321438598..541f1db73 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -26,17 +26,16 @@ const log = require('loglevel') @class - @param {Object} opts - @property {Object} opts.initState initial transaction list default is an empty array - @property {Object} opts.networkStore an observable store for network number - @param {Object} opts.blockTracker - An instance of eth-blocktracker - @property {Object} opts.provider - @param {Object} opts.provider - A network provider. - @property {Object} opts.signTransaction function the signs an ethereumjs-tx - @property {function} opts.getGasPrice optional gas price calculator - @property {function} opts.signTransaction ethTx signer that returns a rawTx - @property {number} opts.txHistoryLimit number *optional* for limiting how many transactions are in state - @property {Object} opts.preferencesStore + @param {object} - opts + @param {object} opts.initState - initial transaction list default is an empty array + @param {Object} opts.networkStore - an observable store for network number + @param {Object} opts.blockTracker - An instance of eth-blocktracker + @param {Object} opts.provider - A network provider. + @param {Function} opts.signTransaction - function the signs an ethereumjs-tx + @param {Function} [opts.getGasPrice] - optional gas price calculator + @param {Function} opts.signTransaction - ethTx signer that returns a rawTx + @param {Number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state + @param {Object} opts.preferencesStore */ class TransactionController extends EventEmitter { @@ -105,7 +104,7 @@ class TransactionController extends EventEmitter { } /** - wipes the transactions for a given account + Wipes the transactions for a given account @param {string} address - hex string of the from address for txs being removed */ wipeTransactions (address) { @@ -115,9 +114,9 @@ class TransactionController extends EventEmitter { /** add a new unapproved transaction to the pipeline - @returns {promise} - @param txParams {Object} - txParams for the transaction - @param opts {Object} - with the key origin to put the origin on the txMeta + @returns {Promise} the hash of the transaction after being submitted to the network + @param txParams {object} - txParams for the transaction + @param opts {object} - with the key origin to put the origin on the txMeta */ async newUnapprovedTransaction (txParams, opts = {}) { log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) @@ -142,7 +141,7 @@ class TransactionController extends EventEmitter { } /** - validates and generates a txMeta with defaults and puts it in txStateManager + Validates and generates a txMeta with defaults and puts it in txStateManager store @returns {txMeta} @@ -173,7 +172,7 @@ class TransactionController extends EventEmitter { /** adds the tx gas defaults: gas && gasPrice @param txMeta {Object} - the txMeta object - @returns {promise} resolves with txMeta + @returns {Promise} resolves with txMeta */ async addTxGasDefaults (txMeta) { const txParams = txMeta.txParams @@ -190,7 +189,7 @@ class TransactionController extends EventEmitter { } /** - creates a new txMeta with the same txParams as the original + Creates a new txMeta with the same txParams as the original to allow the user to resign the transaction with a higher gas values @param originalTxId {number} - the id of the txMeta that you want to attempt to retry @@ -290,6 +289,7 @@ class TransactionController extends EventEmitter { publishes the raw tx and sets the txMeta to submitted @param txId {number} - the tx's Id @param rawTx {string} - the hex string of the serialized signed transaction + @returns {Promise} */ async publishTransaction (txId, rawTx) { const txMeta = this.txStateManager.getTx(txId) @@ -301,15 +301,16 @@ class TransactionController extends EventEmitter { } /** - convenience method for the ui thats sets the transaction to rejected + Convenience method for the ui thats sets the transaction to rejected @param txId {number} - the tx's Id + @returns {Promise} */ async cancelTransaction (txId) { this.txStateManager.setTxStatusRejected(txId) } /** - sets the txHas on the txMeta + Sets the txHas on the txMeta @param txId {number} - the tx's Id @param txHash {string} - the hash for the txMeta */ @@ -325,20 +326,29 @@ class TransactionController extends EventEmitter { // /** maps methods for convenience*/ _mapMethods () { - /** Returns the state in transaction controller */ + /** @returns the state in transaction controller */ this.getState = () => this.memStore.getState() - /** Returns the network number stored in networkStore */ + /** @returns the network number stored in networkStore */ this.getNetwork = () => this.networkStore.getState() - /** Returns the user selected address */ + /** @returns the user selected address */ this.getSelectedAddress = () => this.preferencesStore.getState().selectedAddress /** Returns an array of transactions whos status is unapproved */ this.getUnapprovedTxCount = () => Object.keys(this.txStateManager.getUnapprovedTxList()).length - /** Returns a number that represents how many transactions have the status submitted*/ + /** + @returns a number that represents how many transactions have the status submitted + @param account {String} - hex prefixed account + */ this.getPendingTxCount = (account) => this.txStateManager.getPendingTransactions(account).length /** see txStateManager */ this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts) } + /** + If transaction controller was rebooted with transactions that are uncompleted + in steps of the transaction signing or user confirmation process it will either + transition txMetas to a failed state or try to redo those tasks. + */ + _onBootCleanUp () { this.txStateManager.getFilteredTxList({ status: 'unapproved', @@ -364,13 +374,13 @@ class TransactionController extends EventEmitter { /** is called in constructor applies the listeners for pendingTxTracker txStateManager and blockTracker -
*/ _setupListners () { this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) this.pendingTxTracker.on('tx:warning', (txMeta) => { this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning') }) + this.pendingTxTracker.on('tx:confirmed', (txId) => this.txStateManager.setTxStatusConfirmed(txId)) this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId)) this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { @@ -393,8 +403,13 @@ class TransactionController extends EventEmitter { } + /** + Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions + in the list have the same nonce + + @param txId {Number} - the txId of the transaction that has been confirmed in a block + */ _markNonceDuplicatesDropped (txId) { - this.txStateManager.setTxStatusConfirmed(txId) // get the confirmed transactions nonce and from address const txMeta = this.txStateManager.getTx(txId) const { nonce, from } = txMeta.txParams @@ -409,6 +424,9 @@ class TransactionController extends EventEmitter { }) } + /** + Updates the memStore in transaction controller + */ _updateMemstore () { const unapprovedTxs = this.txStateManager.getUnapprovedTxList() const selectedAddressTxList = this.txStateManager.getFilteredTxList({ diff --git a/app/scripts/controllers/transactions/lib/tx-state-history-helper.js b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js index 7a57e3cb5..59a4b562c 100644 --- a/app/scripts/controllers/transactions/lib/tx-state-history-helper.js +++ b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js @@ -52,7 +52,7 @@ function replayHistory (_shortHistory) { } /** - @param txMeta {object} + @param txMeta {Object} @returns {object} a clone object of the txMeta with out history */ function snapshotFromTxMeta (txMeta) { diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js index e2c5dadef..f8cdc5523 100644 --- a/app/scripts/controllers/transactions/nonce-tracker.js +++ b/app/scripts/controllers/transactions/nonce-tracker.js @@ -2,11 +2,11 @@ const EthQuery = require('ethjs-query') const assert = require('assert') const Mutex = require('await-semaphore').Mutex /** - @param opts {Object} - - @property {Object} opts.provider a ethereum provider - @property {Function} opts.getPendingTransactions a function that returns an array of txMeta + @param opts {Object} + @param {Object} opts.provider a ethereum provider + @param {Function} opts.getPendingTransactions a function that returns an array of txMeta whosee status is `submitted` - @property {Function} opts.getConfirmedTransactions a function that returns an array of txMeta + @param {Function} opts.getConfirmedTransactions a function that returns an array of txMeta whose status is `confirmed` @class */ @@ -42,7 +42,7 @@ class NonceTracker { Note: releaseLock must be called after adding a signed tx to pending transactions (or discarding). @param address {string} the hex string for the address whose nonce we are calculating - @returns {Promise} + @returns {Promise} */ async getNonceLock (address) { // await global mutex free @@ -146,6 +146,17 @@ class NonceTracker { return highestNonce } + /** + @typedef {object} highestContinuousFrom + @property {string} - name the name for how the nonce was calculated based on the data used + @property {number} - nonce the next suggested nonce + @property {object} - details the provided starting nonce that was used (for debugging) + */ + /** + @param txList {array} - list of txMeta's + @param startPoint {number} - the highest known locally confirmed nonce + @returns {highestContinuousFrom} + */ _getHighestContinuousFrom (txList, startPoint) { const nonces = txList.map((txMeta) => { const nonce = txMeta.txParams.nonce @@ -163,6 +174,10 @@ class NonceTracker { // this is a hotfix for the fact that the blockTracker will // change when the network changes + + /** + @returns {Object} the current blockTracker + */ _getBlockTracker () { return this.provider._blockTracker } diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index f7c9993b3..6e2fcb40b 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -9,10 +9,10 @@ const EthQuery = require('ethjs-query') As well as continues broadcast while in the pending state
@param config {object} - non optional configuration object consists of: - @property {Object} config.provider - @property {Object} config.nonceTracker see nonce tracker - @property {function} config.getPendingTransactions a function for getting an array of transactions, - @property {function} config.publishTransaction a async function for publishing raw transactions, + @param {Object} config.provider - A network provider. + @param {Object} config.nonceTracker see nonce tracker + @param {function} config.getPendingTransactions a function for getting an array of transactions, + @param {function} config.publishTransaction a async function for publishing raw transactions, @class @@ -117,7 +117,7 @@ class PendingTransactionTracker extends EventEmitter { /** resubmits the individual txMeta used in resubmitPendingTxs - @param txMeta {object} - txMeta object + @param txMeta {Object} - txMeta object @param latestBlockNumber {string} - hex string for the latest block number @emits tx:retry @returns txHash {string} @@ -147,7 +147,7 @@ class PendingTransactionTracker extends EventEmitter { } /** Ask the network for the transaction to see if it has been include in a block - @param txMeta {object} - the txMeta object + @param txMeta {Object} - the txMeta object @emits tx:failed @emits tx:confirmed @emits tx:warning @@ -208,7 +208,7 @@ class PendingTransactionTracker extends EventEmitter { /** checks to see if a confirmed txMeta has the same nonce - @param txMeta {object} - txMeta object + @param txMeta {Object} - txMeta object @returns {boolean} */ async _checkIfNonceIsTaken (txMeta) { diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 31a5bfcf4..36b5cdbc9 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -7,19 +7,23 @@ const { const { addHexPrefix } = require('ethereumjs-util') const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. -/* -tx-utils are utility methods for Transaction manager +/** +tx-gas-utils are gas utility methods for Transaction manager its passed ethquery and used to do things like calculate gas of a tx. -@param provider {object} +@param {Object} provider - A network provider. */ -module.exports = class TxGasUtil { +class TxGasUtil { constructor (provider) { this.query = new EthQuery(provider) } + /** + @param txMeta {Object} - the txMeta object + @returns {object} the txMeta object with the gas written to the txParams + */ async analyzeGasUsage (txMeta) { const block = await this.query.getBlockByNumber('latest', true) let estimatedGasHex @@ -39,6 +43,12 @@ module.exports = class TxGasUtil { return txMeta } + /** + Estimates the tx's gas usage + @param txMeta {Object} - the txMeta object + @param blockGasLimitHex {string} - hex string of the block's gas limit + @returns {string} the estimated gas limit as a hex string + */ async estimateTxGas (txMeta, blockGasLimitHex) { const txParams = txMeta.txParams @@ -71,6 +81,12 @@ module.exports = class TxGasUtil { return await this.query.estimateGas(txParams) } + /** + Writes the gas on the txParams in the txMeta + @param txMeta {Object} - the txMeta object to write to + @param blockGasLimitHex {string} - the block gas limit hex + @param estimatedGasHex {string} - the estimated gas hex + */ setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) { txMeta.estimatedGas = addHexPrefix(estimatedGasHex) const txParams = txMeta.txParams @@ -88,6 +104,13 @@ module.exports = class TxGasUtil { return } + /** + Adds a gas buffer with out exceeding the block gas limit + + @param initialGasLimitHex {string} - the initial gas limit to add the buffer too + @param blockGasLimitHex {string} - the block gas limit + @returns {string} the buffered gas limit as a hex string + */ addGasBuffer (initialGasLimitHex, blockGasLimitHex) { const initialGasLimitBn = hexToBn(initialGasLimitHex) const blockGasLimitBn = hexToBn(blockGasLimitHex) @@ -102,3 +125,5 @@ module.exports = class TxGasUtil { return bnToHex(upperGasLimitBn) } } + +module.exports = TxGasUtil \ No newline at end of file diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 328024925..53428c333 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -20,11 +20,11 @@ const { getFinalStates } = require('./lib/util')
- `'confirmed'` the tx has been included in a block.
- `'failed'` the tx failed for some reason, included on tx data.
- `'dropped'` the tx nonce was already used - @param opts {object} - - @property {object} opts.initState with the key transaction {array} - @property {number} opts.txHistoryLimit limit for how many finished + @param opts {object} + @param {object} [opts.initState={ transactions: [] }] initial transactions list with the key transaction {array} + @param {number} [opts.txHistoryLimit] limit for how many finished transactions can hang around in state - @property {function} opts.getNetwork return network number + @param {function} opts.getNetwork return network number @class */ class TransactionStateManager extends EventEmitter { @@ -81,8 +81,9 @@ class TransactionStateManager extends EventEmitter { } /** - @param address {string} - hex prefixed address to sort the txMetas for [optional] - @returns {array} the tx list whos status is submitted + @param [address] {string} - hex prefixed address to sort the txMetas for [optional] + @returns {array} the tx list whos status is submitted if no address is provide + returns all txMetas who's status is submitted for the current network */ getPendingTransactions (address) { const opts = { status: 'submitted' } @@ -91,8 +92,9 @@ class TransactionStateManager extends EventEmitter { } /** - @param address {string} - hex prefixed address to sort the txMetas for [optional] - @returns {array} the tx list whos status is confirmed + @param [address] {string} - hex prefixed address to sort the txMetas for [optional] + @returns {array} the tx list whos status is confirmed if no address is provide + returns all txMetas who's status is confirmed for the current network */ getConfirmedTransactions (address) { const opts = { status: 'confirmed' } @@ -106,7 +108,7 @@ class TransactionStateManager extends EventEmitter { is in its final state it will allso add the key `history` to the txMeta with the snap shot of the original object - @param txMeta {object} + @param txMeta {Object} @returns {object} the txMeta */ addTx (txMeta) { @@ -155,8 +157,8 @@ class TransactionStateManager extends EventEmitter { /** updates the txMeta in the list and adds a history entry - @param txMeta {object} - the txMeta to update - @param note {string} - a not about the update for history + @param txMeta {Object} - the txMeta to update + @param [note] {string} - a not about the update for history */ updateTx (txMeta, note) { // validate txParams @@ -225,6 +227,7 @@ class TransactionStateManager extends EventEmitter { status: 'signed',
err: undefined,
}
+ @param [initialList=this.getTxList()] @returns a {array} of txMeta with all options matching */ @@ -253,7 +256,7 @@ class TransactionStateManager extends EventEmitter { @param key {string} - the key to check @param value - the value your looking for - @param txList {array} - [optional] the list to search. default is the txList + @param [txList=this.getTxList()] {array} - the list to search. default is the txList from txStateManager#getTxList @returns {array} a list of txMetas who matches the search params */