mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 17:33:23 +01:00
Merge pull request #4042 from MetaMask/tx-controller-rewrite-v3
docs and file organization for txController
This commit is contained in:
commit
dcd04091cc
92
app/scripts/controllers/transactions/README.md
Normal file
92
app/scripts/controllers/transactions/README.md
Normal file
@ -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
|
||||
}
|
||||
```
|
@ -3,28 +3,42 @@ 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')
|
||||
const log = require('loglevel')
|
||||
|
||||
/*
|
||||
/**
|
||||
Transaction Controller is an aggregate of sub-controllers and trackers
|
||||
composing them in a way to be exposed to the metamask controller
|
||||
- txStateManager
|
||||
<br>- txStateManager
|
||||
responsible for the state of a transaction and
|
||||
storing the transaction
|
||||
- pendingTxTracker
|
||||
<br>- pendingTxTracker
|
||||
watching blocks for transactions to be include
|
||||
and emitting confirmed events
|
||||
- txGasUtil
|
||||
<br>- txGasUtil
|
||||
gas calculations and safety buffering
|
||||
- nonceTracker
|
||||
<br>- nonceTracker
|
||||
calculating nonces
|
||||
|
||||
|
||||
@class
|
||||
@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
|
||||
*/
|
||||
|
||||
module.exports = class TransactionController extends EventEmitter {
|
||||
class TransactionController extends EventEmitter {
|
||||
constructor (opts) {
|
||||
super()
|
||||
this.networkStore = opts.networkStore || new ObservableStore({})
|
||||
@ -38,45 +52,19 @@ module.exports = 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.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({
|
||||
@ -88,60 +76,14 @@ 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())
|
||||
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)
|
||||
@ -152,16 +94,30 @@ module.exports = class TransactionController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
wipeTransactions (address) {
|
||||
this.txStateManager.wipeTransactions(address)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
/**
|
||||
Wipes the transactions for a given account
|
||||
@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<string>} 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)}`)
|
||||
const initialTxMeta = await this.addUnapprovedTransaction(txParams)
|
||||
@ -184,17 +140,24 @@ 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 = 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)
|
||||
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)
|
||||
@ -206,21 +169,33 @@ 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<object>} resolves with txMeta
|
||||
*/
|
||||
async addTxGasDefaults (txMeta) {
|
||||
const txParams = txMeta.txParams
|
||||
// ensure value
|
||||
txParams.value = txParams.value ? ethUtil.addHexPrefix(txParams.value) : '0x0'
|
||||
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
|
||||
let gasPrice = txParams.gasPrice
|
||||
if (!gasPrice) {
|
||||
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
|
||||
@ -234,15 +209,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 {
|
||||
@ -274,7 +265,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
|
||||
@ -290,6 +285,12 @@ 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
|
||||
@returns {Promise<void>}
|
||||
*/
|
||||
async publishTransaction (txId, rawTx) {
|
||||
const txMeta = this.txStateManager.getTx(txId)
|
||||
txMeta.rawTx = rawTx
|
||||
@ -299,11 +300,20 @@ 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
|
||||
@returns {Promise<void>}
|
||||
*/
|
||||
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)
|
||||
@ -314,63 +324,92 @@ 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
|
||||
@param account {String} - hex prefixed account
|
||||
*/
|
||||
this.getPendingTxCount = (account) => this.txStateManager.getPendingTransactions(account).length
|
||||
/** see txStateManager */
|
||||
this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts)
|
||||
}
|
||||
|
||||
_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),
|
||||
}
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
|
||||
// apply only keys in the whiteList
|
||||
const normalizedTxParams = {}
|
||||
Object.keys(whiteList).forEach((key) => {
|
||||
if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key])
|
||||
_onBootCleanUp () {
|
||||
this.txStateManager.getFilteredTxList({
|
||||
status: 'unapproved',
|
||||
loadingDefaults: true,
|
||||
}).forEach((tx) => {
|
||||
this.addTxGasDefaults(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)
|
||||
})
|
||||
})
|
||||
|
||||
return normalizedTxParams
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
_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.`)
|
||||
/**
|
||||
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) => {
|
||||
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))
|
||||
|
||||
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
|
||||
}
|
||||
/**
|
||||
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
|
||||
@ -385,6 +424,9 @@ module.exports = class TransactionController extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Updates the memStore in transaction controller
|
||||
*/
|
||||
_updateMemstore () {
|
||||
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
|
||||
const selectedAddressTxList = this.txStateManager.getFilteredTxList({
|
||||
@ -394,3 +436,5 @@ module.exports = class TransactionController extends EventEmitter {
|
||||
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TransactionController
|
@ -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)
|
99
app/scripts/controllers/transactions/lib/util.js
Normal file
99
app/scripts/controllers/transactions/lib/util.js
Normal file
@ -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
|
||||
]
|
||||
}
|
||||
|
@ -1,7 +1,15 @@
|
||||
const EthQuery = require('ethjs-query')
|
||||
const assert = require('assert')
|
||||
const Mutex = require('await-semaphore').Mutex
|
||||
|
||||
/**
|
||||
@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`
|
||||
@param {Function} opts.getConfirmedTransactions a function that returns an array of txMeta
|
||||
whose status is `confirmed`
|
||||
@class
|
||||
*/
|
||||
class NonceTracker {
|
||||
|
||||
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
|
||||
@ -12,6 +20,9 @@ class NonceTracker {
|
||||
this.lockMap = {}
|
||||
}
|
||||
|
||||
/**
|
||||
@returns {Promise<Object>} with the key releaseLock (the gloabl mutex)
|
||||
*/
|
||||
async getGlobalLock () {
|
||||
const globalMutex = this._lookupMutex('global')
|
||||
// await global mutex free
|
||||
@ -19,8 +30,20 @@ class NonceTracker {
|
||||
return { releaseLock }
|
||||
}
|
||||
|
||||
// releaseLock must be called
|
||||
// releaseLock must be called after adding signed tx to pending transactions (or discarding)
|
||||
/**
|
||||
* @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<NonceDetails>}
|
||||
*/
|
||||
async getNonceLock (address) {
|
||||
// await global mutex free
|
||||
await this._globalMutexFree()
|
||||
@ -123,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
|
||||
@ -140,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
|
||||
}
|
@ -1,23 +1,24 @@
|
||||
const EventEmitter = require('events')
|
||||
const log = require('loglevel')
|
||||
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<br>
|
||||
go from a pending state to a confirmed (mined in a block) state<br>
|
||||
<br>
|
||||
As well as continues broadcast while in the pending state
|
||||
<br>
|
||||
@param config {object} - non optional configuration object consists of:
|
||||
@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,
|
||||
|
||||
~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 +30,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 +58,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 +74,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 +115,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 +145,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 +190,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
|
||||
@ -171,12 +200,17 @@ 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()
|
||||
}
|
||||
|
||||
/**
|
||||
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 +219,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
||||
})
|
||||
return sameNonce.length > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = PendingTransactionTracker
|
@ -3,22 +3,27 @@ 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.
|
||||
|
||||
/*
|
||||
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 {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
|
||||
@ -38,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
|
||||
|
||||
@ -70,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
|
||||
@ -87,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)
|
||||
@ -100,4 +124,6 @@ module.exports = class TxGasUtil {
|
||||
// otherwise use blockGasLimit
|
||||
return bnToHex(upperGasLimitBn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TxGasUtil
|
@ -1,22 +1,33 @@
|
||||
const extend = require('xtend')
|
||||
const EventEmitter = require('events')
|
||||
const ObservableStore = require('obs-store')
|
||||
const createId = require('./random-id')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const txStateHistoryHelper = require('./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 txStateHistoryHelper = require('./lib/tx-state-history-helper')
|
||||
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
|
||||
<br>statuses:
|
||||
<br> - `'unapproved'` the user has not responded
|
||||
<br> - `'rejected'` the user has responded no!
|
||||
<br> - `'approved'` the user has approved the tx
|
||||
<br> - `'signed'` the tx is signed
|
||||
<br> - `'submitted'` the tx is sent to a server
|
||||
<br> - `'confirmed'` the tx has been included in a block.
|
||||
<br> - `'failed'` the tx failed for some reason, included on tx data.
|
||||
<br> - `'dropped'` the tx nonce was already used
|
||||
@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
|
||||
@param {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,37 @@ 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 if no address is provide
|
||||
returns all txMetas who's status is submitted for the current network
|
||||
*/
|
||||
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 if no address is provide
|
||||
returns all txMetas who's status is confirmed for the current network
|
||||
*/
|
||||
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`)
|
||||
@ -92,7 +134,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')
|
||||
const index = transactions.findIndex((metaTx) => {
|
||||
return getFinalStates().includes(metaTx.status)
|
||||
})
|
||||
if (index !== -1) {
|
||||
transactions.splice(index, 1)
|
||||
}
|
||||
@ -101,12 +145,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) {
|
||||
@ -134,16 +187,23 @@ 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
|
||||
validateTxParams(txParams) {
|
||||
/**
|
||||
validates txParams members by type
|
||||
@param txParams {object} - txParams to validate
|
||||
*/
|
||||
validateTxParams (txParams) {
|
||||
Object.keys(txParams).forEach((key) => {
|
||||
const value = txParams[key]
|
||||
// validate types
|
||||
@ -159,17 +219,19 @@ 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:<br>
|
||||
let <code>thingsToLookFor = {<br>
|
||||
to: '0x0..',<br>
|
||||
from: '0x0..',<br>
|
||||
status: 'signed',<br>
|
||||
err: undefined,<br>
|
||||
}<br></code>
|
||||
@param [initialList=this.getTxList()]
|
||||
@returns a {array} of txMeta with all
|
||||
options matching
|
||||
|
||||
*/
|
||||
/*
|
||||
****************HINT****************
|
||||
| `err: undefined` is like looking |
|
||||
| for a tx with no err |
|
||||
@ -190,7 +252,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=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
|
||||
*/
|
||||
getTxsByMetaData (key, value, txList = this.getTxList()) {
|
||||
return txList.filter((txMeta) => {
|
||||
if (txMeta.txParams[key]) {
|
||||
@ -203,33 +272,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()
|
||||
@ -237,17 +324,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 = {
|
||||
@ -258,6 +357,11 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
||||
this._setTxStatus(txId, 'failed')
|
||||
}
|
||||
|
||||
/**
|
||||
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()
|
||||
@ -273,9 +377,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 +386,15 @@ 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
|
||||
|
||||
/**
|
||||
@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
|
||||
@ -295,9 +407,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
|
@ -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 = {
|
||||
|
BIN
docs/transaction-flow.png
Normal file
BIN
docs/transaction-flow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 138 KiB |
@ -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 = {}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -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')
|
||||
@ -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 = {
|
||||
|
@ -1,14 +1,77 @@
|
||||
const assert = require('assert')
|
||||
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
|
||||
const { createTestProviderTools } = require('../stub/provider')
|
||||
const Transaction = require('ethereumjs-tx')
|
||||
const BN = require('bn.js')
|
||||
|
||||
describe('Tx Gas Util', function () {
|
||||
let txGasUtil, provider, providerResultStub
|
||||
beforeEach(function () {
|
||||
providerResultStub = {}
|
||||
provider = createTestProviderTools({ scaffold: providerResultStub }).provider
|
||||
txGasUtil = new TxGasUtils({
|
||||
provider,
|
||||
|
||||
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,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 () {
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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 () {
|
||||
|
@ -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('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,
|
||||
describe('#validateTxParams', function () {
|
||||
it('does not throw for positive values', function () {
|
||||
var sample = {
|
||||
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||
value: '0x01',
|
||||
}
|
||||
txUtils.validateTxParams(sample)
|
||||
})
|
||||
|
||||
it('returns error for negative values', function () {
|
||||
var sample = {
|
||||
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||
value: '-0x01',
|
||||
}
|
||||
try {
|
||||
txUtils.validateTxParams(sample)
|
||||
} catch (err) {
|
||||
assert.ok(err, 'error')
|
||||
}
|
||||
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')
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
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('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('should error when recipient is 0x', function () {
|
||||
const zeroRecipientTxParams = {
|
||||
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||
to: '0x',
|
||||
}
|
||||
assert.throws(() => { txUtils.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
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
Block a user