mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-26 12:29:06 +01:00
Merge branch 'master' into greenkeeper/initial
This commit is contained in:
commit
922ecb9149
@ -5,6 +5,7 @@ const ObservableStore = require('obs-store')
|
|||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
const EthQuery = require('ethjs-query')
|
const EthQuery = require('ethjs-query')
|
||||||
const TxProviderUtil = require('../lib/tx-utils')
|
const TxProviderUtil = require('../lib/tx-utils')
|
||||||
|
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
|
||||||
const createId = require('../lib/random-id')
|
const createId = require('../lib/random-id')
|
||||||
const NonceTracker = require('../lib/nonce-tracker')
|
const NonceTracker = require('../lib/nonce-tracker')
|
||||||
|
|
||||||
@ -20,6 +21,9 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
this.txHistoryLimit = opts.txHistoryLimit
|
this.txHistoryLimit = opts.txHistoryLimit
|
||||||
this.provider = opts.provider
|
this.provider = opts.provider
|
||||||
this.blockTracker = opts.blockTracker
|
this.blockTracker = opts.blockTracker
|
||||||
|
this.signEthTx = opts.signTransaction
|
||||||
|
this.ethStore = opts.ethStore
|
||||||
|
|
||||||
this.nonceTracker = new NonceTracker({
|
this.nonceTracker = new NonceTracker({
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
getPendingTransactions: (address) => {
|
getPendingTransactions: (address) => {
|
||||||
@ -31,15 +35,37 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.query = new EthQuery(this.provider)
|
this.query = new EthQuery(this.provider)
|
||||||
this.txProviderUtils = new TxProviderUtil(this.query)
|
this.txProviderUtil = new TxProviderUtil(this.provider)
|
||||||
this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this))
|
|
||||||
|
this.pendingTxTracker = new PendingTransactionTracker({
|
||||||
|
provider: this.provider,
|
||||||
|
nonceTracker: this.nonceTracker,
|
||||||
|
getBalance: async (address) => {
|
||||||
|
const account = this.ethStore.getState().accounts[address]
|
||||||
|
if (!account) return
|
||||||
|
return account.balance
|
||||||
|
},
|
||||||
|
publishTransaction: this.txProviderUtil.publishTransaction.bind(this.txProviderUtil),
|
||||||
|
getPendingTransactions: (address) => {
|
||||||
|
return this.getFilteredTxList({
|
||||||
|
from: address,
|
||||||
|
status: 'submitted',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pendingTxTracker.on('txWarning', this.updateTx.bind(this))
|
||||||
|
this.pendingTxTracker.on('txFailed', this.setTxStatusFailed.bind(this))
|
||||||
|
this.pendingTxTracker.on('txConfirmed', this.setTxStatusConfirmed.bind(this))
|
||||||
|
|
||||||
|
this.blockTracker.on('rawBlock', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
|
||||||
// this is a little messy but until ethstore has been either
|
// this is a little messy but until ethstore has been either
|
||||||
// removed or redone this is to guard against the race condition
|
// removed or redone this is to guard against the race condition
|
||||||
// where ethStore hasent been populated by the results yet
|
// where ethStore hasent been populated by the results yet
|
||||||
this.blockTracker.once('latest', () => this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this)))
|
this.blockTracker.once('latest', () => {
|
||||||
this.blockTracker.on('sync', this.queryPendingTxs.bind(this))
|
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
|
||||||
this.signEthTx = opts.signTransaction
|
})
|
||||||
this.ethStore = opts.ethStore
|
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.store.subscribe(() => this._updateMemstore())
|
this.store.subscribe(() => this._updateMemstore())
|
||||||
@ -171,7 +197,7 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
|
|
||||||
async addUnapprovedTransaction (txParams) {
|
async addUnapprovedTransaction (txParams) {
|
||||||
// validate
|
// validate
|
||||||
await this.txProviderUtils.validateTxParams(txParams)
|
await this.txProviderUtil.validateTxParams(txParams)
|
||||||
// construct txMeta
|
// construct txMeta
|
||||||
const txMeta = {
|
const txMeta = {
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@ -197,7 +223,7 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
txParams.gasPrice = gasPrice
|
txParams.gasPrice = gasPrice
|
||||||
}
|
}
|
||||||
// set gasLimit
|
// set gasLimit
|
||||||
return await this.txProviderUtils.analyzeGasUsage(txMeta)
|
return await this.txProviderUtil.analyzeGasUsage(txMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAndApproveTransaction (txMeta) {
|
async updateAndApproveTransaction (txMeta) {
|
||||||
@ -226,11 +252,7 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
// must set transaction to submitted/failed before releasing lock
|
// must set transaction to submitted/failed before releasing lock
|
||||||
nonceLock.releaseLock()
|
nonceLock.releaseLock()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setTxStatusFailed(txId, {
|
this.setTxStatusFailed(txId, err)
|
||||||
stack: err.stack || err.message,
|
|
||||||
errCode: err.errCode || err,
|
|
||||||
message: err.message || 'Transaction failed during approval',
|
|
||||||
})
|
|
||||||
// must set transaction to submitted/failed before releasing lock
|
// must set transaction to submitted/failed before releasing lock
|
||||||
if (nonceLock) nonceLock.releaseLock()
|
if (nonceLock) nonceLock.releaseLock()
|
||||||
// continue with error chain
|
// continue with error chain
|
||||||
@ -244,7 +266,7 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
const fromAddress = txParams.from
|
const fromAddress = txParams.from
|
||||||
// add network/chain id
|
// add network/chain id
|
||||||
txParams.chainId = this.getChainId()
|
txParams.chainId = this.getChainId()
|
||||||
const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams)
|
const ethTx = this.txProviderUtil.buildEthTxFromParams(txParams)
|
||||||
await this.signEthTx(ethTx, fromAddress)
|
await this.signEthTx(ethTx, fromAddress)
|
||||||
this.setTxStatusSigned(txMeta.id)
|
this.setTxStatusSigned(txMeta.id)
|
||||||
const rawTx = ethUtil.bufferToHex(ethTx.serialize())
|
const rawTx = ethUtil.bufferToHex(ethTx.serialize())
|
||||||
@ -255,7 +277,7 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.getTx(txId)
|
||||||
txMeta.rawTx = rawTx
|
txMeta.rawTx = rawTx
|
||||||
this.updateTx(txMeta)
|
this.updateTx(txMeta)
|
||||||
const txHash = await this.txProviderUtils.publishTransaction(rawTx)
|
const txHash = await this.txProviderUtil.publishTransaction(rawTx)
|
||||||
this.setTxHash(txId, txHash)
|
this.setTxHash(txId, txHash)
|
||||||
this.setTxStatusSubmitted(txId)
|
this.setTxStatusSubmitted(txId)
|
||||||
}
|
}
|
||||||
@ -374,73 +396,6 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
this.updateTx(txMeta)
|
this.updateTx(txMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if a signed tx is in a block and
|
|
||||||
// if included sets the tx status as 'confirmed'
|
|
||||||
checkForTxInBlock (block) {
|
|
||||||
const signedTxList = this.getFilteredTxList({status: 'submitted'})
|
|
||||||
if (!signedTxList.length) return
|
|
||||||
signedTxList.forEach((txMeta) => {
|
|
||||||
const txHash = txMeta.hash
|
|
||||||
const txId = txMeta.id
|
|
||||||
|
|
||||||
if (!txHash) {
|
|
||||||
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
|
|
||||||
noTxHashErr.name = 'NoTxHashError'
|
|
||||||
this.setTxStatusFailed(txId, noTxHashErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
block.transactions.forEach((tx) => {
|
|
||||||
if (tx.hash === txHash) this.setTxStatusConfirmed(txId)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
queryPendingTxs ({oldBlock, newBlock}) {
|
|
||||||
// check pending transactions on start
|
|
||||||
if (!oldBlock) {
|
|
||||||
this._checkPendingTxs()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// if we synced by more than one block, check for missed pending transactions
|
|
||||||
const diff = Number.parseInt(newBlock.number) - Number.parseInt(oldBlock.number)
|
|
||||||
if (diff > 1) this._checkPendingTxs()
|
|
||||||
}
|
|
||||||
|
|
||||||
resubmitPendingTxs () {
|
|
||||||
const pending = this.getTxsByMetaData('status', 'submitted')
|
|
||||||
// only try resubmitting if their are transactions to resubmit
|
|
||||||
if (!pending.length) return
|
|
||||||
pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => {
|
|
||||||
/*
|
|
||||||
Dont marked as failed if the error is a "known" transaction warning
|
|
||||||
"there is already a transaction with the same sender-nonce
|
|
||||||
but higher/same gas price"
|
|
||||||
*/
|
|
||||||
const errorMessage = err.message.toLowerCase()
|
|
||||||
const isKnownTx = (
|
|
||||||
// geth
|
|
||||||
errorMessage.includes('replacement transaction underpriced')
|
|
||||||
|| errorMessage.includes('known transaction')
|
|
||||||
// parity
|
|
||||||
|| errorMessage.includes('gas price too low to replace')
|
|
||||||
|| errorMessage.includes('transaction with the same hash was already imported')
|
|
||||||
// other
|
|
||||||
|| errorMessage.includes('gateway timeout')
|
|
||||||
|| errorMessage.includes('nonce too low')
|
|
||||||
)
|
|
||||||
// ignore resubmit warnings, return early
|
|
||||||
if (isKnownTx) return
|
|
||||||
// encountered real error - transition to error state
|
|
||||||
this.setTxStatusFailed(txMeta.id, {
|
|
||||||
stack: err.stack || err.message,
|
|
||||||
errCode: err.errCode || err,
|
|
||||||
message: err.message,
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* _____________________________________
|
/* _____________________________________
|
||||||
| |
|
| |
|
||||||
| PRIVATE METHODS |
|
| PRIVATE METHODS |
|
||||||
@ -482,75 +437,4 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
})
|
})
|
||||||
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
|
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
|
||||||
}
|
}
|
||||||
|
|
||||||
async _resubmitTx (txMeta) {
|
|
||||||
const address = txMeta.txParams.from
|
|
||||||
const balance = this.ethStore.getState().accounts[address].balance
|
|
||||||
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
|
|
||||||
|
|
||||||
// if the value of the transaction is greater then the balance, fail.
|
|
||||||
if (!this.txProviderUtils.sufficientBalance(txMeta.txParams, balance)) {
|
|
||||||
const message = 'Insufficient balance.'
|
|
||||||
this.setTxStatusFailed(txMeta.id, {
|
|
||||||
stack: '_resubmitTx: custom tx-controller error',
|
|
||||||
message,
|
|
||||||
})
|
|
||||||
log.error(message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only auto-submit already-signed txs:
|
|
||||||
if (!('rawTx' in txMeta)) return
|
|
||||||
|
|
||||||
// Increment a try counter.
|
|
||||||
txMeta.retryCount++
|
|
||||||
const rawTx = txMeta.rawTx
|
|
||||||
return await this.txProviderUtils.publishTransaction(rawTx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checks the network for signed txs and
|
|
||||||
// if confirmed sets the tx status as 'confirmed'
|
|
||||||
async _checkPendingTxs () {
|
|
||||||
const signedTxList = this.getFilteredTxList({status: 'submitted'})
|
|
||||||
// in order to keep the nonceTracker accurate we block it while updating pending transactions
|
|
||||||
const nonceGlobalLock = await this.nonceTracker.getGlobalLock()
|
|
||||||
try {
|
|
||||||
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
|
|
||||||
} catch (err) {
|
|
||||||
console.error('TransactionController - Error updating pending transactions')
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
nonceGlobalLock.releaseLock()
|
|
||||||
}
|
|
||||||
|
|
||||||
async _checkPendingTx (txMeta) {
|
|
||||||
const txHash = txMeta.hash
|
|
||||||
const txId = txMeta.id
|
|
||||||
// extra check in case there was an uncaught error during the
|
|
||||||
// signature and submission process
|
|
||||||
if (!txHash) {
|
|
||||||
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
|
|
||||||
noTxHashErr.name = 'NoTxHashError'
|
|
||||||
this.setTxStatusFailed(txId, noTxHashErr)
|
|
||||||
}
|
|
||||||
// get latest transaction status
|
|
||||||
let txParams
|
|
||||||
try {
|
|
||||||
txParams = await this.query.getTransactionByHash(txHash)
|
|
||||||
if (!txParams) return
|
|
||||||
if (txParams.blockNumber) {
|
|
||||||
this.setTxStatusConfirmed(txId)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err || !txParams) {
|
|
||||||
txMeta.err = {
|
|
||||||
isWarning: true,
|
|
||||||
errorCode: err,
|
|
||||||
message: 'There was a problem loading this transaction.',
|
|
||||||
}
|
|
||||||
this.updateTx(txMeta)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
163
app/scripts/lib/pending-tx-tracker.js
Normal file
163
app/scripts/lib/pending-tx-tracker.js
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
const EventEmitter = require('events')
|
||||||
|
const EthQuery = require('ethjs-query')
|
||||||
|
const sufficientBalance = require('./util').sufficientBalance
|
||||||
|
/*
|
||||||
|
|
||||||
|
Utility class for tracking the transactions as they
|
||||||
|
go from a pending state to a confirmed (mined in a block) state
|
||||||
|
|
||||||
|
As well as continues broadcast while in the pending state
|
||||||
|
|
||||||
|
~config is not optional~
|
||||||
|
requires a: {
|
||||||
|
provider: //,
|
||||||
|
nonceTracker: //see nonce tracker,
|
||||||
|
getBalnce: //(address) a function for getting balances,
|
||||||
|
getPendingTransactions: //() a function for getting an array of transactions,
|
||||||
|
publishTransaction: //(rawTx) a async function for publishing raw transactions,
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = class PendingTransactionTracker extends EventEmitter {
|
||||||
|
constructor (config) {
|
||||||
|
super()
|
||||||
|
this.query = new EthQuery(config.provider)
|
||||||
|
this.nonceTracker = config.nonceTracker
|
||||||
|
|
||||||
|
this.getBalance = config.getBalance
|
||||||
|
this.getPendingTransactions = config.getPendingTransactions
|
||||||
|
this.publishTransaction = config.publishTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks if a signed tx is in a block and
|
||||||
|
// if included sets the tx status as 'confirmed'
|
||||||
|
checkForTxInBlock (block) {
|
||||||
|
const signedTxList = this.getPendingTransactions()
|
||||||
|
if (!signedTxList.length) return
|
||||||
|
signedTxList.forEach((txMeta) => {
|
||||||
|
const txHash = txMeta.hash
|
||||||
|
const txId = txMeta.id
|
||||||
|
|
||||||
|
if (!txHash) {
|
||||||
|
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
|
||||||
|
noTxHashErr.name = 'NoTxHashError'
|
||||||
|
this.emit('txFailed', txId, noTxHashErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
block.transactions.forEach((tx) => {
|
||||||
|
if (tx.hash === txHash) this.emit('txConfirmed', txId)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
queryPendingTxs ({oldBlock, newBlock}) {
|
||||||
|
// check pending transactions on start
|
||||||
|
if (!oldBlock) {
|
||||||
|
this._checkPendingTxs()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if we synced by more than one block, check for missed pending transactions
|
||||||
|
const diff = Number.parseInt(newBlock.number, 16) - Number.parseInt(oldBlock.number, 16)
|
||||||
|
if (diff > 1) this._checkPendingTxs()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
resubmitPendingTxs () {
|
||||||
|
const pending = this.getPendingTransactions('status', 'submitted')
|
||||||
|
// only try resubmitting if their are transactions to resubmit
|
||||||
|
if (!pending.length) return
|
||||||
|
pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => {
|
||||||
|
/*
|
||||||
|
Dont marked as failed if the error is a "known" transaction warning
|
||||||
|
"there is already a transaction with the same sender-nonce
|
||||||
|
but higher/same gas price"
|
||||||
|
*/
|
||||||
|
const errorMessage = err.message.toLowerCase()
|
||||||
|
const isKnownTx = (
|
||||||
|
// geth
|
||||||
|
errorMessage.includes('replacement transaction underpriced')
|
||||||
|
|| errorMessage.includes('known transaction')
|
||||||
|
// parity
|
||||||
|
|| errorMessage.includes('gas price too low to replace')
|
||||||
|
|| errorMessage.includes('transaction with the same hash was already imported')
|
||||||
|
// other
|
||||||
|
|| errorMessage.includes('gateway timeout')
|
||||||
|
|| errorMessage.includes('nonce too low')
|
||||||
|
)
|
||||||
|
// ignore resubmit warnings, return early
|
||||||
|
if (isKnownTx) return
|
||||||
|
// encountered real error - transition to error state
|
||||||
|
this.emit('txFailed', txMeta.id, err)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async _resubmitTx (txMeta) {
|
||||||
|
const address = txMeta.txParams.from
|
||||||
|
const balance = this.getBalance(address)
|
||||||
|
if (balance === undefined) return
|
||||||
|
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
|
||||||
|
|
||||||
|
// if the value of the transaction is greater then the balance, fail.
|
||||||
|
if (!sufficientBalance(txMeta.txParams, balance)) {
|
||||||
|
const insufficientFundsError = new Error('Insufficient balance during rebroadcast.')
|
||||||
|
this.emit('txFailed', txMeta.id, insufficientFundsError)
|
||||||
|
log.error(insufficientFundsError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only auto-submit already-signed txs:
|
||||||
|
if (!('rawTx' in txMeta)) return
|
||||||
|
|
||||||
|
// Increment a try counter.
|
||||||
|
txMeta.retryCount++
|
||||||
|
const rawTx = txMeta.rawTx
|
||||||
|
return await this.publishTransaction(rawTx)
|
||||||
|
}
|
||||||
|
|
||||||
|
async _checkPendingTx (txMeta) {
|
||||||
|
const txHash = txMeta.hash
|
||||||
|
const txId = txMeta.id
|
||||||
|
// extra check in case there was an uncaught error during the
|
||||||
|
// signature and submission process
|
||||||
|
if (!txHash) {
|
||||||
|
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
|
||||||
|
noTxHashErr.name = 'NoTxHashError'
|
||||||
|
this.emit('txFailed', txId, noTxHashErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// get latest transaction status
|
||||||
|
let txParams
|
||||||
|
try {
|
||||||
|
txParams = await this.query.getTransactionByHash(txHash)
|
||||||
|
if (!txParams) return
|
||||||
|
if (txParams.blockNumber) {
|
||||||
|
this.emit('txConfirmed', txId)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
txMeta.warning = {
|
||||||
|
error: err,
|
||||||
|
message: 'There was a problem loading this transaction.',
|
||||||
|
}
|
||||||
|
this.emit('txWarning', txMeta)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks the network for signed txs and
|
||||||
|
// if confirmed sets the tx status as 'confirmed'
|
||||||
|
async _checkPendingTxs () {
|
||||||
|
const signedTxList = this.getPendingTransactions()
|
||||||
|
// in order to keep the nonceTracker accurate we block it while updating pending transactions
|
||||||
|
const nonceGlobalLock = await this.nonceTracker.getGlobalLock()
|
||||||
|
try {
|
||||||
|
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('PendingTransactionWatcher - Error updating pending transactions')
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
nonceGlobalLock.releaseLock()
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,11 @@
|
|||||||
const ethUtil = require('ethereumjs-util')
|
const EthQuery = require('ethjs-query')
|
||||||
const Transaction = require('ethereumjs-tx')
|
const Transaction = require('ethereumjs-tx')
|
||||||
const normalize = require('eth-sig-util').normalize
|
const normalize = require('eth-sig-util').normalize
|
||||||
const BN = ethUtil.BN
|
const {
|
||||||
|
hexToBn,
|
||||||
|
BnMultiplyByFraction,
|
||||||
|
bnToHex,
|
||||||
|
} = require('./util')
|
||||||
|
|
||||||
/*
|
/*
|
||||||
tx-utils are utility methods for Transaction manager
|
tx-utils are utility methods for Transaction manager
|
||||||
@ -9,9 +13,9 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = class txProvideUtils {
|
module.exports = class txProvideUtil {
|
||||||
constructor (ethQuery) {
|
constructor (provider) {
|
||||||
this.query = ethQuery
|
this.query = new EthQuery(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
async analyzeGasUsage (txMeta) {
|
async analyzeGasUsage (txMeta) {
|
||||||
@ -91,31 +95,4 @@ module.exports = class txProvideUtils {
|
|||||||
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
|
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
sufficientBalance (txParams, hexBalance) {
|
|
||||||
const balance = hexToBn(hexBalance)
|
|
||||||
const value = hexToBn(txParams.value)
|
|
||||||
const gasLimit = hexToBn(txParams.gas)
|
|
||||||
const gasPrice = hexToBn(txParams.gasPrice)
|
|
||||||
|
|
||||||
const maxCost = value.add(gasLimit.mul(gasPrice))
|
|
||||||
return balance.gte(maxCost)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// util
|
|
||||||
|
|
||||||
function bnToHex (inputBn) {
|
|
||||||
return ethUtil.addHexPrefix(inputBn.toString(16))
|
|
||||||
}
|
|
||||||
|
|
||||||
function hexToBn (inputHex) {
|
|
||||||
return new BN(ethUtil.stripHexPrefix(inputHex), 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BnMultiplyByFraction (targetBN, numerator, denominator) {
|
|
||||||
const numBN = new BN(numerator)
|
|
||||||
const denomBN = new BN(denominator)
|
|
||||||
return targetBN.mul(numBN).div(denomBN)
|
|
||||||
}
|
|
@ -1,8 +1,39 @@
|
|||||||
|
const ethUtil = require('ethereumjs-util')
|
||||||
|
const BN = ethUtil.BN
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getStack,
|
getStack,
|
||||||
|
sufficientBalance,
|
||||||
|
hexToBn,
|
||||||
|
bnToHex,
|
||||||
|
BnMultiplyByFraction,
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStack () {
|
function getStack () {
|
||||||
const stack = new Error('Stack trace generator - not an error').stack
|
const stack = new Error('Stack trace generator - not an error').stack
|
||||||
return stack
|
return stack
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sufficientBalance (txParams, hexBalance) {
|
||||||
|
const balance = hexToBn(hexBalance)
|
||||||
|
const value = hexToBn(txParams.value)
|
||||||
|
const gasLimit = hexToBn(txParams.gas)
|
||||||
|
const gasPrice = hexToBn(txParams.gasPrice)
|
||||||
|
|
||||||
|
const maxCost = value.add(gasLimit.mul(gasPrice))
|
||||||
|
return balance.gte(maxCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bnToHex (inputBn) {
|
||||||
|
return ethUtil.addHexPrefix(inputBn.toString(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToBn (inputHex) {
|
||||||
|
return new BN(ethUtil.stripHexPrefix(inputHex), 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BnMultiplyByFraction (targetBN, numerator, denominator) {
|
||||||
|
const numBN = new BN(numerator)
|
||||||
|
const denomBN = new BN(denominator)
|
||||||
|
return targetBN.mul(numBN).div(denomBN)
|
||||||
|
}
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
"babel-runtime": "^6.23.0",
|
"babel-runtime": "^6.23.0",
|
||||||
"bip39": "^2.2.0",
|
"bip39": "^2.2.0",
|
||||||
"bluebird": "^3.5.0",
|
"bluebird": "^3.5.0",
|
||||||
|
"bn.js": "^4.11.7",
|
||||||
"browser-passworder": "^2.0.3",
|
"browser-passworder": "^2.0.3",
|
||||||
"browserify-derequire": "^0.9.4",
|
"browserify-derequire": "^0.9.4",
|
||||||
"client-sw-ready-event": "^3.3.0",
|
"client-sw-ready-event": "^3.3.0",
|
||||||
@ -155,6 +156,7 @@
|
|||||||
"eslint-plugin-chai": "0.0.1",
|
"eslint-plugin-chai": "0.0.1",
|
||||||
"eslint-plugin-mocha": "^4.9.0",
|
"eslint-plugin-mocha": "^4.9.0",
|
||||||
"fs-promise": "^2.0.3",
|
"fs-promise": "^2.0.3",
|
||||||
|
"eth-json-rpc-middleware": "^1.2.7",
|
||||||
"gulp": "github:gulpjs/gulp#4.0",
|
"gulp": "github:gulpjs/gulp#4.0",
|
||||||
"gulp-if": "^2.0.1",
|
"gulp-if": "^2.0.1",
|
||||||
"gulp-json-editor": "^2.2.1",
|
"gulp-json-editor": "^2.2.1",
|
||||||
@ -168,6 +170,7 @@
|
|||||||
"jsdom": "^11.1.0",
|
"jsdom": "^11.1.0",
|
||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"jshint-stylish": "~2.2.1",
|
"jshint-stylish": "~2.2.1",
|
||||||
|
"json-rpc-engine": "^3.0.1",
|
||||||
"lodash.assign": "^4.0.6",
|
"lodash.assign": "^4.0.6",
|
||||||
"mocha": "^3.4.2",
|
"mocha": "^3.4.2",
|
||||||
"mocha-eslint": "^4.0.0",
|
"mocha-eslint": "^4.0.0",
|
||||||
|
25
test/stub/provider.js
Normal file
25
test/stub/provider.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const JsonRpcEngine = require('json-rpc-engine')
|
||||||
|
const scaffoldMiddleware = require('eth-json-rpc-middleware/scaffold')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createEngineForTestData,
|
||||||
|
providerFromEngine,
|
||||||
|
scaffoldMiddleware,
|
||||||
|
createStubedProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createEngineForTestData () {
|
||||||
|
return new JsonRpcEngine()
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerFromEngine (engine) {
|
||||||
|
const provider = { sendAsync: engine.handle.bind(engine) }
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStubedProvider (resultStub) {
|
||||||
|
const engine = createEngineForTestData()
|
||||||
|
engine.push(scaffoldMiddleware(resultStub))
|
||||||
|
return providerFromEngine(engine)
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const MessageManger = require('../../app/scripts/lib/message-manager')
|
const MessageManger = require('../../app/scripts/lib/message-manager')
|
||||||
|
|
||||||
describe('Transaction Manager', function () {
|
describe('Message Manager', function () {
|
||||||
let messageManager
|
let messageManager
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
260
test/unit/pending-tx-test.js
Normal file
260
test/unit/pending-tx-test.js
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
const assert = require('assert')
|
||||||
|
const ethUtil = require('ethereumjs-util')
|
||||||
|
const EthTx = require('ethereumjs-tx')
|
||||||
|
const ObservableStore = require('obs-store')
|
||||||
|
const clone = require('clone')
|
||||||
|
const { createStubedProvider } = require('../stub/provider')
|
||||||
|
const PendingTransactionTracker = require('../../app/scripts/lib/pending-tx-tracker')
|
||||||
|
const noop = () => true
|
||||||
|
const currentNetworkId = 42
|
||||||
|
const otherNetworkId = 36
|
||||||
|
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
|
||||||
|
|
||||||
|
describe('PendingTransactionTracker', function () {
|
||||||
|
let pendingTxTracker, txMeta, txMetaNoHash, txMetaNoRawTx, providerResultStub, provider
|
||||||
|
this.timeout(10000)
|
||||||
|
beforeEach(function () {
|
||||||
|
txMeta = {
|
||||||
|
id: 1,
|
||||||
|
hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb',
|
||||||
|
status: 'signed',
|
||||||
|
txParams: {
|
||||||
|
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||||
|
nonce: '0x1',
|
||||||
|
value: '0xfffff',
|
||||||
|
},
|
||||||
|
rawTx: '0xf86c808504a817c800827b0d940c62bb85faa3311a998d3aba8098c1235c564966880de0b6b3a7640000802aa08ff665feb887a25d4099e40e11f0fef93ee9608f404bd3f853dd9e84ed3317a6a02ec9d3d1d6e176d4d2593dd760e74ccac753e6a0ea0d00cc9789d0d7ff1f471d',
|
||||||
|
}
|
||||||
|
txMetaNoHash = {
|
||||||
|
id: 2,
|
||||||
|
status: 'signed',
|
||||||
|
txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'},
|
||||||
|
}
|
||||||
|
txMetaNoRawTx = {
|
||||||
|
hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb',
|
||||||
|
status: 'signed',
|
||||||
|
txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'},
|
||||||
|
}
|
||||||
|
providerResultStub = {}
|
||||||
|
provider = createStubedProvider(providerResultStub)
|
||||||
|
|
||||||
|
pendingTxTracker = new PendingTransactionTracker({
|
||||||
|
provider,
|
||||||
|
getBalance: () => {},
|
||||||
|
nonceTracker: {
|
||||||
|
getGlobalLock: async () => {
|
||||||
|
return { releaseLock: () => {} }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPendingTransactions: () => {return []},
|
||||||
|
sufficientBalance: () => {},
|
||||||
|
publishTransaction: () => {},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#checkForTxInBlock', function () {
|
||||||
|
it('should return if no pending transactions', function () {
|
||||||
|
// throw a type error if it trys to do anything on the block
|
||||||
|
// thus failing the test
|
||||||
|
const block = Proxy.revocable({}, {}).revoke()
|
||||||
|
pendingTxTracker.checkForTxInBlock(block)
|
||||||
|
})
|
||||||
|
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) {
|
||||||
|
const block = Proxy.revocable({}, {}).revoke()
|
||||||
|
pendingTxTracker.getPendingTransactions = () => [txMetaNoHash]
|
||||||
|
pendingTxTracker.once('txFailed', (txId, err) => {
|
||||||
|
assert(txId, txMetaNoHash.id, 'should pass txId')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
pendingTxTracker.checkForTxInBlock(block)
|
||||||
|
})
|
||||||
|
it('should emit \'txConfirmed\' if the tx is in the block', function (done) {
|
||||||
|
const block = { transactions: [txMeta]}
|
||||||
|
pendingTxTracker.getPendingTransactions = () => [txMeta]
|
||||||
|
pendingTxTracker.once('txConfirmed', (txId) => {
|
||||||
|
assert(txId, txMeta.id, 'should pass txId')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
pendingTxTracker.once('txFailed', (_, err) => { done(err) })
|
||||||
|
pendingTxTracker.checkForTxInBlock(block)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('#queryPendingTxs', function () {
|
||||||
|
it('should call #_checkPendingTxs if their is no oldBlock', function (done) {
|
||||||
|
let newBlock, oldBlock
|
||||||
|
newBlock = { number: '0x01' }
|
||||||
|
pendingTxTracker._checkPendingTxs = done
|
||||||
|
pendingTxTracker.queryPendingTxs({oldBlock, newBlock})
|
||||||
|
})
|
||||||
|
it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) {
|
||||||
|
let newBlock, oldBlock
|
||||||
|
oldBlock = { number: '0x01' }
|
||||||
|
newBlock = { number: '0x03' }
|
||||||
|
pendingTxTracker._checkPendingTxs = done
|
||||||
|
pendingTxTracker.queryPendingTxs({oldBlock, newBlock})
|
||||||
|
})
|
||||||
|
it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) {
|
||||||
|
let newBlock, oldBlock
|
||||||
|
oldBlock = { number: '0x1' }
|
||||||
|
newBlock = { number: '0x2' }
|
||||||
|
pendingTxTracker._checkPendingTxs = () => {
|
||||||
|
const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less')
|
||||||
|
done(err)
|
||||||
|
}
|
||||||
|
pendingTxTracker.queryPendingTxs({oldBlock, newBlock})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#_checkPendingTx', function () {
|
||||||
|
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) {
|
||||||
|
pendingTxTracker.once('txFailed', (txId, err) => {
|
||||||
|
assert(txId, txMetaNoHash.id, 'should pass txId')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
pendingTxTracker._checkPendingTx(txMetaNoHash)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should should return if query does not return txParams', function () {
|
||||||
|
providerResultStub.eth_getTransactionByHash = null
|
||||||
|
pendingTxTracker._checkPendingTx(txMeta)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should emit \'txConfirmed\'', function (done) {
|
||||||
|
providerResultStub.eth_getTransactionByHash = {blockNumber: '0x01'}
|
||||||
|
pendingTxTracker.once('txConfirmed', (txId) => {
|
||||||
|
assert(txId, txMeta.id, 'should pass txId')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
pendingTxTracker.once('txFailed', (_, err) => { done(err) })
|
||||||
|
pendingTxTracker._checkPendingTx(txMeta)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#_checkPendingTxs', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txMeta2 = txMeta3 = txMeta
|
||||||
|
txMeta2.id = 2
|
||||||
|
txMeta3.id = 3
|
||||||
|
txList = [txMeta, txMeta2, txMeta3].map((tx) => {
|
||||||
|
tx.processed = new Promise ((resolve) => { tx.resolve = resolve })
|
||||||
|
return tx
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should warp all txMeta\'s in #_checkPendingTx', function (done) {
|
||||||
|
pendingTxTracker.getPendingTransactions = () => txList
|
||||||
|
pendingTxTracker._checkPendingTx = (tx) => { tx.resolve(tx) }
|
||||||
|
const list = txList.map
|
||||||
|
Promise.all(txList.map((tx) => tx.processed))
|
||||||
|
.then((txCompletedList) => done())
|
||||||
|
.catch(done)
|
||||||
|
|
||||||
|
pendingTxTracker._checkPendingTxs()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#resubmitPendingTxs', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txMeta2 = txMeta3 = txMeta
|
||||||
|
txList = [txMeta, txMeta2, txMeta3].map((tx) => {
|
||||||
|
tx.processed = new Promise ((resolve) => { tx.resolve = resolve })
|
||||||
|
return tx
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return if no pending transactions', function () {
|
||||||
|
pendingTxTracker.resubmitPendingTxs()
|
||||||
|
})
|
||||||
|
it('should call #_resubmitTx for all pending tx\'s', function (done) {
|
||||||
|
pendingTxTracker.getPendingTransactions = () => txList
|
||||||
|
pendingTxTracker._resubmitTx = async (tx) => { tx.resolve(tx) }
|
||||||
|
Promise.all(txList.map((tx) => tx.processed))
|
||||||
|
.then((txCompletedList) => done())
|
||||||
|
.catch(done)
|
||||||
|
pendingTxTracker.resubmitPendingTxs()
|
||||||
|
})
|
||||||
|
it('should not emit \'txFailed\' if the txMeta throws a known txError', function (done) {
|
||||||
|
knownErrors =[
|
||||||
|
// geth
|
||||||
|
' Replacement transaction Underpriced ',
|
||||||
|
' known transaction',
|
||||||
|
// parity
|
||||||
|
'Gas price too low to replace ',
|
||||||
|
' transaction with the sAme hash was already imported',
|
||||||
|
// other
|
||||||
|
' gateway timeout',
|
||||||
|
' noncE too low ',
|
||||||
|
]
|
||||||
|
const enoughForAllErrors = txList.concat(txList)
|
||||||
|
|
||||||
|
pendingTxTracker.on('txFailed', (_, err) => done(err))
|
||||||
|
|
||||||
|
pendingTxTracker.getPendingTransactions = () => enoughForAllErrors
|
||||||
|
pendingTxTracker._resubmitTx = async (tx) => {
|
||||||
|
tx.resolve()
|
||||||
|
throw new Error(knownErrors.pop())
|
||||||
|
}
|
||||||
|
Promise.all(txList.map((tx) => tx.processed))
|
||||||
|
.then((txCompletedList) => done())
|
||||||
|
.catch(done)
|
||||||
|
|
||||||
|
pendingTxTracker.resubmitPendingTxs()
|
||||||
|
})
|
||||||
|
it('should emit \'txFailed\' if it encountered a real error', function (done) {
|
||||||
|
pendingTxTracker.once('txFailed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err))
|
||||||
|
|
||||||
|
pendingTxTracker.getPendingTransactions = () => txList
|
||||||
|
pendingTxTracker._resubmitTx = async (tx) => { throw new TypeError('im some real error') }
|
||||||
|
Promise.all(txList.map((tx) => tx.processed))
|
||||||
|
.then((txCompletedList) => done())
|
||||||
|
.catch(done)
|
||||||
|
|
||||||
|
pendingTxTracker.resubmitPendingTxs()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('#_resubmitTx with a too-low balance', function () {
|
||||||
|
it('should return before publishing the transaction because to low of balance', function (done) {
|
||||||
|
const lowBalance = '0x0'
|
||||||
|
pendingTxTracker.getBalance = (address) => {
|
||||||
|
assert.equal(address, txMeta.txParams.from, 'Should pass the address')
|
||||||
|
return lowBalance
|
||||||
|
}
|
||||||
|
pendingTxTracker.publishTransaction = async (rawTx) => {
|
||||||
|
done(new Error('tried to publish transaction'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stubbing out current account state:
|
||||||
|
// Adding the fake tx:
|
||||||
|
pendingTxTracker.once('txFailed', (txId, err) => {
|
||||||
|
assert(err, 'Should have a error')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
pendingTxTracker._resubmitTx(txMeta)
|
||||||
|
.catch((err) => {
|
||||||
|
assert.ifError(err, 'should not throw an error')
|
||||||
|
done(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should publishing the transaction', function (done) {
|
||||||
|
const enoughBalance = '0x100000'
|
||||||
|
pendingTxTracker.getBalance = (address) => {
|
||||||
|
assert.equal(address, txMeta.txParams.from, 'Should pass the address')
|
||||||
|
return enoughBalance
|
||||||
|
}
|
||||||
|
pendingTxTracker.publishTransaction = async (rawTx) => {
|
||||||
|
assert.equal(rawTx, txMeta.rawTx, 'Should pass the rawTx')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stubbing out current account state:
|
||||||
|
// Adding the fake tx:
|
||||||
|
pendingTxTracker._resubmitTx(txMeta)
|
||||||
|
.then(() => done())
|
||||||
|
.catch((err) => {
|
||||||
|
assert.ifError(err, 'should not throw an error')
|
||||||
|
done(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -10,16 +10,20 @@ const noop = () => true
|
|||||||
const currentNetworkId = 42
|
const currentNetworkId = 42
|
||||||
const otherNetworkId = 36
|
const otherNetworkId = 36
|
||||||
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
|
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
|
||||||
|
const { createStubedProvider } = require('../stub/provider')
|
||||||
|
|
||||||
describe('Transaction Controller', function () {
|
describe('Transaction Controller', function () {
|
||||||
let txController
|
let txController, engine, provider, providerResultStub
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
|
providerResultStub = {}
|
||||||
|
provider = createStubedProvider(providerResultStub)
|
||||||
|
|
||||||
txController = new TransactionController({
|
txController = new TransactionController({
|
||||||
|
provider,
|
||||||
networkStore: new ObservableStore(currentNetworkId),
|
networkStore: new ObservableStore(currentNetworkId),
|
||||||
txHistoryLimit: 10,
|
txHistoryLimit: 10,
|
||||||
blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
|
blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
|
||||||
provider: { sendAsync: noop },
|
|
||||||
ethStore: { getState: noop },
|
ethStore: { getState: noop },
|
||||||
signTransaction: (ethTx) => new Promise((resolve) => {
|
signTransaction: (ethTx) => new Promise((resolve) => {
|
||||||
ethTx.sign(privKey)
|
ethTx.sign(privKey)
|
||||||
@ -27,19 +31,7 @@ describe('Transaction Controller', function () {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
|
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
|
||||||
txController.query = new Proxy({}, {
|
txController.txProviderUtils = new TxProvideUtils(txController.provider)
|
||||||
get: (queryStubResult, key) => {
|
|
||||||
if (key === 'stubResult') {
|
|
||||||
return function (method, ...args) {
|
|
||||||
queryStubResult[method] = args
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const returnValues = queryStubResult[key]
|
|
||||||
return () => Promise.resolve(...returnValues)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
txController.txProviderUtils = new TxProvideUtils(txController.query)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#newUnapprovedTransaction', function () {
|
describe('#newUnapprovedTransaction', function () {
|
||||||
@ -76,7 +68,6 @@ describe('Transaction Controller', function () {
|
|||||||
it('should resolve when finished and status is submitted and resolve with the hash', function (done) {
|
it('should resolve when finished and status is submitted and resolve with the hash', function (done) {
|
||||||
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
|
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('HELLLO')
|
|
||||||
txController.setTxHash(txMetaFromEmit.id, '0x0')
|
txController.setTxHash(txMetaFromEmit.id, '0x0')
|
||||||
txController.setTxStatusSubmitted(txMetaFromEmit.id)
|
txController.setTxStatusSubmitted(txMetaFromEmit.id)
|
||||||
}, 10)
|
}, 10)
|
||||||
@ -93,7 +84,6 @@ describe('Transaction Controller', function () {
|
|||||||
it('should reject when finished and status is rejected', function (done) {
|
it('should reject when finished and status is rejected', function (done) {
|
||||||
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
|
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('HELLLO')
|
|
||||||
txController.setTxStatusRejected(txMetaFromEmit.id)
|
txController.setTxStatusRejected(txMetaFromEmit.id)
|
||||||
}, 10)
|
}, 10)
|
||||||
})
|
})
|
||||||
@ -133,11 +123,9 @@ describe('Transaction Controller', function () {
|
|||||||
'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
providerResultStub.eth_gasPrice = '4a817c800'
|
||||||
txController.query.stubResult('gasPrice', '0x4a817c800')
|
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }
|
||||||
txController.query.stubResult('getBlockByNumber', { gasLimit: '0x47b784' })
|
providerResultStub.eth_estimateGas = '5209'
|
||||||
txController.query.stubResult('estimateGas', '0x5209')
|
|
||||||
|
|
||||||
txController.addTxDefaults(txMeta)
|
txController.addTxDefaults(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')
|
||||||
@ -394,9 +382,8 @@ describe('Transaction Controller', function () {
|
|||||||
const wrongValue = '0x05'
|
const wrongValue = '0x05'
|
||||||
|
|
||||||
txController.addTx(txMeta)
|
txController.addTx(txMeta)
|
||||||
|
providerResultStub.eth_gasPrice = wrongValue
|
||||||
txController.query.stubResult('estimateGas', wrongValue)
|
providerResultStub.eth_estimateGas = '0x5209'
|
||||||
txController.query.stubResult('gasPrice', wrongValue)
|
|
||||||
|
|
||||||
const signStub = sinon.stub(txController, 'signTransaction').callsFake(() => Promise.resolve())
|
const signStub = sinon.stub(txController, 'signTransaction').callsFake(() => Promise.resolve())
|
||||||
|
|
||||||
@ -429,46 +416,4 @@ describe('Transaction Controller', function () {
|
|||||||
}).catch(done)
|
}).catch(done)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#_resubmitTx with a too-low balance', function () {
|
|
||||||
it('should fail the transaction', function (done) {
|
|
||||||
const from = '0xda0da0'
|
|
||||||
const txMeta = {
|
|
||||||
id: 1,
|
|
||||||
status: 'submitted',
|
|
||||||
metamaskNetworkId: currentNetworkId,
|
|
||||||
txParams: {
|
|
||||||
from,
|
|
||||||
nonce: '0x1',
|
|
||||||
value: '0xfffff',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const lowBalance = '0x0'
|
|
||||||
const fakeStoreState = { accounts: {} }
|
|
||||||
fakeStoreState.accounts[from] = {
|
|
||||||
balance: lowBalance,
|
|
||||||
nonce: '0x0',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stubbing out current account state:
|
|
||||||
const getStateStub = sinon.stub(txController.ethStore, 'getState')
|
|
||||||
.returns(fakeStoreState)
|
|
||||||
|
|
||||||
// Adding the fake tx:
|
|
||||||
txController.addTx(clone(txMeta))
|
|
||||||
|
|
||||||
txController._resubmitTx(txMeta)
|
|
||||||
.then(() => {
|
|
||||||
const updatedMeta = txController.getTx(txMeta.id)
|
|
||||||
assert.notEqual(updatedMeta.status, txMeta.status, 'status changed.')
|
|
||||||
assert.equal(updatedMeta.status, 'failed', 'tx set to failed.')
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
assert.ifError(err, 'should not throw an error')
|
|
||||||
done(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
@ -1,7 +1,7 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const ethUtil = require('ethereumjs-util')
|
const BN = require('bn.js')
|
||||||
const BN = ethUtil.BN
|
|
||||||
|
|
||||||
|
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
|
||||||
const TxUtils = require('../../app/scripts/lib/tx-utils')
|
const TxUtils = require('../../app/scripts/lib/tx-utils')
|
||||||
|
|
||||||
|
|
||||||
@ -16,44 +16,6 @@ describe('txUtils', function () {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#sufficientBalance', function () {
|
|
||||||
it('returns true if max tx cost is equal to balance.', function () {
|
|
||||||
const tx = {
|
|
||||||
'value': '0x1',
|
|
||||||
'gas': '0x2',
|
|
||||||
'gasPrice': '0x3',
|
|
||||||
}
|
|
||||||
const balance = '0x8'
|
|
||||||
|
|
||||||
const result = txUtils.sufficientBalance(tx, balance)
|
|
||||||
assert.ok(result, 'sufficient balance found.')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns true if max tx cost is less than balance.', function () {
|
|
||||||
const tx = {
|
|
||||||
'value': '0x1',
|
|
||||||
'gas': '0x2',
|
|
||||||
'gasPrice': '0x3',
|
|
||||||
}
|
|
||||||
const balance = '0x9'
|
|
||||||
|
|
||||||
const result = txUtils.sufficientBalance(tx, balance)
|
|
||||||
assert.ok(result, 'sufficient balance found.')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns false if max tx cost is more than balance.', function () {
|
|
||||||
const tx = {
|
|
||||||
'value': '0x1',
|
|
||||||
'gas': '0x2',
|
|
||||||
'gasPrice': '0x3',
|
|
||||||
}
|
|
||||||
const balance = '0x6'
|
|
||||||
|
|
||||||
const result = txUtils.sufficientBalance(tx, balance)
|
|
||||||
assert.ok(!result, 'insufficient balance found.')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('chain Id', function () {
|
describe('chain Id', function () {
|
||||||
it('prepares a transaction with the provided chainId', function () {
|
it('prepares a transaction with the provided chainId', function () {
|
||||||
const txParams = {
|
const txParams = {
|
||||||
@ -96,7 +58,7 @@ describe('txUtils', function () {
|
|||||||
assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value')
|
assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('buffers up to reccomend gas limit reccomended ceiling', function () {
|
it('buffers up to recommend gas limit recommended ceiling', function () {
|
||||||
// naive estimatedGas: 0x16e360 (1.5 mil)
|
// naive estimatedGas: 0x16e360 (1.5 mil)
|
||||||
const inputHex = '0x16e360'
|
const inputHex = '0x16e360'
|
||||||
// dummy gas limit: 0x1e8480 (2 mil)
|
// dummy gas limit: 0x1e8480 (2 mil)
|
||||||
@ -107,17 +69,7 @@ describe('txUtils', function () {
|
|||||||
// const inputBn = hexToBn(inputHex)
|
// const inputBn = hexToBn(inputHex)
|
||||||
// const outputBn = hexToBn(output)
|
// const outputBn = hexToBn(output)
|
||||||
const expectedHex = bnToHex(ceilGasLimitBn)
|
const expectedHex = bnToHex(ceilGasLimitBn)
|
||||||
assert.equal(output, expectedHex, 'returns the gas limit reccomended ceiling value')
|
assert.equal(output, expectedHex, 'returns the gas limit recommended ceiling value')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// util
|
|
||||||
|
|
||||||
function hexToBn (inputHex) {
|
|
||||||
return new BN(ethUtil.stripHexPrefix(inputHex), 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
function bnToHex (inputBn) {
|
|
||||||
return ethUtil.addHexPrefix(inputBn.toString(16))
|
|
||||||
}
|
|
41
test/unit/util-test.js
Normal file
41
test/unit/util-test.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const assert = require('assert')
|
||||||
|
const { sufficientBalance } = require('../../app/scripts/lib/util')
|
||||||
|
|
||||||
|
|
||||||
|
describe('SufficientBalance', function () {
|
||||||
|
it('returns true if max tx cost is equal to balance.', function () {
|
||||||
|
const tx = {
|
||||||
|
'value': '0x1',
|
||||||
|
'gas': '0x2',
|
||||||
|
'gasPrice': '0x3',
|
||||||
|
}
|
||||||
|
const balance = '0x8'
|
||||||
|
|
||||||
|
const result = sufficientBalance(tx, balance)
|
||||||
|
assert.ok(result, 'sufficient balance found.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true if max tx cost is less than balance.', function () {
|
||||||
|
const tx = {
|
||||||
|
'value': '0x1',
|
||||||
|
'gas': '0x2',
|
||||||
|
'gasPrice': '0x3',
|
||||||
|
}
|
||||||
|
const balance = '0x9'
|
||||||
|
|
||||||
|
const result = sufficientBalance(tx, balance)
|
||||||
|
assert.ok(result, 'sufficient balance found.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false if max tx cost is more than balance.', function () {
|
||||||
|
const tx = {
|
||||||
|
'value': '0x1',
|
||||||
|
'gas': '0x2',
|
||||||
|
'gasPrice': '0x3',
|
||||||
|
}
|
||||||
|
const balance = '0x6'
|
||||||
|
|
||||||
|
const result = sufficientBalance(tx, balance)
|
||||||
|
assert.ok(!result, 'insufficient balance found.')
|
||||||
|
})
|
||||||
|
})
|
@ -143,7 +143,7 @@ BuyButtonSubview.prototype.formVersionSubview = function () {
|
|||||||
return h('div.flex-column', {
|
return h('div.flex-column', {
|
||||||
style: {
|
style: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
margin: '50px',
|
margin: '20px 50px',
|
||||||
},
|
},
|
||||||
}, [
|
}, [
|
||||||
h('h3.text-transform-uppercase', {
|
h('h3.text-transform-uppercase', {
|
||||||
|
@ -33,10 +33,10 @@ ShapeshiftForm.prototype.renderMain = function () {
|
|||||||
|
|
||||||
return h('.flex-column', {
|
return h('.flex-column', {
|
||||||
style: {
|
style: {
|
||||||
// marginTop: '10px',
|
position: 'relative',
|
||||||
padding: '25px',
|
padding: '25px',
|
||||||
paddingTop: '5px',
|
paddingTop: '5px',
|
||||||
width: '100%',
|
width: '90%',
|
||||||
minHeight: '215px',
|
minHeight: '215px',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
@ -58,7 +58,9 @@ ShapeshiftForm.prototype.renderMain = function () {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
h('.input-container', [
|
h('.input-container', {
|
||||||
|
position: 'relative',
|
||||||
|
}, [
|
||||||
h('input#fromCoin.buy-inputs.ex-coins', {
|
h('input#fromCoin.buy-inputs.ex-coins', {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
list: 'coinList',
|
list: 'coinList',
|
||||||
@ -79,27 +81,31 @@ ShapeshiftForm.prototype.renderMain = function () {
|
|||||||
style: {
|
style: {
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#F7861C',
|
color: '#F7861C',
|
||||||
position: 'relative',
|
position: 'absolute',
|
||||||
bottom: '48px',
|
|
||||||
left: '106px',
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
h('.icon-control', [
|
h('.icon-control', {
|
||||||
h('i.fa.fa-refresh.fa-4.orange', {
|
style: {
|
||||||
style: {
|
position: 'relative',
|
||||||
bottom: '5px',
|
},
|
||||||
left: '5px',
|
}, [
|
||||||
color: '#F7861C',
|
// Not visible on the screen, can't see it on master.
|
||||||
},
|
|
||||||
onClick: this.updateCoin.bind(this),
|
// h('i.fa.fa-refresh.fa-4.orange', {
|
||||||
}),
|
// style: {
|
||||||
|
// bottom: '5px',
|
||||||
|
// left: '5px',
|
||||||
|
// color: '#F7861C',
|
||||||
|
// },
|
||||||
|
// onClick: this.updateCoin.bind(this),
|
||||||
|
// }),
|
||||||
h('i.fa.fa-chevron-right.fa-4.orange', {
|
h('i.fa.fa-chevron-right.fa-4.orange', {
|
||||||
style: {
|
style: {
|
||||||
position: 'relative',
|
position: 'absolute',
|
||||||
bottom: '26px',
|
bottom: '35%',
|
||||||
left: '10px',
|
left: '0%',
|
||||||
color: '#F7861C',
|
color: '#F7861C',
|
||||||
},
|
},
|
||||||
onClick: this.updateCoin.bind(this),
|
onClick: this.updateCoin.bind(this),
|
||||||
@ -117,69 +123,73 @@ ShapeshiftForm.prototype.renderMain = function () {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
h('.flex-column', {
|
h('.flex-column', {
|
||||||
style: {
|
style: {
|
||||||
|
marginTop: '1%',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
},
|
},
|
||||||
}, [
|
}, [
|
||||||
this.props.warning ? this.props.warning && h('span.error.flex-center', {
|
this.props.warning
|
||||||
style: {
|
? this.props.warning
|
||||||
textAlign: 'center',
|
&& h('span.error.flex-center', {
|
||||||
width: '229px',
|
style: {
|
||||||
height: '82px',
|
textAlign: 'center',
|
||||||
},
|
width: '229px',
|
||||||
},
|
height: '82px',
|
||||||
this.props.warning) : this.renderInfo(),
|
},
|
||||||
|
}, this.props.warning)
|
||||||
|
: this.renderInfo(),
|
||||||
|
|
||||||
|
this.renderRefundAddressForCoin(coin),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
h(this.activeToggle('.input-container'), {
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
ShapeshiftForm.prototype.renderRefundAddressForCoin = function (coin) {
|
||||||
|
return h(this.activeToggle('.input-container'), {
|
||||||
|
style: {
|
||||||
|
marginTop: '1%',
|
||||||
|
},
|
||||||
|
}, [
|
||||||
|
|
||||||
|
h('div', `${coin} Address:`),
|
||||||
|
|
||||||
|
h('input#fromCoinAddress.buy-inputs', {
|
||||||
|
type: 'text',
|
||||||
|
placeholder: `Your ${coin} Refund Address`,
|
||||||
|
dataset: {
|
||||||
|
persistentFormId: 'refund-address',
|
||||||
|
|
||||||
|
},
|
||||||
style: {
|
style: {
|
||||||
padding: '10px',
|
boxSizing: 'border-box',
|
||||||
paddingTop: '0px',
|
width: '227px',
|
||||||
width: '100%',
|
height: '30px',
|
||||||
|
padding: ' 5px ',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
h('i.fa.fa-pencil-square-o.edit-text', {
|
||||||
|
style: {
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#F7861C',
|
||||||
|
position: 'absolute',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
h('div.flex-row', {
|
||||||
|
style: {
|
||||||
|
justifyContent: 'flex-start',
|
||||||
},
|
},
|
||||||
}, [
|
}, [
|
||||||
|
h('button', {
|
||||||
h('div', `${coin} Address:`),
|
onClick: this.shift.bind(this),
|
||||||
|
|
||||||
h('input#fromCoinAddress.buy-inputs', {
|
|
||||||
type: 'text',
|
|
||||||
placeholder: `Your ${coin} Refund Address`,
|
|
||||||
dataset: {
|
|
||||||
persistentFormId: 'refund-address',
|
|
||||||
},
|
|
||||||
style: {
|
style: {
|
||||||
boxSizing: 'border-box',
|
marginTop: '1%',
|
||||||
width: '227px',
|
|
||||||
height: '30px',
|
|
||||||
padding: ' 5px ',
|
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
|
'Submit'),
|
||||||
h('i.fa.fa-pencil-square-o.edit-text', {
|
|
||||||
style: {
|
|
||||||
fontSize: '12px',
|
|
||||||
color: '#F7861C',
|
|
||||||
position: 'relative',
|
|
||||||
bottom: '10px',
|
|
||||||
right: '11px',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
h('.flex-row', {
|
|
||||||
style: {
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
},
|
|
||||||
}, [
|
|
||||||
h('button', {
|
|
||||||
onClick: this.shift.bind(this),
|
|
||||||
style: {
|
|
||||||
marginTop: '10px',
|
|
||||||
position: 'relative',
|
|
||||||
bottom: '40px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'Submit'),
|
|
||||||
]),
|
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -154,12 +154,21 @@ function failIfFailed (transaction) {
|
|||||||
if (transaction.status === 'rejected') {
|
if (transaction.status === 'rejected') {
|
||||||
return h('span.error', ' (Rejected)')
|
return h('span.error', ' (Rejected)')
|
||||||
}
|
}
|
||||||
if (transaction.err) {
|
if (transaction.err || transaction.warning) {
|
||||||
|
const { err, warning = {} } = transaction
|
||||||
|
const errFirst = !!(( err && warning ) || err)
|
||||||
|
const message = errFirst ? err.message : warning.message
|
||||||
|
|
||||||
|
errFirst ? err.message : warning.message
|
||||||
|
|
||||||
|
|
||||||
return h(Tooltip, {
|
return h(Tooltip, {
|
||||||
title: transaction.err.message,
|
title: message,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
}, [
|
}, [
|
||||||
h('span.error', ' (Failed)'),
|
h(`span.${errFirst ? 'error' : 'warning'}`,
|
||||||
|
` (${errFirst ? 'Failed' : 'Warning'})`
|
||||||
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user