remix-ide
Version:
Extendable Web IDE for Ethereum
568 lines (528 loc) • 19.4 kB
JavaScript
'use strict'
var yo = require('yo-yo')
var copyToClipboard = require('./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 modalDialog = require('./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 (terminal, blockchain) {
this.event = new EventManager()
this.seen = {}
function filterTx (value, query) {
if (value.length) {
return helper.find(value, query)
}
return false
}
this.eventsDecoder = globlalRegistry.get('eventsDecoder').api
this.txListener = globlalRegistry.get('txlistener').api
this.terminal = terminal
// dependencies
this._deps = {
compilersArtefacts: globlalRegistry.get('compilersartefacts').api
}
this.logKnownTX = this.terminal.registerCommand('knownTransaction', (args, cmds, append) => {
var data = args[0]
var el
if (data.tx.isCall) {
el = renderCall(this, data)
} else {
el = renderKnownTransaction(this, data, blockchain)
}
this.seen[data.tx.hash] = el
append(el)
}, { activate: true, filterFn: filterTx })
this.logUnknownTX = this.terminal.registerCommand('unknownTransaction', (args, cmds, append) => {
// triggered for transaction AND call
var data = args[0]
var el = renderUnknownTransaction(this, data, blockchain)
append(el)
}, { activate: false, filterFn: filterTx })
this.logEmptyBlock = this.terminal.registerCommand('emptyBlock', (args, cmds, append) => {
var data = args[0]
var el = renderEmptyBlock(this, data)
append(el)
}, { activate: true })
this.txListener.event.register('newBlock', (block) => {
if (!block.transactions || block.transactions && !block.transactions.length) {
this.logEmptyBlock({ block: block })
}
})
this.txListener.event.register('newTransaction', (tx, receipt) => {
log(this, tx, receipt)
})
this.txListener.event.register('newCall', (tx) => {
log(this, tx, null)
})
this.terminal.updateJournal({ type: 'select', value: 'unknownTransaction' })
this.terminal.updateJournal({ 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.txListener.resolvedTransaction(tx.hash)
if (resolvedTransaction) {
var compiledContracts = null
if (self._deps.compilersArtefacts['__last']) {
compiledContracts = self._deps.compilersArtefacts['__last'].getContracts()
}
self.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, blockchain) {
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}" data-id="txLogger${data.tx.hash}">
<div class="${css.log}" onclick=${e => txDetails(e, tx, data, obj)}>
${checkTxStatus(data.receipt, txType)}
${context(self, {from, to, data}, blockchain)}
<div class=${css.buttons}>
<button class="${css.debug} btn btn-primary btn-sm" data-shared="txLoggerDebugButton" data-id="txLoggerDebugButton${data.tx.hash}" 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, blockchain) {
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}, blockchain)}
<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' || tx.status === true) {
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' || tx.status === false) {
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, blockchain) {
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.receipt ? data.receipt.blockNumber : data.tx.blockNumber || ''
var i = data.receipt ? data.receipt.transactionIndex : data.tx.transactionIndex
var value = val ? typeConversion.toInt(val) : 0
if (blockchain.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 (blockchain.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) {
const from = obj.from
const to = obj.to
const arrowUp = yo`<i class="${css.arrow} fas fa-angle-up"></i>`
const arrowDown = yo`<i class="${css.arrow} fas fa-angle-down"></i>`
let blockElement = e.target
while (true) { // get the parent block element
if (blockElement.className.startsWith('block')) break
else if (blockElement.parentElement) {
blockElement = blockElement.parentElement
} else break
}
let table = blockElement.querySelector(`#${tx.id} [class^="txTable"]`)
const log = blockElement.querySelector(`#${tx.id} [class^='log']`)
const arrow = blockElement.querySelector(`#${tx.id} [class^='arrow']`)
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" data-id="txLoggerTable${opts.hash}"></table>`
if (!opts.isCall) {
var msg = ''
if (opts.status !== undefined && opts.status !== null) {
if (opts.status === '0x0' || opts.status === false) {
msg = ' Transaction mined but execution failed'
} else if (opts.status === '0x1' || opts.status === true) {
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}" data-shared="key_${opts.hash}"> status </td>
<td class="${css.td}" data-id="txLoggerTableStatus${opts.hash}" data-shared="pair_${opts.hash}">${opts.status}${msg}</td>
</tr>`)
}
var transactionHash = yo`
<tr class="${css.tr}">
<td class="${css.td}" data-shared="key_${opts.hash}"> transaction hash </td>
<td class="${css.td}" data-id="txLoggerTableHash${opts.hash}" data-shared="pair_${opts.hash}">${opts.hash}
${copyToClipboard(() => opts.hash)}
</td>
</tr>
`
table.appendChild(transactionHash)
var contractAddress = yo`
<tr class="${css.tr}">
<td class="${css.td}" data-shared="key_${opts.hash}"> contract address </td>
<td class="${css.td}" data-id="txLoggerTableContractAddress${opts.hash}" data-shared="pair_${opts.hash}">${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}" data-shared="key_${opts.hash}"> from </td>
<td class="${css.td}" data-id="txLoggerTableFrom${opts.hash}" data-shared="pair_${opts.hash}">${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}" data-shared="key_${opts.hash}"> to </td>
<td class="${css.td}" data-id="txLoggerTableTo${opts.hash}" data-shared="pair_${opts.hash}">${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}" data-shared="key_${opts.hash}"> gas </td>
<td class="${css.td}" data-id="txLoggerTableGas${opts.hash}" data-shared="pair_${opts.hash}">${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}" data-shared="key_${opts.hash}"> transaction cost </td>
<td class="${css.td}" data-id="txLoggerTableTransactionCost${opts.hash}" data-shared="pair_${opts.hash}">${opts.transactionCost} gas ${callWarning}
${copyToClipboard(() => opts.transactionCost)}
</td>
</tr>`)
}
if (opts.executionCost) {
table.appendChild(yo`
<tr class="${css.tr}">
<td class="${css.td}" data-shared="key_${opts.hash}"> execution cost </td>
<td class="${css.td}" data-id="txLoggerTableExecutionHash${opts.hash}" data-shared="pair_${opts.hash}">${opts.executionCost} gas ${callWarning}
${copyToClipboard(() => opts.executionCost)}
</td>
</tr>`)
}
var hash = yo`
<tr class="${css.tr}">
<td class="${css.td}" data-shared="key_${opts.hash}"> hash </td>
<td class="${css.td}" data-id="txLoggerTableHash${opts.hash}" data-shared="pair_${opts.hash}">${opts.hash}
${copyToClipboard(() => opts.hash)}
</td>
</tr>
`
if (opts.hash) table.appendChild(hash)
var input = yo`
<tr class="${css.tr}">
<td class="${css.td}" data-shared="key_${opts.hash}"> input </td>
<td class="${css.td}" data-id="txLoggerTableInput${opts.hash}" data-shared="pair_${opts.hash}">${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}" data-shared="key_${opts.hash}"> decoded input </td>
<td class="${css.td}" data-id="txLoggerTableDecodedInput${opts.hash}" data-shared="pair_${opts.hash}">${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}" data-shared="key_${opts.hash}"> decoded output </td>
<td class="${css.td}" id="decodedoutput" data-id="txLoggerTableDecodedOutput${opts.hash}" data-shared="pair_${opts.hash}">${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}" data-shared="key_${opts.hash}"> logs </td>
<td class="${css.td}" id="logs" data-id="txLoggerTableLogs${opts.hash}" data-shared="pair_${opts.hash}">
${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}" data-shared="key_${opts.hash}"> value </td>
<td class="${css.td}" data-id="txLoggerTableValue${opts.hash}" data-shared="pair_${opts.hash}">${val} wei
${copyToClipboard(() => `${val} wei`)}
</td>
</tr>
`
if (opts.val) table.appendChild(val)
return table
}