UNPKG

remix-ide

Version:
330 lines (307 loc) 11.8 kB
var async = require('async') var ethutil = require('ethereumjs-util') var remixLib = require('remix-lib') var EventManager = remixLib.EventManager var format = remixLib.execution.txFormat var txHelper = remixLib.execution.txHelper var helper = require('../../../../lib/helper.js') /** * Record transaction as long as the user create them. * * */ class Recorder { constructor (blockchain, fileManager, config) { var self = this self.event = new EventManager() self.blockchain = blockchain self.data = { _listen: true, _replay: false, journal: [], _createdContracts: {}, _createdContractsReverse: {}, _usedAccounts: {}, _abis: {}, _contractABIReferences: {}, _linkReferences: {} } this.fileManager = fileManager this.config = config this.blockchain.event.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 abi = payLoad.contractABI var keccak = ethutil.bufferToHex(ethutil.keccak(abi)) record.abi = keccak record.contractName = payLoad.contractName record.bytecode = payLoad.contractBytecode record.linkReferences = payLoad.linkReferences if (record.linkReferences && 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[keccak] = abi this.data._contractABIReferences[timestamp] = keccak } else { var creationTimestamp = this.data._createdContracts[to] record.to = `created{${creationTimestamp}}` record.abi = this.data._contractABIReferences[creationTimestamp] } record.name = payLoad.funAbi.name record.inputs = txHelper.serializeInputs(payLoad.funAbi) record.type = payLoad.funAbi.type for (var p in record.parameters) { var thisarg = record.parameters[p] var thistimestamp = this.data._createdContracts[thisarg] if (thistimestamp) record.parameters[p] = `created{${thistimestamp}}` } this.blockchain.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) }) } }) this.blockchain.event.register('transactionExecuted', (error, from, to, data, call, txResult, timestamp, _payload, rawAddress) => { if (error) return console.log(error) if (call) return if (!rawAddress) return // not a contract creation const stringAddress = this.addressToString(rawAddress) const address = ethutil.toChecksumAddress(stringAddress) // save back created addresses for the convertion from tokens to real adresses this.data._createdContracts[address] = timestamp this.data._createdContractsReverse[timestamp] = address }) this.blockchain.event.register('contextChanged', this.clearAll.bind(this)) this.event.register('newTxRecorded', (count) => { this.event.trigger('recorderCountChange', [count]) }) this.event.register('cleared', () => { this.event.trigger('recorderCountChange', [0]) }) } /** * 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 }) self.event.trigger('newTxRecorded', [self.data.journal.length]) } /** * 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 = {} self.event.trigger('cleared', []) } /** * run the list of records * * @param {Object} accounts * @param {Object} options * @param {Object} abis * @param {Function} newContractFn * */ run (records, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, newContractFn) { var self = this self.setListen(false) logCallBack(`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) { return alertCb('cannot find ABI for ' + tx.record.abi + '. Execution stopped at ' + index) } /* 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 if (tx.record.type === 'fallback') { fnABI = txHelper.getFallbackInterface(abi) } else if (tx.record.type === 'receive') { fnABI = txHelper.getReceiveInterface(abi) } else { fnABI = txHelper.getFunction(abi, record.name + record.inputs) } if (!fnABI) { alertCb('cannot resolve abi of ' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index) return cb('cannot resolve abi') } if (tx.record.parameters) { /* check if we have some params to resolve */ try { tx.record.parameters.forEach((value, index) => { var isString = true if (typeof value !== 'string') { isString = false value = JSON.stringify(value) } for (var timestamp in self.data._createdContractsReverse) { value = value.replace(new RegExp('created\\{' + timestamp + '\\}', 'g'), self.data._createdContractsReverse[timestamp]) } if (!isString) value = JSON.parse(value) tx.record.parameters[index] = value }) } catch (e) { return alertCb('cannot resolve input parameters ' + JSON.stringify(tx.record.parameters) + '. Execution stopped at ' + index) } } var data = format.encodeData(fnABI, tx.record.parameters, tx.record.bytecode) if (data.error) { alertCb(data.error + '. Record:' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index) return cb(data.error) } logCallBack(`(${index}) ${JSON.stringify(record, null, '\t')}`) logCallBack(`(${index}) data: ${data.data}`) record.data = { dataHex: data.data, funArgs: tx.record.parameters, funAbi: fnABI, contractBytecode: tx.record.bytecode, contractName: tx.record.contractName, timestamp: tx.timestamp } self.blockchain.runTx(record, confirmationCb, continueCb, promptCb, function (err, txResult, rawAddress) { if (err) { console.error(err) return logCallBack(err + '. Execution failed at ' + index) } if (rawAddress) { const stringAddress = self.addressToString(rawAddress) const address = ethutil.toChecksumAddress(stringAddress) // 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() }) } addressToString (address) { if (!address) return null if (typeof address !== 'string') { address = address.toString('hex') } if (address.indexOf('0x') === -1) { address = '0x' + address } return address } runScenario (continueCb, promptCb, alertCb, confirmationCb, logCallBack, cb) { var currentFile = this.config.get('currentFile') this.fileManager.fileProviderOf(currentFile).get(currentFile, (error, json) => { if (error) { return cb('Invalid Scenario File ' + error) } if (!currentFile.match('.json$')) { return cb('A scenario file is required. Please make sure a scenario file is currently displayed in the editor. The file must be of type JSON. Use the "Save Transactions" Button to generate a new Scenario File.') } 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 cb('Invalid Scenario File, please try again') } if (!txArray.length) { return } this.run(txArray, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, (abi, address, contractName) => { cb(null, abi, address, contractName) }) }) } saveScenario (promptCb, cb) { var txJSON = JSON.stringify(this.getAll(), null, 2) var path = this.fileManager.currentPath() promptCb(path, input => { var fileProvider = this.fileManager.fileProviderOf(path) if (!fileProvider) return var newFile = path + '/' + input helper.createNonClashingName(newFile, fileProvider, (error, newFile) => { if (error) return cb('Failed to create file. ' + newFile + ' ' + error) if (!fileProvider.set(newFile, txJSON)) return cb('Failed to create file ' + newFile) this.fileManager.open(newFile) }) }) } } module.exports = Recorder