UNPKG

remix-ide

Version:

Minimalistic browser-based Solidity IDE

562 lines (524 loc) 18 kB
'use strict' var yo = require('yo-yo') var copyToClipboard = require('../ui/copy-to-clipboard') // -------------- styling ---------------------- var csjs = require('csjs-inject') var remixLib = require('remix-lib') var EventManager = require('../../lib/events') var helper = require('../../lib/helper') var executionContext = require('../../execution-context') var modalDialog = require('../ui/modal-dialog-custom') var typeConversion = remixLib.execution.typeConversion var globlalRegistry = require('../../global/registry') var css = csjs` .log { display: flex; cursor: pointer; align-items: center; cursor: pointer; } .log:hover { opacity: 0.8; } .arrow { color: var(--text-info); font-size: 20px; cursor: pointer; display: flex; margin-left: 10px; } .arrow:hover { color: var(--secondary); } .txLog { } .txStatus { display: flex; font-size: 20px; margin-right: 20px; float: left; } .succeeded { color: var(--success); } .failed { color: var(--danger); } .notavailable { } .call { font-size: 7px; border-radius: 50%; min-width: 20px; min-height: 20px; display: flex; justify-content: center; align-items: center; color: var(--text-info); text-transform: uppercase; font-weight: bold; } .txItem { color: var(--text-info); margin-right: 5px; float: left; } .txItemTitle { font-weight: bold; } .tx { color: var(--text-info); font-weight: bold; float: left; margin-right: 10px; } .txTable, .tr, .td { border-collapse: collapse; font-size: 10px; color: var(--text-info); border: 1px solid var(--text-info); } #txTable { margin-top: 1%; margin-bottom: 5%; align-self: center; width: 85%; } .tr, .td { padding: 4px; vertical-align: baseline; } .td:first-child { min-width: 30%; width: 30%; align-items: baseline; font-weight: bold; } .tableTitle { width: 25%; } .buttons { display: flex; margin-left: auto; } .debug { white-space: nowrap; } .debug:hover { opacity: 0.8; }` /** * This just export a function that register to `newTransaction` and forward them to the logger. * */ class TxLogger { constructor (localRegistry) { this.event = new EventManager() this.seen = {} function filterTx (value, query) { if (value.length) { return helper.find(value, query) } return false } this._components = {} this._components.registry = localRegistry || globlalRegistry // dependencies this._deps = { mainView: this._components.registry.get('mainview').api, txListener: this._components.registry.get('txlistener').api, eventsDecoder: this._components.registry.get('eventsdecoder').api, compilersArtefacts: this._components.registry.get('compilersartefacts').api } this.logKnownTX = this._deps.mainView.registerCommand('knownTransaction', (args, cmds, append) => { var data = args[0] var el if (data.tx.isCall) { el = renderCall(this, data) } else { el = renderKnownTransaction(this, data) } this.seen[data.tx.hash] = el append(el) }, { activate: true, filterFn: filterTx }) this.logUnknownTX = this._deps.mainView.registerCommand('unknownTransaction', (args, cmds, append) => { // triggered for transaction AND call var data = args[0] var el = renderUnknownTransaction(this, data) append(el) }, { activate: false, filterFn: filterTx }) this.logEmptyBlock = this._deps.mainView.registerCommand('emptyBlock', (args, cmds, append) => { var data = args[0] var el = renderEmptyBlock(this, data) append(el) }, { activate: true }) this._deps.txListener.event.register('newBlock', (block) => { if (!block.transactions || block.transactions && !block.transactions.length) { this.logEmptyBlock({ block: block }) } }) this._deps.txListener.event.register('newTransaction', (tx, receipt) => { log(this, tx, receipt) }) this._deps.txListener.event.register('newCall', (tx) => { log(this, tx, null) }) this._deps.mainView.updateTerminalFilter({ type: 'select', value: 'unknownTransaction' }) this._deps.mainView.updateTerminalFilter({ type: 'select', value: 'knownTransaction' }) } } function debug (e, data, self) { e.stopPropagation() if (data.tx.isCall && data.tx.envMode !== 'vm') { modalDialog.alert('Cannot debug this call. Debugging calls is only possible in JavaScript VM mode.') } else { self.event.trigger('debuggingRequested', [data.tx.hash]) } } function log (self, tx, receipt) { var resolvedTransaction = self._deps.txListener.resolvedTransaction(tx.hash) if (resolvedTransaction) { var compiledContracts = null if (self._deps.compilersArtefacts['__last']) { compiledContracts = self._deps.compilersArtefacts['__last'].getContracts() } self._deps.eventsDecoder.parseLogs(tx, resolvedTransaction.contractName, compiledContracts, (error, logs) => { if (!error) { self.logKnownTX({ tx: tx, receipt: receipt, resolvedData: resolvedTransaction, logs: logs }) } }) } else { // contract unknown - just displaying raw tx. self.logUnknownTX({ tx: tx, receipt: receipt }) } } function renderKnownTransaction (self, data) { var from = data.tx.from var to = data.resolvedData.contractName + '.' + data.resolvedData.fn var obj = {from, to} var txType = 'knownTx' var tx = yo` <span id="tx${data.tx.hash}"> <div class="${css.log}" onclick=${e => txDetails(e, tx, data, obj)}> ${checkTxStatus(data.receipt, txType)} ${context(self, {from, to, data})} <div class=${css.buttons}> <button class="${css.debug} btn btn-primary btn-sm" onclick=${(e) => debug(e, data, self)}>Debug</div> </div> <i class="${css.arrow} fas fa-angle-down"></i> </div> </span> ` return tx } function renderCall (self, data) { var to = data.resolvedData.contractName + '.' + data.resolvedData.fn var from = data.tx.from ? data.tx.from : ' - ' var input = data.tx.input ? helper.shortenHexData(data.tx.input) : '' var obj = {from, to} var txType = 'call' var tx = yo` <span id="tx${data.tx.hash}"> <div class="${css.log}" onclick=${e => txDetails(e, tx, data, obj)}> ${checkTxStatus(data.tx, txType)} <span class=${css.txLog}> <span class=${css.tx}>[call]</span> <div class=${css.txItem}><span class=${css.txItemTitle}>from:</span> ${from}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>to:</span> ${to}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>data:</span> ${input}</div> </span> <div class=${css.buttons}> <div class="${css.debug} btn btn-primary btn-sm" onclick=${(e) => debug(e, data, self)}>Debug</div> </div> <i class="${css.arrow} fas fa-angle-down"></i> </div> </span> ` return tx } function renderUnknownTransaction (self, data) { var from = data.tx.from var to = data.tx.to var obj = {from, to} var txType = 'unknown' + (data.tx.isCall ? 'Call' : 'Tx') var tx = yo` <span id="tx${data.tx.hash}"> <div class="${css.log}" onclick=${e => txDetails(e, tx, data, obj)}> ${checkTxStatus(data.receipt || data.tx, txType)} ${context(self, {from, to, data})} <div class=${css.buttons}> <div class="${css.debug} btn btn-primary btn-sm" onclick=${(e) => debug(e, data, self)}>Debug</div> </div> <i class="${css.arrow} fas fa-angle-down"></i> </div> </span> ` return tx } function renderEmptyBlock (self, data) { return yo` <span class=${css.txLog}> <span class='${css.tx}'><div class=${css.txItem}>[<span class=${css.txItemTitle}>block:${data.block.number} - </span> 0 transactions]</span></span> </span>` } function checkTxStatus (tx, type) { if (tx.status === '0x1') { return yo`<i class="${css.txStatus} ${css.succeeded} fas fa-check-circle"></i>` } if (type === 'call' || type === 'unknownCall') { return yo`<i class="${css.txStatus} ${css.call}">call</i>` } else if (tx.status === '0x0') { return yo`<i class="${css.txStatus} ${css.failed} fas fa-times-circle"></i>` } else { return yo`<i class="${css.txStatus} ${css.notavailable} fas fa-circle-thin" title='Status not available' ></i>` } } function context (self, opts) { var data = opts.data || '' var from = opts.from ? helper.shortenHexData(opts.from) : '' var to = opts.to if (data.tx.to) to = to + ' ' + helper.shortenHexData(data.tx.to) var val = data.tx.value var hash = data.tx.hash ? helper.shortenHexData(data.tx.hash) : '' var input = data.tx.input ? helper.shortenHexData(data.tx.input) : '' var logs = data.logs && data.logs.decoded && data.logs.decoded.length ? data.logs.decoded.length : 0 var block = data.tx.blockNumber || '' var i = data.tx.transactionIndex var value = val ? typeConversion.toInt(val) : 0 if (executionContext.getProvider() === 'vm') { return yo` <div> <span class=${css.txLog}> <span class=${css.tx}>[vm]</span> <div class=${css.txItem}><span class=${css.txItemTitle}>from:</span> ${from}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>to:</span> ${to}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>value:</span> ${value} wei</div> <div class=${css.txItem}><span class=${css.txItemTitle}>data:</span> ${input}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>logs:</span> ${logs}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>hash:</span> ${hash}</div> </span> </div>` } else if (executionContext.getProvider() !== 'vm' && data.resolvedData) { return yo` <div> <span class=${css.txLog}> <span class='${css.tx}'>[block:${block} txIndex:${i}]</span> <div class=${css.txItem}><span class=${css.txItemTitle}>from:</span> ${from}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>to:</span> ${to}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>value:</span> ${value} wei</div> <div class=${css.txItem}><span class=${css.txItemTitle}>data:</span> ${input}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>logs:</span> ${logs}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>hash:</span> ${hash}</div> </span> </div>` } else { to = helper.shortenHexData(to) hash = helper.shortenHexData(data.tx.blockHash) return yo` <div> <span class=${css.txLog}> <span class='${css.tx}'>[block:${block} txIndex:${i}]</span> <div class=${css.txItem}><span class=${css.txItemTitle}>from:</span> ${from}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>to:</span> ${to}</div> <div class=${css.txItem}><span class=${css.txItemTitle}>value:</span> ${value} wei</div> </span> </div>` } } module.exports = TxLogger // helpers function txDetails (e, tx, data, obj) { var table = document.querySelector(`#${tx.id} [class^="txTable"]`) var from = obj.from var to = obj.to var log = document.querySelector(`#${tx.id} [class^='log']`) var arrow = document.querySelector(`#${tx.id} [class^='arrow']`) var arrowUp = yo`<i class="${css.arrow} fas fa-angle-up"></i>` var arrowDown = yo`<i class="${css.arrow} fas fa-angle-down"></i>` if (table && table.parentNode) { tx.removeChild(table) log.removeChild(arrow) log.appendChild(arrowDown) } else { log.removeChild(arrow) log.appendChild(arrowUp) table = createTable({ hash: data.tx.hash, status: data.receipt ? data.receipt.status : null, isCall: data.tx.isCall, contractAddress: data.tx.contractAddress, data: data.tx, from, to, gas: data.tx.gas, input: data.tx.input, 'decoded input': data.resolvedData && data.resolvedData.params ? JSON.stringify(typeConversion.stringify(data.resolvedData.params), null, '\t') : ' - ', 'decoded output': data.resolvedData && data.resolvedData.decodedReturnValue ? JSON.stringify(typeConversion.stringify(data.resolvedData.decodedReturnValue), null, '\t') : ' - ', logs: data.logs, val: data.tx.value, transactionCost: data.tx.transactionCost, executionCost: data.tx.executionCost }) tx.appendChild(table) } } function createTable (opts) { var table = yo`<table class="${css.txTable}" id="txTable"></table>` if (!opts.isCall) { var msg = '' if (opts.status) { if (opts.status === '0x0') { msg = ' Transaction mined but execution failed' } else if (opts.status === '0x1') { msg = ' Transaction mined and execution succeed' } } else { msg = ' Status not available at the moment' } table.appendChild(yo` <tr class="${css.tr}"> <td class="${css.td}"> status </td> <td class="${css.td}">${opts.status}${msg}</td> </tr>`) } var transactionHash = yo` <tr class="${css.tr}"> <td class="${css.td}"> transaction hash </td> <td class="${css.td}">${opts.hash} ${copyToClipboard(() => opts.hash)} </td> </tr> ` table.appendChild(transactionHash) var contractAddress = yo` <tr class="${css.tr}"> <td class="${css.td}"> contract address </td> <td class="${css.td}">${opts.contractAddress} ${copyToClipboard(() => opts.contractAddress)} </td> </tr> ` if (opts.contractAddress) table.appendChild(contractAddress) var from = yo` <tr class="${css.tr}"> <td class="${css.td} ${css.tableTitle}"> from </td> <td class="${css.td}">${opts.from} ${copyToClipboard(() => opts.from)} </td> </tr> ` if (opts.from) table.appendChild(from) var toHash var data = opts.data // opts.data = data.tx if (data.to) { toHash = opts.to + ' ' + data.to } else { toHash = opts.to } var to = yo` <tr class="${css.tr}"> <td class="${css.td}"> to </td> <td class="${css.td}">${toHash} ${copyToClipboard(() => data.to ? data.to : toHash)} </td> </tr> ` if (opts.to) table.appendChild(to) var gas = yo` <tr class="${css.tr}"> <td class="${css.td}"> gas </td> <td class="${css.td}">${opts.gas} gas ${copyToClipboard(() => opts.gas)} </td> </tr> ` if (opts.gas) table.appendChild(gas) var callWarning = '' if (opts.isCall) { callWarning = '(Cost only applies when called by a contract)' } if (opts.transactionCost) { table.appendChild(yo` <tr class="${css.tr}"> <td class="${css.td}"> transaction cost </td> <td class="${css.td}">${opts.transactionCost} gas ${callWarning} ${copyToClipboard(() => opts.transactionCost)} </td> </tr>`) } if (opts.executionCost) { table.appendChild(yo` <tr class="${css.tr}"> <td class="${css.td}"> execution cost </td> <td class="${css.td}">${opts.executionCost} gas ${callWarning} ${copyToClipboard(() => opts.executionCost)} </td> </tr>`) } var hash = yo` <tr class="${css.tr}"> <td class="${css.td}"> hash </td> <td class="${css.td}">${opts.hash} ${copyToClipboard(() => opts.hash)} </td> </tr> ` if (opts.hash) table.appendChild(hash) var input = yo` <tr class="${css.tr}"> <td class="${css.td}"> input </td> <td class="${css.td}">${helper.shortenHexData(opts.input)} ${copyToClipboard(() => opts.input)} </td> </tr> ` if (opts.input) table.appendChild(input) if (opts['decoded input']) { var inputDecoded = yo` <tr class="${css.tr}"> <td class="${css.td}"> decoded input </td> <td class="${css.td}">${opts['decoded input']} ${copyToClipboard(opts['decoded input'])} </td> </tr>` table.appendChild(inputDecoded) } if (opts['decoded output']) { var outputDecoded = yo` <tr class="${css.tr}"> <td class="${css.td}"> decoded output </td> <td class="${css.td}" id="decodedoutput" >${opts['decoded output']} ${copyToClipboard(opts['decoded output'])} </td> </tr>` table.appendChild(outputDecoded) } var stringified = ' - ' if (opts.logs && opts.logs.decoded) { stringified = typeConversion.stringify(opts.logs.decoded) } var logs = yo` <tr class="${css.tr}"> <td class="${css.td}"> logs </td> <td class="${css.td}" id="logs"> ${JSON.stringify(stringified, null, '\t')} ${copyToClipboard(() => JSON.stringify(stringified, null, '\t'))} ${copyToClipboard(() => JSON.stringify(opts.logs.raw || '0'))} </td> </tr> ` if (opts.logs) table.appendChild(logs) var val = opts.val != null ? typeConversion.toInt(opts.val) : 0 val = yo` <tr class="${css.tr}"> <td class="${css.td}"> value </td> <td class="${css.td}">${val} wei ${copyToClipboard(() => `${val} wei`)} </td> </tr> ` if (opts.val) table.appendChild(val) return table }