'use strict'
import { Transaction } from '@ethereumjs/tx'
import { Block } from '@ethereumjs/block'
import { BN , bufferToHex , Address } from 'ethereumjs-util'
import { ExecutionContext } from './execution-context'
import { EventManager } from '../eventManager'
export class TxRunner {
event
executionContext
_api
blockNumber
runAsync
pendingTxs
vmaccounts
queusTxs
blocks
commonContext
constructor ( vmaccounts , api , executionContext ) {
this . event = new EventManager ( )
// has a default for now for backwards compatability
this . executionContext = executionContext || new ExecutionContext ( )
this . commonContext = this . executionContext . vmObject ( ) . common
this . _api = api
this . blockNumber = 0
this . runAsync = true
if ( this . executionContext . isVM ( ) ) {
// this.blockNumber = 1150000 // The VM is running in Homestead mode, which started at this block.
this . blockNumber = 0 // The VM is running in Homestead mode, which started at this block.
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 = [ ]
this . blocks = [ ]
}
rawRun ( args , confirmationCb , gasEstimationForceSend , promptCb , cb ) {
let timestamp = Date . now ( )
if ( args . timestamp ) {
timestamp = args . timestamp
}
run ( this , args , timestamp , confirmationCb , gasEstimationForceSend , promptCb , cb )
}
_executeTx ( tx , gasPrice , api , promptCb , callback ) {
if ( gasPrice ) tx . gasPrice = this . executionContext . web3 ( ) . utils . toHex ( gasPrice )
if ( api . personalMode ( ) ) {
promptCb (
( value ) = > {
this . _sendTransaction ( this . executionContext . web3 ( ) . personal . sendTransaction , tx , value , callback )
} ,
( ) = > {
return callback ( 'Canceled by user.' )
}
)
} else {
this . _sendTransaction ( this . executionContext . web3 ( ) . eth . sendTransaction , tx , null , callback )
}
}
_sendTransaction ( sendTx , tx , pass , callback ) {
const cb = ( err , resp ) = > {
if ( err ) {
return callback ( err , resp )
}
this . event . trigger ( 'transactionBroadcasted' , [ resp ] )
var listenOnResponse = ( ) = > {
// eslint-disable-next-line no-async-promise-executor
return new Promise ( async ( resolve , reject ) = > {
const result = await tryTillReceiptAvailable ( resp , this . executionContext )
tx = await tryTillTxAvailable ( resp , this . executionContext )
resolve ( {
result ,
tx ,
transactionHash : result ? result [ 'transactionHash' ] : null
} )
} )
}
listenOnResponse ( ) . then ( ( txData ) = > { callback ( null , txData ) } ) . catch ( ( error ) = > { callback ( error ) } )
}
const args = pass !== null ? [ tx , pass , cb ] : [ tx , cb ]
try {
sendTx . apply ( { } , args )
} catch ( e ) {
return callback ( ` Send transaction failed: ${ e . message } . if you use an injected provider, please check it is properly unlocked. ` )
}
}
execute ( args , confirmationCb , gasEstimationForceSend , promptCb , callback ) {
let data = args . data
if ( data . slice ( 0 , 2 ) !== '0x' ) {
data = '0x' + data
}
if ( ! this . executionContext . isVM ( ) ) {
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 )
}
}
runInVm ( from , to , data , value , gasLimit , useCall , timestamp , callback ) {
const self = this
const account = self . vmaccounts [ from ]
if ( ! account ) {
return callback ( 'Invalid account selected' )
}
if ( Number . isInteger ( gasLimit ) ) {
gasLimit = '0x' + gasLimit . toString ( 16 )
}
this . executionContext . vm ( ) . stateManager . getAccount ( Address . fromString ( from ) ) . then ( ( res ) = > {
// See https://github.com/ethereumjs/ethereumjs-tx/blob/master/docs/classes/transaction.md#constructor
// for initialization fields and their types
value = value ? parseInt ( value ) : 0
const tx = Transaction . fromTxData ( {
nonce : new BN ( res . nonce ) ,
gasPrice : '0x1' ,
gasLimit : gasLimit ,
to : to ,
value : value ,
data : Buffer.from ( data . slice ( 2 ) , 'hex' )
} , { common : this.commonContext } ) . sign ( account . privateKey )
const coinbases = [ '0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a' , '0x8945a1288dc78a6d8952a92c77aee6730b414778' , '0x94d76e24f818426ae84aa404140e8d5f60e10e7e' ]
const difficulties = [ new BN ( '69762765929000' , 10 ) , new BN ( '70762765929000' , 10 ) , new BN ( '71762765929000' , 10 ) ]
var block = Block . fromBlockData ( {
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 . replace ( '0x' , '' ) , 16 ) . imuln ( 2 )
} ,
transactions : [ tx ]
} , { common : this.commonContext } )
if ( ! useCall ) {
++ self . blockNumber
this . runBlockInVm ( tx , block , callback )
} else {
this . executionContext . vm ( ) . stateManager . checkpoint ( ) . then ( ( ) = > {
this . runBlockInVm ( tx , block , ( err , result ) = > {
this . executionContext . vm ( ) . stateManager . revert ( ) . then ( ( ) = > {
callback ( err , result )
} )
} )
} )
}
} ) . catch ( ( e ) = > {
callback ( e )
} )
}
runBlockInVm ( tx , block , callback ) {
this . executionContext . vm ( ) . runBlock ( { block : block , generate : true , skipBlockValidation : true , skipBalance : false } ) . then ( ( results ) = > {
const result = results . results [ 0 ]
if ( result ) {
const status = result . execResult . exceptionError ? 0 : 1
result . status = ` 0x ${ status } `
}
this . executionContext . addBlock ( block )
this . executionContext . trackTx ( '0x' + tx . hash ( ) . toString ( 'hex' ) , block )
callback ( null , {
result : result ,
transactionHash : bufferToHex ( Buffer . from ( tx . hash ( ) ) )
} )
} ) . catch ( ( err ) = > {
callback ( err )
} )
}
runInNode ( from , to , data , value , gasLimit , useCall , confirmCb , gasEstimationForceSend , promptCb , callback ) {
const tx = { from : from , to : to , data : data , value : value }
if ( useCall ) {
tx [ 'gas' ] = gasLimit
return this . executionContext . web3 ( ) . eth . call ( tx , function ( error , result ) {
callback ( error , {
result : result ,
transactionHash : result ? result.transactionHash : null
} )
} )
}
this . executionContext . web3 ( ) . eth . estimateGas ( tx , ( err , gasEstimation ) = > {
if ( err && err . message . indexOf ( 'Invalid JSON RPC response' ) !== - 1 ) {
// // @todo(#378) this should be removed when https://github.com/WalletConnect/walletconnect-monorepo/issues/334 is fixed
err = 'Gas estimation failed because of an unknown internal error. This may indicated that the transaction will fail.'
}
gasEstimationForceSend ( err , ( ) = > {
// callback is called whenever no error
tx [ 'gas' ] = ! gasEstimation ? gasLimit : gasEstimation
if ( this . _api . config . getUnpersistedProperty ( 'doNotShowTransactionConfirmationAgain' ) ) {
return this . _executeTx ( tx , null , this . _api , promptCb , callback )
}
this . _api . detectNetwork ( ( err , network ) = > {
if ( err ) {
console . log ( err )
return
}
confirmCb ( network , tx , tx [ 'gas' ] , ( gasPrice ) = > {
return this . _executeTx ( tx , gasPrice , this . _api , promptCb , callback )
} , ( error ) = > {
callback ( error )
} )
} )
} , ( ) = > {
const blockGasLimit = this . executionContext . currentblockGasLimit ( )
// 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 )
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). '
warnEstimation += ' ' + err
if ( gasEstimation > gasLimit ) {
return callback ( 'Gas required exceeds limit: ' + gasLimit + '. ' + warnEstimation )
}
if ( gasEstimation > blockGasLimit ) {
return callback ( 'Gas required exceeds block gas limit: ' + gasLimit + '. ' + warnEstimation )
}
} )
} )
}
}
async function tryTillReceiptAvailable ( txhash , executionContext ) {
return new Promise ( ( resolve , reject ) = > {
executionContext . web3 ( ) . eth . getTransactionReceipt ( txhash , async ( err , receipt ) = > {
if ( err || ! receipt ) {
// Try again with a bit of delay if error or if result still null
await pause ( )
return resolve ( await tryTillReceiptAvailable ( txhash , executionContext ) )
}
return resolve ( receipt )
} )
} )
}
async function tryTillTxAvailable ( txhash , executionContext ) {
return new Promise ( ( resolve , reject ) = > {
executionContext . web3 ( ) . eth . getTransaction ( txhash , async ( err , tx ) = > {
if ( err || ! tx ) {
// Try again with a bit of delay if error or if result still null
await pause ( )
return resolve ( await tryTillTxAvailable ( txhash , executionContext ) )
}
return resolve ( tx )
} )
} )
}
async function pause ( ) { return new Promise ( ( resolve , reject ) = > { setTimeout ( resolve , 500 ) } ) }
function run ( self , tx , stamp , confirmationCb , gasEstimationForceSend = null , promptCb = null , callback = null ) {
if ( ! self . runAsync && Object . keys ( self . pendingTxs ) . length ) {
return self . queusTxs . push ( { tx , stamp , callback } )
}
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 )
}
} )
}