Merge pull request #2582 from ethereum/refactor_iuri

refactor, abstract and remove dependencies on executionContext and udapp - 2
pull/1/head
yann300 5 years ago committed by GitHub
commit 50b2c87578
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      src/app.js
  2. 6
      src/app/tabs/debugger-tab.js
  3. 10
      src/app/tabs/debugger/debuggerUI.js
  4. 18
      src/app/tabs/network-module.js
  5. 270
      src/app/tabs/runTab/model/blockchain.js
  6. 1
      src/app/tabs/runTab/settings.js
  7. 70
      src/app/udapp/run-tab.js
  8. 64
      src/app/ui/universal-dapp-ui.js
  9. 665
      src/blockchain/blockchain.js
  10. 35
      src/blockchain/pluginUDapp.js
  11. 46
      src/blockchain/txResultHelper.js

@ -22,9 +22,8 @@ var toolTip = require('./app/ui/tooltip')
var CompilerMetadata = require('./app/files/compiler-metadata')
var CompilerImport = require('./app/compiler/compiler-imports')
var executionContext = remixLib.execution.executionContext
const Blockchain = require('./app/tabs/runTab/model/blockchain.js')
const Blockchain = require('./blockchain/blockchain.js')
const PluginUDapp = require('./blockchain/pluginUDapp.js')
const PluginManagerComponent = require('./app/components/plugin-manager-component')
const CompilersArtefacts = require('./app/compiler/compiler-artefacts')
@ -50,7 +49,6 @@ import { HiddenPanel } from './app/components/hidden-panel'
import { VerticalIcons } from './app/components/vertical-icons'
import { LandingPage } from './app/ui/landing-page/landing-page'
import { MainPanel } from './app/components/main-panel'
import { UniversalDApp } from 'remix-lib'
import migrateFileSystem from './migrateFileSystem'
@ -225,9 +223,8 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
const fileManager = new FileManager(editor)
registry.put({api: fileManager, name: 'filemanager'})
// ----------------- universal dapp: run transaction, listen on transactions, decode events
const udapp = new UniversalDApp(registry.get('config').api, executionContext)
const blockchain = new Blockchain(executionContext, udapp)
const blockchain = new Blockchain(registry.get('config').api)
const pluginUdapp = new PluginUDapp(blockchain)
// ----------------- compilation metadata generation servive ----------------------------
const compilerMetadataGenerator = new CompilerMetadata(blockchain, fileManager, registry.get('config').api)
@ -237,7 +234,7 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
const {eventsDecoder, txlistener} = makeUdapp(blockchain, compilersArtefacts, (domEl) => mainview.getTerminal().logHtml(domEl))
// ----------------- network service (resolve network id / name) ----------------------------
const networkModule = new NetworkModule(executionContext)
const networkModule = new NetworkModule(blockchain)
// ----------------- convert offset to line/column service ----------------------------
var offsetToLineColumnConverter = new OffsetToLineColumnConverter()
registry.put({api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter'})
@ -300,8 +297,7 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
)
const run = new RunTab(
blockchain,
udapp,
executionContext,
pluginUdapp,
registry.get('config').api,
registry.get('filemanager').api,
registry.get('editor').api,
@ -311,7 +307,7 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
mainview
)
const analysis = new AnalysisTab(registry)
const debug = new DebuggerTab(executionContext)
const debug = new DebuggerTab(blockchain)
const test = new TestTab(
registry.get('filemanager').api,
filePanel,

@ -21,10 +21,10 @@ const profile = {
class DebuggerTab extends ViewPlugin {
constructor (executionContext) {
constructor (blockchain) {
super(profile)
this.el = null
this.executionContext = executionContext
this.blockchain = blockchain
}
render () {
@ -34,7 +34,7 @@ class DebuggerTab extends ViewPlugin {
<div class="${css.debuggerTabView}" id="debugView">
<div id="debugger" class="${css.debugger}"></div>
</div>`
this.debuggerUI = new DebuggerUI(this.el.querySelector('#debugger'), this.executionContext)
this.debuggerUI = new DebuggerUI(this.el.querySelector('#debugger'), this.blockchain)
return this.el
}

@ -30,9 +30,9 @@ var css = csjs`
class DebuggerUI {
constructor (container, executionContext) {
constructor (container, blockchain) {
this.registry = globalRegistry
this.executionContext = executionContext
this.blockchain = blockchain
this.event = new EventManager()
this.isActive = false
@ -105,13 +105,13 @@ class DebuggerUI {
getDebugWeb3 () {
return new Promise((resolve, reject) => {
this.executionContext.detectNetwork((error, network) => {
this.blockchain.detectNetwork((error, network) => {
let web3
if (error || !network) {
web3 = init.web3DebugNode(this.executionContext.web3())
web3 = init.web3DebugNode(this.blockchain.web3())
} else {
const webDebugNode = init.web3DebugNode(network.name)
web3 = !webDebugNode ? this.executionContext.web3() : webDebugNode
web3 = !webDebugNode ? this.blockchain.web3() : webDebugNode
}
init.extendWeb3(web3)
resolve(web3)

@ -14,11 +14,11 @@ export const profile = {
// - methods: ['getNetworkProvider', 'getEndpoint', 'detectNetwork', 'addNetwork', 'removeNetwork']
export class NetworkModule extends Plugin {
constructor (executionContext) {
constructor (blockchain) {
super(profile)
this.executionContext = executionContext
this.blockchain = blockchain
// TODO: See with remix-lib to make sementic coherent
this.executionContext.event.register('contextChanged', (provider) => {
this.blockchain.event.register('contextChanged', (provider) => {
this.emit('providerChanged', provider)
})
/*
@ -37,13 +37,13 @@ export class NetworkModule extends Plugin {
/** Return the current network provider (web3, vm, injected) */
getNetworkProvider () {
return this.executionContext.getProvider()
return this.blockchain.getProvider()
}
/** Return the current network */
detectNetwork () {
return new Promise((resolve, reject) => {
this.executionContext.detectNetwork((error, network) => {
this.blockchain.detectNetwork((error, network) => {
error ? reject(error) : resolve(network)
})
})
@ -51,20 +51,20 @@ export class NetworkModule extends Plugin {
/** Return the url only if network provider is 'web3' */
getEndpoint () {
const provider = this.executionContext.getProvider()
const provider = this.blockchain.getProvider()
if (provider !== 'web3') {
throw new Error('no endpoint: current provider is either injected or vm')
}
return this.executionContext.web3().currentProvider.host
return this.blockchain.web3().currentProvider.host
}
/** Add a custom network to the list of available networks */
addNetwork (customNetwork) {
this.executionContext.addProvider(customNetwork)
this.blockchain.addProvider(customNetwork)
}
/** Remove a network to the list of availble networks */
removeNetwork (name) {
this.executionContext.removeProvider(name)
this.blockchain.removeProvider(name)
}
}

@ -1,270 +0,0 @@
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 EventManager = remixLib.EventManager
const ethJSUtil = require('ethereumjs-util')
const Personal = require('web3-eth-personal')
const Web3 = require('web3')
class Blockchain {
constructor (executionContext, udapp) {
this.event = new EventManager()
this.executionContext = executionContext
this.udapp = udapp
this.networkcallid = 0
this.setupEvents()
}
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])
})
this.udapp.event.register('initiatingTransaction', (timestamp, tx, payLoad) => {
this.event.trigger('initiatingTransaction', [timestamp, tx, payLoad])
})
this.udapp.event.register('transactionExecuted', (error, from, to, data, call, txResult, timestamp) => {
this.event.trigger('transactionExecuted', [error, from, to, data, call, txResult, timestamp])
})
this.udapp.event.register('transactionBroadcasted', (txhash, networkName) => {
this.event.trigger('transactionBroadcasted', [txhash, networkName])
})
}
async deployContract (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) {
const { continueCb, promptCb, statusCb, finalCb } = callbacks
var constructor = selectedContract.getConstructorInterface()
if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) {
return 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.runTransaction(data, continueCb, promptCb, confirmationCb, runTxCallback)
})
}
if (Object.keys(selectedContract.bytecodeLinkReferences).length) statusCb(`linking ${JSON.stringify(selectedContract.bytecodeLinkReferences, null, '\t')} using ${JSON.stringify(contractMetadata.linkReferences, null, '\t')}`)
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)
})
}
runTransaction (data, continueCb, promptCb, confirmationCb, finalCb) {
this.udapp.runTx(data, confirmationCb, continueCb, promptCb, finalCb)
}
createContract (selectedContract, data, continueCb, promptCb, confirmationCb, finalCb) {
if (data) {
data.contractName = selectedContract.name
data.linkReferences = selectedContract.bytecodeLinkReferences
data.contractABI = selectedContract.abi
}
this.udapp.createContract(data, confirmationCb, continueCb, promptCb,
(error, txResult) => {
if (error) {
return finalCb(`creation of ${selectedContract.name} errored: ${error}`)
}
var isVM = this.executionContext.isVM()
if (isVM) {
var vmError = txExecution.checkVMError(txResult)
if (vmError.error) {
return finalCb(vmError.message)
}
}
if (txResult.result.status === false || txResult.result.status === '0x0') {
return finalCb(`creation of ${selectedContract.name} errored: transaction execution failed`)
}
var address = isVM ? txResult.result.createdAddress : txResult.result.contractAddress
finalCb(null, selectedContract, address)
}
)
}
determineGasPrice (cb) {
this.getGasPrice((error, gasPrice) => {
var warnMessage = ' Please fix this issue before sending any transaction. '
if (error) {
return cb('Unable to retrieve the current network gas price.' + warnMessage + error)
}
try {
var gasPriceValue = this.fromWei(gasPrice, false, 'gwei')
cb(null, gasPriceValue)
} catch (e) {
cb(warnMessage + e.message, null, false)
}
})
}
getGasPrice (cb) {
return this.executionContext.web3().eth.getGasPrice(cb)
}
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 {
var 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
}
getAddressFromTransactionResult (txResult) {
return this.executionContext.isVM() ? txResult.result.createdAddress : txResult.result.contractAddress
}
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)
}
getProvider () {
return this.executionContext.getProvider()
}
getAccountBalanceForAddress (address, cb) {
return this.udapp.getBalanceInEther(address, cb)
}
updateNetwork (cb) {
this.networkcallid++
((callid) => {
this.executionContext.detectNetwork((err, { id, name } = {}) => {
if (this.networkcallid > callid) return
this.networkcallid++
if (err) {
return cb(err)
}
cb(null, {id, name})
})
})(this.networkcallid)
}
detectNetwork (cb) {
return this.executionContext.detectNetwork(cb)
}
newAccount (passphraseCb, cb) {
return this.udapp.newAccount('', passphraseCb, cb)
}
getAccounts (cb) {
return this.udapp.getAccounts(cb)
}
isWeb3Provider () {
var isVM = this.executionContext.isVM()
var isInjected = this.executionContext.getProvider() === 'injected'
return (!isVM && !isInjected)
}
isInjectedWeb3 () {
return this.executionContext.getProvider() === 'injected'
}
signMessage (message, account, passphrase, cb) {
var isVM = this.executionContext.isVM()
var isInjected = this.executionContext.getProvider() === 'injected'
if (isVM) {
const personalMsg = ethJSUtil.hashPersonalMessage(Buffer.from(message))
var privKey = this.udapp.accounts[account].privateKey
try {
var rsv = ethJSUtil.ecsign(personalMsg, privKey)
var signedData = ethJSUtil.toRpcSig(rsv.v, rsv.r, rsv.s)
cb(null, '0x' + personalMsg.toString('hex'), signedData)
} catch (e) {
cb(e.message)
}
return
}
if (isInjected) {
const hashedMsg = Web3.utils.sha3(message)
try {
this.executionContext.web3().eth.sign(account, hashedMsg, (error, signedData) => {
cb(error.message, hashedMsg, signedData)
})
} catch (e) {
cb(e.message)
}
return
}
const hashedMsg = Web3.utils.sha3(message)
try {
var personal = new Personal(this.executionContext.web3().currentProvider)
personal.sign(hashedMsg, account, passphrase, (error, signedData) => {
cb(error.message, hashedMsg, signedData)
})
} catch (e) {
cb(e.message)
}
}
web3 () {
return this.executionContext.web3()
}
getTxListener (opts) {
opts.event = {
udapp: this.udapp.event
}
const txlistener = new Txlistener(opts, this.executionContext)
return txlistener
}
startListening (txlistener) {
this.udapp.startListening(txlistener)
}
}
module.exports = Blockchain

@ -251,6 +251,7 @@ class SettingsUI {
newAccount () {
this.blockchain.newAccount(
'',
(cb) => {
modalDialogCustom.promptPassphraseCreation((error, passphrase) => {
if (error) {

@ -33,12 +33,10 @@ const profile = {
export class RunTab extends LibraryPlugin {
constructor (blockchain, udapp, executionContext, config, fileManager, editor, filePanel, compilersArtefacts, networkModule, mainView) {
super(udapp, profile)
constructor (blockchain, pluginUDapp, config, fileManager, editor, filePanel, compilersArtefacts, networkModule, mainView) {
super(pluginUDapp, profile)
this.event = new EventManager()
this.config = config
this.udapp = udapp
this.executionContext = executionContext
this.blockchain = blockchain
this.fileManager = fileManager
this.editor = editor
@ -48,37 +46,6 @@ export class RunTab extends LibraryPlugin {
this.networkModule = networkModule
}
onActivationInternal () {
this.udappUI = new UniversalDAppUI(this.udapp, this.logCallback, this.executionContext)
this.udapp.resetAPI({
getAddress: (cb) => {
cb(null, $('#txorigin').val())
},
getValue: (cb) => {
try {
const number = document.querySelector('#value').value
const select = document.getElementById('unit')
const index = select.selectedIndex
const selectedUnit = select.querySelectorAll('option')[index].dataset.unit
let unit = 'ether' // default
if (['ether', 'finney', 'gwei', 'wei'].indexOf(selectedUnit) >= 0) {
unit = selectedUnit
}
cb(null, Web3.utils.toWei(number, unit))
} catch (e) {
cb(e)
}
},
getGasLimit: (cb) => {
try {
cb(null, '0x' + new ethJSUtil.BN($('#gasLimit').val(), 10).toString(16))
} catch (e) {
cb(e.message)
}
}
})
}
renderContainer () {
this.container = yo`<div class="${css.runTabView} py-0 px-2" id="runTabView" ></div>`
@ -200,11 +167,34 @@ export class RunTab extends LibraryPlugin {
}
render () {
this.onActivationInternal()
this.executionContext.init(this.config)
this.executionContext.stopListenOnLastBlock()
this.executionContext.listenOnLastBlock()
this.udapp.resetEnvironment()
this.udappUI = new UniversalDAppUI(this.blockchain, this.logCallback)
this.blockchain.resetAndInit(this.config, {
getAddress: (cb) => {
cb(null, $('#txorigin').val())
},
getValue: (cb) => {
try {
const number = document.querySelector('#value').value
const select = document.getElementById('unit')
const index = select.selectedIndex
const selectedUnit = select.querySelectorAll('option')[index].dataset.unit
let unit = 'ether' // default
if (['ether', 'finney', 'gwei', 'wei'].indexOf(selectedUnit) >= 0) {
unit = selectedUnit
}
cb(null, Web3.utils.toWei(number, unit))
} catch (e) {
cb(e)
}
},
getGasLimit: (cb) => {
try {
cb(null, '0x' + new ethJSUtil.BN($('#gasLimit').val(), 10).toString(16))
} catch (e) {
cb(e.message)
}
}
})
this.renderInstanceContainer()
this.renderSettings()
this.renderDropdown(this.udappUI, this.fileManager, this.compilersArtefacts, this.config, this.editor, this.logCallback)

@ -10,16 +10,15 @@ var copyToClipboard = require('./copy-to-clipboard')
var css = require('../../universal-dapp-styles')
var MultiParamManager = require('./multiParamManager')
var remixLib = require('remix-lib')
var txExecution = remixLib.execution.txExecution
var txFormat = remixLib.execution.txFormat
const txHelper = remixLib.execution.txHelper
var TreeView = require('./TreeView')
var txCallBacks = require('./sendTxCallbacks')
function UniversalDAppUI (udapp, logCallback, executionContext) {
this.udapp = udapp
function UniversalDAppUI (blockchain, logCallback) {
this.blockchain = blockchain
this.logCallback = logCallback
this.compilerData = {contractsDetails: {}}
this.executionContext = executionContext
}
function decodeResponseToTreeView (response, fnabi) {
@ -43,7 +42,7 @@ UniversalDAppUI.prototype.renderInstance = function (contract, address, contract
if (noInstances) {
noInstances.parentNode.removeChild(noInstances)
}
var abi = this.udapp.getABI(contract)
const abi = txHelper.sortAbiFunction(contract.abi)
return this.renderInstanceFromABI(abi, address, contractName)
}
@ -52,11 +51,11 @@ UniversalDAppUI.prototype.renderInstance = function (contract, address, contract
// basically this has to be called for the "atAddress" (line 393) and when a contract creation succeed
// this returns a DOM element
UniversalDAppUI.prototype.renderInstanceFromABI = function (contractABI, address, contractName) {
var self = this
let self = this
address = (address.slice(0, 2) === '0x' ? '' : '0x') + address.toString('hex')
address = ethJSUtil.toChecksumAddress(address)
var instance = yo`<div class="instance ${css.instance} ${css.hidesub}" id="instance${address}"></div>`
var context = self.udapp.context()
const context = this.blockchain.context()
var shortAddress = helper.shortenAddress(address)
var title = yo`
@ -160,8 +159,8 @@ UniversalDAppUI.prototype.renderInstanceFromABI = function (contractABI, address
}
setLLIError('')
const fallback = self.udapp.getFallbackInterface(contractABI)
const receive = self.udapp.getReceiveInterface(contractABI)
const fallback = self.blockchain.getFallbackInterface(contractABI)
const receive = self.blockchain.getReceiveInterface(contractABI)
const args = {
funABI: fallback || receive,
address: address,
@ -221,7 +220,7 @@ UniversalDAppUI.prototype.getCallButton = function (args) {
lookupOnly,
args.funABI,
(valArray, inputsValues) => self.runTransaction(lookupOnly, args, valArray, inputsValues, outputOverride),
self.udapp.getInputs(args.funABI)
self.blockchain.getInputs(args.funABI)
)
const contractActionsContainer = yo`<div class="${css.contractActionsContainer}" >${multiParamManager.render()}</div>`
@ -231,55 +230,20 @@ UniversalDAppUI.prototype.getCallButton = function (args) {
}
UniversalDAppUI.prototype.runTransaction = function (lookupOnly, args, valArr, inputsValues, outputOverride) {
let self = this
const functionName = args.funABI.type === 'function' ? args.funABI.name : `(${args.funABI.type})`
const logMsg = `${lookupOnly ? 'call' : 'transact'} to ${args.contractName}.${functionName}`
var value = inputsValues
const callbacksInContext = txCallBacks.getCallBacksWithContext(this, this.executionContext)
const outputCb = (decoded) => {
const outputCb = (returnValue) => {
if (outputOverride) {
const decoded = decodeResponseToTreeView(returnValue, args.funABI)
outputOverride.innerHTML = ''
outputOverride.appendChild(decoded)
}
}
// contractsDetails is used to resolve libraries
const callbacksInContext = txCallBacks.getCallBacksWithContext(self, self.executionContext)
txFormat.buildData(args.contractName, args.contractABI, {}, false, args.funABI, args.funABI.type !== 'fallback' ? value : '', (error, data) => {
if (!error) {
if (!lookupOnly) {
self.logCallback(`${logMsg} pending ... `)
} else {
self.logCallback(`${logMsg}`)
}
if (args.funABI.type === 'fallback') data.dataHex = value
self.udapp.callFunction(args.address, data, args.funABI, callbacksInContext.confirmationCb.bind(callbacksInContext), callbacksInContext.continueCb.bind(callbacksInContext), callbacksInContext.promptCb.bind(callbacksInContext), (error, txResult) => {
if (!error) {
var isVM = self.executionContext.isVM()
if (isVM) {
var vmError = txExecution.checkVMError(txResult)
if (vmError.error) {
self.logCallback(`${logMsg} errored: ${vmError.message} `)
return
}
}
if (lookupOnly) {
const decoded = decodeResponseToTreeView(self.executionContext.isVM() ? txResult.result.execResult.returnValue : ethJSUtil.toBuffer(txResult.result), args.funABI)
outputCb(decoded)
}
} else {
self.logCallback(`${logMsg} errored: ${error} `)
}
})
} else {
self.logCallback(`${logMsg} errored: ${error} `)
}
}, (msg) => {
self.logCallback(msg)
}, (data, runTxCallback) => {
// called for libraries deployment
self.udapp.runTx(data, callbacksInContext.confirmationCb.bind(callbacksInContext), runTxCallback)
})
const params = args.funABI.type !== 'fallback' ? inputsValues : ''
this.blockchain.runOrCallContractMethod(args.contractName, args.contractAbi, args.funABI, inputsValues, args.address, params, lookupOnly, logMsg, this.logCallback, outputCb, callbacksInContext)
}
module.exports = UniversalDAppUI

@ -0,0 +1,665 @@
const remixLib = require('remix-lib')
const txFormat = remixLib.execution.txFormat
const txExecution = remixLib.execution.txExecution
const typeConversion = remixLib.execution.typeConversion
const TxRunner = remixLib.execution.txRunner
const Txlistener = remixLib.execution.txListener
const txHelper = remixLib.execution.txHelper
const EventManager = remixLib.EventManager
const executionContext = remixLib.execution.executionContext
const ethJSUtil = require('ethereumjs-util')
const Personal = require('web3-eth-personal')
const Web3 = require('web3')
const async = require('async')
const { BN, privateToAddress, isValidPrivate, stripHexPrefix, toChecksumAddress } = require('ethereumjs-util')
const crypto = require('crypto')
const { EventEmitter } = require('events')
const { resultToRemixTx } = require('./txResultHelper')
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.executionContext.getProvider() === 'web3' ? this.config.get('settings/personal-mode') : false
}
}, this.executionContext)
this.accounts = {}
this.executionContext.event.register('contextChanged', this.resetEnvironment.bind(this))
this.networkcallid = 0
this.setupEvents()
}
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])
})
}
async deployContract (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) {
const { continueCb, promptCb, statusCb, finalCb } = callbacks
const constructor = selectedContract.getConstructorInterface()
if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) {
return 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.runTransaction(data, continueCb, promptCb, confirmationCb, runTxCallback)
})
}
if (Object.keys(selectedContract.bytecodeLinkReferences).length) statusCb(`linking ${JSON.stringify(selectedContract.bytecodeLinkReferences, null, '\t')} using ${JSON.stringify(contractMetadata.linkReferences, null, '\t')}`)
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)
})
}
runTransaction (data, continueCb, promptCb, confirmationCb, finalCb) {
this.runTx(data, confirmationCb, continueCb, promptCb, finalCb)
}
createContract (selectedContract, data, continueCb, promptCb, confirmationCb, finalCb) {
if (data) {
data.contractName = selectedContract.name
data.linkReferences = selectedContract.bytecodeLinkReferences
data.contractABI = selectedContract.abi
}
this._createContract(data, confirmationCb, continueCb, promptCb,
(error, txResult) => {
if (error) {
return finalCb(`creation of ${selectedContract.name} errored: ${error}`)
}
const isVM = this.executionContext.isVM()
if (isVM) {
const vmError = txExecution.checkVMError(txResult)
if (vmError.error) {
return finalCb(vmError.message)
}
}
if (txResult.result.status === false || txResult.result.status === '0x0') {
return finalCb(`creation of ${selectedContract.name} errored: transaction execution failed`)
}
const address = isVM ? txResult.result.createdAddress : txResult.result.contractAddress
finalCb(null, selectedContract, address)
}
)
}
determineGasPrice (cb) {
this.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)
}
})
}
getGasPrice (cb) {
return this.executionContext.web3().eth.getGasPrice(cb)
}
getFallbackInterface (contractABI) {
return txHelper.getFallbackInterface(contractABI)
}
getReceiveInterface (contractABI) {
return txHelper.getReceiveInterface(contractABI)
}
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
}
getAddressFromTransactionResult (txResult) {
return this.executionContext.isVM() ? txResult.result.createdAddress : txResult.result.contractAddress
}
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)
}
getProvider () {
return this.executionContext.getProvider()
}
getAccountBalanceForAddress (address, cb) {
return this.getBalanceInEther(address, cb)
}
updateNetwork (cb) {
this.networkcallid++
((callid) => {
this.executionContext.detectNetwork((err, { id, name } = {}) => {
if (this.networkcallid > callid) return
this.networkcallid++
if (err) {
return cb(err)
}
cb(null, {id, name})
})
})(this.networkcallid)
}
detectNetwork (cb) {
return this.executionContext.detectNetwork(cb)
}
isWeb3Provider () {
const isVM = this.executionContext.isVM()
const isInjected = this.executionContext.getProvider() === 'injected'
return (!isVM && !isInjected)
}
isInjectedWeb3 () {
return this.executionContext.getProvider() === 'injected'
}
signMessage (message, account, passphrase, cb) {
const isVM = this.executionContext.isVM()
const isInjected = this.executionContext.getProvider() === 'injected'
if (isVM) {
const personalMsg = ethJSUtil.hashPersonalMessage(Buffer.from(message))
const privKey = this.accounts[account].privateKey
try {
const rsv = ethJSUtil.ecsign(personalMsg, privKey)
const signedData = ethJSUtil.toRpcSig(rsv.v, rsv.r, rsv.s)
cb(null, '0x' + personalMsg.toString('hex'), signedData)
} catch (e) {
cb(e.message)
}
return
}
if (isInjected) {
const hashedMsg = Web3.utils.sha3(message)
try {
this.executionContext.web3().eth.sign(account, hashedMsg, (error, signedData) => {
cb(error.message, hashedMsg, signedData)
})
} catch (e) {
cb(e.message)
}
return
}
const hashedMsg = Web3.utils.sha3(message)
try {
const personal = new Personal(this.executionContext.web3().currentProvider)
personal.sign(hashedMsg, account, passphrase, (error, signedData) => {
cb(error.message, hashedMsg, signedData)
})
} catch (e) {
cb(e.message)
}
}
web3 () {
return this.executionContext.web3()
}
getTxListener (opts) {
opts.event = {
udapp: this.event
}
const txlistener = new Txlistener(opts, this.executionContext)
return txlistener
}
runOrCallContractMethod (contractName, contractABI, funABI, value, address, params, lookupOnly, logMsg, logCallback, outputCb, callbacksInContext) {
// contractsDetails is used to resolve libraries
txFormat.buildData(contractName, contractABI, {}, false, funABI, params, (error, data) => {
if (!error) {
if (!lookupOnly) {
logCallback(`${logMsg} pending ... `)
} else {
logCallback(`${logMsg}`)
}
if (funABI.type === 'fallback') data.dataHex = value
this.callFunction(address, data, funABI, callbacksInContext.confirmationCb.bind(callbacksInContext), callbacksInContext.continueCb.bind(callbacksInContext), callbacksInContext.promptCb.bind(callbacksInContext), (error, txResult) => {
if (!error) {
const isVM = this.executionContext.isVM()
if (isVM) {
const vmError = txExecution.checkVMError(txResult)
if (vmError.error) {
logCallback(`${logMsg} errored: ${vmError.message} `)
return
}
}
if (lookupOnly) {
const returnValue = (this.executionContext.isVM() ? txResult.result.execResult.returnValue : ethJSUtil.toBuffer(txResult.result))
outputCb(returnValue)
}
} else {
logCallback(`${logMsg} errored: ${error} `)
}
})
} else {
logCallback(`${logMsg} errored: ${error} `)
}
}, (msg) => {
logCallback(msg)
}, (data, runTxCallback) => {
// called for libraries deployment
this.runTx(data, callbacksInContext.confirmationCb.bind(callbacksInContext), runTxCallback)
})
}
// NOTE: the config is only needed because exectuionContext.init does
// if config.get('settings/always-use-vm'), we can simplify this later
resetAndInit (config, transactionContext) {
this.resetAPI(transactionContext)
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.accounts = {}
if (this.executionContext.isVM()) {
this._addAccount('3cd7232cd6f3fc66a57a6bedc1a8ed6c228fff0a327e169c2bcc5e869ed49511', '0x56BC75E2D63100000')
this._addAccount('2ac6c190b09897cd8987869cc7b918cfea07ee82038d492abce033c75c1b1d0c', '0x56BC75E2D63100000')
this._addAccount('dae9801649ba2d95a21e688b56f77905e5667c44ce868ec83f82e838712a2c7a', '0x56BC75E2D63100000')
this._addAccount('d74aa6d18aa79a05f3473dd030a97d3305737cbc8337d940344345c1f6b72eea', '0x56BC75E2D63100000')
this._addAccount('71975fbf7fe448e004ac7ae54cad0a383c3906055a65468714156a07385e96ce', '0x56BC75E2D63100000')
}
// TODO: most params here can be refactored away in txRunner
this.txRunner = new TxRunner(this.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.executionContext.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])
})
})
}
resetAPI (transactionContextAPI) {
this.transactionContextAPI = transactionContextAPI
}
/**
* Create a VM Account
* @param {{privateKey: string, balance: string}} newAccount The new account to create
*/
createVMAccount (newAccount) {
const { privateKey, balance } = newAccount
if (this.executionContext.getProvider() !== 'vm') {
throw new Error('plugin API does not allow creating a new account through web3 connection. Only vm mode is allowed')
}
this._addAccount(privateKey, balance)
const privKey = Buffer.from(privateKey, 'hex')
return '0x' + privateToAddress(privKey).toString('hex')
}
newAccount (_password, passwordPromptCb, cb) {
if (!this.executionContext.isVM()) {
if (!this.config.get('settings/personal-mode')) {
return cb('Not running in personal mode')
}
passwordPromptCb((passphrase) => {
this.executionContext.web3().personal.newAccount(passphrase, cb)
})
} else {
let privateKey
do {
privateKey = crypto.randomBytes(32)
} while (!isValidPrivate(privateKey))
this._addAccount(privateKey, '0x56BC75E2D63100000')
cb(null, '0x' + privateToAddress(privateKey).toString('hex'))
}
}
/** Add an account to the list of account (only for Javascript VM) */
_addAccount (privateKey, balance) {
if (!this.executionContext.isVM()) {
throw new Error('_addAccount() cannot be called in non-VM mode')
}
if (this.accounts) {
privateKey = Buffer.from(privateKey, 'hex')
const address = privateToAddress(privateKey)
// FIXME: we don't care about the callback, but we should still make this proper
let stateManager = this.executionContext.vm().stateManager
stateManager.getAccount(address, (error, account) => {
if (error) return console.log(error)
account.balance = balance || '0xf00000000000000001'
stateManager.putAccount(address, account, (error) => {
if (error) console.log(error)
})
})
this.accounts[toChecksumAddress('0x' + address.toString('hex'))] = { privateKey, nonce: 0 }
}
}
/** Return the list of accounts */
getAccounts (cb) {
return new Promise((resolve, reject) => {
const provider = this.executionContext.getProvider()
switch (provider) {
case 'vm': {
if (!this.accounts) {
if (cb) cb('No accounts?')
reject('No accounts?')
return
}
if (cb) cb(null, Object.keys(this.accounts))
resolve(Object.keys(this.accounts))
}
break
case 'web3': {
if (this.config.get('settings/personal-mode')) {
return this.executionContext.web3().personal.getListAccounts((error, accounts) => {
if (cb) cb(error, accounts)
if (error) return reject(error)
resolve(accounts)
})
} else {
this.executionContext.web3().eth.getAccounts((error, accounts) => {
if (cb) cb(error, accounts)
if (error) return reject(error)
resolve(accounts)
})
}
}
break
case 'injected': {
this.executionContext.web3().eth.getAccounts((error, accounts) => {
if (cb) cb(error, accounts)
if (error) return reject(error)
resolve(accounts)
})
}
}
})
}
/** Get the balance of an address */
getBalance (address, cb) {
address = stripHexPrefix(address)
if (!this.executionContext.isVM()) {
this.executionContext.web3().eth.getBalance(address, (err, res) => {
if (err) {
cb(err)
} else {
cb(null, res.toString(10))
}
})
} else {
if (!this.accounts) {
return cb('No accounts?')
}
this.executionContext.vm().stateManager.getAccount(Buffer.from(address, 'hex'), (err, res) => {
if (err) {
cb('Account not found')
} else {
cb(null, new BN(res.balance).toString(10))
}
})
}
}
/** Get the balance of an address, and convert wei to ether */
getBalanceInEther (address, callback) {
this.getBalance(address, (error, balance) => {
if (error) {
callback(error)
} else {
// callback(null, this.executionContext.web3().fromWei(balance, 'ether'))
callback(null, Web3.utils.fromWei(balance.toString(10), 'ether'))
}
})
}
pendingTransactionsCount () {
return Object.keys(this.txRunner.pendingTxs).length
}
/**
* deploy the given contract
*
* @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ).
* @param {Function} callback - callback.
*/
_createContract (data, confirmationCb, continueCb, promptCb, callback) {
this.runTx({data: data, useCall: false}, confirmationCb, continueCb, promptCb, (error, txResult) => {
// see universaldapp.js line 660 => 700 to check possible values of txResult (error case)
callback(error, txResult)
})
}
/**
* call the current given contract
*
* @param {String} to - address of the contract to call.
* @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ).
* @param {Object} funAbi - abi definition of the function to call.
* @param {Function} callback - callback.
*/
callFunction (to, data, funAbi, confirmationCb, continueCb, promptCb, callback) {
const useCall = funAbi.stateMutability === 'view' || funAbi.stateMutability === 'pure'
this.runTx({to, data, useCall}, confirmationCb, continueCb, promptCb, (error, txResult) => {
// see universaldapp.js line 660 => 700 to check possible values of txResult (error case)
callback(error, txResult)
})
}
context () {
return (this.executionContext.isVM() ? 'memory' : 'blockchain')
}
/**
* 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.silentRunTx(tx, (error, result) => {
if (error) return reject(error)
try {
resolve(resultToRemixTx(result))
} catch (e) {
reject(e)
}
})
})
})
}
/**
* This function send a tx without alerting the user (if mainnet or if gas estimation too high).
* SHOULD BE TAKEN CAREFULLY!
*
* @param {Object} tx - transaction.
* @param {Function} callback - callback.
*/
silentRunTx (tx, cb) {
this.txRunner.rawRun(
tx,
(network, tx, gasEstimation, continueTxExecution, cancelCb) => { continueTxExecution() },
(error, continueTxExecution, cancelCb) => { if (error) { cb(error) } else { continueTxExecution() } },
(okCb, cancelCb) => { okCb() },
cb
)
}
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.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) {
let eventName = (tx.useCall ? 'callExecuted' : 'transactionExecuted')
self.event.trigger(eventName, [error, tx.from, tx.to, tx.data, tx.useCall, result, timestamp, payLoad])
if (error && (typeof (error) !== 'string')) {
if (error.message) error = error.message
else {
try { error = 'error: ' + JSON.stringify(error) } catch (e) {}
}
}
next(error, result)
}
)
}
], cb)
}
}
module.exports = Blockchain

@ -0,0 +1,35 @@
const { EventEmitter } = require('events')
class PluginUdapp {
constructor (blockchain) {
this.blockchain = blockchain
this.events = new EventEmitter()
this.setupEvents()
}
setupEvents () {
this.blockchain.event.register('newTransaction', (tx, receipt) => {
this.events.trigger('newTransaction', [tx, receipt])
})
}
createVMAccount (newAccount) {
return this.blockchain.createVMAccount(newAccount)
}
sendTransaction (tx) {
return this.blockchain.sendTransaction(tx)
}
getAccounts (cb) {
return this.blockchain.getAccounts(cb)
}
pendingTransactionsCount () {
return this.blockchain.pendingTransactionsCount()
}
}
module.exports = PluginUdapp

@ -0,0 +1,46 @@
'use strict'
const { bufferToHex, isHexString } = require('ethereumjs-util')
function convertToPrefixedHex (input) {
if (input === undefined || input === null || isHexString(input)) {
return input
} else if (Buffer.isBuffer(input)) {
return bufferToHex(input)
}
return '0x' + input.toString(16)
}
/*
txResult.result can be 3 different things:
- VM call or tx: ethereumjs-vm result object
- Node transaction: object returned from eth.getTransactionReceipt()
- Node call: return value from function call (not an object)
Also, VM results use BN and Buffers, Node results use hex strings/ints,
So we need to normalize the values to prefixed hex strings
*/
function resultToRemixTx (txResult) {
const { result, transactionHash } = txResult
const { status, execResult, gasUsed, createdAddress, contractAddress } = result
let returnValue, errorMessage
if (isHexString(result)) {
returnValue = result
} else if (execResult !== undefined) {
returnValue = execResult.returnValue
errorMessage = execResult.exceptionError
}
return {
transactionHash,
status,
gasUsed: convertToPrefixedHex(gasUsed),
error: errorMessage,
return: convertToPrefixedHex(returnValue),
createdAddress: convertToPrefixedHex(createdAddress || contractAddress)
}
}
module.exports = {
resultToRemixTx
}
Loading…
Cancel
Save