You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
277 lines
10 KiB
277 lines
10 KiB
7 years ago
|
'use strict'
|
||
5 years ago
|
const EthJSTX = require('ethereumjs-tx').Transaction
|
||
|
const EthJSBlock = require('ethereumjs-block')
|
||
4 years ago
|
import { BN } from 'ethereumjs-util'
|
||
5 years ago
|
const defaultExecutionContext = require('./execution-context')
|
||
|
const EventManager = require('../eventManager')
|
||
7 years ago
|
|
||
7 years ago
|
class TxRunner {
|
||
4 years ago
|
|
||
|
event
|
||
|
executionContext
|
||
|
_api
|
||
|
blockNumber
|
||
|
runAsync
|
||
|
pendingTxs
|
||
|
vmaccounts
|
||
|
queusTxs
|
||
|
blocks
|
||
|
|
||
5 years ago
|
constructor (vmaccounts, api, executionContext) {
|
||
7 years ago
|
this.event = new EventManager()
|
||
5 years ago
|
// has a default for now for backwards compatability
|
||
|
this.executionContext = executionContext || defaultExecutionContext
|
||
7 years ago
|
this._api = api
|
||
|
this.blockNumber = 0
|
||
|
this.runAsync = true
|
||
5 years ago
|
if (this.executionContext.isVM()) {
|
||
6 years ago
|
// this.blockNumber = 1150000 // The VM is running in Homestead mode, which started at this block.
|
||
6 years ago
|
this.blockNumber = 0 // The VM is running in Homestead mode, which started at this block.
|
||
7 years ago
|
this.runAsync = false // We have to run like this cause the VM Event Manager does not support running multiple txs at the same time.
|
||
|
}
|
||
|
this.pendingTxs = {}
|
||
|
this.vmaccounts = vmaccounts
|
||
|
this.queusTxs = []
|
||
6 years ago
|
this.blocks = []
|
||
7 years ago
|
}
|
||
|
|
||
7 years ago
|
rawRun (args, confirmationCb, gasEstimationForceSend, promptCb, cb) {
|
||
5 years ago
|
let timestamp = Date.now()
|
||
6 years ago
|
if (args.timestamp) {
|
||
6 years ago
|
timestamp = args.timestamp
|
||
6 years ago
|
}
|
||
|
run(this, args, timestamp, confirmationCb, gasEstimationForceSend, promptCb, cb)
|
||
7 years ago
|
}
|
||
|
|
||
7 years ago
|
_executeTx (tx, gasPrice, api, promptCb, callback) {
|
||
5 years ago
|
if (gasPrice) tx.gasPrice = this.executionContext.web3().utils.toHex(gasPrice)
|
||
7 years ago
|
if (api.personalMode()) {
|
||
|
promptCb(
|
||
|
(value) => {
|
||
5 years ago
|
this._sendTransaction(this.executionContext.web3().personal.sendTransaction, tx, value, callback)
|
||
7 years ago
|
},
|
||
|
() => {
|
||
|
return callback('Canceled by user.')
|
||
|
}
|
||
|
)
|
||
|
} else {
|
||
5 years ago
|
this._sendTransaction(this.executionContext.web3().eth.sendTransaction, tx, null, callback)
|
||
7 years ago
|
}
|
||
|
}
|
||
7 years ago
|
|
||
7 years ago
|
_sendTransaction (sendTx, tx, pass, callback) {
|
||
5 years ago
|
const cb = (err, resp) => {
|
||
7 years ago
|
if (err) {
|
||
|
return callback(err, resp)
|
||
|
}
|
||
5 years ago
|
this.event.trigger('transactionBroadcasted', [resp])
|
||
7 years ago
|
var listenOnResponse = () => {
|
||
5 years ago
|
// eslint-disable-next-line no-async-promise-executor
|
||
7 years ago
|
return new Promise(async (resolve, reject) => {
|
||
5 years ago
|
const result = await tryTillReceiptAvailable(resp, this.executionContext)
|
||
|
tx = await tryTillTxAvailable(resp, this.executionContext)
|
||
7 years ago
|
resolve({
|
||
7 years ago
|
result,
|
||
7 years ago
|
tx,
|
||
4 years ago
|
transactionHash: result ? result['transactionHash'] : null
|
||
7 years ago
|
})
|
||
|
})
|
||
|
}
|
||
|
listenOnResponse().then((txData) => { callback(null, txData) }).catch((error) => { callback(error) })
|
||
|
}
|
||
5 years ago
|
const args = pass !== null ? [tx, pass, cb] : [tx, cb]
|
||
7 years ago
|
try {
|
||
7 years ago
|
sendTx.apply({}, args)
|
||
7 years ago
|
} catch (e) {
|
||
7 years ago
|
return callback(`Send transaction failed: ${e.message} . if you use an injected provider, please check it is properly unlocked. `)
|
||
7 years ago
|
}
|
||
|
}
|
||
|
|
||
5 years ago
|
execute(args, confirmationCb, gasEstimationForceSend, promptCb, callback) {
|
||
5 years ago
|
let data = args.data
|
||
7 years ago
|
if (data.slice(0, 2) !== '0x') {
|
||
|
data = '0x' + data
|
||
7 years ago
|
}
|
||
7 years ago
|
|
||
5 years ago
|
if (!this.executionContext.isVM()) {
|
||
5 years ago
|
return this.runInNode(args.from, args.to, data, args.value, args.gasLimit, args.useCall, confirmationCb, gasEstimationForceSend, promptCb, callback)
|
||
|
}
|
||
|
try {
|
||
|
this.runInVm(args.from, args.to, data, args.value, args.gasLimit, args.useCall, args.timestamp, callback)
|
||
|
} catch (e) {
|
||
|
callback(e, null)
|
||
7 years ago
|
}
|
||
7 years ago
|
}
|
||
7 years ago
|
|
||
6 years ago
|
runInVm (from, to, data, value, gasLimit, useCall, timestamp, callback) {
|
||
7 years ago
|
const self = this
|
||
5 years ago
|
const account = self.vmaccounts[from]
|
||
7 years ago
|
if (!account) {
|
||
|
return callback('Invalid account selected')
|
||
|
}
|
||
6 years ago
|
|
||
5 years ago
|
this.executionContext.vm().stateManager.getAccount(Buffer.from(from.replace('0x', ''), 'hex'), (err, res) => {
|
||
6 years ago
|
if (err) {
|
||
|
callback('Account not found')
|
||
|
} else {
|
||
5 years ago
|
// See https://github.com/ethereumjs/ethereumjs-tx/blob/master/docs/classes/transaction.md#constructor
|
||
|
// for initialization fields and their types
|
||
5 years ago
|
value = value ? parseInt(value) : 0
|
||
5 years ago
|
const tx = new EthJSTX({
|
||
5 years ago
|
nonce: new BN(res.nonce),
|
||
5 years ago
|
gasPrice: '0x1',
|
||
6 years ago
|
gasLimit: gasLimit,
|
||
|
to: to,
|
||
5 years ago
|
value: value,
|
||
6 years ago
|
data: Buffer.from(data.slice(2), 'hex')
|
||
|
})
|
||
|
tx.sign(account.privateKey)
|
||
|
const coinbases = ['0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e']
|
||
|
const difficulties = [new BN('69762765929000', 10), new BN('70762765929000', 10), new BN('71762765929000', 10)]
|
||
5 years ago
|
const block = new EthJSBlock({
|
||
6 years ago
|
header: {
|
||
|
timestamp: timestamp || (new Date().getTime() / 1000 | 0),
|
||
|
number: self.blockNumber,
|
||
|
coinbase: coinbases[self.blockNumber % coinbases.length],
|
||
|
difficulty: difficulties[self.blockNumber % difficulties.length],
|
||
|
gasLimit: new BN(gasLimit, 10).imuln(2)
|
||
|
},
|
||
|
transactions: [tx],
|
||
|
uncleHeaders: []
|
||
|
})
|
||
|
if (!useCall) {
|
||
|
++self.blockNumber
|
||
|
this.runBlockInVm(tx, block, callback)
|
||
|
} else {
|
||
5 years ago
|
this.executionContext.vm().stateManager.checkpoint(() => {
|
||
6 years ago
|
this.runBlockInVm(tx, block, (err, result) => {
|
||
5 years ago
|
this.executionContext.vm().stateManager.revert(() => {
|
||
6 years ago
|
callback(err, result)
|
||
|
})
|
||
|
})
|
||
|
})
|
||
6 years ago
|
}
|
||
6 years ago
|
}
|
||
|
})
|
||
|
}
|
||
6 years ago
|
|
||
6 years ago
|
runBlockInVm (tx, block, callback) {
|
||
5 years ago
|
this.executionContext.vm().runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false }).then((results) => {
|
||
6 years ago
|
let result = results.results[0]
|
||
|
if (result) {
|
||
5 years ago
|
const status = result.execResult.exceptionError ? 0 : 1
|
||
|
result.status = `0x${status}`
|
||
6 years ago
|
}
|
||
5 years ago
|
this.executionContext.addBlock(block)
|
||
|
this.executionContext.trackTx('0x' + tx.hash().toString('hex'), block)
|
||
5 years ago
|
callback(null, {
|
||
6 years ago
|
result: result,
|
||
|
transactionHash: ethJSUtil.bufferToHex(Buffer.from(tx.hash()))
|
||
6 years ago
|
})
|
||
5 years ago
|
}).catch(function (err) {
|
||
|
callback(err)
|
||
6 years ago
|
})
|
||
7 years ago
|
}
|
||
|
|
||
7 years ago
|
runInNode (from, to, data, value, gasLimit, useCall, confirmCb, gasEstimationForceSend, promptCb, callback) {
|
||
5 years ago
|
const tx = { from: from, to: to, data: data, value: value }
|
||
7 years ago
|
|
||
|
if (useCall) {
|
||
4 years ago
|
tx['gas'] = gasLimit
|
||
5 years ago
|
return this.executionContext.web3().eth.call(tx, function (error, result) {
|
||
7 years ago
|
callback(error, {
|
||
|
result: result,
|
||
|
transactionHash: result ? result.transactionHash : null
|
||
|
})
|
||
|
})
|
||
|
}
|
||
5 years ago
|
this.executionContext.web3().eth.estimateGas(tx, (err, gasEstimation) => {
|
||
5 years ago
|
if (err && err.message.indexOf('Invalid JSON RPC response') !== -1) {
|
||
5 years ago
|
// // @todo(#378) this should be removed when https://github.com/WalletConnect/walletconnect-monorepo/issues/334 is fixed
|
||
5 years ago
|
err = 'Gas estimation failed because of an unknown internal error. This may indicated that the transaction will fail.'
|
||
|
}
|
||
7 years ago
|
gasEstimationForceSend(err, () => {
|
||
|
// callback is called whenever no error
|
||
4 years ago
|
tx['gas'] = !gasEstimation ? gasLimit : gasEstimation
|
||
7 years ago
|
|
||
5 years ago
|
if (this._api.config.getUnpersistedProperty('doNotShowTransactionConfirmationAgain')) {
|
||
|
return this._executeTx(tx, null, this._api, promptCb, callback)
|
||
7 years ago
|
}
|
||
|
|
||
5 years ago
|
this._api.detectNetwork((err, network) => {
|
||
7 years ago
|
if (err) {
|
||
|
console.log(err)
|
||
|
return
|
||
|
}
|
||
|
|
||
4 years ago
|
confirmCb(network, tx, tx['gas'], (gasPrice) => {
|
||
5 years ago
|
return this._executeTx(tx, gasPrice, this._api, promptCb, callback)
|
||
7 years ago
|
}, (error) => {
|
||
|
callback(error)
|
||
|
})
|
||
7 years ago
|
})
|
||
7 years ago
|
}, () => {
|
||
5 years ago
|
const blockGasLimit = this.executionContext.currentblockGasLimit()
|
||
7 years ago
|
// NOTE: estimateGas very likely will return a large limit if execution of the code failed
|
||
|
// we want to be able to run the code in order to debug and find the cause for the failure
|
||
|
if (err) return callback(err)
|
||
7 years ago
|
|
||
5 years ago
|
let warnEstimation = ' An important gas estimation might also be the sign of a problem in the contract code. Please check loops and be sure you did not sent value to a non payable function (that\'s also the reason of strong gas estimation). '
|
||
7 years ago
|
warnEstimation += ' ' + err
|
||
7 years ago
|
|
||
7 years ago
|
if (gasEstimation > gasLimit) {
|
||
|
return callback('Gas required exceeds limit: ' + gasLimit + '. ' + warnEstimation)
|
||
|
}
|
||
|
if (gasEstimation > blockGasLimit) {
|
||
|
return callback('Gas required exceeds block gas limit: ' + gasLimit + '. ' + warnEstimation)
|
||
|
}
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
5 years ago
|
async function tryTillReceiptAvailable (txhash, executionContext) {
|
||
7 years ago
|
return new Promise((resolve, reject) => {
|
||
5 years ago
|
executionContext.web3().eth.getTransactionReceipt(txhash, async (err, receipt) => {
|
||
7 years ago
|
if (err || !receipt) {
|
||
|
// Try again with a bit of delay if error or if result still null
|
||
|
await pause()
|
||
5 years ago
|
return resolve(await tryTillReceiptAvailable(txhash, executionContext))
|
||
7 years ago
|
}
|
||
5 years ago
|
return resolve(receipt)
|
||
7 years ago
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
5 years ago
|
async function tryTillTxAvailable (txhash, executionContext) {
|
||
7 years ago
|
return new Promise((resolve, reject) => {
|
||
5 years ago
|
executionContext.web3().eth.getTransaction(txhash, async (err, tx) => {
|
||
7 years ago
|
if (err || !tx) {
|
||
|
// Try again with a bit of delay if error or if result still null
|
||
|
await pause()
|
||
5 years ago
|
return resolve(await tryTillTxAvailable(txhash, executionContext))
|
||
7 years ago
|
}
|
||
5 years ago
|
return resolve(tx)
|
||
7 years ago
|
})
|
||
7 years ago
|
})
|
||
|
}
|
||
|
|
||
7 years ago
|
async function pause () { return new Promise((resolve, reject) => { setTimeout(resolve, 500) }) }
|
||
|
|
||
4 years ago
|
function run(self, tx, stamp, confirmationCb, gasEstimationForceSend = null, promptCb = null, callback = null) {
|
||
7 years ago
|
if (!self.runAsync && Object.keys(self.pendingTxs).length) {
|
||
5 years ago
|
return self.queusTxs.push({ tx, stamp, callback })
|
||
7 years ago
|
}
|
||
5 years ago
|
self.pendingTxs[stamp] = tx
|
||
|
self.execute(tx, confirmationCb, gasEstimationForceSend, promptCb, function (error, result) {
|
||
|
delete self.pendingTxs[stamp]
|
||
|
if (callback && typeof callback === 'function') callback(error, result)
|
||
|
if (self.queusTxs.length) {
|
||
|
const next = self.queusTxs.pop()
|
||
|
run(self, next.tx, next.stamp, next.callback)
|
||
|
}
|
||
|
})
|
||
7 years ago
|
}
|
||
|
|
||
|
module.exports = TxRunner
|