remix-ide
Version:
Extendable Web IDE for Ethereum
480 lines (418 loc) • 16.9 kB
JavaScript
const remixLib = require('remix-lib')
const txFormat = remixLib.execution.txFormat
const txExecution = remixLib.execution.txExecution
const typeConversion = remixLib.execution.typeConversion
const Txlistener = remixLib.execution.txListener
const TxRunner = remixLib.execution.txRunner
const txHelper = remixLib.execution.txHelper
const EventManager = remixLib.EventManager
const executionContext = remixLib.execution.executionContext
const Web3 = require('web3')
const async = require('async')
const { EventEmitter } = require('events')
const { resultToRemixTx } = require('./txResultHelper')
const VMProvider = require('./providers/vm.js')
const InjectedProvider = require('./providers/injected.js')
const NodeProvider = require('./providers/node.js')
class Blockchain {
// NOTE: the config object will need to be refactored out in remix-lib
constructor (config) {
this.event = new EventManager()
this.executionContext = executionContext
this.events = new EventEmitter()
this.config = config
this.txRunner = new TxRunner({}, {
config: config,
detectNetwork: (cb) => {
this.executionContext.detectNetwork(cb)
},
personalMode: () => {
return this.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false
}
}, this.executionContext)
this.executionContext.event.register('contextChanged', this.resetEnvironment.bind(this))
this.networkcallid = 0
this.setupEvents()
this.setupProviders()
}
setupEvents () {
this.executionContext.event.register('contextChanged', (context, silent) => {
this.event.trigger('contextChanged', [context, silent])
})
this.executionContext.event.register('addProvider', (network) => {
this.event.trigger('addProvider', [network])
})
this.executionContext.event.register('removeProvider', (name) => {
this.event.trigger('removeProvider', [name])
})
}
setupProviders () {
this.providers = {}
this.providers.vm = new VMProvider(this.executionContext)
this.providers.injected = new InjectedProvider(this.executionContext)
this.providers.web3 = new NodeProvider(this.executionContext, this.config)
}
getCurrentProvider () {
const provider = this.getProvider()
return this.providers[provider]
}
/** Return the list of accounts */
// note: the dual promise/callback is kept for now as it was before
getAccounts (cb) {
return new Promise((resolve, reject) => {
this.getCurrentProvider().getAccounts((error, accounts) => {
if (cb) {
return cb(error, accounts)
}
if (error) {
reject(error)
}
resolve(accounts)
})
})
}
deployContractAndLibraries (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) {
const { continueCb, promptCb, statusCb, finalCb } = callbacks
const constructor = selectedContract.getConstructorInterface()
txFormat.buildData(selectedContract.name, selectedContract.object, compilerContracts, true, constructor, args, (error, data) => {
if (error) return statusCb(`creation of ${selectedContract.name} errored: ` + error)
statusCb(`creation of ${selectedContract.name} pending...`)
this.createContract(selectedContract, data, continueCb, promptCb, confirmationCb, finalCb)
}, statusCb, (data, runTxCallback) => {
// called for libraries deployment
this.runTx(data, confirmationCb, continueCb, promptCb, runTxCallback)
})
}
deployContractWithLibrary (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) {
const { continueCb, promptCb, statusCb, finalCb } = callbacks
const constructor = selectedContract.getConstructorInterface()
txFormat.encodeConstructorCallAndLinkLibraries(selectedContract.object, args, constructor, contractMetadata.linkReferences, selectedContract.bytecodeLinkReferences, (error, data) => {
if (error) return statusCb(`creation of ${selectedContract.name} errored: ` + error)
statusCb(`creation of ${selectedContract.name} pending...`)
this.createContract(selectedContract, data, continueCb, promptCb, confirmationCb, finalCb)
})
}
createContract (selectedContract, data, continueCb, promptCb, confirmationCb, finalCb) {
if (data) {
data.contractName = selectedContract.name
data.linkReferences = selectedContract.bytecodeLinkReferences
data.contractABI = selectedContract.abi
}
this.runTx({ data: data, useCall: false }, confirmationCb, continueCb, promptCb,
(error, txResult, address) => {
if (error) {
return finalCb(`creation of ${selectedContract.name} errored: ${error}`)
}
if (txResult.result.status && txResult.result.status === '0x0') {
return finalCb(`creation of ${selectedContract.name} errored: transaction execution failed`)
}
finalCb(null, selectedContract, address)
}
)
}
determineGasPrice (cb) {
this.getCurrentProvider().getGasPrice((error, gasPrice) => {
const warnMessage = ' Please fix this issue before sending any transaction. '
if (error) {
return cb('Unable to retrieve the current network gas price.' + warnMessage + error)
}
try {
const gasPriceValue = this.fromWei(gasPrice, false, 'gwei')
cb(null, gasPriceValue)
} catch (e) {
cb(warnMessage + e.message, null, false)
}
})
}
getInputs (funABI) {
if (!funABI.inputs) {
return ''
}
return txHelper.inputParametersDeclarationToString(funABI.inputs)
}
fromWei (value, doTypeConversion, unit) {
if (doTypeConversion) {
return Web3.utils.fromWei(typeConversion.toInt(value), unit || 'ether')
}
return Web3.utils.fromWei(value.toString(10), unit || 'ether')
}
toWei (value, unit) {
return Web3.utils.toWei(value, unit || 'gwei')
}
calculateFee (gas, gasPrice, unit) {
return Web3.utils.toBN(gas).mul(Web3.utils.toBN(Web3.utils.toWei(gasPrice.toString(10), unit || 'gwei')))
}
determineGasFees (tx) {
const determineGasFeesCb = (gasPrice, cb) => {
let txFeeText, priceStatus
// TODO: this try catch feels like an anti pattern, can/should be
// removed, but for now keeping the original logic
try {
const fee = this.calculateFee(tx.gas, gasPrice)
txFeeText = ' ' + this.fromWei(fee, false, 'ether') + ' Ether'
priceStatus = true
} catch (e) {
txFeeText = ' Please fix this issue before sending any transaction. ' + e.message
priceStatus = false
}
cb(txFeeText, priceStatus)
}
return determineGasFeesCb
}
changeExecutionContext (context, confirmCb, infoCb, cb) {
return this.executionContext.executionContextChange(context, null, confirmCb, infoCb, cb)
}
setProviderFromEndpoint (target, context, cb) {
return this.executionContext.setProviderFromEndpoint(target, context, cb)
}
updateNetwork (cb) {
this.executionContext.detectNetwork((err, { id, name } = {}) => {
if (err) {
return cb(err)
}
cb(null, {id, name})
})
}
detectNetwork (cb) {
return this.executionContext.detectNetwork(cb)
}
getProvider () {
return this.executionContext.getProvider()
}
isWeb3Provider () {
const isVM = this.getProvider() === 'vm'
const isInjected = this.getProvider() === 'injected'
return (!isVM && !isInjected)
}
isInjectedWeb3 () {
return this.getProvider() === 'injected'
}
signMessage (message, account, passphrase, cb) {
this.getCurrentProvider().signMessage(message, account, passphrase, cb)
}
web3 () {
return this.executionContext.web3()
}
getTxListener (opts) {
opts.event = {
// udapp: this.udapp.event
udapp: this.event
}
const txlistener = new Txlistener(opts, this.executionContext)
return txlistener
}
runOrCallContractMethod (contractName, contractAbi, funABI, value, address, callType, lookupOnly, logMsg, logCallback, outputCb, confirmationCb, continueCb, promptCb) {
// contractsDetails is used to resolve libraries
txFormat.buildData(contractName, contractAbi, {}, false, funABI, callType, (error, data) => {
if (error) {
return logCallback(`${logMsg} errored: ${error} `)
}
if (!lookupOnly) {
logCallback(`${logMsg} pending ... `)
} else {
logCallback(`${logMsg}`)
}
if (funABI.type === 'fallback') data.dataHex = value
const useCall = funABI.stateMutability === 'view' || funABI.stateMutability === 'pure'
this.runTx({to: address, data, useCall}, confirmationCb, continueCb, promptCb, (error, txResult, _address, returnValue) => {
if (error) {
return logCallback(`${logMsg} errored: ${error} `)
}
if (lookupOnly) {
outputCb(returnValue)
}
})
},
(msg) => {
logCallback(msg)
},
(data, runTxCallback) => {
// called for libraries deployment
this.runTx(data, confirmationCb, runTxCallback, promptCb, () => {})
})
}
context () {
return (this.executionContext.isVM() ? 'memory' : 'blockchain')
}
// NOTE: the config is only needed because exectuionContext.init does
// if config.get('settings/always-use-vm'), we can simplify this later
resetAndInit (config, transactionContextAPI) {
this.transactionContextAPI = transactionContextAPI
this.executionContext.init(config)
this.executionContext.stopListenOnLastBlock()
this.executionContext.listenOnLastBlock()
this.resetEnvironment()
}
addNetwork (customNetwork) {
this.executionContext.addProvider(customNetwork)
}
removeNetwork (name) {
this.executionContext.removeProvider(name)
}
// TODO : event should be triggered by Udapp instead of TxListener
/** Listen on New Transaction. (Cannot be done inside constructor because txlistener doesn't exist yet) */
startListening (txlistener) {
txlistener.event.register('newTransaction', (tx) => {
this.events.emit('newTransaction', tx)
})
}
resetEnvironment () {
this.getCurrentProvider().resetEnvironment()
// TODO: most params here can be refactored away in txRunner
// this.txRunner = new TxRunner(this.providers.vm.accounts, {
this.txRunner = new TxRunner(this.providers.vm.RemixSimulatorProvider.Accounts.accounts, {
// TODO: only used to check value of doNotShowTransactionConfirmationAgain property
config: this.config,
// TODO: to refactor, TxRunner already has access to executionContext
detectNetwork: (cb) => {
this.executionContext.detectNetwork(cb)
},
personalMode: () => {
return this.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false
}
}, this.executionContext)
this.txRunner.event.register('transactionBroadcasted', (txhash) => {
this.executionContext.detectNetwork((error, network) => {
if (error || !network) return
this.event.trigger('transactionBroadcasted', [txhash, network.name])
})
})
}
/**
* Create a VM Account
* @param {{privateKey: string, balance: string}} newAccount The new account to create
*/
createVMAccount (newAccount) {
if (this.getProvider() !== 'vm') {
throw new Error('plugin API does not allow creating a new account through web3 connection. Only vm mode is allowed')
}
return this.providers.vm.createVMAccount(newAccount)
}
newAccount (_password, passwordPromptCb, cb) {
return this.getCurrentProvider().newAccount(passwordPromptCb, cb)
}
/** Get the balance of an address, and convert wei to ether */
getBalanceInEther (address, cb) {
this.getCurrentProvider().getBalanceInEther(address, cb)
}
pendingTransactionsCount () {
return Object.keys(this.txRunner.pendingTxs).length
}
/**
* This function send a tx only to javascript VM or testnet, will return an error for the mainnet
* SHOULD BE TAKEN CAREFULLY!
*
* @param {Object} tx - transaction.
*/
sendTransaction (tx) {
return new Promise((resolve, reject) => {
this.executionContext.detectNetwork((error, network) => {
if (error) return reject(error)
if (network.name === 'Main' && network.id === '1') {
return reject(new Error('It is not allowed to make this action against mainnet'))
}
this.txRunner.rawRun(
tx,
(network, tx, gasEstimation, continueTxExecution, cancelCb) => { continueTxExecution() },
(error, continueTxExecution, cancelCb) => { if (error) { reject(error) } else { continueTxExecution() } },
(okCb, cancelCb) => { okCb() },
(error, result) => {
if (error) return reject(error)
try {
resolve(resultToRemixTx(result))
} catch (e) {
reject(e)
}
}
)
})
})
}
runTx (args, confirmationCb, continueCb, promptCb, cb) {
const self = this
async.waterfall([
function getGasLimit (next) {
if (self.transactionContextAPI.getGasLimit) {
return self.transactionContextAPI.getGasLimit(next)
}
next(null, 3000000)
},
function queryValue (gasLimit, next) {
if (args.value) {
return next(null, args.value, gasLimit)
}
if (args.useCall || !self.transactionContextAPI.getValue) {
return next(null, 0, gasLimit)
}
self.transactionContextAPI.getValue(function (err, value) {
next(err, value, gasLimit)
})
},
function getAccount (value, gasLimit, next) {
if (args.from) {
return next(null, args.from, value, gasLimit)
}
if (self.transactionContextAPI.getAddress) {
return self.transactionContextAPI.getAddress(function (err, address) {
next(err, address, value, gasLimit)
})
}
self.getAccounts(function (err, accounts) {
let address = accounts[0]
if (err) return next(err)
if (!address) return next('No accounts available')
// if (self.executionContext.isVM() && !self.providers.vm.accounts[address]) {
if (self.executionContext.isVM() && !self.providers.vm.RemixSimulatorProvider.Accounts.accounts[address]) {
return next('Invalid account selected')
}
next(null, address, value, gasLimit)
})
},
function runTransaction (fromAddress, value, gasLimit, next) {
const tx = { to: args.to, data: args.data.dataHex, useCall: args.useCall, from: fromAddress, value: value, gasLimit: gasLimit, timestamp: args.data.timestamp }
const payLoad = { funAbi: args.data.funAbi, funArgs: args.data.funArgs, contractBytecode: args.data.contractBytecode, contractName: args.data.contractName, contractABI: args.data.contractABI, linkReferences: args.data.linkReferences }
let timestamp = Date.now()
if (tx.timestamp) {
timestamp = tx.timestamp
}
self.event.trigger('initiatingTransaction', [timestamp, tx, payLoad])
self.txRunner.rawRun(tx, confirmationCb, continueCb, promptCb,
function (error, result) {
if (error) return next(error)
const rawAddress = self.executionContext.isVM() ? result.result.createdAddress : result.result.contractAddress
let eventName = (tx.useCall ? 'callExecuted' : 'transactionExecuted')
self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad, rawAddress])
if (error && (typeof (error) !== 'string')) {
if (error.message) error = error.message
else {
try { error = 'error: ' + JSON.stringify(error) } catch (e) {}
}
}
next(error, result)
}
)
}
],
(error, txResult) => {
if (error) {
return cb(error)
}
const isVM = this.executionContext.isVM()
if (isVM) {
const vmError = txExecution.checkVMError(txResult)
if (vmError.error) {
return cb(vmError.message)
}
}
let address = null
let returnValue = null
if (txResult && txResult.result) {
address = isVM ? txResult.result.createdAddress : txResult.result.contractAddress
// if it's not the VM, we don't have return value. We only have the transaction, and it does not contain the return value.
returnValue = (txResult.result.execResult && isVM) ? txResult.result.execResult.returnValue : txResult.result
}
cb(error, txResult, address, returnValue)
})
}
}
module.exports = Blockchain