Merge pull request #802 from ethereum/recordTxs

Record Transactions
pull/1/head
yann300 7 years ago committed by GitHub
commit 6884d6dc2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .travis.yml
  2. 29
      ci/makeMockCompiler.js
  3. 10
      src/app.js
  4. 5
      src/app/debugger/debugger.js
  5. 4
      src/app/editor/contextView.js
  6. 55
      src/app/execution/txFormat.js
  7. 4
      src/app/execution/txLogger.js
  8. 4
      src/app/execution/txRunner.js
  9. 7
      src/app/files/fileManager.js
  10. 150
      src/app/tabs/run-tab.js
  11. 16
      src/app/ui/modal-dialog-custom.js
  12. 256
      src/recorder.js
  13. 140
      src/universal-dapp.js
  14. 61
      test-browser/helpers/contracts.js
  15. 40
      test-browser/tests/ballot.js
  16. 5
      test-browser/tests/compiling.js
  17. 1
      test-browser/tests/simpleContract.js
  18. 1
      test-browser/tests/staticanalysis.js
  19. 172
      test-browser/tests/units/testRecorder.js

@ -2,7 +2,7 @@ language: node_js
node_js:
- "7"
script:
- npm run lint && npm run test && npm run downloadsolc && npm run make-mock-compiler && npm run pullremix && npm run linkremix && npm run build
- npm run lint && npm run test && npm run downloadsolc && npm run make-mock-compiler && npm run build
- ./ci/browser_tests.sh
deploy:
- provider: script

@ -6,25 +6,16 @@ var soljson = require('../soljson')
var compiler = solc(soljson)
var compilerInput = require('../src/app/compiler/compiler-input')
gatherCompilationResults(function (error, data) {
if (error) {
console.log(error)
process.exit(1)
} else {
replaceSolCompiler(data)
}
})
function gatherCompilationResults (callback) {
var compilationResult = {}
fs.readdir('./test-browser/tests', 'utf8', function (error, filenames) {
if (error) {
console.log(error)
process.exit(1)
} else {
gatherCompilationResults('./test-browser/tests/', compilationResult)
gatherCompilationResults('./test-browser/tests/units/', compilationResult)
replaceSolCompiler(compilationResult)
function gatherCompilationResults (dir, compilationResult, callback) {
var filenames = fs.readdirSync(dir, 'utf8')
filenames.map(function (item, i) {
var testDef = require('../test-browser/tests/' + item)
if (item.endsWith('.js')) {
var testDef = require('.' + dir + item)
if ('@sources' in testDef) {
var sources = testDef['@sources']()
for (var files in sources) {
@ -36,11 +27,9 @@ function gatherCompilationResults (callback) {
})
}
}
})
callback(null, compilationResult)
}
})
return compilationResult
}
function compile (source, optimization, addCompilationResult) {

@ -537,6 +537,9 @@ function run () {
document.querySelector(`.${css.dragbar2}`).style.right = delta + 'px'
onResize()
},
getAccounts: (cb) => {
udapp.getAccounts(cb)
},
getSource: (fileName) => {
return compiler.getSource(fileName)
},
@ -558,12 +561,19 @@ function run () {
udapp: () => {
return udapp
},
switchFile: function (path) {
fileManager.switchFile(path)
},
filesProviders: filesProviders,
fileProviderOf: (path) => {
return fileManager.fileProviderOf(path)
},
fileProvider: (name) => {
return self._api.filesProviders[name]
},
currentPath: function () {
return fileManager.currentPath()
},
getBalance: (address, callback) => {
udapp.getBalance(address, (error, balance) => {
if (error) {

@ -75,7 +75,10 @@ Debugger.prototype.debug = function (txHash) {
var self = this
this.debugger.web3().eth.getTransaction(txHash, function (error, tx) {
if (!error) {
self.debugger.setCompilationResult(self.appAPI.lastCompilationResult().data)
var compilationResult = self.appAPI.lastCompilationResult()
if (compilationResult) {
self.debugger.setCompilationResult(compilationResult.data)
}
self.debugger.debug(tx)
}
})

@ -109,7 +109,7 @@ class ContextView {
if (target) {
this._current = target
} else {
this._current = last
this._current = null
}
}
}
@ -123,7 +123,7 @@ class ContextView {
if (!node) return yo`<div></div>`
var self = this
var references = this._api.contextualListener.referencesOf(node)
var type = node.attributes.type ? node.attributes.type : node.name
var type = (node.attributes && node.attributes.type) ? node.attributes.type : node.name
references = `${references ? references.length : '0'} reference(s)`
var ref = 0

@ -8,9 +8,34 @@ var TreeView = require('remix-debugger').ui.TreeView
var executionContext = require('../../execution-context')
module.exports = {
/**
* build the transaction data
*
* @param {Object} function abi
* @param {Object} values to encode
* @param {String} contractbyteCode
*/
encodeData: function (funABI, values, contractbyteCode) {
var encoded
var encodedHex
try {
encoded = helper.encodeParams(funABI, values)
encodedHex = encoded.toString('hex')
} catch (e) {
return { error: 'cannot encode arguments' }
}
if (contractbyteCode) {
return { data: contractbyteCode + encodedHex }
} else {
return { data: Buffer.concat([helper.encodeFunctionId(funABI), encoded]).toString('hex') }
}
},
/**
* build the transaction data
*
* @param {String} contractName
* @param {Object} contract - abi definition of the current contract.
* @param {Object} contracts - map of all compiled contracts.
* @param {Bool} isConstructor - isConstructor.
@ -20,7 +45,7 @@ module.exports = {
* @param {Function} callback - callback
* @param {Function} callbackStep - callbackStep
*/
buildData: function (contract, contracts, isConstructor, funAbi, params, udapp, callback, callbackStep) {
buildData: function (contractName, contract, contracts, isConstructor, funAbi, params, udapp, callback, callbackStep) {
var funArgs = ''
try {
funArgs = $.parseJSON('[' + params + ']')
@ -45,7 +70,9 @@ module.exports = {
if (data.slice(0, 2) === '0x') {
dataHex = data.slice(2)
}
var contractBytecode
if (isConstructor) {
contractBytecode = contract.evm.bytecode.object
var bytecodeToDeploy = contract.evm.bytecode.object
if (bytecodeToDeploy.indexOf('_') >= 0) {
this.linkBytecode(contract, contracts, udapp, (err, bytecode) => {
@ -53,7 +80,7 @@ module.exports = {
callback('Error deploying required libraries: ' + err)
} else {
bytecodeToDeploy = bytecode + dataHex
return callback(null, bytecodeToDeploy)
return callback(null, {dataHex: bytecodeToDeploy, funAbi, funArgs, contractBytecode, contractName: contractName})
}
}, callbackStep)
return
@ -63,7 +90,7 @@ module.exports = {
} else {
dataHex = Buffer.concat([helper.encodeFunctionId(funAbi), data]).toString('hex')
}
callback(null, dataHex)
callback(null, { dataHex, funAbi, funArgs, contractBytecode, contractName: contractName })
},
atAddress: function () {},
@ -90,7 +117,7 @@ module.exports = {
if (!library) {
return callback('Library ' + libraryName + ' not found.')
}
this.deployLibrary(libraryName, library, contracts, udapp, (err, address) => {
this.deployLibrary(libraryName, libraryShortName, library, contracts, udapp, (err, address) => {
if (err) {
return callback(err)
}
@ -104,7 +131,7 @@ module.exports = {
}, callbackStep)
},
deployLibrary: function (libraryName, library, contracts, udapp, callback, callbackStep) {
deployLibrary: function (libraryName, libraryShortName, library, contracts, udapp, callback, callbackStep) {
var address = library.address
if (address) {
return callback(null, address)
@ -113,11 +140,12 @@ module.exports = {
if (bytecode.indexOf('_') >= 0) {
this.linkBytecode(libraryName, contracts, udapp, (err, bytecode) => {
if (err) callback(err)
else this.deployLibrary(libraryName, library, contracts, udapp, callback, callbackStep)
else this.deployLibrary(libraryName, libraryShortName, library, contracts, udapp, callback, callbackStep)
}, callbackStep)
} else {
callbackStep(`creation of library ${libraryName} pending...`)
udapp.runTx({ data: bytecode, useCall: false }, (err, txResult) => {
var data = {dataHex: bytecode, funAbi: {type: 'constructor'}, funArgs: [], contractBytecode: bytecode, contractName: libraryShortName}
udapp.runTx({ data: data, useCall: false }, (err, txResult) => {
if (err) {
return callback(err)
}
@ -128,18 +156,21 @@ module.exports = {
}
},
linkLibraryStandard: function (libraryName, address, contract) {
var bytecode = contract.evm.bytecode.object
for (var file in contract.evm.bytecode.linkReferences) {
for (var libName in contract.evm.bytecode.linkReferences[file]) {
linkLibraryStandardFromlinkReferences: function (libraryName, address, bytecode, linkReferences) {
for (var file in linkReferences) {
for (var libName in linkReferences[file]) {
if (libraryName === libName) {
bytecode = this.setLibraryAddress(address, bytecode, contract.evm.bytecode.linkReferences[file][libName])
bytecode = this.setLibraryAddress(address, bytecode, linkReferences[file][libName])
}
}
}
return bytecode
},
linkLibraryStandard: function (libraryName, address, contract) {
return this.linkLibraryStandardFromlinkReferences(libraryName, address, contract.evm.bytecode.object, contract.evm.bytecode.linkReferences)
},
setLibraryAddress: function (address, bytecodeToLink, positions) {
if (positions) {
for (var pos of positions) {

@ -269,7 +269,7 @@ function renderUnknownTransaction (self, data) {
input: data.tx.input,
hash: data.tx.hash,
gas: data.tx.gas,
logs: data.logs,
logs: data.tx.logs,
transactionCost: data.tx.transactionCost,
executionCost: data.tx.executionCost,
status: data.tx.status
@ -440,7 +440,7 @@ function createTable (opts) {
}
var stringified = ' - '
if (opts.logs.decoded) {
if (opts.logs && opts.logs.decoded) {
stringified = typeConversion.stringify(opts.logs.decoded)
}
var logs = yo`

@ -156,8 +156,8 @@ function run (self, tx, stamp, callback) {
self.execute(tx, (error, result) => {
delete self.pendingTxs[stamp]
callback(error, result)
if (Object.keys(self.pendingTxs).length) {
var next = self.pendingTxs.pop()
if (self.queusTxs.length) {
var next = self.queusTxs.pop()
run(self, next.tx, next.stamp, next.callback)
}
})

@ -79,6 +79,13 @@ class FileManager {
this.refreshTabs()
}
currentPath () {
var currentFile = this.opt.config.get('currentFile')
var reg = /(.*\/).*/
var path = reg.exec(currentFile)
return path ? path[1] : null
}
fileRemovedEvent (path) {
if (path === this.opt.config.get('currentFile')) {
this.opt.config.set('currentFile', '')

@ -8,6 +8,8 @@ var txHelper = require('../execution/txHelper')
var modalDialogCustom = require('../ui/modal-dialog-custom')
var executionContext = require('../../execution-context')
var copyToClipboard = require('../ui/copy-to-clipboard')
var Recorder = require('../../recorder')
var EventManager = require('remix-lib').EventManager
// -------------- styling ----------------------
var csjs = require('csjs-inject')
@ -89,19 +91,15 @@ var css = csjs`
flex-direction: row;
align-items: baseline;
}
.buttons {
display: flex;
cursor: pointer;
justify-content: center;
flex-direction: column;
text-align: center;
font-size: 12px;
}
.button {
display: flex;
align-items: center;
margin-top: 2%;
}
.transaction {
${styles.rightPanel.runTab.button_transaction}
}
.atAddress {
${styles.rightPanel.runTab.button_atAddress}
}
@ -187,13 +185,18 @@ var instanceContainer = yo`<div class="${css.instanceContainer}"></div>`
var noInstancesText = yo`<div class="${css.noInstancesText}">0 contract Instances</div>`
var pendingTxsText = yo`<div class="${css.pendingTxsText}"></div>`
var pendingTxsContainer = yo`<div class="${css.pendingTxsContainer}">${pendingTxsText}</div>`
function runTab (container, appAPI, appEvents, opts) {
var events = new EventManager()
var pendingTxsContainer = yo`
<div class="${css.pendingTxsContainer}">
${pendingTxsText}
</div>`
var el = yo`
<div class="${css.runTabView}" id="runTabView">
${settings(appAPI, appEvents)}
${contractDropdown(appAPI, appEvents, instanceContainer)}
${contractDropdown(events, appAPI, appEvents, instanceContainer)}
${pendingTxsContainer}
${instanceContainer}
</div>
@ -213,11 +216,8 @@ function runTab (container, appAPI, appEvents, opts) {
// set the final context. Cause it is possible that this is not the one we've originaly selected
selectExEnv.value = executionContext.getProvider()
fillAccountsList(appAPI, el)
clearInstance()
})
instanceContainer.innerHTML = '' // clear the instances list
noInstancesText.style.display = 'block'
instanceContainer.appendChild(noInstancesText)
})
selectExEnv.value = executionContext.getProvider()
fillAccountsList(appAPI, el)
@ -225,6 +225,13 @@ function runTab (container, appAPI, appEvents, opts) {
updateAccountBalances(container, appAPI)
updatePendingTxs(container, appAPI)
}, 500)
var clearInstance = function () {
instanceContainer.innerHTML = '' // clear the instances list
noInstancesText.style.display = 'block'
instanceContainer.appendChild(noInstancesText)
events.trigger('clearInstance', [])
}
}
function fillAccountsList (appAPI, container) {
@ -254,11 +261,90 @@ function updateAccountBalances (container, appAPI) {
})
}
/* ------------------------------------------------
RECORDER
------------------------------------------------ */
function makeRecorder (events, appAPI, appEvents) {
var recorder = new Recorder({
events: {
udapp: appEvents.udapp,
executioncontext: executionContext.event,
runtab: events
},
api: appAPI
})
var css2 = csjs`
.container {
margin: 10px;
display: flex;
justify-content: space-evenly;
width: 100%;
}
.recorder {
${styles.button}
width: 135px;
}
.runTxs {
${styles.button}
margin-left: 10px;
width: 135px;
}
`
var recordButton = yo`<button class="${css.transaction} savetransaction">save transactions</button>`
var runButton = yo`<button class="${css.transaction} runtransaction">run transactions</button>`
var el = yo`
<div class=${css2.container}>
${recordButton}
${runButton}
</div>
`
recordButton.onclick = () => {
var txJSON = JSON.stringify(recorder.getAll(), null, 2)
var path = appAPI.currentPath()
modalDialogCustom.prompt(null, 'save ran transactions to file (e.g. `scenario.json`). The file is going to be saved under ' + path, 'scenario.json', input => {
var fileProvider = appAPI.fileProviderOf(path)
if (fileProvider) {
var newFile = path + input
newFile = helper.createNonClashingName(newFile, fileProvider, '.json')
if (!fileProvider.set(newFile, txJSON)) {
modalDialogCustom.alert('Failed to create file ' + newFile)
} else {
appAPI.switchFile(newFile)
}
}
})
}
runButton.onclick = () => {
var currentFile = appAPI.config.get('currentFile')
var json = appAPI.filesProviders['browser'].get(currentFile)
if (currentFile.match('.json$')) {
try {
var obj = JSON.parse(json)
var txArray = obj.transactions || []
var accounts = obj.accounts || []
var options = obj.options
var abis = obj.abis
var linkReferences = obj.linkReferences || {}
} catch (e) {
return modalDialogCustom.alert('Invalid Scenario File, please try again')
}
if (txArray.length) {
noInstancesText.style.display = 'none'
recorder.run(txArray, accounts, options, abis, linkReferences, (abi, address, contractName) => {
instanceContainer.appendChild(appAPI.udapp().renderInstanceFromABI(abi, address, contractName))
})
}
} else {
modalDialogCustom.alert('Scenario File require JSON type')
}
}
return el
}
/* ------------------------------------------------
section CONTRACT DROPDOWN and BUTTONS
------------------------------------------------ */
function contractDropdown (appAPI, appEvents, instanceContainer) {
function contractDropdown (events, appAPI, appEvents, instanceContainer) {
instanceContainer.appendChild(noInstancesText)
var compFails = yo`<i title="Contract compilation failed. Please check the compile tab for more information." class="fa fa-thumbs-down ${css.errorIcon}" ></i>`
appEvents.compiler.register('compilationFinished', function (success, data, source) {
@ -273,6 +359,18 @@ function contractDropdown (appAPI, appEvents, instanceContainer) {
var atAddressButtonInput = yo`<input class="${css.input} ataddressinput" placeholder="Load contract from Address" title="atAddress" />`
var createButtonInput = yo`<input class="${css.input}" placeholder="" title="Create" />`
var selectContractNames = yo`<select class="${css.contractNames}" disabled></select>`
function getSelectedContract () {
var contractName = selectContractNames.children[selectContractNames.selectedIndex].innerHTML
if (contractName) {
return {
name: contractName,
contract: appAPI.getContract(contractName)
}
}
return null
}
appAPI.getSelectedContract = getSelectedContract
var el = yo`
<div class="${css.container}">
<div class="${css.subcontainer}">
@ -287,6 +385,7 @@ function contractDropdown (appAPI, appEvents, instanceContainer) {
${atAddressButtonInput}
<div class="${css.atAddress}" onclick=${function () { loadFromAddress(appAPI) }}>At Address</div>
</div>
<div class=${css.buttons}>${makeRecorder(events, appAPI, appEvents)}</div>
</div>
</div>
`
@ -294,8 +393,7 @@ function contractDropdown (appAPI, appEvents, instanceContainer) {
function setInputParamsPlaceHolder () {
createButtonInput.value = ''
if (appAPI.getContract && selectContractNames.selectedIndex >= 0 && selectContractNames.children.length > 0) {
var contract = appAPI.getContract(selectContractNames.children[selectContractNames.selectedIndex].innerHTML)
var ctrabi = txHelper.getConstructorInterface(contract.object.abi)
var ctrabi = txHelper.getConstructorInterface(getSelectedContract().contract.object.abi)
if (ctrabi.inputs.length) {
createButtonInput.setAttribute('placeholder', txHelper.inputParametersDeclarationToString(ctrabi.inputs))
createButtonInput.removeAttribute('disabled')
@ -310,20 +408,18 @@ function contractDropdown (appAPI, appEvents, instanceContainer) {
// ADD BUTTONS AT ADDRESS AND CREATE
function createInstance () {
var contractNames = document.querySelector(`.${css.contractNames.classNames[0]}`)
var contractName = contractNames.children[contractNames.selectedIndex].innerHTML
var contract = appAPI.getContract(contractName)
var selectedContract = getSelectedContract()
if (contract.object.evm.bytecode.object.length === 0) {
if (selectedContract.contract.object.evm.bytecode.object.length === 0) {
modalDialogCustom.alert('This contract does not implement all functions and thus cannot be created.')
return
}
var constructor = txHelper.getConstructorInterface(contract.object.abi)
var constructor = txHelper.getConstructorInterface(selectedContract.contract.object.abi)
var args = createButtonInput.value
txFormat.buildData(contract.object, appAPI.getContracts(), true, constructor, args, appAPI.udapp(), (error, data) => {
txFormat.buildData(selectedContract.name, selectedContract.contract.object, appAPI.getContracts(), true, constructor, args, appAPI.udapp(), (error, data) => {
if (!error) {
appAPI.logMessage(`creation of ${contractName} pending...`)
appAPI.logMessage(`creation of ${selectedContract.name} pending...`)
txExecution.createContract(data, appAPI.udapp(), (error, txResult) => {
if (!error) {
var isVM = executionContext.isVM()
@ -336,13 +432,13 @@ function contractDropdown (appAPI, appEvents, instanceContainer) {
}
noInstancesText.style.display = 'none'
var address = isVM ? txResult.result.createdAddress : txResult.result.contractAddress
instanceContainer.appendChild(appAPI.udapp().renderInstance(contract.object, address, selectContractNames.value))
instanceContainer.appendChild(appAPI.udapp().renderInstance(selectedContract.contract.object, address, selectContractNames.value))
} else {
appAPI.logMessage(`creation of ${contractName} errored: ` + error)
appAPI.logMessage(`creation of ${selectedContract.name} errored: ` + error)
}
})
} else {
appAPI.logMessage(`creation of ${contractName} errored: ` + error)
appAPI.logMessage(`creation of ${selectedContract.name} errored: ` + error)
}
}, (msg) => {
appAPI.logMessage(msg)

@ -13,8 +13,20 @@ module.exports = {
},
prompt: function (title, text, inputValue, ok, cancel) {
if (!inputValue) inputValue = ''
modal(title,
yo`<div>${text}<div><input type='text' name='prompt_text' id='prompt_text' class="${css['prompt_text']}" value='${inputValue}' ></div></div>`,
var input = yo`<input type='text' name='prompt_text' id='prompt_text' class="${css['prompt_text']}" value='${inputValue}' >`
modal(title, yo`<div>${text}<div>${input}</div></div>`,
{
fn: () => { if (typeof ok === 'function') ok(document.getElementById('prompt_text').value) }
},
{
fn: () => { if (typeof cancel === 'function') cancel() }
}
)
},
promptMulti: function ({ title, text, inputValue }, ok, cancel) {
if (!inputValue) inputValue = ''
var input = yo`<textarea id="prompt_text" class=${css.prompt_text} rows="4" cols="50"></textarea>`
modal(title, yo`<div>${text}<div>${input}</div></div>`,
{
fn: () => { if (typeof ok === 'function') ok(document.getElementById('prompt_text').value) }
},

@ -0,0 +1,256 @@
var remixLib = require('remix-lib')
var EventManager = remixLib.EventManager
var ethutil = require('ethereumjs-util')
var executionContext = require('./execution-context')
var format = require('./app/execution/txFormat')
var txHelper = require('./app/execution/txHelper')
var async = require('async')
var modal = require('./app/ui/modal-dialog-custom')
/**
* Record transaction as long as the user create them.
*
*
*/
class Recorder {
constructor (opts = {}) {
var self = this
self._api = opts.api
self.event = new EventManager()
self.data = { _listen: true, _replay: false, journal: [], _createdContracts: {}, _createdContractsReverse: {}, _usedAccounts: {}, _abis: {}, _contractABIReferences: {}, _linkReferences: {} }
opts.events.executioncontext.register('contextChanged', () => {
self.clearAll()
})
opts.events.runtab.register('clearInstances', () => {
self.clearAll()
})
opts.events.udapp.register('initiatingTransaction', (timestamp, tx, payLoad) => {
if (tx.useCall) return
var { from, to, value } = tx
// convert to and from to tokens
if (this.data._listen) {
var record = { value, parameters: payLoad.funArgs }
if (!to) {
var selectedContract = self._api.getContract(payLoad.contractName)
if (selectedContract) {
var abi = selectedContract.object.abi
var sha3 = ethutil.bufferToHex(ethutil.sha3(abi))
record.abi = sha3
record.contractName = payLoad.contractName
record.bytecode = payLoad.contractBytecode
record.linkReferences = selectedContract.object.evm.bytecode.linkReferences
if (Object.keys(record.linkReferences).length) {
for (var file in record.linkReferences) {
for (var lib in record.linkReferences[file]) {
self.data._linkReferences[lib] = '<address>'
}
}
}
self.data._abis[sha3] = abi
this.data._contractABIReferences[timestamp] = sha3
}
} else {
var creationTimestamp = this.data._createdContracts[to]
record.to = `created{${creationTimestamp}}`
record.abi = this.data._contractABIReferences[creationTimestamp]
}
record.name = payLoad.funAbi.name
record.type = payLoad.funAbi.type
self._api.getAccounts((error, accounts) => {
if (error) return console.log(error)
record.from = `account{${accounts.indexOf(from)}}`
self.data._usedAccounts[record.from] = from
self.append(timestamp, record)
})
}
})
opts.events.udapp.register('transactionExecuted', (error, from, to, data, call, txResult, timestamp) => {
if (error) return console.log(error)
if (call) return
var address = executionContext.isVM() ? txResult.result.createdAddress : txResult.result.contractAddress
if (!address) return // not a contract creation
address = addressToString(address)
// save back created addresses for the convertion from tokens to real adresses
this.data._createdContracts[address] = timestamp
this.data._createdContractsReverse[timestamp] = address
})
}
/**
* stop/start saving txs. If not listenning, is basically in replay mode
*
* @param {Bool} listen
*/
setListen (listen) {
this.data._listen = listen
this.data._replay = !listen
}
extractTimestamp (value) {
var stamp = /created{(.*)}/g.exec(value)
if (stamp) {
return stamp[1]
}
return null
}
/**
* convert back from/to from tokens to real addresses
*
* @param {Object} record
* @param {Object} accounts
* @param {Object} options
*
*/
resolveAddress (record, accounts, options) {
if (record.to) {
var stamp = this.extractTimestamp(record.to)
if (stamp) {
record.to = this.data._createdContractsReverse[stamp]
}
}
record.from = accounts[record.from]
// @TODO: writing browser test
return record
}
/**
* save the given @arg record
*
* @param {Number/String} timestamp
* @param {Object} record
*
*/
append (timestamp, record) {
var self = this
self.data.journal.push({ timestamp, record })
}
/**
* basically return the records + associate values (like abis / accounts)
*
*/
getAll () {
var self = this
var records = [].concat(self.data.journal)
return {
accounts: self.data._usedAccounts,
linkReferences: self.data._linkReferences,
transactions: records.sort((A, B) => {
var stampA = A.timestamp
var stampB = B.timestamp
return stampA - stampB
}),
abis: self.data._abis
}
}
/**
* delete the seen transactions
*
*/
clearAll () {
var self = this
self.data._listen = true
self.data._replay = false
self.data.journal = []
self.data._createdContracts = {}
self.data._createdContractsReverse = {}
self.data._usedAccounts = {}
self.data._abis = {}
self.data._contractABIReferences = {}
self.data._linkReferences = {}
}
/**
* run the list of records
*
* @param {Object} accounts
* @param {Object} options
* @param {Object} abis
* @param {Function} newContractFn
*
*/
run (records, accounts, options, abis, linkReferences, newContractFn) {
var self = this
self.setListen(false)
self._api.logMessage(`Running ${records.length} transaction(s) ...`)
async.eachOfSeries(records, function (tx, index, cb) {
var record = self.resolveAddress(tx.record, accounts, options)
var abi = abis[tx.record.abi]
if (!abi) {
modal.alert('cannot find ABI for ' + tx.record.abi + '. Execution stopped at ' + index)
return
}
/* Resolve Library */
if (record.linkReferences && Object.keys(record.linkReferences).length) {
for (var k in linkReferences) {
var link = linkReferences[k]
var timestamp = self.extractTimestamp(link)
if (timestamp && self.data._createdContractsReverse[timestamp]) {
link = self.data._createdContractsReverse[timestamp]
}
tx.record.bytecode = format.linkLibraryStandardFromlinkReferences(k, link.replace('0x', ''), tx.record.bytecode, tx.record.linkReferences)
}
}
/* Encode params */
var fnABI
if (tx.record.type === 'constructor') {
fnABI = txHelper.getConstructorInterface(abi)
} else {
fnABI = txHelper.getFunction(abi, record.name)
}
if (!fnABI) {
modal.alert('cannot resolve abi of ' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index)
cb('cannot resolve abi')
return
}
var data = format.encodeData(fnABI, tx.record.parameters, tx.record.bytecode)
if (data.error) {
modal.alert(data.error + '. Record:' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index)
cb(data.error)
return
} else {
self._api.logMessage(`(${index}) ${JSON.stringify(record, null, '\t')}`)
self._api.logMessage(`(${index}) data: ${data.data}`)
record.data = { dataHex: data.data, funArgs: tx.record.parameters, funAbi: fnABI, contractBytecode: tx.record.bytecode, contractName: tx.record.contractName }
}
self._api.udapp().runTx(record, function (err, txResult) {
if (err) {
console.error(err)
self._api.logMessage(err + '. Execution failed at ' + index)
} else {
var address = executionContext.isVM() ? txResult.result.createdAddress : txResult.result.contractAddress
if (address) {
address = addressToString(address)
// save back created addresses for the convertion from tokens to real adresses
self.data._createdContracts[address] = tx.timestamp
self.data._createdContractsReverse[tx.timestamp] = address
newContractFn(abi, address, record.contractName)
}
}
cb(err)
})
}, () => { self.setListen(true); self.clearAll() })
}
}
function addressToString (address) {
if (!address) return null
if (typeof address !== 'string') {
address = address.toString('hex')
}
if (address.indexOf('0x') === -1) {
address = '0x' + address
}
return address
}
module.exports = Recorder

@ -7,7 +7,6 @@ var BN = ethJSUtil.BN
var remixLib = require('remix-lib')
var EventManager = remixLib.EventManager
var crypto = require('crypto')
var async = require('async')
var TxRunner = require('./app/execution/txRunner')
var yo = require('yo-yo')
var txFormat = require('./app/execution/txFormat')
@ -291,10 +290,10 @@ UniversalDApp.prototype.renderInstanceFromABI = function (contractABI, address,
function remove () { instance.remove() }
var instance = yo`<div class="instance ${css.instance}"></div>`
address = (address.slice(0, 2) === '0x' ? '' : '0x') + address.toString('hex')
var instance = yo`<div class="instance ${css.instance}" id="instance${address}"></div>`
var context = executionContext.isVM() ? 'memory' : 'blockchain'
address = (address.slice(0, 2) === '0x' ? '' : '0x') + address.toString('hex')
var shortAddress = helper.shortenAddress(address)
var title = yo`<div class="${css.title}" onclick=${toggleClass}>
<div class="${css.titleText}"> ${contractName} at ${shortAddress} (${context}) </div>
@ -310,22 +309,20 @@ UniversalDApp.prototype.renderInstanceFromABI = function (contractABI, address,
$(instance).toggleClass(`${css.hidesub}`)
}
var abi = txHelper.sortAbiFunction(contractABI)
instance.appendChild(title)
// Add the fallback function
var fallback = txHelper.getFallbackInterface(abi)
var fallback = txHelper.getFallbackInterface(contractABI)
if (fallback) {
instance.appendChild(this.getCallButton({
funABI: fallback,
address: address,
contractAbi: abi,
contractAbi: contractABI,
contractName: contractName
}))
}
$.each(abi, (i, funABI) => {
$.each(contractABI, (i, funABI) => {
if (funABI.type !== 'function') {
return
}
@ -333,7 +330,7 @@ UniversalDApp.prototype.renderInstanceFromABI = function (contractABI, address,
instance.appendChild(this.getCallButton({
funABI: funABI,
address: address,
contractAbi: abi,
contractAbi: contractABI,
contractName: contractName
}))
})
@ -384,7 +381,7 @@ UniversalDApp.prototype.getCallButton = function (args) {
logMsg = `call to ${args.contractName}.${(args.funABI.name) ? args.funABI.name : '(fallback)'}`
}
}
txFormat.buildData(args.contractAbi, self.contracts, false, args.funABI, inputField.value, self, (error, data) => {
txFormat.buildData(args.contractName, args.contractAbi, self.contracts, false, args.funABI, inputField.value, self, (error, data) => {
if (!error) {
if (isUserAction) {
if (!args.funABI.constant) {
@ -435,7 +432,6 @@ UniversalDApp.prototype.getCallButton = function (args) {
if (lookupOnly) {
contractProperty.classList.add(css.constant)
button.setAttribute('title', (title + ' - call'))
call(false)
}
if (args.funABI.inputs && args.funABI.inputs.length > 0) {
@ -458,103 +454,99 @@ UniversalDApp.prototype.pendingTransactions = function () {
return this.txRunner.pendingTxs
}
function execute (pipeline, env, callback) {
function next (err, env) {
if (err) return callback(err)
var step = pipeline.shift()
if (step) step(env, next)
else callback(null, env.result)
}
next(null, env)
}
UniversalDApp.prototype.runTx = function (args, cb) {
var self = this
var tx = {
to: args.to,
data: args.data,
useCall: args.useCall
}
async.waterfall([
// query gas limit
function (callback) {
tx.gasLimit = 3000000
var tx = { to: args.to, data: args.data.dataHex, useCall: args.useCall, from: args.from, value: args.value }
var payLoad = { funAbi: args.data.funAbi, funArgs: args.data.funArgs, contractBytecode: args.data.contractBytecode, contractName: args.data.contractName } // contains decoded parameters
var pipeline = [queryGasLimit]
if (!args.value) {
pipeline.push(queryValue)
}
if (!args.from) {
pipeline.push(queryAddress)
}
pipeline.push(runTransaction)
var env = { self, tx, payLoad }
execute(pipeline, env, cb)
}
function queryGasLimit (env, next) {
var { self, tx } = env
tx.gasLimit = 3000000
if (self.transactionContextAPI.getGasLimit) {
self.transactionContextAPI.getGasLimit(function (err, ret) {
if (err) {
return callback(err)
}
if (err) return next(err)
tx.gasLimit = ret
callback()
next(null, env)
})
} else {
callback()
} else next(null, env)
}
},
// query value
function (callback) {
function queryValue (env, next) {
var { self, tx } = env
tx.value = 0
if (tx.useCall) return callback()
if (tx.useCall) return next(null, env)
if (self.transactionContextAPI.getValue) {
self.transactionContextAPI.getValue(function (err, ret) {
if (err) {
return callback(err)
}
if (err) return next(err)
tx.value = ret
callback()
next(null, env)
})
} else {
callback()
} else next(null, env)
}
},
// query address
function (callback) {
function queryAddress (env, next) {
var { self, tx } = env
if (self.transactionContextAPI.getAddress) {
self.transactionContextAPI.getAddress(function (err, ret) {
if (err) {
return callback(err)
}
if (err) return next(err)
tx.from = ret
callback()
next(null, env)
})
} else {
self.getAccounts(function (err, ret) {
if (err) {
return callback(err)
}
if (ret.length === 0) {
return callback('No accounts available')
}
if (err) return next(err)
if (ret.length === 0) return next('No accounts available')
if (executionContext.isVM() && !self.accounts[ret[0]]) {
return callback('Invalid account selected')
return next('Invalid account selected')
}
tx.from = ret[0]
callback()
next(null, env)
})
}
},
// run transaction
function (callback) {
}
function runTransaction (env, next) {
var { self, tx, payLoad } = env
var timestamp = Date.now()
self.event.trigger('initiatingTransaction', [timestamp, tx, payLoad])
self.txRunner.rawRun(tx, function (error, result) {
if (!args.useCall) {
self.event.trigger('transactionExecuted', [error, args.from, args.to, args.data, false, result])
if (!tx.useCall) {
self.event.trigger('transactionExecuted', [error, tx.from, tx.to, tx.data, false, result, timestamp, payLoad])
} else {
self.event.trigger('callExecuted', [error, args.from, args.to, args.data, true, result])
self.event.trigger('callExecuted', [error, tx.from, tx.to, tx.data, true, result, timestamp, payLoad])
}
if (error) {
if (typeof (error) !== 'string') {
if (error.message) {
error = error.message
} else {
try {
error = 'error: ' + JSON.stringify(error)
} catch (e) {}
if (error.message) error = error.message
else {
try { error = 'error: ' + JSON.stringify(error) } catch (e) {}
}
}
}
callback(error, result)
env.result = result
next(error, env)
})
}
], cb)
}
module.exports = UniversalDApp

@ -11,7 +11,10 @@ module.exports = {
checkDebug,
goToVMtraceStep,
useFilter,
addInstance
addInstance,
clickFunction,
verifyCallReturnValue,
setEditorValue
}
function getCompiledContracts (browser, compiled, callback) {
@ -65,7 +68,41 @@ function testContracts (browser, fileName, contractCode, compiledContractNames,
})
}
function testFunction (fnFullName, txHash, log, expectedInput, expectedReturn, expectedEvent) {
function clickFunction (fnFullName, expectedInput) {
this.waitForElementPresent('.instance button[title="' + fnFullName + '"]')
.perform(function (client, done) {
client.execute(function () {
document.querySelector('#optionViews').scrollTop = document.querySelector('#optionViews').scrollHeight
}, [], function () {
if (expectedInput) {
client.setValue('#runTabView input[title="' + expectedInput.types + '"]', expectedInput.values, function () {})
}
done()
})
})
.click('.instance button[title="' + fnFullName + '"]')
.pause(500)
return this
}
function verifyCallReturnValue (browser, address, checks, done) {
browser.execute(function (address) {
var nodes = document.querySelectorAll('#instance' + address + ' div[class^="contractProperty"] div[class^="value"]')
var ret = []
for (var k = 0; k < nodes.length; k++) {
var text = nodes[k].innerText ? nodes[k].innerText : nodes[k].textContent
ret.push(text.replace('\n', ''))
}
return ret
}, [address], function (result) {
for (var k in checks) {
browser.assert.equal(checks[k], result.value[k])
}
done()
})
}
function testFunction (fnFullName, txHash, log, expectedInput, expectedReturn, expectedEvent, callback) {
// this => browser
this.waitForElementPresent('.instance button[title="' + fnFullName + '"]')
.perform(function (client, done) {
@ -94,19 +131,33 @@ function testFunction (fnFullName, txHash, log, expectedInput, expectedReturn, e
client.assert.containsText('#editor-container div[class^="terminal"] span[id="tx' + txHash + '"] table[class^="txTable"] #logs', expectedEvent)
}
done()
if (callback) callback()
})
return this
}
function addInstance (browser, address, done) {
function setEditorValue (value) {
this.perform((client, done) => {
this.execute(function (value) {
document.getElementById('input').editor.session.setValue(value)
}, [value], function (result) {
done()
})
})
return this
}
function addInstance (browser, address, callback) {
browser.setValue('.ataddressinput', address, function () {
browser.click('div[class^="atAddress"]')
.perform((client) => {
.perform((client, done) => {
browser.execute(function () {
document.querySelector('#modal-footer-ok').click()
}, [], function (result) {
done()
})
}).perform(() => {
callback()
})
})
}
@ -124,7 +175,7 @@ function addFile (browser, name, content, done) {
done()
})
})
.setValue('#input textarea', content.content, function () {})
.setEditorValue(content.content)
.pause(1000)
.perform(function () {
done()

@ -23,29 +23,34 @@ module.exports = {
function runTests (browser, testData) {
browser.testFunction = contractHelper.testFunction
browser.setEditorValue = contractHelper.setEditorValue
browser
.waitForElementVisible('.newFile', 10000)
.click('.compileView')
.perform((client, done) => {
contractHelper.testContracts(browser, 'Untitled.sol', sources[0]['browser/Untitled.sol'], ['Ballot'], function () {
browser
.click('.runView')
.setValue('input[placeholder="uint8 _numProposals"]', '1', () => {})
done()
})
}).click('.runView')
.setValue('input[placeholder="uint8 _numProposals"]', '1')
.click('#runTabView div[class^="create"]')
.testFunction('delegate - transact (not payable)', '0xd3cd54e2f76f3993078ecf9e1b54a148def4520afc141a182293b3610bddf10f',
'[vm] from:0xca3...a733c, to:Ballot.delegate(address) 0x692...77b3a, value:0 wei, data:0x5c1...4d2db, 0 logs, hash:0xd3c...df10f',
.testFunction('delegate - transact (not payable)', '0x0571a2439ea58bd349dd130afb8aff62a33af14c06de0dbc3928519bdf13ce2e',
'[vm] from:0xca3...a733c, to:Ballot.delegate(address) 0x692...77b3a, value:0 wei, data:0x5c1...4d2db, 0 logs, hash:0x057...3ce2e',
{types: 'address to', values: '"0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"'}, null, null)
.click('span#tx0xd3cd54e2f76f3993078ecf9e1b54a148def4520afc141a182293b3610bddf10f button[class^="debug"]')
.pause(500)
.click('span#tx0x0571a2439ea58bd349dd130afb8aff62a33af14c06de0dbc3928519bdf13ce2e button[class^="debug"]')
.pause(1000)
.click('#jumppreviousbreakpoint')
.click('#stepdetail .title')
.click('#asmcodes .title')
.pause(500)
.perform(function (client, done) {
console.log('goToVMtraceStep')
contractHelper.goToVMtraceStep(browser, 39, () => {
done()
})
})
.pause(5000)
.pause(1000)
.perform(function (client, done) {
contractHelper.checkDebug(browser, 'soliditystate', stateCheck, () => {
done()
@ -58,19 +63,24 @@ function runTests (browser, testData) {
})
.click('.runView')
.click('div[class^="udappClose"]')
.perform(function (client, done) {
contractHelper.addFile(client, 'ballot.abi', { content: ballotABI }, () => {
contractHelper.addInstance(client, '0x692a70d2e424a56d2c6c27aa97d1a86395877b3a', () => {
browser.testFunction('delegate - transact (not payable)', '0x7a9ebc90614274b7eb6b072f9bba7825e588cf88ae00598cfdbc4c215b88433e',
'[vm] from:0xca3...a733c, to:Ballot.delegate(address) 0x692...77b3a, value:0 wei, data:0x5c1...4d2db, 0 logs, hash:0x7a9...8433e',
{types: 'address to', values: '"0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"'}, null, null).perform(() => {
.perform((client, done) => {
console.log('ballot.abi')
contractHelper.addFile(browser, 'ballot.abi', { content: ballotABI }, () => {
done()
browser.end()
})
})
})
.perform((client, done) => {
console.log('addInstance 0x692a70d2e424a56d2c6c27aa97d1a86395877b3a')
contractHelper.addInstance(browser, '0x692a70d2e424a56d2c6c27aa97d1a86395877b3a', () => {
done()
})
})
.perform((client, done) => {
console.log('delegate - transact (not payable)')
browser.testFunction('delegate - transact (not payable)', '0xd3cd54e2f76f3993078ecf9e1b54a148def4520afc141a182293b3610bddf10f',
'[vm] from:0xca3...a733c, to:Ballot.delegate(address) 0x692...77b3a, value:0 wei, data:0x5c1...4d2db, 0 logs, hash:0xd3c...df10f',
{types: 'address to', values: '"0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"'}, null, null, () => { done() })
}).end()
}
var localsCheck = {

@ -3,6 +3,7 @@ var contractHelper = require('../helpers/contracts')
var init = require('../helpers/init')
var sauce = require('./sauce')
var async = require('async')
var testRecorder = require('./units/testRecorder')
module.exports = {
before: function (browser, done) {
@ -19,12 +20,14 @@ module.exports = {
function runTests (browser) {
browser.testFunction = contractHelper.testFunction
browser.clickFunction = contractHelper.clickFunction
browser.setEditorValue = contractHelper.setEditorValue
browser
.waitForElementVisible('.newFile', 10000)
.click('.compileView')
.perform(() => {
// the first fn is used to pass browser to the other ones.
async.waterfall([function (callback) { callback(null, browser) }, testSimpleContract, testReturnValues, testInputValues], function () {
async.waterfall([function (callback) { callback(null, browser) }, testSimpleContract, testReturnValues, testInputValues, testRecorder], function () {
browser.end()
})
})

@ -18,6 +18,7 @@ module.exports = {
}
function runTests (browser) {
browser.setEditorValue = contractHelper.setEditorValue
browser
.waitForElementVisible('.newFile', 10000)
.click('.compileView')

@ -33,6 +33,7 @@ module.exports = {
}
function runTests (browser) {
browser.setEditorValue = contractHelper.setEditorValue
browser
.waitForElementVisible('.newFile', 10000)
.click('.compileView')

@ -0,0 +1,172 @@
'use strict'
var contractHelper = require('../../helpers/contracts')
module.exports = function (browser, callback) {
contractHelper.addFile(browser, 'scenario.json', {content: records}, () => {
browser
.click('.runView')
.click('#runTabView .runtransaction')
.clickFunction('getInt - call')
.clickFunction('getAddress - call')
.clickFunction('getFromLib - call')
.waitForElementPresent('div[class^="contractProperty"] div[class^="value"]')
.perform(() => {
contractHelper.verifyCallReturnValue(browser, '0x35ef07393b57464e93deb59175ff72e6499450cf', ['0: uint256: 1', '0: uint256: 3456', '0: address: 0xca35b7d915458ef540ade6068dfe2f44e8fa733c'], () => { callback() })
})
})
}
var records = `{
"accounts": {
"account{0}": "0xca35b7d915458ef540ade6068dfe2f44e8fa733c"
},
"linkReferences": {
"testLib": "created{1512830014773}"
},
"transactions": [
{
"timestamp": 1512830014773,
"record": {
"value": "0",
"parameters": [],
"abi": "0xbc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a",
"contractName": "testLib",
"bytecode": "60606040523415600e57600080fd5b60968061001c6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636d4ce63c146044575b600080fd5b604a6060565b6040518082815260200191505060405180910390f35b6000610d809050905600a165627a7a7230582022d123b15248b8176151f8d45c2dc132063bcc9bb8d5cd652aea7efae362c8050029",
"linkReferences": {},
"type": "constructor",
"from": "account{0}"
}
},
{
"timestamp": 1512830015080,
"record": {
"value": "100",
"parameters": [
11
],
"abi": "0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec",
"contractName": "test",
"bytecode": "60606040526040516020806102b183398101604052808051906020019091905050806000819055505061027a806100376000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632f30c6f61461006757806338cc48311461009e57806362738998146100f357806387cc10e11461011c575b600080fd5b61009c600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610145565b005b34156100a957600080fd5b6100b1610191565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34156100fe57600080fd5b6101066101bb565b6040518082815260200191505060405180910390f35b341561012757600080fd5b61012f6101c4565b6040518082815260200191505060405180910390f35b8160008190555080600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505050565b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b60008054905090565b600073__browser/ballot.sol:testLib____________636d4ce63c6000604051602001526040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160206040518083038186803b151561022e57600080fd5b6102c65a03f4151561023f57600080fd5b505050604051805190509050905600a165627a7a72305820e0b2510bb2890a0334bfe5613d96db3e72442e63b514cdeaee8fc2c6bbd19d3a0029",
"linkReferences": {
"browser/ballot.sol": {
"testLib": [
{
"length": 20,
"start": 511
}
]
}
},
"name": "",
"type": "constructor",
"from": "account{0}"
}
},
{
"timestamp": 1512830034180,
"record": {
"value": "1000000000000000000",
"parameters": [
1,
"0xca35b7d915458ef540ade6068dfe2f44e8fa733c"
],
"to": "created{1512830015080}",
"abi": "0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec",
"name": "set",
"type": "function",
"from": "account{0}"
}
}
],
"abis": {
"0xbc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a": [
{
"constant": true,
"inputs": [],
"name": "get",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
],
"0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec": [
{
"constant": true,
"inputs": [],
"name": "getInt",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getFromLib",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getAddress",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_t",
"type": "uint256"
},
{
"name": "_add",
"type": "address"
}
],
"name": "set",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"name": "_r",
"type": "uint256"
}
],
"payable": true,
"stateMutability": "payable",
"type": "constructor"
}
]
}
}`
Loading…
Cancel
Save