Merge pull request #886 from ethereum/ui-fixes

Fix RunTab contract instance close button and title
pull/3094/head
yann300 7 years ago committed by GitHub
commit 2afc385a77
  1. 87
      src/app/execution/txLogger.js
  2. 6
      src/app/panels/file-panel.js
  3. 58
      src/app/panels/terminal.js
  4. 10
      src/app/tabs/compile-tab.js
  5. 20
      src/app/tabs/run-tab.js
  6. 1
      src/app/tabs/support-tab.js
  7. 37
      src/app/ui/copy-to-clipboard.js
  8. 2
      src/app/ui/dropdown.js
  9. 52
      src/app/ui/tooltip.js
  10. 69
      src/universal-dapp.js
  11. 2
      test-browser/tests/compiling.js

@ -1,6 +1,6 @@
'use strict'
var yo = require('yo-yo')
const copy = require('clipboard-copy')
var copyToClipboard = require('../ui/copy-to-clipboard')
// -------------- styling ----------------------
var csjs = require('csjs-inject')
@ -31,8 +31,8 @@ var css = csjs`
.txTable, .tr, .td {
border-collapse: collapse;
font-size: 10px;
color: ${styles.terminal.text_Primary};
border: 1px solid ${styles.terminal.text_Primary};
color: ${styles.terminal.text_Secondary};
border: 1px solid ${styles.terminal.text_Secondary};
}
#txTable {
margin-top: 1%;
@ -41,6 +41,7 @@ var css = csjs`
}
.tr, .td {
padding: 4px;
vertical-align: baseline;
}
.tableTitle {
width: 25%;
@ -54,14 +55,6 @@ var css = csjs`
margin-left: 5px;
cursor: pointer;
}
.clipboardCopy {
margin-right: 0.5em;
cursor: pointer;
color: ${styles.terminal.icon_Color_CopyToClipboard};
}
.clipboardCopy:hover {
color: ${styles.terminal.icon_HoverColor_CopyToClipboard};
}
`
/**
* This just export a function that register to `newTransaction` and forward them to the logger.
@ -323,22 +316,26 @@ function createTable (opts) {
<tr class="${css.tr}">
<td class="${css.td}"> status </td>
<td class="${css.td}">${opts.status}${msg}</td>
</tr class="${css.tr}">`)
</tr>`)
}
var contractAddress = yo`
<tr class="${css.tr}">
<td class="${css.td}"> contractAddress </td>
<td class="${css.td}"><i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(opts.contractAddress) }} title='Copy to clipboard'></i>${opts.contractAddress}</td>
</tr class="${css.tr}">
<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}"><i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(opts.from) }} title='Copy to clipboard'></i>${opts.from}</td>
</tr class="${css.tr}">
<td class="${css.td}">${opts.from}
${copyToClipboard(() => opts.from)}
</td>
</tr>
`
if (opts.from) table.appendChild(from)
@ -352,16 +349,20 @@ function createTable (opts) {
var to = yo`
<tr class="${css.tr}">
<td class="${css.td}"> to </td>
<td class="${css.td}"><i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(data.to ? data.to : toHash) }} title='Copy to clipboard'></i>${toHash}</td>
</tr class="${css.tr}">
<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}"><i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(opts.gas) }} title='Copy to clipboard'></i>${opts.gas} gas</td>
</tr class="${css.tr}">
<td class="${css.td}">${opts.gas} gas
${copyToClipboard(() => opts.gas)}
</td>
</tr>
`
if (opts.gas) table.appendChild(gas)
@ -373,31 +374,39 @@ function createTable (opts) {
table.appendChild(yo`
<tr class="${css.tr}">
<td class="${css.td}"> transaction cost </td>
<td class="${css.td}"><i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(opts.transactionCost) }} title='Copy to clipboard'></i>${opts.transactionCost} gas ${callWarning}</td>
</tr class="${css.tr}">`)
<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}"><i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(opts.executionCost) }} title='Copy to clipboard'></i>${opts.executionCost} gas ${callWarning}</td>
</tr class="${css.tr}">`)
<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}"><i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(opts.hash) }} title='Copy to clipboard'></i>${opts.hash}</td>
</tr class="${css.tr}">
<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}"><i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(opts.input) }} title='Copy to clipboard'></i>${opts.input}</td>
</tr class="${css.tr}">
<td class="${css.td}">${opts.input}
${copyToClipboard(() => opts.input)}
</td>
</tr>
`
if (opts.input) table.appendChild(input)
@ -405,8 +414,10 @@ function createTable (opts) {
var inputDecoded = yo`
<tr class="${css.tr}">
<td class="${css.td}"> decoded input </td>
<td class="${css.td}"><i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(opts['decoded input']) }} title='Copy to clipboard'></i>${opts['decoded input']}</td>
</tr class="${css.tr}">`
<td class="${css.td}">${opts['decoded input']}
${copyToClipboard(opts['decoded input'])}
</td>
</tr>`
table.appendChild(inputDecoded)
}
@ -415,7 +426,7 @@ function createTable (opts) {
<tr class="${css.tr}">
<td class="${css.td}"> decoded output </td>
<td class="${css.td}" id="decodedoutput" >${opts['decoded output']}</td>
</tr class="${css.tr}">`
</tr>`
table.appendChild(outputDecoded)
}
@ -427,9 +438,11 @@ function createTable (opts) {
<tr class="${css.tr}">
<td class="${css.td}"> logs </td>
<td class="${css.td}" id="logs">
<i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(JSON.stringify(stringified, null, '\t')) }} title='Copy Logs to clipboard'></i>
<i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(JSON.stringify(opts.logs.raw || '0')) }} title='Copy Raw Logs to clipboard'></i>${JSON.stringify(stringified, null, '\t')}</td>
</tr class="${css.tr}">
${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)
@ -437,8 +450,10 @@ function createTable (opts) {
val = yo`
<tr class="${css.tr}">
<td class="${css.td}"> value </td>
<td class="${css.td}"><i class="fa fa-clipboard ${css.clipboardCopy}" aria-hidden="true" onclick=${function () { copy(`${val} wei`) }} title='Copy to clipboard'></i>${val} wei</td>
</tr class="${css.tr}">
<td class="${css.td}">${val} wei
${copyToClipboard(() => `${val} wei`)}
</td>
</tr>
`
if (opts.val) table.appendChild(val)

@ -32,9 +32,11 @@ var css = csjs`
width : 100%;
}
.menu {
height : 2em;
margin-top : 0.5em;
margin-top : -0.2em;
flex-shrink : 0;
display : flex;
flex-direction : row;
min-width : 160px;
}
.newFile {
padding : 10px;

@ -43,26 +43,26 @@ var css = csjs`
width : 100%;
padding : 5px;
}
.toggleTerminal {
margin-left : auto;
.clear {
margin-left : 10px;
margin-right : 10px;
width : 10px;
cursor : pointer;
color : ${styles.terminal.icon_Color_TogglePanel};
font-size : 14px;
font-weight : bold;
}
.toggleTerminal:hover {
color : ${styles.terminal.icon_HoverColor_TogglePanel};
.clear:hover {
color : ${styles.terminal.icon_HoverColor_Menu};
}
.clear {
margin-right : 5px;
.toggleTerminal {
margin-right : 10px;
font-size : 14px;
font-weight : bold;
cursor : pointer;
color : ${styles.terminal.icon_Color_Menu};
}
.clear:hover {
color : ${styles.terminal.icon_HoverColor_Menu};
.toggleTerminal:hover {
color : ${styles.terminal.icon_HoverColor_TogglePanel};
}
.terminal {
background-color : ${styles.terminal.backgroundColor_Terminal};
display : flex;
@ -113,12 +113,36 @@ var css = csjs`
word-break : break-all;
outline : none;
font-family : monospace;
font-family: FontAwesome;
}
.search {
display: flex;
align-items: center;
margin-right: 10px;
}
.filter {
${styles.terminal.input_Search_MenuBar}
width : 150px;
width : 150px;
padding-right : 0px;
margin-right : 0px;
border-top-left-radius : 0px;
border-bottom-left-radius : 0px;
}
.searchIcon {
background-color : ${styles.colors.veryLightGrey};
color : ${styles.terminal.icon_Color_Menu};
height : 25px;
width : 25px;
border-top-left-radius : 3px;
border-bottom-left-radius : 3px;
display : flex;
align-items : center;
justify-content : center;
}
.listen {
min-width : 120px;
display : flex;
}
.dragbarHorizontal {
position : absolute;
top : 0;
@ -226,13 +250,13 @@ class Terminal {
<div class=${css.bar}>
${self._view.dragbar}
<div class=${css.menu}>
${self._view.icon}
<div class=${css.clear} onclick=${clear}>
<i class="fa fa-ban" aria-hidden="true" onmouseenter=${hover} onmouseleave=${hover}></i>
<i class="fa fa-ban" aria-hidden="true" onmouseenter=${hover} onmouseleave=${hover}></i>
</div>
${self._view.dropdown}
<input type="text" class=${css.filter} onkeyup=${filter}>
<input onchange=${listenOnNetwork} type="checkbox"><label title="If checked Remix will listen on all transactions mined in the current environment and not only transactions created from the GUI">Listen on network</label>
${self._view.icon}
<div class=${css.search}><i class="fa fa-search ${css.searchIcon}" aria-hidden="true"></i><input type="text" class=${css.filter} onkeydown=${filter} placeholder="Search transactions"></div>
<div class=${css.listen}><input onchange=${listenOnNetwork} type="checkbox"><label title="If checked Remix will listen on all transactions mined in the current environment and not only transactions created from the GUI">Listen on network</label></div>
</div>
</div>
`

@ -2,13 +2,13 @@
var $ = require('jquery')
var yo = require('yo-yo')
const copy = require('clipboard-copy')
var parseContracts = require('../contract/contractParser')
var publishOnSwarm = require('../contract/publishOnSwarm')
var modalDialog = require('../ui/modaldialog')
var modalDialogCustom = require('../ui/modal-dialog-custom')
var TreeView = require('remix-debugger').ui.TreeView
var copyToClipboard = require('../ui/copy-to-clipboard')
// -------------- styling ----------------------
var csjs = require('csjs-inject')
@ -96,6 +96,7 @@ var css = csjs`
display: flex;
flex-direction: column;
margin-bottom: 5%;
overflow: visible;
}
.key {
margin-right: 5px;
@ -108,13 +109,11 @@ var css = csjs`
width: 100%;
margin-top: 1.5%;
}
.copyDetails,
.questionMark {
margin-left: 2%;
cursor: pointer;
color: ${styles.rightPanel.icon_Color_TogglePanel};
}
.copyDetails:hover,
.questionMark:hover {
color: ${styles.rightPanel.icon_HoverColor_TogglePanel};
}
@ -357,12 +356,15 @@ function compileTab (container, appAPI, appEvents, opts) {
function details () {
var select = el.querySelector('select')
if (select.children.length > 0 && select.selectedIndex >= 0) {
var contractName = select.children[select.selectedIndex].innerHTML
var contractProperties = contractsDetails[contractName]
var log = yo`<div class="${css.detailsJSON}"></div>`
Object.keys(contractProperties).map(propertyName => {
var copyDetails = yo`<span class="${css.copyDetails}"><i title="Copy value to clipboard" class="fa fa-clipboard" onclick=${() => { copy(contractProperties[propertyName]) }} aria-hidden="true"></i></span>`
var copyDetails = yo`<span class="${css.copyDetails}">
${copyToClipboard(() => contractProperties[propertyName])}
</span>`
var questionMark = yo`<span class="${css.questionMark}"><i title="${detailsHelpSection()[propertyName]}" class="fa fa-question-circle" aria-hidden="true"></i></span>`
log.appendChild(yo`
<div class=${css.log}>

@ -7,7 +7,7 @@ var txFormat = require('../execution/txFormat')
var txHelper = require('../execution/txHelper')
var modalDialogCustom = require('../ui/modal-dialog-custom')
var executionContext = require('../../execution-context')
const copy = require('clipboard-copy')
var copyToClipboard = require('../ui/copy-to-clipboard')
// -------------- styling ----------------------
var csjs = require('csjs-inject')
@ -29,6 +29,7 @@ var css = csjs`
.crow {
margin-top: .5em;
display: flex;
align-items: center;
}
.col1 {
width: 30%;
@ -60,17 +61,7 @@ var css = csjs`
font-weight: normal;
min-width: 150px;
}
.copyaddress {
margin-left: 0.5em;
margin-top: 0.7em;
cursor: pointer;
color: ${styles.rightPanel.runTab.icon_Color_Instance_CopyToClipboard};
}
.copyaddress:hover {
color: ${styles.rightPanel.runTab.icon_HoverColor_Instance_CopyToClipboard};
}
.instanceContainer {
${styles.rightPanel.runTab.box_Instance}
display: flex;
flex-direction: column;
margin-top: 2%;
@ -374,11 +365,6 @@ function contractDropdown (appAPI, appEvents, instanceContainer) {
section SETTINGS: Environment, Account, Gas, Value
------------------------------------------------ */
function settings (appAPI, appEvents) {
// COPY ADDRESS
function copyAddress () {
copy(document.querySelector('#runTabView #txorigin').value)
}
// SETTINGS HTML
var el = yo`
<div class="${css.settings}">
@ -411,7 +397,7 @@ function settings (appAPI, appEvents) {
<div class="${css.crow}">
<div class="${css.col1_1}">Account</div>
<select name="txorigin" class="${css.select}" id="txorigin"></select>
<i title="Copy Address" class="copytxorigin fa fa-clipboard ${css.copyaddress}" onclick=${copyAddress} aria-hidden="true"></i>
${copyToClipboard(() => document.querySelector('#runTabView #txorigin').value)}
</div>
<div class="${css.crow}">
<div class="${css.col1_1}">Gas limit</div>

@ -50,6 +50,7 @@ var css = csjs`
height: 100%;
transform: scale(0.9);
padding: 0;
border: none;
}
.infoBox {
${styles.rightPanel.supportTab.box_SupportInfo}

@ -0,0 +1,37 @@
var yo = require('yo-yo')
// -------------- copyToClipboard ----------------------
const copy = require('clipboard-copy')
var addTooltip = require('./tooltip')
// -------------- styling ----------------------
var csjs = require('csjs-inject')
var remixLib = require('remix-lib')
var styleGuide = remixLib.ui.styleGuide
var styles = styleGuide()
var css = csjs`
.copyIcon {
margin-left: 5px;
cursor: pointer;
}
`
module.exports = function copyToClipboard (getContent) {
var copyIcon = yo`<i title="Copy value to clipboard" class="${css.copyIcon} fa fa-clipboard" aria-hidden="true"></i>`
copyIcon.style.color = styles.remix.icon_Color_CopyToClipboard
copyIcon.onmouseenter = function (event) { copyIcon.style.color = styles.remix.icon_HoverColor_CopyToClipboard }
copyIcon.onmouseleave = function (event) { copyIcon.style.color = styles.remix.icon_Color_CopyToClipboard }
copyIcon.onclick = (event) => {
event.stopPropagation()
var copiableContent = getContent()
if (copiableContent) { // module `copy` keeps last copied thing in the memory, so don't show tooltip if nothing is copied, because nothing was added to memory
try {
if (typeof copiableContent !== 'string') {
copiableContent = JSON.stringify(copiableContent, null, '\t')
}
} catch (e) {}
copy(copiableContent)
addTooltip(event)
}
}
return copyIcon
}

@ -13,7 +13,7 @@ var css = csjs`
position : relative;
display : flex;
flex-direction : column;
margin-right : 3px;
margin-right : 10px;
}
.selectbox {
display : flex;

@ -0,0 +1,52 @@
var yo = require('yo-yo')
// -------------- styling ----------------------
var csjs = require('csjs-inject')
var remixLib = require('remix-lib')
var styleGuide = remixLib.ui.styleGuide
var styles = styleGuide()
var css = csjs`
.tooltip {
visibility: hidden;
width: 100px;
background-color: ${styles.remix.tooltip_CopyToClipboard_BackgroundColor};
color: ${styles.remix.tooltip_CopyToClipboard_Color};
font-weight: bold;
font-size: 12px;
text-transform: initial;
text-align: center;
border-radius: 3px;
padding: 5px 0;
position: absolute;
z-index: 1;
bottom: 160%;
left: 100%;
margin-left: -60px;
opacity: 0;
transition: opacity 1s;
}
.tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: ${styles.remix.tooltip_CopyToClipboard_BackgroundColor} transparent transparent transparent;
}
`
module.exports = function addTooltip (event) {
var icon = event.target
icon.style.position = 'relative'
var tooltip = yo`<div class=${css.tooltip}>Copied!</div>`
icon.appendChild(tooltip)
var copiedToolbox = event.target.children[0]
copiedToolbox.style.visibility = 'visible'
copiedToolbox.style.opacity = 1
setTimeout(function () {
copiedToolbox.style.visibility = 'hidden'
copiedToolbox.style.opacity = 0
}, 1000)
}

@ -15,9 +15,7 @@ var txHelper = require('./app/execution/txHelper')
var txExecution = require('./app/execution/txExecution')
var helper = require('./lib/helper')
var executionContext = require('./execution-context')
// copy to copyToClipboard
const copy = require('clipboard-copy')
var copyToClipboard = require('./app/ui/copy-to-clipboard')
// -------------- styling ----------------------
var csjs = require('csjs-inject')
@ -32,14 +30,17 @@ var css = csjs`
}
.title {
${styles.rightPanel.runTab.dropdown_RunTab}
margin-top: 5px;
display: flex;
justify-content: space-between;
justify-content: end;
align-items: center;
font-size: 11px;
min-width: 350px;
height: 30px;
min-width: 100%;
overflow: hidden;
word-break: break-word;
line-height: initial;
overflow: visible;
}
.titleLine {
display: flex;
@ -52,7 +53,7 @@ var css = csjs`
}
.instance {
${styles.rightPanel.runTab.box_Instance}
margin-bottom: 2px;
margin-bottom: 10px;
padding: 10px 15px 6px 15px;
}
.instance .title:before {
@ -66,16 +67,11 @@ var css = csjs`
.instance.hidesub > * {
display: none;
}
.instance.hidesub .titleLine {
.instance.hidesub .title {
display: flex;
}
.copy {
cursor: pointer;
margin-left: 3%;
color: ${styles.rightPanel.runTab.icon_Color_Instance_CopyToClipboard};
}
.copy:hover{
color: ${styles.rightPanel.runTab.icon_HoverColor_Instance_CopyToClipboard};
.instance.hidesub .udappClose {
display: flex;
}
.buttonsContainer {
margin-top: 2%;
@ -87,14 +83,12 @@ var css = csjs`
}
.instanceButton {}
.closeIcon {
font-size: 10px;
position: relative;
top: -5px;
right: -2px;
font-size: 12px;
cursor: pointer;
}
.udappClose {
margin-left: 3%;
align-self: center;
display: flex;
justify-content: flex-end;
}
.contractProperty {
overflow: auto;
@ -292,42 +286,35 @@ UniversalDApp.prototype.getBalance = function (address, cb) {
UniversalDApp.prototype.renderInstance = function (contract, address, contractName) {
var self = this
function remove () { $instance.remove() }
var $instance = $(`<div class="instance ${css.instance}"/>`)
function remove () { instance.remove() }
var instance = yo`<div class="instance ${css.instance}"></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.titleLine}>
<div class="${css.title}" onclick=${toggleClass}>
<div class="${css.titleText}"> ${contractName} at ${shortAddress} (${context}) </div>
</div>
<i class="fa fa-clipboard ${css.copy}" aria-hidden="true" onclick=${copyToClipboard} title='Copy to clipboard'></i>
</div>
`
var title = yo`<div class="${css.title}" onclick=${toggleClass}>
<div class="${css.titleText}"> ${contractName} at ${shortAddress} (${context}) </div>
${copyToClipboard(() => address)}
</div>`
if (self.removable_instances) {
var close = yo`<div class="${css.udappClose}" onclick=${remove}><i class="${css.closeIcon} fa fa-close" aria-hidden="true"></i></div>`
title.querySelector(`.${css.title}`).appendChild(close)
instance.append(close)
}
function toggleClass () {
$instance.toggleClass(`${css.hidesub}`)
}
function copyToClipboard (event) {
event.stopPropagation()
copy(address)
$(instance).toggleClass(`${css.hidesub}`)
}
var abi = txHelper.sortAbiFunction(contract)
$instance.get(0).appendChild(title)
instance.appendChild(title)
// Add the fallback function
var fallback = txHelper.getFallbackInterface(abi)
if (fallback) {
$instance.append(this.getCallButton({
instance.appendChild(this.getCallButton({
funABI: fallback,
address: address,
contractAbi: abi,
@ -340,7 +327,7 @@ UniversalDApp.prototype.renderInstance = function (contract, address, contractNa
return
}
// @todo getData cannot be used with overloaded functions
$instance.append(this.getCallButton({
instance.appendChild(this.getCallButton({
funABI: funABI,
address: address,
contractAbi: abi,
@ -348,7 +335,7 @@ UniversalDApp.prototype.renderInstance = function (contract, address, contractNa
}))
})
return $instance.get(0)
return instance
}
// TODO this is used by renderInstance when a new instance is displayed.

@ -35,6 +35,8 @@ function testSimpleContract (browser, callback) {
browser.click('.runView')
.click('#runTabView div[class^="create"]')
.pause(500)
.click('#runTabView .instance div[class^="title"]')
.click('#runTabView .instance div[class^="title"]')
.testFunction('f - transact (not payable)',
'0xa178c603400a184ce5fedbcfab392d9b77822f6ffa7facdec693aded214523bc',
'[vm] from:0xca3...a733c, to:TestContract.f() 0x692...77b3a, value:0 wei, data:0x261...21ff0, 0 logs, hash:0xa17...523bc', null,

Loading…
Cancel
Save