Merge pull request #4042 from MetaMask/tx-controller-rewrite-v3
docs and file organization for txControllerfeature/default_network_editable
commit
dcd04091cc
@ -0,0 +1,92 @@ |
||||
# Transaction Controller |
||||
|
||||
Transaction Controller is an aggregate of sub-controllers and trackers |
||||
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 diagram 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, //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": { |
||||
"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 |
||||
} |
||||
``` |
@ -0,0 +1,99 @@ |
||||
const { |
||||
addHexPrefix, |
||||
isValidAddress, |
||||
} = require('ethereumjs-util') |
||||
|
||||
/** |
||||
@module |
||||
*/ |
||||
module.exports = { |
||||
normalizeTxParams, |
||||
validateTxParams, |
||||
validateFrom, |
||||
validateRecipient, |
||||
getFinalStates, |
||||
} |
||||
|
||||
|
||||
// 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 => 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 (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) |
||||
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`) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
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) { |
||||
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 |
||||
} |
||||
|
||||
/** |
||||
@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
|
||||
] |
||||
} |
||||
|
After Width: | Height: | Size: 138 KiB |
@ -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') |
||||
}) |
||||
}) |
||||
}) |
||||
|
@ -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) |
||||
}) |
||||
}) |
||||
}) |
Loading…
Reference in new issue