mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +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 ethUtil = require('ethereumjs-util')
|
||||||
const Transaction = require('ethereumjs-tx')
|
const Transaction = require('ethereumjs-tx')
|
||||||
const EthQuery = require('ethjs-query')
|
const EthQuery = require('ethjs-query')
|
||||||
const TransactionStateManager = require('../lib/tx-state-manager')
|
const TransactionStateManager = require('./tx-state-manager')
|
||||||
const TxGasUtil = require('../lib/tx-gas-utils')
|
const TxGasUtil = require('./tx-gas-utils')
|
||||||
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
|
const PendingTransactionTracker = require('./pending-tx-tracker')
|
||||||
const NonceTracker = require('../lib/nonce-tracker')
|
const NonceTracker = require('./nonce-tracker')
|
||||||
|
const txUtils = require('./lib/util')
|
||||||
const log = require('loglevel')
|
const log = require('loglevel')
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Transaction Controller is an aggregate of sub-controllers and trackers
|
Transaction Controller is an aggregate of sub-controllers and trackers
|
||||||
composing them in a way to be exposed to the metamask controller
|
composing them in a way to be exposed to the metamask controller
|
||||||
- txStateManager
|
<br>- txStateManager
|
||||||
responsible for the state of a transaction and
|
responsible for the state of a transaction and
|
||||||
storing the transaction
|
storing the transaction
|
||||||
- pendingTxTracker
|
<br>- pendingTxTracker
|
||||||
watching blocks for transactions to be include
|
watching blocks for transactions to be include
|
||||||
and emitting confirmed events
|
and emitting confirmed events
|
||||||
- txGasUtil
|
<br>- txGasUtil
|
||||||
gas calculations and safety buffering
|
gas calculations and safety buffering
|
||||||
- nonceTracker
|
<br>- nonceTracker
|
||||||
calculating nonces
|
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) {
|
constructor (opts) {
|
||||||
super()
|
super()
|
||||||
this.networkStore = opts.networkStore || new ObservableStore({})
|
this.networkStore = opts.networkStore || new ObservableStore({})
|
||||||
@ -38,45 +52,19 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
this.query = new EthQuery(this.provider)
|
this.query = new EthQuery(this.provider)
|
||||||
this.txGasUtil = new TxGasUtil(this.provider)
|
this.txGasUtil = new TxGasUtil(this.provider)
|
||||||
|
|
||||||
|
this._mapMethods()
|
||||||
this.txStateManager = new TransactionStateManager({
|
this.txStateManager = new TransactionStateManager({
|
||||||
initState: opts.initState,
|
initState: opts.initState,
|
||||||
txHistoryLimit: opts.txHistoryLimit,
|
txHistoryLimit: opts.txHistoryLimit,
|
||||||
getNetwork: this.getNetwork.bind(this),
|
getNetwork: this.getNetwork.bind(this),
|
||||||
})
|
})
|
||||||
|
this._onBootCleanUp()
|
||||||
this.txStateManager.getFilteredTxList({
|
|
||||||
status: 'unapproved',
|
|
||||||
loadingDefaults: true,
|
|
||||||
}).forEach((tx) => {
|
|
||||||
this.addTxDefaults(tx)
|
|
||||||
.then((txMeta) => {
|
|
||||||
txMeta.loadingDefaults = false
|
|
||||||
this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot')
|
|
||||||
}).catch((error) => {
|
|
||||||
this.txStateManager.setTxStatusFailed(tx.id, error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.txStateManager.getFilteredTxList({
|
|
||||||
status: 'approved',
|
|
||||||
}).forEach((txMeta) => {
|
|
||||||
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
|
|
||||||
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
this.store = this.txStateManager.store
|
this.store = this.txStateManager.store
|
||||||
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
|
|
||||||
this.nonceTracker = new NonceTracker({
|
this.nonceTracker = new NonceTracker({
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
|
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
|
||||||
getConfirmedTransactions: (address) => {
|
getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
|
||||||
return this.txStateManager.getFilteredTxList({
|
|
||||||
from: address,
|
|
||||||
status: 'confirmed',
|
|
||||||
err: undefined,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pendingTxTracker = new PendingTransactionTracker({
|
this.pendingTxTracker = new PendingTransactionTracker({
|
||||||
@ -88,60 +76,14 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
|
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
|
||||||
|
this._setupListners()
|
||||||
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))
|
|
||||||
// memstore is computed from a few different stores
|
// memstore is computed from a few different stores
|
||||||
this._updateMemstore()
|
this._updateMemstore()
|
||||||
this.txStateManager.store.subscribe(() => this._updateMemstore())
|
this.txStateManager.store.subscribe(() => this._updateMemstore())
|
||||||
this.networkStore.subscribe(() => this._updateMemstore())
|
this.networkStore.subscribe(() => this._updateMemstore())
|
||||||
this.preferencesStore.subscribe(() => this._updateMemstore())
|
this.preferencesStore.subscribe(() => this._updateMemstore())
|
||||||
}
|
}
|
||||||
|
/** @returns {number} the chainId*/
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
getChainId () {
|
getChainId () {
|
||||||
const networkState = this.networkStore.getState()
|
const networkState = this.networkStore.getState()
|
||||||
const getChainId = parseInt(networkState)
|
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
|
||||||
}
|
@emits ${txMeta.id}:unapproved
|
||||||
|
*/
|
||||||
// Adds a tx to the txlist
|
|
||||||
addTx (txMeta) {
|
addTx (txMeta) {
|
||||||
this.txStateManager.addTx(txMeta)
|
this.txStateManager.addTx(txMeta)
|
||||||
this.emit(`${txMeta.id}:unapproved`, 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 = {}) {
|
async newUnapprovedTransaction (txParams, opts = {}) {
|
||||||
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
|
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
|
||||||
const initialTxMeta = await this.addUnapprovedTransaction(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) {
|
async addUnapprovedTransaction (txParams) {
|
||||||
// validate
|
// validate
|
||||||
const normalizedTxParams = this._normalizeTxParams(txParams)
|
const normalizedTxParams = txUtils.normalizeTxParams(txParams)
|
||||||
this._validateTxParams(normalizedTxParams)
|
txUtils.validateTxParams(normalizedTxParams)
|
||||||
// construct txMeta
|
// construct txMeta
|
||||||
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
|
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
|
||||||
this.addTx(txMeta)
|
this.addTx(txMeta)
|
||||||
this.emit('newUnapprovedTx', txMeta)
|
this.emit('newUnapprovedTx', txMeta)
|
||||||
// add default tx params
|
// add default tx params
|
||||||
try {
|
try {
|
||||||
txMeta = await this.addTxDefaults(txMeta)
|
txMeta = await this.addTxGasDefaults(txMeta)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
this.txStateManager.setTxStatusFailed(txMeta.id, error)
|
this.txStateManager.setTxStatusFailed(txMeta.id, error)
|
||||||
@ -206,21 +169,33 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
|
|
||||||
return txMeta
|
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
|
const txParams = txMeta.txParams
|
||||||
// ensure value
|
// ensure value
|
||||||
|
txParams.value = txParams.value ? ethUtil.addHexPrefix(txParams.value) : '0x0'
|
||||||
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
|
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
|
||||||
let gasPrice = txParams.gasPrice
|
let gasPrice = txParams.gasPrice
|
||||||
if (!gasPrice) {
|
if (!gasPrice) {
|
||||||
gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice()
|
gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice()
|
||||||
}
|
}
|
||||||
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
|
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
|
||||||
txParams.value = txParams.value || '0x0'
|
|
||||||
// set gasLimit
|
// set gasLimit
|
||||||
return await this.txGasUtil.analyzeGasUsage(txMeta)
|
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) {
|
async retryTransaction (originalTxId) {
|
||||||
const originalTxMeta = this.txStateManager.getTx(originalTxId)
|
const originalTxMeta = this.txStateManager.getTx(originalTxId)
|
||||||
const lastGasPrice = originalTxMeta.txParams.gasPrice
|
const lastGasPrice = originalTxMeta.txParams.gasPrice
|
||||||
@ -234,15 +209,31 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
return txMeta
|
return txMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
updates the txMeta in the txStateManager
|
||||||
|
@param txMeta {Object} - the updated txMeta
|
||||||
|
*/
|
||||||
async updateTransaction (txMeta) {
|
async updateTransaction (txMeta) {
|
||||||
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
|
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
updates and approves the transaction
|
||||||
|
@param txMeta {Object}
|
||||||
|
*/
|
||||||
async updateAndApproveTransaction (txMeta) {
|
async updateAndApproveTransaction (txMeta) {
|
||||||
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
|
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
|
||||||
await this.approveTransaction(txMeta.id)
|
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) {
|
async approveTransaction (txId) {
|
||||||
let nonceLock
|
let nonceLock
|
||||||
try {
|
try {
|
||||||
@ -274,7 +265,11 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
throw err
|
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) {
|
async signTransaction (txId) {
|
||||||
const txMeta = this.txStateManager.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
// add network/chain id
|
// add network/chain id
|
||||||
@ -290,6 +285,12 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
return rawTx
|
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) {
|
async publishTransaction (txId, rawTx) {
|
||||||
const txMeta = this.txStateManager.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
txMeta.rawTx = rawTx
|
txMeta.rawTx = rawTx
|
||||||
@ -299,11 +300,20 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
this.txStateManager.setTxStatusSubmitted(txId)
|
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) {
|
async cancelTransaction (txId) {
|
||||||
this.txStateManager.setTxStatusRejected(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) {
|
setTxHash (txId, txHash) {
|
||||||
// Add the tx hash to the persisted meta-tx object
|
// Add the tx hash to the persisted meta-tx object
|
||||||
const txMeta = this.txStateManager.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
@ -314,63 +324,92 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
//
|
//
|
||||||
// PRIVATE METHODS
|
// 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
|
If transaction controller was rebooted with transactions that are uncompleted
|
||||||
const whiteList = {
|
in steps of the transaction signing or user confirmation process it will either
|
||||||
from: from => ethUtil.addHexPrefix(from).toLowerCase(),
|
transition txMetas to a failed state or try to redo those tasks.
|
||||||
to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(),
|
*/
|
||||||
nonce: nonce => ethUtil.addHexPrefix(nonce),
|
|
||||||
value: value => ethUtil.addHexPrefix(value),
|
|
||||||
data: data => ethUtil.addHexPrefix(data),
|
|
||||||
gas: gas => ethUtil.addHexPrefix(gas),
|
|
||||||
gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice),
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply only keys in the whiteList
|
_onBootCleanUp () {
|
||||||
const normalizedTxParams = {}
|
this.txStateManager.getFilteredTxList({
|
||||||
Object.keys(whiteList).forEach((key) => {
|
status: 'unapproved',
|
||||||
if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key])
|
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)
|
is called in constructor applies the listeners for pendingTxTracker txStateManager
|
||||||
this._validateRecipient(txParams)
|
and blockTracker
|
||||||
if ('value' in txParams) {
|
*/
|
||||||
const value = txParams.value.toString()
|
_setupListners () {
|
||||||
if (value.includes('-')) {
|
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
|
||||||
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
|
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`)
|
Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions
|
||||||
if (!ethUtil.isValidAddress(txParams.from)) throw new Error('Invalid from address')
|
in the list have the same nonce
|
||||||
}
|
|
||||||
|
|
||||||
_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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@param txId {Number} - the txId of the transaction that has been confirmed in a block
|
||||||
|
*/
|
||||||
_markNonceDuplicatesDropped (txId) {
|
_markNonceDuplicatesDropped (txId) {
|
||||||
this.txStateManager.setTxStatusConfirmed(txId)
|
|
||||||
// get the confirmed transactions nonce and from address
|
// get the confirmed transactions nonce and from address
|
||||||
const txMeta = this.txStateManager.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
const { nonce, from } = txMeta.txParams
|
const { nonce, from } = txMeta.txParams
|
||||||
@ -385,6 +424,9 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Updates the memStore in transaction controller
|
||||||
|
*/
|
||||||
_updateMemstore () {
|
_updateMemstore () {
|
||||||
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
|
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
|
||||||
const selectedAddressTxList = this.txStateManager.getFilteredTxList({
|
const selectedAddressTxList = this.txStateManager.getFilteredTxList({
|
||||||
@ -394,3 +436,5 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
|
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = TransactionController
|
@ -1,6 +1,6 @@
|
|||||||
const jsonDiffer = require('fast-json-patch')
|
const jsonDiffer = require('fast-json-patch')
|
||||||
const clone = require('clone')
|
const clone = require('clone')
|
||||||
|
/** @module*/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
generateHistoryEntry,
|
generateHistoryEntry,
|
||||||
replayHistory,
|
replayHistory,
|
||||||
@ -8,7 +8,11 @@ module.exports = {
|
|||||||
migrateFromSnapshotsToDiffs,
|
migrateFromSnapshotsToDiffs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
converts non-initial history entries into diffs
|
||||||
|
@param longHistory {array}
|
||||||
|
@returns {array}
|
||||||
|
*/
|
||||||
function migrateFromSnapshotsToDiffs (longHistory) {
|
function migrateFromSnapshotsToDiffs (longHistory) {
|
||||||
return (
|
return (
|
||||||
longHistory
|
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) {
|
function generateHistoryEntry (previousState, newState, note) {
|
||||||
const entry = jsonDiffer.compare(previousState, newState)
|
const entry = jsonDiffer.compare(previousState, newState)
|
||||||
// Add a note to the first op, since it breaks if we append it to the entry
|
// 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
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Recovers previous txMeta state obj
|
||||||
|
@return {object}
|
||||||
|
*/
|
||||||
function replayHistory (_shortHistory) {
|
function replayHistory (_shortHistory) {
|
||||||
const shortHistory = clone(_shortHistory)
|
const shortHistory = clone(_shortHistory)
|
||||||
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
|
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) {
|
function snapshotFromTxMeta (txMeta) {
|
||||||
// create txMeta snapshot for history
|
// create txMeta snapshot for history
|
||||||
const snapshot = clone(txMeta)
|
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 EthQuery = require('ethjs-query')
|
||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const Mutex = require('await-semaphore').Mutex
|
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 {
|
class NonceTracker {
|
||||||
|
|
||||||
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
|
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
|
||||||
@ -12,6 +20,9 @@ class NonceTracker {
|
|||||||
this.lockMap = {}
|
this.lockMap = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@returns {Promise<Object>} with the key releaseLock (the gloabl mutex)
|
||||||
|
*/
|
||||||
async getGlobalLock () {
|
async getGlobalLock () {
|
||||||
const globalMutex = this._lookupMutex('global')
|
const globalMutex = this._lookupMutex('global')
|
||||||
// await global mutex free
|
// await global mutex free
|
||||||
@ -19,8 +30,20 @@ class NonceTracker {
|
|||||||
return { releaseLock }
|
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) {
|
async getNonceLock (address) {
|
||||||
// await global mutex free
|
// await global mutex free
|
||||||
await this._globalMutexFree()
|
await this._globalMutexFree()
|
||||||
@ -123,6 +146,17 @@ class NonceTracker {
|
|||||||
return highestNonce
|
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) {
|
_getHighestContinuousFrom (txList, startPoint) {
|
||||||
const nonces = txList.map((txMeta) => {
|
const nonces = txList.map((txMeta) => {
|
||||||
const nonce = txMeta.txParams.nonce
|
const nonce = txMeta.txParams.nonce
|
||||||
@ -140,6 +174,10 @@ class NonceTracker {
|
|||||||
|
|
||||||
// this is a hotfix for the fact that the blockTracker will
|
// this is a hotfix for the fact that the blockTracker will
|
||||||
// change when the network changes
|
// change when the network changes
|
||||||
|
|
||||||
|
/**
|
||||||
|
@returns {Object} the current blockTracker
|
||||||
|
*/
|
||||||
_getBlockTracker () {
|
_getBlockTracker () {
|
||||||
return this.provider._blockTracker
|
return this.provider._blockTracker
|
||||||
}
|
}
|
@ -1,23 +1,24 @@
|
|||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
|
const log = require('loglevel')
|
||||||
const EthQuery = require('ethjs-query')
|
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
|
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) {
|
constructor (config) {
|
||||||
super()
|
super()
|
||||||
this.query = new EthQuery(config.provider)
|
this.query = new EthQuery(config.provider)
|
||||||
@ -29,8 +30,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
this._checkPendingTxs()
|
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) {
|
checkForTxInBlock (block) {
|
||||||
const signedTxList = this.getPendingTransactions()
|
const signedTxList = this.getPendingTransactions()
|
||||||
if (!signedTxList.length) return
|
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 }) {
|
queryPendingTxs ({ oldBlock, newBlock }) {
|
||||||
// check pending transactions on start
|
// check pending transactions on start
|
||||||
if (!oldBlock) {
|
if (!oldBlock) {
|
||||||
@ -63,7 +74,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
if (diff > 1) this._checkPendingTxs()
|
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) {
|
resubmitPendingTxs (block) {
|
||||||
const pending = this.getPendingTransactions()
|
const pending = this.getPendingTransactions()
|
||||||
// only try resubmitting if their are transactions to resubmit
|
// 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) {
|
async _resubmitTx (txMeta, latestBlockNumber) {
|
||||||
if (!txMeta.firstRetryBlockNumber) {
|
if (!txMeta.firstRetryBlockNumber) {
|
||||||
this.emit('tx:block-update', txMeta, latestBlockNumber)
|
this.emit('tx:block-update', txMeta, latestBlockNumber)
|
||||||
@ -123,7 +145,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
this.emit('tx:retry', txMeta)
|
this.emit('tx:retry', txMeta)
|
||||||
return txHash
|
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) {
|
async _checkPendingTx (txMeta) {
|
||||||
const txHash = txMeta.hash
|
const txHash = txMeta.hash
|
||||||
const txId = txMeta.id
|
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 () {
|
async _checkPendingTxs () {
|
||||||
const signedTxList = this.getPendingTransactions()
|
const signedTxList = this.getPendingTransactions()
|
||||||
// in order to keep the nonceTracker accurate we block it while updating pending transactions
|
// 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 {
|
try {
|
||||||
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
|
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('PendingTransactionWatcher - Error updating pending transactions')
|
log.error('PendingTransactionWatcher - Error updating pending transactions')
|
||||||
console.error(err)
|
log.error(err)
|
||||||
}
|
}
|
||||||
nonceGlobalLock.releaseLock()
|
nonceGlobalLock.releaseLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
checks to see if a confirmed txMeta has the same nonce
|
||||||
|
@param txMeta {Object} - txMeta object
|
||||||
|
@returns {boolean}
|
||||||
|
*/
|
||||||
async _checkIfNonceIsTaken (txMeta) {
|
async _checkIfNonceIsTaken (txMeta) {
|
||||||
const address = txMeta.txParams.from
|
const address = txMeta.txParams.from
|
||||||
const completed = this.getCompletedTransactions(address)
|
const completed = this.getCompletedTransactions(address)
|
||||||
@ -185,5 +219,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
})
|
})
|
||||||
return sameNonce.length > 0
|
return sameNonce.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = PendingTransactionTracker
|
@ -3,22 +3,27 @@ const {
|
|||||||
hexToBn,
|
hexToBn,
|
||||||
BnMultiplyByFraction,
|
BnMultiplyByFraction,
|
||||||
bnToHex,
|
bnToHex,
|
||||||
} = require('./util')
|
} = require('../../lib/util')
|
||||||
const { addHexPrefix } = require('ethereumjs-util')
|
const { addHexPrefix } = require('ethereumjs-util')
|
||||||
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
|
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
|
its passed ethquery
|
||||||
and used to do things like calculate gas of a tx.
|
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) {
|
constructor (provider) {
|
||||||
this.query = new EthQuery(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) {
|
async analyzeGasUsage (txMeta) {
|
||||||
const block = await this.query.getBlockByNumber('latest', true)
|
const block = await this.query.getBlockByNumber('latest', true)
|
||||||
let estimatedGasHex
|
let estimatedGasHex
|
||||||
@ -38,6 +43,12 @@ module.exports = class TxGasUtil {
|
|||||||
return txMeta
|
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) {
|
async estimateTxGas (txMeta, blockGasLimitHex) {
|
||||||
const txParams = txMeta.txParams
|
const txParams = txMeta.txParams
|
||||||
|
|
||||||
@ -70,6 +81,12 @@ module.exports = class TxGasUtil {
|
|||||||
return await this.query.estimateGas(txParams)
|
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) {
|
setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) {
|
||||||
txMeta.estimatedGas = addHexPrefix(estimatedGasHex)
|
txMeta.estimatedGas = addHexPrefix(estimatedGasHex)
|
||||||
const txParams = txMeta.txParams
|
const txParams = txMeta.txParams
|
||||||
@ -87,6 +104,13 @@ module.exports = class TxGasUtil {
|
|||||||
return
|
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) {
|
addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
|
||||||
const initialGasLimitBn = hexToBn(initialGasLimitHex)
|
const initialGasLimitBn = hexToBn(initialGasLimitHex)
|
||||||
const blockGasLimitBn = hexToBn(blockGasLimitHex)
|
const blockGasLimitBn = hexToBn(blockGasLimitHex)
|
||||||
@ -101,3 +125,5 @@ module.exports = class TxGasUtil {
|
|||||||
return bnToHex(upperGasLimitBn)
|
return bnToHex(upperGasLimitBn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = TxGasUtil
|
@ -1,22 +1,33 @@
|
|||||||
const extend = require('xtend')
|
const extend = require('xtend')
|
||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const createId = require('./random-id')
|
|
||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
const txStateHistoryHelper = require('./tx-state-history-helper')
|
const txStateHistoryHelper = require('./lib/tx-state-history-helper')
|
||||||
|
const createId = require('../../lib/random-id')
|
||||||
// STATUS METHODS
|
const { getFinalStates } = require('./lib/util')
|
||||||
// statuses:
|
/**
|
||||||
// - `'unapproved'` the user has not responded
|
TransactionStateManager is responsible for the state of a transaction and
|
||||||
// - `'rejected'` the user has responded no!
|
storing the transaction
|
||||||
// - `'approved'` the user has approved the tx
|
it also has some convenience methods for finding subsets of transactions
|
||||||
// - `'signed'` the tx is signed
|
*
|
||||||
// - `'submitted'` the tx is sent to a server
|
*STATUS METHODS
|
||||||
// - `'confirmed'` the tx has been included in a block.
|
<br>statuses:
|
||||||
// - `'failed'` the tx failed for some reason, included on tx data.
|
<br> - `'unapproved'` the user has not responded
|
||||||
// - `'dropped'` the tx nonce was already used
|
<br> - `'rejected'` the user has responded no!
|
||||||
|
<br> - `'approved'` the user has approved the tx
|
||||||
module.exports = class TransactionStateManager extends EventEmitter {
|
<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 }) {
|
constructor ({ initState, txHistoryLimit, getNetwork }) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
@ -28,6 +39,10 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
this.getNetwork = getNetwork
|
this.getNetwork = getNetwork
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@param opts {object} - the object to use when overwriting defaults
|
||||||
|
@returns {txMeta} the default txMeta object
|
||||||
|
*/
|
||||||
generateTxMeta (opts) {
|
generateTxMeta (opts) {
|
||||||
return extend({
|
return extend({
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@ -38,17 +53,25 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
}, opts)
|
}, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@returns {array} of txMetas that have been filtered for only the current network
|
||||||
|
*/
|
||||||
getTxList () {
|
getTxList () {
|
||||||
const network = this.getNetwork()
|
const network = this.getNetwork()
|
||||||
const fullTxList = this.getFullTxList()
|
const fullTxList = this.getFullTxList()
|
||||||
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
|
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@returns {array} of all the txMetas in store
|
||||||
|
*/
|
||||||
getFullTxList () {
|
getFullTxList () {
|
||||||
return this.store.getState().transactions
|
return this.store.getState().transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the tx list
|
/**
|
||||||
|
@returns {array} the tx list whos status is unapproved
|
||||||
|
*/
|
||||||
getUnapprovedTxList () {
|
getUnapprovedTxList () {
|
||||||
const txList = this.getTxsByMetaData('status', 'unapproved')
|
const txList = this.getTxsByMetaData('status', 'unapproved')
|
||||||
return txList.reduce((result, tx) => {
|
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) {
|
getPendingTransactions (address) {
|
||||||
const opts = { status: 'submitted' }
|
const opts = { status: 'submitted' }
|
||||||
if (address) opts.from = address
|
if (address) opts.from = address
|
||||||
return this.getFilteredTxList(opts)
|
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) {
|
getConfirmedTransactions (address) {
|
||||||
const opts = { status: 'confirmed' }
|
const opts = { status: 'confirmed' }
|
||||||
if (address) opts.from = address
|
if (address) opts.from = address
|
||||||
return this.getFilteredTxList(opts)
|
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) {
|
addTx (txMeta) {
|
||||||
this.once(`${txMeta.id}:signed`, function (txId) {
|
this.once(`${txMeta.id}:signed`, function (txId) {
|
||||||
this.removeAllListeners(`${txMeta.id}:rejected`)
|
this.removeAllListeners(`${txMeta.id}:rejected`)
|
||||||
@ -92,7 +134,9 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
// or rejected tx's.
|
// or rejected tx's.
|
||||||
// not tx's that are pending or unapproved
|
// not tx's that are pending or unapproved
|
||||||
if (txCount > txHistoryLimit - 1) {
|
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) {
|
if (index !== -1) {
|
||||||
transactions.splice(index, 1)
|
transactions.splice(index, 1)
|
||||||
}
|
}
|
||||||
@ -101,12 +145,21 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
this._saveTxList(transactions)
|
this._saveTxList(transactions)
|
||||||
return txMeta
|
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) {
|
getTx (txId) {
|
||||||
const txMeta = this.getTxsByMetaData('id', txId)[0]
|
const txMeta = this.getTxsByMetaData('id', txId)[0]
|
||||||
return txMeta
|
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) {
|
updateTx (txMeta, note) {
|
||||||
// validate txParams
|
// validate txParams
|
||||||
if (txMeta.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) {
|
updateTxParams (txId, txParams) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.getTx(txId)
|
||||||
txMeta.txParams = extend(txMeta.txParams, txParams)
|
txMeta.txParams = extend(txMeta.txParams, txParams)
|
||||||
this.updateTx(txMeta, `txStateManager#updateTxParams`)
|
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) => {
|
Object.keys(txParams).forEach((key) => {
|
||||||
const value = txParams[key]
|
const value = txParams[key]
|
||||||
// validate types
|
// validate types
|
||||||
@ -159,17 +219,19 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Takes an object of fields to search for eg:
|
@param opts {object} - an object of fields to search for eg:<br>
|
||||||
let thingsToLookFor = {
|
let <code>thingsToLookFor = {<br>
|
||||||
to: '0x0..',
|
to: '0x0..',<br>
|
||||||
from: '0x0..',
|
from: '0x0..',<br>
|
||||||
status: 'signed',
|
status: 'signed',<br>
|
||||||
err: undefined,
|
err: undefined,<br>
|
||||||
}
|
}<br></code>
|
||||||
and returns a list of tx with all
|
@param [initialList=this.getTxList()]
|
||||||
|
@returns a {array} of txMeta with all
|
||||||
options matching
|
options matching
|
||||||
|
*/
|
||||||
|
/*
|
||||||
****************HINT****************
|
****************HINT****************
|
||||||
| `err: undefined` is like looking |
|
| `err: undefined` is like looking |
|
||||||
| for a tx with no err |
|
| for a tx with no err |
|
||||||
@ -190,7 +252,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
})
|
})
|
||||||
return filteredTxList
|
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()) {
|
getTxsByMetaData (key, value, txList = this.getTxList()) {
|
||||||
return txList.filter((txMeta) => {
|
return txList.filter((txMeta) => {
|
||||||
if (txMeta.txParams[key]) {
|
if (txMeta.txParams[key]) {
|
||||||
@ -203,33 +272,51 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
|
|
||||||
// get::set status
|
// 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) {
|
getTxStatus (txId) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.getTx(txId)
|
||||||
return txMeta.status
|
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) {
|
setTxStatusRejected (txId) {
|
||||||
this._setTxStatus(txId, 'rejected')
|
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) {
|
setTxStatusUnapproved (txId) {
|
||||||
this._setTxStatus(txId, 'unapproved')
|
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) {
|
setTxStatusApproved (txId) {
|
||||||
this._setTxStatus(txId, 'approved')
|
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) {
|
setTxStatusSigned (txId) {
|
||||||
this._setTxStatus(txId, 'signed')
|
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) {
|
setTxStatusSubmitted (txId) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.getTx(txId)
|
||||||
txMeta.submittedTime = (new Date()).getTime()
|
txMeta.submittedTime = (new Date()).getTime()
|
||||||
@ -237,17 +324,29 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
this._setTxStatus(txId, 'submitted')
|
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) {
|
setTxStatusConfirmed (txId) {
|
||||||
this._setTxStatus(txId, 'confirmed')
|
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) {
|
setTxStatusDropped (txId) {
|
||||||
this._setTxStatus(txId, 'dropped')
|
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) {
|
setTxStatusFailed (txId, err) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.getTx(txId)
|
||||||
txMeta.err = {
|
txMeta.err = {
|
||||||
@ -258,6 +357,11 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
this._setTxStatus(txId, 'failed')
|
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) {
|
wipeTransactions (address) {
|
||||||
// network only tx
|
// network only tx
|
||||||
const txs = this.getFullTxList()
|
const txs = this.getFullTxList()
|
||||||
@ -273,9 +377,8 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
// PRIVATE METHODS
|
// PRIVATE METHODS
|
||||||
//
|
//
|
||||||
|
|
||||||
// Should find the tx in the tx list and
|
// STATUS METHODS
|
||||||
// update it.
|
// statuses:
|
||||||
// should set the status in txData
|
|
||||||
// - `'unapproved'` the user has not responded
|
// - `'unapproved'` the user has not responded
|
||||||
// - `'rejected'` the user has responded no!
|
// - `'rejected'` the user has responded no!
|
||||||
// - `'approved'` the user has approved the tx
|
// - `'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
|
// - `'submitted'` the tx is sent to a server
|
||||||
// - `'confirmed'` the tx has been included in a block.
|
// - `'confirmed'` the tx has been included in a block.
|
||||||
// - `'failed'` the tx failed for some reason, included on tx data.
|
// - `'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) {
|
_setTxStatus (txId, status) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.getTx(txId)
|
||||||
txMeta.status = status
|
txMeta.status = status
|
||||||
@ -295,9 +407,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
|
|||||||
this.emit('update:badge')
|
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
|
// Function is intended only for internal use
|
||||||
_saveTxList (transactions) {
|
_saveTxList (transactions) {
|
||||||
this.store.updateState({ 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 clone = require('clone')
|
||||||
const txStateHistoryHelper = require('../lib/tx-state-history-helper')
|
const txStateHistoryHelper = require('../controllers/transactions/lib/tx-state-history-helper')
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
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 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')
|
const MockTxGen = require('../lib/mock-tx-gen')
|
||||||
let providerResultStub = {}
|
let providerResultStub = {}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ const EthTx = require('ethereumjs-tx')
|
|||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const clone = require('clone')
|
const clone = require('clone')
|
||||||
const { createTestProviderTools } = require('../stub/provider')
|
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 MockTxGen = require('../lib/mock-tx-gen')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const noop = () => true
|
const noop = () => true
|
||||||
|
@ -5,7 +5,7 @@ const EthjsQuery = require('ethjs-query')
|
|||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const TransactionController = require('../../app/scripts/controllers/transactions')
|
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 { createTestProviderTools } = require('../stub/provider')
|
||||||
|
|
||||||
const noop = () => true
|
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) {
|
it('should add the tx defaults if their are none', function (done) {
|
||||||
const txMeta = {
|
const txMeta = {
|
||||||
'txParams': {
|
'txParams': {
|
||||||
@ -199,7 +199,7 @@ describe('Transaction Controller', function () {
|
|||||||
providerResultStub.eth_gasPrice = '4a817c800'
|
providerResultStub.eth_gasPrice = '4a817c800'
|
||||||
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }
|
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }
|
||||||
providerResultStub.eth_estimateGas = '5209'
|
providerResultStub.eth_estimateGas = '5209'
|
||||||
txController.addTxDefaults(txMeta)
|
txController.addTxGasDefaults(txMeta)
|
||||||
.then((txMetaWithDefaults) => {
|
.then((txMetaWithDefaults) => {
|
||||||
assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value')
|
assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value')
|
||||||
assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price')
|
assert(txMetaWithDefaults.txParams.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 () {
|
describe('#addTx', function () {
|
||||||
it('should emit updates', function (done) {
|
it('should emit updates', function (done) {
|
||||||
const txMeta = {
|
const txMeta = {
|
||||||
|
@ -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,6 +1,6 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const clone = require('clone')
|
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 () {
|
describe('deepCloneFromTxMeta', function () {
|
||||||
it('should clone deep', function () {
|
it('should clone deep', function () {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const assert = require('assert')
|
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')
|
const testVault = require('../data/v17-long-history.json')
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const clone = require('clone')
|
const clone = require('clone')
|
||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const TxStateManager = require('../../app/scripts/lib/tx-state-manager')
|
const TxStateManager = require('../../app/scripts/controllers/transactions/tx-state-manager')
|
||||||
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
|
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
|
||||||
const noop = () => true
|
const noop = () => true
|
||||||
|
|
||||||
describe('TransactionStateManager', function () {
|
describe('TransactionStateManager', function () {
|
||||||
|
@ -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)
|
||||||
}))
|
})
|
||||||
})
|
|
||||||
|
it('returns error for negative values', function () {
|
||||||
describe('chain Id', function () {
|
var sample = {
|
||||||
it('prepares a transaction with the provided chainId', function () {
|
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||||
const txParams = {
|
value: '-0x01',
|
||||||
to: '0x70ad465e0bab6504002ad58c744ed89c7da38524',
|
}
|
||||||
from: '0x69ad465e0bab6504002ad58c744ed89c7da38525',
|
try {
|
||||||
value: '0x0',
|
txUtils.validateTxParams(sample)
|
||||||
gas: '0x7b0c',
|
} catch (err) {
|
||||||
gasPrice: '0x199c82cc00',
|
assert.ok(err, 'error')
|
||||||
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 () {
|
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')
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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 () {
|
it('should error when recipient is 0x', function () {
|
||||||
// naive estimatedGas: 0x16e360 (1.5 mil)
|
const zeroRecipientTxParams = {
|
||||||
const inputHex = '0x16e360'
|
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||||
// dummy gas limit: 0x0f4240 (1 mil)
|
to: '0x',
|
||||||
const blockGasLimitHex = '0x0f4240'
|
}
|
||||||
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
|
assert.throws(() => { txUtils.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
|
||||||
// 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)
|
describe('#validateFrom', () => {
|
||||||
const inputHex = '0x16e360'
|
it('should error when from is not a hex string', function () {
|
||||||
// dummy gas limit: 0x1e8480 (2 mil)
|
|
||||||
const blockGasLimitHex = '0x1e8480'
|
// where from is undefined
|
||||||
const blockGasLimitBn = hexToBn(blockGasLimitHex)
|
const txParams = {}
|
||||||
const ceilGasLimitBn = blockGasLimitBn.muln(0.9)
|
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
|
||||||
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
|
|
||||||
// const inputBn = hexToBn(inputHex)
|
// where from is array
|
||||||
// const outputBn = hexToBn(output)
|
txParams.from = []
|
||||||
const expectedHex = bnToHex(ceilGasLimitBn)
|
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
|
||||||
assert.equal(output, expectedHex, 'returns the gas limit recommended ceiling value')
|
|
||||||
})
|
// 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