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 assert = require('assert') |
||||||
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils') |
const Transaction = require('ethereumjs-tx') |
||||||
const { createTestProviderTools } = require('../stub/provider') |
const BN = require('bn.js') |
||||||
|
|
||||||
describe('Tx Gas Util', function () { |
|
||||||
let txGasUtil, provider, providerResultStub |
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util') |
||||||
beforeEach(function () { |
const TxUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils') |
||||||
providerResultStub = {} |
|
||||||
provider = createTestProviderTools({ scaffold: providerResultStub }).provider |
|
||||||
txGasUtil = new TxGasUtils({ |
describe('txUtils', function () { |
||||||
provider, |
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 assert = require('assert') |
||||||
const Transaction = require('ethereumjs-tx') |
const txUtils = require('../../app/scripts/controllers/transactions/lib/util') |
||||||
const BN = require('bn.js') |
|
||||||
|
|
||||||
|
|
||||||
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util') |
|
||||||
const TxUtils = require('../../app/scripts/lib/tx-gas-utils') |
|
||||||
|
|
||||||
|
|
||||||
describe('txUtils', function () { |
describe('txUtils', function () { |
||||||
let txUtils |
describe('#validateTxParams', function () { |
||||||
|
it('does not throw for positive values', function () { |
||||||
before(function () { |
var sample = { |
||||||
txUtils = new TxUtils(new Proxy({}, { |
from: '0x1678a085c290ebd122dc42cba69373b5953b831d', |
||||||
get: (obj, name) => { |
value: '0x01', |
||||||
return () => {} |
} |
||||||
}, |
txUtils.validateTxParams(sample) |
||||||
})) |
}) |
||||||
}) |
|
||||||
|
|
||||||
describe('chain Id', function () { |
it('returns error for negative values', function () { |
||||||
it('prepares a transaction with the provided chainId', function () { |
var sample = { |
||||||
const txParams = { |
from: '0x1678a085c290ebd122dc42cba69373b5953b831d', |
||||||
to: '0x70ad465e0bab6504002ad58c744ed89c7da38524', |
value: '-0x01', |
||||||
from: '0x69ad465e0bab6504002ad58c744ed89c7da38525', |
} |
||||||
value: '0x0', |
try { |
||||||
gas: '0x7b0c', |
txUtils.validateTxParams(sample) |
||||||
gasPrice: '0x199c82cc00', |
} catch (err) { |
||||||
data: '0x', |
assert.ok(err, 'error') |
||||||
nonce: '0x3', |
|
||||||
chainId: 42, |
|
||||||
} |
} |
||||||
const ethTx = new Transaction(txParams) |
|
||||||
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params') |
|
||||||
}) |
}) |
||||||
}) |
}) |
||||||
|
|
||||||
describe('addGasBuffer', function () { |
describe('#normalizeTxParams', () => { |
||||||
it('multiplies by 1.5, when within block gas limit', function () { |
it('should normalize txParams', () => { |
||||||
// naive estimatedGas: 0x16e360 (1.5 mil)
|
let txParams = { |
||||||
const inputHex = '0x16e360' |
chainId: '0x1', |
||||||
// dummy gas limit: 0x3d4c52 (4 mil)
|
from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402', |
||||||
const blockGasLimitHex = '0x3d4c52' |
to: null, |
||||||
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex) |
data: '68656c6c6f20776f726c64', |
||||||
const inputBn = hexToBn(inputHex) |
random: 'hello world', |
||||||
const outputBn = hexToBn(output) |
} |
||||||
const expectedBn = inputBn.muln(1.5) |
|
||||||
assert(outputBn.eq(expectedBn), 'returns 1.5 the input value') |
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 () { |
describe('#validateRecipient', () => { |
||||||
// naive estimatedGas: 0x16e360 (1.5 mil)
|
it('removes recipient for txParams with 0x when contract data is provided', function () { |
||||||
const inputHex = '0x16e360' |
const zeroRecipientandDataTxParams = { |
||||||
// dummy gas limit: 0x0f4240 (1 mil)
|
from: '0x1678a085c290ebd122dc42cba69373b5953b831d', |
||||||
const blockGasLimitHex = '0x0f4240' |
to: '0x', |
||||||
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex) |
data: 'bytecode', |
||||||
// const inputBn = hexToBn(inputHex)
|
} |
||||||
const outputBn = hexToBn(output) |
const sanitizedTxParams = txUtils.validateRecipient(zeroRecipientandDataTxParams) |
||||||
const expectedBn = hexToBn(inputHex) |
assert.deepEqual(sanitizedTxParams, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', data: 'bytecode' }, 'no recipient with 0x') |
||||||
assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value') |
|
||||||
}) |
}) |
||||||
|
|
||||||
it('buffers up to recommend gas limit recommended ceiling', function () { |
it('should error when recipient is 0x', function () { |
||||||
// naive estimatedGas: 0x16e360 (1.5 mil)
|
const zeroRecipientTxParams = { |
||||||
const inputHex = '0x16e360' |
from: '0x1678a085c290ebd122dc42cba69373b5953b831d', |
||||||
// dummy gas limit: 0x1e8480 (2 mil)
|
to: '0x', |
||||||
const blockGasLimitHex = '0x1e8480' |
} |
||||||
const blockGasLimitBn = hexToBn(blockGasLimitHex) |
assert.throws(() => { txUtils.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address') |
||||||
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') |
|
||||||
}) |
}) |
||||||
}) |
}) |
||||||
|
|
||||||
|
|
||||||
|
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